MCP入门套件实战:快速构建AI应用数据连接工具
1. 项目概述:MCP入门套件,为你的AI应用注入“活数据”
如果你最近在折腾AI应用开发,特别是想给大语言模型(LLM)配上更强大的“手脚”,让它能操作你的数据库、读取你的文档,甚至控制你的智能家居,那你大概率已经听说过“Model Context Protocol”,也就是MCP。简单来说,MCP就像一套标准化的“插头”和“插座”规范,它定义了AI应用(比如ChatGPT、Claude Desktop)如何安全、高效地调用外部工具和数据源。而vinkius-labs/mcp-starter-kits这个项目,就是一套帮你快速上手MCP开发的“瑞士军刀”式入门套件。
想象一下,你有一个绝妙的想法:让Claude帮你分析公司上周的销售数据,并自动生成报告。数据在公司的PostgreSQL数据库里,报告模板在Google Docs上。没有MCP之前,你需要写一堆复杂的API桥接、处理认证、定义数据格式,头疼不已。有了MCP,数据库和Google Docs可以各自提供一个标准的“MCP服务器”,Claude这类AI应用通过一个“MCP客户端”就能以统一的方式调用它们。mcp-starter-kits的核心价值,就是帮你省去从零搭建“MCP服务器”和“MCP客户端”的繁琐基础工作,直接聚焦在你的业务逻辑上。它提供了多种编程语言(如TypeScript、Python)的模板和示例,无论你是前端全栈还是数据科学家,都能找到熟悉的起点,快速构建出能与主流AI应用无缝集成的数据工具。
这个项目解决的痛点非常明确:降低MCP生态的准入门槛。MCP协议本身很优秀,但直接阅读协议文档并实现一个兼容的服务器,涉及传输层(SSE或stdio)、协议序列化(JSON-RPC)、资源(Resources)和工具(Tools)的定义等,对新手来说有一定挑战。这个入门套件把这些底层复杂性都封装好了,你只需要像写普通后端API一样,定义“你有什么数据”(资源)和“你能做什么操作”(工具),剩下的通信、协议封装、生命周期管理,套件都帮你搞定了。接下来,我们就深入拆解一下,如何利用这套工具包,亲手打造一个属于自己的MCP工具。
2. 核心概念与项目架构解析
在动手写代码之前,我们必须先理清MCP模型中的几个核心角色,以及mcp-starter-kits是如何组织代码来对应这些角色的。理解这些,后续的配置和开发才会事半功倍。
2.1 MCP模型的三位主角:客户端、服务器与传输层
任何MCP交互都离不开这三个核心部分:
- MCP 服务器 (Server):这是数据或能力的提供方。比如,一个“天气预报服务器”可以提供今天的天气数据(资源),也可以提供“查询未来三天天气”的操作(工具)。服务器负责实现具体的业务逻辑。
- MCP 客户端 (Client):这是能力的消费方,通常是AI应用本身。例如Claude Desktop、Cursor IDE,它们内置了MCP客户端,用于发现并调用服务器提供的资源和工具。
- 传输层 (Transport):连接客户端和服务器的桥梁。MCP主要支持两种方式:
- stdio (标准输入输出):最常见于本地开发。服务器作为一个独立的进程启动,客户端通过标准输入(stdin)发送请求,通过标准输出(stdout)接收响应。这种方式简单、隔离性好。
- SSE (Server-Sent Events):适用于网络环境。服务器作为一个HTTP服务运行,客户端通过HTTP长连接接收服务器推送的事件。
mcp-starter-kits项目为服务器和客户端的开发都提供了样板。它的目录结构通常清晰地反映了这种分离。例如,你可能会看到servers/目录下有针对不同语言(如typescript-server/,python-server/)的模板,而clients/目录下可能有简单的演示性客户端代码。我们的开发重点,绝大多数时候都集中在MCP 服务器上。
2.2 入门套件代码结构一览
以TypeScript版本的服务器模板为例,一个典型的项目结构可能如下:
mcp-starter-kits/ ├── servers/ │ └── typescript-server/ │ ├── src/ │ │ ├── index.ts # 服务器主入口,初始化并启动MCP服务器 │ │ ├── tools/ # 存放“工具”定义模块 │ │ │ └── calculator.ts # 示例:一个计算器工具 │ │ └── resources/ # 存放“资源”定义模块 │ │ └── time.ts # 示例:一个提供当前时间的资源 │ ├── package.json │ ├── tsconfig.json │ └── build/ ├── clients/ │ └── simple-ts-client/ # 一个用于测试的简单客户端 └── README.md这个结构非常直观。src/index.ts是心脏,它使用@modelcontextprotocol/sdk或其他MCP SDK来创建一个服务器实例,然后将定义好的工具和资源注册进去。tools/和resources/目录是你大展拳脚的地方,你的业务逻辑就在这里实现。
一个关键的心得:刚开始接触时,很容易被“资源”和“工具”的概念绕晕。你可以这样理解:
- 资源 (Resource):是“名词”,是AI可以“读取”或“查看”的静态或动态数据。比如一个文件的内容、数据库的某张表、系统的CPU使用率。AI可以“获取”它。
- 工具 (Tool):是“动词”,是AI可以“执行”的操作。比如执行一个Shell命令、发送一封邮件、在数据库中插入一条记录。AI可以“调用”它,并传入参数。
很多功能既可以设计成资源,也可以设计成工具,这取决于你的场景。例如,“获取用户列表”可以是一个返回用户列表资源的接口,也可以是一个调用后返回用户列表的工具。通常,只读的、用于提供上下文信息的,适合定义为资源;需要改变状态、执行计算的,适合定义为工具。套件中的示例会很好地展示这两种模式。
3. 从零开始:构建你的第一个MCP服务器
理论说得再多,不如动手跑一遍。我们以最流行的 TypeScript 模板为例,带你一步步创建一个简单的“系统信息”MCP服务器,它可以告诉AI当前的时间(资源)和查询磁盘空间(工具)。
3.1 环境准备与项目初始化
首先,确保你的开发环境已经就绪:
- Node.js:版本18或以上。推荐使用nvm管理Node版本。
- 包管理器:npm或yarn、pnpm均可。
- 代码编辑器:VS Code,并安装TypeScript插件。
接下来,获取入门套件代码并安装依赖:
# 克隆项目仓库 git clone https://github.com/vinkius-labs/mcp-starter-kits.git cd mcp-starter-kits/servers/typescript-server # 安装依赖 npm install # 或使用 yarn/pnpm yarn install安装完成后,先别急着运行。用编辑器打开项目,重点查看package.json文件。你会发现核心依赖是@modelcontextprotocol/sdk,这是Anthropic官方维护的TypeScript SDK,封装了所有协议细节。另外,模板通常已经配置好了TypeScript编译脚本和开发热重载脚本(如npm run dev)。
3.2 定义你的第一个资源:动态时间服务
资源的目标是让AI能“读到”一些信息。我们来创建一个动态显示当前时间的资源。
- 创建资源文件:在
src/resources/目录下,新建一个文件systemTime.ts。 - 编写资源逻辑:
代码解读:// src/resources/systemTime.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallbackRequestHandler } from '@modelcontextprotocol/sdk/shared/requestHandler.js'; import { ListResourcesRequestSchema, ReadResourceRequestSchema, Resource, } from '@modelcontextprotocol/sdk/types.js'; // 定义一个唯一的资源URI模板 const RESOURCE_URI_TEMPLATE = 'time://current'; /** * 注册系统时间资源到MCP服务器 */ export function registerSystemTimeResource(server: Server) { // 1. 处理“列出资源”请求:告诉客户端本服务器提供了哪些资源 server.setRequestHandler(ListResourcesRequestSchema, async () => { const resource: Resource = { uri: RESOURCE_URI_TEMPLATE, name: 'Current System Time', description: '获取当前的系统日期和时间', mimeType: 'text/plain', // 返回纯文本格式 }; return { resources: [resource], }; }); // 2. 处理“读取资源”请求:当客户端请求读取特定资源时,返回实际内容 server.setRequestHandler(ReadResourceRequestSchema, async (request) => { // 检查请求的URI是否匹配我们提供的资源 if (request.params.uri !== RESOURCE_URI_TEMPLATE) { throw new Error(`Resource not found: ${request.params.uri}`); } // 生成当前时间字符串 const now = new Date(); const timeString = `当前系统时间(UTC): ${now.toISOString()}\n本地格式: ${now.toLocaleString()}`; // 返回资源内容 return { contents: [ { uri: request.params.uri, mimeType: 'text/plain', text: timeString, }, ], }; }); }- 我们定义了一个资源URI:
time://current。URI是资源的唯一标识符,可以自定义格式,但最好能清晰表达含义。 ListResourcesRequestSchema处理器:当AI客户端(如Claude)初次连接服务器时,会询问“你有什么资源?”。这个处理器返回一个资源描述列表。ReadResourceRequestSchema处理器:当AI想要“读取”这个时间资源时,会发起请求。我们在这里生成当前的日期时间字符串并返回。mimeType指定内容类型,这里是纯文本。
- 我们定义了一个资源URI:
3.3 创建你的第一个工具:磁盘空间查询
工具允许AI执行操作。我们来创建一个查询磁盘使用情况的工具。
- 创建工具文件:在
src/tools/目录下,新建一个文件diskSpace.ts。 - 编写工具逻辑:这里我们需要执行系统命令,可以使用Node.js的
child_process模块。
代码解读与注意事项:// src/tools/diskSpace.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // 工具定义 const DISK_SPACE_TOOL: Tool = { name: 'get_disk_space', description: '查询指定磁盘路径的可用空间和使用情况。', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '要查询的磁盘路径,例如 `/` 或 `C:\\`。默认为当前目录。', }, }, required: [], // path 不是必填项 }, }; /** * 注册磁盘空间查询工具到MCP服务器 */ export function registerDiskSpaceTool(server: Server) { // 1. 在服务器初始化时声明本工具(通常在别处通过server.setRequestHandler处理ListTools) // 这里我们主要关注工具的执行逻辑 // 2. 处理“调用工具”请求 server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== DISK_SPACE_TOOL.name) { // 如果不是本工具,则不应在此处理(应由其他工具处理器或默认逻辑处理) return; } const path = request.params.arguments?.path || '.'; let command: string; let parseOutput: (stdout: string) => string; // 根据操作系统选择命令 if (process.platform === 'win32') { command = `wmic logicaldisk where "DeviceID='${path.toUpperCase()}'" get Size,FreeSpace`; parseOutput = (stdout) => { const lines = stdout.trim().split('\n'); if (lines.length < 2) return `无法获取路径 ${path} 的磁盘信息。`; const values = lines[1].trim().split(/\s+/); const total = parseInt(values[0], 10); const free = parseInt(values[1], 10); const used = total - free; const usedPercent = ((used / total) * 100).toFixed(1); return `磁盘 ${path}:\n总空间: ${(total / 1e9).toFixed(2)} GB\n已用空间: ${(used / 1e9).toFixed(2)} GB\n可用空间: ${(free / 1e9).toFixed(2)} GB\n使用率: ${usedPercent}%`; }; } else { // Linux/macOS command = `df -h ${path}`; parseOutput = (stdout) => { const lines = stdout.trim().split('\n'); // 返回第一行标题和第二行数据 return lines.slice(0, 2).join('\n'); }; } try { const { stdout } = await execAsync(command); const resultText = parseOutput(stdout); return { content: [ { type: 'text', text: `磁盘空间查询成功:\n${resultText}`, }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: `查询磁盘空间失败:${error.message}`, }, ], isError: true, }; } }); } // 导出工具定义,方便在主文件中统一注册 export { DISK_SPACE_TOOL };- 工具定义 (
DISK_SPACE_TOOL):这是一个JSON Schema对象,定义了工具的名称、描述和输入参数。AI客户端会根据这个定义来生成调用界面和参数提示。inputSchema非常关键,它决定了AI如何理解和使用你的工具。 - 安全警告:这个工具会执行系统命令!在真实生产环境中,这是极高风险的操作。绝对不要在未经严格校验和授权的情况下,暴露执行任意命令的工具。这里仅为演示,实际应用中必须进行白名单校验、路径限制、用户权限控制等。
- 平台兼容性:我们通过
process.platform判断操作系统,执行不同的命令(wmic用于Windows,df用于Unix-like系统)。这是编写跨平台工具时常见的做法。 - 错误处理:使用
try-catch包裹命令执行,并将错误信息通过isError: true标记返回给客户端,这样AI能知道操作失败了。
- 工具定义 (
3.4 组装并启动服务器
现在我们需要把资源和工具“安装”到服务器上,并启动它。
- 修改主文件 (
src/index.ts):// src/index.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { registerSystemTimeResource } from './resources/systemTime.js'; import { registerDiskSpaceTool, DISK_SPACE_TOOL } from './tools/diskSpace.js'; // 创建MCP服务器实例 const server = new Server( { name: 'system-info-server', // 你的服务器名称 version: '0.1.0', }, { capabilities: { resources: {}, // 声明支持资源功能 tools: {}, // 声明支持工具功能 }, } ); // 注册资源处理器 registerSystemTimeResource(server); // 注册工具处理器 registerDiskSpaceTool(server); // 处理工具列表请求(需要将我们定义的工具告知客户端) server.setRequestHandler( { method: 'tools/list' }, async () => ({ tools: [DISK_SPACE_TOOL], }) ); // 处理连接初始化 server.setRequestHandler( { method: 'initialize' }, async () => ({ protocolVersion: '2024-11-05', // 使用最新的协议版本 capabilities: server.capabilities, serverInfo: { name: server.info.name, version: server.info.version, }, }) ); // 创建stdio传输层并连接 const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP System Info Server running on stdio...'); - 编译与运行:
运行后,服务器会进入等待状态,监听标准输入。它自己不会输出任何东西到控制台(除了初始的日志),因为它现在是一个“后台服务”,等待MCP客户端(比如Claude Desktop)来连接它。# 编译TypeScript代码 npm run build # 运行服务器(通过stdio传输) node build/index.js
4. 连接与测试:让你的服务器被AI使用
服务器跑起来了,但怎么让Claude或其它AI应用知道它呢?这就需要配置MCP客户端。
4.1 配置Claude Desktop集成(以Mac/Linux为例)
Claude Desktop是目前最常用的MCP客户端之一。它通过一个配置文件来管理可连接的MCP服务器。
找到配置文件路径:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json如果文件或目录不存在,可以手动创建。
- macOS:
编辑配置文件:将你的服务器作为一个新的“mcpServers”项添加进去。
{ "mcpServers": { "system-info": { "command": "node", "args": [ "/ABSOLUTE/PATH/TO/YOUR/mcp-starter-kits/servers/typescript-server/build/index.js" ] } } }关键点:
"system-info"是你给这个服务器起的名字,可以自定义。"command"是启动服务器的命令,这里是node。"args"是命令的参数,必须使用绝对路径指向你编译好的JS文件。- 确保你已通过
npm run build成功编译。
重启Claude Desktop:保存配置文件后,完全退出并重新启动Claude Desktop应用。
4.2 在Claude中验证与使用
重启Claude后,你的服务器就应该自动连接了。
- 验证连接:在Claude的聊天框中,你可以尝试问:“你现在有哪些可用的工具?” 或者 “你能访问哪些资源?”。Claude应该会列出
get_disk_space工具和time://current资源。 - 测试资源:尝试让Claude“读取当前时间”。你可以说:“告诉我现在的系统时间。” Claude会调用你的资源处理器,并返回你编写的动态时间字符串。
- 测试工具:尝试让Claude“查询根目录的磁盘空间”。你可以说:“检查一下根目录
/的磁盘使用情况。” Claude会调用你的工具,传入{“path”: “/”}参数,并返回df -h /命令的结果。
成功标志:Claude能够正确理解你的指令,并返回符合预期的结果。如果失败,Claude通常会返回一个错误信息,提示“工具调用失败”或“资源未找到”,这时就需要去检查服务器的日志(如果你在开发模式下运行,可能会有错误输出到控制台)和配置文件路径。
4.3 使用附带的测试客户端进行调试
mcp-starter-kits项目里通常包含一个简单的测试客户端(可能在clients/目录下)。在服务器开发阶段,用这个客户端调试比反复重启Claude要高效得多。
运行测试客户端(如果项目提供):
# 假设在 clients/simple-ts-client 目录下 cd clients/simple-ts-client npm install npm start -- ../servers/typescript-server/build/index.js这个客户端会通过stdio启动你的服务器,并提供一个简单的REPL界面或自动执行一些测试请求。
查看原始协议消息:测试客户端的一个巨大优势是能打印出原始的MCP协议请求和响应JSON。这对于调试协议层面的错误(比如JSON格式不对、方法名错误)至关重要。当Claude只是模糊地报错时,查看这里的日志能立刻定位问题。
5. 进阶开发:构建实用的MCP服务器
掌握了基础之后,我们可以尝试构建更复杂、更实用的服务器。这里以两个常见场景为例:连接数据库和操作本地文件系统。
5.1 场景一:构建数据库查询服务器
让AI能安全地查询你的数据库,是MCP一个非常强大的应用。我们以SQLite为例(因为它无需安装额外服务),构建一个工具。
安装依赖:
cd servers/typescript-server npm install sqlite3 better-sqlite3这里选择
better-sqlite3,因为它提供同步API,在MCP的异步处理模型中更简单。创建数据库工具文件 (
src/tools/database.ts):import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'; import Database from 'better-sqlite3'; // 定义数据库文件路径(示例,实际应从配置读取) const DB_PATH = './example.db'; const DB_QUERY_TOOL: Tool = { name: 'query_database', description: '在示例数据库上执行安全的只读SQL查询。仅支持SELECT语句。', inputSchema: { type: 'object', properties: { sql: { type: 'string', description: '要执行的SQL SELECT查询语句。', }, }, required: ['sql'], }, }; export function registerDatabaseTool(server: Server) { const db = new Database(DB_PATH, { readonly: true }); // 以只读模式打开,增加安全性 server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== DB_QUERY_TOOL.name) { return; } const sql = request.params.arguments?.sql; if (!sql || typeof sql !== 'string') { throw new Error('SQL query is required and must be a string.'); } // 基础SQL注入防护:确保是SELECT语句(简易版,生产环境需要更严格的解析) const trimmedSql = sql.trim().toUpperCase(); if (!trimmedSql.startsWith('SELECT')) { return { content: [{ type: 'text', text: '错误:只允许执行SELECT查询语句。', }], isError: true, }; } try { const stmt = db.prepare(sql); const results = stmt.all(); // 获取所有结果 const resultText = JSON.stringify(results, null, 2); // 美化JSON输出 return { content: [{ type: 'text', text: `查询成功,返回 ${results.length} 条记录:\n\`\`\`json\n${resultText}\n\`\`\``, }], }; } catch (error: any) { return { content: [{ type: 'text', text: `数据库查询失败:${error.message}`, }], isError: true, }; } }); } export { DB_QUERY_TOOL };核心安全考量:
- 只读模式:打开数据库时使用
{ readonly: true },防止数据被意外修改或删除。 - 输入校验:虽然只是简单检查是否以SELECT开头,但在生产环境中,必须使用更严格的SQL解析器或查询构建器来限制可访问的表和字段,避免敏感数据泄露。
- 错误隔离:用try-catch包裹数据库操作,防止服务器因SQL错误而崩溃。
- 只读模式:打开数据库时使用
在主文件中注册此工具,然后你就可以让Claude帮你查询数据了:“查询一下用户表中所有活跃用户的信息。”
5.2 场景二:构建文件系统浏览器(资源示例)
让AI能读取指定目录下的文件列表作为上下文,也很有用。我们可以将其设计为一个资源。
- 创建文件列表资源 (
src/resources/fileList.ts):
核心安全考量:import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { ListResourcesRequestSchema, ReadResourceRequestSchema, Resource, } from '@modelcontextprotocol/sdk/types.js'; import fs from 'fs/promises'; import path from 'path'; const RESOURCE_URI_PREFIX = 'filelist://'; export function registerFileListResource(server: Server) { server.setRequestHandler(ListResourcesRequestSchema, async (request) => { // 这里可以动态生成资源列表,例如基于某个配置的根目录 // 为了简单,我们只提供一个静态资源URI模板 const resource: Resource = { uri: `${RESOURCE_URI_PREFIX}{path}`, name: 'Directory File List', description: '列出指定目录下的文件和文件夹。使用示例:filelist:///Users/name/Documents', mimeType: 'application/json', }; return { resources: [resource] }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; if (!uri.startsWith(RESOURCE_URI_PREFIX)) { throw new Error(`Unsupported resource URI: ${uri}`); } const dirPath = uri.slice(RESOURCE_URI_PREFIX.length); if (!dirPath) { throw new Error('Directory path is required in the URI.'); } // 安全检查:限制可访问的路径范围(极其重要!) const ALLOWED_BASE = process.env.ALLOWED_FILE_PATH || '/tmp'; // 示例:只允许访问/tmp目录 const resolvedPath = path.resolve(dirPath); if (!resolvedPath.startsWith(path.resolve(ALLOWED_BASE))) { throw new Error(`Access to path ${resolvedPath} is not allowed.`); } try { const items = await fs.readdir(resolvedPath, { withFileTypes: true }); const fileList = items.map((item) => ({ name: item.name, type: item.isDirectory() ? 'directory' : 'file', path: path.join(resolvedPath, item.name), })); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(fileList, null, 2), }], }; } catch (error: any) { throw new Error(`Failed to read directory: ${error.message}`); } }); }- 路径限制:这是重中之重。绝对不能让AI通过你的服务器访问任意文件路径(如
/etc/passwd)。这里通过ALLOWED_BASE环境变量定义了一个白名单基础目录,所有请求路径都必须在此目录下。生产环境中,这个白名单应该配置得非常严格。 - 路径解析:使用
path.resolve()来规范化路径,防止目录遍历攻击(如../../../etc/passwd)。
- 路径限制:这是重中之重。绝对不能让AI通过你的服务器访问任意文件路径(如
6. 生产环境部署与安全加固指南
开发调试完成,想要分享给团队或长期运行,就需要考虑部署和安全问题。
6.1 部署模式选择
- 本地Stdio模式:这是Claude Desktop默认的集成方式,适合个人使用。服务器作为子进程由客户端启动。优点是简单、隔离。缺点是每个用户都需要在本地安装和运行服务器进程。
- SSE服务器模式:将你的MCP服务器部署为一个独立的HTTP服务(使用SSE传输)。客户端通过URL连接。
- 优点:集中部署,团队共享;可以更好地管理认证、监控和更新。
- 实现:你需要使用SDK的
SSEServerTransport。mcp-starter-kits可能提供了相关示例。服务器需要处理HTTP请求和SSE长连接。 - 安全:必须添加认证(如API Key),否则你的服务器将对互联网开放。
6.2 安全加固清单
将MCP服务器暴露给AI,相当于赋予AI一部分系统能力,安全必须放在第一位。
- 认证与授权:
- SSE模式:必须实现API Key、JWT Token等认证机制。在初始化握手阶段验证客户端。
- Stdio模式:相对安全,因为进程在本地运行。但仍需确保服务器代码本身没有远程执行漏洞。
- 输入验证与消毒:
- 对所有来自客户端的输入(工具参数、资源URI)进行严格校验。
- 使用白名单机制,限制可访问的文件路径、可执行的命令、可查询的数据库表。
- 对SQL查询,使用参数化查询或ORM,绝不拼接字符串。
- 权限最小化:
- 运行服务器的操作系统用户应具有最小必要权限。不要用root或管理员账户运行。
- 数据库连接使用只读账号。
- 文件系统访问限制在特定沙箱目录。
- 错误处理与日志:
- 避免在错误信息中泄露内部路径、栈跟踪等敏感信息。返回给AI的错误信息应通用化。
- 在服务器端记录详细的审计日志(谁、什么时候、执行了什么操作),便于事后追溯。
- 依赖安全:
- 定期更新
@modelcontextprotocol/sdk和其他依赖库,修复已知漏洞。 - 使用
npm audit或类似工具扫描依赖。
- 定期更新
6.3 性能与可维护性优化
- 连接管理:对于SSE服务器,需要妥善管理多个并发连接,避免内存泄漏。
- 资源清理:确保数据库连接、文件句柄等在工具调用结束后被正确关闭。
- 配置化:将服务器名称、版本、允许的路径、数据库连接字符串等提取到环境变量或配置文件中(如
config.yaml或.env),便于不同环境部署。 - 健康检查:为SSE服务器添加一个
/health端点,供监控系统检查服务状态。
7. 常见问题与故障排除实录
在实际开发和集成过程中,你肯定会遇到各种问题。下面是我踩过的一些坑和解决方案。
7.1 连接与配置问题
问题1:Claude Desktop重启后,找不到我的服务器/工具。
- 检查:首先确认
claude_desktop_config.json文件路径和格式完全正确。JSON格式非常严格,多一个逗号或少一个引号都会导致整个配置被忽略。 - 检查:确保配置中
args里的JS文件路径是绝对路径,并且该文件确实存在(已成功编译)。 - 检查:查看Claude Desktop的日志。在macOS上,可以在终端运行
log stream --predicate 'subsystem == "com.anthropic.Claude-Desktop"'来查看实时日志,里面常有连接失败的详细原因。 - 终极方案:使用项目自带的测试客户端先验证你的服务器是否能正常启动和响应基本请求,排除服务器本身的问题。
问题2:服务器启动后立即退出,或客户端报“进程意外退出”。
- 检查:在终端直接运行
node build/index.js,查看是否有未捕获的异常或语法错误。通常是因为依赖未安装、TypeScript编译错误或代码中有运行时错误。 - 检查:确保你的服务器代码正确处理了
initialize请求,并返回了正确的protocolVersion和capabilities。这是握手的第一步,失败会导致连接立即关闭。
7.2 协议与通信问题
问题3:AI说“调用工具失败”,但没有具体错误。
- 调试:使用测试客户端。它能显示原始的请求和响应JSON,这是最有效的调试手段。对比你的工具返回的JSON结构是否符合 MCP协议规范 。
- 常见错误:
- 工具调用返回的JSON中,
content字段不是数组。 content数组中的对象缺少type或text字段。- 工具名称在
CallToolRequestSchema处理器中没有被正确匹配。
- 工具调用返回的JSON中,
- 排查:在服务器的工具处理器开头添加
console.error(JSON.stringify(request, null, 2));,将请求打印到标准错误输出(Claude Desktop可能会捕获并显示),看看请求是否真的到达了你的处理器。
问题4:资源可以列出,但读取时返回“未找到”。
- 检查:
ReadResourceRequestSchema处理器中,对request.params.uri的判断逻辑是否正确。确保与ListResourcesRequestSchema处理器中返回的uri完全一致。 - 注意:URI是大小写敏感的。
7.3 安全与权限问题
问题5:工具执行系统命令被拒绝(Permission denied)。
- 分析:Node.js子进程以运行服务器的用户身份执行命令。确保该用户对要执行的命令和涉及的文件路径有执行和读取权限。
- 建议:如非必要,避免开发需要高权限的系统工具。如果必须,考虑使用更安全的方式,如通过一个具有严格白名单的中间脚本去执行。
问题6:担心SQL注入或路径遍历。
- 重申:这必须通过代码逻辑解决。不要信任任何来自AI的输入。
- SQL:使用参数化查询(
db.prepare(‘SELECT * FROM users WHERE id = ?’).get(userId))或查询构建器。 - 路径:使用
path.resolve()解析后,与一个预先定义好的白名单基础路径进行前缀比较,严格限制访问范围。
- SQL:使用参数化查询(
7.4 性能问题
问题7:工具调用响应慢,特别是涉及网络或复杂计算时。
- 优化:MCP协议是同步请求-响应模型,AI会等待你的工具返回。如果操作耗时较长(>10秒),可能会导致客户端超时。
- 策略:
- 异步通知:对于超长任务,可以让工具立即返回一个“任务已提交”的消息,然后通过其他方式(如另一个资源)让AI后续查询结果。但这需要更复杂的设计。
- 优化工具本身:检查你的工具实现是否有性能瓶颈,如低效的数据库查询、未使用索引等。
- 设置超时:在服务器端为工具执行设置超时限制,避免一个慢请求拖死整个服务器进程。
开发MCP服务器的过程,是一个在“赋予AI强大能力”和“确保系统安全可控”之间不断权衡的过程。vinkius-labs/mcp-starter-kits提供了一个坚实的起点,让你能快速跨越协议实现的复杂性,直接专注于创造有价值的功能。从今天起,试着将你日常工作中那些重复、繁琐的查询和操作封装成MCP工具,你会发现,一个能直接理解你需求并操作你数字世界的AI助手,其效率提升是颠覆性的。
