MCP Server 开发实战:从零构建你的第一个 AI 工具服务器

手把手教你用 TypeScript 开发 MCP Server,涵盖 stdio/SSE 双传输模式、工具设计模式、认证安全、性能优化,附完整可运行代码和生产部署方案。

开发者效率 2026-05-29 16 分钟

当我们之前深入对比 MCP、Function Calling、A2A 三大协议时,很多读者追问:「道理我都懂,但 MCP Server 到底怎么写?」 根据 npm 下载数据,@modelcontextprotocol/sdk 的月下载量已突破 500 万,但社区中高质量的实战教程依然稀缺。本文将带你从零开始构建一个生产级 MCP Server,覆盖传输模式选择、工具设计、安全认证、性能优化等核心议题。

📌 **本文定位:**这不是 API 文档的搬运,而是基于真实项目经验的实战指南。如果你还在纠结「要不要用 MCP」,建议先读我们的 MCP vs Function Calling vs A2A 深度对比

🔧 一、MCP Server 架构与开发环境搭建

1.1 理解 MCP 的核心架构

MCP(Model Context Protocol)采用 Client-Server 架构,但这里的 Client 不是你的前端应用,而是 AI 应用(Host)内部的 MCP Client。整体数据流如下:

用户 → AI 应用(Host) → MCP Client → [传输层] → MCP Server → 外部资源

MCP Server 对外暴露三类能力:

能力类型 说明 典型场景
Tools 可被 LLM 调用的函数 查询数据库、调用 API、执行计算
Resources 可被读取的数据源 文件内容、数据库记录、配置信息
Prompts 预定义的提示词模板 代码审查模板、文档生成模板

⚠️ **关键区别:**Tools 是 LLM 主动调用的(需要模型推理决定何时调用),Resources 是应用层按需读取的(不经过模型决策)。这个区别决定了你的能力应该设计为 Tool 还是 Resource。

1.2 项目初始化与 SDK 配置

# 初始化项目
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
// tsconfig.json — 关键配置
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"]
}
// package.json — 关键字段
{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js"
  }
}

💡 提示:"type": "module""module": "Node16" 是 MCP SDK 的要求——它使用 ESM 模块系统。如果你的项目还在用 CommonJS,需要单独创建一个 ESM 子项目来承载 MCP Server。

🚀 二、实现你的第一个 MCP Server

2.1 基础 Server 骨架

我们以一个「代码工具箱」MCP Server 为例,它提供 JSON 格式化、Base64 编解码、Hash 计算等实用工具:

// src/index.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: "code-toolbox",
  version: "1.0.0",
  capabilities: {
    tools: {},
  },
});

// 注册工具:JSON 格式化
server.tool(
  "json_format",
  "格式化 JSON 字符串,支持自定义缩进",
  {
    input: z.string().describe("要格式化的 JSON 字符串"),
    indent: z.number().min(1).max(8).default(2).describe("缩进空格数"),
  },
  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 {
        isError: true,
        content: [{ type: "text", text: `JSON 解析失败: ${err.message}` }],
      };
    }
  }
);

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

main().catch(console.error);

这段代码有几个值得注意的设计决策:

  • ✅ 使用 zod 做参数校验——MCP SDK 原生支持 zod schema,它会自动转换为 JSON Schema 传递给 LLM
  • ✅ 错误处理返回 isError: true 而非抛异常——这会让 LLM 知道是工具执行失败,而不是系统崩溃
  • ✅ 日志输出到 stderr 而非 stdout——stdio 模式下 stdout 是 MCP 协议通道,混入日志会破坏通信

2.2 添加更多实用工具

// src/tools/hash.ts — Hash 计算工具
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { createHash } from "node:crypto";

export function registerHashTools(server: McpServer) {
  server.tool(
    "hash_calculate",
    "计算字符串的哈希值,支持 MD5/SHA1/SHA256/SHA512",
    {
      input: z.string().describe("要计算哈希的字符串"),
      algorithm: z
        .enum(["md5", "sha1", "sha256", "sha512"])
        .default("sha256")
        .describe("哈希算法"),
      encoding: z
        .enum(["hex", "base64"])
        .default("hex")
        .describe("输出编码格式"),
    },
    async ({ input, algorithm, encoding }) => {
      const hash = createHash(algorithm).update(input).digest(encoding);
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              { algorithm, encoding, input, hash },
              null,
              2
            ),
          },
        ],
      };
    }
  );
}

