Model Context Protocol(MCP)发布不到一年,GitHub 上已有超过 15,000 个 MCP Server 仓库,但其中能真正用于生产环境的不足 5%。问题不在协议本身——MCP 的 JSON-RPC 2.0 通信模型设计得相当优雅——而在于大多数开发者在构建 MCP Server 时,跳过了资源抽象、错误处理和安全隔离这些关键工程环节。本文将从实战角度出发,用 TypeScript 从零构建一个生产级 MCP Server,覆盖架构设计、工具定义、资源管理、安全防护和性能优化,并给出可直接复用的代码模板。
🏗️ 一、MCP 协议核心与架构设计
理解 MCP 的三层模型
MCP 协议定义了三个核心概念:Tools(工具,让 LLM 执行操作)、Resources(资源,为 LLM 提供上下文数据)和 Prompts(提示模板,标准化交互模式)。大多数开发者只关注 Tools,但实际上 Resources 才是 MCP Server 的价值核心——它让 AI 能以结构化方式访问你的数据。
MCP 通信基于 JSON-RPC 2.0,支持两种传输模式:
| 传输方式 | 适用场景 | 延迟 | 连接模式 | 生产适用性 |
|---|---|---|---|---|
| Stdio | 本地 CLI 工具、IDE 插件 | 极低(<1ms) | 进程间管道 | ⚠️ 仅限本地 |
| SSE (Server-Sent Events) | 远程服务、Web 应用 | 低(10-50ms) | HTTP 长连接 | ✅ 推荐 |
| Streamable HTTP | 云原生、Serverless | 中(50-100ms) | 无状态 HTTP | ✅ 新标准 |
💡 **提示:**MCP 2025-11 规范已引入 Streamable HTTP 传输,它是 SSE 的演进版本,支持无状态部署和断线重连。新项目应优先选择 Streamable HTTP。
项目结构设计
一个生产级 MCP Server 的目录结构应该是这样的:
my-mcp-server/
├── src/
│ ├── index.ts # 入口,传输层初始化
│ ├── server.ts # MCP Server 实例配置
│ ├── tools/ # 工具定义
│ │ ├── index.ts # 工具注册表
│ │ ├── query-database.ts # 具体工具实现
│ │ └── send-message.ts
│ ├── resources/ # 资源定义
│ │ ├── index.ts
│ │ └── schema-resource.ts
│ └── utils/
│ ├── auth.ts # 认证中间件
│ ├── logger.ts # 结构化日志
│ └── errors.ts # 错误处理
├── package.json
└── tsconfig.json
⚠️ **关键原则:**每个工具一个文件,工具定义与业务逻辑分离。这样可以独立测试每个工具,也方便后续维护。
🔧 二、从零实现一个 MCP Server
环境搭建与依赖安装
# 初始化项目
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
zod 是必需的——MCP SDK 使用 Zod schema 来定义工具参数,它会自动转换为 JSON Schema 供 LLM 理解。
💡 **提示:**如果你使用 ESM 模块系统(推荐),确保
tsconfig.json中设置"module": "ESNext"和"moduleResolution": "bundler",否则 MCP SDK 的子路径导入(如/server/mcp.js)会报模块解析错误。以下是一个经过验证的配置:
// tsconfig.json — MCP Server 推荐配置
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
package.json 中需要设置 "type": "module" 以启用 ESM。如果使用 tsx 运行开发服务器,它会自动处理 TypeScript 编译,无需额外的构建步骤。
核心 Server 实例
以下是 MCP Server 的骨架代码,注意 server.ts 中的错误处理模式:
// src/server.ts — MCP Server 核心配置
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function createServer(): McpServer {
const server = new McpServer({
name: "production-mcp-server",
version: "1.0.0",
capabilities: {
tools: {},
resources: { subscribe: true },
prompts: {},
},
});
// 注册工具:查询数据库
server.tool(
"query-database",
"执行只读 SQL 查询,返回结构化结果",
{
sql: z.string().describe("SELECT 查询语句,不支持 INSERT/UPDATE/DELETE"),
database: z.enum(["analytics", "users", "products"]).describe("目标数据库"),
limit: z.number().min(1).max(1000).default(100).describe("最大返回行数"),
},
async ({ sql, database, limit }) => {
// 参数验证:阻止写操作
const normalized = sql.trim().toUpperCase();
if (!normalized.startsWith("SELECT")) {
return {
content: [{ type: "text", text: "❌ 错误:仅支持 SELECT 查询" }],
isError: true,
};
}
try {
const results = await executeQuery(database, sql, limit);
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2),
}],
};
} catch (error) {
return {
content: [{
type: "text",
text: `查询执行失败: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
}
);
// 注册资源:数据库 Schema
server.resource(
"database-schema",
"db://schema",
{ description: "提供当前数据库的表结构和字段说明" },
async (uri) => {
const schema = await getDatabaseSchema();
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(schema, null, 2),
}],
};
}
);
return server;
}
这段代码有三个值得关注的设计决策:
- 工具参数用 Zod 定义——类型安全 + 自动生成 JSON Schema,LLM 能准确理解参数约束。每个字段的
.describe()会直接暴露给 LLM 作为参数说明 - 工具内部做二次验证——即使 LLM 可能绕过 schema 约束,服务端也要拦截危险操作。永远不要信任来自客户端的任何输入
- 错误返回统一格式——
isError: true告知调用方这不是正常结果,而不是抛出未捕获异常导致整个 Server 崩溃
除了核心工具外,你还可以注册 Prompt 模板,让 LLM 以标准化方式与服务交互。例如一个「生成报表」的 Prompt 可以预设好参数格式和输出模板,减少 LLM 的试错次数。
工具设计的核心原则是单一职责:每个工具只做一件事,但要把这件事做到极致。一个「查询数据库」工具如果同时支持查询、更新、删除,不仅安全风险高,LLM 也容易误用。拆分成 query-database、describe-table、get-row-count 三个独立工具,效果反而更好。
Stdio 传输层实现
最简单的启动方式是 Stdio,适合本地 CLI 和 IDE 集成:
// src/index.ts — Stdio 传输入口
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";
async function main() {
const server = createServer();
const transport = new StdioServerTransport();
// 优雅关闭
process.on("SIGINT", async () => {
await server.close();
process.exit(0);
});
await server.connect(transport);
console.error("MCP Server started (stdio mode)");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
⚠️ **警告:**Stdio 模式下,
console.log会污染 JSON-RPC 通信管道。所有日志必须输出到stderr(console.error),或者使用文件日志。建议封装一个专用日志工具:
// src/utils/logger.ts — Stdio 安全日志封装
export const logger = {
info: (...args: unknown[]) =>
process.stderr.write(`[INFO ${new Date().toISOString()}] ${args.join(" ")}\n`),
warn: (...args: unknown[]) =>
process.stderr.write(`[WARN ${new Date().toISOString()}] ${args.join(" ")}\n`),
error: (...args: unknown[]) =>
process.stderr.write(`[ERROR ${new Date().toISOString()}] ${args.join(" ")}\n`),
};
这样在 IDE 集成时(如 Cursor、VS Code),日志会出现在 Output 面板而不会干扰 MCP 通信管道。
Streamable HTTP 传输层(生产推荐)
对于需要远程访问的场景,使用 Streamable HTTP 传输:
⚠️ **警告:**Streamable HTTP 是 MCP 2025-11 规范引入的新传输方式,部分旧版客户端可能不支持。如果需要兼容旧客户端,可同时暴露 SSE 端点作为回退。
// src/http-entry.ts — HTTP 传输入口(生产部署用)
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "./server.js";
import { randomUUID } from "node:crypto";
const app = express();
app.use(express.json());
// 会话管理
const sessions = new Map<string, StreamableHTTPServerTransport>();
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && sessions.has(sessionId)) {
// 已有会话,复用 transport
const transport = sessions.get(sessionId)!;
await transport.handleRequest(req, res);
return;
}
// 新会话
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
sessions.set(id, transport);
},
});
transport.onclose = () => {
if (transport.sessionId) sessions.delete(transport.sessionId);
};
const server = createServer();
await server.connect(transport);
await transport.handleRequest(req, res);
});
app.get("/mcp", (req, res) => {
// SSE 流式连接
const sessionId = req.headers["mcp-session-id"] as string;
if (!sessionId || !sessions.has(sessionId)) {
res.status(400).json({ error: "Invalid session" });
return;
}
sessions.get(sessionId)!.handleRequest(req, res);
});
app.listen(3100, () => {
console.log("MCP HTTP Server on :3100");
});
🚀 三、生产级最佳实践与避坑指南
🔐 安全防护:不可忽视的三层防线
MCP Server 的安全问题被严重低估。根据公开安全审计报告,超过 60% 的社区 MCP Server 存在至少一个高危漏洞,最常见的是未过滤的参数注入和缺少速率限制。一个暴露在网络中的 MCP Server,如果没有做好防护,等同于把数据库直接开放给互联网。
第一层:输入验证与 SQL 注入防护
// src/utils/sanitize.ts — SQL 注入检测
const DANGEROUS_PATTERNS = [
/;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|TRUNCATE)/i,
/UNION\s+SELECT/i,
/--\s/, // SQL 注释
/\/\*[\s\S]*?\*\//, // 块注释
/0x[0-9a-f]+/i, // 十六进制编码
/CHAR\s*\(/i, // 字符函数绕过
];
export function validateSql(sql: string): { safe: boolean; reason?: string } {
// 只允许 SELECT
if (!/^\s*SELECT\s/i.test(sql)) {
return { safe: false, reason: "仅支持 SELECT 查询" };
}
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(sql)) {
return { safe: false, reason: `检测到危险模式: ${pattern.source}` };
}
}
// 长度限制
if (sql.length > 10000) {
return { safe: false, reason: "查询语句过长" };
}
return { safe: true };
}
第二层:速率限制与资源隔离
MCP 客户端可能发送大量并发请求(特别是 AI Agent 场景),必须限制资源使用:
// src/utils/rate-limiter.ts — 滑动窗口限流器
export class RateLimiter {
private windows = new Map<string, number[]>();
constructor(
private maxRequests: number = 30,
private windowMs: number = 60_000
) {}
check(clientId: string): { allowed: boolean; retryAfterMs?: number } {
const now = Date.now();
const window = this.windows.get(clientId) ?? [];
const valid = window.filter((t) => now - t < this.windowMs);
if (valid.length >= this.maxRequests) {
const oldest = valid[0];
return { allowed: false, retryAfterMs: this.windowMs - (now - oldest) };
}
valid.push(now);
this.windows.set(clientId, valid);
return { allowed: true };
}
}
第三层:权限控制与审计日志
每个工具调用都应该记录审计日志,包括调用者、参数和执行结果。这是排查安全事件的关键数据。
// src/utils/audit.ts — 审计日志记录
interface AuditEntry {
timestamp: string;
toolName: string;
params: Record<string, unknown>;
result: "success" | "error";
durationMs: number;
clientId: string;
}
export function auditLog(entry: AuditEntry): void {
// 生产环境应写入专用审计日志文件或发送到日志服务
process.stderr.write(`[AUDIT] ${JSON.stringify(entry)}\n`);
}
审计日志不仅用于安全排查,还能帮助你分析哪些工具被频繁调用、哪些参数组合容易出错,从而持续优化工具设计。
📊 性能优化:资源订阅与缓存
MCP 支持资源订阅(Resource Subscription),当资源变化时主动推送更新。这对于数据库 Schema 等相对静态但偶尔变化的资源非常有用。
| 优化策略 | 适用场景 | 复杂度 | 效果 |
|---|---|---|---|
| 结果缓存 | 重复查询、静态数据 | 低 | ⚡ 延迟降低 90% |
| 资源订阅 | Schema、配置变化 | 中 | 减少轮询开销 |
| 分页返回 | 大结果集 | 低 | 避免 token 浪费 |
| 连接池 | 数据库查询 | 中 | 并发能力 5-10x |
📌 **记住:**LLM 的上下文窗口是有限的。一个返回 10,000 行数据的工具,不仅浪费 token 费用,还可能超出 LLM 的处理能力。始终设置合理的
limit参数默认值。
⚠️ 常见坑点清单
坑 1:Zod schema 的 .describe() 不是可选的。 如果不写 describe,LLM 无法理解参数的用途,会频繁使用错误参数。每个字段都必须有清晰的中文描述。
坑 2:工具返回值不是纯文本。 MCP 支持多种内容类型(text、image、resource),但很多开发者只返回 text。对于结构化数据,应该返回格式化的 JSON;对于二进制数据,使用 base64 编码。
坑 3:Stdio 模式下的缓冲问题。 Node.js 的 stdout 是行缓冲的,大量数据输出时可能出现截断。解决方案是使用 MCP SDK 提供的 StdioServerTransport,它已经处理了消息分帧。
坑 4:忘记处理 LLM 幻觉调用。 LLM 有时会调用不存在的工具或传入格式错误的参数。MCP SDK 会返回标准错误,但你的工具函数也应该对边界情况返回清晰的错误信息,而不是抛出未捕获异常。
坑 5:资源 URI 设计不合理。 资源 URI 应该具有层次结构和可读性(如 db://analytics/orders/schema),而不是无意义的 ID。LLM 会根据 URI 来理解资源的语义,好的 URI 设计能显著提高 LLM 选择正确资源的准确率。
坑 6:忽略并发安全。 当多个 AI Agent 同时连接同一个 MCP Server 时,共享状态(如数据库连接池、缓存)需要考虑并发安全。使用不可变数据结构或适当的锁机制,避免竞态条件。
🎯 MCP Server 质量检查清单
发布前,对照以下清单逐项检查:
- ✅ 所有工具有清晰的
description(中文,50字以内) - ✅ 所有参数有
z.describe()说明 - ✅ 工具内部做了权限验证(不信任 LLM 的输入)
- ✅ 查询工具有数量限制(防止返回海量数据)
- ✅ 写操作工具有确认机制或二次验证
- ✅ 错误信息对开发者友好(包含原因和修复建议)
- ✅ 有请求日志和审计追踪
- ✅ Stdio 模式下无
console.log污染 - ✅ 提供了
README.md和配置示例
💡 总结与资源推荐
MCP 协议正处于快速增长期,但「能用」和「能用于生产」之间差距巨大。本文的核心观点是:MCP Server 的质量不在于工具数量,而在于每个工具的安全性、可靠性和描述质量。 一个有 5 个设计精良工具的 MCP Server,远胜于有 50 个粗制滥造工具的版本。
推荐的技术栈组合:
| 层面 | 推荐方案 | 备选方案 |
|---|---|---|
| SDK | @modelcontextprotocol/sdk |
自研(不推荐) |
| 参数验证 | Zod | TypeBox |
| HTTP 框架 | Express 5 / Hono | Fastify |
| 日志 | Pino | Winston |
| 测试 | MCP Inspector + Vitest | 手动测试 |
实用工具推荐:
- ✅ MCP Inspector——官方调试工具,可视化测试工具调用
- ✅ MCP Server Creator——快速生成项目脚手架
- ✅ Smithery——MCP Server 注册与发现平台
⚡ **关键结论:**构建 MCP Server 的核心不是「让 AI 能调用」,而是「让 AI 安全、高效、正确地调用」。把每个工具当作一个 API endpoint 来设计,你就能避开 90% 的坑。