跳转至

Walkthrough | 构建一个 MCP Server

难度:⭐⭐⭐ 时间:~2h 目标:手写一个简单的 MCP server 并集成到 Claude Code


1. 目标

构建一个 GitHub Issues MCP server: - 3 个工具:list_issues / create_issue / add_comment - stdio transport - 接入 Claude Code


2. 项目结构

my-github-mcp/
├── package.json
├── tsconfig.json
├── src/
│   └── index.ts
└── README.md

5 文件


3. 完整代码

3.1 package.json

{
  "name": "@my-org/github-mcp",
  "version": "0.1.0",
  "description": "GitHub Issues MCP server",
  "type": "module",
  "bin": { "github-mcp": "./dist/index.js" },
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "@octokit/rest": "^20.0.0",
    "zod": "^3.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}

npm

3.2 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

TS

3.3 src/index.ts

#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { Octokit } from '@octokit/rest'
import { z } from 'zod'

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })

const server = new Server(
  { name: 'github-mcp', version: '0.1.0' },
  { capabilities: { tools: {} } }
)

// 列出工具
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'list_issues',
      description: 'List GitHub issues for a repository. Use to find existing issues.',
      inputSchema: {
        type: 'object',
        properties: {
          repo: {
            type: 'string',
            description: 'Repository in owner/repo format',
            pattern: '^[\\w.-]+/[\\w.-]+$',
          },
          state: {
            type: 'string',
            enum: ['open', 'closed', 'all'],
            default: 'open',
          },
          limit: {
            type: 'number',
            default: 30,
            maximum: 100,
          },
        },
        required: ['repo'],
      },
    },
    {
      name: 'create_issue',
      description: 'Create a new GitHub issue. Use when user asks to file a bug or request a feature.',
      inputSchema: {
        type: 'object',
        properties: {
          repo: { type: 'string', pattern: '^[\\w.-]+/[\\w.-]+$' },
          title: { type: 'string', minLength: 1, maxLength: 256 },
          body: { type: 'string' },
        },
        required: ['repo', 'title'],
      },
    },
    {
      name: 'add_comment',
      description: 'Add a comment to an existing issue.',
      inputSchema: {
        type: 'object',
        properties: {
          repo: { type: 'string', pattern: '^[\\w.-]+/[\\w.-]+$' },
          issue_number: { type: 'number' },
          body: { type: 'string' },
        },
        required: ['repo', 'issue_number', 'body'],
      },
    },
  ],
}))

// 工具实现
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params

  try {
    if (name === 'list_issues') {
      const { repo, state, limit } = z.object({
        repo: z.string().regex(/^[\w.-]+/),
        state: z.enum(['open', 'closed', 'all']).default('open'),
        limit: z.number().max(100).default(30),
      }).parse(args)

      const [owner, repoName] = repo.split('/')
      const { data } = await octokit.issues.listForRepo({
        owner,
        repo: repoName,
        state,
        per_page: limit,
      })

      return {
        content: [{
          type: 'text',
          text: JSON.stringify(
            data.map((issue) => ({
              number: issue.number,
              title: issue.title,
              state: issue.state,
              url: issue.html_url,
              created_at: issue.created_at,
              user: issue.user?.login,
            })),
            null,
            2,
          ),
        }],
      }
    }

    if (name === 'create_issue') {
      const { repo, title, body } = z.object({
        repo: z.string(),
        title: z.string().min(1).max(256),
        body: z.string().optional(),
      }).parse(args)

      const [owner, repoName] = repo.split('/')
      const { data } = await octokit.issues.create({
        owner,
        repo: repoName,
        title,
        body: body || '',
      })

      return {
        content: [{
          type: 'text',
          text: `Issue created: ${data.html_url}`,
        }],
      }
    }

    if (name === 'add_comment') {
      const { repo, issue_number, body } = z.object({
        repo: z.string(),
        issue_number: z.number(),
        body: z.string(),
      }).parse(args)

      const [owner, repoName] = repo.split('/')
      const { data } = await octokit.issues.createComment({
        owner,
        repo: repoName,
        issue_number,
        body,
      })

      return {
        content: [{
          type: 'text',
          text: `Comment added: ${data.html_url}`,
        }],
      }
    }

    throw new Error(`Unknown tool: ${name}`)
  } catch (e) {
    if (e instanceof z.ZodError) {
      return {
        content: [{
          type: 'text',
          text: `Invalid input: ${e.message}`,
        }],
        isError: true,
      }
    }
    throw e
  }
})

