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

深入解析 Model Context Protocol(MCP)架构原理,手把手用 TypeScript 从零构建 MCP Server 和 Client,掌握 Tool、Resource、Prompt 三大原语,附完整可运行代码与避坑指南。

前端开发 2026-06-08 15 分钟

2025 年底 Anthropic 发布 Model Context Protocol(MCP)以来,这个协议已经迅速成为 AI 工具调用的事实标准——GitHub 上 MCP 相关仓库超过 5000 个,Cursor、Claude Desktop、Continue 等主流 AI 编程工具全部支持。MCP 协议定义了一套标准化的方式,让大模型(LLM)与外部工具、数据源进行交互,本质上是 AI 时代的"USB 接口"。如果你正在构建 AI Agent 或者想让自己的工具被 LLM 调用,理解 MCP 已经不是可选项,而是必修课。

🔐 一、MCP 架构原理与核心概念

1.1 为什么需要 MCP?

在 MCP 出现之前,每个 AI 应用都需要自己实现工具调用的集成逻辑。ChatGPT 用 Function Calling,Claude 用 Tool Use,各家格式不同、协议不同。这导致了一个严重的碎片化问题:你写了一个"读取数据库"的工具,要在 Claude Desktop 里用一套代码,换到 Cursor 里又要改一套。

📌 记住:MCP 的核心价值不是技术创新,而是标准化。它把"LLM 调用外部工具"这件事从各厂商的私有协议中抽离出来,形成一个开放标准。

MCP 采用经典的 Client-Server 架构

  • MCP Host:发起请求的 AI 应用(如 Claude Desktop、Cursor)
  • MCP Client:Host 内部的协议客户端,负责与 Server 通信
  • MCP Server:暴露工具(Tool)、资源(Resource)、提示模板(Prompt)的服务端

通信方式支持两种传输层:

传输方式 适用场景 连接模型 延迟 复杂度
stdio 本地进程 1:1 极低(进程间通信) ⭐ 简单
SSE (Streamable HTTP) 远程服务 1:N 中等(网络往返) ⭐⭐⭐ 较复杂

⚠️ **警告:**MCP 规范在 2025 年 3 月移除了旧版 sse 传输,统一为 streamable-http。如果你看到教程还在用旧的 SSE 端点,说明内容已过时。

1.2 三大原语:Tool、Resource、Prompt

MCP 定义了三个核心原语,理解它们的区别是正确使用 MCP 的前提:

  • Tool(工具):模型可以主动调用的函数,如搜索网页、执行 SQL、发送邮件。这是最常用的原语。
  • Resource(资源):模型可以读取的数据源,如文件内容、数据库记录。由 Client 主动拉取,不是模型调用。
  • Prompt(提示模板):预定义的提示词模板,如"代码审查"模板。由用户选择触发,不是模型自动调用。

💡 **提示:**大多数 MCP Server 只需要实现 Tool。Resource 和 Prompt 是可选的,适合需要暴露结构化数据或工作流的场景。

1.3 协议握手流程

MCP 使用 JSON-RPC 2.0 作为消息格式。一次完整的交互流程如下:

Client                          Server
  |                                |
  |--- initialize (能力协商) ----->|
  |<-- initialize result ---------|
  |--- initialized 通知 ---------->|
  |                                |
  |--- tools/list (发现工具) ----->|
  |<-- tools/list result ---------|
  |                                |
  |--- tools/call (调用工具) ----->|
  |<-- tools/call result ---------|

初始化阶段的核心是能力协商——Client 和 Server 互相告知自己支持哪些功能:

// Client 发送的 initialize 请求
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "roots": { "listChanged": true }
    },
    "clientInfo": {
      "name": "my-client",
      "version": "1.0.0"
    }
  }
}

🚀 二、从零构建 MCP Server 实战

2.1 项目初始化

我们用 TypeScript 构建一个实用的 MCP Server——JSON 处理工具集,提供 JSON 格式化、校验、路径提取等功能。这正好是开发者日常高频使用的操作。

# 初始化项目
mkdir mcp-json-tools && cd mcp-json-tools
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init --outDir dist --rootDir src

📌 记住:@modelcontextprotocol/sdk 是官方提供的 TypeScript SDK,是目前最成熟的 MCP 开发库。Python SDK 为 mcp(PyPI 包名),但 TypeScript 版本生态更完善。

