TypeScript MCP SDK:为AI应用构建标准化工具调用服务器的完整指南

张开发
2026/5/1 18:07:02 15 分钟阅读

分享文章

TypeScript MCP SDK:为AI应用构建标准化工具调用服务器的完整指南
1. 项目概述一个为AI应用注入“工具调用”能力的核心SDK如果你正在构建一个需要与外部世界交互的AI应用比如让AI帮你分析数据库、操作文件、调用API那么你大概率会遇到一个核心问题如何让AI模型安全、高效、标准化地使用这些工具这正是MCPModel Context Protocol协议试图解决的而palald/mcp-sdk-typescript这个项目则是为TypeScript/JavaScript开发者提供的一个官方级SDK实现。简单来说这个SDK是一个“桥梁”或“适配器”的构建工具包。它让你能够用TypeScript轻松创建符合MCP协议的服务器Server。这个服务器可以理解为一个“工具包”的提供者它向AI客户端比如Claude Desktop、Cursor等宣告“嘿我这里有一些工具比如读写文件、查询数据库、调用天气API你可以安全地通过标准化的方式来调用它们。” 对于开发者而言你不再需要为每个AI应用单独编写复杂的集成代码只需用这个SDK构建一个MCP服务器所有兼容MCP的客户端就都能使用你的工具了。我最初接触这个项目是因为在做一个内部数据分析助手时希望Claude能直接查询我们的PostgreSQL数据。手动对接不仅麻烦而且安全性和复用性都很差。MCP协议和这个SDK的出现完美地将工具能力标准化、服务化。它解决的核心痛点就是工具集成的碎片化和安全管控的复杂性。通过它你可以专注于实现工具的业务逻辑而将协议通信、资源管理、权限控制等脏活累活交给SDK。这个SDK适合任何希望为AI应用尤其是基于Claude系列模型的应用扩展能力的开发者。无论你是想为团队内部构建一个能操作Jira、查询CRM的智能助手还是开发一个面向所有MCP客户端的通用工具比如一个强大的计算器或代码解释器这个项目都是你快速入门的绝佳起点。它的设计充分考虑了TypeScript/JavaScript生态的开发习惯提供了清晰的类型定义和友好的API即使你对MCP协议本身不熟悉也能很快上手。2. MCP协议核心思想与SDK的定位在深入代码之前我们必须先理解MCPModel Context Protocol协议到底在做什么。你可以把它想象成AI世界的“USB协议”。在USB协议出现之前每个外设鼠标、键盘、打印机都需要自己的专用接口和驱动混乱且低效。MCP协议的目标就是为AI模型客户端和外部工具服务器定义一个标准的“插口”和“通信语言”。2.1 MCP协议的三大核心抽象MCP协议主要定义了三种核心资源这也是SDK中你需要操作的主要对象工具Tools这是最直接的能力。一个工具就是一个可以被AI调用的函数它有明确的名称、描述、输入参数JSON Schema定义和输出。例如“get_weather”工具输入是{“city”: “string”}输出是天气信息的JSON。AI在需要时会请求调用这个工具服务器执行并返回结果。资源Resources代表可以被AI读取的静态或动态内容。资源有唯一的URI如file:///path/to/doc.md或dynamic://news/latest、一个MIME类型和内容本身。AI客户端可以列出list和读取read资源。这非常适合用于向AI提供背景知识、文档、或实时数据流。例如你可以提供一个company://metrics/daily资源其内容是每日业务报表的Markdown文本。提示词模板Prompts这是一组可复用的对话开场白或指令模板。它包含参数和具体的提示词内容。AI客户端可以获取模板列表并利用参数实例化一个完整的提示词用于引导对话。比如一个“代码审查”提示词模板参数是{“code”: “string”, “language”: “string”}实例化后就是一个请求AI审查特定代码的完整指令。这个SDK (palald/mcp-sdk-typescript) 的定位就是帮助你作为一个“服务器”开发者轻松地创建和管理这些工具、资源和提示词模板并处理与AI客户端之间基于JSON-RPC over STDIO/SSE的通信协议。它封装了底层的消息序列化、连接管理、错误处理让你可以像写一个普通的Node.js模块一样专注于实现addTooladdResource等高级API。2.2 为什么选择这个SDK官方品质与生态兼容性市面上可能还有其他非官方的MCP实现但palald/mcp-sdk-typescript具有不可替代的优势。首先它是由Anthropic官方维护的这意味着它与Claude生态的兼容性最好更新最及时能第一时间支持协议的新特性。其次它的代码质量非常高提供了完整的TypeScript类型支持这在开发涉及复杂数据结构的工具时至关重要能极大减少运行时错误。从架构上看这个SDK采用了清晰的依赖注入和生命周期管理。你创建的服务器实例Server是一个核心容器你向其中注册工具、资源等处理器。SDK会负责在服务器启动时将这些能力以标准格式通告给客户端。这种设计使得代码组织非常模块化你可以将不同的工具集拆分成独立的模块进行注册。注意虽然协议叫“Model Context Protocol”但服务器即工具提供方的开发者并不需要关心对面是哪个具体的AI模型Claude-3.5-Sonnet还是GPT-4。你的服务器只负责按照协议提供能力。这带来了极大的灵活性你构建的工具可以被任何兼容MCP的客户端使用。3. 开发环境搭建与项目初始化让我们从零开始创建一个最简单的MCP服务器体验一下这个SDK的工作流程。这里假设你已经有基本的Node.js和TypeScript开发环境。3.1 基础环境准备首先确保你的系统满足以下要求Node.js: 版本18或更高。推荐使用LTS版本如20.x可以通过node -v检查。包管理器: npm 或 yarn 或 pnpm。本文使用 npm。TypeScript: 我们将使用TypeScript以获得最佳开发体验。全局安装或项目内安装均可。创建一个新的项目目录并初始化mkdir my-first-mcp-server cd my-first-mcp-server npm init -y接下来安装核心依赖npm install modelcontextprotocol/sdk同时安装TypeScript及相关类型定义作为开发依赖npm install -D typescript types/node初始化TypeScript配置npx tsc --init这会在项目根目录生成tsconfig.json文件。我们需要对其进行一些调整以适配现代Node.js开发。一个推荐的基础配置如下{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true }, include: [src/**/*], exclude: [node_modules, dist] }3.2 创建第一个“Hello World” MCP服务器现在在项目下创建src目录并在其中创建入口文件src/index.ts。让我们编写一个最简单的服务器它只提供一个工具greet。这个工具接收一个名字参数返回一句问候语。// src/index.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; // 1. 创建Server实例 // name 和 version 会在初始化握手时发送给客户端用于标识你的服务器。 const server new Server( { name: my-first-mcp-server, version: 1.0.0, }, { // 可选的服务器能力声明这里我们先声明支持工具。 capabilities: { tools: {}, // 表示本服务器提供工具 }, } ); // 2. 定义并注册工具 // 使用 server.setRequestHandler 来处理来自客户端的特定请求。 // 这里我们处理 tools/list 和 tools/call 请求。 server.setRequestHandler( // 处理 tools/list 请求用于向客户端列出所有可用工具。 async () { return { tools: [ { name: greet, // 工具的唯一标识符 description: 向某人发送问候语。, // 对AI清晰的描述 inputSchema: { type: object, properties: { name: { type: string, description: 需要问候的人的名字。, }, }, required: [name], // 声明必填参数 }, }, ], }; } ); // 处理 tools/call 请求当AI决定调用某个工具时触发。 server.setRequestHandler( async (request) { // 确保请求的方法是 tools/call if (request.method ! tools/call) { return undefined; // 如果不是我们处理的请求返回undefinedSDK会传递给其他处理器或返回错误。 } const params request.params; // 检查调用的工具名是否是 greet if (params.name ! greet) { throw new Error(未知的工具: ${params.name}); } // 从参数中获取 name。客户端传递的参数会在 params.arguments 中。 const userName params.arguments?.name; if (!userName || typeof userName ! string) { throw new Error(参数 name 是必须的且应为字符串。); } // 执行工具逻辑 const greeting 你好${userName}欢迎使用MCP服务器。; // 返回工具调用结果 return { content: [ { type: text, text: greeting, }, ], }; } ); // 3. 启动服务器并连接传输层 // MCP服务器通常通过 STDIO标准输入输出与客户端通信。 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP服务器已启动正在通过STDIO等待连接...); } // 启动服务器并处理错误 runServer().catch((error) { console.error(服务器启动失败:, error); process.exit(1); });这个服务器虽然简单但包含了所有核心要素创建服务器、声明能力、注册工具列表、处理工具调用请求。StdioServerTransport是SDK提供的一个传输层实现它使用标准输入输出流进行通信这是MCP服务器最常用、最兼容的通信方式。3.3 编译、运行与测试首先我们需要将TypeScript编译为JavaScript。在package.json的scripts部分添加构建和启动命令{ scripts: { build: tsc, start: node dist/index.js } }运行npm run build进行编译然后使用npm start启动服务器。此时服务器会挂起等待通过STDIO接收客户端连接。如何测试单独运行这个服务器是看不到效果的因为它需要和一个MCP客户端对话。最方便的测试方法是使用Claude Desktop。找到Claude Desktop的配置文件位置macOS通常在~/Library/Application Support/Claude/claude_desktop_config.jsonWindows在%APPDATA%\Claude\claude_desktop_config.json。在配置文件的mcpServers部分添加你的服务器配置。为了测试我们可以直接指向编译后的JS文件。{ mcpServers: { my-first-server: { command: node, args: [/ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/index.js] } } }重启Claude Desktop。在Claude Desktop的对话窗口中你现在可以尝试说“请使用greet工具我的名字是小明。” Claude应该会识别到这个工具并调用它你将看到返回的问候语。实操心得路径与权限在配置args时务必使用绝对路径。相对路径在Claude Desktop的上下文中可能无法正确解析。另外确保你的Node.js脚本具有可执行权限并且Claude Desktop进程有权限访问该路径和运行Node。4. 核心功能深度实现与最佳实践掌握了基础流程后我们来深入探讨如何实现更复杂、更实用的功能。一个好的MCP服务器不仅仅是提供几个工具更需要考虑工具的组织、错误处理、资源管理和性能。4.1 实现一个实用的工具文件系统浏览器让我们构建一个更真实的工具一个安全的、受限的文件系统浏览器。它允许AI列出指定目录下的文件并读取文本文件的内容。出于安全考虑我们会将操作限制在某个“工作区”目录内。首先安装必要的Node.js内置模块无需额外安装并更新代码。我们创建src/tools/filesystem.ts来模块化地组织这个工具集。// src/tools/filesystem.ts import * as fs from fs/promises; import * as path from path; // 定义工作区的根目录。在实际应用中这可以通过环境变量或配置传入。 const WORKSPACE_ROOT process.env.MCP_WORKSPACE || path.join(process.cwd(), workspace); /** * 确保目标路径被限制在工作区根目录内防止目录遍历攻击。 * param userPath 用户请求的路径相对或绝对 * returns 标准化且安全的绝对路径 * throws 如果路径尝试跳出工作区则抛出错误。 */ function resolveSafePath(userPath: string): string { const requestedPath path.isAbsolute(userPath) ? userPath : path.join(WORKSPACE_ROOT, userPath); const normalizedPath path.normalize(requestedPath); // 安全检查解析后的路径必须以工作区根目录开头 if (!normalizedPath.startsWith(path.normalize(WORKSPACE_ROOT) path.sep) normalizedPath ! path.normalize(WORKSPACE_ROOT)) { throw new Error(访问路径 ${userPath} 被拒绝超出允许的工作区范围。); } return normalizedPath; } /** * 工具list_directory - 列出目录内容 */ export const listDirectoryTool { name: list_directory, description: 列出指定目录下的文件和子目录。, inputSchema: { type: object, properties: { dirPath: { type: string, description: 要列出的目录路径。可以是绝对路径或相对于工作区根目录的相对路径。默认为工作区根目录。, }, }, required: [], }, handler: async (args: { dirPath?: string }) { const targetDir args.dirPath ? resolveSafePath(args.dirPath) : WORKSPACE_ROOT; try { const entries await fs.readdir(targetDir, { withFileTypes: true }); const result entries.map((entry) ({ name: entry.name, type: entry.isDirectory() ? directory : file, // 可以添加更多信息如大小、修改时间等 })); return { content: [{ type: text, text: 目录 ${targetDir} 下的内容\n JSON.stringify(result, null, 2), }], }; } catch (error: any) { throw new Error(无法读取目录 ${targetDir}: ${error.message}); } }, }; /** * 工具read_text_file - 读取文本文件内容 */ export const readTextFileTool { name: read_text_file, description: 读取一个文本文件的内容。支持常见的文本格式.txt, .md, .json, .js, .ts等。, inputSchema: { type: object, properties: { filePath: { type: string, description: 要读取的文本文件路径。可以是绝对路径或相对于工作区根目录的相对路径。, }, }, required: [filePath], }, handler: async (args: { filePath: string }) { const safeFilePath resolveSafePath(args.filePath); // 可选检查文件扩展名避免读取二进制大文件 const allowedExt [.txt, .md, .json, .js, .ts, .html, .css, .yml, .yaml, .xml]; const ext path.extname(safeFilePath).toLowerCase(); if (!allowedExt.includes(ext)) { // 不是阻止而是给出警告。AI可能仍需读取其他格式。 console.warn(警告正在读取非标准文本文件 ${safeFilePath}扩展名 ${ext} 不在推荐列表中。); } try { const content await fs.readFile(safeFilePath, utf-8); return { content: [{ type: text, text: content, }], // 可选提供资源的URI便于客户端后续引用 _meta: { uri: file://${safeFilePath}, }, }; } catch (error: any) { throw new Error(无法读取文件 ${safeFilePath}: ${error.message}); } }, }; // 导出所有工具的定义方便在主文件中注册 export const fileSystemTools [listDirectoryTool, readTextFileTool];接下来我们需要修改主文件src/index.ts以更优雅的方式注册和管理多个工具。我们将采用一个“工具注册表”的模式。// src/index.ts (更新版) import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { fileSystemTools } from ./tools/filesystem.js; // 定义工具处理器的类型 interface ToolDefinition { name: string; description: string; inputSchema: any; handler: (args: any) Promise{ content: Array{ type: string; text: string }; _meta?: any; }; } class McpServer { private server: Server; private tools: Mapstring, ToolDefinition new Map(); constructor() { this.server new Server( { name: enhanced-mcp-server, version: 1.0.0, }, { capabilities: { tools: {}, // 我们现在也声明支持资源下一步会实现 resources: {}, }, } ); this.setupRequestHandlers(); } // 注册工具到内部映射表 registerTool(tool: ToolDefinition) { if (this.tools.has(tool.name)) { throw new Error(工具名 ${tool.name} 已存在。); } this.tools.set(tool.name, tool); } // 批量注册工具 registerTools(toolList: ToolDefinition[]) { for (const tool of toolList) { this.registerTool(tool); } } private setupRequestHandlers() { // 处理 tools/list this.server.setRequestHandler( async () { const toolsList Array.from(this.tools.values()).map(({ name, description, inputSchema }) ({ name, description, inputSchema, })); return { tools: toolsList }; } ); // 处理 tools/call this.server.setRequestHandler( async (request) { if (request.method ! tools/call) { return undefined; } const { name, arguments: args } request.params; const tool this.tools.get(name); if (!tool) { throw new Error(工具 ${name} 未找到。); } // 调用工具对应的处理器 return await tool.handler(args || {}); } ); // 处理 resources/list 和 resources/read (暂时返回空下一节实现) this.server.setRequestHandler( async (request) { if (request.method resources/list) { return { resources: [] }; // 暂时没有资源 } return undefined; } ); } async start() { const transport new StdioServerTransport(); await this.server.connect(transport); console.error(增强版MCP服务器已启动。); } } // 启动逻辑 async function main() { const mcpServer new McpServer(); // 注册文件系统工具 mcpServer.registerTools(fileSystemTools); // 未来可以在这里注册更多工具模块 // mcpServer.registerTools(databaseTools); // mcpServer.registerTools(apiTools); await mcpServer.start(); } main().catch((error) { console.error(致命错误:, error); process.exit(1); });这种设计模式的优势在于模块化工具逻辑被封装在独立的模块中如filesystem.ts职责清晰易于维护和测试。集中管理主服务器类McpServer负责所有工具的注册、生命周期和请求路由。易于扩展要添加新工具只需创建新的模块并在main()函数中注册即可。4.2 实现动态资源Resources工具适合“操作”而资源适合“提供信息”。让我们实现一个动态资源一个显示当前服务器状态和环境的“仪表板”。资源的关键在于它有一个唯一的URI并且内容可以是动态生成的。我们在src/resources/下创建status.ts// src/resources/status.ts import os from os; /** * 系统状态资源 */ export const statusResource { // 资源的唯一标识符URI uri: dynamic://server/status, name: 服务器状态仪表板, description: 显示当前MCP服务器的运行状态、系统信息和环境变量。, mimeType: text/markdown, // 使用Markdown格式便于AI客户端渲染 // 一个返回资源内容的函数。每次读取请求都会调用。 getContent: async (): Promisestring { const now new Date().toISOString(); const uptime process.uptime(); const hours Math.floor(uptime / 3600); const minutes Math.floor((uptime % 3600) / 60); const seconds Math.floor(uptime % 60); const markdown # MCP 服务器状态报告 **生成时间:** ${now} ## 系统信息 - **主机名:** ${os.hostname()} - **平台:** ${os.platform()} (${os.arch()}) - **CPU核心数:** ${os.cpus().length} - **总内存:** ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)} GB - **可用内存:** ${(os.freemem() / 1024 / 1024 / 1024).toFixed(2)} GB - **系统负载 (1, 5, 15分钟):** ${os.loadavg().map(l l.toFixed(2)).join(, )} ## 进程信息 - **Node.js 版本:** ${process.version} - **进程ID:** ${process.pid} - **服务器运行时间:** ${hours}小时 ${minutes}分钟 ${seconds}秒 - **工作目录:** ${process.cwd()} ## 环境变量摘要 \\\ ${Object.keys(process.env) .filter(key key.startsWith(MCP_) || key NODE_ENV) .map(key ${key}${process.env[key]}) .join(\n)} \\\ --- *此资源由增强版MCP服务器动态生成。* ; return markdown; }, }; // 可以导出多个资源 export const serverResources [statusResource];现在我们需要更新主服务器类以支持资源的注册和列表/读取请求。修改src/index.ts中的McpServer类// 在 McpServer 类中添加 interface ResourceDefinition { uri: string; name: string; description?: string; mimeType: string; getContent: () Promisestring; } class McpServer { private server: Server; private tools: Mapstring, ToolDefinition new Map(); private resources: Mapstring, ResourceDefinition new Map(); // 新增资源映射表 // ... 构造函数保持不变 ... // 新增注册资源 registerResource(resource: ResourceDefinition) { if (this.resources.has(resource.uri)) { throw new Error(资源URI ${resource.uri} 已存在。); } this.resources.set(resource.uri, resource); } registerResources(resourceList: ResourceDefinition[]) { for (const resource of resourceList) { this.registerResource(resource); } } private setupRequestHandlers() { // ... tools/list 和 tools/call 处理器保持不变 ... // 更新 resources/list 处理器 this.server.setRequestHandler( async (request) { if (request.method resources/list) { const resourcesList Array.from(this.resources.values()).map(({ uri, name, description, mimeType }) ({ uri, name, description, mimeType, })); return { resources: resourcesList }; } // 处理 resources/read 请求 if (request.method resources/read) { const { uri } request.params; const resource this.resources.get(uri); if (!resource) { throw new Error(资源 ${uri} 未找到。); } const content await resource.getContent(); return { contents: [{ uri, mimeType: resource.mimeType, // 根据MIME类型内容可以是 text 或 blob text: content, }], }; } return undefined; } ); } // ... start 方法不变 ... }最后在main()函数中注册这个资源async function main() { const mcpServer new McpServer(); mcpServer.registerTools(fileSystemTools); mcpServer.registerResources(serverResources); // 注册资源 await mcpServer.start(); }现在当AI客户端连接到你的服务器时它不仅能调用list_directory和read_text_file工具还能发现并读取dynamic://server/status这个资源。AI可以主动获取服务器的状态信息作为上下文这对于诊断问题或生成包含环境信息的报告非常有用。注意事项资源URI的设计URI是资源的全局唯一标识。好的URI设计应具有层次性和描述性。例如dynamic://开头表示动态资源file://表示文件资源db://表示数据库资源。这有助于客户端理解和分类资源。5. 高级主题错误处理、日志与性能优化构建一个健壮的MCP服务器仅仅实现功能是不够的。我们需要考虑它在生产环境中的表现。5.1 结构化的错误处理在工具和资源处理器中我们使用了throw new Error()。SDK会捕获这些错误并将其转换为标准的JSON-RPC错误响应返回给客户端。但有时我们需要更细粒度的控制比如区分“用户输入错误”和“服务器内部错误”。我们可以定义一个自定义错误类并扩展请求处理器来提供更友好的错误信息。// src/errors.ts export class McpUserError extends Error { constructor(message: string, public code?: string) { super(message); this.name McpUserError; } } export class McpServerError extends Error { constructor(message: string, public originalError?: any) { super(message); this.name McpServerError; } } // 在工具处理器中使用 // 例如在 filesystem.ts 的 resolveSafePath 函数中 function resolveSafePath(userPath: string): string { // ... 路径解析逻辑 ... if (!normalizedPath.startsWith(/* ... */)) { // 使用自定义错误类 throw new McpUserError(访问路径 ${userPath} 被拒绝超出允许的工作区范围。, PATH_TRAVERSAL); } return normalizedPath; } // 在主服务器的 tools/call 处理器中可以添加更精细的错误处理 private setupRequestHandlers() { this.server.setRequestHandler( async (request) { if (request.method ! tools/call) return undefined; const { name, arguments: args } request.params; const tool this.tools.get(name); if (!tool) { // 返回一个结构化的“未找到”错误 return { isError: true, content: [{ type: text, text: 错误找不到名为 ${name} 的工具。, }], // 或者使用SDK的ErrorResponse这里演示自定义处理 }; } try { return await tool.handler(args || {}); } catch (error: any) { console.error(工具 ${name} 执行失败:, error); // 根据错误类型返回不同的信息 let errorMessage 执行工具 ${name} 时发生意外错误。; if (error instanceof McpUserError) { errorMessage 输入错误${error.message}; } // 注意实际返回给客户端的应该是标准JSON-RPC错误或包含错误信息的content。 // 这里简单返回一个错误内容。更佳实践是让SDK处理错误抛出。 return { content: [{ type: text, text: errorMessage, }], }; } } ); }5.2 日志记录与调试MCP服务器通常以后台进程运行良好的日志对于调试和监控至关重要。建议使用结构化的日志库如winston或pino。npm install winston创建src/logger.tsimport winston from winston; const logger winston.createLogger({ level: process.env.LOG_LEVEL || info, format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() // 结构化日志便于收集到ELK等系统 ), transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), }), new winston.transports.File({ filename: mcp-server-error.log, level: error }), new winston.transports.File({ filename: mcp-server-combined.log }), ], }); export default logger;然后在服务器各处使用它import logger from ./logger.js; // 在工具处理器中 handler: async (args) { logger.info(调用工具 read_text_file文件路径: ${args.filePath}); // ... 业务逻辑 ... } // 在主服务器启动时 async start() { logger.info(正在启动MCP服务器...); const transport new StdioServerTransport(); await this.server.connect(transport); logger.info(MCP服务器已成功启动并等待连接。); }5.3 性能优化与工具设计建议工具粒度工具应该保持“单一职责”。不要创建一个“do_everything”的工具。细粒度的工具如read_file,write_file,list_dir更易于AI理解、组合使用也更容易维护和测试。输入模式Schema是文档inputSchema不仅是参数验证器更是给AI的“说明书”。务必为每个属性提供清晰、准确的description。使用enum限制可选值使用pattern验证格式如日期、邮箱这能极大提高AI调用的准确性。异步操作与超时如果工具执行可能耗时较长如网络请求、复杂计算务必将其设计为异步并考虑在服务器层面或工具内部设置超时避免阻塞客户端。资源缓存对于动态资源如果内容生成成本高但更新不频繁可以考虑在服务器端实现缓存。例如状态资源可以每5秒更新一次而不是每次读取都重新生成。内存管理避免在工具或资源处理器中累积大量数据。特别是读取大文件或处理大数据集时使用流Stream或分页处理。6. 部署、配置与生态集成6.1 打包与发布为了便于分发和部署我们需要将TypeScript项目打包成一个独立的、可执行的包。编译为单一可执行文件可以使用pkg或nexe将Node.js项目打包成针对不同平台Windows, macOS, Linux的可执行文件。这样最终用户无需安装Node.js即可运行。npm install -g pkg # 在 package.json 中指定入口文件 # pkg: { scripts: dist/**/*.js } pkg . --targets node18-linux-x64,node18-win-x64,node18-macos-x64 --output dist/mcp-server创建配置文件将硬编码的配置如WORKSPACE_ROOT外置。可以使用环境变量或配置文件如config.yaml。// 从环境变量读取或从配置文件加载 const config { workspace: process.env.MCP_WORKSPACE, allowedFileExtensions: process.env.ALLOWED_EXT?.split(,) || [.txt, .md, ...], server: { name: process.env.SERVER_NAME || my-mcp-server, version: process.env.SERVER_VERSION || 1.0.0, }, };6.2 与Claude Desktop及其他客户端的集成我们已经介绍了如何通过编辑配置文件将自定义服务器添加到Claude Desktop。对于其他支持MCP的客户端如Cursor、Windsurf等集成方式类似通常都是在其配置文件中指定服务器的启动命令和参数。一个更专业的做法是将你的服务器发布为NPM包并提供一个全局可执行的命令。这样用户可以通过npm install -g your-mcp-server安装然后在客户端配置中直接使用命令名。在package.json中添加bin字段{ name: my-mcp-filesystem, version: 1.0.0, bin: { mcp-filesystem: ./dist/index.js } }用户安装后在Claude Desktop配置中就可以简化为{ mcpServers: { my-filesystem: { command: mcp-filesystem } } }6.3 安全考量沙箱与权限永远不要信任来自AI客户端的输入。像我们之前做的路径解析一样任何涉及文件系统、系统命令、网络访问的操作都必须进行严格的输入验证和权限限制。考虑在Docker容器或具有严格权限的独立用户环境中运行服务器。认证与授权高级对于需要访问敏感数据如生产数据库、内部API的服务器必须实现认证机制。MCP协议本身不强制规定认证方式。一种常见模式是服务器在启动时从环境变量或安全存储中读取访问令牌API Key并在调用外部服务时使用。绝对不要在工具的参数中传递明文密钥。审计日志记录所有工具调用和资源访问的日志包括时间、工具名、参数注意脱敏敏感参数、调用结果成功/失败。这对于安全审计和问题排查至关重要。7. 常见问题与排查技巧实录在实际开发和部署中你肯定会遇到各种问题。以下是我踩过的一些坑和解决方案。7.1 连接与通信问题问题服务器启动后Claude Desktop无法连接或者连接立即断开。检查点1STDIO传输确保你的服务器使用的是StdioServerTransport并且正确调用了server.connect(transport)。这是与桌面客户端通信的标准方式。检查点2配置文件路径Claude Desktop配置文件中指定的服务器命令路径必须是绝对路径。使用which node和pwd命令来确认Node.js和你的脚本的完整路径。检查点3日志输出将服务器的启动日志和错误日志输出到标准错误console.error。Claude Desktop有时会捕获这些日志并显示在调试信息中。确保你的服务器没有在启动初期就因为未捕获的异常而崩溃。检查点4端口冲突如果使用SSE如果你使用SSEServerTransport用于Web环境请检查指定的端口是否被占用。7.2 工具调用失败或AI无法识别工具问题AI客户端列出了你的工具但调用时失败或者AI根本“看不到”你的工具。检查点1工具定义格式仔细检查tools/list返回的JSON结构是否符合MCP协议规范。name,description,inputSchema字段必不可少且inputSchema必须是有效的JSON Schema。可以使用在线JSON Schema验证器检查。检查点2输入模式描述AI严重依赖description和properties中的description来理解工具。确保描述清晰、无歧义。例如对于date参数描述为“日期格式为 YYYY-MM-DD”比单纯“日期”要好得多。检查点3初始化握手服务器启动时会与客户端进行初始化握手交换name和version以及capabilities。确保你的capabilities对象中正确声明了tools: {}和resources: {}如果你提供了的话。声明缺失会导致客户端不向服务器查询相应列表。检查点4客户端缓存Claude Desktop等客户端可能会缓存服务器的能力列表。如果你修改了工具定义尝试重启客户端或者清除其缓存具体位置参考客户端文档。7.3 性能与稳定性问题问题服务器在处理某些请求时响应缓慢或者运行一段时间后内存占用过高。检查点1异步处理确保所有可能耗时的操作文件I/O、网络请求、数据库查询都是异步的使用async/await或返回Promise。同步操作会阻塞整个事件循环导致其他请求排队。检查点2错误边界在每个工具和资源处理器的外部使用try...catch确保单个请求的失败不会导致整个服务器进程崩溃。将未捕获的异常记录到日志并返回友好的错误信息。检查点3内存泄漏避免在全局变量或闭包中累积数据。特别是对于动态资源如果getContent函数引用了不断增长的数据结构会导致内存泄漏。使用WeakMap或定期清理缓存。检查点4负载测试对于复杂的工具可以编写简单的脚本模拟客户端进行高频调用观察服务器的CPU和内存使用情况。7.4 调试技巧独立测试脚本创建一个不依赖MCP客户端的测试脚本直接导入你的工具函数并进行单元测试。这能快速定位业务逻辑错误。// test-tool.js import { readTextFileTool } from ./dist/tools/filesystem.js; process.env.MCP_WORKSPACE /tmp/test-workspace; readTextFileTool.handler({ filePath: test.txt }).then(console.log).catch(console.error);协议层日志modelcontextprotocol/sdk本身可以提供详细的通信日志。在创建Server实例时可以尝试启用调试模式如果SDK支持或者通过设置环境变量NODE_DEBUG来查看底层通信。使用MCP Inspector社区有一些工具如mcp-inspector可以充当一个“中间人”记录服务器和客户端之间的所有JSON-RPC消息这对于调试协议级别的错误非常有用。简化复现当遇到问题时尝试创建一个最小复现代码Minimal Reproducible Example剥离所有复杂业务只保留最核心的服务器和问题工具定义。这能帮助你快速判断问题是出在你的代码、SDK还是客户端上。构建一个稳定、功能丰富的MCP服务器是一个迭代的过程。从最简单的“Hello World”开始逐步添加工具和资源并辅以严格的测试和清晰的文档你就能为AI世界创造出强大而可靠的工具扩展。这个SDK提供的坚实基础让这一切变得触手可及。

更多文章