news 2026/5/1 18:06:57

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript MCP SDK:为AI应用构建标准化工具调用服务器的完整指南

1. 项目概述:一个为AI应用注入“工具调用”能力的核心SDK

如果你正在构建一个需要与外部世界交互的AI应用,比如让AI帮你分析数据库、操作文件、调用API,那么你大概率会遇到一个核心问题:如何让AI模型安全、高效、标准化地使用这些工具?这正是MCP(Model 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的定位

在深入代码之前,我们必须先理解MCP(Model Context Protocol)协议到底在做什么。你可以把它想象成AI世界的“USB协议”。在USB协议出现之前,每个外设(鼠标、键盘、打印机)都需要自己的专用接口和驱动,混乱且低效。MCP协议的目标就是为AI模型(客户端)和外部工具(服务器)定义一个标准的“插口”和“通信语言”。

2.1 MCP协议的三大核心抽象

MCP协议主要定义了三种核心资源,这也是SDK中你需要操作的主要对象:

  1. 工具(Tools):这是最直接的能力。一个工具就是一个可以被AI调用的函数,它有明确的名称、描述、输入参数(JSON Schema定义)和输出。例如,“get_weather”工具,输入是{“city”: “string”},输出是天气信息的JSON。AI在需要时会请求调用这个工具,服务器执行并返回结果。

  2. 资源(Resources):代表可以被AI读取的静态或动态内容。资源有唯一的URI(如file:///path/to/doc.mddynamic://news/latest)、一个MIME类型和内容本身。AI客户端可以列出(list)和读取(read)资源。这非常适合用于向AI提供背景知识、文档、或实时数据流。例如,你可以提供一个company://metrics/daily资源,其内容是每日业务报表的Markdown文本。

  3. 提示词模板(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; // 如果不是我们处理的请求,返回undefined,SDK会传递给其他处理器或返回错误。 } 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.jsonscripts部分添加构建和启动命令:

{ "scripts": { "build": "tsc", "start": "node dist/index.js" } }

运行npm run build进行编译,然后使用npm start启动服务器。此时,服务器会挂起,等待通过STDIO接收客户端连接。

如何测试?单独运行这个服务器是看不到效果的,因为它需要和一个MCP客户端对话。最方便的测试方法是使用Claude Desktop

  1. 找到Claude Desktop的配置文件位置(macOS通常在~/Library/Application Support/Claude/claude_desktop_config.json,Windows在%APPDATA%\Claude\claude_desktop_config.json)。
  2. 在配置文件的mcpServers部分添加你的服务器配置。为了测试,我们可以直接指向编译后的JS文件。
{ "mcpServers": { "my-first-server": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/index.js"] } } }
  1. 重启Claude Desktop。
  2. 在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: Map<string, 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); });

这种设计模式的优势在于:

  1. 模块化:工具逻辑被封装在独立的模块中(如filesystem.ts),职责清晰,易于维护和测试。
  2. 集中管理:主服务器类McpServer负责所有工具的注册、生命周期和请求路由。
  3. 易于扩展:要添加新工具,只需创建新的模块,并在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 (): Promise<string> => { 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: () => Promise<string>; } class McpServer { private server: Server; private tools: Map<string, ToolDefinition> = new Map(); private resources: Map<string, 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_directoryread_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服务器通常以后台进程运行,良好的日志对于调试和监控至关重要。建议使用结构化的日志库,如winstonpino

npm install winston

创建src/logger.ts

import 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 性能优化与工具设计建议

  1. 工具粒度:工具应该保持“单一职责”。不要创建一个“do_everything”的工具。细粒度的工具(如read_file,write_file,list_dir)更易于AI理解、组合使用,也更容易维护和测试。

  2. 输入模式(Schema)是文档inputSchema不仅是参数验证器,更是给AI的“说明书”。务必为每个属性提供清晰、准确的description。使用enum限制可选值,使用pattern验证格式(如日期、邮箱),这能极大提高AI调用的准确性。

  3. 异步操作与超时:如果工具执行可能耗时较长(如网络请求、复杂计算),务必将其设计为异步,并考虑在服务器层面或工具内部设置超时,避免阻塞客户端。

  4. 资源缓存:对于动态资源,如果内容生成成本高但更新不频繁,可以考虑在服务器端实现缓存。例如,状态资源可以每5秒更新一次,而不是每次读取都重新生成。

  5. 内存管理:避免在工具或资源处理器中累积大量数据。特别是读取大文件或处理大数据集时,使用流(Stream)或分页处理。

6. 部署、配置与生态集成

6.1 打包与发布

为了便于分发和部署,我们需要将TypeScript项目打包成一个独立的、可执行的包。

  1. 编译为单一可执行文件:可以使用pkgnexe将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
  2. 创建配置文件:将硬编码的配置(如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 安全考量

  1. 沙箱与权限:永远不要信任来自AI客户端的输入。像我们之前做的路径解析一样,任何涉及文件系统、系统命令、网络访问的操作都必须进行严格的输入验证和权限限制。考虑在Docker容器或具有严格权限的独立用户环境中运行服务器。

  2. 认证与授权(高级):对于需要访问敏感数据(如生产数据库、内部API)的服务器,必须实现认证机制。MCP协议本身不强制规定认证方式。一种常见模式是服务器在启动时从环境变量或安全存储中读取访问令牌(API Key),并在调用外部服务时使用。绝对不要在工具的参数中传递明文密钥。

  3. 审计日志:记录所有工具调用和资源访问的日志,包括时间、工具名、参数(注意脱敏敏感参数)、调用结果(成功/失败)。这对于安全审计和问题排查至关重要。

7. 常见问题与排查技巧实录

在实际开发和部署中,你肯定会遇到各种问题。以下是我踩过的一些坑和解决方案。

7.1 连接与通信问题

问题:服务器启动后,Claude Desktop无法连接,或者连接立即断开。

  • 检查点1:STDIO传输:确保你的服务器使用的是StdioServerTransport并且正确调用了server.connect(transport)。这是与桌面客户端通信的标准方式。
  • 检查点2:配置文件路径:Claude Desktop配置文件中指定的服务器命令路径必须是绝对路径。使用which nodepwd命令来确认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严重依赖descriptionproperties中的description来理解工具。确保描述清晰、无歧义。例如,对于date参数,描述为“日期,格式为 YYYY-MM-DD”比单纯“日期”要好得多。
  • 检查点3:初始化握手:服务器启动时,会与客户端进行初始化握手,交换nameversion以及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 调试技巧

  1. 独立测试脚本:创建一个不依赖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);
  2. 协议层日志@modelcontextprotocol/sdk本身可以提供详细的通信日志。在创建Server实例时,可以尝试启用调试模式(如果SDK支持),或者通过设置环境变量NODE_DEBUG来查看底层通信。

  3. 使用MCP Inspector:社区有一些工具,如mcp-inspector,可以充当一个“中间人”,记录服务器和客户端之间的所有JSON-RPC消息,这对于调试协议级别的错误非常有用。

  4. 简化复现:当遇到问题时,尝试创建一个最小复现代码(Minimal Reproducible Example),剥离所有复杂业务,只保留最核心的服务器和问题工具定义。这能帮助你快速判断问题是出在你的代码、SDK还是客户端上。

构建一个稳定、功能丰富的MCP服务器是一个迭代的过程。从最简单的“Hello World”开始,逐步添加工具和资源,并辅以严格的测试和清晰的文档,你就能为AI世界创造出强大而可靠的工具扩展。这个SDK提供的坚实基础,让这一切变得触手可及。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 18:05:47

上市公司黑马程序员 | 2026 AI 学习指南:不同人群如何开启 AI 之路?

不同人群学习 AI 的痛点 在当今科技飞速发展的时代&#xff0c;AI 领域成为了众多人关注的焦点。然而&#xff0c;不同人群在学习 AI 时面临着不同的痛点。零基础的人不知从何入手&#xff0c;有一定编程基础的人想省力转型&#xff0c;应届生缺乏进入大模型领域的能力认知&…

作者头像 李华
网站建设 2026/5/1 18:05:40

对接Hermes Agent工具链,配置Taotoken自定义供应商的完整流程

对接Hermes Agent工具链&#xff0c;配置Taotoken自定义供应商的完整流程 1. 准备工作 在开始配置之前&#xff0c;请确保您已经拥有以下资源&#xff1a;一个有效的Taotoken API Key&#xff0c;以及安装好的Hermes Agent工具链。API Key可以在Taotoken控制台的「API密钥管理…

作者头像 李华
网站建设 2026/5/1 18:05:34

2026心理咨询机构排名揭晓:这些服务真的靠谱吗?

最近&#xff0c;一份“2026年心理咨询机构排行榜”在社交媒体上流传&#xff0c;引发了不少关注。作为一个经常与心理行业打交道的人&#xff0c;我决定从行业数据和真实案例出发&#xff0c;理性分析这些排名背后的可信度&#xff0c;并给出一些实操建议。1. 排名背后的“水分…

作者头像 李华
网站建设 2026/5/1 18:04:30

从零到一:手把手教你用Ansible搞定RHCE考试(附避坑指南)

从零到一&#xff1a;手把手教你用Ansible搞定RHCE考试&#xff08;附避坑指南&#xff09; 在当今IT运维领域&#xff0c;自动化已成为提升效率的关键。红帽认证工程师(RHCE)作为Linux领域的中级认证&#xff0c;近年来将考试重点全面转向Ansible自动化工具。对于许多备考者来…

作者头像 李华
网站建设 2026/5/1 18:02:51

双碳目标下的园区微电网:光储充+能耗管理的协同控制策略

一、能耗监测的“三大死穴”&#xff0c;90%的企业都在踩做工业自动化这么多年&#xff0c;我发现能耗管理的痛点逃不出这三个“死穴”&#xff1a;1. 设备“语言不通”&#xff1a;旧设备成了“数据孤岛”很多工厂的“能耗黑洞”藏在老设备里。比如我接触过的某汽配厂&#xf…

作者头像 李华
网站建设 2026/5/1 17:59:36

Proteus仿真DS18B20测温的3个常见坑:时序、负温度与LCD显示乱码解决

Proteus仿真DS18B20测温的3个实战陷阱与深度解决方案 当你在Proteus中搭建MSP430与DS18B20的温度监测系统时&#xff0c;是否遇到过温度读数忽高忽低、负值显示异常或者LCD1602屏幕出现乱码的情况&#xff1f;这些看似简单的故障背后&#xff0c;往往隐藏着单总线时序、数据格式…

作者头像 李华