// src/tools/base64.ts — Base64 编解码工具
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function registerBase64Tools(server: McpServer) {
  server.tool(
    "base64_convert",
    "Base64 编码或解码",
    {
      input: z.string().describe("输入内容"),
      direction: z
        .enum(["encode", "decode"])
        .describe("编码(encode)或解码(decode)"),
    },
    async ({ input, direction }) => {
      try {
        const result =
          direction === "encode"
            ? Buffer.from(input).toString("base64")
            : Buffer.from(input, "base64").toString("utf-8");
        return {
          content: [{ type: "text", text: result }],
        };
      } catch (err) {
        return {
          isError: true,
          content: [{ type: "text", text: `转换失败: ${err.message}` }],
        };
      }
    }
  );
}
// src/index.ts — 组装所有工具
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { registerHashTools } from "./tools/hash.js";
import { registerBase64Tools } from "./tools/base64.js";

const server = new McpServer({
  name: "code-toolbox",
  version: "1.0.0",
  capabilities: { tools: {} },
});

// 注册内联工具
server.tool(
  "json_format",
  "格式化 JSON 字符串",
  {
    input: z.string().describe("要格式化的 JSON 字符串"),
    indent: z.number().min(1).max(8).default(2),
  },
  async ({ input, indent }) => {
    try {
      return {
        content: [{ type: "text", text: JSON.stringify(JSON.parse(input), null, indent) }],
      };
    } catch (err) {
      return { isError: true, content: [{ type: "text", text: `JSON 解析失败: ${err.message}` }] };
    }
  }
);

// 注册模块化工具
registerHashTools(server);
registerBase64Tools(server);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("code-toolbox MCP Server 已启动");
}

main().catch(console.error);

📌 **记住:**工具的 description 字段至关重要——LLM 根据它决定何时调用哪个工具。描述要准确、具体、包含使用场景,而非简单的「计算哈希」三个字。

⚡ 三、传输模式选择与生产级实践

3.1 Stdio vs SSE:如何选择

MCP 支持两种传输模式,选错会导致架构问题:

维度 Stdio SSE (Streamable HTTP)
通信方式 标准输入/输出 HTTP + Server-Sent Events
适用场景 本地 CLI 工具、IDE 插件 远程服务、Web 应用、多客户端
部署复杂度 极低,随应用启动 需要 Web 服务器
并发能力 单客户端 多客户端并发
网络要求 同机通信 跨网络访问
安全模型 操作系统级隔离 需要认证/授权

⚠️ **警告:**不要试图用 Stdio 做远程服务——它设计为进程间通信,跨网络使用会遇到缓冲、编码、断线等一系列问题。远程场景必须用 SSE。

3.2 SSE 传输模式实现

// src/sse-server.ts — SSE 传输模式(适用于远程部署)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { registerHashTools } from "./tools/hash.js";
import { registerBase64Tools } from "./tools/base64.js";

const app = express();
let transport: SSEServerTransport | null = null;

// SSE 连接端点
app.get("/sse", async (req, res) => {
  transport = new SSEServerTransport("/messages", res);
  await server.connect(transport);
});

// 消息接收端点
app.post("/messages", async (req, res) => {
  if (!transport) {
    res.status(400).json({ error: "No active SSE connection" });
    return;
  }
  await transport.handlePostMessage(req, res);
});

const server = new McpServer({
  name: "code-toolbox-remote",
  version: "1.0.0",
  capabilities: { tools: {} },
});

registerHashTools(server);
registerBase64Tools(server);

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`MCP SSE Server 运行在 http://localhost:${PORT}/sse`);
});

在 Claude Desktop 配置文件中连接远程 MCP Server:

{
  "mcpServers": {
    "code-toolbox": {
      "url": "http://localhost:3001/sse"
    }
  }
}

3.3 工具设计的黄金法则

在开发了多个 MCP Server 之后,我总结出几条核心设计原则:

法则一:工具粒度要适中

太粗——一个 database_query 工具接收原始 SQL,LLM 需要理解你的数据库 schema ❌ 太细——get_user_nameget_user_emailget_user_phone 三个工具,产生大量调用开销 ✅ 适中——search_users 接收关键词和过滤条件,返回结构化的用户列表

法则二:返回值要有结构

// ❌ 返回纯文本 — LLM 需要自己解析
return { content: [{ type: "text", text: "张三,28,北京" }] };

// ✅ 返回 JSON — LLM 可以直接理解和引用
return {
  content: [{
    type: "text",
    text: JSON.stringify({
      name: "张三",
      age: 28,
      city: "北京",
      source: "user_database",
      queriedAt: new Date().toISOString()
    }, null, 2)
  }]
};

法则三:错误信息要对 LLM 友好

// ❌ 技术性错误信息
return { isError: true, content: [{ type: "text", text: "ECONNREFUSED 127.0.0.1:5432" }] };