const transport = new StdioServerTransport()
await server.connect(transport)

~150 行


4. Build & test

# Install
npm install

# Build
npm run build

# Set GitHub token
export GITHUB_TOKEN=ghp_...

# Test directly
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js

5 步


5. 接入 Claude Code

# 添加 MCP server
claude mcp add --transport stdio -- \
  --env GITHUB_TOKEN=ghp_... \
  -- node /path/to/dist/index.js

# 验证
claude mcp list

2 步


6. 实际使用

> List open issues in anthropics/claude-code
> Create a new issue in my-org/repo: "Bug: ..."
> Add comment to issue 42 in my-org/repo: "Fixed in PR #43"

3 例子


7. 5 个关键设计

7.1 zod 验证

const { repo, state } = z.object({...}).parse(args)

strict

7.2 友好描述

description: 'List GitHub issues for a repository. Use to find existing issues.'

LLM 友好

7.3 错误处理

try {
  // ...
} catch (e) {
  if (e instanceof z.ZodError) {
    return { isError: true, ... }
  }
  throw e
}

zod 错误 isError。

7.4 octokit 库

const { data } = await octokit.issues.listForRepo(...)

成熟 SDK

7.5 JSON 输出

text: JSON.stringify(data, null, 2)

格式化


8. 5 个扩展

8.1 加 OAuth

// MCP server 暴露 /authorize endpoint
// 处理 callback

OAuth

8.2 加 rate limit

// 429 → retry with backoff

rate

8.3 加 progress 通知

for await (const page of pages) {
  await server.notification({ method: 'notifications/progress', ... })
}

progress

8.4 加 caching

// 缓存 list_issues 结果 5min

cache

8.5 加 resources

// resources/read 读 issue 内容

resources


9. 5 个调试

9.1 inspector

npx @modelcontextprotocol/inspector /path/to/dist/index.js

inspector

9.2 debug mode

claude --debug mcp

debug

9.3 直接测

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js

stdin/stdout

9.4 看 log

tail -f ~/.claude/logs/<date>.jsonl | grep mcp

log

9.5 验证 token

echo $GITHUB_TOKEN | head -c 4

env


10. 5 个发布

10.1 npm publish

npm publish --access public

npm

10.2 GitHub release

git tag v0.1.0
git push --tags
gh release create

GitHub

10.3 MCP 官方

# PR 到 https://github.com/modelcontextprotocol/servers

official

10.4 README

# GitHub Issues MCP

## Install
\`\`\`bash
claude mcp add --transport stdio -- ...
\`\`\`

## Tools
- list_issues
- create_issue
- add_comment

README

10.5 marketplace

// marketplace.json
{
  "plugins": [
    { "name": "github-mcp", "source": "..." }
  ]
}

marketplace


11. 5 个最佳实践

  1. zod 验证 —— strict
  2. description 友好 —— LLM 读
  3. 错误返回 —— isError: true
  4. JSON 输出 —— 格式化
  5. timeout 30s —— 防止挂起

12. 总结

构建 MCP Server = define tools + implement + test + publish

核心: - 3 工具 - zod 验证 - octokit - 接入 Claude Code

下一步: - 看 tutorials/build-mcp-server.md - 看 docs/MCP_PROTOCOL.md - 加 OAuth