MCP 协议完全指南:从零构建 AI 工具服务器的实战教程

深入解析 Model Context Protocol(MCP)协议规范,手把手教你用 TypeScript 构建生产级 MCP Server,涵盖工具定义、资源管理、传输层选择与安全最佳实践。

前端开发 2026-07-01 15 分钟

2026 年,几乎所有主流 AI 平台都支持了 MCP(Model Context Protocol)协议。Anthropic、OpenAI、Google、微软的 Copilot 生态全部接入,GitHub 上 MCP Server 仓库数量突破 5 万。如果你还在用自定义的 function calling JSON 和大模型"对话",是时候了解这个正在统一 AI 工具生态的协议了。本文不讲概念,直接上手——从协议规范到生产级代码,带你完整走一遍 MCP Server 的开发流程。

🔧 一、MCP 协议核心架构

MCP 的本质是一个JSON-RPC 2.0 协议,定义了 AI 模型(Client)与外部工具(Server)之间的标准通信方式。它不是又一个"万能框架",而是一个精心设计的接口规范。

1.1 三大核心原语

MCP 定义了三个核心概念(Primitives),每个都有明确的职责边界:

原语 方向 用途 类比
Tools Client → Server AI 调用的可执行操作 REST API 的 POST 端点
Resources Server → Client Server 暴露的数据源 REST API 的 GET 端点
Prompts Server → Client 预定义的提示词模板 API 的文档/Schema

💡 **提示:**很多开发者把 Tools 和 Resources 搞混。简单判断标准:如果操作有副作用(写数据库、发邮件),用 Tool;如果只是读数据,用 Resource。

1.2 传输层:两种模式

MCP 支持两种传输方式,选择取决于你的部署场景:

特性 stdio(标准输入输出) HTTP + SSE
部署方式 本地进程 远程服务器
延迟 极低(本地) 取决于网络
多客户端 ❌ 单客户端 ✅ 多客户端
安全性 依赖操作系统隔离 需要认证授权
适用场景 IDE 插件、CLI 工具 Web 应用、SaaS 平台
生产推荐 ✅ 桌面端工具 ✅ 云端服务

⚠️ **警告:**不要在生产环境用 stdio 模式暴露到网络上。stdio 设计为进程间通信,不支持认证、不支持并发。远程场景必须用 HTTP + SSE。

1.3 消息格式

MCP 使用 JSON-RPC 2.0,所有消息都是标准的请求-响应格式:

// 请求示例:客户端调用 Tool
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "query_database",
    "arguments": {
      "sql": "SELECT * FROM users LIMIT 10"
    }
  }
}

// 响应示例
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"id\": 1, \"name\": \"Alice\"}, {\"id\": 2, \"name\": \"Bob\"}]"
      }
    ]
  }
}

🚀 二、从零构建一个 MCP Server

理论讲够了,直接写代码。我们用 TypeScript 构建一个数据库查询 MCP Server,支持 SQL 查询、表结构查看、数据导出三个功能。

2.1 项目初始化

# 初始化项目
mkdir mcp-db-server && cd mcp-db-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init

目录结构如下:

mcp-db-server/
├── src/
│   ├── index.ts          # 入口文件
│   ├── tools/            # 工具定义
│   │   ├── query.ts      # SQL 查询工具
│   │   ├── schema.ts     # 表结构工具
│   │   └── export.ts     # 数据导出工具
│   └── resources/        # 资源定义
│       └── tables.ts     # 表列表资源
├── tsconfig.json
└── package.json

2.2 定义工具(Tools)

每个 Tool 需要三要素:名称、描述、输入 Schema。用 Zod 做输入校验是最佳实践:

// src/tools/query.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import Database from "better-sqlite3";

export function registerQueryTool(server: McpServer, db: Database.Database) {
  server.tool(
    "query_database",
    "执行 SQL 查询并返回结果。支持 SELECT 查询,禁止执行 DDL/DML 操作。",
    {
      sql: z
        .string()
        .describe("要执行的 SQL 查询语句")
        .refine(
          (sql) => sql.trim().toUpperCase().startsWith("SELECT"),
          "只允许 SELECT 查询"
        ),
      limit: z
        .number()
        .min(1)
        .max(1000)
        .default(100)
        .describe("返回的最大行数"),
    },
    async ({ sql, limit }) => {
      try {
        // 安全检查:强制加 LIMIT
        const safeSql = sql.includes("LIMIT")
          ? sql
          : `${sql} LIMIT ${limit}`;

        const rows = db.prepare(safeSql).all();
        const columns = rows.length > 0 ? Object.keys(rows[0] as object) : [];

        return {
          content: [
            {
              type: "text" as const,
              text: JSON.stringify(
                {
                  success: true,
                  rowCount: rows.length,
                  columns,
                  data: rows,
                },
                null,
                2
              ),
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text" as const,
              text: JSON.stringify({
                success: false,
                error: error instanceof Error ? error.message : "Unknown error",
              }),
            },
          ],
          isError: true,
        };
      }
    }
  );
}

