0

MCP Server从零开发实战:用Node.js把内部API封装为AI Agent可调用工具

2026.05.19 | youres | 18次围观

最近我花了三天时间把公司内部的一个工单查询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标准输入/输出本地开发、个人工具极低
SSEServer-Sent Events远程单机部署、轻量服务
Streamable HTTPHTTP长连接生产环境、多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辅助作者原创,未经许可,转载请保留原文链接。

发表评论