Anthropic Skills 中文文档
首页
使用指南
技能列表
  • 🎨 创意与设计
  • 💻 开发与技术
  • 🏢 企业与沟通
  • 📄 文档处理
  • 🔧 元技能
  • GitHub 仓库
  • Claude 官网
  • Skills 官方文档
GitHub
首页
使用指南
技能列表
  • 🎨 创意与设计
  • 💻 开发与技术
  • 🏢 企业与沟通
  • 📄 文档处理
  • 🔧 元技能
  • GitHub 仓库
  • Claude 官网
  • Skills 官方文档
GitHub
  • 技能列表
  • 🎨 创意与设计

    • 🎨 算法艺术生成

      • 📋 概览
      • 📖 完整指南
    • 🖼️ 画布设计

      • 📋 概览
      • 📖 完整指南
    • 🎬 Slack GIF 创建器

      • 📋 概览
      • 📖 完整指南
    • 🎨 主题工厂

      • 📋 概览
      • 📖 完整指南
  • 💻 开发与技术

    • 🎨 Web 组件构建器

      • 📋 概览
      • 📖 完整指南
    • 📦 MCP 服务器构建器

      • 📋 概览
      • 📖 完整指南
    • 🧪 Web 应用测试工具

      • 📋 概览
      • 📖 完整指南
  • 🏢 企业与沟通

    • 🎨 品牌指南

      • 📋 概览
      • 📖 完整指南
    • 📢 企业内部沟通

      • 📋 概览
      • 📖 完整指南
    • 💎 前端设计

      • 📋 概览
      • 📖 完整指南
  • 📄 文档处理

    • 📘 Word 文档处理

      • 📋 概览
      • 📖 完整指南
    • 📕 PDF 文档处理

      • 📋 概览
      • 📖 完整指南
    • 📙 PowerPoint 演示文稿处理

      • 📋 概览
      • 📖 完整指南
    • 📗 Excel 表格处理

      • 📋 概览
      • 📖 完整指南
  • 🔧 元技能

    • 🛠️ Skill 创建器

      • 📋 概览
      • 📖 完整指南
    • 📝 Skill 模板

      • 📋 概览
      • 📖 完整参考

MCP Builder 完整指南

本文档包含创建高质量 MCP (Model Context Protocol) 服务器的完整技术细节。如果您是初次接触,建议先阅读 📋 概览。

目录

  • 目录
  • 概述
    • 协议架构
  • 阶段 1: 深入研究和规划
    • 1.1 理解现代 MCP 设计
    • 1.2 学习 MCP 协议文档
    • 1.3 学习框架文档
    • 1.4 规划实现
  • 阶段 2: 实现
    • 2.1 项目结构
    • 2.2 实现核心基础设施
    • 2.3 实现工具
  • 阶段 3: 审查和测试
    • 3.1 代码质量检查清单
    • 3.2 构建和测试
  • 阶段 4: 创建评估
    • 4.1 评估目的
    • 4.2 创建流程
    • 4.3 评估文件格式
    • 4.4 运行评估
  • 附录:参考资源
    • 官方文档
    • 示例代码仓库
    • 相关工具
    • 技能文档

概述

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 服务器
高级工作流单次调用完成任务缺乏灵活性特定客户端优化

推荐策略:

  1. 优先全面覆盖:提供所有核心 API 端点
  2. 按需添加工作流:基于用户反馈添加高级工具
  3. 保持一致性:两种方式使用相同的底层逻辑

示例对比:

// ❌ 仅提供高级工作流(不灵活)
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           // 缩写不清晰

可发现性技巧:

  1. 一致的前缀:同一服务的工具使用相同前缀
  2. 动作导向:使用清晰的动词(create、list、update、delete)
  3. 详细描述:在 description 中说明用途和场景
server.registerTool({
  name: 'github_create_issue',
  description: `在指定的 GitHub 仓库中创建新的 issue。
  
  用途:
  - 报告 bug
  - 提出功能请求
  - 创建任务追踪
  
  注意:需要仓库的写权限`,
  // ...
})

上下文管理

设计原则:

  1. 简洁的工具描述:50-150 词,突出关键信息
  2. 支持过滤和分页:避免一次返回大量数据
  3. 返回聚焦数据:只包含必要字段

分页示例:

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)
      }]
    }
  }
})

可操作的错误消息

错误消息应包含:

  1. 问题描述:发生了什么
  2. 可能原因:为什么发生
  3. 解决步骤:如何修复
  4. 相关工具:可以用什么工具验证

示例对比:

// ❌ 不好的错误
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

关键特性:

  1. Zod 集成:类型安全的 schema 定义
  2. Streamable HTTP:内置 HTTP 服务器
  3. structuredContent:结构化响应(TypeScript 独有)
  4. 工具注解:丰富的元数据支持

Python SDK (FastMCP):

curl https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md

关键特性:

  1. 装饰器语法:@mcp.tool() 简化注册
  2. Pydantic 模型:类型验证和序列化
  3. 异步支持:async/await 处理 I/O
  4. 简单部署:单文件即可运行

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

测试步骤:

  1. 验证工具列表:

    • 确认所有工具都出现在列表中
    • 检查工具名称、描述是否正确
  2. 测试只读工具:

    // github_list_repos
    {
      "username": "anthropics"
    }
    
    • 验证返回数据格式正确
    • 检查分页信息
  3. 测试创建工具(使用测试仓库):

    // github_create_issue
    {
      "owner": "your-test-org",
      "repo": "test-repo",
      "title": "Test issue from MCP Inspector",
      "body": "This is a test"
    }
    
    • 验证创建成功
    • 检查返回的 URL
  4. 测试错误处理:

    // 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

返回:📋 概览 | 技能列表

Prev
📋 概览