最近我花了三天时间把公司内部的一个工单查询API封装成了MCP Server,让AI Agent可以直接调用。做完之后最大的感受是:MCP的本质不是什么高深协议,它就是一个让AI和你的系统"说上话"的翻译层。网上很多教程一上来就讲协议规范、JSON-RPC、SSE传输,看得人头大。我决定换一种方式——直接从真实场景出发,带你用Node.js从零写一个能用的MCP Server。
为什么要把API封装成MCP Server
先说我的实际痛点。我们团队有个内部工单系统,查询接口是REST API,需要鉴权Token。每次排查问题都要:打开Postman → 找Token → 填参数 → 发请求 → 复制结果。一天操作十几次,繁琐且低效。
封装成MCP Server后,我对AI说一句"查一下工单#12345的状态",Agent自动调用工具返回结果。整个过程从2分钟缩短到5秒。
但MCP Server的价值远不止于此。一旦你的工具以MCP标准暴露出去,所有支持MCP的AI客户端都能调用它——Claude Desktop、Cursor、OpenClaw、任何实现了MCP协议的框架。你写一次,到处可用,这才是核心价值。
一个最小可运行的MCP Server
先别管那些复杂的协议细节。我们用官方的MCP SDK写一个最简单的Server,暴露一个"查询当前时间"的工具:
// server.js
const { MCPServer } = require('@anthropic/mcp-sdk');
const server = new MCPServer({
name: 'demo-server',
version: '1.0.0'
});
server.tool('get_current_time', '获取当前服务器时间', {}, async () => {
const now = new Date().toLocaleString('zh-CN');
return { content: [{ type: 'text', text: now }] };
});
server.run();
这段代码做了什么?三件事:
- 创建一个MCP Server实例,声明名称和版本
- 用
server.tool()注册一个工具,定义名称、描述和参数schema - 用
server.run()启动服务,默认使用stdio传输模式
默认的stdio模式意味着Server通过标准输入输出与Client通信。这是最简单的模式,适合本地开发。如果你的Server需要远程调用,后面会讲到SSE和Streamable HTTP模式。
实战:封装真实API为MCP工具
现在把事情变复杂一点——封装一个需要鉴权的HTTP API。以天气查询为例(公开API,无需申请):
// weather-server.js
const { MCPServer } = require('@anthropic/mcp-sdk');
const https = require('https');
const server = new MCPServer({
name: 'weather-server',
version: '1.0.0'
});
function fetchWeather(city) {
return new Promise((resolve, reject) => {
const url = 'https://api.open-meteo.com/v1/forecast'
+ '?current_weather=true&timezone=Asia/Shanghai'
+ '&latitude=39.9&longitude=116.4';
https.get(url, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
server.tool(
'query_weather',
'查询指定城市的实时天气信息',
{
city: { type: 'string', description: '城市名称,如:北京、上海' }
},
async ({ city }) => {
try {
const data = await fetchWeather(city);
const w = data.current_weather;
const text = city + ' 当前天气:' + w.temperature
+ '°C,风速 ' + w.windspeed + ' km/h';
return { content: [{ type: 'text', text: text }] };
} catch (err) {
return {
content: [{ type: 'text', text: '查询失败:' + err.message }],
isError: true
};
}
}
);
server.run();
这里有几个值得注意的设计细节:
- 错误处理返回isError:MCP协议规定工具调用失败时在响应中标记isError=true,而不是抛异常。这样Client能优雅地处理错误并让LLM重试
- 参数描述必须清晰:LLM根据description理解工具用途,根据schema构造参数。模糊的描述会导致调用失败
- 返回纯文本而非JSON:除非Client明确需要结构化数据,否则text格式更适合LLM理解
加入鉴权:内部API的安全封装
实际工作中,大部分内部API都有鉴权。我的做法是在Server初始化时读取环境变量,而不是在每次工具调用时传参:
const API_TOKEN = process.env.INTERNAL_API_TOKEN;
if (!API_TOKEN) {
console.error('请设置环境变量 INTERNAL_API_TOKEN');
process.exit(1);
n}
function callInternalAPI(path, params) {
return new Promise((resolve, reject) => {
n const postData = JSON.stringify(params);
const options = {
hostname: 'internal-api.company.com',
path: path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + API_TOKEN,
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(options, res => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => resolve(JSON.parse(body)));
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
这样做的好处是Token不暴露给LLM,也不需要在每次对话中传递。Server进程持有凭证,Client只通过MCP协议调用工具——安全边界清晰。
三种传输模式的选择建议
MCP目前支持三种传输模式,我在实际项目中都用过,给你一个选型参考:
| 传输模式 | 通信方式 | 适用场景 | 延迟 |
|---|---|---|---|
| stdio | 标准输入/输出 | 本地开发、个人工具 | 极低 |
| SSE | Server-Sent Events | 远程单机部署、轻量服务 | 低 |
| Streamable HTTP | HTTP长连接 | 生产环境、多Client并发 | 中 |
我的经验是:开发阶段用stdio,部署用Streamable HTTP。SSE在2026年已经被官方标记为过渡方案,新项目不建议采用。Streamable HTTP支持多路复用,一个Server可以同时服务多个Client。
切换到Streamable HTTP只需修改启动方式:
const { MCPServerHTTPTransport } = require('@anthropic/mcp-sdk');
const transport = new MCPServerHTTPTransport({ port: 3000 });
server.connect(transport);
我踩过的三个坑
坑一:工具描述写成技术文档
我最初把工具描述写成"调用POST /api/v1/tickets/{id}查询工单详情"。LLM完全不知道这是什么意思,调用成功率不到30%。
改成"根据工单ID查询工单的详细信息,包括状态、处理人、创建时间和最新回复"后,成功率直接飙升到95%以上。记住:工具的受众是LLM,不是开发者。
坑二:超时处理缺失
有一次内部API响应超慢(超过30秒),Server端没有设置超时,导致整个MCP连接卡死。Client无法接收新请求,只能重启。
解决方案是在所有HTTP请求中加上超时:
const req = https.request(options, res => { /* ... */ });
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('请求超时(10秒)'));
});
坑三:返回数据过大
我封装的工单列表接口一次返回50条记录,JSON超过100KB。LLM处理这么大的输入会严重降质。
最佳实践是Server端做数据裁剪,只返回关键字段:
// 只返回Agent关心的字段
const summary = data.tickets.map(t => ({
id: t.id,
title: t.title,
status: t.status,
assignee: t.assignee_name
}));
快速验证你的MCP Server
写完Server后,用官方的mcp-inspector工具验证。这是官方提供的可视化调试工具,能直接测试工具调用:
npx @anthropic/mcp-inspector node weather-server.js
浏览器打开后,你会看到一个界面,左侧显示可用工具列表,右侧可以输入参数测试调用。确认工具能正常返回结果后,再接入实际的AI客户端。
与AI客户端对接
以Claude Desktop为例,在配置文件中加入:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/path/to/weather-server.js"]
}
}
}
如果是OpenClaw,在技能配置中声明MCP Server的启动命令即可。一旦配置完成,AI就能自动发现并使用你注册的工具。
总结
开发MCP Server的核心步骤可以简化为:定义工具 → 实现逻辑 → 处理错误 → 选择传输模式 → 接入客户端。整个过程不复杂,但细节决定成败——工具描述影响调用成功率,超时处理保证稳定性,数据裁剪控制Token消耗。
我建议从最简单的stdio模式和一个真实API开始,跑通之后再考虑扩展。MCP Server的开发是一个"做一遍就懂"的事情,纸上谈兵不如动手实践。
如果你也在做MCP相关的开发,欢迎交流经验。后续我会写一篇关于MCP Server生产部署(Docker + Nginx + 认证)的实战文章。
相关阅读:豆包大模型API接入实战教程 | UI-TARS桌面助手安装配置实战
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论