MCP 协议深度实战:从零构建 AI 工具服务端与客户端

深入解析 Model Context Protocol (MCP) 架构设计与实现细节,包含完整的 TypeScript 服务端和客户端代码示例,对比 Function Calling 与 MCP 的优劣,帮助开发者构建标准化 AI 工具集成方案。

前端开发 2026-06-10 18 分钟

当 Anthropic 在 2024 年底开源 Model Context Protocol(MCP)时,很多人以为这只是又一个内部标准。但到 2026 年,MCP 已经成为 AI 工具集成的事实标准——OpenAI、Google、Microsoft 的 Agent 框架全部支持 MCP,GitHub 上 MCP Server 超过 8000 个。如果你还在为每个 LLM 单独写 Function Calling 的 JSON Schema,这篇文章会彻底改变你的工作方式。

MCP 的核心价值很简单:用一套标准协议替代 N 套私有集成方案。就像 USB-C 统一了充电接口,MCP 统一了 AI 模型与外部工具之间的通信方式。但「简单」的背后是精密的协议设计,理解这些细节决定了你能构建出怎样的 AI 工具生态。

🔧 一、MCP 协议架构与核心概念

MCP 采用 JSON-RPC 2.0 作为底层通信协议,支持三种传输方式:stdio(标准输入输出)、SSE(Server-Sent Events)和 Streamable HTTP。协议定义了三个核心原语:Tools(工具调用)、Resources(资源读取)和 Prompts(提示模板)。

很多开发者第一次接触 MCP 时会困惑:这和 Function Calling 有什么区别?答案是——区别很大。Function Calling 是单次请求级别的工具描述,而 MCP 是一个持久化的服务端协议。打个比方:Function Calling 像是临时找人帮忙,MCP 像是签了长期合作协议的供应商。

协议通信模型

MCP 的通信分为两个阶段:初始化握手和能力协商。客户端连接到服务端后,双方交换 initialize 消息,声明各自支持的能力(capabilities)。这个设计让协议具备了向前兼容性——旧客户端连接新服务端时,不会因为遇到未知能力而崩溃。

Client                          Server
  |--- initialize (capabilities) -->|
  |<-- initialize (capabilities) ---|
  |--- initialized (ack) ---------->|
  |                                 |
  |--- tools/list ----------------->|
  |<-- tools (schema) --------------|
  |                                 |
  |--- tools/call (name, args) ---->|
  |<-- result (content) ------------|

Tools vs Resources vs Prompts

这三个概念是 MCP 的基础,理解它们的区别至关重要:

概念用途触发方式类比
Tools执行操作(写数据库、发邮件、调用 API)LLM 主动调用函数/方法
Resources提供上下文数据(文件内容、数据库记录)客户端显式读取GET 请求
Prompts预定义的交互模板用户选择使用模板/快捷指令

💡 **提示:**大多数 MCP Server 只需要实现 Tools。Resources 适合需要被 LLM「看到」但不需要「执行」的数据,比如文件系统内容或数据库 Schema。Prompts 则用于封装复杂的工作流模板。

🚀 二、从零实现一个 MCP Server

理论讲够了,直接上代码。我们用 TypeScript 实现一个完整的 MCP Server,提供三个实用工具:JSON 格式化、Base64 编解码和 Hash 计算——这三个正好是开发者日常高频使用的功能。

项目初始化与依赖

# 初始化项目
mkdir mcp-devtools-server && cd mcp-devtools-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext

⚠️ 警告:@modelcontextprotocol/sdk 的版本迭代很快,生产环境务必锁定版本号。截至 2026 年 6 月,推荐使用 1.12.x 系列。

核心服务端实现

// 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";
import { createHash } from "node:crypto";
import { Buffer } from "node:buffer";

const server = new McpServer({
  name: "devtools-mcp-server",
  version: "1.0.0",
});

// 工具1:JSON 格式化与校验
server.tool(
  "json_format",
  "格式化 JSON 字符串,支持压缩和美化两种模式",
  {
    input: z.string().describe("要格式化的 JSON 字符串"),
    indent: z.number().min(0).max(8).default(2).describe("缩进空格数,0 表示压缩"),
  },
  async ({ input, indent }) => {
    try {
      const parsed = JSON.parse(input);
      const formatted = JSON.stringify(parsed, null, indent || undefined);
      return {
        content: [{
          type: "text",
          text: `✅ 格式化成功(${indent === 0 ? "压缩" : `${indent}空格缩进`}):\n\n${formatted}`,
        }],
      };
    } catch (err) {
      return {
        content: [{
          type: "text",
          text: `❌ JSON 解析失败: ${(err as Error).message}`,
        }],
        isError: true,
      };
    }
  }
);

