ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GitHub MCP Server 代码规范解析:5 个设计决策 + 1 张执行链路图,看懂官方 MCP 工具怎么写

GitHub MCP Server 代码规范解析:5 个设计决策 + 1 张执行链路图,看懂官方 MCP 工具怎么写 GitHub MCP Server 代码规范解析5 个设计决策 1 张执行链路图看懂官方 MCP 工具怎么写【免费下载链接】github-mcp-serverGitHubs official MCP Server项目地址: https://gitcode.com/GitHub_Trending/gi/github-mcp-serverGitHub MCP Server 代码规范是理解这套官方服务工程实践最直接的切入点——它决定了你新增一个 MCP 工具时代码该长什么样、错误该怎么处理、分页参数怎么声明。下面这套「开发者视角」解读会带你把 pkg/github/ 下真实代码串起来读一遍目标是读完能直接照着官方风格写出合规的新工具。1. 一句话它是什么为什么值得读一句话这是 GitHub 官方的 Model Context ProtocolMCP服务端把 REST 和 GraphQL 两套 API 封进上百个 MCP 工具供 AI 客户端调用。为什么值得读工具数量多、场景广是理解「如何在 Go 工程里规模化写 MCP 工具」的绝佳样板——参数校验、错误分层、分页、资源清理这些「小细节」在这里被提炼成了可复用的规范。2. 30 秒看懂架构模块怎么分层、数据怎么走三句话分层清晰pkg/http负责传输与鉴权pkg/inventory负责工具注册与可见性过滤pkg/github才是真正干活的业务工具。数据单向流动MCP 请求 → 路由到具体工具 handler → handler 调用 pkg/github/params.go 里的校验函数取参 → 调 GitHub API → 结果/错误经 pkg/errors/ 封装 → 回传。辅助能力独立成包错误处理、性能剖析、翻译、作用域控制各自成包工具层保持「薄而统一」。3. 一个工具的一生注册 → 声明 → 校验 → 调 API → 处理错误 → 返回我们用一个真实的读工具get_repository_tree来自 pkg/github/git.go走一遍完整链路3.1 注册把工具挂进 Inventory工具不再是一个孤立的函数而是实现inventory.ServerTool由 pkg/inventory/registry.go 统一注册。Builder 模式负责按 toolset、只读开关、特性开关做可见性过滤。// pkg/github/git.go func GetRepositoryTree(t translations.TranslationHelperFunc) inventory.ServerTool { return NewTool( ToolsetMetadataGit, // 归入 git 工具集 mcp.Tool{ Name: get_repository_tree, Description: t(TOOL_GET_REPOSITORY_TREE_DESCRIPTION, ...), Annotations: mcp.ToolAnnotations{ Title: t(TOOL_GET_REPOSITORY_TREE_USER_TITLE, Get repository tree), ReadOnlyHint: true, // 只读工具必须显式标注 }, InputSchema: jsonschema.Schema{ Type: object, Properties: map[string]*jsonschema.Schema{ owner: {Type: string, Description: Repository owner}, repo: {Type: string, Description: Repository name}, // ... }, Required: []string{owner, repo}, }, }, []scopes.Scope{scopes.Repo}, // 声明所需 scope // handler 见下 ) }规范要点工具名、描述、所需 scope、所属工具集、读写属性五项元数据缺一不可。3.2 声明参数Schema 即契约InputSchema直接就是 JSON SchemaMCP 客户端会据此做前置校验。写工具时这里要把枚举范围、默认值、最小最大值全部写进去别让校验逻辑散落进 handler。tree_sha: {Type: string, Description: SHA1 or ref name. Defaults to default branch}, recursive: {Type: boolean, Default: json.RawMessage(false)}, path_filter: {Type: string, Description: Optional prefix filter (e.g. src/)},3.3 校验统一走 params.go 的泛型工具为什么统一如果每个工具自己写args[owner].(string)会重复造轮子且错误信息不一致。pkg/github/params.go 把「必填 / 可选 / 带默认值」三件事收敛成泛型函数。// pkg/github/git.go handler 内部 owner, err : RequiredParamstring if err ! nil { return utils.NewToolResultError(err.Error()), nil, nil // 参数错直接返回 } repo, err : RequiredParamstring treeSHA, err : OptionalParamstring recursive, err : OptionalBoolParamWithDefault(args, recursive, false)RequiredParam做三重检查是否存在 → 类型是否正确 → 是否零值任何一环不通过都返回带清晰文案的错误。3.4 调 APIclient 从依赖注入不自己 newclient, err : deps.GetClient(ctx) if err ! nil { return utils.NewToolResultError(failed to get GitHub client), nil, nil } tree, resp, err : client.Git.GetTree(ctx, owner, repo, treeSHA, recursive) if err ! nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, failed to get repository tree, resp, err), nil, nil } defer func() { _ resp.Body.Close() }() // ✅ 资源清理见第 4.4 节规范要点client 永远来自deps.GetClient(ctx)不直接构造——这样认证、限流、特性开关才可能生效。3.5 处理错误 3.6 返回结果错误分层 结果封装错误统一走 pkg/errors/error.go 的NewGitHubAPIErrorResponse它对速率限制、滥用限制、普通 API 错误做了差异化文案并把错误记进context供中间件做可观测性上报。treeEntries : make([]TreeEntryResponse, 0, len(filteredEntries)) // ... 组装响应结构 r, err : json.Marshal(response) if err ! nil { return nil, nil, fmt.Errorf(failed to marshal response: %w, err) } result : utils.NewToolResultText(string(r)) // 附加可见性相关的 IFC 标签 result attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, result, ifc.LabelCommitContents) return result, nil, nil这一条链路就是全文主线新增任何工具都是把这 6 步重复一遍细节交给统一工具函数。4. 五个关键设计决策为什么这么写4.1 模块化按功能域拆文件而不是按技术层拆结论pkg/github下每个文件对应一个 GitHub 能力域actions / issues / search / search_utils...而不是把「所有 REST 调用」塞一个文件。理由MCP 工具天然是「按功能域聚合」的这样改 Issues 工具时不会被 Actions 代码干扰review 也更聚焦。// pkg/github/issues.go —— Issue 相关工具 // pkg/github/search.go —— 搜索相关工具 // pkg/github/search_utils.go —— 搜索的共享辅助同域内拆细4.2 统一参数一套泛型函数覆盖必填/可选/默认结论不重复实现参数取值逻辑全部走 params.go 的RequiredParam/OptionalParam/*WithDefault。// 必填 func RequiredParamT comparable (T, error) { var zero T if _, ok : args[p]; !ok { return zero, fmt.Errorf(missing required parameter: %s, p) } val, ok : args[p].(T) if !ok { return zero, fmt.Errorf(parameter %s is not of type %T, p, zero) } if val zero { return zero, fmt.Errorf(missing required parameter: %s, p) } return val, nil }好处错误文案全局一致排查参数问题时看错误信息就能定位是哪个工具。4.3 分层错误参数错 / API 错 / 限流错各走各的通道结论不把所有err都fmt.Errorf后返回而是按类型分派。// pkg/errors/error.go func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github.Response, err error) *mcp.CallToolResult { var rateLimitErr *github.RateLimitError if errors.As(err, rateLimitErr) { // 返回带「Retry after Ns」的友好提示 return utils.NewToolResultError( fmt.Sprintf(%s: rate limit exceeded. Retry after %v., message, time.Until(rateLimitErr.Rate.Reset.Time))) } // ... 普通错误走统一封装 return utils.NewToolResultErrorFromErr(message, err) }为什么AI 客户端拿到「限流 可重试时间」这类结构化信息才能做智能退避普通错误则走统一上下文记录供中间件统计。4.4 资源清理响应体必须 defer 关闭结论所有返回*github.Response的调用成功后必须defer关闭Body。tree, resp, err : client.Git.GetTree(ctx, owner, repo, treeSHA, recursive) if err ! nil { /* 走错误处理 */ } defer func() { _ resp.Body.Close() }() // 用过的杯子要放回架子为什么HTTP 连接池复用依赖Body被正确关闭泄漏会导致文件句柄耗尽、连接无法复用。这里用defer包裹的闭包写法而非defer resp.Body.Close()是为了在err ! nil分支提前返回时依然执行因为resp只在成功分支非空需要条件 defer 的语义由调用顺序保证——在err检查之后再defer。4.5 分页 / 日志性能把「重活」限制在可控范围结论一分页提供统一的WithPagination把 REST 分页参数声明固化避免每个工具手写page/perPage。// pkg/github/params.go func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { schema.Properties[page] jsonschema.Schema{ Type: number, Description: Page number (min 1), Minimum: jsonschema.Ptr(1.0), } schema.Properties[perPage] jsonschema.Schema{ Type: number, Description: Results per page (min 1, max 100), Minimum: jsonschema.Ptr(1.0), Maximum: jsonschema.Ptr(100.0), } return schema }结论二日志/大对象处理 GitHub Actions 日志这类可能几十 MB 的大文件时用环形缓冲只保留尾部 N 行避免整块读进内存并配合 internal/profiler/ 记录耗时与内存增量。prof : profiler.New(nil, profiler.IsProfilingEnabled()) finish : prof.Start(ctx, log_buffer_processing) // ... 处理日志 _ finish(lines, int64(len(result)))为什么MCP 工具常被 AI 客户端高频调用任何一处「无脑读全量」都可能拖垮服务把上限如perPage ≤ 100、日志只取尾部写进规范是从源头控制成本。⚠️5. 排错与踩坑手册高频问题速查现象常见原因正确做法官方风格客户端报missing required parameterSchema 里标了必填但 handler 里没校验统一用RequiredParam[T]别自己写args[x].(string)参数类型不匹配报错客户端把数字传成了字符串或反之用RequiredInt/OptionalIntParam这类兼容float64与数字字符串的工具函数工具返回AcceptedError但你当普通错误抛了GitHub 某些操作如触发 Actions返回 202 表示「已受理未完成」用isAcceptedError(err)判断后走专门的handleAcceptedError分支服务跑久了文件句柄/连接耗尽忘记关闭resp.Body每次client.*.Xxx(ctx, ...)成功后紧跟defer func(){ _ resp.Body.Close() }()AI 客户端反复重试触发限流错误信息没告诉客户端「多久后可重试」错误统一经NewGitHubAPIErrorResponse它会解析RateLimitError并输出Retry after Ns工具在只读模式下仍出现忘了设置ReadOnlyHint或没归入正确 toolsetmcp.ToolAnnotations{ReadOnlyHint: true}必须显式声明配合Inventory的过滤描述文案中英混杂没走翻译 helper所有用户可见文本用t(KEY, default)便于多语言小技巧看到*WithDefault系列的参数工具说明该字段允许缺省缺省时用默认值而非报错——这和RequiredParam的语义是相反的别混用。6. 上手清单写一个合规的新工具逐条过一遍✅元数据完整工具名snake_case、描述走翻译 helper、ReadOnlyHint、所属 toolset、所需scopes五项齐全。✅Schema 即契约枚举、默认值、min/max全部写进InputSchema不要把校验逻辑散落进 handler。✅参数校验走统一函数必填用RequiredParam/RequiredInt可选用OptionalParam/*WithDefault分页用WithPagination。✅client 从deps.GetClient(ctx)取不手动构造。✅错误分层参数错 →utils.NewToolResultErrorAPI 错 →ghErrors.NewGitHubAPIErrorResponse限流/滥用走其内部分支。✅响应体 defer 关闭resp非空路径必须有defer func(){ _ resp.Body.Close() }()。✅结果统一封装用utils.NewToolResultText/NewToolResultError不裸返回字符串。✅大对象有上限列表类接口必带分页且perPage ≤ 100大文本如日志只取尾部并配profiler记录开销。✅可测handler 里的每个分支参数缺失、类型错、API 失败、限流都能被单测覆盖参数工具函数已自带测试见params_test.go你的新工具也应至少覆盖「成功 / 参数错 / API 错」三条路径。✅安全令牌按最小权限申请只读工具不要申请写 scope敏感值不写进日志/描述文案。小结GitHub MCP Server 的工程规范本质是把「写一个 MCP 工具」拆成了 6 步可重复的动作并用独立的小包params / errors / profiler / inventory把每个环节的实现细节固化。照着第 6 节清单逐条打勾你写出的新工具就能和官方代码保持同一「味道」——这比读懂任何单个文件更重要。【免费下载链接】github-mcp-serverGitHubs official MCP Server项目地址: https://gitcode.com/GitHub_Trending/gi/github-mcp-server创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表