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 上最想让哪类工具长出界面?评论区见,有价值的我整理进后续篇目。


参考:

相关推荐
m0_587383003 小时前
深圳 24 小时自助健身房系统软件开发实战指南与案例解析
java·spring boot·小程序·架构·需求分析
Nebula_g3 小时前
JavaSE项目实践:红包雨-线程池/线程
java·开发语言
小羊没烦恼!3 小时前
jQuery1.5的改进细节
java·服务器·开发语言·前端·c#
霸道流氓气质3 小时前
Prompt 版本控制与 A/B 测试实战:语义版本管理、一致性哈希分流与 Thompson Sampling 的 Java 生产级实现
java·prompt·哈希算法
嵌入式er4 小时前
影石系统组 一二面OC面经
java·开发语言
春涧草茶4 小时前
慢就是快12-12手动抛出异常
java·linux·前端
企业数字化笔记5 小时前
AI工具参数很多怎么办?预设、表单校验、危险参数与配置审计
java·spring boot·python·音视频
huaweichenai5 小时前
spring boot对接阿里短信
java·spring boot
KING-WU5125 小时前
Linux 工具之Make 与 Makefile
java·linux·服务器
马剑威(威哥爱编程)6 小时前
【AI全栈后端12-08】Spring Boot 用 MCP 统一接入内部系统:让 AI 接一次,全公司复用
java·人工智能·spring boot