MCP 协议实战:让 AI Agent 真正连上你的业务系统
MCP 协议实战:让 AI Agent 真正连上你的业务系统
MCP 正在成为 AI Agent 接入外部工具的"HTTP 时刻"。但读文档和实际落地之间,隔着一个真实的业务系统。
背景
上个月给出海电商团队搭一个运营数据 Agent,需求很朴素:自然语言查订单、分析 SKU 周转、对比各站点 GMV。
传统做法是写个 RAG + SQL Generator,Prompt 里拼一堆 Schema 让 LLM 猜怎么查。但有两个根本问题:
- Schema 膨胀:出海业务跨多国多站点,表结构随站点定制,Prompt 塞不下
- 安全风险:让 LLM 直接拼 SQL,白名单纯靠 Prompt 约束 = 防君子不防小人
我需要一种方式,让 Agent 像调用 HTTP API 一样调内部服务 —— MCP (Model Context Protocol) 恰好是这个抽象。
插一句:这让我想起早期门户时代的「频道模板系统」。CMS 后台给编辑一个结构化表单,编辑填参数不用碰 HTML。MCP 对 Agent 做的事本质上一样 —— 给 LLM 一个结构化的工具契约,让它不用碰底层实现。
技术方案
MCP 是什么(两句话说清楚)
MCP 是 Anthropic 提出的开放协议,定义了两个角色:
- MCP Server:暴露 Tools / Resources / Prompts 的服务端
- MCP Client:调用这些能力的一方(通常是 AI Agent Host,如 Claude Desktop、Cursor、自建 Agent)
通信走 JSON-RPC 2.0,支持 stdio 和 Server-Sent Events (SSE) 两种传输。对业务系统集成来说,SSE 是最实用的方案 —— 不需要 Agent 和 Server 在同一台机器上。
架构设计
┌──────────────┐ SSE(HTTP) ┌──────────────┐ gRPC ┌──────────────┐
│ AI Agent │ ◄──────────────► │ MCP Server │ ◄──────────► │ 业务服务 │
│ (Claude/自建) │ │ (Node.js) │ │ (订单/BI等) │
└──────────────┘ └──────────────┘ └──────────────┘
MCP Server 充当"翻译层":把 Agent 的工具调用请求翻译成内部 RPC,把返回结果格式化成 Tool Result。
选型理由:
- 不用改现有服务:MCP Server 作为独立 Sidecar 部署,业务服务零侵入
- SSE 模式天然支持跨机器:Agent 在云端,MCP Server 在内网,过一层 API Gateway 就行
- TypeScript 生态:
@modelcontextprotocol/sdk官方 SDK,和现有 Node 技术栈匹配
实施步骤
Step 1:创建 MCP Server 项目
mkdir order-mcp-server && cd order-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk express cors
npm install -D typescript @types/node tsx
Step 2:定义 Tool 清单
按业务需求定义了 3 个 Tool:
// tools/schema.ts
export const TOOLS: ToolDefinition[] = [
{
name: "query_orders",
description: "按站点、日期范围、订单状态查询订单列表",
parameters: {
type: "object",
properties: {
site: { type: "string", enum: ["US", "SEA", "ME", "LATAM"], description: "站点代码" },
startDate: { type: "string", description: "开始日期,格式 YYYY-MM-DD" },
endDate: { type: "string", description: "结束日期,格式 YYYY-MM-DD" },
status: { type: "string", enum: ["PENDING", "SHIPPED", "DELIVERED", "CANCELED"] },
limit: { type: "number", default: 20 }
},
required: ["site", "startDate", "endDate"]
}
},
{
name: "get_sku_metrics",
description: "查询 SKU 的周转天数、库存深度、近 30 天销量",
parameters: {
type: "object",
properties: {
skuCode: { type: "string", description: "SKU 编码" },
site: { type: "string", enum: ["US", "SEA", "ME", "LATAM"] }
},
required: ["skuCode", "site"]
}
},
{
name: "compare_gmv",
description: "对比多个站点的 GMV,支持同比/环比",
parameters: {
type: "object",
properties: {
sites: { type: "array", items: { type: "string" }, description: "站点列表" },
compareType: { type: "string", enum: ["yoy", "mom"], description: "同比/环比" }
},
required: ["sites"]
}
}
];
注意:Tool 的 description 就是 Agent 的"API 文档"。写得越精确,Agent 调用越准确。这里踩过一个坑 —— 最早 compare_gmv 没限制 sites 数组长度,Agent 一次传 12 个站点,后端 SQL 直接超时。
Step 3:实现 SSE MCP Server
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { TOOLS } from "./tools/schema";
import { OrderService } from "./services/order";
const app = express();
const transportMap = new Map<string, SSEServerTransport>();
// SSE endpoint — 建立长连接
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
const mcpServer = new McpServer({
name: "order-mcp-server",
version: "1.0.0"
});
// 注册 Tools
for (const tool of TOOLS) {
mcpServer.tool(
tool.name,
tool.description,
tool.parameters,
async (args: any) => {
return await OrderService.handleToolCall(tool.name, args);
}
);
}
transportMap.set(transport.sessionId, transport);
await mcpServer.connect(transport);
res.on("close", () => transportMap.delete(transport.sessionId));
});
// POST endpoint — 接收 JSON-RPC 消息
app.post("/messages", express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transportMap.get(sessionId);
if (!transport) { res.status(404).end(); return; }
await transport.handlePostMessage(req, res);
});
app.listen(3001, () => console.log("MCP Server running on :3001"));
关键点:SSE 模式下需要维护 sessionId → transport 映射。每次请求带 sessionId query param 来路由到正确的长连接。
Step 4:在 Agent 侧配置 MCP Client
以 Claude Desktop 为例,编辑 claude_desktop_config.json:
{
"mcpServers": {
"order-service": {
"url": "https://internal-api.your-company.com/mcp/sse",
"headers": {
"Authorization": "Bearer <your-api-token>"
}
}
}
}
自建 Agent(WorkBuddy 等)则需要在 Agent 侧实现 MCP Client 的 SSE transport。SDK 提供了标准客户端,接入代码如下:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
const transport = new SSEClientTransport(
new URL("https://internal-api/mcp/sse")
);
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
// 获取可用工具列表
const tools = await client.listTools();
// 调工具
const result = await client.callTool({
name: "compare_gmv",
arguments: { sites: ["US", "SEA"], compareType: "mom" }
});
踩坑记录
坑 1:SSE 重连风暴
现象:Agent 掉线后重连,短时间内创建了 200+ 个 SSE 连接,服务 OOM。
根因:Claude Desktop 的重连退避算法默认最大值太小(30s),突发重连时瞬间打满连接池。
解决:
- Server 侧加
maxConnectionsPerClient限制(我们设 5) - 旧连接加 60s TTL,超时主动断
- Agent 侧
reconnect.backoff调到最大 120s
// 连接数限制
const MAX_PER_CLIENT = 5;
const clientConnectionCount = new Map<string, number>();
app.get("/sse", (req, res) => {
const clientId = req.headers["x-client-id"] as string || req.ip;
const count = clientConnectionCount.get(clientId) || 0;
if (count >= MAX_PER_CLIENT) {
res.status(429).json({ error: "Too many connections" });
return;
}
clientConnectionCount.set(clientId, count + 1);
res.on("close", () => {
const c = clientConnectionCount.get(clientId) || 1;
clientConnectionCount.set(clientId, Math.max(0, c - 1));
});
// ... rest of handler
});
坑 2:Tool Result 太大导致上下文爆炸
现象:query_orders 一次返回 500 条订单,每条含完整地址、物流轨迹、备注。Agent 收到后 Token 直接飙到上限,后续对话全丢。
解决:
- Tool 返回做摘要化:只返回关键字段,列表类结果限制行数
- 大量数据走 Resource 模式而非 Tool Result。Agent 拿到 Resource URI 后按需读取
// Tool 返回精简版,附带 Resource URI
async function queryOrders(args: QueryOrdersArgs) {
const orders = await OrderService.query(args);
return {
content: [{
type: "text",
text: JSON.stringify({
total: orders.total,
summary: orders.items.slice(0, 20).map(o => ({
id: o.id, site: o.site, status: o.status,
amount: o.amount, createdAt: o.createdAt
})),
_more: `mcp-resource://orders/detail?ids=${orders.itemIds.join(',')}`
})
}]
};
}
坑 3:认证 token 泄露到 LLM
现象:有一天翻 Agent 日志,发现一次 Tool 调用的错误信息里包含了服务端返回的 Authorization header expired: Bearer sk-xxx...。这个信息构成了 LLM 的上下文,如果后续对话持续,token 有概率被泄露。
解决:MCP Server 层的错误信息做脱敏处理,不透露任何 credential/token/secrets:
async function handleToolCall(toolName: string, args: any) {
try {
return await callInternalService(toolName, args);
} catch (err: any) {
// 永远不要让原始错误信息进入 Agent 上下文
return {
isError: true,
content: [{ type: "text", text: "Service temporarily unavailable, please retry" }]
};
}
}
实际内网的错误信息打全量日志到 ELK,Agent 只看脱敏后的消息。
总结
MCP 的意义不在于技术本身有多复杂(它就是个 JSON-RPC over SSE),而在于标准化了 Agent ↔ 工具之间的契约。
这跟当年互联网从各种自定义二进制协议收敛到 HTTP 的逻辑一样:标准化降低集成成本、催生生态。
几个关键收获:
- Tool description 是最重要的"文档",写得不精确 = Agent 调不对
- SSE 连接的生命周期管理是线上最大的坑,重连策略、连接池、TTL 都要提前设计
- Tool Result 的粒度决定了 Token 消耗,宁可多分几个小 Tool,也别用一个巨型 Tool 塞所有数据
- 安全审计要覆盖 Tool→Agent 的返回路径,任何错误信息都可能成为 LLM 的上下文
目前在出海业务侧,MCP Server 已经接了订单、库存、物流三个域。下一步打算把多站点的 BI 指标也通过 MCP 暴露,让运营能把「东南亚站上月 GMV 环比为什么掉了 15%」这种问题直接问 Agent。
作者:lotusxyhf,互联网老兵转型出海 + AI Agent 赛道。 从门户到 AI,踩过的坑比写过的代码还多。欢迎评论区交流。
更多推荐



所有评论(0)