2.2 实现核心 Server

// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 创建 MCP Server 实例
const server = new McpServer({
  name: "json-tools",
  version: "1.0.0",
});

// Tool 1: JSON 格式化
server.tool(
  "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);
      return {
        content: [{ type: "text", text: formatted }],
      };
    } catch (err) {
      return {
        content: [{ type: "text", text: `JSON 解析错误: ${(err as Error).message}` }],
        isError: true,
      };
    }
  }
);

// Tool 2: JSON Path 提取(从嵌套 JSON 中按路径取值)
server.tool(
  "json_path_extract",
  "从 JSON 中按点分路径提取值,如 user.address.city",
  {
    input: z.string().describe("JSON 字符串"),
    path: z.string().describe("点分路径,如 data.users.0.name"),
  },
  async ({ input, path }) => {
    try {
      const obj = JSON.parse(input);
      const keys = path.split(".");
      let current: any = obj;
      for (const key of keys) {
        if (current === undefined || current === null) {
          return {
            content: [{ type: "text", text: `路径 "${path}" 不存在` }],
            isError: true,
          };
        }
        current = Array.isArray(current) ? current[Number(key)] : current[key];
      }
      return {
        content: [{ type: "text", text: JSON.stringify(current, null, 2) }],
      };
    } catch (err) {
      return {
        content: [{ type: "text", text: `错误: ${(err as Error).message}` }],
        isError: true,
      };
    }
  }
);

// Tool 3: JSON Schema 生成(从样本数据推断 Schema)
server.tool(
  "generate_schema",
  "从 JSON 样本数据推断出 JSON Schema",
  {
    input: z.string().describe("JSON 样本数据"),
  },
  async ({ input }) => {
    try {
      const sample = JSON.parse(input);
      const schema = inferSchema(sample);
      return {
        content: [{ type: "text", text: JSON.stringify(schema, null, 2) }],
      };
    } catch (err) {
      return {
        content: [{ type: "text", text: `错误: ${(err as Error).message}` }],
        isError: true,
      };
    }
  }
);

// Schema 推断辅助函数
function inferSchema(value: any): any {
  if (value === null) return { type: "null" };
  if (Array.isArray(value)) {
    return {
      type: "array",
      items: value.length > 0 ? inferSchema(value[0]) : {},
    };
  }
  if (typeof value === "object") {
    const properties: Record<string, any> = {};
    for (const [key, val] of Object.entries(value)) {
      properties[key] = inferSchema(val);
    }
    return { type: "object", properties, required: Object.keys(value) };
  }
  const typeMap: Record<string, string> = {
    string: "string", number: "number", boolean: "boolean",
  };
  return { type: typeMap[typeof value] || "string" };
}

// Resource:暴露一个示例 JSON Schema 参考
server.resource(
  "json-schema-reference",
  "schema://json-schema-reference",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          "$schema": "http://json-schema.org/draft-07/schema#",
          "type": "object",
          "description": "JSON Schema 示例参考",
        }, null, 2),
      },
    ],
  })
);

// 启动 Server(stdio 传输)
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("JSON Tools MCP Server 已启动");
}

main().catch(console.error);

2.3 配置 package.json

{
  "name": "mcp-json-tools",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/server.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

编译并测试:

npm run build
# 用 MCP Inspector 测试(官方调试工具)
npx @modelcontextprotocol/inspector node dist/server.js

⚠️ **警告:**MCP Server 通过 console.error 输出日志,因为 stdout 被用于 JSON-RPC 通信。绝对不要在 MCP Server 中使用 console.log 输出调试信息,否则会破坏协议消息。

2.4 在 Claude Desktop 中注册

编辑 claude_desktop_config.json

{
  "mcpServers": {
    "json-tools": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-json-tools/dist/server.js"]
    }
  }
}

重启 Claude Desktop 后,你会在工具列表中看到 format_jsonjson_path_extractgenerate_schema 三个工具。直接对话"帮我格式化这个 JSON"即可触发调用。

💡 三、构建 MCP Client 与高级技巧

3.1 Client 端代码

