- 教程
- 文档
- 人工智能
【免费下载链接】mcp-for-beginners
This open-source curriculum introduces the fundamentals of Model Context Protocol (MCP) through real-world, cross-language examples in .NET, Java, TypeScript, JavaScript, Rust and Python. Designed for developers, it focuses on practical techniques for building modular, scalable, and secure AI workflows from session setup to service orchestration.
本篇技术指南围绕 mcp-for-beginners 开源课程第 15 章(MCP Apps)的 TypeScript 作业展开,完整讲解「石头剪刀布(Rock Paper Scissors)」解决方案的三层代码结构:server.ts中的工具与组件资源注册、mcp-app.ts中的事件绑定与工具调用、mcp-app.html中的交互式 UI。读完本文,你将掌握如何用@modelcontextprotocol/ext-apps与@modelcontextprotocol/sdk搭建一个同时返回数据与可渲染 UI 的 MCP App,并能在 Visual Studio Code 或独立 Host 中启动、联调与验证它。
MCP Apps:让工具返回「可交互的组件」而不是裸数据
在进入作业代码之前,需要先理解作业背后的范式。正如 15-mcp-apps 课程主文档 所阐述的:MCP Apps 是 MCP 标准中的新范式,它不再要求工具调用只返回数据,而是允许服务器同时声明「这份数据应该如何被用户交互」。也就是说,工具结果可以携带 UI 信息——一个自包含的组件,从数据到用户界面一应俱全,避免了开发者自行编写和长期维护前置前端页面的开销。
实现一个 MCP App 需要两部分彼此关联的注册:
- 工具(Tool):负责接收参数、执行逻辑并返回数据;
- 组件资源(App Resource):负责提供打包后的 HTML/JavaScript,即可渲染的 UI。
两者通过同一个resourceUri连接起来。整个 MCP Apps 的运行模型可以概括为:Host 应用把 MCP App 的 UI 注入到 IFrame 容器中,IFrame 内的事件处理器通过向父页面发送消息来调用服务器工具,再把工具结果数据渲染回界面(详见主文档中的 Mermaid 流程图)。
本作业正是对上述范式的直接练习:作业要求实现一个石头剪刀布游戏,UI 部分需要一个下拉列表、一个提交按钮和一个显示「谁出了什么、谁赢了」的标签;服务端部分需要一个以choice为输入、随机生成电脑选择并判定胜负的工具。对应的解决方案文档即 assignment/typescript/README.md。
解决方案的整体结构
作业的官方解答刻意只保留了「真正关键」的代码:UI 标记(markup)、事件绑定(event wire up)和服务端功能(server features),完整的构建配置、依赖声明与启动入口则在课程code目录中提供。
my-app server.ts -- 服务端功能(工具 + 组件资源注册) src mcp-app.ts -- UI 与事件绑定逻辑 mcp-app.html -- UI 标记对照仓库中的实际文件,作业解答存放于 assignment/typescript/my-app:
| 文件 | 职责 | 仓库路径 |
|---|---|---|
server.ts | 注册play-rps工具与ui://get-time/mcp-app.html资源,并读取打包后的 HTML | server.ts |
src/mcp-app.ts | 创建App实例、绑定下拉框与按钮事件、通过callServerTool调用后端工具 | mcp-app.ts |
mcp-app.html | 定义石头剪刀布的下拉列表、提交按钮与结果展示区 | mcp-app.html |
服务端:registerAppTool注册游戏工具
服务端核心是 assignment 版 server.ts。它先创建一个McpServer实例,然后用@modelcontextprotocol/ext-apps/server提供的registerAppTool注册名为play-rps的工具:
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import zod from "zod"; const resourceUri = "ui://get-time/mcp-app.html"; registerAppTool( server, "play-rps", { title: "Play Rock-Paper-Scissors", description: "Play a game of rock-paper-scissors with the server.", inputSchema: zod.object({ choice: zod.enum(["rock", "paper", "scissors"]), }), _meta: { ui: { resourceUri } }, // 将工具与其 UI 资源关联 }, async ({ choice }) => { const options = ["rock", "paper", "scissors"] as const; const serverChoice = options[Math.floor(Math.random() * options.length)]; let result: string; if (choice === serverChoice) { result = `It's a tie! We both chose ${choice}.`; } else if ( (choice === "rock" && serverChoice === "scissors") || (choice === "paper" && serverChoice === "rock") || (choice === "scissors" && serverChoice === "paper") ) { result = `You win! You chose ${choice} and I chose ${serverChoice}.`; } else { result = `I win! You chose ${choice} and I chose ${serverChoice}.`; } return { content: [{ type: "text", text: result }], }; }, );值得注意的三个设计点:
- 输入校验:
inputSchema使用 zod 的zod.enum(["rock", "paper", "scissors"])限定choice只能取三种合法值,从协议层杜绝非法输入。这与课程主文档中 FAQ 示例的zod.string().default("shipping")一脉相承——需要接受用户输入的工具都应声明inputSchema。 - UI 关联元数据:
_meta: { ui: { resourceUri } }是 MCP Apps 的关键连线——Host 调用该工具时,会读取_meta.ui.resourceUri得知应该获取并渲染哪个资源作为交互式 UI。 - 游戏判定逻辑:服务端随机生成电脑选择,按「平局—玩家赢—服务器赢」的顺序判定,并把结果以标准 MCP
content: [{ type: "text", text: result }]结构返回。
服务端:registerAppResource注册组件资源
工具只负责「算」,UI 则来自组件资源。同一文件的第 64-86 行 用registerAppResource把打包产物注册为资源:
registerAppResource( server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => { const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8"); return { contents: [ { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html, _meta: { ui: {} }, }, ], }; }, );关键点:
DIST_DIR = path.join(import.meta.dirname, "dist"),即读取 Vite 构建产物目录中的mcp-app.html;- 资源回调返回
contents数组,其中text字段携带完整的 HTML 字符串,mimeType使用RESOURCE_MIME_TYPE; - 注意工具与资源共用同一个
resourceUri常量——这正是文档强调的「组件与工具通过resourceUri连接」的实现证据。
如果继续阅读课程code目录下的完整版 server.ts,可以看到 FAQ 工具的完整形态:faq键值对数据、带zod.object({ query: zod.string().default("shipping") })的inputSchema,以及registerAppTool+registerAppResource成对注册的模式——作业解答是这套模式的精炼版。
前端 UI:mcp-app.html标记
作业的 UI 标记位于 mcp-app.html,满足作业全部三条 UI 要求:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Rock paper scissor</title> </head> <body> <div class="rock-paper-scissors"> <h1>Rock Paper Scissors</h1> <select id="rps-options" value="rock"> <option value="rock">Rock</option> <option value="paper">Paper</option> <option value="scissors">Scissors</option> </select> <button class="select" id="rps-button">Select</button> <p>Result: <code id="rps-result">...</code></p> </div> <script type="module" src="/src/mcp-app.ts"></script> </body> </html>对应作业验收点:<select id="rps-options">提供三个选项的下拉列表、<button id="rps-button">是提交选择的按钮、<code id="rps-result">是展示结果的标签。页面最后以模块方式引入/src/mcp-app.ts,即事件绑定脚本。
前端逻辑:src/mcp-app.ts的事件绑定与工具调用
assignment 版 mcp-app.ts 是整个交互的枢纽,它完整演示了「获取元素引用 → 创建 App → 处理工具结果 → 绑定事件 → 连接 Host」的标准流程:
import { App } from "@modelcontextprotocol/ext-apps"; // 获取元素引用 const serverTimeEl = document.getElementById("server-time")!; // rps const getRpsBtn = document.getElementById("rps-button")!; const rpsResponseEl = document.getElementById("rps-result")!; const rpsOptions = document.getElementById("rps-options") as HTMLSelectElement; // 创建 App 实例 const app = new App({ name: "Get Time App", version: "1.0.0" }); // 处理来自服务器的工具结果。必须在 `app.connect()` 之前设置, // 以免错过最初的工具结果。 app.ontoolresult = (result) => { const time = result.content?.find((c) => c.type === "text")?.text; serverTimeEl.textContent = time ?? "[ERROR]"; }; getRpsBtn.addEventListener("click", async () => { const userChoice = rpsOptions.value; const result = await app.callServerTool({ name: "play-rps", arguments: { choice: userChoice } }); const rpsResult = result.content?.find((c) => c.type === "text")?.text; rpsResponseEl.textContent = rpsResult ?? "[ERROR]"; }); // 连接到 Host app.connect();这段代码的核心机制:
new App({ name, version })来自@modelcontextprotocol/ext-apps包,封装了与父页面通信的协议细节;app.callServerTool({ name: "play-rps", arguments: { choice: userChoice } })是请求后端的关键调用——正如课程主文档所述,它实际上向父窗口发送消息,由父窗口(Host)转发并调用 MCP Server;- 从返回结果中取出
content里type === "text"的文本渲染到rps-result,没有结果时兜底显示[ERROR]; app.ontoolresult在connect()之前赋值,避免遗漏初始工具结果;app.connect()放在最后,完成与 Host 的连接握手。
如何运行与验证
作业文档给出的运行方式为:参考 code/typescript/README.md 的完整工程,再把作业三个文件的内容分别填入对应文件中。完整工程的运行步骤如下:
安装依赖并校验编译
# 进入 my-app 目录 npm install # 同时安装前端与后端依赖 npx tsc --noEmit # 校验后端编译,无输出即代表通过依赖清单可在 code 版 package.json 中看到:运行时依赖@modelcontextprotocol/ext-apps、@modelcontextprotocol/sdk、express、cors,开发依赖包括typescript、tsx、vite、vite-plugin-singlefile、concurrently、cross-env等,要求 Node.js >= 20。
启动后端
npm start该命令由concurrently并行执行两件事:cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch(开发模式增量打包 UI)和tsx watch main.ts(热重启 MCP 服务端)。启动后 MCP 端点位于http://localhost:3001/mcp。
注意:若在 Windows 上运行,
concurrently可能需要替换为等效工具;若在 Codespace 中,需要把端口可见性设为 public,并通过https://<Codespace 名称>.app.github.dev/mcp验证端点可达。
后端入口 main.ts 揭示了端点的实现方式:使用createMcpExpressApp创建 Express 应用,启用 CORS(允许GET/POST/OPTIONS,请求头含Content-Type、Authorization、MCP-Protocol-Version),并在/mcp路由上为每个请求创建一次McpServer实例与StreamableHTTPServerTransport(无会话 ID 的无状态模式)。配套的 vite.config.ts 使用vite-plugin-singlefile把 UI 打包为单个 HTML 文件,开发模式内联 sourcemap,这正是registerAppResource能直接读取dist/mcp-app.html的原因。
方案一:在 Visual Studio Code 中测试
在.vscode/mcp.json中注册服务器:
{ "servers": { "my-mcp-server-7178eca7": { "url": "http://localhost:3001/mcp", "type": "http" } }, "inputs": [] }点击mcp.json中的启动按钮,在聊天窗口输入工具名(课程示例为get-faq,作业对应play-rps),即可看到 MCP App 的 UI 渲染出来——这是目前测试 MCP Apps 最便捷的方式之一。
方案二:使用独立 Host 测试
- 克隆
ext-apps仓库后进入ext-apps目录,执行npm install; - 在另一个终端进入
ext-apps/examples/basic-host,执行npm start; - Host 会连接到后端并渲染 MCP App 界面。在 Codespace 环境中,需要修改
serve.ts中第 27 行的http://localhost:3001/mcp为你的 Codespace 后端地址(形如https://<name>-3001.app.github.dev/mcp)。
验证游戏功能
在渲染出的界面上从下拉框选择 Rock / Paper / Scissors,点击 Select 按钮,rps-result区域会显示类似「You win! You chose rock and I chose scissors.」的判定结果,同时可通过点击「Call Tool」按钮查看工具返回的原始数据。
从源码看 MCP Apps 的工作原理
结合本作业与课程完整版代码,可以提炼出 MCP Apps 的几条核心原理:
- 工具 + 资源成对注册,
resourceUri是唯一纽带:registerAppTool的_meta.ui.resourceUri与registerAppResource的第一个参数指向同一个 URI(如ui://get-time/mcp-app.html),Host 据此把工具调用和 UI 渲染绑定。 - UI 在 IFrame 中运行:出于安全考虑,MCP App 的 HTML 被注入 IFrame 容器,与父页面隔离。
- 通信走消息转发:IFrame 内的
app.callServerTool()不直接发起网络请求,而是向父窗口postMessage,由 Host 代为调用 MCP Server 并回传结果——这是@modelcontextprotocol/ext-apps等库封装的核心能力。 - 数据与 UI 一同交付:工具仍以标准 MCP 的
content结构返回文本数据,同时服务器附带可渲染的组件资源,这正是「让 MCP Server 对数据如何呈现也有发言权」的范式落点。
小结与关键收获
本作业是对 MCP Apps 范式的完整练习:服务端用registerAppTool+registerAppResource成对注册play-rps工具与其 UI 资源,前端用mcp-app.html定义交互界面、src/mcp-app.ts负责事件绑定与callServerTool调用,最终通过 VSCode 或独立 Host 验证游戏可玩。完成本作业后,你已具备在 TypeScript 中构建和集成自有 MCP App 的能力,可以继续深入 04-PracticalImplementation 章节,把这些组件化能力应用到更完整的实际实现中。
- 教程
- 文档
- 人工智能
【免费下载链接】mcp-for-beginners
This open-source curriculum introduces the fundamentals of Model Context Protocol (MCP) through real-world, cross-language examples in .NET, Java, TypeScript, JavaScript, Rust and Python. Designed for developers, it focuses on practical techniques for building modular, scalable, and secure AI workflows from session setup to service orchestration.
相关推荐
MCP Apps 实战作业:用 TypeScript 构建带 UI 的石头剪刀布(Rock-Paper-Scissors)MCP App
MCP Apps 实战作业:用 TypeScript 构建带 UI 的石头剪刀布(Rock Paper Scissors)MCP App 本篇文章基于 mcp
教程文档人工智能MCP Apps 实战:在 mcp-for-beginners 中用 TypeScript 构建带交互 UI 的石头剪刀布 MCP 应用
MCP Apps 实战:在 mcp for beginners 中用 TypeScript 构建带交互 UI 的石头剪刀布 MCP 应用 导读 本文围绕 mcp
教程文档人工智能在 TypeScript 中构建剪刀石头布 MCP App:registerAppTool 与 registerAppResource 实战
在 TypeScript 中构建剪刀石头布 MCP App:registerAppTool 与 registerAppResource 实战 MCP Apps
教程文档人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考