📌 **记住:**Zod Schema 不只是校验——它会自动转换为 MCP 的 inputSchema,AI 模型能直接理解参数含义。describe() 的文字会作为参数说明传给模型,写得越清晰,模型调用越准确。

2.3 定义资源(Resources)

Resource 用于暴露只读数据。与 Tool 不同,Resource 有固定的 URI 格式:

// src/resources/tables.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import Database from "better-sqlite3";

export function registerTableResource(server: McpServer, db: Database.Database) {
  // 注册动态资源:每个表一个 URI
  server.resource(
    "table-schema",
    "table://schema/{tableName}",
    { description: "获取指定表的列定义和索引信息" },
    async (uri, { tableName }) => {
      const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
      const indexes = db.prepare(`PRAGMA index_list(${tableName})`).all();

      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify({ table: tableName, columns, indexes }, null, 2),
          },
        ],
      };
    }
  );

  // 注册静态资源:表列表
  server.resource(
    "table-list",
    "table://list",
    { description: "获取数据库中所有表的列表" },
    async (uri) => {
      const tables = db
        .prepare("SELECT name FROM sqlite_master WHERE type='table'")
        .all()
        .map((row: any) => row.name);

      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify({ tables }, null, 2),
          },
        ],
      };
    }
  );
}

2.4 组装并启动 Server

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import Database from "better-sqlite3";
import { registerQueryTool } from "./tools/query.js";
import { registerSchemaTool } from "./tools/schema.js";
import { registerExportTool } from "./tools/export.js";
import { registerTableResource } from "./resources/tables.js";

async function main() {
  const dbPath = process.argv[2] || ":memory:";
  const db = new Database(dbPath);

  // 启用 WAL 模式,提升并发读性能
  db.pragma("journal_mode = WAL");

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

  // 注册所有工具和资源
  registerQueryTool(server, db);
  registerSchemaTool(server, db);
  registerExportTool(server, db);
  registerTableResource(server, db);

  // 使用 stdio 传输层启动
  const transport = new StdioServerTransport();
  await server.connect(transport);

  console.error("MCP DB Server started on stdio");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

启动后,在 Claude Desktop 的配置文件中添加:

{
  "mcpServers": {
    "db-server": {
      "command": "node",
      "args": ["dist/index.js", "/path/to/your/database.sqlite"]
    }
  }
}

⚡ 三、生产级进阶:认证、性能与安全

本地跑通只是第一步。生产环境还需要处理认证、限流、错误恢复等问题。

3.1 HTTP + SSE 传输层

远程部署时,用 HTTP 替代 stdio。MCP SDK 提供了 StreamableHTTPServerTransport

// src/http-server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { randomUUID } from "crypto";

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

// 存储活跃会话
const sessions = new Map<string, StreamableHTTPServerTransport>();

app.all("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (!sessionId && req.method === "POST") {
    // 新建会话
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (id) => {
        sessions.set(id, transport);
      },
    });

    const server = createServer(); // 你的 McpServer 实例
    await server.connect(transport);
    await transport.handleRequest(req, res);
  } else if (sessionId && sessions.has(sessionId)) {
    // 复用已有会话
    const transport = sessions.get(sessionId)!;
    await transport.handleRequest(req, res);
  } else {
    res.status(400).json({ error: "Invalid session" });
  }
});

app.listen(3001, () => {
  console.log("MCP HTTP Server listening on :3001");
});

3.2 安全防护清单

⚠️ **警告:**MCP Server 在 AI 生态中相当于一个 API 网关。一旦被恶意调用,后果比传统 API 注入更严重——攻击者可以通过 AI 模型间接执行任意操作。

生产环境必须做到以下几点:

  • ✅ **输入校验:**所有 Tool 参数用 Zod 严格校验,拒绝未知字段
  • ✅ **权限最小化:**数据库连接使用只读账号,文件系统用沙箱路径
  • ✅ **SQL 注入防护:**禁止字符串拼接 SQL,使用参数化查询
  • ✅ **速率限制:**每个会话每分钟最多 60 次 Tool 调用
  • ✅ **日志审计:**记录所有 Tool 调用的输入、输出和耗时
  • ❌ **避免:**在 Tool 中暴露 exec()eval() 等危险操作
  • ❌ **避免:**让 Tool 返回原始堆栈信息(信息泄露)

3.3 性能对比:stdio vs HTTP

我们在同一台机器(8核16GB)上测试两种传输层的性能:

指标 stdio HTTP + SSE
单次请求延迟 0.3ms 2.1ms
并发 100 请求吞吐 N/A(单客户端) 4,200 req/s
内存占用(空闲) 45MB 78MB
连接建立时间 0ms(进程级) 12ms(TLS 握手)
适用 QPS 无上限(本地) 5,000+

⚡ **关键结论:**本地工具(IDE 插件、CLI)用 stdio,零配置零延迟;远程服务用 HTTP + SSE,支持认证和并发。不要"为了统一"而强制用一种。

3.4 错误处理最佳实践

MCP 的错误分为三层,每层处理方式不同:

// 工具级别的优雅错误处理
async function handleToolCall(args: any) {
  try {
    const result = await executeQuery(args.sql);
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (error) {
    if (error instanceof ValidationError) {
      // 业务错误:返回给 AI 模型,让它理解并重试
      return {
        content: [{ type: "text", text: `参数错误:${error.message}` }],
        isError: true,
      };
    }
    if (error instanceof DatabaseError) {
      // 数据库错误:返回安全的错误信息
      return {
        content: [{ type: "text", text: `查询失败:${error.code}` }],
        isError: true,
      };
    }
    // 未知错误:记录日志,返回通用错误
    logger.error("Unexpected error", { error, args });
    return {
      content: [{ type: "text", text: "服务器内部错误,请稍后重试" }],
      isError: true,
    };
  }
}

💡 提示:isError: true 不会让 AI 停止工作——它会把错误信息作为上下文,尝试换一种方式调用。所以错误消息要写得对人类和 AI 都友好。

💡 四、开发经验与避坑指南

4.1 常见踩坑

坑 1:Tool 描述写得太模糊

❌ 避免做法:

{
  "name": "process",
  "description": "处理数据"
}

✅ 推荐做法:

{
  "name": "query_users",
  "description": "查询用户表,支持按姓名、邮箱、注册时间筛选。返回 JSON 数组,每行包含 id、name、email、created_at 字段。最多返回 100 行。"
}

AI 模型完全依赖描述来决定何时调用、如何传参。描述越精确,调用成功率越高。实测显示,详细的工具描述能让模型调用准确率从 60% 提升到 95%。

坑 2:忘记处理大结果集

一次返回 10 万行数据会撑爆 AI 模型的上下文窗口。必须分页:

const MAX_ROWS = 100;
const results = db.prepare(`${sql} LIMIT ${MAX_ROWS + 1}`).all();
const hasMore = results.length > MAX_ROWS;
const data = results.slice(0, MAX_ROWS);

return {
  content: [{
    type: "text",
    text: JSON.stringify({
      data,
      hasMore,
      totalShown: data.length,
      hint: hasMore ? "结果已截断,请添加更精确的筛选条件" : "已显示全部结果",
    }),
  }],
};

坑 3:stdio 模式下用 console.log

stdio 模式下 stdout 是协议通道,console.log 会破坏消息格式。调试信息必须用 console.error

// ❌ 错误写法:会污染协议消息
console.log("Processing query...");

// ✅ 正确写法:stderr 不影响协议
console.error("Processing query...");

4.2 调试工具

开发 MCP Server 时,推荐以下调试方式:

  1. MCP Inspector:官方提供的可视化调试工具,连接 Server 后可以直接调用 Tool、查看 Resource
  2. 日志中间件:在 Server 层加一个日志打印所有请求和响应
  3. 单元测试:用 InMemoryTransport 做端到端测试,不依赖真实传输
// 测试示例:用内存传输测试 Tool
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";

const [clientTransport, serverTransport] = InMemoryTransport.createPair();
const server = createServer();
await server.connect(serverTransport);

const client = new Client({ name: "test", version: "1.0.0" });
await client.connect(clientTransport);

const result = await client.callTool({
  name: "query_database",
  arguments: { sql: "SELECT 1 as test" },
});

console.assert(result.content[0].text.includes('"test": 1'));

📊 总结

MCP 协议用一个简单的 JSON-RPC 接口,统一了 AI 模型与外部工具的交互方式。它的设计哲学是"做好一件事"——不负责模型推理,不负责数据存储,只负责定义一套清晰的通信规范。

选择 MCP 的理由:

  • 🎯 **生态统一:**一次开发,所有支持 MCP 的 AI 客户端都能调用
  • 🎯 **协议稳定:**基于 JSON-RPC 2.0,成熟可靠
  • 🎯 **渐进式:**从最简单的 Tool 开始,逐步添加 Resource 和 Prompt

相关工具推荐:

  • 🔧 @modelcontextprotocol/sdk:官方 TypeScript SDK,开箱即用
  • 🔧 MCP Inspector:可视化调试工具,开发必备
  • 🔧 create-mcp-server:官方脚手架,npx create-mcp-server 一键初始化
  • 🔧 MCP Hub:MCP Server 注册中心,发现和分享你的 Server

📚 相关文章