计算机访问系统

构建你的第一个 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 将样板代码减到最少:

Terminal window
mkdir mcp-server && cd mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm 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:

Terminal window
claude mcp add my-server node /绝对路径/dist/index.js

Claude Code 在处理能从这些工具中获益的任务时会自动使用它们。

下一步构建什么

MCP 服务器本质上是一个能力边界:智能体能看到什么、能做什么。一些值得探索的方向:

  • 数据库服务器 — 为 Postgres、SQLite 或 DynamoDB 提供只读查询访问,附带 Schema 自省工具
  • 代码分析服务器 — 封装 tree-sitter 或语言服务器协议,为智能体提供超越简单文件读取的语义理解
  • 监控服务器 — 桥接指标系统(Prometheus、Datadog),让智能体用真实可观测数据调查事故
  • 文档服务器 — 索引并提供内部文档,让智能体无需知道所有内容的位置就能回答运营问题

MCP 生态系统仍处于早期阶段。大多数有用的服务器尚未被构建。构建解决你所在环境中真实问题的那个,协议会处理好其余的一切。


相关文章