1. 从零到一:理解 MCP 与 mcp-use 的核心价值
如果你最近在关注 AI 应用开发,尤其是想让 ChatGPT、Claude 这类大模型助手变得更“能干”,那你大概率已经听说过 MCP(Model Context Protocol)这个词了。简单来说,MCP 就像是为 AI 模型定义的一套“插件”或“外挂”标准协议。它允许开发者创建各种工具(比如查询天气、读取文件、操作数据库),然后让 AI 模型能够安全、标准化地调用这些工具,从而突破其自身知识库和功能的限制。
听起来很美好,对吧?但问题来了:协议是协议,真要动手去实现一个 MCP 服务器,或者想给 AI 助手做个带界面的交互小工具(MCP App),你会发现这里面涉及的东西相当繁杂。从底层的协议通信、工具定义、资源管理,到前端的 UI 渲染、状态同步,再到部署、调试、监控……每一个环节都可能让你从“想做个酷东西”变成“在技术细节里挣扎”。
这就是mcp-use出现的背景。它不是另一个 MCP 协议的实现,而是一个全栈的 MCP 框架。你可以把它理解为一个“超级工具箱”,它把构建 MCP 服务器和 MCP 应用所需的所有轮子都给你造好了,并且按照最佳实践的方式组装在了一起。无论是用 TypeScript 还是 Python,你都能用一套简洁、统一的 API,快速构建出功能强大、生产就绪的 MCP 服务。
我第一次接触 mcp-use 时,最直接的感受是:它把 MCP 开发的“心智负担”降到了最低。你不用再纠结于如何解析 SSE(Server-Sent Events)流、如何管理工具的生命周期、如何让前端组件和后端逻辑联动。你只需要关注你的业务逻辑本身:“我这个工具要做什么?”、“我这个界面要展示什么?”。剩下的,框架都帮你处理好了。
2. 核心架构与设计哲学:为什么选择 mcp-use?
在深入代码之前,我们先拆解一下 mcp-use 的设计思路。理解这个,能让你在后续开发中少走很多弯路,也能更好地利用框架的特性。
2.1 一体化全栈体验
很多框架只解决单点问题,比如只提供 MCP 服务器 SDK,或者只提供一个简陋的客户端。mcp-use 的野心更大,它要提供从开发、调试到部署、监控的完整闭环。
- 开发时:它提供了
MCPServer类来快速定义工具和资源,提供了MCPClient和MCPAgent来连接和使用其他 MCP 服务,还提供了 React 组件库来构建交互式 UI(MCP Apps)。 - 调试时:内置的 Inspector 是一个可视化调试工具,让你可以像测试 API 接口一样,手动触发工具调用、查看返回结果、监控通信过程,这比在终端看日志高效太多了。
- 部署时:它提供了 CLI 工具和与 Manufact Cloud 的深度集成,让你能一键将服务部署到生产环境,并自带可观测性、日志和指标。
这种一体化设计,意味着你不需要在多个不兼容的库之间做胶水代码,学习成本更低,项目结构也更清晰。
2.2 “约定大于配置”与自动发现
这是 mcp-use 提升开发效率的一个关键设计。以创建 MCP App(带 UI 的工具)为例,传统的做法可能是:先在服务器端注册工具,然后在某个地方手动注册对应的 UI 组件,最后还要处理路由。在 mcp-use 里,这个过程被极大地简化了。
你只需要在服务器端定义工具时,指定一个widget字段(例如widget: "weather-display"),然后在项目的resources/目录下,创建一个对应的 React 组件文件(例如resources/weather-display/widget.tsx)。框架会自动扫描resources/目录,发现并关联这个组件。你完全不需要写任何额外的注册或绑定代码。
这种基于目录结构和命名约定的自动发现机制,让添加新功能变得异常简单,也减少了因手动注册遗漏导致的 Bug。
2.3 类型安全与开发体验
mcp-use 对 TypeScript 和 Python 都提供了优秀的类型支持。在 TypeScript 版本中,工具的参数定义使用 Zod 库,UI 组件的 Props 也使用 Zod 定义,这确保了从后端到前端整个数据流的类型安全。你在编写工具处理函数时,参数的类型是自动推断的;在编写 UI 组件时,能通过useWidgetHook 获得完全类型化的props。
在 Python 版本中,则深度集成了 Pydantic,利用Annotated和Field来定义工具参数,同样能获得良好的类型提示和运行时验证。
注意:虽然框架提供了便利,但定义清晰、严谨的工具 Schema(参数结构)仍然是你的责任。一个模糊的
description或松散的类型定义,会导致 AI 模型难以正确调用你的工具。务必花时间把工具的名称、描述和参数定义得清晰、无歧义。
2.4 生产就绪的考量
从代码中可以看到,mcp-use 不是玩具项目。它提供了:
- 健壮的错误处理:工具调用异常会被框架捕获并以标准化的错误信息返回。
- 可观测性:与 Manufact Cloud 集成后,你可以看到工具调用的次数、延迟、成功率等指标。
- 资源管理:对于需要初始化或清理的资源(如数据库连接),框架提供了生命周期钩子。
- 协议一致性:项目通过了一系列 MCP 协议一致性测试(Conformance Tests),确保与不同客户端(如 Claude Desktop, Cursor)的兼容性。
3. 手把手实战:构建你的第一个 MCP 应用
理论说再多,不如动手写一行代码。我们以构建一个“待办事项(Todo List)管理” MCP App 为例,走通从创建到部署的完整流程。这个应用将允许 AI 助手帮你查看、添加和完成待办事项,并且有一个漂亮的界面来展示列表。
3.1 环境准备与项目初始化
首先,确保你的开发环境已经安装了 Node.js(建议 18+ 或 20+)和 npm。
最快捷的方式是使用官方脚手架:
npx create-mcp-use-app@latest my-todo-app cd my-todo-app这个命令会创建一个包含基础结构的 TypeScript 项目。你会看到类似如下的目录结构:
my-todo-app/ ├── package.json ├── tsconfig.json ├── src/ │ └── server.ts # MCP 服务器主文件 ├── resources/ # MCP App UI 组件存放目录 └── .env.example进入项目,安装依赖:
npm install现在,打开src/server.ts,你会看到一个简单的示例服务器。我们先清空它,从头开始构建我们的 Todo 服务。
3.2 定义数据模型与模拟存储
在真实项目中,你可能会连接数据库。为了简化,我们先在内存中用一个数组来模拟。
在src/server.ts中,我们先定义 Todo 的类型和初始数据:
// src/server.ts import { MCPServer, text, widget } from "mcp-use/server"; import { z } from "zod"; // 1. 定义 Todo 类型 interface TodoItem { id: string; title: string; completed: boolean; createdAt: Date; } // 2. 模拟内存存储 let todoStore: TodoItem[] = [ { id: '1', title: '学习 mcp-use 框架', completed: true, createdAt: new Date('2024-01-01') }, { id: '2', title: '写一篇技术博客', completed: false, createdAt: new Date('2024-01-02') }, { id: '3', title: '部署一个 MCP 服务', completed: false, createdAt: new Date('2024-01-03') }, ]; // 生成唯一ID的辅助函数 function generateId(): string { return Math.random().toString(36).substring(2, 9); }3.3 创建 MCP 服务器与工具
接下来,我们初始化服务器,并定义三个核心工具:list_todos(列出所有待办)、add_todo(添加待办)、complete_todo(完成待办)。
// 3. 创建 MCP 服务器实例 const server = new MCPServer({ name: "todo-list-manager", version: "1.0.0", }); // 4. 定义工具:列出所有待办事项 server.tool({ name: "list_todos", description: "获取所有的待办事项列表。可以通过可选参数过滤已完成或未完成的项目。", schema: z.object({ filter: z.enum(['all', 'active', 'completed']).optional().default('all'), }), widget: "todo-list", // 关键!这将关联到 resources/todo-list/widget.tsx }, async ({ filter }) => { let filteredTodos = todoStore; if (filter === 'active') { filteredTodos = todoStore.filter(todo => !todo.completed); } else if (filter === 'completed') { filteredTodos = todoStore.filter(todo => todo.completed); } // 返回 widget,将数据传递给前端组件 return widget({ props: { todos: filteredTodos, filter: filter, }, // 同时返回一个文本摘要,供纯文本客户端(如某些AI界面)使用 message: `找到 ${filteredTodos.length} 个待办事项。`, }); }); // 5. 定义工具:添加新待办事项 server.tool({ name: "add_todo", description: "添加一个新的待办事项。需要提供事项的标题。", schema: z.object({ title: z.string().min(1, "标题不能为空").describe("待办事项的标题"), }), }, async ({ title }) => { const newTodo: TodoItem = { id: generateId(), title, completed: false, createdAt: new Date(), }; todoStore.push(newTodo); // 添加成功后,可以返回一个成功消息,并建议用户列出事项查看 return text(`待办事项 "${title}" 已成功添加。ID: ${newTodo.id}`); }); // 6. 定义工具:标记待办事项为完成 server.tool({ name: "complete_todo", description: "根据ID将一个待办事项标记为已完成。", schema: z.object({ id: z.string().describe("要完成的待办事项的ID"), }), }, async ({ id }) => { const todoIndex = todoStore.findIndex(todo => todo.id === id); if (todoIndex === -1) { // 工具调用出错时,可以抛出错误,框架会将其转换为标准错误响应 throw new Error(`未找到ID为 ${id} 的待办事项。`); } todoStore[todoIndex].completed = true; return text(`待办事项 "${todoStore[todoIndex].title}" 已标记为完成。`); }); // 7. 启动服务器 const PORT = 3000; await server.listen(PORT); console.log(`✅ MCP 服务器运行在 http://localhost:${PORT}`); console.log(`🔧 Inspector 调试界面: http://localhost:${PORT}/inspector`);关键点解析:
- 工具定义:每个
server.tool()调用定义了一个 AI 模型可以调用的工具。schema使用 Zod 定义参数,这对 AI 理解工具用法至关重要。 - Widget 关联:
list_todos工具指定了widget: "todo-list"。这意味着当工具被调用时,如果客户端支持 UI(如 Claude Desktop 的最新版本),它将尝试渲染resources/todo-list/下的组件。 - 返回值:
widget()函数返回一个特殊的响应,它包含了给前端的数据 (props) 和一个后备的文本信息 (message)。text()函数则返回纯文本响应。
3.4 构建交互式 UI 组件 (MCP App)
现在,我们来创建那个会被自动发现的 UI 组件。在resources/目录下创建新的文件夹和文件:
resources/ └── todo-list/ ├── widget.tsx # React 组件 └── widget.css // 可选:样式文件首先,定义组件的属性 Schema 和元数据:
// resources/todo-list/widget.tsx import { useWidget, type WidgetMetadata } from "mcp-use/react"; import { z } from "zod"; // 1. 定义组件接收的 Props 结构,必须与服务器端 widget() 调用中的 props 匹配 const propSchema = z.object({ todos: z.array(z.object({ id: z.string(), title: z.string(), completed: z.boolean(), createdAt: z.string().or(z.date()).transform(v => new Date(v)), // 处理日期格式 })), filter: z.enum(['all', 'active', 'completed']).optional().default('all'), }); // 2. 导出元数据,框架会自动读取 export const widgetMetadata: WidgetMetadata = { description: "一个美观的待办事项列表视图,支持过滤和状态展示。", props: propSchema, // 关联 Schema }; // 3. 定义 React 组件 const TodoListWidget: React.FC = () => { // useWidget Hook 会自动注入 props、加载状态和客户端主题 const { props, isPending, theme } = useWidget<z.infer<typeof propSchema>>(); const isDark = theme === 'dark'; if (isPending) { return <div style={{ padding: '20px', textAlign: 'center' }}>加载待办事项中...</div>; } const { todos, filter } = props; const activeCount = todos.filter(t => !t.completed).length; return ( <div style={{ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', backgroundColor: isDark ? '#1e1e1e' : '#f9f9f9', color: isDark ? '#e0e0e0' : '#333', borderRadius: '12px', padding: '24px', maxWidth: '500px', margin: '0 auto', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}> <h2 style={{ margin: 0, color: isDark ? '#fff' : '#222' }}>📝 我的待办列表</h2> <div style={{ fontSize: '0.9em', padding: '4px 12px', borderRadius: '20px', backgroundColor: isDark ? '#333' : '#eaeaea', color: isDark ? '#ccc' : '#666', }}> 过滤: <strong>{filter === 'all' ? '全部' : filter === 'active' ? '未完成' : '已完成'}</strong> </div> </div> <div style={{ marginBottom: '16px', color: isDark ? '#aaa' : '#666' }}> 总计 <strong>{todos.length}</strong> 项,其中 <strong>{activeCount}</strong> 项待完成。 </div> {todos.length === 0 ? ( <div style={{ textAlign: 'center', padding: '40px 20px', color: isDark ? '#888' : '#999' }}> 暂无待办事项。尝试添加一个吧! </div> ) : ( <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}> {todos.map(todo => ( <li key={todo.id} style={{ display: 'flex', alignItems: 'center', padding: '12px 16px', marginBottom: '8px', backgroundColor: isDark ? '#2a2a2a' : '#fff', borderRadius: '8px', borderLeft: `4px solid ${todo.completed ? '#10b981' : '#3b82f6'}`, opacity: todo.completed ? 0.7 : 1, }} > <input type="checkbox" checked={todo.completed} readOnly // 因为是只读展示,实际交互需通过工具调用 style={{ marginRight: '12px', transform: 'scale(1.2)' }} /> <div style={{ flex: 1 }}> <div style={{ textDecoration: todo.completed ? 'line-through' : 'none', color: todo.completed ? (isDark ? '#888' : '#999') : (isDark ? '#e0e0e0' : '#333'), fontWeight: todo.completed ? 'normal' : '500', }}> {todo.title} </div> <div style={{ fontSize: '0.8em', color: isDark ? '#777' : '#aaa', marginTop: '4px' }}> 创建于: {new Date(todo.createdAt).toLocaleDateString('zh-CN')} </div> </div> <div style={{ fontSize: '0.75em', padding: '2px 8px', borderRadius: '10px', backgroundColor: todo.completed ? (isDark ? '#064e3b' : '#d1fae5') : (isDark ? '#1e3a8a' : '#dbeafe'), color: todo.completed ? (isDark ? '#6ee7b7' : '#065f46') : (isDark ? '#93c5fd' : '#1e40af'), }}> {todo.completed ? '已完成' : '进行中'} </div> </li> ))} </ul> )} <div style={{ marginTop: '20px', fontSize: '0.85em', color: isDark ? '#888' : '#666', textAlign: 'center' }}> 提示:你可以对我说“添加一个待办事项:买牛奶”或“把ID为2的事项标记为完成”。 </div> </div> ); }; export default TodoListWidget;实操心得:
- 样式内联:为了确保组件在不同客户端(ChatGPT, Claude等)中渲染一致,建议使用内联样式或 CSS-in-JS 方案。这里使用了简单的内联样式。
- 主题适配:
useWidgetHook 提供了theme属性(‘light’ 或 ‘dark’),可以根据客户端主题动态调整颜色,提升用户体验。 - 只读 UI:目前的 UI 主要是“展示层”。用户与 AI 对话来修改数据(如添加、完成)。未来,mcp-use 可能会支持更复杂的双向交互组件。
3.5 运行与调试
现在,启动你的服务器:
npm run dev如果一切顺利,你会看到服务器启动的日志,并提示 Inspector 地址。打开浏览器,访问http://localhost:3000/inspector。
在 Inspector 界面中,你可以:
- 在左侧看到你定义的三个工具:
list_todos,add_todo,complete_todo。 - 点击
list_todos,在参数输入框里可以尝试输入{"filter": "active"},然后点击 “Call Tool”。 - 右侧会显示返回结果。你不仅能看到文本消息,还能看到一个 “Open Widget” 按钮。点击它,就能在一个独立窗口中看到我们刚刚编写的精美待办事项列表界面!
这就是 mcp-use Inspector 的强大之处:无需启动前端项目,无需配置复杂的环境,直接可视化调试你的 MCP 工具和 UI。
3.6 连接 AI 助手进行测试
真正的测试是让 AI 来调用。你需要一个支持 MCP 协议的客户端。以Claude Desktop为例:
- 找到 Claude Desktop 的配置文件。通常在
~/Library/Application Support/Claude/claude_desktop_config.json(Mac) 或%APPDATA%\Claude\claude_desktop_config.json(Windows)。 - 在配置文件中添加你的本地 MCP 服务器:
{ "mcpServers": { "my-todo-app": { "command": "npx", "args": ["tsx", "/ABSOLUTE/PATH/TO/YOUR/my-todo-app/src/server.ts"], "env": { "NODE_ENV": "development" } } } }重要提示:必须使用
tsx或ts-node等工具来直接运行 TypeScript 文件,或者将代码编译成 JS 后指向 JS 文件。同时,务必使用绝对路径。
- 保存配置文件并重启 Claude Desktop。
- 打开 Claude,现在你可以直接对它说:“请列出我所有的待办事项。” 或者 “帮我添加一个待办事项:阅读 mcp-use 文档。” Claude 会自动识别并调用对应的工具,并将 UI 组件渲染在对话中。
4. 进阶技巧与生产部署
4.1 处理异步操作与长任务
有些工具操作可能很耗时,比如调用一个外部 API 或处理大量数据。mcp-use 支持工具返回Promise。对于特别长的任务,你可以考虑使用进度指示。框架支持在工具执行过程中发送进度更新。
server.tool({ name: "process_data", description: "处理一批数据,这是一个长时间运行的任务。", schema: z.object({ dataset: z.string() }), }, async ({ dataset }, { sendProgress }) => { const totalSteps = 10; for (let i = 1; i <= totalSteps; i++) { // 模拟处理步骤 await new Promise(resolve => setTimeout(resolve, 500)); // 发送进度更新,客户端可以显示进度条 sendProgress(`正在处理... (${i}/${totalSteps})`, i / totalSteps); } return text(`数据集 "${dataset}" 处理完成!`); });4.2 错误处理与用户反馈
良好的错误处理能提升 AI 助手和最终用户的体验。除了在工具函数内throw new Error(),你还可以返回更结构化的错误信息。
server.tool({ name: "get_user_profile", description: "根据用户ID获取资料", schema: z.object({ userId: z.string() }), }, async ({ userId }) => { const user = await db.user.findUnique({ where: { id: userId } }); if (!user) { // 抛出的错误信息会被 AI 模型看到,用于组织回复 throw new Error(`未找到用户 ID: ${userId}。请检查用户ID是否正确。`); } if (user.isSuspended) { // 你也可以返回一个包含错误信息的文本,而不是抛出异常 return text(`用户 ${user.name} 的账户已被封禁,无法查看资料。`); } return widget({ props: { user }, message: `找到用户: ${user.name}` }); });4.3 部署到生产环境
本地开发测试完成后,是时候部署了。mcp-use 提供了无缝的部署体验。
方案一:使用 Manufact Cloud(推荐)这是最省心的方式,提供了开箱即用的监控、日志和自动 HTTPS。
- 将你的代码推送到一个 GitHub 仓库。
- 访问 manufact.com ,用 GitHub 登录。
- 点击 “New Project”,连接你的仓库。
- 配置构建命令(通常是
npm run build)和启动命令(npm start)。 - 点击部署。完成后,你会获得一个永久的
.run.mcp-use.com域名。
方案二:使用 CLI 部署如果你有自己的服务器或容器平台,可以使用 CLI 工具构建和部署。
# 1. 登录 npx @mcp-use/cli login # 2. 构建(如果需要) npm run build # 3. 部署(假设你已配置好服务器环境,CLI 会引导你) npx @mcp-use/cli deploy方案三:传统服务器部署你也可以将 mcp-use 服务当作一个标准的 Node.js 应用来部署。
- 构建:确保你的
package.json中有build脚本(例如tsc编译 TypeScript)。 - 进程管理:在生产环境使用
pm2或systemd来管理进程,保证其常驻和自动重启。 - 反向代理:使用 Nginx 或 Caddy 将你的域名(如
mcp.yourdomain.com)反向代理到localhost:3000,并配置 SSL 证书。
4.4 监控与日志
一旦上线,监控至关重要。
- Manufact Cloud:内置了仪表盘,展示请求量、延迟、错误率,以及每个工具的具体调用情况。
- 自定义日志:在工具函数中,使用你喜欢的日志库(如
winston,pino)记录关键操作和错误。 - 健康检查:mcp-use 服务器通常会在根路径或
/health提供健康检查端点,方便你配置 Kubernetes 存活探针或外部监控。
5. 常见问题与排查实录
在实际开发和部署中,你肯定会遇到各种问题。这里记录了几个我踩过的坑和解决方案。
5.1 工具调用失败:“Tool not found” 或 “Invalid arguments”
- 问题:AI 助手说找不到工具,或者参数不对。
- 排查:
- 检查服务器日志:首先看你的服务器控制台有没有错误输出。工具定义语法错误会导致服务器启动失败。
- 使用 Inspector:在 Inspector 中手动调用工具,看是否能成功。如果 Inspector 里也不行,问题肯定在服务器端。
- 检查 Schema 定义:Zod Schema 是否正确定义了所有参数?
description字段是否清晰?AI 模型严重依赖这些描述来理解如何调用工具。一个模糊的describe(“城市”)不如describe(“要查询天气的城市名称,例如‘北京’或‘San Francisco’”)。 - 检查客户端配置:确保 Claude Desktop 或其它客户端的配置文件路径正确,且命令能成功启动你的服务器。可以尝试在终端手动运行配置中的
command和args,看能否启动。
5.2 UI 组件不显示,只看到文本消息
- 问题:AI 助手返回了结果,但只显示了
message里的文本,没有渲染出漂亮的 Widget。 - 排查:
- 确认客户端支持:不是所有 MCP 客户端都支持 UI 渲染。确保你使用的是最新版的 Claude Desktop 或明确支持 MCP Apps 的客户端。
- 检查
widget名称:服务器端widget: “todo-list”必须与resources/todo-list/widget.tsx的目录名完全匹配(区分大小写)。 - 检查组件导出:确保
widget.tsx文件默认导出了 React 组件,并且导出了widgetMetadata。 - 查看 Inspector:在 Inspector 中调用工具,查看返回的原始数据。确认
content数组里包含{“type”: “widget”, “widget”: …}对象,而不仅仅是{“type”: “text”, …}。
5.3 部署后无法访问或超时
- 问题:本地运行正常,部署到云服务器后无法连接。
- 排查:
- 端口与防火墙:确保你的服务器应用监听的是
0.0.0.0而不是127.0.0.1(localhost)。检查云服务器的安全组/防火墙规则,是否开放了对应的端口(如 3000)。 - 环境变量:生产环境可能需要不同的数据库连接字符串或 API 密钥。使用
.env文件或平台的环境变量配置功能。 - 资源路径:如果你的代码中有读取本地文件(如
fs.readFileSync(‘./data.json’)),部署后相对路径可能失效。建议使用path.join(__dirname, ‘…’)构造绝对路径,或使用云存储服务。 - 查看日志:登录到云服务器,查看应用日志和系统日志(
journalctl -u your-service或pm2 logs)。
- 端口与防火墙:确保你的服务器应用监听的是
5.4 性能优化与冷启动
- 问题:工具调用响应慢,尤其是部署在 Serverless 环境时冷启动明显。
- 优化建议:
- 连接池与缓存:对于数据库、外部 API 客户端,使用连接池并在应用生命周期内保持单例,避免每次调用都创建新连接。
- 精简依赖:定期检查
package.json,移除未使用的依赖。庞大的node_modules会拖慢冷启动。 - 考虑预热:如果是重要的生产服务,可以设置一个简单的 cron job 定期调用健康检查端点,保持实例活跃。
- 使用更轻量运行时:对于特别简单的工具,可以评估使用 mcp-use 的 Python 版本,在某些场景下可能启动更快。
mcp-use 框架极大地降低了进入 MCP 世界的门槛,但它本身也在快速迭代。保持关注官方文档和 Discord 社区,是获取最新最佳实践和解决疑难杂症的最好方式。我最深的体会是,它把开发者从协议实现的复杂性中解放出来,让我们能更专注于创造有价值的 AI 功能交互。现在,你可以基于这个 Todo List 的模板,去构思和实现更复杂、更有趣的 MCP 应用了,比如股票查看器、智能家居控制器,或者一个专属于你工作流的自动化看板。