MCP实战手记系列(五):让工具返回一个能点的界面(文末附github源码链接)

MCP 实战手记系列(五)· 代码实操

系列(三)把服务改成无状态之后,工具返回的还是一串 JSON------给模型看刚好,给人看不行。用户在对话里想看一眼设备状态、想点一下开关,只能盯着文本数字符。MCP Apps 补的就是这一公里。这篇在 Spring AI 上把它跑通,代码在 tag v05

引子:JSON 是给模型看的,不是给人看的

以机房面板为例:模型读得懂 {"ac-01":true,"fan-02":false},用户看到的是一行天书------他想要的是三张卡片,点一下把风扇打开。

以前只有两条路:要么让模型吐一段 HTML(不安全、没法复用),要么自己再写一个前端(跟 MCP 就没关系了)。2026 年 1 月,官方把第三条路收成了扩展:MCP Apps(SEP-1865),现在是稳定版规范。

一、MCP Apps 是什么

拆开只有三个角色:界面资源 (一份 HTML,注册在 ui:// 协议下)、工具 (声明"我的结果有配套界面")、宿主(取资源、塞进沙箱 iframe、代理界面的调用)。前两个在服务端,第三个在聊天客户端。

关键是模板与数据分离ui:// 那份 HTML 是模板,动态数据走工具结果灌进去。它不是"让模型临时写一段 HTML",所以能缓存、能预加载、能复用。

界面和宿主之间走 postMessage 上的 MCP 风格 JSON-RPC :界面能拿到工具结果,也能反过来请求宿主调工具,但放不放行由宿主决定

二、绑定写在工具元数据里,不在结果里

这是最容易理解错的一处。绑定是静态声明 ,宿主在 tools/list 阶段就能读到,不用等调用完再猜:

json 复制代码
{
  "name": "get_room_dashboard",
  "_meta": { "ui": { "resourceUri": "ui://room-dashboard", "visibility": ["model"] } }
}

visibility 是两个可见范围:model 表示模型能当普通工具调,app 表示只留在宿主侧给界面用。我给控制设备的工具设的是 ["app"]------点按钮能关设备,但模型不能直接关

三、动手:三段代码

代码在 com.ethanliang.mcp.apps,tag v05。沿用系列(三)的无状态配置。

① 界面资源:一个方法返回 HTML,mimeType 是宿主识别「这是个 MCP App」的依据。

java 复制代码
@McpResource(uri = "ui://room-dashboard", name = "room-dashboard",
             mimeType = "text/html;profile=mcp-app")
public String roomDashboardUi() { return DashboardHtml.TEMPLATE; }

② 工具绑定

java 复制代码
@McpTool(name = "get_room_dashboard", description = "获取机房仪表盘数据...",
         generateOutputSchema = true,
         metaProvider = DashboardUiMetaProvider.class,   // ← 界面绑定,关键就这一行
         annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false))
public RoomDashboard getRoomDashboard() { ... }

顺手提醒:MCP 默认 destructiveHint=true,只读查询不改掉的话,宿主会把它当破坏性操作。

③ MetaProvider (实现 MetaProvider,注册成 @Component 即可):

java 复制代码
@Override
public Map<String, Object> getMeta() {
    return Map.of("ui", Map.of("resourceUri", "ui://room-dashboard",
                               "visibility", List.of("model")));
}

四、怎么验证真的生效

起服务(端口 8085)后三条 curl 就能验完,不需要真实宿主:

bash 复制代码
curl -sS localhost:8085/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
  • tools/list → 工具带 _meta.ui.resourceUri
  • resources/list → 返回 {"uri":"ui://room-dashboard","mimeType":"text/html;profile=mcp-app"}
  • tools/call → 同时给 structuredContent(界面用)和 content[0].text(不支持的宿主用)

第二条最关键:mimeType 写错,宿主就不认这是界面,只会当普通文本资源,界面永远出不来。

五、踩坑记录

