先给结论:MCP 服务端想给客户端一个可交互界面,实际只有三条路——返回结构化数据让客户端自己渲染、以资源形式返回一段界面内容由宿主在隔离环境里展示、或者只返回一个外部页面地址把界面交给自己的 Web 服务。选哪条取决于你要的交互复杂度,以及对宿主兼容性的要求。

新手容易以为协议里有一个"返回页面"的字段,找一圈没找到就卡住了。真实情况是协议提供的是工具、资源、提示三类能力,界面是建立在这些能力之上的组合,不同宿主的呈现能力差别很大。
本文把三种做法各自的实现方式、验证手段与适用边界讲清,并标注哪些属于协议明确支持的能力、哪些属于社区仍在演进的方向。涉及演进方向的部分请以官方文档为准。
先看结论
- 只要展示数据:返回结构化结果,兼容性最好,可交互界面由客户端决定。
- 要自定义界面:以资源形式返回界面内容,需要宿主支持隔离渲染。
- 交互非常复杂:返回一个外部页面地址,界面完全由自己的服务托管。
MCP 的基础接入方式记不清时,翻 Codex MCP 配置 里的步骤最快。
一、做法一:结构化数据 + 让客户端渲染
这是兼容性最好的做法。服务端的工具照常返回数据,同时给出一份描述,由客户端决定怎么呈现。
# server.py:工具返回结构化结果
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo")
@mcp.tool()
def list_orders(status: str = "all") -> dict:
"""按状态查询订单列表。"""
rows = [
{"id": "A001", "amount": 129, "status": "paid"},
{"id": "A002", "amount": 88, "status": "unpaid"},
]
picked = [r for r in rows if status == "all" or r["status"] == status]
return {"total": len(picked), "orders": picked}
这段做的是让工具返回一个字典而不是一段拼好的文本。结构化结果的优点是客户端可以按自己的组件渲染成表格、卡片或图表,缺点是你完全无法控制呈现样式。
适用边界要讲清:只要任务是"把数据给到对话里",这种做法就够用,而且在所有支持协议的宿主上都能工作。它不能做表单回填、多步向导这类需要用户输入的界面。
二、做法二:以资源形式返回界面内容
第二种做法是把界面当作资源内容返回(通常是 HTML 片段或指向界面资源的标识),由客户端在隔离沙箱里展示。
@mcp.resource("ui://order-table")
def order_table() -> str:
"""返回一个订单表格的界面片段。"""
return """
<table>
<tr><th>订单号</th><th>金额</th><th>状态</th></tr>
<tr><td>A001</td><td>129</td><td>已支付</td></tr>
</table>
"""
这段做的是用资源的形式返回一段界面片段。能否真的被渲染出来,取决于宿主是否实现了隔离渲染能力——这一点在不同客户端之间差异很大,不能默认支持。
安全边界必须讲清:这类界面内容应当放进受限的隔离环境里执行,不能让它访问宿主的页面上下文、Cookie 或本地存储。服务端返回的内容对用户来说是不可信输入,宿主有责任做隔离。

