2025 年 Anthropic 发布的 Model Context Protocol(MCP)已经成为 AI 工具调用的事实标准——Claude Desktop、Cursor、Windsurf 等主流 AI 客户端全部支持。但大多数开发者还停留在"用别人写好的 MCP Server"阶段,很少有人真正理解协议细节并构建自己的 Server。本文将带你从零实现一个生产级 MCP Server,掌握 Tool、Resource、Prompt 三大原语,并对比 Function Calling 与 MCP 的本质区别。
🧩 一、MCP 协议核心概念与架构
MCP 的本质是一个 JSON-RPC 2.0 协议,运行在 stdio 或 HTTP(SSE/Streamable HTTP)传输层之上。它定义了三种核心原语:
- Tool(工具):AI 可以调用的函数,类似 Function Calling
- Resource(资源):AI 可以读取的数据源,类似 GET 接口
- Prompt(提示模板):预定义的交互模板
与 OpenAI Function Calling 最大的区别是:MCP Server 是独立进程,AI 客户端通过协议发现和调用工具,而不是在 API 请求中硬编码工具定义。
| 特性 | Function Calling | MCP Server |
|---|---|---|
| 工具定义位置 | API 请求体中 | 独立 Server 进程 |
| 工具发现 | 手动声明 | 协议自动发现(tools/list) |
| 传输方式 | HTTP API 内嵌 | stdio / SSE / Streamable HTTP |
| 复用性 | 绑定特定 API 提供商 | 任何支持 MCP 的客户端通用 |
| 安全隔离 | 无 | 独立进程,权限隔离 |
| 资源管理 | 不支持 | 原生 Resource 支持 |
📌 记住: MCP 不是 Function Calling 的替代品,而是更高层的抽象。Function Calling 是 API 层面的能力,MCP 是应用层面的工具框架。
🔧 二、从零实现一个 MCP Server
我们来构建一个实用的 MCP Server——提供 JSON 分析、格式化、Diff 对比等开发者工具。这个 Server 可以被 Claude Desktop 直接调用。
2.1 项目初始化
# 初始化项目
mkdir mcp-devtools && cd mcp-devtools
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init
// tsconfig.json — 关键配置
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
}
}
2.2 Server 核心实现
// src/server.ts — MCP Server 主文件
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "devtools-mcp",
version: "1.0.0",
});
// Tool 1: JSON 格式化与验证
server.tool(
"json_format",
"格式化 JSON 字符串,支持自定义缩进,同时验证 JSON 合法性",
{
input: z.string().describe("需要格式化的 JSON 字符串"),
indent: z.number().default(2).describe("缩进空格数,默认 2"),
},
async ({ input, indent }) => {
try {
const parsed = JSON.parse(input);
const formatted = JSON.stringify(parsed, null, indent);
const originalSize = new TextEncoder().encode(input).length;
const formattedSize = new TextEncoder().encode(formatted).length;
return {
content: [{
type: "text",
text: `✅ JSON 格式化成功\n\n` +
`📊 原始大小: ${originalSize} bytes → 格式化后: ${formattedSize} bytes\n\n` +
"```json\n" + formatted + "\n```",
}],
};
} catch (e) {
return {
content: [{
type: "text",
text: `❌ JSON 解析失败: ${(e as Error).message}`,
}],
isError: true,
};
}
}
);
// Tool 2: JSON Diff 对比
server.tool(
"json_diff",
"对比两个 JSON 对象的差异,输出结构化的 diff 结果",
{
left: z.string().describe("左侧 JSON 字符串"),
right: z.string().describe("右侧 JSON 字符串"),
},
async ({ left, right }) => {
try {
const leftObj = JSON.parse(left);
const rightObj = JSON.parse(right);
const diffs = findDiffs(leftObj, rightObj, "");
if (diffs.length === 0) {
return { content: [{ type: "text", text: "✅ 两个 JSON 完全相同" }] };
}
const report = diffs.map(d => {
const icon = d.type === "added" ? "🟢" : d.type === "removed" ? "🔴" : "🟡";
return `${icon} [${d.type}] ${d.path}: ${d.type === "removed" ? d.oldVal : d.newVal}`;
}).join("\n");
return {
content: [{
type: "text",
text: `📊 发现 ${diffs.length} 处差异:\n\n${report}`,
}],
};
} catch (e) {
return {
content: [{ type: "text", text: `❌ 解析失败: ${(e as Error).message}` }],
isError: true,
};
}
}
);
// Tool 3: Base64 编解码
server.tool(
"base64_codec",
"Base64 编码/解码工具,支持 UTF-8 中文",
{
action: z.enum(["encode", "decode"]).describe("操作类型: encode 或 decode"),
input: z.string().describe("输入内容"),
},
async ({ action, input }) => {
try {
if (action === "encode") {
const encoded = Buffer.from(input, "utf-8").toString("base64");
return { content: [{ type: "text", text: `🔐 Base64 编码结果:\n\`${encoded}\`` }] };
} else {
const decoded = Buffer.from(input, "base64").toString("utf-8");
return { content: [{ type: "text", text: `🔓 Base64 解码结果:\n${decoded}` }] };
}
} catch (e) {
return {
content: [{ type: "text", text: `❌ 操作失败: ${(e as Error).message}` }],
isError: true,
};
}
}
);
// Diff 辅助函数
interface DiffResult {
path: string;
type: "added" | "removed" | "changed";
oldVal?: unknown;
newVal?: unknown;
}
function findDiffs(left: unknown, right: unknown, path: string): DiffResult[] {
const diffs: DiffResult[] = [];
if (typeof left !== typeof right || Array.isArray(left) !== Array.isArray(right)) {
diffs.push({ path: path || "/", type: "changed", oldVal: left, newVal: right });
return diffs;
}
if (typeof left === "object" && left !== null && typeof right === "object" && right !== null) {
const leftKeys = Object.keys(left as Record<string, unknown>);
const rightKeys = Object.keys(right as Record<string, unknown>);
for (const key of leftKeys) {
const childPath = path ? `${path}.${key}` : key;
if (!(key in (right as Record<string, unknown>))) {
diffs.push({ path: childPath, type: "removed", oldVal: (left as Record<string, unknown>)[key] });
} else {
diffs.push(...findDiffs(
(left as Record<string, unknown>)[key],
(right as Record<string, unknown>)[key],
childPath
));
}
}
for (const key of rightKeys) {
if (!(key in (left as Record<string, unknown>))) {
const childPath = path ? `${path}.${key}` : key;
diffs.push({ path: childPath, type: "added", newVal: (right as Record<string, unknown>)[key] });
}
}
} else if (left !== right) {
diffs.push({ path: path || "/", type: "changed", oldVal: left, newVal: right });
}
return diffs;
}
// 启动 Server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP DevTools Server started on stdio");
}
main().catch(console.error);
2.3 注册到 Claude Desktop
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"devtools": {
"command": "node",
"args": ["/absolute/path/to/mcp-devtools/dist/server.js"]
}
}
}
重启 Claude Desktop 后,在对话中输入"帮我格式化这个 JSON",Claude 就会自动调用你的 json_format 工具。
⚡ 三、进阶:Resource、Prompt 与生产实践
3.1 Resource — 让 AI 读取你的数据
Resource 是 MCP 的第二大原语,适合暴露只读数据源:
// src/resources.ts — 注册文件系统资源
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { readFile, readdir } from "fs/promises";
import { join } from "path";
export function registerResources(server: McpServer, workspacePath: string) {
// 静态资源:暴露 package.json
server.resource(
"package-json",
"file:///package.json",
{ mimeType: "application/json" },
async () => ({
contents: [{
uri: "file:///package.json",
mimeType: "application/json",
text: await readFile(join(workspacePath, "package.json"), "utf-8"),
}],
})
);
// 动态资源:列出目录结构
server.resource(
"workspace-files",
"file:///workspace",
{ mimeType: "text/plain" },
async () => {
const files = await readdir(workspacePath, { recursive: true });
return {
contents: [{
uri: "file:///workspace",
mimeType: "text/plain",
text: files.join("\n"),
}],
};
}
);
}
⚠️ 警告: 永远不要将 Resource 暴露到文件系统的敏感路径(如
~/.ssh、/etc)。MCP Server 以当前用户权限运行,一个恶意的 AI 提示可能通过 Resource 读取私钥。
3.2 Prompt — 预定义交互模板
// src/prompts.ts — 注册提示模板
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function registerPrompts(server: McpServer) {
server.prompt(
"code-review",
"代码审查助手,对给定代码进行全面审查",
{ code: (val) => val.text() },
async ({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `请对以下代码进行全面审查,关注:\n` +
`1. 潜在 bug 和边界情况\n` +
`2. 性能问题\n` +
`3. 安全漏洞\n` +
`4. 代码风格和可维护性\n\n` +
`\`\`\`\n${code}\n\`\`\``,
},
}],
})
);
}
3.3 Streamable HTTP 传输(替代 SSE)
MCP 2025-03-26 规范引入了 Streamable HTTP 传输,替代了早期的 SSE 方案。这对远程部署的 MCP Server 至关重要:
// src/http-server.ts — HTTP 传输层(适合远程部署)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use(express.json());
const server = new McpServer({ name: "remote-devtools", version: "1.0.0" });
// ... 注册 tools/resources/prompts ...
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3001, () => {
console.log("MCP HTTP Server on http://localhost:3001/mcp");
});
| 传输方式 | 适用场景 | 连接方式 | 认证 |
|---|---|---|---|
| stdio | 本地 Claude Desktop | 父进程管道 | 无需 |
| Streamable HTTP | 远程/云部署 | HTTP POST | OAuth 2.0 / API Key |
| SSE(已废弃) | 旧版兼容 | GET 长连接 | 手动实现 |
💡 提示: 如果你在做新的远程 MCP Server,直接用 Streamable HTTP。SSE 传输已经被标记为 deprecated,未来版本会移除。
🛡️ 四、安全与生产部署
MCP Server 的安全问题常被忽视。它本质上是一个带执行能力的服务端进程,必须严格控制权限。
4.1 输入验证
// ❌ 错误写法 — 直接信任 AI 传入的参数
server.tool("exec", "执行命令", { cmd: z.string() }, async ({ cmd }) => {
const result = execSync(cmd); // 危险!AI 可能传入 rm -rf /
return { content: [{ type: "text", text: result.toString() }] };
});
// ✅ 正确写法 — 白名单 + 参数清洗
server.tool("query_db", "查询数据库", {
table: z.enum(["users", "orders", "products"]), // 白名单表名
limit: z.number().min(1).max(100).default(10), // 限制返回数量
where: z.string().regex(/^[a-zA-Z0-9_=<> ]+$/), // 严格正则过滤
}, async ({ table, limit, where }) => {
// 参数化查询,防止 SQL 注入
const result = await db.query(
`SELECT * FROM ${table} WHERE ${where} LIMIT $1`,
[limit]
);
return { content: [{ type: "text", text: JSON.stringify(result.rows) }] };
});
4.2 部署检查清单
- ✅ MCP Server 以最小权限用户运行,不要用 root
- ✅ 文件系统访问限制在特定目录(workspace sandbox)
- ✅ 网络请求限制白名单域名
- ✅ 所有 Tool 输入用 Zod schema 严格验证
- ✅ 添加请求频率限制(防止 AI Agent 循环调用)
- ❌ 不要在 Tool 中暴露 shell 执行能力
- ❌ 不要在 Resource 中暴露敏感配置文件
- ❌ 不要在日志中记录完整的 Tool 参数(可能含敏感数据)
⚡ 关键结论: MCP Server 的信任边界是 AI 客户端 → MCP Server → 系统资源。AI 模型的输出是不可信的用户输入,必须像处理任何外部输入一样做验证和过滤。
📝 总结
MCP 协议正在快速成为 AI 工具生态的基础设施。掌握 MCP Server 开发,意味着你可以让任何本地工具、数据库、API 被 AI 直接调用。核心要点:
- Tool 是核心——大部分场景只需要实现 Tool,Resource 和 Prompt 是锦上添花
- Zod schema 是你的安全防线——所有输入必须严格验证
- stdio 适合本地,Streamable HTTP 适合远程——不要用已废弃的 SSE
- 安全第一——AI 的输出等于用户输入,永远不要信任
相关工具推荐:jsjson.com JSON 格式化工具 可以快速验证 MCP 返回的 JSON 数据;jsjson.com Base64 编解码 可用于调试 MCP 协议中的编码问题。