当我们之前深入对比 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_name、get_user_email、get_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 的核心不是协议实现,而是工具设计能力。一个好的工具描述、合理的参数设计、友好的错误处理,远比花哨的传输层更重要。从一个小工具开始,逐步迭代,这才是正确的节奏。
🔗 相关工具推荐:
- jsjson.com JSON 格式化工具 — 开发 MCP 工具时格式化调试数据
- jsjson.com Base64 编解码工具 — 测试 Base64 相关工具的输入输出
- jsjson.com Hash 计算工具 — 验证哈希计算工具的正确性