// 工具2:Base64 编解码
server.tool(
  "base64_codec",
  "对文本或二进制数据进行 Base64 编码或解码",
  {
    input: z.string().describe("要编码或解码的字符串"),
    mode: z.enum(["encode", "decode"]).describe("encode 编码,decode 解码"),
  },
  async ({ input, mode }) => {
    try {
      const result = mode === "encode"
        ? Buffer.from(input, "utf-8").toString("base64")
        : Buffer.from(input, "base64").toString("utf-8");
      return {
        content: [{
          type: "text",
          text: `Base64 ${mode === "encode" ? "编码" : "解码"}结果:\n\n${result}`,
        }],
      };
    } catch (err) {
      return {
        content: [{
          type: "text",
          text: `❌ Base64 操作失败: ${(err as Error).message}`,
        }],
        isError: true,
      };
    }
  }
);

// 工具3:Hash 计算(支持多种算法)
server.tool(
  "hash_compute",
  "计算字符串的哈希值,支持 MD5/SHA1/SHA256/SHA512",
  {
    input: z.string().describe("要计算哈希的字符串"),
    algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256"),
    encoding: z.enum(["hex", "base64"]).default("hex"),
  },
  async ({ input, algorithm, encoding }) => {
    const hash = createHash(algorithm).update(input, "utf-8").digest(encoding);
    return {
      content: [{
        type: "text",
        text: [
          `算法: ${algorithm.toUpperCase()}`,
          `编码: ${encoding}`,
          `输入: "${input.substring(0, 50)}${input.length > 50 ? "..." : ""}"`,
          `哈希: ${hash}`,
        ].join("\n"),
      }],
    };
  }
);

// 启动服务
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("DevTools MCP Server running on stdio");
}

main().catch(console.error);

这段代码的关键设计决策有三个:

  1. 错误处理返回 isError: true 而不是抛出异常。MCP 协议明确规定,工具执行失败时应该通过 isError 字段告知客户端,而不是中断连接。
  2. 输入验证使用 Zod Schema。MCP SDK 深度集成了 Zod,它不仅做运行时验证,还会自动将 Zod Schema 转换为 JSON Schema 供 LLM 理解。
  3. 日志输出到 stderr。因为 stdio 模式下 stdout 是协议通信通道,任何非协议数据写入 stdout 都会导致连接断开。

⚠️ **警告:**stdio 模式下,console.log 会破坏协议通信!所有调试信息必须用 console.error 输出到 stderr。这是新手最容易踩的坑。

注册为可执行文件

// package.json 中添加
{
  "bin": {
    "devtools-mcp": "./dist/server.js"
  },
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js"
  }
}
// src/server.ts 文件头部添加 shebang
#!/usr/bin/env node
// ... 其余代码不变

📊 三、MCP vs Function Calling:深度对比

很多人问:既然 OpenAI 的 Function Calling 已经很好用了,为什么还需要 MCP?这个问题值得认真回答。

架构差异

Function Calling 是嵌入在对话请求中的工具描述,每次调用都要携带完整的工具定义。而 MCP 是独立运行的服务进程,工具描述只在初始化时交换一次。

这个差异在实际工程中的影响非常大:

维度Function CallingMCP
Token 消耗每次请求都携带工具 Schema仅初始化时传输一次
工具数量建议不超过 20 个(token 成本)理论无上限(按需加载)
状态管理无状态,每次独立有状态,可维护连接上下文
跨模型兼容每家 Schema 格式不同一套 Server 适配所有客户端
安全控制应用层自行实现协议内置权限协商机制
开发复杂度低(一个 JSON 对象)中(需要实现协议)

⚡ **关键结论:**如果你只用一个 LLM 且工具不超过 5 个,Function Calling 完全够用。但当你的 Agent 需要连接 10+ 个外部服务、支持多个 LLM 后端时,MCP 的优势会指数级放大。

真实场景分析

假设你在做一个「AI 代码助手」,需要集成以下能力:

  • Git 仓库操作(clone、commit、diff)
  • 代码搜索(grep、ripgrep)
  • 文件系统读写
  • 终端命令执行
  • GitHub API(PR、Issue)
  • 数据库查询

