从零构建 MCP Server:让 AI 大模型调用你的自定义工具

手把手教你用 TypeScript 构建 Model Context Protocol (MCP) Server,让 Claude、GPT 等大模型安全调用你的本地工具和数据源,含完整代码与生产部署方案。

前端开发 2026-06-11 12 分钟

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 直接调用。核心要点:

  1. Tool 是核心——大部分场景只需要实现 Tool,Resource 和 Prompt 是锦上添花
  2. Zod schema 是你的安全防线——所有输入必须严格验证
  3. stdio 适合本地,Streamable HTTP 适合远程——不要用已废弃的 SSE
  4. 安全第一——AI 的输出等于用户输入,永远不要信任

相关工具推荐:jsjson.com JSON 格式化工具 可以快速验证 MCP 返回的 JSON 数据;jsjson.com Base64 编解码 可用于调试 MCP 协议中的编码问题。

📚 相关文章