@Tool 挂不上 _meta Spring AI 的 ToolDefinition 只有 name / description / inputSchema,没有 meta@Tool + MethodToolCallbackProvider 这条路声明不了界面,必须走 @McpTool(metaProvider = ...)

② 启动日志里那句 WARN 是误导。 会打 No resource methods found in the provided resource objects,但 resources/list 照样把资源返回了。以协议返回为准,别被日志骗去改代码。

③ 界面必须自包含。 宿主在 deny-by-default 的 CSP 下渲染,外链 CDN 脚本一律被拦,界面直接白板。CSS/JS 全内联,要用第三方库就打进同一份 HTML;确实要连外部域,得在资源的 _meta.ui.csp 里显式声明------宿主只会收紧,不会放宽。

④ 文本回退不能省。 structuredContent 喂界面,content[0].text 喂不支持的宿主。关键结论不能只藏在界面里------可访问性、日志审计、自动化测试都还指着文本这条链。

⑤ visibility 只是声明。 visibility: ["app"] 说的是"模型别调",宿主不执行就等于没设。渲染权限和业务权限得分开管,不能因为界面上多了个按钮就自动放行高风险操作。

⑥ Windows 下 curl 传 JSON 会被引号吃掉 ,服务端只报 Failed to deserialize message: Failed to read value。把 body 写进文件、用 --data-binary @file 更稳。

六、还没解决什么

Java 侧只负责声明资源和返回 HTML,渲染完全在宿主------Claude、VS Code Copilot、Microsoft 365 Copilot、Postman 这些都已支持。没有支持 MCP Apps 的宿主,这篇的代码跑得起来,但界面看不见------协议层能验,视觉效果得靠宿主

信任边界也要想清楚:自有可控的工具适合 MCP Apps;不可信的远程智能体更适合 A2UI 那种不执行外部代码的路线,选错后面补安全成本高得多。

小结

  1. 绑定在工具元数据_meta.ui.resourceUri 指向 ui:// 资源,宿主发现工具时就知道有界面
  2. Spring AI 要走 @McpTool + metaProvider@Tool 那条路没有 meta 入口
  3. 界面自包含、文本留回退,漏一条界面就是白板或黑盒

完整可运行代码:github.com/ethanliang2016/mcp-in-action(tag: v05),目录 05-mcp-apps

MCP 实战手记系列路线图

# 篇目 状态
1 总纲篇:MCP 到哪一步了
2 跑通第一个 MCP Server
3 把 MCP Server 改成无状态
4 CIMD 授权实战
5 让工具返回一个能点的界面(本篇)
6 自建 MCP 网关 规划
7 MCP 安全接入检查清单 规划

关注我,更新第一时间看到。你在 MCP 上最想让哪类工具长出界面?评论区见,有价值的我整理进后续篇目。


参考:

相关推荐
APIshop2 小时前
淘宝选品接口实战:从召回、详情、佣金到转链的 Java 接入笔记
java
泡茶喝茶写代码2 小时前
量化数据进阶:多维实战篇(第 11 篇):历史涨跌停价:涨跌停序列与止损线测算
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据api
景熙55232 小时前
单体项目编写思路和落地形式
java·开发语言
Ivanqhz2 小时前
Slope One 算法详解:与矩阵分解、共现矩阵的异同
java·服务器·网络·人工智能·深度学习
咖啡星人k3 小时前
给 AI 接工具的权限设计:账号、Key、参数三层
安全·mcp·权限设计
Bs_MoneyMagnet3 小时前
基于springboot+vue的非遗物质文化遗产系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计
万物智能3 小时前
波形发生AD9833模块配置—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
java
xrlfreedom3 小时前
大厂 MCP 面试实录:高风险 Tool 调用的 OAuth 2.1 防护方案设计
mcp·oauth 2.1·python mcp sdk
计算机毕设定制辅导-无忧学长3 小时前
《基于SpringBoot的未成年人健康知识科普平台的设计与实现》
java·开发语言·vue.js·spring boot·未成年人健康知识科普平台