如果用 Function Calling,你需要在每个 LLM 的 API 请求中嵌入所有工具的 JSON Schema,大约消耗 3000-5000 tokens。假设平均每个请求节省 2000 tokens,一天处理 500 个请求,就是 100 万 tokens——按 GPT-4o 的价格大约 $2.5/天。一个月就是 $75,一年 $900。

用 MCP 的话,工具描述只在初始化时传输,后续请求的 token 消耗几乎为零。这个成本差异在规模化场景下非常显著。

🎯 四、构建 MCP 客户端:实战集成

有了 Server,还需要客户端来连接和调用。MCP 客户端的核心职责是:连接 Server、发现工具、将工具注册为 LLM 可用的 Function Calling Schema。

// src/client.ts - MCP Client 实现
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

async function main() {
  // 创建传输层,启动 Server 子进程
  const transport = new StdioClientTransport({
    command: "node",
    args: ["dist/server.js"],
  });

  // 创建客户端并连接
  const client = new Client({
    name: "devtools-client",
    version: "1.0.0",
  });

  await client.connect(transport);
  console.log("✅ 已连接到 MCP Server");

  // 列出所有可用工具
  const { tools } = await client.listTools();
  console.log(`📦 发现 ${tools.length} 个工具:`);
  for (const tool of tools) {
    console.log(`  - ${tool.name}: ${tool.description}`);
  }

  // 调用 JSON 格式化工具
  const jsonResult = await client.callTool({
    name: "json_format",
    arguments: {
      input: '{"name":"jsjson","version":"1.0","features":["json","base64","hash"]}',
      indent: 4,
    },
  });
  console.log("\n🔧 JSON 格式化结果:");
  console.log((jsonResult.content as any[])[0].text);

  // 调用 Hash 计算工具
  const hashResult = await client.callTool({
    name: "hash_compute",
    arguments: {
      input: "Hello, MCP!",
      algorithm: "sha256",
    },
  });
  console.log("\n🔐 Hash 计算结果:");
  console.log((hashResult.content as any[])[0].text);

  // 断开连接
  await client.close();
}

main().catch(console.error);

运行效果:

✅ 已连接到 MCP Server
📦 发现 3 个工具:
  - json_format: 格式化 JSON 字符串,支持压缩和美化两种模式
  - base64_codec: 对文本或二进制数据进行 Base64 编码或解码
  - hash_compute: 计算字符串的哈希值,支持 MD5/SHA1/SHA256/SHA512

🔧 JSON 格式化结果:
✅ 格式化成功(4空格缩进):
{
    "name": "jsjson",
    "version": "1.0",
    "features": [
        "json",
        "base64",
        "hash"
    ]
}

🔐 Hash 计算结果:
算法: SHA256
编码: hex
输入: "Hello, MCP!"
哈希: 84d5f2b8c0e4a3c7...

与 LLM Agent 集成

MCP Client 的真正威力在于与 LLM Agent 框架集成。核心模式是:MCP 工具自动转换为 LLM 的 Function Calling Schema。

// src/agent-integration.ts - 将 MCP 工具转为 LLM Function Calling 格式
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

interface FunctionTool {
  type: "function";
  function: {
    name: string;
    description: string;
    parameters: Record<string, unknown>;
  };
}

async function mcpToolsToLLMSchema(client: Client): Promise<FunctionTool[]> {
  const { tools } = await client.listTools();

  return tools.map((tool) => ({
    type: "function" as const,
    function: {
      name: tool.name,
      description: tool.description || "",
      parameters: tool.inputSchema,
    },
  }));
}

// 使用示例:与 OpenAI API 集成
async function agentLoop(client: Client, userMessage: string) {
  const tools = await mcpToolsToLLMSchema(client);

  // 构造 OpenAI 兼容的请求
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: "gpt-4o",
      messages: [{ role: "user", content: userMessage }],
      tools,
      tool_choice: "auto",
    }),
  });

  const data = await response.json();
  const message = data.choices[0].message;

  // 如果 LLM 选择调用工具
  if (message.tool_calls) {
    for (const call of message.tool_calls) {
      const args = JSON.parse(call.function.arguments);
      const result = await client.callTool({
        name: call.function.name,
        arguments: args,
      });
      console.log(`🔧 调用 ${call.function.name}:`, result);
    }
  }
}

