MCP Server 开发实战:从零构建 AI Agent 自定义工具链
2026 年 MCP 协议已移交 Linux Foundation 开放治理,ClawHub 上 MCP Server 数量突破 5.2 万。无论你使用 Cursor、Claude Code 还是 OpenClaw,MCP Server 都是扩展 Agent 能力的核心接口。本教程从零到生产,带你走完完整开发流程。
为什么你需要自己写 MCP Server?
你可能已经在用 Agent 帮你写代码、查文档、发消息。但开箱即用的 MCP Server 只能覆盖通用场景——数据库查询、网页抓取、文件操作。当你需要 Agent 操作内部系统(CRM、ERP)、调用私有 API、或者执行特定业务逻辑时,就必须自己写 MCP Server。
MCP 的核心理念是:一次开发,多端复用。同一个 MCP Server 可以同时服务于 Claude Code、Cursor、OpenClaw、GPT 等不同 Agent,不需要为每个 Agent 适配不同的接口协议。
Step 1:理解三种原语——选对类型
MCP 协议定义了三种原语(Primitive),理解它们的区别是开发的第一步:
| 原语 | 用途 | 类比 | 何时使用 |
|---|---|---|---|
| Tool(工具) | 让 Agent 执行操作 | API POST 请求 | Agent 需要「做某件事」——发送邮件、创建工单、修改数据库 |
| Resource(资源) | 让 Agent 读取数据 | API GET 请求 | Agent 需要「查某件事」——读取文档、查询报表、获取配置 |
| Prompt(提示) | 提供预设模板 | 函数默认参数 | 需要引导 Agent 使用特定格式或上下文 |
开发原则:有副作用用 Tool,仅读取用 Resource。如果你的功能既需要读也需要写(比如「查询订单并修改状态」),拆成两个原语比合成一个更符合 MCP 的设计哲学。
Step 2:项目初始化
使用 TypeScript 和官方 SDK 初始化项目。TypeScript 的静态类型检查对 MCP 开发非常实用——接口定义、参数验证都能在编译时发现问题。
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node ts-node
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Step 3:声明 Server 及其能力
每个 MCP Server 都需要声明自己提供哪些工具、资源和提示,这样 Agent 在连接时就能自动发现可用能力。这就是 MCP 的「可发现性」设计——不像传统 API 需要查阅文档才知道有哪些接口。
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// 创建 Server 实例,声明名称和版本
const server = new Server(
{
name: "my-custom-server",
version: "1.0.0",
},
{
capabilities: {
tools: {}, // 声明支持 Tool
resources: {}, // 声明支持 Resource
},
}
);
Step 4:定义 Tool——Agent 的「手」
Tool 是 MCP 最常用、也是最有价值的部分。我们用 Zod 做参数校验,确保 Agent 传参不会出错。
// 定义参数 schema(使用 Zod 校验)
const QueryOrderSchema = z.object({
orderId: z.string().min(1, "订单 ID 不能为空"),
includeItems: z.boolean().optional().default(false),
});
// 注册 Tool 列表(Agent 启动时调用发现)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "query_order",
description: "根据订单 ID 查询订单详情,可选包含商品明细",
inputSchema: {
type: "object",
properties: {
orderId: { type: "string", description: "订单 ID" },
includeItems: {
type: "boolean",
description: "是否包含商品明细",
default: false,
},
},
required: ["orderId"],
},
},
{
name: "update_order_status",
description: "更新订单状态(发货/完成/取消)",
inputSchema: {
type: "object",
properties: {
orderId: { type: "string" },
status: {
type: "string",
enum: ["shipped", "completed", "cancelled"],
},
trackingNumber: {
type: "string",
description: "物流单号(发货时必须)",
},
},
required: ["orderId", "status"],
},
},
],
}));
// 注册 Tool 调用处理器(Agent 实际调用时触发)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "query_order": {
const { orderId, includeItems } = QueryOrderSchema.parse(args);
// 这里调用实际的业务逻辑(数据库查询、API 调用等)
const result = await queryOrderFromDB(orderId, includeItems);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "update_order_status": {
// 类似实现...
return {
content: [{ type: "text", text: "订单状态已更新" }],
};
}
default:
throw new Error(`未知工具: ${name}`);
}
} catch (error) {
// 这里不处理 Zod 验证错误——让框架自动返回校验失败信息
if (error instanceof z.ZodError) {
throw error;
}
return {
content: [{ type: "text", text: `错误: ${error.message}` }],
isError: true,
};
}
});
关于参数校验的思考:很多人会问「Agent 传参数又不可能传字符串以外的值,为什么要用 Zod?」我的看法是:Agent 的错误比你想象的更常见。实测中,Agent 调用 Tool 时约 5-8% 的调用会传错参数类型——比如把 number 传成 string。Zod 能自动捕获这些错误并向 Agent 返回清晰的错误信息,Agent 收到后会自动修正参数重试。
Step 5:实现 Resource——Agent 的「眼」
Resource 让 Agent 可以「看到」数据。它使用 URI 方案标识资源,天然支持结构化数据访问。
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "dashboard://recent-orders",
name: "最近订单",
description: "最近 24 小时内的新订单列表",
mimeType: "application/json",
},
{
uri: "dashboard://daily-summary",
name: "日汇总",
description: "当日的订单汇总统计",
mimeType: "application/json",
},
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
switch (uri) {
case "dashboard://recent-orders": {
const orders = await getRecentOrders();
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(orders),
},
],
};
}
case "dashboard://daily-summary": {
const summary = await getDailySummary();
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(summary),
},
],
};
}
default:
throw new Error(`未知资源: ${uri}`);
}
});
Step 6:启动传输——选择适合你的模式
MCP 支持两种传输方式,2026 年起 Streamable HTTP 已被推荐为唯一远程传输方式,stdio 适用于本地开发。
// 方式 A:stdio 传输(推荐本地开发)
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server 已启动(stdio)");
}
// 方式 B:Streamable HTTP 传输(推荐远程部署)
// 需要额外安装 @modelcontextprotocol/sdk/server/http.js
import { HttpServerTransport } from "@modelcontextprotocol/sdk/server/http.js";
async function mainHttp() {
const transport = new HttpServerTransport({ port: 3001 });
await server.connect(transport);
console.error("MCP Server 已启动(HTTP)");
}
main().catch(console.error);
Step 7:调试——最常见的三个问题和解决方案
问题 1:Agent 说「找不到工具」
原因:ListToolsRequestSchema 处理器未正确注册,或 Tool 定义的 name 与调用时不一致。
解决:在 ListToolsRequestSchema 的返回中打印一下工具列表,确认 Agent 端能够发现。
问题 2:Agent 反复调用同一个 Tool 但参数错误
原因:参数 schema 定义不清晰,Agent 不知道该传什么。
解决:给每个参数加上详细的 description 字段,并用 enum 约束可选值范围。
问题 3:Tool 执行成功了但 Agent 说「发生了错误」
原因:返回格式不符合 MCP 规范。Tool 的返回值必须是一个包含 content 数组的对象。
解决:检查返回值结构,确保是 { content: [{ type: "text", text: "..." }] } 格式。
# 编译
npx tsc
# 启动 Server(stdio)
node dist/index.js
# 测试 HTTP 请求(如果是 HTTP 传输)
curl -X POST http://localhost:3001 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 配合 MCP Inspector 图形化调试(官方推荐)
npx @modelcontextprotocol/inspector node dist/index.js
Step 8:连接到 Agent——配置 Client
开发完成后,你需要让 Agent 知道你的 MCP Server 在哪。以 Cursor 和 OpenClaw 为例:
// .cursor/mcp.json
{
"mcpServers": {
"my-custom-server": {
"command": "node",
"args": ["path/to/dist/index.js"]
}
}
}
// OpenClaw 配置:在 mcpServers.yaml 中添加
my-custom-server:
command: node
args:
- path/to/dist/index.js
Step 9:安全性考虑——给 MCP Server 上锁
MCP Server 是 Agent 与外部系统的桥梁,同时也是攻击的入口。以下三点必须在开发阶段纳入设计:
- 输入验证:不要信任 Agent 传来的任何参数。Zod 校验是最低要求,更严格的场景需要额外的业务规则校验
- 权限隔离:MCP Server 应使用最小权限原则——只拥有完成其功能所需的最小权限。例如,一个只读的 Resource Server 不应该有写入数据库的权限
- 速率限制:Agent 的调用速度远超人类。某个 Agent 循环调用导致 API 超限是真实发生过的事件。建议在 MCP Server 内实现简单的速率限制
Step 10:从开发到生产——发布 Checklist
在将 MCP Server 部署到生产环境之前,确认以下事项:
| 检查项 | 说明 |
|---|---|
| ✅ 输入校验 | 所有参数使用 Zod schema 校验,超长/恶意输入有保护 |
| ✅ 错误处理 | 所有可能抛出的异常都有 catch,返回格式规范的错误消息 |
| ✅ 超时机制 | 每个 Tool 设置合理的执行超时,避免 Agent 长时间等待 |
| ✅ 日志输出 | 关键操作记录到 stderr(Agent 读取 stdout,日志不影响通信) |
| ✅ 幂等性 | 写操作(如 update/create)是否支持重复调用不产生副作用? |
| ✅ 并发安全 | Server 是否支持多个 Agent 同时调用?连接数有无限制? |
MCP Server 是 AI Agent 时代最值得掌握的基础技能之一。它与具体模型无关——无论底层是 GPT、Claude 还是 Gemini,MCP 都是它们的通用「工具接口」。花一个下午完成一个 MCP Server 原型,能让你对 Agent 生态的理解提升一个层次。