MCP Builder 完整指南
本文档包含创建高质量 MCP (Model Context Protocol) 服务器的完整技术细节。如果您是初次接触,建议先阅读 📋 概览。
目录
概述
MCP (Model Context Protocol) 是一个标准协议,使 LLM 能够通过精心设计的工具与外部服务交互。MCP 服务器的质量取决于它能多好地帮助 LLM 完成实际任务。
协议架构
┌─────────────┐
│ LLM Client │
└──────┬──────┘
│ MCP Protocol
│
┌──────▼──────┐
│ MCP Server │
├─────────────┤
│ Tools │ ← 可调用的函数
│ Resources │ ← 可访问的数据
│ Prompts │ ← 预定义提示词
└──────┬──────┘
│
┌──────▼──────┐
│ External │
│ Services │ ← GitHub, Slack, DB...
└─────────────┘
阶段 1: 深入研究和规划
1.1 理解现代 MCP 设计
API 覆盖率 vs. 工作流工具
设计权衡:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全面 API 覆盖 | 灵活组合、适应性强 | 需要多次调用 | 通用 MCP 服务器 |
| 高级工作流 | 单次调用完成任务 | 缺乏灵活性 | 特定客户端优化 |
推荐策略:
- 优先全面覆盖:提供所有核心 API 端点
- 按需添加工作流:基于用户反馈添加高级工具
- 保持一致性:两种方式使用相同的底层逻辑
示例对比:
// ❌ 仅提供高级工作流(不灵活)
server.registerTool({
name: 'github_setup_project',
description: '创建仓库、添加协作者、创建 README',
async handler({ project_name, collaborators }) {
// 一次性完成所有操作
// 问题:如果只想创建仓库呢?
}
})
// ✅ 提供底层 API(推荐)
server.registerTool({
name: 'github_create_repo',
// ...
})
server.registerTool({
name: 'github_add_collaborator',
// ...
})
server.registerTool({
name: 'github_create_file',
// ...
})
工具命名和可发现性
命名公式:{服务}_{动作}_{对象}
// ✅ 好的命名
github_create_issue // 清晰、一致
github_list_repos
github_merge_pull_request
github_close_issue
// ❌ 不好的命名
create // 太模糊
issue // 不是动作
createIssue // 驼峰式不一致
gh_create_iss // 缩写不清晰
可发现性技巧:
- 一致的前缀:同一服务的工具使用相同前缀
- 动作导向:使用清晰的动词(create、list、update、delete)
- 详细描述:在 description 中说明用途和场景
server.registerTool({
name: 'github_create_issue',
description: `在指定的 GitHub 仓库中创建新的 issue。
用途:
- 报告 bug
- 提出功能请求
- 创建任务追踪
注意:需要仓库的写权限`,
// ...
})
上下文管理
设计原则:
- 简洁的工具描述:50-150 词,突出关键信息
- 支持过滤和分页:避免一次返回大量数据
- 返回聚焦数据:只包含必要字段
分页示例:
const ListReposSchema = z.object({
username: z.string(),
page: z.number().default(1).describe("页码,从 1 开始"),
per_page: z.number().default(30).max(100).describe("每页数量,最多 100")
})
server.registerTool({
name: 'github_list_repos',
inputSchema: zodToJsonSchema(ListReposSchema),
async handler(args) {
const { username, page, per_page } = ListReposSchema.parse(args)
const repos = await client.listRepos(username, { page, per_page })
return {
content: [{
type: 'text',
text: JSON.stringify({
repos: repos.map(r => ({
name: r.name,
description: r.description,
stars: r.stargazers_count,
url: r.html_url
})),
pagination: {
page,
per_page,
total: repos.length,
has_more: repos.length === per_page
}
}, null, 2)
}]
}
}
})
可操作的错误消息
错误消息应包含:
- 问题描述:发生了什么
- 可能原因:为什么发生
- 解决步骤:如何修复
- 相关工具:可以用什么工具验证
示例对比:
// ❌ 不好的错误
throw new Error('Repository not found')
// ✅ 好的错误
throw new Error(`
仓库 '${owner}/${repo}' 未找到
可能原因:
1. 仓库名称拼写错误
2. 仓库是私有的,但使用了公开 token
3. 仓库已被删除或重命名
4. 您没有访问权限
解决步骤:
1. 检查仓库名称:确保格式为 'owner/repo'
2. 验证权限:使用 github_list_repos 查看可访问的仓库
3. 检查 token 范围:确保 token 具有 'repo' 权限
示例:
github_list_repos(username="${owner}")
`.trim())
错误处理模板:
async function handleGitHubError(error: any, context: string) {
if (error.status === 404) {
throw new Error(`
${context} - 资源未找到
${error.message}
请检查:
1. 资源名称是否正确
2. 是否有访问权限
3. 资源是否存在
`.trim())
}
if (error.status === 401) {
throw new Error(`
${context} - 身份验证失败
请确保:
1. API token 有效且未过期
2. Token 具有必要的权限范围
3. 环境变量已正确设置
当前 token 范围可能不足。
`.trim())
}
if (error.status === 403) {
throw new Error(`
${context} - 权限不足
${error.message}
可能原因:
1. Token 缺少必要的权限范围
2. 达到 API 速率限制
3. 资源需要更高级别的权限
如果是速率限制,请稍后重试。
`.trim())
}
// 通用错误
throw new Error(`${context} - ${error.message || '未知错误'}`)
}
1.2 学习 MCP 协议文档
导航 MCP 规范
步骤 1:获取站点地图
import requests
sitemap_url = 'https://modelcontextprotocol.io/sitemap.xml'
response = requests.get(sitemap_url)
# 解析 XML,找到相关页面
步骤 2:获取 Markdown 文档
# 在 URL 后添加 .md 后缀
spec_url = 'https://modelcontextprotocol.io/specification/draft.md'
spec_content = requests.get(spec_url).text
关键文档清单:
- [ ] 规范概述 (
/specification/draft.md) - [ ] 架构概念 (
/concepts/architecture.md) - [ ] 传输机制 (
/specification/transport.md) - [ ] 工具定义 (
/specification/tools.md) - [ ] 资源定义 (
/specification/resources.md) - [ ] 提示词定义 (
/specification/prompts.md)
核心概念理解
1. 传输层 (Transport)
Streamable HTTP (推荐)
├── 优点:无状态、易扩展、云友好
├── 格式:JSON over HTTP
└── 适用:远程服务器
stdio
├── 优点:简单、本地集成
├── 格式:标准输入/输出
└── 适用:桌面应用(如 Claude Desktop)
2. 工具 (Tools)
{
"name": "tool_name",
"description": "工具描述",
"inputSchema": {
"type": "object",
"properties": {
"param1": { "type": "string" }
},
"required": ["param1"]
},
"outputSchema": {
"type": "object",
"properties": {
"result": { "type": "string" }
}
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true
}
}
3. 工具注解 (Annotations)
| 注解 | 含义 | 示例 |
|---|---|---|
readOnlyHint | 只读操作 | github_list_repos: true |
destructiveHint | 破坏性操作 | github_delete_repo: true |
idempotentHint | 幂等操作 | github_update_issue: true |
openWorldHint | 可能返回新字段 | github_get_repo: true |
1.3 学习框架文档
推荐技术栈
语言选择:TypeScript
理由:
- ✅ 官方 SDK 成熟:功能完善、文档齐全
- ✅ AI 生成友好:模型擅长 TypeScript
- ✅ 类型安全:编译时发现错误
- ✅ 执行环境广:Node.js、Deno、浏览器、边缘函数
- ✅ 工具链丰富:ESLint、Prettier、VS Code 支持
传输选择:
| 场景 | 推荐传输 | 理由 |
|---|---|---|
| 云部署 | Streamable HTTP | 无状态、易扩展、负载均衡友好 |
| 桌面集成 | stdio | 简单、本地通信、Claude Desktop 支持 |
| 边缘函数 | Streamable HTTP | 无状态、快速冷启动 |
| 企业内网 | Streamable HTTP | 标准 HTTP、防火墙友好 |
加载框架文档
TypeScript SDK:
# 使用 WebFetch 工具获取最新文档
curl https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md
关键特性:
- Zod 集成:类型安全的 schema 定义
- Streamable HTTP:内置 HTTP 服务器
- structuredContent:结构化响应(TypeScript 独有)
- 工具注解:丰富的元数据支持
Python SDK (FastMCP):
curl https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md
关键特性:
- 装饰器语法:
@mcp.tool()简化注册 - Pydantic 模型:类型验证和序列化
- 异步支持:
async/await处理 I/O - 简单部署:单文件即可运行
1.4 规划实现
理解 API
步骤 1:阅读 API 文档
以 GitHub API 为例:
# 查看 API 文档
open https://docs.github.com/en/rest
# 关键信息:
# - 基础 URL: https://api.github.com
# - 认证方式: Bearer token
# - 速率限制: 5000 req/hr (authenticated)
# - 分页: Link header
步骤 2:识别核心端点
仓库操作
├── GET /repos/{owner}/{repo}
├── POST /repos
├── PATCH /repos/{owner}/{repo}
└── DELETE /repos/{owner}/{repo}
Issue 操作
├── GET /repos/{owner}/{repo}/issues
├── POST /repos/{owner}/{repo}/issues
├── PATCH /repos/{owner}/{repo}/issues/{number}
└── POST /repos/{owner}/{repo}/issues/{number}/comments
Pull Request 操作
├── GET /repos/{owner}/{repo}/pulls
├── POST /repos/{owner}/{repo}/pulls
├── PATCH /repos/{owner}/{repo}/pulls/{number}
└── PUT /repos/{owner}/{repo}/pulls/{number}/merge
步骤 3:规划工具列表
## Phase 1: 仓库管理(必需)
- [ ] github_list_repos
- [ ] github_get_repo
- [ ] github_create_repo
## Phase 2: Issue 管理(高优先级)
- [ ] github_list_issues
- [ ] github_get_issue
- [ ] github_create_issue
- [ ] github_update_issue
- [ ] github_add_comment
## Phase 3: Pull Request(中优先级)
- [ ] github_list_pulls
- [ ] github_create_pull
- [ ] github_merge_pull
## Phase 4: 高级功能(低优先级)
- [ ] github_search_code
- [ ] github_create_workflow
- [ ] github_trigger_action
认证和配置
环境变量设计:
# .env
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
GITHUB_API_URL=https://api.github.com
GITHUB_RATE_LIMIT_THRESHOLD=100
配置加载:
import { z } from 'zod'
const ConfigSchema = z.object({
token: z.string().min(1, "GITHUB_TOKEN 是必需的"),
apiUrl: z.string().url().default("https://api.github.com"),
rateLimitThreshold: z.number().default(100)
})
const config = ConfigSchema.parse({
token: process.env.GITHUB_TOKEN,
apiUrl: process.env.GITHUB_API_URL,
rateLimitThreshold: parseInt(process.env.GITHUB_RATE_LIMIT_THRESHOLD || '100')
})
阶段 2: 实现
2.1 项目结构
TypeScript 项目结构
github-mcp/
├── src/
│ ├── index.ts # 入口文件
│ ├── server.ts # MCP 服务器定义
│ ├── client/
│ │ └── github.ts # GitHub API 客户端
│ ├── tools/
│ │ ├── repos.ts # 仓库相关工具
│ │ ├── issues.ts # Issue 相关工具
│ │ └── pulls.ts # PR 相关工具
│ ├── schemas/
│ │ └── index.ts # Zod schemas
│ └── utils/
│ ├── errors.ts # 错误处理
│ └── formatting.ts # 响应格式化
├── package.json
├── tsconfig.json
├── .env.example
└── README.md
package.json:
{
"name": "github-mcp-server",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/index.js",
"test": "npx @modelcontextprotocol/inspector dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.0",
"zod-to-json-schema": "^3.22.0",
"dotenv": "^16.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Python 项目结构
github-mcp/
├── github_mcp/
│ ├── __init__.py
│ ├── server.py # MCP 服务器
│ ├── client.py # GitHub API 客户端
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── repos.py # 仓库工具
│ │ ├── issues.py # Issue 工具
│ │ └── pulls.py # PR 工具
│ └── utils/
│ ├── __init__.py
│ ├── errors.py # 错误处理
│ └── formatting.py # 响应格式化
├── requirements.txt
├── setup.py
├── .env.example
└── README.md
requirements.txt:
fastmcp>=1.0.0
pydantic>=2.0.0
httpx>=0.24.0
python-dotenv>=1.0.0
2.2 实现核心基础设施
TypeScript API 客户端
import { z } from 'zod'
export class GitHubClient {
private baseURL: string
private token: string
constructor(token: string, baseURL: string = 'https://api.github.com') {
this.token = token
this.baseURL = baseURL
}
/**
* 发送 HTTP 请求到 GitHub API
*/
async request<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseURL}${endpoint}`
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
...options.headers
}
})
// 检查速率限制
const remaining = response.headers.get('X-RateLimit-Remaining')
if (remaining && parseInt(remaining) < 100) {
console.warn(`⚠️ API 速率限制剩余: ${remaining}`)
}
if (!response.ok) {
await this.handleError(response, endpoint)
}
return response.json()
}
/**
* 处理 API 错误
*/
private async handleError(response: Response, endpoint: string) {
const errorBody = await response.json().catch(() => ({}))
const context = `GitHub API ${response.status} - ${endpoint}`
if (response.status === 404) {
throw new Error(`
${context}
资源未找到: ${errorBody.message || '未知错误'}
可能原因:
1. 资源名称拼写错误
2. 资源不存在或已被删除
3. 您没有访问权限
`.trim())
}
if (response.status === 401) {
throw new Error(`
${context}
身份验证失败
请检查:
1. GITHUB_TOKEN 环境变量是否设置
2. Token 是否有效且未过期
3. Token 格式是否正确(以 ghp_ 开头)
`.trim())
}
if (response.status === 403) {
if (errorBody.message?.includes('rate limit')) {
const resetTime = response.headers.get('X-RateLimit-Reset')
const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000) : null
throw new Error(`
${context}
API 速率限制已达上限
${errorBody.message}
重置时间: ${resetDate?.toLocaleString() || '未知'}
解决方案:
1. 等待速率限制重置
2. 使用已认证的 token(更高限制)
3. 减少 API 调用频率
`.trim())
}
throw new Error(`
${context}
权限不足: ${errorBody.message || '未知错误'}
请确保 token 具有必要的权限范围。
`.trim())
}
// 通用错误
throw new Error(`
${context}
${errorBody.message || response.statusText || '未知错误'}
`.trim())
}
/**
* GET 请求
*/
async get<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> {
const url = new URL(endpoint, this.baseURL)
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value))
}
})
}
return this.request(url.pathname + url.search)
}
/**
* POST 请求
*/
async post<T = any>(endpoint: string, data?: any): Promise<T> {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
})
}
/**
* PATCH 请求
*/
async patch<T = any>(endpoint: string, data?: any): Promise<T> {
return this.request(endpoint, {
method: 'PATCH',
body: JSON.stringify(data)
})
}
/**
* DELETE 请求
*/
async delete(endpoint: string): Promise<void> {
await this.request(endpoint, { method: 'DELETE' })
}
}
Python API 客户端
import httpx
from typing import Optional, Dict, Any
import os
class GitHubClient:
"""GitHub API 客户端"""
def __init__(
self,
token: Optional[str] = None,
base_url: str = "https://api.github.com"
):
self.token = token or os.getenv("GITHUB_TOKEN")
if not self.token:
raise ValueError("GITHUB_TOKEN 是必需的")
self.base_url = base_url
self.client = httpx.AsyncClient(
base_url=base_url,
headers={
"Authorization": f"Bearer {self.token}",
"Accept": "application/vnd.github.v3+json"
},
timeout=30.0
)
async def request(
self,
method: str,
endpoint: str,
**kwargs
) -> Dict[str, Any]:
"""发送 HTTP 请求"""
try:
response = await self.client.request(method, endpoint, **kwargs)
# 检查速率限制
remaining = response.headers.get("X-RateLimit-Remaining")
if remaining and int(remaining) < 100:
print(f"⚠️ API 速率限制剩余: {remaining}")
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
await self._handle_error(e)
async def _handle_error(self, error: httpx.HTTPStatusError):
"""处理 API 错误"""
response = error.response
try:
error_data = response.json()
message = error_data.get("message", "未知错误")
except:
message = response.text or "未知错误"
context = f"GitHub API {response.status_code} - {response.request.url.path}"
if response.status_code == 404:
raise ValueError(f"""
{context}
资源未找到: {message}
可能原因:
1. 资源名称拼写错误
2. 资源不存在或已被删除
3. 您没有访问权限
""".strip())
if response.status_code == 401:
raise ValueError(f"""
{context}
身份验证失败
请检查:
1. GITHUB_TOKEN 环境变量是否设置
2. Token 是否有效且未过期
3. Token 格式是否正确
""".strip())
if response.status_code == 403:
if "rate limit" in message.lower():
reset_time = response.headers.get("X-RateLimit-Reset")
raise ValueError(f"""
{context}
API 速率限制已达上限
{message}
重置时间戳: {reset_time}
解决方案:
1. 等待速率限制重置
2. 使用已认证的 token
3. 减少调用频率
""".strip())
raise ValueError(f"{context}\n\n权限不足: {message}")
# 通用错误
raise ValueError(f"{context}\n\n{message}")
async def get(self, endpoint: str, params: Optional[Dict] = None):
"""GET 请求"""
return await self.request("GET", endpoint, params=params)
async def post(self, endpoint: str, json: Optional[Dict] = None):
"""POST 请求"""
return await self.request("POST", endpoint, json=json)
async def patch(self, endpoint: str, json: Optional[Dict] = None):
"""PATCH 请求"""
return await self.request("PATCH", endpoint, json=json)
async def delete(self, endpoint: str):
"""DELETE 请求"""
await self.request("DELETE", endpoint)
async def close(self):
"""关闭客户端"""
await self.client.aclose()
2.3 实现工具
TypeScript 工具示例
schemas/index.ts:
import { z } from 'zod'
export const CreateIssueSchema = z.object({
owner: z.string().describe("仓库所有者(用户名或组织名)"),
repo: z.string().describe("仓库名称"),
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("指派给的用户列表")
})
export const ListIssuesSchema = z.object({
owner: z.string().describe("仓库所有者"),
repo: z.string().describe("仓库名称"),
state: z.enum(['open', 'closed', 'all']).default('open').describe("Issue 状态"),
page: z.number().default(1).describe("页码"),
per_page: z.number().default(30).max(100).describe("每页数量")
})
export const UpdateIssueSchema = z.object({
owner: z.string().describe("仓库所有者"),
repo: z.string().describe("仓库名称"),
issue_number: z.number().describe("Issue 编号"),
title: z.string().optional().describe("新标题"),
body: z.string().optional().describe("新内容"),
state: z.enum(['open', 'closed']).optional().describe("新状态"),
labels: z.array(z.string()).optional().describe("标签列表")
})
tools/issues.ts:
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { GitHubClient } from '../client/github.js'
import { CreateIssueSchema, ListIssuesSchema, UpdateIssueSchema } from '../schemas/index.js'
export function registerIssueTools(server: Server, client: GitHubClient) {
// 创建 Issue
server.registerTool({
name: 'github_create_issue',
description: `在 GitHub 仓库中创建新的 issue。
用途:
- 报告 bug 或问题
- 提出功能请求
- 创建任务追踪
- 记录待办事项
注意:需要仓库的写权限`,
inputSchema: zodToJsonSchema(CreateIssueSchema),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true
},
async handler(args) {
const { owner, repo, title, body, labels, assignees } = CreateIssueSchema.parse(args)
const issue = await client.post(`/repos/${owner}/${repo}/issues`, {
title,
body,
labels,
assignees
})
return {
content: [{
type: 'text',
text: `✅ Issue 创建成功!
**#${issue.number}** ${issue.title}
**状态**: ${issue.state}
**URL**: ${issue.html_url}
${labels ? `**标签**: ${labels.join(', ')}` : ''}
${assignees ? `**指派给**: ${assignees.join(', ')}` : ''}`
}],
structuredContent: {
number: issue.number,
title: issue.title,
state: issue.state,
url: issue.html_url,
labels: labels || [],
assignees: assignees || []
}
}
}
})
// 列出 Issues
server.registerTool({
name: 'github_list_issues',
description: `列出仓库的 issues,支持状态过滤和分页。
用途:
- 查看所有打开的 issues
- 检查已关闭的 issues
- 分页浏览大量 issues
返回信息:
- Issue 编号、标题、状态
- 创建时间和作者
- 标签和指派信息
- 分页信息`,
inputSchema: zodToJsonSchema(ListIssuesSchema),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
},
async handler(args) {
const { owner, repo, state, page, per_page } = ListIssuesSchema.parse(args)
const issues = await client.get(`/repos/${owner}/${repo}/issues`, {
state,
page,
per_page
})
const issueList = issues.map((issue: any) => ({
number: issue.number,
title: issue.title,
state: issue.state,
author: issue.user.login,
created_at: issue.created_at,
labels: issue.labels.map((l: any) => l.name),
assignees: issue.assignees.map((a: any) => a.login),
url: issue.html_url
}))
const markdown = `# Issues: ${owner}/${repo}
**状态**: ${state} | **页码**: ${page} | **每页**: ${per_page}
${issueList.map((issue: any) => `
## #${issue.number} ${issue.title}
- **状态**: ${issue.state}
- **作者**: ${issue.author}
- **创建时间**: ${issue.created_at}
${issue.labels.length > 0 ? `- **标签**: ${issue.labels.join(', ')}` : ''}
${issue.assignees.length > 0 ? `- **指派**: ${issue.assignees.join(', ')}` : ''}
- **URL**: ${issue.url}
`).join('\n---\n')}
**分页信息**:
- 当前页: ${page}
- 返回数量: ${issueList.length}
- 可能有更多: ${issueList.length === per_page ? '是' : '否'}
`.trim()
return {
content: [{
type: 'text',
text: markdown
}],
structuredContent: {
issues: issueList,
pagination: {
page,
per_page,
count: issueList.length,
has_more: issueList.length === per_page
}
}
}
}
})
// 更新 Issue
server.registerTool({
name: 'github_update_issue',
description: `更新 GitHub issue 的信息。
可更新内容:
- 标题和内容
- 状态(打开/关闭)
- 标签
- 指派人
注意:需要仓库的写权限`,
inputSchema: zodToJsonSchema(UpdateIssueSchema),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
},
async handler(args) {
const { owner, repo, issue_number, ...updates } = UpdateIssueSchema.parse(args)
const issue = await client.patch(
`/repos/${owner}/${repo}/issues/${issue_number}`,
updates
)
return {
content: [{
type: 'text',
text: `✅ Issue #${issue.number} 更新成功!
**标题**: ${issue.title}
**状态**: ${issue.state}
**URL**: ${issue.html_url}`
}],
structuredContent: {
number: issue.number,
title: issue.title,
state: issue.state,
url: issue.html_url
}
}
}
})
}
Python 工具示例
tools/issues.py:
from fastmcp import FastMCP
from pydantic import BaseModel, Field
from typing import Optional, List, Literal
from ..client import GitHubClient
class CreateIssueInput(BaseModel):
"""创建 Issue 的输入参数"""
owner: str = Field(description="仓库所有者")
repo: str = Field(description="仓库名称")
title: str = Field(description="Issue 标题")
body: Optional[str] = Field(None, description="Issue 内容")
labels: Optional[List[str]] = Field(None, description="标签列表")
assignees: Optional[List[str]] = Field(None, description="指派人列表")
class ListIssuesInput(BaseModel):
"""列出 Issues 的输入参数"""
owner: str = Field(description="仓库所有者")
repo: str = Field(description="仓库名称")
state: Literal["open", "closed", "all"] = Field("open", description="Issue 状态")
page: int = Field(1, description="页码")
per_page: int = Field(30, description="每页数量", le=100)
def register_issue_tools(mcp: FastMCP, client: GitHubClient):
@mcp.tool()
async def github_create_issue(
owner: str,
repo: str,
title: str,
body: Optional[str] = None,
labels: Optional[List[str]] = None,
assignees: Optional[List[str]] = None
) -> str:
"""在 GitHub 仓库中创建新的 issue
用途:
- 报告 bug 或问题
- 提出功能请求
- 创建任务追踪
注意:需要仓库的写权限
"""
data = {"title": title}
if body:
data["body"] = body
if labels:
data["labels"] = labels
if assignees:
data["assignees"] = assignees
issue = await client.post(f"/repos/{owner}/{repo}/issues", json=data)
result = f"""✅ Issue 创建成功!
**#{issue['number']}** {issue['title']}
**状态**: {issue['state']}
**URL**: {issue['html_url']}"""
if labels:
result += f"\n**标签**: {', '.join(labels)}"
if assignees:
result += f"\n**指派给**: {', '.join(assignees)}"
return result
@mcp.tool()
async def github_list_issues(
owner: str,
repo: str,
state: Literal["open", "closed", "all"] = "open",
page: int = 1,
per_page: int = 30
) -> str:
"""列出仓库的 issues,支持状态过滤和分页
用途:
- 查看所有打开的 issues
- 检查已关闭的 issues
- 分页浏览大量 issues
返回信息:
- Issue 编号、标题、状态
- 创建时间和作者
- 标签和指派信息
"""
issues = await client.get(
f"/repos/{owner}/{repo}/issues",
params={"state": state, "page": page, "per_page": per_page}
)
result = f"# Issues: {owner}/{repo}\n\n"
result += f"**状态**: {state} | **页码**: {page} | **每页**: {per_page}\n\n"
for issue in issues:
result += f"## #{issue['number']} {issue['title']}\n"
result += f"- **状态**: {issue['state']}\n"
result += f"- **作者**: {issue['user']['login']}\n"
result += f"- **创建时间**: {issue['created_at']}\n"
labels = [l['name'] for l in issue.get('labels', [])]
if labels:
result += f"- **标签**: {', '.join(labels)}\n"
assignees = [a['login'] for a in issue.get('assignees', [])]
if assignees:
result += f"- **指派**: {', '.join(assignees)}\n"
result += f"- **URL**: {issue['html_url']}\n\n"
result += "---\n\n"
result += f"\n**分页信息**:\n"
result += f"- 当前页: {page}\n"
result += f"- 返回数量: {len(issues)}\n"
result += f"- 可能有更多: {'是' if len(issues) == per_page else '否'}\n"
return result
@mcp.tool()
async def github_update_issue(
owner: str,
repo: str,
issue_number: int,
title: Optional[str] = None,
body: Optional[str] = None,
state: Optional[Literal["open", "closed"]] = None,
labels: Optional[List[str]] = None
) -> str:
"""更新 GitHub issue 的信息
可更新内容:
- 标题和内容
- 状态(打开/关闭)
- 标签
注意:需要仓库的写权限
"""
data = {}
if title:
data["title"] = title
if body:
data["body"] = body
if state:
data["state"] = state
if labels:
data["labels"] = labels
issue = await client.patch(
f"/repos/{owner}/{repo}/issues/{issue_number}",
json=data
)
return f"""✅ Issue #{issue['number']} 更新成功!
**标题**: {issue['title']}
**状态**: {issue['state']}
**URL**: {issue['html_url']}"""
阶段 3: 审查和测试
3.1 代码质量检查清单
通用检查
- [ ] 无重复代码:提取共享逻辑到工具函数
- [ ] 一致的错误处理:所有工具使用统一的错误格式
- [ ] 完整的类型覆盖:TypeScript 无
any类型,Python 有类型注解 - [ ] 清晰的工具描述:每个工具有详细的 description
- [ ] 输入验证:使用 Zod/Pydantic 验证所有输入
- [ ] 响应格式化:JSON 和 Markdown 格式一致
TypeScript 特定
- [ ] Zod schemas:所有输入使用 Zod 定义
- [ ] structuredContent:返回结构化数据
- [ ] 工具注解:设置正确的 annotations
- [ ] 错误类型:使用自定义 Error 类
- [ ] async/await:所有 I/O 操作异步
Python 特定
- [ ] Pydantic 模型:复杂输入使用 Pydantic
- [ ] 类型提示:函数签名包含完整类型
- [ ] docstrings:每个工具有文档字符串
- [ ] async 函数:使用
async def和await - [ ] 异常处理:转换为 ValueError
3.2 构建和测试
TypeScript 构建
# 安装依赖
npm install
# 编译
npm run build
# 检查编译输出
ls -la dist/
# 应该看到:
# dist/
# ├── index.js
# ├── server.js
# ├── client/
# ├── tools/
# └── ...
使用 MCP Inspector 测试
# 启动 Inspector
npx @modelcontextprotocol/inspector dist/index.js
# 浏览器自动打开:http://localhost:5173
测试步骤:
验证工具列表:
- 确认所有工具都出现在列表中
- 检查工具名称、描述是否正确
测试只读工具:
// github_list_repos { "username": "anthropics" }- 验证返回数据格式正确
- 检查分页信息
测试创建工具(使用测试仓库):
// github_create_issue { "owner": "your-test-org", "repo": "test-repo", "title": "Test issue from MCP Inspector", "body": "This is a test" }- 验证创建成功
- 检查返回的 URL
测试错误处理:
// github_get_repo { "owner": "nonexistent", "repo": "nonexistent" }- 验证错误消息清晰
- 确认包含解决建议
Python 测试
# 安装依赖
pip install -r requirements.txt
# 运行服务器
python -m github_mcp.server
# 或使用 Inspector(如果支持)
单元测试示例:
import pytest
from github_mcp.client import GitHubClient
@pytest.mark.asyncio
async def test_list_repos():
client = GitHubClient(token="test_token")
# 使用 mock 进行测试
# ...
阶段 4: 创建评估
评估用于验证 LLM 能否有效使用您的 MCP 服务器。
4.1 评估目的
- 验证实用性:LLM 能否回答真实问题?
- 测试覆盖:工具是否足够完整?
- 发现问题:哪些工具需要改进?
4.2 创建流程
步骤 1:工具检查
列出所有可用工具:
可用工具:
- github_list_repos: 列出用户仓库
- github_get_repo: 获取仓库详情
- github_list_issues: 列出 issues
- github_create_issue: 创建 issue
- github_search_code: 搜索代码
- ...
步骤 2:内容探索
使用只读工具探索数据:
# 探索仓库
github_list_repos(username="anthropics")
→ 发现仓库: claude-tools, mcp-sdk, ...
# 探索 issues
github_list_issues(owner="anthropics", repo="claude-tools")
→ 发现 issues: #1, #2, #3, ...
# 搜索代码
github_search_code(query="MCP server language:typescript")
→ 发现代码示例...
步骤 3:问题生成
创建 10 个复杂、真实的问题:
好问题特征:
- ✅ 多步骤:需要 3+ 次工具调用
- ✅ 真实:人类会真正关心的问题
- ✅ 独立:不依赖其他问题
- ✅ 稳定:答案不会随时间变化
- ✅ 可验证:有唯一的正确答案
示例问题:
<qa_pair>
<question>
在 anthropics/claude-tools 仓库中,找到标题包含"bug"的 issue。
这些 issue 中,哪个是最早创建的?返回该 issue 的编号。
</question>
<answer>42</answer>
</qa_pair>
<qa_pair>
<question>
在 GitHub 上搜索包含"FastMCP"的 Python 文件。
在搜索结果中,哪个仓库的 star 数最多?
返回仓库的完整名称(owner/repo 格式)。
</question>
<answer>jlowin/fastmcp</answer>
</qa_pair>
<qa_pair>
<question>
查看 modelcontextprotocol/python-sdk 仓库的最近 5 个 closed issues。
这些 issue 中,哪个关闭时间最晚?返回 issue 编号。
</question>
<answer>23</answer>
</qa_pair>
步骤 4:答案验证
自己解决每个问题,记录步骤:
问题:anthropics/claude-tools 中最早的 bug issue?
步骤:
1. github_list_issues(owner="anthropics", repo="claude-tools", state="all")
→ 获取所有 issues
2. 过滤标题包含 "bug" 的 issues
→ Issue #12, #18, #42
3. 比较创建时间
→ #42: 2024-01-15
→ #18: 2024-01-20
→ #12: 2024-01-25
4. 最早的是 #42
答案:42 ✅
4.3 评估文件格式
evaluation.xml:
<?xml version="1.0" encoding="UTF-8"?>
<evaluation>
<!-- 问题 1 -->
<qa_pair>
<question>在 anthropics/claude-tools 仓库中,找到标题包含 "documentation" 的打开状态的 issue。这些 issue 中,哪个的评论数最多?返回 issue 编号。</question>
<answer>67</answer>
</qa_pair>
<!-- 问题 2 -->
<qa_pair>
<question>搜索 modelcontextprotocol 组织下的所有仓库,找到 description 包含 "TypeScript" 的仓库。这些仓库中,哪个的 fork 数最多?返回仓库名称(不含组织名)。</question>
<answer>typescript-sdk</answer>
</qa_pair>
<!-- 问题 3 -->
<qa_pair>
<question>在 anthropics/courses 仓库的 main 分支中,找到 README.md 文件。文件内容中提到的第一个外部链接(http/https 开头)的域名是什么?</question>
<answer>docs.anthropic.com</answer>
</qa_pair>
<!-- 问题 4 -->
<qa_pair>
<question>列出 modelcontextprotocol/python-sdk 仓库最近关闭的 10 个 issues。这些 issue 中,从创建到关闭时间最短的是哪个?返回 issue 编号。</question>
<answer>15</answer>
</qa_pair>
<!-- 问题 5 -->
<qa_pair>
<question>在 GitHub 上搜索文件名为 "SKILL.md" 且包含文本 "MCP" 的文件。搜索结果中,第一个(按相关性排序)文件所属的仓库所有者是谁?</question>
<answer>anthropics</answer>
</qa_pair>
<!-- 问题 6 -->
<qa_pair>
<question>查看 jlowin/fastmcp 仓库的 releases。找到最新的 release,其 tag 版本号是多少?(如 v1.2.3)</question>
<answer>v0.3.1</answer>
</qa_pair>
<!-- 问题 7 -->
<qa_pair>
<question>在 anthropics 组织的仓库中,找到 topics 包含 "ai" 的仓库。这些仓库中,最近更新(pushed_at)的是哪个?返回仓库名称。</question>
<answer>anthropic-sdk-python</answer>
</qa_pair>
<!-- 问题 8 -->
<qa_pair>
<question>搜索代码:在 TypeScript 文件中查找字符串 "registerTool"。在前 10 个搜索结果中,有多少个不同的仓库?</question>
<answer>7</answer>
</qa_pair>
<!-- 问题 9 -->
<qa_pair>
<question>查看 modelcontextprotocol/typescript-sdk 仓库的 package.json 文件(main 分支)。dependencies 中列出的第一个依赖包的名称是什么?</question>
<answer>zod</answer>
</qa_pair>
<!-- 问题 10 -->
<qa_pair>
<question>列出用户 "simonw" 的仓库,按 star 数降序排列前 20 个。这些仓库中,description 包含 "LLM" 的有几个?</question>
<answer>8</answer>
</qa_pair>
</evaluation>
4.4 运行评估
如果提供了评估脚本:
# 运行评估
python scripts/run_evaluation.py \
--evaluation evaluation.xml \
--server dist/index.js \
--output results.json
# 查看结果
cat results.json
结果示例:
{
"total": 10,
"passed": 8,
"failed": 2,
"score": 0.80,
"details": [
{
"question": "...",
"expected": "67",
"actual": "67",
"passed": true,
"tool_calls": 3
},
{
"question": "...",
"expected": "typescript-sdk",
"actual": "python-sdk",
"passed": false,
"tool_calls": 5
}
]
}
附录:参考资源
官方文档
- MCP 协议: https://modelcontextprotocol.io
- TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
- Python SDK: https://github.com/modelcontextprotocol/python-sdk
- MCP Inspector: https://github.com/modelcontextprotocol/inspector
示例代码仓库
- 官方示例: https://github.com/modelcontextprotocol/servers
- 社区 MCP 服务器: https://github.com/topics/mcp-server
相关工具
- Zod: https://zod.dev - TypeScript schema 验证
- Pydantic: https://docs.pydantic.dev - Python 数据验证
- FastMCP: https://github.com/jlowin/fastmcp - Python MCP 框架
技能文档
- MCP 最佳实践: 查看原始 SKILL.md
- TypeScript 实现指南: 查看原始 SKILL.md
- Python 实现指南: 查看原始 SKILL.md
- 评估指南: 查看原始 SKILL.md