MCP 怎么返回可交互界面:3 种做法与各自边界

编程狮(w3cschool.cn) 2026-10-23 07:06:42 浏览数 (30)
反馈

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

MCP 怎么返回可交互界面?

新手容易以为协议里有一个"返回页面"的字段,找一圈没找到就卡住了。真实情况是协议提供的是工具、资源、提示三类能力,界面是建立在这些能力之上的组合,不同宿主的呈现能力差别很大。

本文把三种做法各自的实现方式、验证手段与适用边界讲清,并标注哪些属于协议明确支持的能力、哪些属于社区仍在演进的方向。涉及演进方向的部分请以官方文档为准。

先看结论

  • 只要展示数据:返回结构化结果,兼容性最好,可交互界面由客户端决定。
  • 要自定义界面:以资源形式返回界面内容,需要宿主支持隔离渲染。
  • 交互非常复杂:返回一个外部页面地址,界面完全由自己的服务托管。

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 这块做扎实,可以按这个顺序来:

  1. 想补一遍 AI 开发实践,AI 开发实操课程 可以跟一遍;
  2. 读一篇相关笔记 通俗易懂一文讲透什么是 MCP,把协议基础理清;
  3. 想随手验证一段脚本,用 在线代码实例 跑一遍最快。

0 人点赞