《MCP 协议设计与实现》完整目录
- 前言
- 第1章 为什么需要 MCP
- 第02章 架构总览:Host-Client-Server 模型
- 第03章 JSON-RPC 与消息格式
- 第04章 生命周期与能力协商
- 第05章 Tool:让 Agent 调用世界
- 第6章 Resource:结构化的上下文注入
- 第7章 Prompt:可复用的交互模板
- 第8章 TypeScript Server 实现剖析
- 第09章 TypeScript Client 实现剖析
- 第10章 Python Server 实现剖析
- 第11章 Python Client 实现剖析
- 第12章 STDIO 传输:本地进程通信
- 第13章 Streamable HTTP:远程流式传输
- 第14章 SSE 与 WebSocket
- 第15章 OAuth 2.1 认证框架
- 第16章 服务发现与客户端注册
- 第17章 sampling
- 第18章 Elicitation、Roots 与配置管理
- 第19章 Claude Code 的 MCP 客户端:12 万行的实战
- 第20章 从零构建一个生产级 MCP Server(当前)
- 第21章 设计模式与架构决策
第20章 从零构建一个生产级 MCP Server
"The best way to learn a protocol is to build something real with it --- not a hello world, but something you'd actually deploy."
:::tip 本章要点
- 从需求分析到技术选型:TypeScript 与 Python SDK 的取舍考量
- 实现完整的 Tools、Resources、Prompts 三大原语
- 为 Server 添加 OAuth 认证与安全防护
- 编写协议级集成测试
- 部署策略:stdio 本地模式与 Streamable HTTP 远程模式
- 发布到 MCP 生态 :::
20.1 我们要构建什么
本章将构建一个 GitHub MCP Server------让 AI 助手能够与 GitHub 仓库交互。这不是一个玩具示例,而是一个覆盖了 MCP 核心特性的真实集成:
| MCP 原语 | 实现内容 |
|---|---|
| Tools | 创建 Issue、搜索仓库、创建 PR |
| Resources | 暴露仓库文件内容、Issue 列表 |
| Prompts | 提供 Code Review、Bug Report 模板 |
| Completion | 仓库名、分支名自动补全 |
| Progress | 批量操作的进度追踪 |
20.2 技术选型:TypeScript vs Python
20.2.1 两个 SDK 的定位差异
MCP 官方提供两个 Tier-1 SDK,各有所长:
选 TypeScript 的理由:
- 如果你的 Server 主要做 Web API 集成(如 GitHub、Slack、Jira),TypeScript 的异步模型和 npm 生态更成熟
- Zod +
completable()的组合让参数验证和自动补全的 DX 极佳 - 前端/全栈团队更容易上手
选 Python 的理由:
- 如果你的 Server 涉及数据分析、机器学习模型调用,Python 生态无可替代
@server.tool()装饰器语法更简洁,类型推断基于 Python 原生 type hints- 数据科学团队更熟悉
本章以 TypeScript 为主线,关键差异处会给出 Python 的对照。
20.3 项目初始化
20.3.1 创建项目
bash
mkdir mcp-server-github && cd mcp-server-github
npm init -y
npm install @modelcontextprotocol/server @modelcontextprotocol/core zod
npm install -D typescript @types/node vitest
TypeScript 配置:
json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src"]
}
20.3.2 入口文件
typescript
// src/index.ts
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server';
const server = new McpServer({
name: 'github-mcp-server',
version: '1.0.0'
}, {
capabilities: {
logging: {},
completions: {}
},
instructions: '这是一个 GitHub 集成 Server。使用前请确保已配置 GITHUB_TOKEN。' +
'建议先用 search_repos 查找仓库,再用具体的工具操作。'
});
// 注册 Tools、Resources、Prompts(后续各节详述)
// ...
// 启动
const transport = new StdioServerTransport();
await server.connect(transport);
instructions 字段值得注意------它不是给人看的,而是给 LLM 看的。好的 instructions 能显著提高 LLM 选择正确工具的准确率。
20.4 实现 Tools
20.4.1 搜索仓库
typescript
import * as z from 'zod/v4';
import { completable } from '@modelcontextprotocol/server';
// GitHub API 客户端(简化版)
async function githubFetch(path: string, options?: RequestInit) {
const token = process.env.GITHUB_TOKEN;
if (!token) throw new Error('GITHUB_TOKEN 环境变量未设置');
const resp = await fetch(`https://api.github.com${path}`, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'mcp-server-github/1.0',
...options?.headers
}
});
if (!resp.ok) {
throw new Error(`GitHub API 错误: ${resp.status} ${resp.statusText}`);
}
return resp.json();
}
// 搜索仓库工具
server.registerTool(
'search_repos',
{
title: '搜索 GitHub 仓库',
description: '根据关键词搜索 GitHub 仓库,返回仓库名、描述、Star 数等信息',
inputSchema: z.object({
query: z.string().describe('搜索关键词,支持 GitHub 搜索语法'),
language: completable(
z.string().optional().describe('编程语言过滤'),
(value) => ['typescript', 'javascript', 'python', 'rust', 'go', 'java', 'c++']
.filter(lang => lang.startsWith(value.toLowerCase()))
),
sort: z.enum(['stars', 'forks', 'updated']).default('stars')
.describe('排序方式'),
limit: z.number().min(1).max(30).default(10)
.describe('返回结果数量')
}),
annotations: {
readOnlyHint: true,
openWorldHint: true
}
},
async ({ query, language, sort, limit }) => {
let q = query;
if (language) q += ` language:${language}`;
const data = await githubFetch(
`/search/repositories?q=${encodeURIComponent(q)}&sort=${sort}&per_page=${limit}`
);
const repos = data.items.map((repo: any) => ({
name: repo.full_name,
description: repo.description || '无描述',
stars: repo.stargazers_count,
language: repo.language,
url: repo.html_url
}));
return {
content: [{
type: 'text',
text: JSON.stringify(repos, null, 2)
}]
};
}
);
注意 annotations 的使用------readOnlyHint: true 告诉 Client 这个工具不会修改任何数据,openWorldHint: true 表示它访问外部网络。这些元信息帮助 Client 做出更好的权限决策。
20.4.2 创建 Issue
typescript
server.registerTool(
'create_issue',
{
title: '创建 GitHub Issue',
description: '在指定仓库中创建一个新的 Issue',
inputSchema: z.object({
repo: completable(
z.string().describe('仓库全名,格式: owner/repo'),
async (value) => {
// 动态补全:搜索用户有权限的仓库
try {
const data = await githubFetch(
`/search/repositories?q=${encodeURIComponent(value)}+in:name&per_page=5`
);
return data.items.map((r: any) => r.full_name);
} catch {
return [];
}
}
),
title: z.string().min(1).describe('Issue 标题'),
body: z.string().optional().describe('Issue 正文(Markdown 格式)'),
labels: z.array(z.string()).optional().describe('标签列表'),
assignees: z.array(z.string()).optional().describe('指派人列表')
}),
annotations: {
destructiveHint: false,
idempotentHint: false // 每次调用都会创建新 Issue
}
},
async ({ repo, title, body, labels, assignees }) => {
const issue = await githubFetch(`/repos/${repo}/issues`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, body, labels, assignees })
});
return {
content: [{
type: 'text',
text: `Issue 创建成功!\n\n` +
`标题: ${issue.title}\n` +
`编号: #${issue.number}\n` +
`链接: ${issue.html_url}`
}]
};
}
);
repo 参数使用了 completable 封装,当用户开始输入仓库名时,Server 会实时调用 GitHub API 搜索匹配的仓库------这就是第 18 章介绍的 Completion 机制在实践中的应用。
20.4.3 Python 对照
同样的 create_issue 工具在 Python SDK 中的写法:
python
from mcp.server.mcpserver import McpServer
from mcp.server.mcpserver.context import Context
server = McpServer("github-mcp-server")
@server.tool(
name="create_issue",
title="创建 GitHub Issue",
description="在指定仓库中创建一个新的 Issue"
)
async def create_issue(
repo: str,
title: str,
body: str | None = None,
labels: list[str] | None = None,
ctx: Context | None = None
) -> str:
"""repo: 仓库全名 (owner/repo), title: Issue 标题"""
if ctx:
await ctx.info(f"正在创建 Issue: {title}")
issue = await github_fetch(f"/repos/{repo}/issues", method="POST",
json={"title": title, "body": body, "labels": labels})
return f"Issue #{issue['number']} 创建成功: {issue['html_url']}"
Python SDK 的 Context 注入机制很优雅------你只需要在函数签名中声明一个 Context 类型的参数,SDK 会自动注入上下文对象,无需显式传递。
20.5 实现 Resources
Resources 让 AI 可以"读取"GitHub 上的内容,就像读取本地文件一样:
typescript
import { ResourceTemplate } from '@modelcontextprotocol/server';
// 静态资源:当前用户信息
server.registerResource(
'github://user/profile',
{
name: '当前 GitHub 用户',
description: '获取当前认证用户的 GitHub 个人信息',
mimeType: 'application/json'
},
async () => {
const user = await githubFetch('/user');
return {
contents: [{
uri: 'github://user/profile',
mimeType: 'application/json',
text: JSON.stringify(user, null, 2)
}]
};
}
);
// 资源模板:仓库文件内容
server.registerResourceTemplate(
new ResourceTemplate('github://repos/{owner}/{repo}/contents/{path}', {
list: async () => {
// 返回一些常见的文件作为示例
return {
resources: [
{
uri: 'github://repos/anthropics/mcp-specification/contents/README.md',
name: 'MCP Specification README',
mimeType: 'text/markdown'
}
]
};
}
}),
{
name: '仓库文件内容',
description: '读取 GitHub 仓库中指定路径的文件内容',
mimeType: 'text/plain'
},
async (uri, { owner, repo, path }) => {
const data = await githubFetch(`/repos/${owner}/${repo}/contents/${path}`);
// GitHub API 返回 base64 编码的内容
const content = Buffer.from(data.content, 'base64').toString('utf-8');
return {
contents: [{
uri: uri.href,
mimeType: data.type === 'file' ? 'text/plain' : 'application/json',
text: content
}]
};
}
);
资源模板中的 URI 变量({owner}、{repo}、{path})由 SDK 自动解析和填充。list 回调是可选的------它提供了预定义的资源列表,帮助 Client 展示可用资源。
20.6 实现 Prompts
Prompts 是预定义的对话模板,引导 LLM 执行特定的工作流:
typescript
server.registerPrompt(
'code_review',
{
title: 'Code Review',
description: '对 Pull Request 进行代码审查,生成结构化的审查意见',
argsSchema: z.object({
repo: z.string().describe('仓库全名 (owner/repo)'),
pr_number: z.number().describe('Pull Request 编号')
})
},
async ({ repo, pr_number }) => {
// 获取 PR 详情和 diff
const pr = await githubFetch(`/repos/${repo}/pulls/${pr_number}`);
const diff = await githubFetch(`/repos/${repo}/pulls/${pr_number}`, {
headers: { 'Accept': 'application/vnd.github.v3.diff' }
});
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请对以下 Pull Request 进行代码审查。\n\n` +
`## PR 信息\n` +
`- 标题: ${pr.title}\n` +
`- 作者: ${pr.user.login}\n` +
`- 描述: ${pr.body || '无'}\n\n` +
`## 代码变更\n\`\`\`diff\n${diff}\n\`\`\`\n\n` +
`请从以下维度审查:\n` +
`1. 代码质量与可读性\n` +
`2. 潜在的 Bug 或边界条件\n` +
`3. 性能影响\n` +
`4. 安全风险\n` +
`5. 测试覆盖`
}
}
]
};
}
);
server.registerPrompt(
'bug_report',
{
title: 'Bug Report',
description: '基于错误信息生成结构化的 Bug 报告,并自动创建 Issue',
argsSchema: z.object({
repo: z.string().describe('仓库全名'),
error_message: z.string().describe('错误信息或堆栈'),
context: z.string().optional().describe('复现步骤或额外上下文')
})
},
async ({ repo, error_message, context }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: `请基于以下错误信息,为仓库 ${repo} 生成一个结构化的 Bug 报告。\n\n` +
`## 错误信息\n\`\`\`\n${error_message}\n\`\`\`\n\n` +
(context ? `## 上下文\n${context}\n\n` : '') +
`请生成包含以下部分的 Bug 报告:\n` +
`1. 问题描述(一句话总结)\n` +
`2. 复现步骤\n` +
`3. 期望行为 vs 实际行为\n` +
`4. 可能的原因分析\n` +
`5. 建议的修复方向\n\n` +
`生成后,请使用 create_issue 工具将报告提交到 ${repo}。`
}
}]
})
);
注意 bug_report Prompt 的最后一句------它引导 LLM 在生成报告后自动调用 create_issue 工具。这是 Prompts 与 Tools 协同的典型模式:Prompt 定义工作流的"脚本",Tool 执行具体的"动作"。
20.7 添加进度追踪
对于可能耗时的操作,使用 Server 底层 API 的进度通知:
typescript
server.registerTool(
'batch_label_issues',
{
title: '批量标记 Issues',
description: '为多个 Issue 添加标签',
inputSchema: z.object({
repo: z.string(),
issue_numbers: z.array(z.number()),
labels: z.array(z.string())
})
},
async ({ repo, issue_numbers, labels }, { reportProgress }) => {
const total = issue_numbers.length;
const results: string[] = [];
for (let i = 0; i < total; i++) {
const num = issue_numbers[i];
try {
await githubFetch(`/repos/${repo}/issues/${num}/labels`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ labels })
});
results.push(`#${num}: 成功`);
} catch (e: any) {
results.push(`#${num}: 失败 - ${e.message}`);
}
// 报告进度
await reportProgress({
progress: i + 1,
total,
message: `正在处理 Issue #${num} (${i + 1}/${total})`
});
}
return {
content: [{
type: 'text',
text: `批量标记完成:\n${results.join('\n')}`
}]
};
}
);
20.8 测试策略
20.8.1 协议级集成测试
MCP 的测试需要模拟完整的 Client-Server 交互。TypeScript SDK 提供了内存传输(in-memory transport)用于测试:
typescript
// tests/tools.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { Client } from '@modelcontextprotocol/client';
import { McpServer } from '@modelcontextprotocol/server';
import { InMemoryTransport } from '@modelcontextprotocol/core';
describe('GitHub MCP Server', () => {
let client: Client;
let server: McpServer;
beforeAll(async () => {
// 创建 Server(使用 mock 的 GitHub API)
server = createServer({ githubToken: 'test-token' });
// 创建内存传输对
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
// 连接
client = new Client({ name: 'test-client', version: '1.0.0' });
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport)
]);
});
afterAll(async () => {
await client.close();
});
it('应该列出所有工具', async () => {
const result = await client.listTools();
const toolNames = result.tools.map(t => t.name);
expect(toolNames).toContain('search_repos');
expect(toolNames).toContain('create_issue');
expect(toolNames).toContain('batch_label_issues');
});
it('搜索仓库应返回结构化结果', async () => {
const result = await client.callTool({
name: 'search_repos',
arguments: { query: 'mcp', language: 'typescript', limit: 3 }
});
expect(result.content).toHaveLength(1);
const data = JSON.parse((result.content[0] as any).text);
expect(data).toBeInstanceOf(Array);
expect(data.length).toBeLessThanOrEqual(3);
});
it('创建 Issue 应返回 Issue 链接', async () => {
const result = await client.callTool({
name: 'create_issue',
arguments: {
repo: 'test-owner/test-repo',
title: '测试 Issue',
body: '这是一个测试'
}
});
const text = (result.content[0] as any).text;
expect(text).toContain('Issue 创建成功');
expect(text).toContain('#');
});
});
20.8.2 Completion 测试
typescript
it('仓库名应该支持自动补全', async () => {
const result = await client.complete({
ref: { type: 'ref/prompt', name: 'code_review' },
argument: { name: 'repo', value: 'ant' }
});
expect(result.completion.values.length).toBeGreaterThan(0);
result.completion.values.forEach(v => {
expect(v.toLowerCase()).toContain('ant');
});
});
20.9 部署
20.9.1 本地模式(stdio)
最简单的部署方式------用户在配置文件中指定命令:
json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "mcp-server-github"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}
为此需要在 package.json 中配置 bin 入口:
json
{
"name": "mcp-server-github",
"version": "1.0.0",
"bin": {
"mcp-server-github": "./dist/index.js"
},
"files": ["dist"]
}
20.9.2 远程模式(Streamable HTTP)
对于需要多用户共享的场景,使用 HTTP 传输:
typescript
// src/remote.ts
import { randomUUID } from 'node:crypto';
import express from 'express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { isInitializeRequest } from '@modelcontextprotocol/server';
const app = express();
app.use(express.json());
const transports: Map<string, NodeStreamableHTTPServerTransport> = new Map();
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId && transports.has(sessionId)) {
const transport = transports.get(sessionId)!;
await transport.handleRequest(req, res, req.body);
return;
}
if (!sessionId && isInitializeRequest(req.body)) {
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => {
transports.set(sid, transport);
}
});
transport.onclose = () => {
if (transport.sessionId) {
transports.delete(transport.sessionId);
}
};
const server = createServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return;
}
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: '无效请求' },
id: null
});
});
app.get('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(404).send('Session not found');
return;
}
await transport.handleRequest(req, res);
});
app.listen(3000, () => {
console.log('MCP Server running on http://localhost:3000/mcp');
});
远程部署时,每个 Client 连接都创建一个独立的 McpServer 实例------这避免了多用户之间的状态污染,是 MCP Server 多租户部署的标准模式。
20.9.3 部署架构对比
本地模式简单可靠,适合个人使用;远程模式支持多用户、可水平扩展,适合团队和企业级部署。两种模式共用同一套 Server 代码,只是传输层不同------这正是 MCP 传输抽象的价值所在。
20.10 安全加固
20.10.1 输入验证
Zod Schema 已经提供了基本的输入验证,但对于安全敏感的操作还需要额外检查:
typescript
// 防止路径穿越
function validateRepoName(repo: string): boolean {
return /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(repo);
}
// 防止注入
function sanitizeSearchQuery(query: string): string {
return query.replace(/[^\w\s\-.:@/]/g, '');
}
20.10.2 速率限制
对 GitHub API 的调用应遵守速率限制:
typescript
class RateLimiter {
private requests: number[] = [];
private readonly maxRequests: number;
private readonly windowMs: number;
constructor(maxRequests: number = 30, windowMs: number = 60000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
}
async acquire(): Promise<void> {
const now = Date.now();
this.requests = this.requests.filter(t => now - t < this.windowMs);
if (this.requests.length >= this.maxRequests) {
const waitTime = this.windowMs - (now - this.requests[0]);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requests.push(Date.now());
}
}
20.10.3 Host Header 验证
远程部署时,防止 DNS 重绑定攻击:
typescript
import { hostHeaderValidation } from '@modelcontextprotocol/server';
// 添加到 Express 中间件
app.use(hostHeaderValidation({
allowedHosts: ['mcp.example.com'],
allowLocalhost: process.env.NODE_ENV === 'development'
}));
20.11 发布到生态
20.11.1 npm 发布
bash
# 构建
npm run build
# 发布
npm publish --access public
20.11.2 MCP Registry
MCP 生态正在建设中央注册表(类似 npm registry),Server 开发者可以提交自己的 Server 元信息:
json
{
"name": "mcp-server-github",
"description": "GitHub integration for MCP - search repos, manage issues, review PRs",
"version": "1.0.0",
"transport": ["stdio", "streamable-http"],
"tools": ["search_repos", "create_issue", "batch_label_issues"],
"resources": ["github://user/profile", "github://repos/{owner}/{repo}/contents/{path}"],
"prompts": ["code_review", "bug_report"],
"author": "your-name",
"repository": "https://github.com/your-name/mcp-server-github"
}
注册后,Client 可以通过标准化的发现机制找到你的 Server,用户可以一键安装和配置。
20.12 本章小结
本章从零构建了一个具备生产级特性的 GitHub MCP Server,覆盖了完整的开发流程:
- 技术选型:TypeScript 和 Python SDK 各有所长,根据项目特点和团队技能选择
- 三大原语:Tools 执行操作,Resources 暴露数据,Prompts 定义工作流模板
- Completion :通过
completable()为参数添加实时补全,显著提升用户体验 - Progress:为长操作提供进度反馈,让用户知道系统在工作
- 测试:使用内存传输进行协议级集成测试,验证完整的 Client-Server 交互
- 部署:同一套代码支持 stdio(本地)和 HTTP(远程)两种部署模式
- 安全:输入验证、速率限制、Host Header 验证、密钥管理
构建一个好的 MCP Server 需要同时关注协议正确性和工程质量。协议告诉你"必须做什么",工程经验告诉你"应该做什么"。在最后一章,我们将把全书的核心洞察提炼为可复用的设计模式和架构决策框架。