基于MCP协议构建AI工具服务器:Node.js实现与Claude集成实战
在AI应用开发领域,工具调用(Tool Calling)是实现模型与外部系统交互的核心技术。其原理是通过标准化接口协议,让AI模型能够安全、可控地调用外部函数或获取动态数据,从而突破模型本身的知识和时间限制。这项技术的价值在于将AI的推理能力与实时信息、业务系统无缝结合,极大扩展了AI助手的应用场景。目前,Model Context Protocol(MCP)正成为这一领域的重要标准协议,它定义了
1. 项目概述:一个为AI应用构建的“标准插件库”
如果你最近在折腾AI应用开发,特别是想让你的AI助手(比如Claude、GPTs)能“联网”查询实时信息、读取本地文件或者操作你的数据库,那你大概率已经接触过“工具调用”(Tool Calling)这个概念。简单说,就是让AI模型在对话中,能按需调用外部函数来获取信息或执行操作。但这里有个痛点:每个开发者、每个项目都在重复造轮子。今天为Claude写个读取GitHub Issues的工具,明天为GPTs写个查询天气的接口,工具的定义、协议、实现方式五花八门,难以复用和共享。
这就是“InstaNode-dev/mcp”这个项目要解决的核心问题。它不是一个具体的工具,而是一个 协议标准 和一套 参考实现 。MCP,全称是 Model Context Protocol ,你可以把它理解为AI应用领域的“USB协议”。它定义了一套标准化的方式,让AI模型(客户端)能够发现、描述并安全地调用来自各种数据源和服务(服务器)提供的“工具”或“上下文”。
这个项目仓库,就是MCP协议在Node.js环境下的官方SDK和工具集。它提供了构建MCP服务器(提供工具和数据)和MCP客户端(调用工具)所需的一切核心库、类型定义和开发样板。对于开发者而言,它的价值在于: 只要你用Node.js按照MCP标准封装好你的数据源(比如公司内部CRM、你的Notion笔记、实时股票API),那么所有兼容MCP的AI应用(如Claude Desktop、Cursor等)都能立即、安全地使用它,无需为每个应用单独适配。
2. MCP协议的核心设计思想与优势拆解
在深入代码之前,理解MCP协议的设计哲学至关重要。这决定了我们为什么要用它,而不是自己随便写个HTTP接口。
2.1 从“一对一”适配到“一对多”标准
传统的AI工具集成是“烟囱式”的。假设你有一个“项目任务查询”接口。如果你想在Claude里用,需要按照Anthropic的特定格式(可能是特定的JSON Schema)封装一个工具;想在Cursor里用,又得按照Cursor的规则再写一遍。这种模式效率低下,且无法积累。
MCP的核心思想是引入一个 标准化中间层 。你只需要按照MCP协议实现一次“项目任务查询”服务器。这个服务器会以一种标准格式向外界宣告:“我这里有一个叫 list_project_tasks 的工具,这是它的输入参数描述,这是它能返回的数据结构。” 任何兼容MCP的客户端(无论是Claude Desktop,还是未来任何其他AI应用)都能理解这个标准描述,并自动生成对应的UI和调用逻辑。这就实现了“一次编写,处处可用”。
2.2 不仅仅是“工具调用”,更是“上下文提供”
MCP协议的能力分为两大类,这比单纯的函数调用更强大:
- 工具(Tools) :即我们常说的“函数调用”。AI可以主动调用,执行一个操作并返回结果。例如:
search_web(搜索)、execute_sql(查询数据库)。 - 资源(Resources) :这是MCP一个非常关键的特性。服务器可以声明一系列“资源”(如
file:///path/to/notes.md或weather://beijing/today)。AI客户端可以“订阅”或“加载”这些资源。当资源内容发生变化时(例如文件被修改),服务器可以主动通知客户端,客户端再将最新的内容作为上下文注入给AI模型。这使得AI能持续获取动态信息,而不必每次都主动调用工具。
这种设计让AI不仅能“做事”,还能持续“感知”环境变化,为实现更智能、更贴身的AI助手奠定了基础。
2.3 传输层抽象与安全性
MCP协议本身是传输层无关的。它定义了客户端与服务器之间通信的消息格式(基于JSON-RPC),但具体是通过标准输入输出(stdio)、HTTP还是WebSocket进行传输,由实现决定。 instanode-dev/mcp 的Node.js SDK主要支持stdio方式,这也是Claude Desktop集成MCP服务器的方式,因为它简单、安全、无需处理网络权限。
安全性是内置设计。服务器运行在本地或受信环境中,通过stdio与客户端通信,避免了敏感数据暴露在公网。客户端只能调用服务器明确声明的工具和访问声明的资源,并且服务器实现中可以包含完整的授权和验证逻辑。
3. 使用 instanode-dev/mcp 快速构建你的第一个MCP服务器
理论讲完,我们动手实战。假设我们要构建一个“时间信息服务器”,为AI提供获取当前时间、计算日期差等工具。
3.1 环境准备与项目初始化
首先,确保你的环境已安装Node.js(建议18.x或更高版本)。然后创建一个新的项目目录并初始化。
mkdir mcp-time-server
cd mcp-time-server
npm init -y
接下来,安装MCP Node.js SDK的核心包。这里我们需要两个包: @modelcontextprotocol/sdk 包含了协议的核心类型和通用逻辑; @modelcontextprotocol/sdk-server 提供了构建服务器的便利类。
npm install @modelcontextprotocol/sdk @modelcontextprotocol/sdk-server
同时,我们安装TypeScript和相关的类型定义,以获得更好的开发体验。
npm install --save-dev typescript @types/node
npx tsc --init
在生成的 tsconfig.json 中,确保 target 设置为 "ES2022" 或更高, module 设置为 "NodeNext" 。
3.2 定义工具(Tools)与资源(Resources)
在项目根目录创建 src/index.ts 文件。我们首先导入必要的模块,并定义服务器提供的工具。
import { Server } from '@modelcontextprotocol/sdk-server/index.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod'; // 用于参数验证,需要安装:npm install zod
// 初始化服务器,给它起个名字
const server = new Server(
{
name: 'mcp-time-server',
version: '1.0.0',
},
{
capabilities: {
tools: {}, // 声明我们支持工具功能
resources: {}, // 声明我们支持资源功能(稍后添加)
},
}
);
// 定义工具:获取当前时间
const getCurrentTimeTool: Tool = {
name: 'get_current_time',
description: '获取当前的系统日期和时间,以及对应的UTC时间和时间戳。',
inputSchema: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: '可选的时区名称,如 Asia/Shanghai, America/New_York。默认为系统时区。',
},
format: {
type: 'string',
description: '输出时间格式。可选值:iso (ISO 8601), human (人类可读), timestamp (毫秒时间戳)。默认为 iso。',
enum: ['iso', 'human', 'timestamp'],
},
},
},
};
// 定义工具:计算两个日期的差值
const calculateDateDiffTool: Tool = {
name: 'calculate_date_diff',
description: '计算两个日期之间的差值,可以按天、周、月、年输出。',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'string',
description: '开始日期,格式为 YYYY-MM-DD 或 ISO 8601。',
},
endDate: {
type: 'string',
description: '结束日期,格式为 YYYY-MM-DD 或 ISO 8601。默认为今天。',
},
unit: {
type: 'string',
description: '差值单位。可选值:days, weeks, months, years。默认为 days。',
enum: ['days', 'weeks', 'months', 'years'],
},
},
required: ['startDate'], // 标记 startDate 为必填参数
},
};
注意 :工具定义中的
inputSchema使用了 JSON Schema 格式。这是MCP协议的要求,AI客户端(如Claude)会解析这个schema来生成调用界面并验证用户输入。务必把description字段写清楚,这直接决定了AI模型是否能正确理解和使用你的工具。
3.3 实现工具处理逻辑
定义了工具之后,我们需要在服务器上注册处理这些工具请求的处理器。
// 处理工具列表请求:当客户端查询“你有什么工具”时,返回定义好的工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [getCurrentTimeTool, calculateDateDiffTool],
};
});
// 处理具体的工具调用请求
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === 'get_current_time') {
const { timezone, format = 'iso' } = args as any;
let now = new Date();
// 处理时区(简化示例,生产环境应用用库如`luxon`)
if (timezone) {
// 这里只是示例,实际转换时区需要更复杂的逻辑
console.log(`Requested timezone: ${timezone}`);
}
let result;
switch (format) {
case 'timestamp':
result = { timestamp: now.getTime() };
break;
case 'human':
result = { humanReadable: now.toLocaleString() };
break;
case 'iso':
default:
result = { isoString: now.toISOString(), localString: now.toLocaleString() };
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} else if (name === 'calculate_date_diff') {
const { startDate, endDate = new Date().toISOString().split('T')[0], unit = 'days' } = args as any;
const start = new Date(startDate);
const end = new Date(endDate);
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
throw new Error('无效的日期格式。请使用 YYYY-MM-DD 或 ISO 8601 格式。');
}
const diffMs = end.getTime() - start.getTime();
const diffDays = diffMs / (1000 * 60 * 60 * 24);
let diffValue: number;
let unitText: string;
switch (unit) {
case 'weeks':
diffValue = diffDays / 7;
unitText = '周';
break;
case 'months':
diffValue = diffDays / 30.44; // 近似值
unitText = '个月';
break;
case 'years':
diffValue = diffDays / 365.25; // 近似值
unitText = '年';
break;
case 'days':
default:
diffValue = diffDays;
unitText = '天';
}
const result = {
startDate: start.toISOString().split('T')[0],
endDate: end.toISOString().split('T')[0],
difference: {
value: parseFloat(diffValue.toFixed(2)),
unit: unit,
unitText: unitText,
},
rawDiffDays: parseFloat(diffDays.toFixed(2)),
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} else {
throw new Error(`未知的工具: ${name}`);
}
} catch (error: any) {
// 将错误信息清晰地返回给客户端
return {
content: [
{
type: 'text',
text: `工具调用失败: ${error.message}`,
},
],
isError: true,
};
}
});
3.4 添加资源(Resources)支持
为了让示例更完整,我们再添加一个“资源”:一个动态显示当前时间的资源。客户端可以加载它,并且当时间变化时(通过轮询),服务器会通知客户端更新上下文。
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
Resource,
ResourceTemplate,
} from '@modelcontextprotocol/sdk/types.js';
// 定义一个时间资源模板
const currentTimeResourceTemplate: ResourceTemplate = {
uriTemplate: 'time://current/{format?}',
name: '当前时间',
description: '获取当前时间的动态资源。格式可以是 text 或 json。',
mimeType: 'application/json',
};
// 定义资源列表
const resources: Resource[] = [
{
uri: 'time://current/json',
name: '当前时间 (JSON)',
description: '以JSON格式返回当前时间信息',
mimeType: 'application/json',
},
{
uri: 'time://current/text',
name: '当前时间 (文本)',
description: '以人类可读文本返回当前时间',
mimeType: 'text/plain',
},
];
// 处理资源列表请求
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: resources,
};
});
// 处理读取资源请求
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const now = new Date();
if (uri === 'time://current/json') {
return {
contents: [
{
uri: uri,
mimeType: 'application/json',
text: JSON.stringify({
iso: now.toISOString(),
local: now.toLocaleString(),
timestamp: now.getTime(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}, null, 2),
},
],
};
} else if (uri === 'time://current/text') {
return {
contents: [
{
uri: uri,
mimeType: 'text/plain',
text: `当前时间是:${now.toLocaleString()} (${Intl.DateTimeFormat().resolvedOptions().timeZone})`,
},
],
};
} else {
throw new Error(`资源未找到: ${uri}`);
}
});
3.5 启动服务器
最后,我们需要让服务器运行起来,并通过标准输入输出(stdio)与客户端通信。这是MCP服务器最常用的运行方式。
// 启动服务器,监听 stdio
async function runServer() {
try {
await server.connect(process.stdin, process.stdout);
console.error('MCP时间服务器已启动,正在通过stdio通信...');
} catch (error) {
console.error('服务器启动失败:', error);
process.exit(1);
}
}
// 处理进程退出信号
process.on('SIGINT', () => {
console.error('收到中断信号,关闭服务器...');
server.close();
process.exit(0);
});
runServer();
在 package.json 中添加启动脚本:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
现在,编译并运行服务器:
npx tsc
npm start
此时,服务器会在后台运行,等待通过stdio接收MCP客户端(如Claude Desktop)的连接。它自己不会输出任何东西到控制台(日志通过 console.error 输出到标准错误,不影响协议通信)。
4. 在Claude Desktop中集成与测试你的MCP服务器
构建好服务器后,最关键的一步是让它被AI客户端使用。我们以目前对MCP支持最完善的Claude Desktop为例。
4.1 配置Claude Desktop
Claude Desktop允许通过一个简单的JSON配置文件来添加本地MCP服务器。配置文件的位置因操作系统而异:
- macOS :
~/Library/Application Support/Claude/claude_desktop_config.json - Windows :
%APPDATA%\Claude\claude_desktop_config.json - Linux :
~/.config/Claude/claude_desktop_config.json
如果文件或目录不存在,请手动创建。配置文件的基本结构如下:
{
"mcpServers": {
"time-server": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/YOUR/mcp-time-server/dist/index.js"
]
}
}
}
重要提示 :
command必须是能在系统PATH中找到的命令,这里是node。args中的路径 必须是绝对路径 。相对路径会导致启动失败。- 确保你的
index.js文件已经通过tsc编译好。
4.2 测试与验证
- 保存配置并重启Claude Desktop :修改配置文件后,完全退出并重新启动Claude Desktop应用。
- 观察连接 :重新启动你的MCP服务器(
npm start)。如果配置正确,Claude Desktop启动时会自动执行你配置的命令,与你的服务器建立stdio连接。你可以在服务器日志中看到连接成功的消息(如果你在代码中添加了连接日志)。 - 在Claude中调用工具 :在Claude Desktop的聊天框中,你可以直接尝试使用工具。例如,输入:“请帮我获取现在的北京时间。” Claude会识别出可用的
get_current_time工具,并可能询问你是否要调用它,或者直接调用并返回结果。你也可以问:“从2023-01-01到今天有多少天了?” 这会触发calculate_date_diff工具。 - 使用资源 :你可以尝试让Claude“加载”或“关注”某个资源,例如:“请加载
time://current/json这个资源作为上下文。” 如果服务器支持资源推送,最新的时间信息会被注入到对话上下文中。
4.3 配置技巧与常见问题
路径问题 :这是最常见的错误。在Windows上,路径分隔符是反斜杠,需要转义或使用正斜杠。
// Windows示例
"args": [
"C:\\Users\\YourName\\Projects\\mcp-time-server\\dist\\index.js"
]
// 或使用正斜杠(Node.js支持)
"args": [
"C:/Users/YourName/Projects/mcp-time-server/dist/index.js"
]
环境变量 :如果你的脚本依赖特定环境变量,可以在配置中指定 env 字段。
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/server.js"],
"env": {
"MY_API_KEY": "your_secret_key_here",
"NODE_ENV": "production"
}
}
}
}
调试 :如果工具没有出现,首先检查Claude Desktop的配置界面(某些版本在设置中有MCP服务器状态显示)。最有效的调试方法是查看服务器进程的标准错误输出。确保你的服务器代码在启动和收到请求时有清晰的 console.error 日志输出。你也可以在配置中暂时将命令改为 node + -e 执行一段打印日志的代码,来测试命令是否被正确执行。
5. 进阶开发:构建生产级MCP服务器的关键考量
一个能在团队或生产环境中使用的MCP服务器,需要考虑更多因素。 instanode-dev/mcp SDK提供的基础能力之上,我们需要自己构建健壮性。
5.1 错误处理与健壮性
上面的示例包含了基础的try-catch,但生产环境需要更精细的错误处理。
-
输入验证 :使用像
zod这样的库严格验证工具参数。MCP协议只要求提供JSON Schema,但服务器端必须自己验证。import { z } from 'zod'; const GetCurrentTimeArgsSchema = z.object({ timezone: z.string().optional(), format: z.enum(['iso', 'human', 'timestamp']).default('iso'), }).strict(); // .strict() 确保没有多余字段 // 在处理函数中 const validatedArgs = GetCurrentTimeArgsSchema.parse(args); -
资源加载失败 :对于资源(如读取文件、访问网络API),要有重试机制和友好的错误信息返回,避免服务器进程崩溃。
-
连接保活与重连 :虽然stdio连接通常稳定,但需要考虑客户端意外退出的情况。服务器应优雅地处理EOF(标准输入结束),并可以安全地关闭自身。
5.2 性能与资源管理
- 工具耗时 :如果某个工具执行时间很长(如大数据查询),应考虑实现异步操作并支持可能的“取消请求”功能(如果MCP客户端支持)。同时,在工具描述中注明可能耗时。
- 资源订阅 :对于支持“订阅”并主动推送更新的资源,要管理好订阅列表,避免内存泄漏。在客户端断开连接时,清理对应的订阅。
- 限制与配额 :对于可能被频繁调用的工具(如搜索),考虑实现简单的速率限制或调用次数配额,防止滥用。
5.3 安全性增强
- 敏感工具 :对于执行删除、修改、支付等敏感操作的工具, 必须在服务器端实现二次确认或权限检查 。不能仅仅依赖AI客户端的调用。可以在工具实现中加入人工确认环节(例如,生成一个需要用户输入的验证码),或者与系统的身份认证绑定。
- 参数净化 :特别是涉及文件路径、系统命令、数据库查询的工具,必须对输入参数进行严格的净化和转义,防止注入攻击。
- 本地运行 :坚持MCP服务器在本地或受信网络环境运行的原则,这是其安全模型的基础。避免将能执行高危操作的MCP服务器暴露给不可信的客户端。
5.4 测试与调试策略
- 单元测试 :为每个工具的处理函数编写单元测试,模拟不同的输入参数和边界情况。
- 集成测试 :可以编写一个简单的MCP客户端测试脚本,模拟Claude Desktop的行为,连接你的服务器并发送标准的JSON-RPC请求,验证返回结果。
// 一个简单的测试客户端示例 import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/stdio.js'; import { spawn } from 'child_process'; const serverProcess = spawn('node', ['dist/index.js']); const transport = new StdioClientTransport(serverProcess); const client = new Client({ name: 'test-client' }, { capabilities: {} }); await client.connect(transport); const tools = await client.listTools(); console.log('Available tools:', tools); - 日志记录 :使用结构化的日志库(如
pino或winston),记录工具调用、参数、执行时间、错误信息等,便于问题追踪和审计。
6. 生态展望与项目实践建议
MCP协议和 instanode-dev/mcp 项目代表了一种重要的趋势:将AI的能力从封闭的模型内部,扩展到开放、可组合的外部系统。对于开发者而言,现在投入学习并构建MCP服务器,是在为未来的AI原生应用生态积累资产。
实践建议 :
- 从封装内部工具开始 :最好的起点是你日常工作中高频使用的内部系统。比如,封装一个查询Jira任务、获取GitLab合并请求状态、查看内部仪表盘数据的MCP服务器。这能立即提升你的工作效率。
- 设计良好的工具语义 :工具的名称和描述要清晰、无歧义。遵循“动词+名词”的命名惯例(如
create_issue,search_documents)。详细的参数描述能极大提升AI模型调用的准确性。 - 模块化设计 :一个MCP服务器可以提供多个相关工具。但如果你有截然不同的数据源(如天气API和数据库操作),考虑拆分成多个独立的、职责单一的服务器。这样更易于维护和更新。
- 关注社区 :关注MCP协议的官方动态和社区。已经有许多优秀的开源MCP服务器出现,例如用于文件系统操作、Git仓库查询、网络搜索等。参考这些项目的代码是快速学习的最佳途径。
踩坑心得 :
- 版本兼容性 :MCP协议和SDK可能还在快速迭代中。注意你使用的
@modelcontextprotocol/sdk版本与目标AI客户端(如Claude Desktop)的兼容性。有时需要根据客户端版本调整SDK版本。 - Claude Desktop缓存 :Claude Desktop可能会缓存工具列表。当你更新了服务器工具定义后,如果Claude里没出现新工具,尝试完全退出Claude Desktop并重启,或者清除其缓存(具体位置参考其文档)。
- 错误信息反馈 :工具调用失败时,返回给AI的
content中的错误信息要尽可能对用户友好。AI可能会直接将这个信息读给用户。避免输出堆栈跟踪等技术细节。
构建MCP服务器的过程,本质上是在为AI定义一套与你的数字世界交互的“标准操作接口”。随着兼容MCP的客户端越来越多,你今天编写的这个时间服务器,也许明天就能在另一个全新的AI办公套件中被调用。这种“一次编写,随处可用”的能力,正是标准化协议带来的最大红利。
更多推荐



所有评论(0)