// ✅ LLM 可理解的错误信息 + 修复建议
return {
  isError: true,
  content: [{
    type: "text",
    text: JSON.stringify({
      error: "数据库连接失败",
      detail: "无法连接到 PostgreSQL 服务器 (localhost:5432)",
      suggestion: "请确认 PostgreSQL 服务已启动,或检查 host/port 配置",
      errorCode: "DB_CONNECTION_REFUSED"
    }, null, 2)
  }]
};

3.4 安全:不可忽视的生产要素

MCP Server 暴露的是可执行能力,安全问题比普通 API 更严重:

// src/middleware/auth.ts — SSE 模式的认证中间件
import { Request, Response, NextFunction } from "express";

export function mcpAuthMiddleware(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace("Bearer ", "");

  if (!token || !isValidToken(token)) {
    res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32001, message: "Unauthorized" },
      id: null,
    });
    return;
  }

  // 将用户信息附加到请求上下文
  (req as any).mcpUser = decodeToken(token);
  next();
}

// 在工具执行中进行权限检查
server.tool(
  "database_query",
  "查询数据库",
  { sql: z.string() },
  async ({ sql }, extra) => {
    const user = (extra as any).req?.mcpUser;

    // SQL 注入防护 + 权限检查
    if (!user?.permissions?.includes("db:read")) {
      return {
        isError: true,
        content: [{ type: "text", text: "权限不足:需要 db:read 权限" }],
      };
    }

    // 禁止危险操作
    const dangerous = /^\s*(DROP|DELETE|TRUNCATE|ALTER|CREATE)\s/i;
    if (dangerous.test(sql)) {
      return {
        isError: true,
        content: [{ type: "text", text: "安全限制:不允许执行 DDL/DML 操作" }],
      };
    }

    // 执行查询...
  }
);

⚠️ **警告:**永远不要把原始 SQL 执行能力直接暴露给 LLM。即使是只读查询,也应该限制表名白名单、行数上限和超时时间。LLM 可能被 Prompt Injection 攻击诱导执行恶意查询。

3.5 测试你的 MCP Server

MCP Server 的测试分为两层:单元测试工具逻辑,集成测试协议通信。

// tests/json-format.test.ts — 工具逻辑单元测试
import { describe, it, expect } from "vitest";

// 直接测试工具的核心逻辑,不涉及 MCP 协议
function formatJson(input: string, indent: number = 2): string {
  return JSON.stringify(JSON.parse(input), null, indent);
}

describe("json_format", () => {
  it("应该正确格式化 JSON", () => {
    const input = '{"name":"张三","age":28}';
    const result = formatJson(input);
    expect(result).toContain("  "); // 有缩进
    expect(JSON.parse(result).name).toBe("张三");
  });

  it("无效 JSON 应该抛出错误", () => {
    expect(() => formatJson("{invalid}")).toThrow();
  });
});
# 使用 MCP Inspector 进行协议级测试
npx @modelcontextprotocol/inspector node dist/index.js

Inspector 会启动一个 Web UI,你可以手动调用工具、查看请求/响应的原始 JSON-RPC 消息,非常适合调试工具定义和参数校验问题。

🎯 四、总结与进阶方向

构建一个可用的 MCP Server 很简单,但构建一个生产级 MCP Server 需要考虑更多:

维度 入门级 生产级
传输模式 Stdio only Stdio + SSE 双模式
错误处理 try-catch 结构化错误 + 重试策略
认证 Bearer Token + 权限矩阵
监控 console.log OpenTelemetry 集成
测试 手动测试 单元测试 + Inspector
部署 本地进程 Docker + 健康检查

我的建议:

  • ✅ 从 Stdio 模式开始开发和测试,用 Inspector 验证功能
  • ✅ 工具设计遵循「一个工具做好一件事」的原则
  • ✅ 返回值用 JSON 结构化格式,错误信息要对 LLM 友好
  • ✅ SSE 模式必须加认证,Stdio 模式依赖操作系统权限
  • ❌ 不要在一个 Server 里塞太多工具——工具越多,LLM 选择越困难,上下文占用越大
  • ❌ 不要把 MCP Server 当作通用 API 网关——它是为 LLM 优化的协议,不是 REST API 的替代品

MCP 生态仍在快速演进。2026 年下半年,我们预计会看到更多标准化的认证方案(OAuth 2.1 集成已在讨论中)、工具发现协议(类似 API 的 OpenAPI Spec),以及跨 Server 的编排能力。现在投入 MCP Server 开发,正是最好的时机。

关键结论:MCP Server 的核心不是协议实现,而是工具设计能力。一个好的工具描述、合理的参数设计、友好的错误处理,远比花哨的传输层更重要。从一个小工具开始,逐步迭代,这才是正确的节奏。


🔗 相关工具推荐:

📚 相关文章