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. 项目结构¶
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 验证¶
strict。
7.2 友好描述¶
LLM 友好。
7.3 错误处理¶
zod 错误 isError。
7.4 octokit 库¶
成熟 SDK。
7.5 JSON 输出¶
格式化。
8. 5 个扩展¶
8.1 加 OAuth¶
OAuth。
8.2 加 rate limit¶
rate。
8.3 加 progress 通知¶
for await (const page of pages) {
await server.notification({ method: 'notifications/progress', ... })
}
progress。
8.4 加 caching¶
cache。
8.5 加 resources¶
resources。
9. 5 个调试¶
9.1 inspector¶
inspector。
9.2 debug mode¶
debug。
9.3 直接测¶
stdin/stdout。
9.4 看 log¶
log。
9.5 验证 token¶
env。
10. 5 个发布¶
10.1 npm publish¶
npm。
10.2 GitHub release¶
GitHub。
10.3 MCP 官方¶
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。
11. 5 个最佳实践¶
- zod 验证 —— strict
- description 友好 —— LLM 读
- 错误返回 ——
isError: true - JSON 输出 —— 格式化
- 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