社区层面也存在专门做这件事的扩展方案,通常以"返回一段界面资源、由客户端渲染"的思路实现;同时协议方向本身也在往"应用式界面"演进。这些属于仍在推进的部分(编辑推断,具体支持范围以官方文档为准),落地前应当先在你目标宿主上实测一次。
# 验证服务端是否正常暴露工具与资源
npx -y @modelcontextprotocol/inspector python server.py
这段做的是用官方调试工具连接本地服务端,列出它暴露的工具与资源。预期结果(未在本机执行)是能在工具列表里看到刚才定义的函数,并能手动调用查看返回结构。这一步是判断"服务端写对了没有"的最快手段。
三、做法三:返回一个外部页面地址
第三种做法最灵活:服务端不返回界面本身,只返回一个地址,真正的界面由自己的 Web 服务提供。
@mcp.tool()
def open_dashboard(order_id: str) -> dict:
"""生成订单详情页的访问地址。"""
return {
"url": f"https://example.com/orders/{order_id}",
"hint": "在浏览器中打开以查看完整交互界面"
}
这段做的是让工具返回一个网址和一句说明。优点是完全不受宿主渲染能力限制,界面想多复杂都行;缺点是用户要离开当前对话环境,且你要自己处理登录态与权限。
判断标准是这样:想要的可交互界面越复杂、越依赖自有系统,越该用外部页面。让协议承担它不擅长的事,只会得到一堆兼容性问题。
四、常见失败分支与验证清单
4.1 客户端完全没有反应
先确认宿主是否支持该能力。用官方调试工具确认服务端暴露正常后,如果宿主仍无反应,多半是宿主这一侧没有实现对应的渲染,而不是服务端写错了。
4.2 资源能被读到但不渲染
多见于内容类型声明不正确,或者宿主只接受特定格式。修复方式是核对资源声明的内容类型,并查阅宿主的说明文档。
4.3 外部地址打不开或权限不对
地址要带上足够的权限凭据,同时不要把长期有效的凭据直接拼在地址里。修复方式是改用一次性令牌,并设置较短的有效期。
4.4 验证清单
- 检查:先用调试工具确认工具与资源都已正常暴露
- 运行:在目标宿主上触发一次,观察是有渲染、只出文本还是无反应
- 预期:结构化数据在任何宿主上都能展示,界面类能力取决于宿主支持
- 失败:调试工具正常但宿主无反应,属于宿主能力限制,不是服务端问题
- 修复:按兼容性要求降级到结构化数据,或改用外部页面方案
- 验证:换一个宿主重复一次,确认结论不是单一客户端的偶发行为
上面这些步骤做的是把"界面没出来"拆成服务端、协议、宿主三层分别定位。预期结果(未在本机执行)是能明确说出卡在哪一层;如果连调试工具都列不出工具,问题一定在服务端配置。

总结
MCP 返回可交互界面没有银弹,只有三条路:结构化数据最兼容、资源界面需要宿主配合、外部页面最自由但要求自建服务。按交互复杂度和兼容性要求选,不要指望一条路覆盖所有场景。
三条要点带走:
- 先分清问题是"服务端没暴露"还是"宿主不支持",用调试工具一测便知;
- 界面内容属于不可信输入,必须放进隔离环境,不能接触宿主上下文;
- 交互复杂且依赖自有系统时,直接返回外部页面地址更省心。
下一步建议先用结构化数据把功能跑通,再按目标宿主的实测结果决定是否上界面方案。接入细节可以对照 CodeBuddy IDE 配置 MCP 再确认一遍。
常见问题
Q:MCP 能不能直接返回一个网页?
A:协议本身提供的是工具、资源、提示三类能力,界面要靠这些能力组合实现。常见做法是返回界面资源或返回外部页面地址,能否渲染取决于宿主支持。
Q:为什么服务端写好了客户端没反应?
A:先用官方调试工具确认工具与资源是否正常暴露。调试工具正常而宿主无反应,说明是宿主没有实现对应能力。
Q:返回的界面内容有什么安全风险?
A:界面内容应当放进隔离环境执行,不能访问宿主页面的上下文、Cookie 或本地存储。宿主有责任做这层隔离。
Q:外部页面方案有什么代价?
A:用户要离开当前对话环境,且你要自己处理登录态与权限。不要把长期有效的凭据直接拼进地址,改用一次性短令牌。
Q:怎么快速判断服务端写对了没有?
A:用官方调试工具连接本地服务端,看工具与资源列表里是否出现你定义的项,并手动调用一次查看返回结构。
延伸学习
想把 MCP 这块做扎实,可以按这个顺序来:
- 想补一遍 AI 开发实践,AI 开发实操课程 可以跟一遍;
- 读一篇相关笔记 通俗易懂一文讲透什么是 MCP,把协议基础理清;
- 想随手验证一段脚本,用 在线代码实例 跑一遍最快。

TRAE-AI编程