💡 **提示:**这个模式是目前最流行的 MCP 集成方式——MCP Server 作为工具提供者,Client 作为中间层将工具 Schema 转换为 LLM 能理解的格式。LangChain、LlamaIndex、Vercel AI SDK 都采用了类似的架构。

⚠️ 五、生产环境踩坑指南

在生产环境使用 MCP,有几个绕不开的坑:

坑1:stdio 进程管理

stdio 模式的 MCP Server 本质上是一个子进程。子进程崩溃、僵尸进程、内存泄漏,这些问题在生产环境中都会暴露出来。

// ❌ 错误写法:不处理子进程崩溃
const transport = new StdioClientTransport({
  command: "node",
  args: ["server.js"],
});

// ✅ 正确写法:添加健康检查和自动重启
class ResilientMcpClient {
  private client: Client | null = null;
  private restartCount = 0;
  private maxRestarts = 3;

  async connect(command: string, args: string[]) {
    const transport = new StdioClientTransport({ command, args });

    // 监听进程退出
    transport.onerror = (err) => {
      console.error("MCP transport error:", err);
      this.tryReconnect(command, args);
    };

    this.client = new Client({ name: "resilient-client", version: "1.0.0" });
    await this.client.connect(transport);
  }

  private async tryReconnect(command: string, args: string[]) {
    if (this.restartCount >= this.maxRestarts) {
      console.error("❌ MCP Server 重启次数超限,放弃重连");
      return;
    }
    this.restartCount++;
    const delay = Math.min(1000 * Math.pow(2, this.restartCount), 30000);
    console.log(`⏳ ${delay}ms 后尝试第 ${this.restartCount} 次重连...`);
    await new Promise((r) => setTimeout(r, delay));
    await this.connect(command, args);
  }
}

坑2:工具执行超时

LLM 调用工具时,用户在等待响应。如果工具执行时间过长,整个体验会很差。

// 添加超时控制的工具包装
async function callToolWithTimeout(
  client: Client,
  name: string,
  args: Record<string, unknown>,
  timeoutMs: number = 30000
) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const result = await client.callTool({ name, arguments: args });
    return result;
  } catch (err) {
    if (controller.signal.aborted) {
      return {
        content: [{ type: "text" as const, text: `⏰ 工具 ${name} 执行超时(${timeoutMs}ms)` }],
        isError: true,
      };
    }
    throw err;
  } finally {
    clearTimeout(timer);
  }
}

坑3:安全权限控制

MCP Server 通常拥有较高的系统权限(文件读写、网络请求、命令执行)。如果不加限制,被恶意 prompt injection 攻击的 LLM 可以通过你的 MCP Server 执行任意操作。

⚠️ **警告:**永远不要在公网暴露未认证的 MCP Server。stdio 模式天然安全(只能本机访问),但 SSE 和 HTTP 模式必须添加认证中间件。

推荐的安全策略:

  • ✅ 最小权限原则:每个 MCP Server 只暴露必要的工具
  • ✅ 参数白名单验证:不要直接将 LLM 输出传给系统命令
  • ✅ 操作审计日志:记录每次工具调用的参数和结果
  • ❌ 避免:在工具中实现 eval() 或 exec() 等危险操作
  • ❌ 避免:让工具直接返回原始的系统错误信息(可能泄露路径等敏感信息)

💡 六、总结与工具推荐

MCP 不是一个「有了更好」的技术选型,而是 AI 工具集成的必然趋势。2026 年的 AI Agent 生态已经证明了这一点——当工具数量超过 10 个、需要支持多个 LLM 后端、团队规模超过 3 人时,MCP 的标准化优势会让开发效率提升一个数量级。

快速上手路径:

  1. 先用 @modelcontextprotocol/sdk 实现一个简单的 MCP Server(本文示例可直接复用)
  2. 用 Claude Desktop 或 Cursor 等已支持 MCP 的客户端测试
  3. 逐步迁移到 SSE/HTTP 传输方式以支持远程部署
  4. 集成到你的 AI Agent 框架中

相关工具和资源:

⚡ **关键结论:**MCP 的学习曲线不高——如果你熟悉 JSON-RPC 和 TypeScript,半天就能上手。但它的回报很高——一次实现的 MCP Server 可以被所有支持 MCP 的 AI 客户端使用,这才是标准化的真正价值。

📚 相关文章