30分钟构建MCP Server:扩展AI能力的实践指南
1. 从“MCP”到“Server”一个30分钟挑战的缘起最近在几个开发者社群里总能看到“MCP Server”这个词在刷屏。一开始我也没太在意以为又是哪个大厂新出的、需要复杂配置的中间件框架。直到上周一个朋友在群里丢了个链接说“用这个SDK半小时就能搓一个MCP Server出来”我才真正来了兴趣。半小时从零到部署这听起来更像是一个周末小挑战而不是一个严肃的后端开发任务。那么MCP Server到底是什么简单来说MCPModel Context Protocol是一种协议它定义了大语言模型比如我们常用的Claude、GPT如何与外部工具、数据源进行安全、结构化的交互。而一个MCP Server就是实现了这套协议的“服务端”。你可以把它想象成一个“翻译官”或者“接线员”当AI模型需要查询数据库、调用某个API、或者读取你本地的一个文件时它不会也不能直接去操作。AI会按照MCP协议规定的格式向MCP Server发送一个请求。MCP Server收到请求后负责执行具体的操作比如跑一段SQL或者调用一个天气API然后将结果按照协议格式打包好再返回给AI。这样一来AI的能力边界就被极大地扩展了它可以“使用”你为它注册的任何工具。为什么说现在手写一个变得很简单核心在于社区生态的成熟。AnthropicClaude的创造者官方以及社区贡献者已经提供了各种语言的SDK把协议底层复杂的JSON-RPC通信、资源Resource和工具Tool的定义、错误处理等脏活累活都封装好了。我们开发者要做的更像是“声明式编程”告诉SDK“我有什么工具”、“工具怎么用”剩下的通信和调度SDK帮你搞定。这就像以前你要自己砌砖盖房子现在有人给了你一套乐高积木和清晰的图纸搭建的速度自然不可同日而语。所以这个“30分钟从零到部署”的挑战其价值不在于创造了一个多么复杂的系统而在于它清晰地展示了一个趋势AI原生应用的开发门槛正在被快速拉低。通过亲手实现一个最简单的MCP Server你能最直观地理解AI如何与外部世界连接掌握为AI“赋能”的基本模式。无论你是前端、后端还是全栈开发者这都是一项值得投入半小时的未来技能。2. 战前准备厘清核心概念与搭建环境在开始敲代码之前我们必须先把几个核心概念和它们之间的关系搞清楚这能避免后续开发中的很多困惑。同时一个干净、正确的开发环境是“30分钟”承诺的基础。2.1 MCP协议的核心三要素MCP协议主要围绕三个核心概念构建资源Resources、工具Tools和提示词Prompts。我们这次构建的Server主要会用到前两者。资源Resources你可以理解为AI模型可以“读取”的静态或动态数据源。每个资源有一个唯一的URI如file:///path/to/note.md或dynamic:///current-time和一个MIME类型如text/plain,application/json。Server可以声明自己提供了哪些资源AI模型可以通过URI来请求读取这些资源的内容。例如一个提供当前股票价格的Server可以定义一个资源stock:///price/AAPL。工具Tools这是MCP Server的“肌肉”是AI模型可以“调用”来执行操作、产生副作用的函数。每个工具需要定义清晰的输入参数Schema和输出描述。当AI决定使用某个工具时它会按照Schema传入参数Server执行对应的函数逻辑并返回结果。例如一个“发送邮件”的工具需要收件人、主题、正文等参数。提示词Prompts这是一组可复用的对话模板或系统指令AI客户端可以请求获取这些提示词用于初始化对话或引导模型行为。对于入门Server来说这部分可以暂时跳过。我们的Server将作为一个独立的进程运行通过标准输入输出stdio或HTTP等传输方式与AI客户端如Claude Desktop、支持MCP的IDE插件进行JSON-RPC通信。SDK帮助我们处理了RPC的细节我们只需关注如何定义资源和工具。2.2 开发环境与工具链配置“30分钟”的挑战需要一个即开即用的环境。我们选择Node.js TypeScript的组合这是目前JavaScript生态中最主流、类型提示最友好的方案。第一步安装Node.js如果你还没有安装请访问 Node.js 官网下载 LTS长期支持版本进行安装。安装完成后打开终端Windows用CMD或PowerShellMac/Linux用Terminal运行以下命令验证node --version npm --version你应该能看到类似v20.x.x和10.x.x的版本号。如果遇到网络问题导致安装缓慢或失败可以搜索“Node.js 国内镜像”来加速npm的包下载例如配置淘宝镜像npm config set registry https://registry.npmmirror.com第二步初始化项目找一个你喜欢的目录新建一个文件夹比如my-first-mcp-server然后进入并初始化项目mkdir my-first-mcp-server cd my-first-mcp-server npm init -y这会生成一个package.json文件。第三步安装TypeScript和必要依赖我们需要TypeScript编译器以及Anthropic官方提供的Node.js SDKmodelcontextprotocol/sdk。npm install typescript ts-node types/node --save-dev npm install modelcontextprotocol/sdktypescript和ts-node让我们能直接运行.ts文件。types/node提供了Node.js API的类型定义。modelcontextprotocol/sdk是今天的核心它封装了所有MCP协议的逻辑。第四步配置TypeScript创建一个tsconfig.json文件来配置TypeScript编译器npx tsc --init打开生成的tsconfig.json文件确保或修改以下关键配置{ compilerOptions: { target: ES2022, module: commonjs, lib: [ES2022], outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true }, include: [src/**/*], exclude: [node_modules] }这个配置告诉TypeScript源代码在src目录下编译输出到dist目录使用较新的ES2022标准并开启严格的类型检查。注意如果你在较旧的教程中看到baseUrl选项请注意它在TypeScript新版本中已被标记为弃用。我们上面的配置没有使用它避免了未来升级到TS 7.0时的问题。如果你的项目需要路径映射应使用paths选项来代替。至此一个最小化但功能完备的开发环境就准备好了。接下来我们进入最激动人心的环节编写Server的核心逻辑。3. 核心实现定义你的第一个工具与资源现在我们来创建Server的“大脑”。根据MCP SDK的指引我们需要创建一个Server实例然后为其注册工具Tools和资源Resources。为了让例子更有趣我们假设在构建一个“个人助理服务器”它目前有两个能力1. 告诉你一个随机冷笑话工具。2. 让你读取一份固定的个人简介资源。3.1 项目结构与入口文件首先创建源代码目录和入口文件mkdir src touch src/index.ts我们的所有代码都将写在src/index.ts中。3.2 引入SDK与创建Server实例打开src/index.ts开始编写代码// 引入必要的模块 import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from modelcontextprotocol/sdk/types.js; // 1. 创建Server实例 const server new Server( { name: my-first-mcp-server, // 你的服务器名称 version: 0.1.0, // 版本号 }, { capabilities: { // 声明本Server支持的能力 tools: {}, // 支持工具 resources: {}, // 支持资源 // 还可以声明 prompts: {} 等 }, } );这段代码做了几件事从SDK中导入核心的Server类和用于标准输入输出通信的StdioServerTransport。导入了一系列“请求模式”Schema它们定义了客户端可能发送的请求的数据结构。SDK会利用这些Schema来自动验证和处理请求。使用new Server()创建了一个服务器实例。第一个参数是服务器的元信息第二个参数capabilities至关重要它像一份“菜单”告诉连接的客户端“我这里有工具和资源可以提供服务”。一开始菜单是空的我们接下来就要往里面加“菜”。3.3 实现“讲冷笑话”工具Tool工具的本质是一个函数当AI客户端调用它时这个函数被执行。我们需要定义这个工具的“说明书”名称、描述、参数和“实现”函数体。在创建Server实例的代码后面添加工具定义// 2. 定义并注册工具 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: get_random_joke, // 工具的唯一标识名 description: 获取一个随机的冷笑话点亮你的心情。, inputSchema: { type: object, properties: { // 这个工具不需要输入参数所以properties为空对象 // 如果需要参数可以在这里定义例如 // category: { type: string, description: 笑话类别 } }, required: [], // 没有必填参数 }, }, ], }; }); // 3. 处理工具的调用请求 server.setRequestHandler(CallToolRequestSchema, async (request) { // 判断客户端调用的是哪个工具 if (request.params.name get_random_joke) { // 这里是一个硬编码的笑话库真实场景可能调用API const jokes [ 为什么程序员讨厌大自然因为里面有太多虫子。, 我告诉电脑我要睡觉了现在它问我是否要进入休眠模式。, SQL查询走进一家酒吧看到两张大桌子它说‘我可以JOIN你们吗’, ]; const randomJoke jokes[Math.floor(Math.random() * jokes.length)]; // 按照MCP协议格式返回结果 return { content: [ { type: text, text: 冷笑话机器人${randomJoke}, }, ], }; } // 如果调用的工具名未匹配抛出错误 throw new Error(未知的工具: ${request.params.name}); });代码解读与注意事项server.setRequestHandler(ListToolsRequestSchema, ...)这个方法注册了一个处理器。当客户端发送“列出所有工具”的请求时这个处理器被调用返回我们定义的get_random_joke工具的“说明书”。注意这里的inputSchema使用了JSON Schema格式来描述参数这对于AI理解如何调用工具至关重要。server.setRequestHandler(CallToolRequestSchema, ...)这个处理器用于实际执行工具。当客户端调用get_random_joke时我们从一个数组中随机选取一个笑话返回。返回格式工具调用结果必须包裹在content数组中其中每个对象通常包含type和text字段。这是一种标准化的返回格式便于AI客户端解析和展示。错误处理如果收到未知的工具调用请求我们抛出一个错误。SDK会捕获这个错误并将其转换为协议规定的错误响应返回给客户端。3.4 实现“读取个人简介”资源Resource资源代表可读的数据。我们需要声明存在哪些资源并处理客户端读取特定资源的请求。在工具定义的代码后面继续添加资源相关的代码// 4. 定义并注册资源列表 server.setRequestHandler(ListResourcesRequestSchema, async () { return { resources: [ { uri: file:///personal/intro, // 资源的唯一标识URI name: 我的个人简介, description: 一份关于我的简单介绍。, mimeType: text/plain, // 资源的MIME类型 }, ], }; }); // 5. 处理读取资源的请求 server.setRequestHandler(ReadResourceRequestSchema, async (request) { if (request.params.uri file:///personal/intro) { // 这里返回静态内容。实际应用中可以读取文件、查询数据库等。 const introContent 你好我是你的MCP Server开发者。 这是一个通过30分钟构建的示例服务器。 我热爱技术尤其是让AI变得更实用的工具。; return { contents: [ { uri: request.params.uri, mimeType: text/plain, text: introContent, }, ], }; } throw new Error(未找到资源: ${request.params.uri}); });代码解读与注意事项ListResourcesRequestSchema处理器返回一个资源列表。这里我们只定义了一个资源其URI是file:///personal/intro。URI的格式可以自定义但最好能清晰表达资源的类型和路径。ReadResourceRequestSchema处理器当客户端请求读取file:///personal/intro这个URI时我们返回硬编码的个人简介文本。MIME类型mimeType字段告诉客户端内容的格式这对于客户端正确渲染内容很重要例如text/markdown会被渲染为富文本application/json会被解析为结构化数据。动态资源资源不一定是静态的。你可以定义一个URI如dynamic:///current-time然后在ReadResourceRequestSchema处理器中动态生成内容例如返回new Date().toISOString()。这就是MCP的强大之处——AI可以通过统一的接口访问动态信息。3.5 启动服务器最后我们需要让服务器运行起来并监听来自客户端的连接。在文件末尾添加// 6. 启动服务器使用标准输入输出进行通信 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Server 已启动正在通过 stdio 等待连接...); } runServer().catch((error) { console.error(服务器启动失败:, error); process.exit(1); });StdioServerTransport是SDK提供的一种传输方式它使用进程的标准输入stdin和标准输出stdout进行通信。这是与Claude Desktop等客户端集成时最常用、最简单的方式。服务器启动后它会阻塞在这里等待客户端连接并发送JSON-RPC消息。至此一个功能完整的、包含一个工具和一个资源的MCP Server就编码完成了。你可以运行npx ts-node src/index.ts来测试它是否报错但目前它还不会做任何事因为没有客户端连接。接下来我们要让它变得可部署、可连接。4. 打包、部署与连接实战代码写完了但它还只是一个本地的TypeScript文件。我们需要将其构建成可独立运行的JavaScript程序并学习如何让AI客户端如Claude Desktop发现并连接它。4.1 构建与打包为了便于分发和运行我们需要将TypeScript编译成JavaScript并处理好依赖。第一步更新package.json脚本打开package.json在scripts部分添加构建和启动命令{ scripts: { build: tsc, start: node dist/index.js, dev: ts-node src/index.ts } }npm run build调用TypeScript编译器tsc根据tsconfig.json配置将src下的.ts文件编译到dist目录。npm start运行编译后的dist/index.js主文件。npm run dev在开发时使用用ts-node直接运行.ts文件省去编译步骤。第二步执行构建在终端运行npm run build如果一切顺利你会看到dist目录被创建里面有一个index.js文件。这就是我们最终要部署的程序。踩坑点模块导入路径如果你在构建或运行时遇到类似Error: Cannot find module modelcontextprotocol/sdk/server/index.js的错误这通常是因为TypeScript的模块解析策略与Node.js运行时的差异。我们代码中使用的import ... from .../index.js是ES模块ESM的写法。确保你的package.json中包含type: module字段以告诉Node.js将此项目作为ESM项目处理。如果项目其他部分有CommonJS模块可能会产生冲突一个稳妥的解决方法是使用动态导入或查阅SDK文档确认其推荐的导入方式。本例中使用的路径是基于SDK最新版本的结构。4.2 配置Claude Desktop进行连接目前体验MCP Server最方便的方式是通过Claude Desktop应用。我们需要创建一个配置文件来告诉Claude Desktop我们的Server在哪里。第一步找到Claude Desktop配置目录macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json如果目录或文件不存在就手动创建它。第二步编辑配置文件在配置文件中添加你的MCP Server配置。以下是一个示例{ mcpServers: { my-first-server: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/index.js ], env: { NODE_ENV: production } } } }配置项详解与避坑指南my-first-server这是你给这个Server起的任意名字用于在Claude内部标识。command启动Server的可执行命令。因为我们的是Node.js脚本所以是node。args传递给命令的参数。这里是最容易出错的地方必须使用绝对路径你不能使用像./dist/index.js这样的相对路径。Claude Desktop启动时的工作目录可能不是你的项目目录会导致找不到文件。务必替换/ABSOLUTE/PATH/TO/YOUR/PROJECT为你项目dist/index.js文件的完整路径。Windows用户注意Windows的路径格式是C:\Users\...\dist\index.js但在JSON中反斜杠\是转义字符需要写成双反斜杠\\例如C:\\Users\\YourName\\projects\\my-first-mcp-server\\dist\\index.js。或者可以使用正斜杠/Node.js通常也能识别C:/Users/YourName/projects/my-first-mcp-server/dist/index.js。env可选为Server进程设置环境变量。第三步重启Claude Desktop保存配置文件后完全退出并重新启动Claude Desktop应用。4.3 验证与测试连接重启Claude Desktop后如果配置正确你的Server进程会在Claude Desktop启动时被自动拉起。如何验证连接成功查看日志在Claude Desktop的界面中你可能看不到明显提示。一个验证方法是在启动Claude Desktop后打开系统的活动监视器Mac或任务管理器Windows查找是否有额外的node进程在运行其参数指向你的脚本。与Claude对话这是最直接的测试方式。在Claude Desktop的聊天框中尝试让Claude使用你的工具。例如你可以输入“你能用get_random_joke工具给我讲个笑话吗”如果一切正常Claude会理解你的意图调用该工具并展示返回的冷笑话。它可能会回复“当然我调用一下讲笑话的工具……稍等…… 为什么程序员讨厌大自然因为里面有太多虫子。”连接失败的排查思路如果Claude没有反应或者提示找不到工具请按以下步骤排查检查配置文件路径百分之九十的问题出在这里。再次确认args中的路径是绝对路径且完全正确。可以尝试在终端中直接用该绝对路径运行node /path/to/dist/index.js看程序是否能正常启动并打印出“MCP Server 已启动...”的日志。检查Claude Desktop日志Claude Desktop通常会记录错误日志。在上述配置目录的同级位置查找logs文件夹查看最新的日志文件里面可能有关于启动MCP Server失败的具体错误信息。检查Server代码确保你的Server代码没有在启动阶段就抛出异常。可以在代码开始部分添加console.error(‘启动参数:’, process.argv)来辅助调试。检查端口/传输方式我们使用的是stdio传输这要求父进程Claude Desktop启动子进程你的Server。确保配置是command: node而不是试图启动一个HTTP服务。如果你后来改成了HTTP传输那么配置方式会完全不同。当你在Claude的对话中看到它成功调用了你亲手编写的工具并返回了结果时那种成就感无疑是巨大的。这意味着你已经成功搭建了一座连接AI模型与外部世界的桥梁。5. 从Demo到生产进阶思考与优化方向恭喜你一个可工作的MCP Server已经搭建并运行起来了但这只是一个起点。要让这个Server变得真正有用、可靠还需要考虑很多问题。下面我们来探讨几个关键的进阶方向。5.1 工具设计的艺术输入Schema与用户体验我们之前定义的get_random_joke工具没有参数。但在实际项目中工具的参数设计至关重要它直接影响了AI模型能否正确、方便地使用它。一个反面教材假设我们有一个查询天气的工具。糟糕的Schema只接受一个query字符串参数让AI自己拼凑“北京天气”或“Beijing weather”。良好的Schema明确定义city(城市名字符串) 和country(国家代码可选字符串) 两个参数。inputSchema: { type: object, properties: { city: { type: string, description: 城市名称例如Beijing, 上海 Tokyo, }, country: { type: string, description: ISO国家代码可选例如CN, US, JP, }, }, required: [city], },为什么重要清晰的Schema就像一份详细的API文档能极大地提高AI模型调用工具的准确率。description字段要尽可能具体甚至可以给出例子。对于枚举值使用enum约束。好的工具设计会让AI感觉像是在调用一个设计良好的函数库。5.2 安全与错误处理构建健壮的Server我们的示例Server几乎没有错误处理。在生产环境中这绝对是致命的。1. 输入验证与清理即使有Schema验证在工具的实现函数内部也应对输入进行二次校验。例如对于城市名参数可以检查是否为空字符串或者是否包含非法字符。2. 异步操作与超时如果工具需要调用外部API或进行数据库查询务必设置超时timeout并使用try...catch包裹异步操作防止未处理的Promise拒绝导致整个Server崩溃。3. 资源访问控制我们的资源URI是硬编码的。在更复杂的Server中资源URI可能包含路径参数如file:///notes/${id}。必须严格验证客户端请求的URI是否在其被允许访问的范围内防止路径遍历攻击如file:///notes/../../etc/passwd。4. 结构化错误返回不要只是throw new Error(“未知错误”)。MCP协议允许返回结构化的错误信息。利用SDK提供的错误类或自定义错误对象给客户端提供更有用的错误码和提示信息。// 一个更好的错误处理示例 server.setRequestHandler(CallToolRequestSchema, async (request) { try { if (request.params.name query_weather) { const { city, country } request.params.arguments || {}; if (!city || typeof city ! string) { // 返回结构化的错误 return { content: [{ type: text, text: 错误缺少或无效的“city”参数。 }], isError: true // MCP协议中可能用其他字段标识错误具体看SDK }; } // ... 调用天气API ... const weatherData await fetchWeatherApi(city, country); return { content: [{ type: text, text: weatherData }] }; } } catch (error) { console.error(工具调用失败:, error); // 返回友好的错误信息而非内部异常 return { content: [{ type: text, text: 抱歉处理您的请求时出现了问题。请稍后再试。 }], isError: true }; } });5.3 性能、可观测性与部署形态性能考量你的Server是单线程的Node.js进程。如果一个工具处理耗时很长如下载大文件它会阻塞其他请求。对于IO密集型操作Node.js的异步特性可以很好处理。但对于CPU密集型任务考虑将其转移到工作线程Worker Thread或拆分为独立的微服务避免影响Server的响应能力。日志与监控在生产环境console.error是不够的。需要集成像Winston、Pino这样的日志库将日志结构化地输出到文件或日志收集系统如ELK、Loki。同时可以暴露简单的健康检查端点如果使用HTTP传输或通过工具调用返回状态信息方便监控。部署形态多样化我们使用了stdio传输这依赖于Claude Desktop作为父进程来启动。另一种更通用的方式是使用HTTP/S 传输。这样你的Server可以作为一个独立的HTTP服务运行在某个端口上例如localhost:3000。任何支持MCP over HTTP的客户端都可以连接它部署也更灵活可以部署到云服务器。SDK也提供了HTTPServerTransport只需更改几行启动代码即可切换。// 使用HTTP传输的示例需安装额外依赖如express import { HTTPServerTransport } from modelcontextprotocol/sdk/server/http.js; import express from express; const app express(); const transport new HTTPServerTransport(app, /mcp); // 处理 /mcp 路径的请求 await server.connect(transport); app.listen(3000, () { console.error(MCP Server 正在监听 http://localhost:3000/mcp); });从一个小小的Demo到一个可用于生产环境的组件中间隔着对细节的深思熟虑。每一次错误处理、每一次参数校验、每一行日志记录都是让这座“桥梁”更稳固的砖石。当你看到自己编写的Server稳定地处理着来自AI的各类请求无缝地扩展着AI的能力时这半小时的投入便获得了远超其时间价值的回报。
