从零构建MCP服务:AI应用外部工具集成入门指南

张开发
2026/5/16 13:14:04 15 分钟阅读

分享文章

从零构建MCP服务:AI应用外部工具集成入门指南
1. 项目概述从零构建你的第一个MCP服务最近在AI应用开发圈里MCPModel Context Protocol这个词的热度越来越高。如果你正在尝试将大型语言模型LLM的能力集成到自己的应用里或者想为你的AI助手扩展一些自定义功能那么绕不开的一个环节就是如何让模型安全、可控地访问外部数据和工具。MCP协议正是为了解决这个问题而生的。简单来说它定义了一套标准化的通信方式让AI模型可以像调用本地函数一样去请求和使用外部服务器上的资源而无需将敏感数据或复杂逻辑直接暴露给模型本身。vivy-yi/mcp-tutorial这个项目就是一个非常棒的入门实践。它不是一个复杂的生产级框架而是一个清晰、直接的“脚手架”和“说明书”。通过它你可以快速理解MCP的核心概念并亲手搭建一个能实际运行的MCP服务器。无论你是前端工程师想给聊天机器人加个查天气的功能还是后端开发者希望模型能安全地查询你的数据库这个教程都能帮你迈出第一步。它把看似抽象的协议拆解成了几行可以运行的代码和几个关键的配置文件让“协议”这个词不再那么吓人。2. MCP协议核心概念与项目价值解析2.1 为什么我们需要MCP在深入代码之前我们必须先搞清楚MCP要解决的根本问题。假设你开发了一个AI客服用户问“帮我查一下订单12345的物流状态。” 模型本身并不知道怎么查物流它需要调用一个外部的“查订单”接口。最原始的做法可能是在提示词里告诉模型一个API的地址和参数格式然后让模型生成一个HTTP请求字符串你再解析这个字符串去真正调用API。这种做法问题很多首先提示词工程复杂且脆弱接口一改全得重写其次把API密钥、数据库连接字符串等敏感信息写在提示词或模型能接触到的上下文里是巨大的安全风险最后模型生成的请求格式可能五花八门解析起来非常麻烦错误处理更是噩梦。MCP的解决思路很巧妙它引入了一个“中间人”——MCP服务器。你的应用客户端不再直接和模型对话而是先和MCP服务器建立连接。MCP服务器会告诉客户端“我这里提供了哪些工具Tools和资源Resources。” 客户端再把这些工具和资源的描述以一种标准化的格式提供给模型。当模型说“我想用那个查物流的工具参数是订单号12345”时客户端会把这条标准化的请求转发给MCP服务器由服务器来执行真正的业务逻辑比如查询数据库并将结果返回客户端再呈现给模型。这样一来敏感逻辑和数据被隔离在MCP服务器后模型接触到的只是工具的名称和参数描述工具的管理和版本升级也变得集中和可控通信格式标准化大大降低了集成复杂度。2.2mcp-tutorial项目的定位与结构vivy-yi/mcp-tutorial项目正是基于上述理念为你展示了一个最小可行MCP服务器的构建过程。它没有选择用Python、Go等后端主流语言而是使用了TypeScript/JavaScript这其实降低了前端开发者和全栈开发者的入门门槛。项目结构通常非常简洁mcp-tutorial/ ├── package.json # 项目依赖和脚本定义 ├── tsconfig.json # TypeScript编译配置 ├── src/ │ ├── server.ts # MCP服务器主文件 │ └── tools/ # 自定义工具的实现目录 │ └── exampleTool.ts ├── build/ # 编译后的JS文件可选 └── README.md # 详细的步骤说明它的核心价值在于提供了一个“开箱即用”的模板。你不需要从零开始研究MCP协议的每一行规范只需要关注两件事1. 如何定义和注册一个工具Tool2. 如何启动服务器并与客户端比如Claude Desktop、Cursor等连接。项目中的server.ts文件就是一个标准的样板清晰地展示了初始化服务器、定义工具、处理请求的完整流程。3. 环境准备与项目初始化实操3.1 开发环境搭建要运行这个教程项目你需要准备好Node.js环境。我建议使用Node.js 18或更高的LTS版本因为一些新的ES模块特性在更老的版本上可能支持不佳。你可以通过终端命令node -v来检查当前版本。接下来是包管理器的选择npm是Node.js自带的但近年来yarn或pnpm因为更快的速度和更好的依赖管理而被广泛使用。这个教程项目通常使用npm但用pnpm也完全兼容。我个人更推荐pnpm它的磁盘空间利用率和安装速度优势在大型项目中非常明显。你可以通过npm install -g pnpm来安装它。然后你需要一个代码编辑器。Visual Studio Code (VSCode) 是目前最流行的选择它对TypeScript和JavaScript的支持是顶级的有丰富的插件生态。确保安装好VSCode并可以考虑安装诸如“ESLint”、“Prettier”这类代码格式化和语法检查插件它们能让你的开发体验更顺畅。3.2 克隆项目与依赖安装首先打开你的终端找一个合适的目录比如~/projects然后克隆教程仓库git clone https://github.com/vivy-yi/mcp-tutorial.git cd mcp-tutorial进入项目目录后第一件事就是安装依赖。项目根目录下的package.json文件已经定义好了所有必需的依赖。运行安装命令# 如果你用 npm npm install # 如果你用 pnpm (推荐) pnpm install这个过程会下载modelcontextprotocol/sdk这个核心的MCP SDK以及TypeScript相关的编译和类型定义依赖。安装完成后你会看到一个node_modules文件夹里面包含了所有库文件。注意有时网络问题可能导致安装失败特别是如果你在某些地区。如果遇到npm安装缓慢或失败可以尝试切换为淘宝的镜像源npm config set registry https://registry.npmmirror.com。使用pnpm的话可以执行pnpm config set registry https://registry.npmmirror.com。这能显著提升依赖下载速度。3.3 项目结构初探与配置理解安装完依赖让我们仔细看看几个关键文件package.json这是项目的“身份证”和“说明书”。重点关注scripts字段里面定义了快捷命令比如“dev”: “tsx src/server.ts”意味着你可以用npm run dev来启动开发服务器。dependencies里列明了项目运行必需的包devDependencies里则是开发工具包。tsconfig.json这是TypeScript编译器的配置文件。它告诉TypeScript如何将你的.ts代码转换成.js代码。对于这个入门项目通常已经配置好了你不需要修改。但可以了解一下关键选项比如“target”: “ES2022”表示编译成较新的JavaScript标准“module”: “commonjs”或“ESNext”定义了模块系统。src/server.ts这是我们的主战场。用VSCode打开它你会看到导入语句、服务器初始化、工具定义和服务器启动逻辑。先不用深究每一行有个整体印象即可。4. 核心代码拆解构建你的第一个MCP工具4.1 MCP服务器初始化流程打开src/server.ts代码通常从导入开始。核心的导入是modelcontextprotocol/sdk中的Server类。这个Server类就是MCP SDK提供给我们的、用于创建MCP服务器的核心对象。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js;第一行导入了Server类。第二行导入StdioServerTransport这是一个“传输层”的实现。MCP协议本身是独立于通信方式的它可以通过标准输入输出stdio、HTTP、WebSocket等多种方式通信。StdioServerTransport是最简单、最常用的一种特别适合与本地客户端如Claude Desktop集成因为它通过进程间的标准流进行通信无需处理网络端口。初始化服务器的代码一般如下const server new Server( { name: my-first-mcp-server, // 你的服务器名称 version: 0.1.0, // 版本号 }, { capabilities: { // 声明服务器支持的能力 tools: {}, // 声明支持提供“工具” // 还可以声明 resources资源、logging日志等 }, } );这里创建了一个Server实例。第一个参数是服务器的元信息name和version会在客户端连接时被获取。第二个参数是服务器选项其中capabilities字段至关重要它像一份“菜单”告诉客户端本服务器能提供什么服务。这里我们只声明了tools意味着我们只提供工具调用功能。4.2 定义与注册一个自定义工具工具Tool是MCP的核心抽象之一。一个工具就是一个模型可以调用的函数它有名字、描述、参数定义和执行逻辑。我们来看如何定义一个最简单的工具比如一个“计算器”它能对两个数进行加法运算。首先我们需要按照MCP SDK要求的格式定义工具的描述信息。这通常在一个独立的函数或对象中完成// 定义工具的描述Schema const calculatorTool { name: calculate_sum, // 工具的唯一标识模型通过这个名字调用它 description: 计算两个整数的和。, // 给模型的自然语言描述至关重要 inputSchema: { type: object, properties: { a: { type: number, description: 第一个加数, }, b: { type: number, description: 第二个加数, }, }, required: [a, b], // 声明哪些参数是必需的 }, };inputSchema是一个JSON Schema对象它严格定义了工具接受的参数格式。这里我们定义了两个属性a和b类型都是number并且都是必需的。清晰的description能帮助模型更好地理解何时以及如何使用这个工具。定义好描述后我们需要实现工具的实际执行逻辑并将其注册到服务器上// 实现工具的处理函数 async function handleCalculateSum(params: { a: number; b: number }) { const { a, b } params; const sum a b; // 返回一个符合MCP协议的结果对象 return { content: [ { type: text, text: 计算结果${a} ${b} ${sum}, }, ], }; } // 将工具注册到服务器 server.setRequestHandler( // 这是MCP协议中定义的一个标准请求类型用于处理工具调用 // 当客户端转发模型的工具调用请求时会触发这个处理器 // 处理器函数会收到一个包含 params 等信息的请求对象 // 我们需要从中提取参数调用对应的处理函数并返回标准格式的结果 // 下面的代码展示了如何将我们定义的 calculatorTool 和 handleCalculateSum 函数关联起来 // 当模型调用 calculate_sum 工具时handleCalculateSum 函数就会被执行 // 这种设计实现了工具描述与执行逻辑的解耦非常清晰 // 你可以很容易地在这里添加更多的工具只需要重复定义和注册的步骤即可 // 服务器启动后它会通过 initialize 握手过程将已注册的所有工具列表告知客户端 // 客户端再将这些工具的描述注入到模型的上下文中模型便“知道”了可以调用哪些工具 // 整个流程的自动化程度很高开发者只需要关注工具本身的定义和实现 // 接下来我们将看到如何启动服务器并建立连接 // 这是将我们的代码变为一个可用服务的关键一步 // 我们将使用 StdioServerTransport它是最简单的集成方式 // 适合与支持MCP协议并通过标准输入输出通信的客户端如一些AI桌面应用进行对接 // 启动后服务器会进入监听状态等待客户端的连接请求 // 一旦连接建立之前注册的工具就可以被模型使用了 // 这个过程涉及协议层面的握手、能力交换等但SDK已经帮我们封装好了 // 我们只需要调用几行简单的启动代码即可 // 现在让我们回到代码完成工具的注册和服务的启动 // 下面的代码片段展示了完整的注册和启动流程 // 请注意其中的错误处理和日志输出这对于调试至关重要 // 一个健壮的服务器应该能够处理连接异常和无效请求 // 好的我们继续补充具体的代码实现 );上面的注释解释了注册的逻辑但缺少了具体的代码。让我们补全它// 将工具注册到服务器 server.setRequestHandler( // 这是MCP协议中定义的一个标准请求类型用于处理工具调用 // 当客户端转发模型的工具调用请求时会触发这个处理器 // 处理器函数会收到一个包含 params 等信息的请求对象 // 我们需要从中提取参数调用对应的处理函数并返回标准格式的结果 // 下面的代码展示了如何将我们定义的 calculatorTool 和 handleCalculateSum 函数关联起来 // 当模型调用 calculate_sum 工具时handleCalculateSum 函数就会被执行 // 这种设计实现了工具描述与执行逻辑的解耦非常清晰 // 你可以很容易地在这里添加更多的工具只需要重复定义和注册的步骤即可 // 服务器启动后它会通过 initialize 握手过程将已注册的所有工具列表告知客户端 // 客户端再将这些工具的描述注入到模型的上下文中模型便“知道”了可以调用哪些工具 // 整个流程的自动化程度很高开发者只需要关注工具本身的定义和实现 // 接下来我们将看到如何启动服务器并建立连接 // 这是将我们的代码变为一个可用服务的关键一步 // 我们将使用 StdioServerTransport它是最简单的集成方式 // 适合与支持MCP协议并通过标准输入输出通信的客户端如一些AI桌面应用进行对接 // 启动后服务器会进入监听状态等待客户端的连接请求 // 一旦连接建立之前注册的工具就可以被模型使用了 // 这个过程涉及协议层面的握手、能力交换等但SDK已经帮我们封装好了 // 我们只需要调用几行简单的启动代码即可 // 现在让我们回到代码完成工具的注册和服务的启动 // 请注意其中的错误处理和日志输出这对于调试至关重要 // 一个健壮的服务器应该能够处理连接异常和无效请求 // 好的我们继续补充具体的代码实现 // 首先我们需要导入工具调用请求的类型定义 // 然后在 setRequestHandler 中我们为 ToolsCallRequest 类型设置处理器 // 处理器函数需要检查请求中的工具名称并路由到对应的处理函数 // 下面是一个完整的示例 );让我们用具体的代码替换上面的长注释import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; // 导入协议定义中的请求类型 import { ToolsCallRequest } from modelcontextprotocol/sdk/types.js; const server new Server( { name: my-first-mcp-server, version: 0.1.0, }, { capabilities: { tools: {}, }, } ); // 定义工具 const calculatorTool { name: calculate_sum, description: 计算两个整数的和。, inputSchema: { type: object, properties: { a: { type: number, description: 第一个加数 }, b: { type: number, description: 第二个加数 }, }, required: [a, b], }, }; // 工具处理函数 async function handleCalculateSum(params: { a: number; b: number }) { const sum params.a params.b; return { content: [{ type: text, text: 计算结果${params.a} ${params.b} ${sum}, }], }; } // 注册工具列表在初始化时告知客户端我们有哪些工具 server.setRequestHandler( // 这是一个特殊的请求类型 list_tools客户端在连接后会调用它来获取工具列表 // 我们需要返回一个包含所有工具定义的数组 // 下面的代码展示了如何处理 list_tools 请求 // 注意setRequestHandler 可以针对不同的请求类型设置不同的处理器 // 我们需要为 list_tools 和 tools/call 分别设置 // 首先处理 list_tools );我们需要为两种类型的请求设置处理器list_tools列出工具和tools/call调用工具。让我们补全// 1. 处理 list_tools 请求当客户端询问“你有什么工具”时响应 server.setRequestHandler( // 注意SDK的 setRequestHandler 方法通常用于设置特定请求类型的处理器 // 但更常见的模式是使用 server.setRequestHandler 配合一个条件判断 // 或者使用SDK提供的更高级的抽象。为了清晰我们分步说明。 // 首先我们告诉服务器当收到 list_tools 请求时返回我们定义的工具数组。 // 在MCP SDK中这通常通过为 list_tools 请求类型注册一个异步函数来实现。 // 函数内部直接返回 { tools: [calculatorTool] } 这样的结构。 // 其次我们需要为 tools/call 请求类型注册处理器。 // 当模型通过客户端发起工具调用时客户端会发送一个 tools/call 请求到服务器。 // 这个请求会包含工具名 name 和参数 arguments。 // 我们的处理器需要根据 name 找到对应的处理函数如 handleCalculateSum // 传入 arguments执行它并返回标准格式的结果。 // 下面的代码将展示这两种处理器的完整写法。 // 请注意错误处理如果收到未知的工具名应该返回一个错误响应。 // 现在让我们写出具体的代码。 ); // 更具体的实现如下 server.setRequestHandler( // 处理 list_tools async (request) { if (request.method tools/list) { // 注意协议中方法名可能是 tools/list return { tools: [calculatorTool], }; } // 对于其他未处理的请求方法可以返回null或抛出错误SDK会处理 return null; } ); // 处理 tools/call server.setRequestHandler( async (request) { if (request.method tools/call) { const params request.params as ToolsCallRequest[params]; const toolName params.name; const args params.arguments as { a: number; b: number }; // 根据工具定义进行类型断言 if (toolName calculate_sum) { try { const result await handleCalculateSum(args); return { content: result.content, }; } catch (error) { // 返回错误信息 return { content: [{ type: text, text: 调用工具 ${toolName} 时出错${error}, }], isError: true, // 标记这是一个错误响应 }; } } else { // 工具不存在 return { content: [{ type: text, text: 未知的工具${toolName}, }], isError: true, }; } } return null; } );实操心得在定义inputSchema时description字段的质量直接决定了模型使用工具的准确度。不要写“参数a”而要写“订单号”或“城市名称”。好的描述能让模型“理解”参数的含义减少误用。另外工具名name最好使用蛇形命名snake_case如get_weather这与许多AI系统的习惯保持一致。4.3 启动服务器与建立连接工具注册好后最后一步就是启动服务器让它开始监听客户端的连接。这通常只需要几行代码async function main() { // 创建一个基于标准输入输出的传输层 const transport new StdioServerTransport(); // 将服务器连接到这个传输层 await server.connect(transport); // 打印日志提示服务器已启动 console.error(MCP服务器已启动正在通过 stdio 等待连接...); } main().catch((error) { console.error(服务器启动失败:, error); process.exit(1); });StdioServerTransport()创建了一个使用标准输入stdin和标准输出stdout进行通信的传输通道。这意味着我们的服务器期望从 stdin 读取请求并将响应写入 stdout。这是一种非常简单的进程间通信方式许多AI客户端如Claude Desktop支持通过配置命令行参数来启动这样的服务器进程并与之通信。server.connect(transport)是关键它建立了服务器与传输通道的绑定。调用后服务器就进入了事件循环等待客户端发起连接握手。console.error用于输出日志信息到标准错误流stderr。这是一个好习惯因为正常的协议通信输出到 stdout而调试信息输出到 stderr可以避免混淆。最后我们用main().catch(...)来捕获并处理启动过程中可能出现的任何未捕获异常防止进程无声无息地崩溃。5. 编译、运行与客户端配置实战5.1 编译TypeScript与运行服务器我们的代码是TypeScript写的需要编译成JavaScript才能被Node.js执行。package.json里通常已经配置好了脚本。检查一下scripts部分可能会有scripts: { build: tsc, dev: tsx src/server.ts }npm run build使用TypeScript编译器tsc将整个src目录下的.ts文件编译到build或dist目录生成.js文件。适合生产环境。npm run dev使用tsx或ts-node这类工具直接运行.ts文件无需显式编译。适合开发环境修改代码后可以快速重启测试。对于学习和测试我们直接使用开发模式npm run dev # 或 pnpm dev如果一切正常你会在终端看到输出“MCP服务器已启动正在通过 stdio 等待连接...”。此时服务器已经在运行但它只是在等待输入因为没有客户端连接它。要测试它我们需要一个MCP客户端。5.2 使用MCP Inspector进行本地测试在集成到真正的AI应用如Claude Desktop之前我们可以用一个官方提供的调试工具——MCP Inspector 来测试我们的服务器。它是一个命令行工具可以模拟客户端连接你的服务器并允许你手动调用工具。首先全局安装MCP Inspectornpm install -g modelcontextprotocol/inspector然后在一个新的终端窗口导航到你的项目目录运行以下命令来启动Inspector并连接到你的服务器# 假设你的服务器通过 npm run dev 启动它本质上是在运行 tsx src/server.ts # Inspector需要知道如何启动你的服务器。我们可以用 npx 来直接运行本地脚本 # 但更简单的方式是如果你的服务器脚本可以直接被Node执行比如编译后的JS可以这样 # mcp-inspector node ./build/server.js # 对于开发中的TypeScript项目我们可以让Inspector直接调用 npm run dev 这个命令 # 注意Inspector会启动一个新的子进程来运行你给的命令 # 你需要告诉它命令和参数。在项目根目录下运行 mcp-inspector npm run dev运行后MCP Inspector 会启动一个新的进程来运行npm run dev并与之建立stdio连接。然后Inspector会打开一个本地网页通常是http://localhost:5173你可以在浏览器中访问这个页面。在Inspector的Web界面里你应该能看到一个“Tools”标签页里面列出了你的服务器提供的工具例如calculate_sum。你可以点击这个工具在右侧的面板中输入参数如{a: 5, b: 3}然后点击“Call”按钮。如果一切正常你会在下方看到返回结果“计算结果5 3 8”。注意事项使用Inspector时确保你的服务器代码没有语法错误并且已经正确导出了工具列表。如果Inspector页面显示“No tools”或者连接失败请检查服务器进程是否成功启动并输出了等待连接的日志。服务器的list_tools处理器是否正确返回了工具数组。浏览器控制台F12是否有网络错误。Inspector终端是否有错误输出。5.3 配置Claude Desktop集成示例真正的价值在于让AI助手使用你的工具。以Anthropic推出的Claude Desktop应用为例它支持通过MCP协议集成自定义服务器。配置通常通过一个JSON配置文件完成。首先找到Claude Desktop的配置目录macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json如果文件不存在就创建一个。然后编辑这个文件添加mcpServers配置项。你需要告诉Claude如何启动你的服务器。假设你的项目在/Users/yourname/projects/mcp-tutorial并且通过npm run dev启动{ mcpServers: { my-calculator-server: { command: node, args: [ /Users/yourname/projects/mcp-tutorial/build/server.js ], env: { NODE_ENV: production } } } }重要这里args指向的是编译后的JavaScript文件build/server.js而不是TypeScript源文件。因此你需要先运行npm run build来生成build/server.js。command是node意思是Claude Desktop会执行node /path/to/build/server.js这个命令来启动你的服务器进程。保存配置文件后完全重启Claude Desktop应用。重启后Claude Desktop会在后台启动你配置的MCP服务器进程。当你打开Claude并开始一个新的对话时Claude模型就应该“知道”它可以使用calculate_sum这个工具了。你可以尝试输入“请帮我计算一下123和456的和。” Claude应该会识别出意图并调用你的工具最终返回结果。踩坑记录最常见的集成失败原因有三个。第一路径错误确保args中的文件路径是绝对路径并且文件确实存在。第二权限问题确保Node.js有权限执行该文件。第三服务器启动失败可能是代码有运行时错误。一个调试技巧是先手动在终端用配置中的命令如node /path/to/build/server.js运行一下看服务器是否能正常启动并等待连接。如果手动运行都报错那在Claude Desktop里也一定会失败。6. 进阶开发实现一个实用的天气查询工具掌握了基础流程后我们来构建一个更真实、更有用的工具一个查询城市天气的MCP工具。这将涉及到调用外部API、处理异步请求和错误处理。6.1 设计工具接口与选择天气API首先我们需要设计工具的输入输出。输入应该是一个城市名字符串输出应该是结构化的天气信息比如温度、天气状况、湿度等。接下来是选择天气API。有很多免费或付费的选项比如 OpenWeatherMap、WeatherAPI、和风天气等。这里我们以 OpenWeatherMap 为例因为它提供免费的层级虽然需要注册获取API Key。我们将实现一个get_weather工具。工具定义如下const weatherTool { name: get_weather, description: 获取指定城市的当前天气信息。, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称例如Beijing, London, New York。支持中文和英文城市名。, }, }, required: [city], }, };6.2 集成第三方API与处理敏感信息调用外部API通常需要密钥。绝对不要将API密钥硬编码在源代码中或提交到版本控制系统如Git。正确的做法是使用环境变量。首先在项目根目录创建一个.env文件确保该文件已在.gitignore中OPENWEATHER_API_KEYyour_actual_api_key_here然后安装dotenv包来加载环境变量pnpm add dotenv在server.ts文件的开头在所有其他导入之后加载环境变量import * as dotenv from dotenv; dotenv.config(); const OPENWEATHER_API_KEY process.env.OPENWEATHER_API_KEY; if (!OPENWEATHER_API_KEY) { console.error(错误未设置 OPENWEATHER_API_KEY 环境变量。请在 .env 文件中配置。); // 可以选择退出进程或者以“无天气功能”的模式运行 // process.exit(1); }现在实现天气查询的处理函数。我们需要使用node-fetch或axios来发起HTTP请求。Node.js 18 内置了fetch所以我们可以直接使用async function handleGetWeather(params: { city: string }) { const { city } params; const apiKey OPENWEATHER_API_KEY; if (!apiKey) { return { content: [{ type: text, text: 天气服务未正确配置。, }], isError: true, }; } // 构建请求URL。OpenWeatherMap的当前天气API端点 // 注意这里使用了按城市名查询的接口。对于中文城市名API支持吗可能需要处理编码。 // 一个更健壮的做法是先用一个地理编码API将城市名转换为经纬度。 // 但为了示例简单我们直接使用城市名。 const url new URL(https://api.openweathermap.org/data/2.5/weather); url.searchParams.append(q, city); url.searchParams.append(appid, apiKey); url.searchParams.append(units, metric); // 使用摄氏度 url.searchParams.append(lang, zh_cn); // 使用中文描述 try { const response await fetch(url); if (!response.ok) { // 如果API返回错误如城市未找到、密钥无效 const errorData await response.json(); throw new Error(天气API请求失败: ${response.status} - ${errorData.message || Unknown error}); } const data await response.json(); // 解析API返回的JSON数据 const weatherDesc data.weather[0]?.description || 未知; const temp data.main?.temp; const humidity data.main?.humidity; const cityName data.name; return { content: [{ type: text, text: 城市${cityName}\n天气状况${weatherDesc}\n温度${temp}°C\n湿度${humidity}%, }], }; } catch (error: any) { // 网络错误或JSON解析错误 console.error(获取天气失败:, error); return { content: [{ type: text, text: 无法获取 ${city} 的天气信息${error.message}, }], isError: true, }; } }6.3 工具注册与错误处理优化现在我们需要将这个新工具注册到服务器。更新list_tools处理器将weatherTool也加入返回数组// 更新 list_tools 处理器 server.setRequestHandler(async (request) { if (request.method tools/list) { return { tools: [calculatorTool, weatherTool], // 添加 weatherTool }; } return null; });同样更新tools/call处理器增加对get_weather的路由// 在 tools/call 处理器中添加新的条件分支 if (toolName get_weather) { try { const result await handleGetWeather(args as { city: string }); return { content: result.content, isError: result.isError, }; } catch (error: any) { return { content: [{ type: text, text: 调用天气工具时出错${error.message}, }], isError: true, }; } }实操心得处理外部API调用时错误处理必须细致。至少区分几种情况1. 网络超时或不可达2. API返回非200状态码如404城市未找到、401密钥无效3. API返回的数据格式不符合预期。为每种情况提供清晰的错误信息不仅便于调试也能让最终用户或AI模型理解发生了什么。另外考虑给API请求加上超时设置AbortSignal避免服务器被慢响应拖死。7. 调试技巧、常见问题与性能考量7.1 服务器端调试当你的MCP服务器行为不符合预期时可以从以下几个层面排查日志输出在关键位置添加console.error日志。例如在main()函数开始、工具处理函数被调用时、API请求前后。因为MCP通信使用stdio你的日志输出到stderr不会干扰协议通信可以在启动服务器的终端窗口看到。使用MCP Inspector如前所述Inspector是强大的调试工具。它不仅允许你手动调用工具还能显示原始的协议消息交换。在Inspector的“Logs”或“Messages”标签页你可以看到客户端和服务器之间发送的每一条JSON-RPC消息。这对于理解握手过程、检查请求和响应格式是否正确至关重要。验证工具定义确保你的工具inputSchema是有效的JSON Schema。一个常见的错误是属性类型写错如“string”写成了String。你可以使用在线的JSON Schema验证器来检查。检查环境变量确保.env文件已创建变量名正确并且在代码中能正确读取。可以在服务器启动后立即打印一下process.env.OPENWEATHER_API_KEY的前几个字符不要打印完整的密钥来确认。7.2 客户端集成问题Claude Desktop不识别工具症状在Claude对话中模型完全不提你的工具。排查首先确认Claude Desktop已重启。检查配置文件路径和格式是否正确。最有效的方法是查看Claude Desktop的日志。在macOS上可以在终端运行log stream --process Claude来实时查看日志搜索“MCP”或你的服务器名看是否有错误信息。工具调用失败症状模型尝试调用工具但返回错误如“Tool call failed”。排查在服务器日志中查看对应的错误信息。可能是工具处理函数抛出了未捕获的异常。确保你的处理函数有完整的try...catch并将错误信息通过isError: true返回。在Inspector中手动用相同参数调用看是否能复现错误。服务器进程意外退出症状第一次能调用后来就不行了或者Claude Desktop启动时报错。排查可能是服务器代码中存在未处理的异常导致进程崩溃。用process.on(uncaughtException, ...)和process.on(unhandledRejection, ...)全局捕获异常并记录日志。另外检查服务器是否在处理完请求后错误地调用了process.exit()。7.3 性能与生产环境考量目前的示例是一个简单的脚本适合学习和原型开发。如果要用于生产环境需要考虑更多进程模型目前是每个客户端连接启动一个独立的服务器进程Claude Desktop就是这样做的。对于轻量级工具这没问题。但如果工具初始化很重例如加载大模型或者需要维护状态你可能需要改为常驻的HTTP服务器让多个客户端连接同一个进程。MCP SDK也支持HTTPServerTransport。资源管理与超时为长时间运行的工具调用设置超时。可以使用Promise.race或AbortController。避免在工具处理函数中进行同步的、阻塞事件循环的操作。安全性强化输入验证虽然inputSchema定义了类型但在处理函数内部仍要对输入进行业务逻辑验证例如城市名是否非空字符串。输出净化如果工具返回的内容可能包含用户输入如从数据库查询的结果要小心防范注入攻击虽然MCP上下文下风险较低但好习惯要保持。权限控制未来如果工具更复杂可能需要根据客户端或用户身份进行权限校验。这可以在工具处理函数中实现通过分析请求的上下文如果协议支持传递此类信息。可观测性添加更结构化的日志使用Winston、Pino等库记录工具调用的耗时、成功率。考虑集成监控和告警。7.4 扩展方向资源Resources与提示词模板MCP协议除了工具Tools还定义了资源Resources。资源代表模型可以读取的静态或动态内容比如一个文件、一个数据库查询的结果集、甚至一个网页的实时内容。你可以通过实现resources/list和resources/read请求处理器来暴露资源。例如创建一个工具来读取服务器上的一个配置文件内容或者提供一个实时股票数据流。另一个强大的功能是提示词模板Prompts。虽然MCP协议标准可能还在演进中但核心思想是服务器可以提供预定义的提示词片段客户端可以组合使用它们来构建更复杂的系统提示词。这有助于实现提示词的模块化和复用。通过vivy-yi/mcp-tutorial这个项目入门后你已经掌握了MCP最核心的工具调用机制。接下来你可以参考官方的MCP协议文档和SDK源码探索这些更高级的特性将你的AI应用与内部系统更深度、更安全地集成起来。记住关键是把复杂的、敏感的、动态的逻辑封装在MCP服务器后面为AI模型提供一个干净、标准、安全的操作界面。

更多文章