15分钟构建本地MCP服务器:为AI智能体打造安全可控的“手和眼”
1. 为什么你需要一个本地MCP服务器?
如果你最近在折腾AI智能体,尤其是像Claude Code这类能帮你写代码、分析项目的工具,你可能会发现一个痛点:它很聪明,但有时候像个“断手”的天才。它能构思出完美的方案,却没法直接帮你执行——比如,它想读取你项目里某个配置文件的内容,或者调用一个你公司内部的API来获取数据。这时候,Model Context Protocol,也就是MCP,就登场了。你可以把它理解成AI智能体的“手”和“眼睛”,一个标准化的协议,让AI能安全、可控地使用你电脑上的工具和数据。
市面上,像Anthropic官方提供的一些MCP服务器(比如文件浏览器、网络搜索)确实方便,开箱即用。但用久了,你就会碰到天花板。这也是我决定自己动手搞一个本地MCP服务器的根本原因。这绝不只是为了“炫技”,而是实打实地为了解决下面这几个核心问题:
数据隐私与安全是头等大事。当你使用云端MCP服务时,你的文件路径、数据库查询语句、甚至是业务数据,都可能需要离开你的本地环境。对于处理敏感代码、内部文档或客户信息的场景,这无异于在钢丝上跳舞。本地部署意味着所有数据交互都发生在你的机器内存里,数据不出境,心里才踏实。
定制化工具是效率的灵魂。通用的工具只能解决通用问题。但我们的工作流往往是独特的。比如,我需要我的AI助手能一键运行项目特定的测试脚本、能查询我们团队内部的文档知识库、或者能按照我们公司的代码规范自动格式化某个目录。这些需求,通用的服务器不可能满足。自己搭建,就意味着你可以打造一套完全贴合你个人或团队工作习惯的“瑞士军刀”。
摆脱网络依赖与速率限制。网络不稳定或者API调用次数受限,都会让流畅的AI协作体验瞬间卡壳。本地服务器运行在localhost,延迟几乎可以忽略不计,而且你想调用多少次就调用多少次,不用担心配额问题。离线环境下,你依然可以让AI助手帮你分析本地代码库,这种自由感是云端服务无法给予的。
深入理解与掌控。作为开发者,使用一个黑盒工具总让人有些不安。自己实现一遍MCP服务器,你会彻底明白AI智能体与外部世界通信的机制。这份理解,能让你更自信地设计工具,更高效地排查问题,甚至能启发你创造出新的应用模式。
所以,构建本地MCP服务器,本质上是一次“赋权”。你把能力的边界从AI模型本身,扩展到了你所能触及的整个数字环境。接下来,我就带你看看这背后的架构是如何支撑起这种扩展的。
1.1 MCP架构:智能体与世界的桥梁
MCP的架构设计得非常精巧且克制,它没有试图做一件复杂的事情,而是定义了一套清晰的“对话规则”。我们可以把它想象成智能体(如Claude Code)和你本地资源之间的一个标准化适配器或协议翻译器。
在这个架构中,主要有三个角色:
- AI智能体 (Client):比如Claude Code,它是发起请求的一方,想要“做某事”。
- MCP服务器 (Server):我们即将要构建的东西。它监听智能体的请求,并将其翻译成对本地资源的实际操作。
- 传输层 (Transport):负责在智能体和服务器之间传递消息。最常见的是
stdio(标准输入输出),也就是通过命令行管道通信,简单又高效。
协议的核心是围绕“工具(Tools)”和“资源(Resources)”这两个概念展开的。简单来说:
- 工具:代表一个可执行的操作。比如“读取文件”、“执行Shell命令”、“调用某API”。智能体可以列出所有可用工具,然后调用其中一个。
- 资源:代表可读取的数据源。比如“一个URI指向的文本文件内容”、“一个数据库查询的结果流”。智能体可以读取这些资源来获取上下文。
我们这次构建的,主要是一个“工具服务器”。它的工作流程可以概括为:
- 智能体启动时,连接到我们指定的MCP服务器。
- 智能体问服务器:“嘿,你都能干嘛?”(即调用
tools/list请求)。 - 服务器回复一个清单:“我能干A、B、C这几件事。”
- 当用户要求智能体做某件事时,智能体判断:“这事需要调用服务器的工具C。” 于是它向服务器发送“请执行工具C,这是参数”的请求(即调用
tools/call)。 - 服务器收到请求,在本地执行对应的代码(比如真的去读文件、跑脚本),然后将结果或错误信息返回给智能体。
- 智能体将结果融入它的思考,回复给用户。
这个架构的美妙之处在于解耦。AI智能体不需要知道你的文件系统具体怎么访问,你的数据库密码是什么,它只需要知道有一个叫read_file的工具。而你的服务器则掌管所有具体的、可能有危险的操作,并可以施加严格的权限控制。下面,我们就进入实战环节,从零开始搭建这个桥梁。
2. 15分钟快速上手:构建你的第一个MCP服务器
理论说得再多,不如动手跑通。我保证,只要你按照下面的步骤来,15分钟内你就能看到一个能工作的本地MCP服务器,并让Claude Code与之对话。我们的目标是先“跑起来”,获得正反馈,再谈优化和深入。
2.1 环境准备与项目初始化
首先,确保你的机器上有Node.js(版本18或以上)和npm。打开你的终端,我们开始。
# 1. 创建一个新的项目目录并进入 mkdir my-first-mcp-server && cd my-first-mcp-server # 2. 初始化一个新的Node.js项目,一路回车用默认值即可 npm init -y # 3. 安装MCP SDK依赖。这是Anthropic官方提供的JavaScript/TypeScript SDK,封装了协议细节,让我们能专注于工具逻辑。 npm install @modelcontextprotocol/sdk # 4. 安装TypeScript及相关类型定义(如果你用纯JavaScript可跳过,但强烈推荐TS) npm install --save-dev typescript @types/node npx tsc --init # 生成tsconfig.json配置文件现在,你的package.json里应该已经有了@modelcontextprotocol/sdk这个依赖。项目结构非常简单,我们只需要一个主文件。
2.2 核心代码:一个最小的“回声”服务器
我们来创建一个最简单的服务器,它只提供一个工具:echo。这个工具接收一段文本,然后原样返回。虽然简单,但它完整演示了定义工具、处理请求的整个流程。
在你的项目根目录下,创建文件server.ts,并写入以下代码:
// 导入MCP SDK的核心类 import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/transport/stdio.js'; // 1. 创建Server实例 // 第一个参数是元数据:服务器名字和版本,会展示给智能体。 // 第二个参数是能力声明:这里我们声明此服务器提供`tools`(工具)能力。 const server = new Server( { name: 'my-echo-server', version: '1.0.0', }, { capabilities: { tools: {}, // 声明支持工具功能 }, } ); // 2. 定义工具列表 // 当智能体查询“你有什么工具”时,我们返回这个列表。 server.setRequestHandler('tools/list', async () => { return { tools: [ { name: 'echo', // 工具的唯一标识符,调用时使用 description: '返回你输入的文本。这是一个简单的测试工具。', inputSchema: { // 定义工具需要的参数JSON Schema type: 'object', properties: { message: { type: 'string', description: '你想要回声的文本', }, }, required: ['message'], // 指定必填参数 }, }, ], }; }); // 3. 处理工具调用 // 当智能体决定调用`echo`工具时,执行这里的逻辑。 server.setRequestHandler('tools/call', async (request) => { // 请求体中包含了工具名和参数 if (request.params.name !== 'echo') { // 如果不是我们认识的工具,抛出错误 throw new Error(`未知工具: ${request.params.name}`); } // 从参数中取出我们定义的`message`字段 const { message } = request.params.arguments as { message: string }; // 执行“业务逻辑”——这里就是简单的返回 // 返回的内容必须符合MCP的调用结果格式 return { content: [ { type: 'text', text: `服务器回声:${message}`, }, ], }; }); // 4. 启动服务器,使用标准输入输出进行通信 const transport = new StdioServerTransport(); server.connect(transport).then(() => { // 这个日志在stdio传输下可能看不到,但这里表示服务器已就绪 console.error('MCP服务器已启动,正在等待连接...'); });注意:上面代码中
console.error的使用是有意为之。在stdio传输模式下,智能体通过标准输入(stdin)和标准输出(stdout)与服务器通信。如果我们用console.log,输出会混入协议消息流,导致通信混乱。将日志输出到标准错误(stderr)是安全的,不会干扰主协议。这是一个非常关键的实操细节。
代码解析完毕,它做了四件事:创建服务器、声明工具、实现工具逻辑、启动监听。接下来,我们需要让它能被Claude Code调用。
2.3 连接Claude Code:让智能体“长出手脚”
Claude Code(或其他兼容MCP的客户端)需要通过命令行参数来连接我们的本地服务器。我们需要将TypeScript代码编译成JavaScript,或者直接使用ts-node来运行。
方法一:使用ts-node(推荐,适合开发)确保你全局安装了ts-node:npm install -g ts-node。然后,在启动Claude Code时指定MCP服务器路径:
# 假设你的server.ts文件在当前目录 claude --mcp ./server.ts # 或者某些版本可能用 --mcp-path # claude --mcp-path ./server.ts方法二:编译后运行首先编译TypeScript:
npx tsc server.ts --outDir dist --module commonjs --target es2020然后指向编译后的JS文件:
claude --mcp ./dist/server.js当你以上述方式启动Claude Code后,神奇的事情就发生了。你可以在对话中尝试说:“请调用echo工具,说一声你好世界。” Claude Code会识别出可用的工具,并询问你是否要调用。你确认后,它就会通过MCP协议将请求发送给你的本地服务器,服务器执行echo逻辑并返回结果,Claude Code再将结果呈现给你。
至此,一个完整的本地MCP服务器从编写到连接的闭环就完成了。你应该能在15分钟内体验到这种“赋能”的感觉。但这只是个开始,一个只会回声的服务器用处不大。接下来,我们要为它注入真正实用的能力。
3. 从玩具到利器:实现实用工具
一个echo工具证明了通路,但要让MCP服务器真正产生价值,我们需要它操作真实的系统资源。下面,我将实现几个最常用、也最能体现本地服务器优势的工具:文件操作、Shell命令执行和自定义API调用。在实现过程中,我会穿插讲解安全考量和性能技巧。
3.1 文件系统操作:赋予智能体“阅读”能力
让AI能安全地读取你项目下的文件,是最高频的需求之一。我们不能让它随意读取整个硬盘,所以需要设计带边界和权限检查的工具。
我们创建一个新的服务器文件file_server.ts,实现一个read_file工具。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/transport/stdio.js'; import * as fs from 'fs/promises'; import * as path from 'path'; // 定义一个允许访问的根目录,比如限制在当前项目内 const ALLOWED_BASE_DIR = process.cwd(); // 当前工作目录 // 或者更严格:path.resolve(__dirname, '..'); const server = new Server( { name: 'file-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); server.setRequestHandler('tools/list', async () => ({ tools: [{ name: 'read_file', description: '读取指定路径的文本文件内容。路径必须是当前项目目录或其子目录下的相对路径。', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: '相对于项目根目录的文件路径,例如:src/config.json', }, }, required: ['filePath'], }, }], })); server.setRequestHandler('tools/call', async (request) => { if (request.params.name !== 'read_file') { throw new Error(`未知工具: ${request.params.name}`); } const { filePath } = request.params.arguments as { filePath: string }; // !!! 关键安全步骤:路径解析与校验 !!! // 1. 解析用户输入的相对路径 const requestedPath = path.resolve(ALLOWED_BASE_DIR, filePath); // 2. 计算规范化后的绝对路径 const normalizedPath = path.normalize(requestedPath); // 3. 检查请求的路径是否在以允许目录为前缀的范围内 if (!normalizedPath.startsWith(path.normalize(ALLOWED_BASE_DIR) + path.sep)) { throw new Error(`安全违规:禁止访问路径 ${filePath}。仅允许访问项目目录下的文件。`); } // 4. 可选:检查路径是否试图向上回溯(防御 path/../../../etc/passwd 这种攻击) const relative = path.relative(ALLOWED_BASE_DIR, normalizedPath); if (relative.startsWith('..') || path.isAbsolute(relative)) { throw new Error(`安全违规:路径 ${filePath} 试图访问项目外部目录。`); } try { // 5. 安全地读取文件 const content = await fs.readFile(normalizedPath, 'utf-8'); return { content: [{ type: 'text', text: `文件 **${filePath}** 的内容:\n\`\`\`\n${content}\n\`\`\``, // 可以返回更结构化的数据,这里用Markdown格式让AI更好呈现 }], }; } catch (error: any) { // 6. 友好的错误处理 if (error.code === 'ENOENT') { throw new Error(`文件未找到:${filePath}`); } if (error.code === 'EACCES') { throw new Error(`权限不足,无法读取文件:${filePath}`); } throw new Error(`读取文件失败:${error.message}`); } }); const transport = new StdioServerTransport(); server.connect(transport).then(() => { console.error('文件MCP服务器已启动。'); });实操心得:安全是重中之重
- 永远不要相信用户输入:即使来自AI的请求,也要将
filePath视为不可信输入。必须进行严格的路径校验,防止目录遍历攻击。- 使用
path.resolve和path.normalize:它们能帮你处理./、../等符号,得到标准的绝对路径,便于进行startsWith前缀检查。- 明确权限边界:一开始就定义好
ALLOWED_BASE_DIR。对于个人使用,可以是项目目录;对于团队,可以考虑通过配置文件来动态设置。- 详细的错误信息:错误信息不仅要给服务器日志看,也会返回给AI智能体。清晰的信息能帮助AI(和背后的用户)理解哪里出错了,而不是得到一个笼统的“调用失败”。
现在,启动这个服务器并连接Claude Code,你就可以说:“请帮我读取package.json文件,看看里面有什么依赖。” AI就能通过你的MCP服务器安全地获取到文件内容,并基于此进行分析或给出建议。
3.2 执行Shell命令:赋予智能体“执行”能力
比读取更强大的是执行。我们可以创建一个工具,让AI在受控条件下运行Shell命令,比如运行测试、启动服务、执行构建脚本等。
创建command_server.ts,实现一个run_command工具。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/transport/stdio.js'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); const server = new Server( { name: 'command-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); // 定义允许的命令白名单。这是最安全的方式,但不够灵活。 // const ALLOWED_COMMANDS = ['npm run test', 'ls -la', 'git status']; // 更灵活但需谨慎的方式:允许特定前缀或目录下的命令。 const ALLOWED_CWD = process.cwd(); // 只允许在当前目录下执行 server.setRequestHandler('tools/list', async () => ({ tools: [{ name: 'run_command', description: '在项目目录下执行一个Shell命令(请谨慎使用,仅运行你信任的命令)。', inputSchema: { type: 'object', properties: { command: { type: 'string', description: '要执行的Shell命令,例如:npm install 或 ls -la', }, timeout: { type: 'number', description: '命令执行超时时间(毫秒),默认10000', default: 10000, }, }, required: ['command'], }, }], })); server.setRequestHandler('tools/call', async (request) => { if (request.params.name !== 'run_command') { throw new Error(`未知工具: ${request.params.name}`); } const { command, timeout = 10000 } = request.params.arguments as { command: string; timeout?: number }; // !!! 关键安全与风险提示 !!! // 这里为了灵活性,没有做严格的白名单限制,但在生产环境中极其危险。 // 你应该至少做以下检查: // 1. 命令黑名单:过滤`rm -rf /`、`:(){ :|:& };:`(fork炸弹)等危险命令。 // 2. 或实现白名单:只允许运行`npm run`、`git`(特定子命令)等。 // 3. 记录日志:所有执行的命令都应记录到安全的地方,用于审计。 console.error(`[SECURITY AUDIT] 即将执行命令: ${command}`); try { // 使用promisify的exec,并设置超时和当前工作目录 const { stdout, stderr } = await execAsync(command, { cwd: ALLOWED_CWD, // 限制工作目录 timeout, // 防止长时间运行命令 // 可以在这里设置环境变量,如 PATH: '/usr/local/bin:/usr/bin:/bin' }); const output = []; if (stdout) output.push(`**标准输出:**\n\`\`\`\n${stdout}\n\`\`\``); if (stderr) output.push(`**标准错误:**\n\`\`\`\n${stderr}\n\`\`\``); return { content: [{ type: 'text', text: `命令 \`${command}\` 执行完成。\n${output.join('\n')}`, }], }; } catch (error: any) { // 处理超时、命令不存在等错误 let errorMessage = `执行命令失败: ${error.message}`; if (error.killed) errorMessage = `命令执行超时(${timeout}ms)或被终止。`; if (error.code === 'ENOENT') errorMessage = `命令未找到: ${command.split(' ')[0]}`; throw new Error(errorMessage); } }); const transport = new StdioServerTransport(); server.connect(transport).then(() => { console.error('命令MCP服务器已启动(请务必注意安全风险)。'); });警告与最佳实践给予AI执行Shell命令的能力是双刃剑,必须极其谨慎。
- 最小权限原则:绝对不要以
root或管理员权限运行此服务器进程。为它创建一个专用的、低权限的系统用户。- 环境隔离:考虑在Docker容器或沙箱环境中运行MCP服务器,限制其能访问的文件系统和网络。
- 命令过滤:示例中为了演示没有过滤,这是极其危险的。在实际部署前,你必须实现一个严格的命令白名单机制。例如,只允许命令以
npm run、git(且排除git push --force等危险操作)、ls、cat(仅限特定文件)开头。- 审计日志:所有命令执行记录必须持久化保存,以便在出现问题时追溯。
- 用户确认:在AI工具调用前,可以设计一个需要用户手动确认的环节,尤其是对于
rm、git push等有破坏性的命令。
3.3 集成自定义API:连接外部数据源
很多时候,我们需要AI能获取外部数据,比如查询内部部署的数据库、调用公司的CRM接口、获取天气信息等。我们可以构建一个“API网关”式的MCP工具,由服务器作为代理去安全地调用这些服务,避免将API密钥等敏感信息暴露给AI模型。
假设我们要调用一个需要认证的天气API。创建api_server.ts。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/transport/stdio.js'; // 假设我们有一个需要API密钥的天气服务 const WEATHER_API_KEY = process.env.WEATHER_API_KEY; // 从环境变量读取,切勿硬编码! const WEATHER_API_BASE = 'https://api.weatherapi.com/v1'; const server = new Server( { name: 'api-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); server.setRequestHandler('tools/list', async () => ({ tools: [{ name: 'get_weather', description: '获取指定城市的当前天气信息。', inputSchema: { type: 'object', properties: { city: { type: 'string', description: '城市名称,例如:Beijing 或 London', }, }, required: ['city'], }, }], })); server.setRequestHandler('tools/call', async (request) => { if (request.params.name !== 'get_weather') { throw new Error(`未知工具: ${request.params.name}`); } const { city } = request.params.arguments as { city: string }; if (!WEATHER_API_KEY) { throw new Error('服务器配置错误:缺少API密钥。'); } try { // 构造请求URL,API密钥由服务器保管,不暴露给AI const apiUrl = `${WEATHER_API_BASE}/current.json?key=${WEATHER_API_KEY}&q=${encodeURIComponent(city)}`; const response = await fetch(apiUrl, { method: 'GET', headers: { 'Accept': 'application/json' }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`天气API请求失败 (${response.status}): ${errorText}`); } const data = await response.json(); // 从API响应中提取我们需要的信息 const { location, current } = data; const weatherText = `城市:${location.name}, ${location.country} 时间:${location.localtime} 天气:${current.condition.text} 温度:${current.temp_c}°C (体感 ${current.feelslike_c}°C) 湿度:${current.humidity}% 风速:${current.wind_kph} km/h,风向 ${current.wind_dir}`; return { content: [{ type: 'text', text: weatherText, }], }; } catch (error: any) { // 网络错误、JSON解析错误等 throw new Error(`获取天气信息失败:${error.message}`); } }); const transport = new StdioServerTransport(); server.connect(transport).then(() => { console.error('API MCP服务器已启动。'); });实操心得:敏感信息管理与性能优化
- 密钥管理:API密钥、数据库密码等绝对不要硬编码在源码中。使用环境变量(
process.env)、密钥管理服务(如AWS Secrets Manager)或配置文件(并加入.gitignore)。- 错误处理与降级:外部API可能失败。除了抛出错误,也可以考虑返回一个友好的降级信息,或者使用缓存的上一次结果。
- 请求缓存:对于更新不频繁的数据(如天气,可以缓存10分钟),可以在服务器内存或Redis中实现一个简单的缓存层,避免重复调用API,提升响应速度并节省配额。
- 请求限流与聚合:如果你的工具可能被频繁调用,考虑在服务器端对请求进行限流(rate limiting),或者将多个AI的请求聚合后一次性调用外部API。
通过这三个例子,你应该已经掌握了实现实用MCP工具的核心模式:定义工具、安全校验、执行业务逻辑、格式化返回。你可以将这些工具合并到一个服务器里,打造一个功能丰富的“全能助手”。
4. 进阶技巧与实战避坑指南
当你成功运行了几个基础工具后,可能会想构建更复杂、更稳定的MCP服务。这一部分,我分享一些从实战中总结出来的进阶技巧和常见问题的解决方法。
4.1 工具设计的艺术:让AI更好用
工具定义(inputSchema)的质量,直接决定了AI智能体能否正确、高效地使用它。设计时需要考虑AI的理解能力。
1. 描述要清晰具体:
- 差:
description: '处理文件' - 优:
description: '读取指定文本文件的内容并返回。仅支持UTF-8编码的文本文件,如图片等二进制文件会出错。路径需为项目根目录下的相对路径。'清晰的描述能减少AI的误用和后续的报错。
2. 参数Schema要严谨:使用JSON Schema的丰富特性来约束输入。
{ "inputSchema": { "type": "object", "properties": { "count": { "type": "integer", "minimum": 1, "maximum": 100, "description": "需要获取的项目数量,必须在1到100之间。" }, "filter": { "type": "string", "enum": ["active", "pending", "completed"], "description": "按状态过滤项目。" } }, "required": ["filter"] } }enum、minimum/maximum、pattern(正则)等约束能极大提升AI调用参数的准确性。
3. 处理复杂输出:工具不仅可以返回文本,还可以返回结构化的数据(如JSON),供AI进一步分析。
return { content: [{ type: 'text', text: `找到${users.length}个用户。`, }, { // 返回一个独立的、结构化的数据块 type: 'object', object: { users: usersArray, // 一个用户对象数组 totalCount: usersArray.length, page: 1 } }], };AI模型可以更好地理解和利用这种结构化数据。
4.2 性能、稳定性与运维
1. 服务器生命周期管理:
- 保持常驻:MCP服务器通常作为后台进程启动。可以使用
pm2、systemd或Docker来管理,确保崩溃后能自动重启。# 使用pm2示例 pm2 start dist/file_server.js --name mcp-file-server pm2 save pm2 startup # 设置开机自启 - 资源清理:如果你的工具创建了临时文件、打开了数据库连接或网络连接,务必在
tools/call处理函数中使用try...catch...finally或在服务器关闭钩子中进行清理,防止资源泄漏。
2. 连接与超时问题:
- 心跳与重连:MCP协议本身有心跳机制(
ping/pong)。确保你的服务器能正确处理。如果连接意外断开,Claude Code可能会尝试重连,你的服务器需要能优雅处理多次连接。 - 工具调用超时:在
tools/call处理函数中,如果操作可能耗时很长(如大数据查询),要设置合理的超时,并考虑支持异步或轮询机制。可以在工具定义中增加一个timeout参数,让调用者指定。
3. 日志与监控:这是生产环境不可或缺的。将日志输出到文件或日志收集系统(如ELK、Sentry)。
import * as fs from 'fs/promises'; const logStream = fs.createWriteStream('./mcp-server.log', { flags: 'a' }); server.setRequestHandler('tools/call', async (request) => { const startTime = Date.now(); const toolName = request.params.name; logStream.write(`[${new Date().toISOString()}] 调用工具: ${toolName}\n`); try { // ... 处理逻辑 const duration = Date.now() - startTime; logStream.write(`[${new Date().toISOString()}] 工具 ${toolName} 成功,耗时 ${duration}ms\n`); return result; } catch (error: any) { logStream.write(`[${new Date().toISOString()}] 工具 ${toolName} 失败: ${error.message}\n`); throw error; } });监控服务器的内存、CPU使用率,以及工具调用的成功率和延迟。
4.3 常见问题排查实录
即使按照指南操作,你也可能会遇到一些问题。下面是一个快速排查清单:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Claude Code启动时提示“无法连接MCP服务器”或直接忽略。 | 1. 服务器脚本存在语法错误,启动即崩溃。 2. 服务器未正确监听 stdio。3. 命令行参数 --mcp路径错误。 | 1. 单独运行你的服务器脚本(node server.js),检查是否有错误输出。2. 确保代码中正确创建了 StdioServerTransport并调用了server.connect(transport)。3. 检查文件路径是否正确,使用绝对路径更可靠( claude --mcp $(pwd)/server.js)。 |
| AI无法识别或列出你定义的工具。 | 1.tools/list请求处理器未正确注册或返回格式错误。2. 服务器能力声明中未包含 tools: {}。 | 1. 检查server.setRequestHandler('tools/list', ...)的回调函数是否返回了正确的{ tools: [...] }结构。2. 检查 new Server()的第二个参数中是否包含了capabilities: { tools: {} }。 |
| 工具调用失败,返回“未知工具”或参数错误。 | 1.tools/call请求处理器中工具名判断错误。2. 客户端传递的参数格式与 inputSchema不匹配。 | 1. 在tools/call处理器中打印request.params,确认收到的工具名和参数是什么。2. 在AI调用工具时,观察Claude Code生成的参数JSON是否符合你定义的Schema。 |
| 服务器进程运行但无响应,或CPU占用高。 | 1. 在tools/call处理器中发生了同步无限循环或阻塞操作。2. 未正确处理异步操作,导致Promise未返回。 | 1. 确保所有IO操作(文件、网络、数据库)都使用异步模式(async/await)。 2. 在处理器函数中做好错误捕获,避免未处理的Promise rejection导致进程不稳定。 |
| 安全校验通过,但操作(如读文件)仍失败。 | 1. 服务器进程运行用户的文件系统权限不足。 2. 路径存在特殊字符或空格未正确处理。 | 1. 检查运行服务器的用户是否有权访问目标文件/目录(ls -l)。2. 在代码中使用 path模块处理路径,并对输入进行适当的trim和转义。 |
一个典型的调试流程是:首先脱离AI客户端,单独运行你的服务器脚本,看它能否正常启动并打印出准备就绪的日志。然后,可以编写一个简单的测试客户端脚本,模拟MCP协议发送tools/list和tools/call请求,来验证服务器的响应是否符合预期。这能帮你快速定位问题是出在服务器逻辑,还是与AI客户端的集成上。
构建本地MCP服务器的旅程,从理解其“赋权”的价值开始,经过快速上手的实践,再到实现各类实用工具,最后深入到设计模式和运维细节。它不是一个高不可攀的技术,而是一个理念非常朴素的协议:为AI提供一套安全、可控、可扩展的“手和脚”。我最深的体会是,最大的障碍往往不是技术实现,而是安全边界的审慎划分和工具设计的用户体验。从提供一个安全的read_file工具开始,逐步迭代,你会发现你的AI工作流变得越来越顺畅、越来越强大。不妨现在就挑一个你最重复、最繁琐的任务,尝试为它打造一个专属的MCP工具,你会立刻感受到那种“自动化成真”的愉悦。
