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

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

前端开发 2026-06-09 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 Calling MCP
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 客户端使用,这才是标准化的真正价值。

📚 相关文章