构建你的第一个 MCP 服务器
Model Context Protocol(MCP)是 AI 生态系统中最接近通用适配器的存在。它定义了 AI 模型如何发现和调用工具,以及这些工具如何返回结构化结果。一旦你的 MCP 服务器运行起来,任何兼容的客户端——Claude、Claude Code,或任何支持 MCP 的智能体框架——都可以直接使用它,无需额外的集成代码。
本指南将从零开始构建一个真实的 MCP 服务器。不是玩具示例,而是可以扩展到生产环境的模式。
MCP 究竟是什么
MCP 是基于 stdio 的 JSON-RPC 2.0 协议(远程服务器也支持 HTTP+SSE)。客户端和服务器交换一小组消息类型:
tools/list— 客户端查询可用工具列表tools/call— 客户端传入参数调用某个工具resources/list/resources/read— 客户端读取文件、数据库,或服务器暴露的任何上下文
服务器预先声明其工具,并为每个工具的输入提供 JSON Schema。客户端使用该 Schema 构造有效调用。从客户端角度看,整个交互是无状态的——服务器在内部管理所有必要的状态。
这种简洁性正是设计目的。MCP 不是框架,而是一份契约:它让你可以将任何东西——Postgres 数据库、Kubernetes 集群、GitHub 仓库、内部专有 API——封装起来,作为一组类型化、可调用的函数暴露给任何 AI 模型。
环境配置
你需要 Node.js 18+ 和一个 TypeScript 项目。官方 MCP SDK 将样板代码减到最少:
mkdir mcp-server && cd mcp-servernpm init -ynpm install @modelcontextprotocol/sdk zodnpm install -D typescript @types/node tsx添加 tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "outDir": "./dist" }, "include": ["src"]}在 package.json 中添加启动脚本:
{ "scripts": { "dev": "tsx src/index.ts", "build": "tsc", "start": "node dist/index.js" }}构建服务器
创建 src/index.ts。结构始终相同:初始化服务器、注册工具、连接到 stdio 传输。
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: "my-mcp-server", version: "1.0.0",});现在注册工具。每个工具都有名称、模型用于决定何时调用它的描述,以及输入 Schema:
server.tool( "get_weather", "获取某个城市的当前天气状况", { city: z.string().describe("要查询天气的城市名称"), units: z.enum(["celsius", "fahrenheit"]).default("celsius"), }, async ({ city, units }) => { // 你的实际实现 const data = await fetchWeather(city, units); return { content: [ { type: "text", text: `${city} 天气:${data.temp}°${units === "celsius" ? "C" : "F"},${data.condition}`, }, ], }; });将服务器连接到 stdio 传输:
const transport = new StdioServerTransport();await server.connect(transport);这就是完整的服务器骨架。其余所有工作都是添加工具。
工具设计模式
MCP 服务器的质量取决于你如何设计工具。以下几个模式至关重要:
窄而可组合的工具,而非宽泛的单体
一个只做一件事的工具比做很多事的工具更有用。模型可以以意想不到的方式组合窄工具;宽工具则会限制它。
避免这样做:
server.tool("manage_database", "对数据库执行任何操作", { operation: z.enum(["read", "write", "delete", "schema"]), query: z.string(), // ... 许多可选参数})推荐这样做:
server.tool("query_records", "执行 SELECT 查询并以 JSON 形式返回结果行", { table: z.string(), where: z.string().optional().describe("SQL WHERE 子句,例如 'status = active'"), limit: z.number().default(50),})
server.tool("get_schema", "返回某张表的列定义", { table: z.string(),})精确描述输入
模型读取你的 describe() 字符串来决定传什么参数。把它们当作给从未见过你代码的开发者写的文档:
{ date_range: z.string().describe( "ISO 8601 日期范围,格式为 YYYY-MM-DD/YYYY-MM-DD。" + "示例:2025-01-01/2025-03-31" ),}描述模糊会产生错误输入,描述精确会产生正确输入。
返回结构化文本,而非原始 JSON
模型解析工具结果中的文本比解析原始 JSON blob 更可靠。格式化你的输出:
return { content: [ { type: "text", text: [ `找到 ${rows.length} 条记录:`, ...rows.map(r => `- ${r.id}:${r.name}(${r.status})`), ].join("\n"), }, ],};错误处理
将错误作为工具结果返回,而非抛出异常。isError: true 标志向智能体发出失败信号,让它决定如何恢复:
async ({ table, where }) => { try { const rows = await db.query(table, where); return { content: [{ type: "text", text: formatRows(rows) }] }; } catch (err) { return { content: [ { type: "text", text: `查询失败:${err instanceof Error ? err.message : "未知错误"}。` + `请检查表名是否正确,以及 WHERE 子句是否使用了有效的列名。`, }, ], isError: true, }; }}实际案例:GitHub 工具服务器
以下是一个具体的工具,封装 GitHub API 让智能体读取仓库内容:
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
server.tool( "get_file_contents", "从 GitHub 仓库读取文件内容", { owner: z.string().describe("仓库所有者(用户名或组织名)"), repo: z.string().describe("仓库名称"), path: z.string().describe("仓库内的文件路径"), ref: z.string().optional().describe("分支、标签或 commit SHA,默认为默认分支"), }, async ({ owner, repo, path, ref }) => { try { const { data } = await octokit.rest.repos.getContent({ owner, repo, path, ref });
if (Array.isArray(data)) { const listing = data.map(f => `${f.type === "dir" ? "📁" : "📄"} ${f.name}`).join("\n"); return { content: [{ type: "text", text: `${path} 目录列表:\n${listing}` }], }; }
if (data.type !== "file" || !data.content) { return { content: [{ type: "text", text: `${path} 不是可读文件` }], isError: true, }; }
const content = Buffer.from(data.content, "base64").toString("utf-8"); return { content: [{ type: "text", text: `${owner}/${repo}/${path} 的内容:\n\`\`\`\n${content}\n\`\`\`` }], }; } catch (err: unknown) { const message = err instanceof Error ? err.message : "未知错误"; return { content: [{ type: "text", text: `读取 ${path} 失败:${message}` }], isError: true, }; } });连接到 Claude Desktop
服务器运行后,通过编辑配置文件将其连接到 Claude Desktop——macOS 位于 ~/Library/Application Support/Claude/claude_desktop_config.json,Windows 位于 %APPDATA%\Claude\claude_desktop_config.json:
{ "mcpServers": { "my-server": { "command": "node", "args": ["/绝对路径/dist/index.js"], "env": { "GITHUB_TOKEN": "你的_token" } } }}重启 Claude Desktop,你的工具会自动出现在工具选择器中。
连接到 Claude Code
MCP 服务器通过 CLI 连接到 Claude Code:
claude mcp add my-server node /绝对路径/dist/index.jsClaude Code 在处理能从这些工具中获益的任务时会自动使用它们。
下一步构建什么
MCP 服务器本质上是一个能力边界:智能体能看到什么、能做什么。一些值得探索的方向:
- 数据库服务器 — 为 Postgres、SQLite 或 DynamoDB 提供只读查询访问,附带 Schema 自省工具
- 代码分析服务器 — 封装 tree-sitter 或语言服务器协议,为智能体提供超越简单文件读取的语义理解
- 监控服务器 — 桥接指标系统(Prometheus、Datadog),让智能体用真实可观测数据调查事故
- 文档服务器 — 索引并提供内部文档,让智能体无需知道所有内容的位置就能回答运营问题
MCP 生态系统仍处于早期阶段。大多数有用的服务器尚未被构建。构建解决你所在环境中真实问题的那个,协议会处理好其余的一切。