资讯动态

MCP Apps 实战:在 mcp-for-beginners 中用 TypeScript 构建带交互 UI 的石头剪刀布 MCP 应用

发布时间:2026/10/2 18:28:39 来源:尧图企业网站定制
教程文档人工智能【免费下载链接】mcp-for-beginnersThis 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.项目地址https://gitcode.com/GitHub_Trending/mc/mcp-for-beginners点击查看免费下载导读本文围绕 mcp-for-beginners 课程第 15 课MCP Apps的作业展开完整讲解如何基于 Model Context Protocol 的 MCP Apps 新范式构建一个数据 用户界面自包含的交互式组件。你将掌握 MCP App 的两段式注册registerAppToolregisterAppResource、resourceUri关联机制、IFrame 内事件绑定与callServerTool通信方式并最终交付一个可运行的石头剪刀布游戏及其在 Visual Studio Code 中的测试方法。什么是 MCP Apps让工具结果自带 UIMCP Apps 是 MCP 生态中的一种新范式。传统模式下MCP Server 只负责在工具调用后返回数据消费这些数据还需要开发者自建前端来展示这部分代码需要单独编写与维护。MCP Apps 的核心想法是工具的结果不仅包含数据还包含这些数据应该如何被交互的信息——工具结果可以直接携带 UI 信息形成从数据到用户界面完全自包含的代码片段。这带来的直接价值是当你想为某个 MCP Server 快速提供一个带界面的入口时不再需要维护一套独立前端而是让服务端直接发布一个小型、自包含的 UI 组件MCP App。正如课程 15-mcp-apps 章节 所总结的MCP Apps 是 MCP 标准中非常新的一项补充适用于同时交付数据与 UI 功能的场景。MCP App 的组成与工作原理一个 MCP App 在服务端由两半组成并通过一个resourceUri把它们连接起来工具Tool提供业务能力接受输入并返回数据应用资源Application Resource提供可被渲染的组件HTML/JavaScript即 UI。从仓库的目录结构可以直观看到这种分工见 code/typescript/README.mdserver.ts -- 负责注册工具并把组件注册为 UI 资源 src/ mcp-app.ts -- 事件处理器装配event wire up mcp-app.html -- 用户界面课程文档用一张流程图描述了完整的运行时架构后端 MCP Server 中registerAppTool()注册工具、registerAppResource()注册组件资源二者通过resourceUri关联前端方面宿主应用Parent Web Page把 MCP App 的 UI 注入到 IFrame 容器中IFrame 内的mcp-app.html负责渲染界面src/mcp-app.ts负责事件处理当用户在界面中点击按钮时事件处理器调用服务端工具工具结果数据回传给事件处理器再由事件处理器向父页面发送消息参见 15-mcp-apps/README.md 中的架构说明。关键的安全设计是这些 MCP Apps 出于安全原因运行在 IFrame 中与宿主的通信需要通过向父 Web 应用发送消息来完成。这也是课程反复强调的要点。后端实现两段式注册后端需要完成两件事注册要交互的工具以及定义组件应用资源。注册工具registerAppTool先看一个最简单的工具get-time它不需要输入参数直接返回当前服务器时间registerAppTool( server, get-time, { title: Get Time, description: Returns the current server time., inputSchema: {}, _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async () { const time new Date().toISOString(); return { content: [{ type: text, text: time }] }; }, );这段代码与普通 MCP 工具注册的最大区别在于_meta: { ui: { resourceUri } }——正是这一行把工具链接到它的 UI 资源。宿主调用该工具时会读取_meta.ui.resourceUri来决定去获取并渲染哪个资源作为交互式 UI该机制在 code 目录的 server.ts 中同样有明确注释说明。注册组件registerAppResource在同一个文件中还需要注册组件资源。资源 URI 与工具通过同一个resourceUri关联const resourceUri ui://get-time/mcp-app.html; // Register the resource, which returns the bundled HTML/JavaScript for the UI. 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 }, ], }; }, );值得注意的实现细节是回调函数它通过fs.readFile从DIST_DIR即 Vite 构建产物dist目录见 server.ts 中的DIST_DIR定义读取打包后的mcp-app.html再以资源内容的形式返回给宿主。也就是说前端代码是在服务端通过资源回调下发给宿主的这与传统前后端分离的开发模式完全不同。前端组件纯 HTML 界面 事件装配与后端对应前端也分两部分纯 HTML 的用户界面以及负责事件处理、工具调用、向父窗口发消息的脚本。用户界面mcp-app.html!-- mcp-app.html -- !DOCTYPE html html langen head meta charsetUTF-8 / titleGet Time App/title /head body p strongServer Time:/strong code idserver-timeLoading.../code /p button idget-time-btnGet Server Time/button script typemodule src/src/mcp-app.ts/script /body /html界面本身是再普通不过的 DOM 结构通过script typemodule引入打包后的mcp-app.ts。事件装配src/mcp-app.ts事件装配负责识别 UI 中哪些元素需要事件处理器以及事件触发时做什么// mcp-app.ts import { App } from modelcontextprotocol/ext-apps; // Get element references const serverTimeEl document.getElementById(server-time)!; const getTimeBtn document.getElementById(get-time-btn)!; // Create app instance const app new App({ name: Get Time App, version: 1.0.0 }); // Handle tool results from the server. Set before app.connect() to avoid // missing the initial tool result. app.ontoolresult (result) { const time result.content?.find((c) c.type text)?.text; serverTimeEl.textContent time ?? [ERROR]; }; // Wire up button click getTimeBtn.addEventListener(click, async () { // app.callServerTool() lets the UI request fresh data from the server const result await app.callServerTool({ name: get-time, arguments: {} }); const time result.content?.find((c) c.type text)?.text; serverTimeEl.textContent time ?? [ERROR]; }); // Connect to host app.connect();这里的核心 API 有两个app.ontoolresult处理来自服务端的工具结果。代码注释特别提示它必须在app.connect()之前设置以免错过初始的工具结果app.callServerTool()让 UI 主动向服务端请求新数据。它内部会向父窗口发送消息由父窗口最终调用 MCP Server——这正好印证了前面IFrame 内通过消息与宿主通信的架构描述。App实例来自modelcontextprotocol/ext-apps包该依赖在 code/typescript/my-app/package.json 中声明modelcontextprotocol/ext-apps: ^1.1.1。处理用户输入FAQ 搜索示例前面的get-time组件只有按钮、没有输入。课程随后演示了如何加入输入框并把参数传给工具以实现 FAQ 搜索功能。后端带 zod inputSchema 的工具后端先定义 FAQ 数据再注册get-faq工具const faq: { [key: string]: string } { shipping: Our standard shipping time is 3-5 business days., return policy: You can return any item within 30 days of purchase., warranty: All products come with a 1-year warranty covering manufacturing defects., }; registerAppTool( server, get-faq, { title: Search FAQ, description: Searches the FAQ for relevant answers., inputSchema: zod.object({ query: zod.string().default(shipping), }), _meta: { ui: { resourceUri: faqResourceUri } }, // Links this tool to its UI resource }, async ({ query }) { const answer: string faq[query.toLowerCase()] || Sorry, I dont have an answer for that.; return { content: [{ type: text, text: answer }] }; }, );与get-time的关键差异在inputSchema。这里使用 zod 声明了一个名为query的输入参数它是可选的且带有默认值shippinginputSchema: zod.object({ query: zod.string().default(shipping), })工具处理器内通过faq[query.toLowerCase()]做不区分大小写的检索未命中时返回兜底文案。可以看到_meta.ui.resourceUri指向了另一个 FAQ 专用的资源 URIfaqResourceUri。前端输入框 按钮 事件对应的 UI 需要加入输入元素和按钮div classfaq h1FAQ response/h1 pFAQ Response: code idfaq-responseLoading.../code/p input typetext idfaq-query placeholderEnter FAQ query / button idget-faq-btnGet FAQ Response/button /div事件装配则在mcp-app.ts中补充const getFaqBtn document.getElementById(get-faq-btn)!; const faqQueryInput document.getElementById(faq-query) as HTMLInputElement; getFaqBtn.addEventListener(click, async () { const query faqQueryInput.value; const result await app.callServerTool({ name: get-faq, arguments: { query } }); const faq result.content?.find((c) c.type text)?.text; faqResponseEl.textContent faq ?? [ERROR]; });这里的模式与get-time完全一致只是app.callServerTool()的arguments中携带了从输入框读取的query值。整个交互链路是UI 事件 → callServerTool 发消息给父窗口 → 父窗口调用 MCP Server → 工具结果回传 → ontoolresult/异步返回值更新界面。输入 warranty 后服务端命中 FAQ 数据并返回保修政策答案作业实战构建石头剪刀布游戏作业需求课程在 15-mcp-apps/README.md 的 Assignment 一节 中布置了作业创建一个石头剪刀布游戏包含UI一个下拉列表选项、一个提交选择的按钮、一个显示谁选了什么、谁赢了的标签Server一个名为 rock-paper-scissors 的工具接收choice作为输入渲染电脑的选择并判定胜负。作业解决方案结构assignment/typescript/README.md 说明了解决方案的组织方式——只保留最核心的三部分代码my-app server.ts -- 服务端功能 src mcp-app.ts -- UI 事件装配 mcp-app.html -- UI 标记运行方式很简单参考 code/typescript/README.md 搭建工程再把 assignment 目录 中对应文件的内容分别填入各自文件即可。服务端play-rps 工具与资源注册先看服务端的核心实现完整源码见 assignment/typescript/my-app/server.tsregisterAppTool( 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 } }, // Links this tool to its UI resource }, 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 Its 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.enum([rock, paper, scissors])限定choice只能是三种合法取值之一非法输入在类型层就被拦截电脑选择通过Math.floor(Math.random() * options.length)在三个选项中随机生成serverChoice实现渲染电脑的选择胜负判定先判断平局再用三条规则判断玩家胜剪刀克布、布克石头、石头克剪刀其余情况为服务端胜返回格式返回标准的content: [{ type: text, text: result }]结果字符串中同时包含双方选择和胜负信息满足显示谁选了什么、谁赢了的需求。随后照例注册配套的 UI 资源与工具共用同一个resourceUriregisterAppResource( 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: {} }, }, ], }; }, );前端下拉框、按钮与结果标签UI 标记完整文件见 assignment/typescript/my-app/mcp-app.html!DOCTYPE html html langen head meta charsetUTF-8 / titleRock paper scissor/title /head body div classrock-paper-scissors h1Rock Paper Scissors/h1 select idrps-options valuerock option valuerockRock/option option valuepaperPaper/option option valuescissorsScissors/option /select button classselect idrps-buttonSelect/button pResult: code idrps-result.../code/p /div script typemodule src/src/mcp-app.ts/script /body /html事件装配完整文件见 assignment/typescript/my-app/src/mcp-app.tsimport { App } from modelcontextprotocol/ext-apps; // Get element references 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; // Create app instance const app new App({ name: Get Time App, version: 1.0.0 }); // Handle tool results from the server. Set before app.connect() to avoid // missing the initial tool result. 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]; }); // Connect to host app.connect();可以看到它与课程中get-time/get-faq的模式完全同构先获取 DOM 元素引用再创建App实例、设置ontoolresult最后在按钮点击时通过app.callServerTool()携带用户在下拉框中选择的choice调用服务端工具并把返回文本写入rps-result标签。整份作业的核心价值在于把下拉列表 按钮 结果标签的 UI 需求、带枚举校验的工具 随机电脑选择 胜负判定的服务端需求完整落地。运行与测试安装与启动后端在 code/typescript/my-app 目录下参考 code/typescript/README.md运行npm install安装前端与后端依赖验证后端能通过编译npx tsc --noEmit一切正常时应无输出启动服务npm start后端将运行在http://localhost:3001/mcp。package.json见 code/typescript/my-app/package.json中的启动脚本使用了concurrently同时跑 Vite 构建vite build --watch与后端tsx watch main.ts。课程特别提示Windows 机器上可能需要为concurrently找替代方案该脚本中同时使用cross-env NODE_ENVdevelopment INPUTmcp-app.html注入构建入口另外如果在 Codespace 中运行可能需要把端口可见性设为 public。方式一在 Visual Studio Code 中测试Visual Studio Code 对 MCP Apps 有很好的支持是测试 MCP App 最省事的方式之一。在.vscode/mcp.json或项目mcp.json中添加服务端条目参见 15-mcp-apps/README.md 的 Testing 一节my-mcp-server-7178eca7: { url: http://localhost:3001/mcp, type: http }完整的mcp.json结构在 code/typescript/README.md 中有示例外层包servers与inputs字段。接着点击mcp.json中的 start 按钮启动服务在安装有 GitHub Copilot 的聊天窗口中即可与你的 MCP App 交互。可以通过 prompt 触发例如输入#get-faq与在浏览器中运行一样它会在聊天界面中以同样的方式渲染 UI方式二使用宿主Host应用测试课程还提供了第二种测试路径使用独立的宿主应用。modelcontextprotocol/ext-apps仓库提供了多种可用于测试 MCP Apps 的宿主。在本地机器的做法是克隆该仓库后进入ext-apps目录运行npm install另开一个终端进入ext-apps/examples/basic-hostCodespace 环境下需要把serve.ts第 27 行的http://localhost:3001/mcp替换为你的 Codespace 后端地址运行npm start启动宿主宿主会连接后端并展示运行中的 App。在宿主界面中点击 Call Tool 按钮即可看到工具调用结果见 code/typescript/README.md。小结与关键要点通过本课作业的完整实现可以得出 MCP Apps 的几条核心结论MCP Apps 是 MCP 标准中一个非常新的补充适用于希望同时交付数据与 UI 功能的场景——服务端不仅对数据是什么有发言权还对数据如何被呈现有发言权两段式注册一个 MCP App 一个工具registerAppTool 一个应用资源registerAppResource二者通过_meta.ui.resourceUri与资源 URI 关联IFrame 隔离出于安全原因MCP Apps 运行在 IFrame 中前端与 MCP Server 的通信必须通过向父 Web 应用发送消息app.callServerTool()即封装了这一过程来完成社区已提供纯 JavaScript、React 等多种库来简化这种通信复用同一套模式从get-time无输入到get-faq带 zod 默认值参数再到石头剪刀布枚举参数 服务端随机逻辑交互组件的开发模式高度一致掌握了事件装配与callServerTool之后即可快速扩展任意带 UI 的工具。接下来可以继续深入第 4 章实践实现04-PracticalImplementation/README.md进一步探索分页等真实场景下的 MCP 落地技巧。赞分享教程文档人工智能【免费下载链接】mcp-for-beginnersThis 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.项目地址https://gitcode.com/GitHub_Trending/mc/mcp-for-beginners点击查看免费下载相关推荐在 TypeScript 中构建剪刀石头布 MCP AppregisterAppTool 与 registerAppResource 实战在 TypeScript 中构建剪刀石头布 MCP AppregisterAppTool 与 registerAppResource 实战 MCP Apps教程文档人工智能MCP Apps 实战指南基于 TypeScript 构建带交互 UI 的 MCP Server 与 Host 测试全流程MCP Apps 实战指南基于 TypeScript 构建带交互 UI 的 MCP Server 与 Host 测试全流程 本篇指南以 mcp for beg教程文档人工智能在 MCP Apps 中集成 A2UI构建基于 Python MCP Server 的交互式 UI 应用服务在 MCP Apps 中集成 A2UI构建基于 Python MCP Server 的交互式 UI 应用服务 导读 本文基于 a2ui 仓库中的 sample人工智能AI AgentAI 应用前端UI组件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