如果你要构建自己的 AI 应用,需要在 Client 端连接 MCP Server:

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

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

  const client = new Client({
    name: "my-client",
    version: "1.0.0",
  });

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

  // 发现可用工具
  const tools = await client.listTools();
  console.log("可用工具:", tools.tools.map((t) => t.name));

  // 调用 format_json 工具
  const result = await client.callTool({
    name: "format_json",
    arguments: {
      input: '{"name":"张三","age":30,"skills":["TypeScript","Go"]}',
      indent: 4,
    },
  });
  console.log("格式化结果:", result.content);

  // 读取 Resource
  const resource = await client.readResource({
    uri: "schema://json-schema-reference",
  });
  console.log("Schema 参考:", resource.contents);

  await client.close();
}

main().catch(console.error);

3.2 Streamable HTTP 远程传输

对于远程 MCP Server,使用 StreamableHTTPClientTransport

// 远程 MCP Client 连接示例
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("https://your-server.com/mcp")
);

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

Server 端对应的实现:

// 远程 MCP Server(Express + Streamable HTTP)
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = express();
app.use(express.json());

app.post("/mcp", async (req, res) => {
  const server = new McpServer({ name: "remote-tools", version: "1.0.0" });
  // ... 注册 tools ...

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined, // 无状态模式
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000, () => console.log("MCP Server 运行在 :3000/mcp"));

3.3 性能与安全避坑指南

在生产环境中使用 MCP,有几个关键的坑点需要注意:

❌ 错误写法:直接暴露危险工具

// 危险!允许执行任意 shell 命令
server.tool("run_shell", "执行 shell 命令", {
  command: z.string(),
}, async ({ command }) => {
  const result = execSync(command); // 任意代码执行!
  return { content: [{ type: "text", text: result.toString() }] };
});

✅ 正确写法:白名单 + 参数校验

// 安全:白名单 + 超时 + 输出限制
import { execSync } from "child_process";

const ALLOWED_COMMANDS = ["ls", "cat", "wc", "du"];

server.tool("safe_shell", "执行受限制的 shell 命令", {
  command: z.enum(ALLOWED_COMMANDS),
  args: z.array(z.string()).max(10),
}, async ({ command, args }) => {
  // 参数中禁止特殊字符
  const sanitized = args.filter(a => /^[a-zA-Z0-9._/\-]+$/.test(a));
  const full = `${command} ${sanitized.join(" ")}`;
  
  const result = execSync(full, {
    timeout: 5000,        // 5 秒超时
    maxBuffer: 1024 * 100, // 100KB 输出上限
  });
  return { content: [{ type: "text", text: result.toString() }] };
});

工具描述的质量直接决定 LLM 能否正确调用:

描述质量 示例 LLM 调用准确率
❌ 模糊 "处理数据" ~30%
⚠️ 一般 "格式化 JSON" ~70%
✅ 精确 "将 JSON 字符串格式化为带缩进的易读格式,支持自定义缩进空格数,默认 2 空格" ~95%

💡 **提示:**Tool 的 description 是 LLM 决定是否调用该工具的唯一依据。把它写成你希望一个新手工程师读到的完整说明——包括功能、参数含义、返回值格式、使用限制。

3.4 调试与测试

MCP Inspector 是官方提供的可视化调试工具,可以单独测试每个 Tool:

# 启动 Inspector(会打开浏览器界面)
npx @modelcontextprotocol/inspector node dist/server.js

在 Inspector 中你可以:

  • 查看 Server 声明的所有 Tool、Resource、Prompt
  • 手动填写参数并调用 Tool
  • 查看完整的 JSON-RPC 消息流
  • 测试错误处理(传入非法参数)

📊 总结与工具推荐

MCP 协议的设计哲学是"简单但完备"——JSON-RPC + 三大原语 + stdio/HTTP 传输,覆盖了绝大多数 AI 工具集成场景。从零构建一个 MCP Server 的核心代码量不到 100 行,但要做好安全、描述、错误处理这些细节,需要对协议有深入理解。

⚡ **关键结论:**如果你正在构建 AI Agent 或工具链,MCP 是 2026 年最值得投入的协议标准。它的生态成熟度已经超过了大多数人的预期——从数据库查询到浏览器自动化,从代码搜索到 CI/CD 集成,几乎所有开发者常用工具都有对应的 MCP Server 实现。

推荐资源与工具:

⚠️ **警告:**MCP 规范仍在快速演进中(当前版本 2025-03-26),关注规范更新是必做的功课。建议订阅 MCP 官方博客 获取变更通知。

📚 相关文章