This document describes the error handling patterns used in the GitHub MCP Server, specifically how we handle GitHub API errors and avoid direct use of mcp-go error types.
The GitHub MCP Server implements a custom error handling approach that serves two primary purposes:
- Tool Response Generation: Return appropriate MCP tool error responses to clients
- Middleware Inspection: Store detailed error information in the request context for middleware analysis
This dual approach enables better observability and debugging capabilities, particularly for remote server deployments where understanding the nature of failures (rate limiting, authentication, 404s, 500s, etc.) is crucial for validation and monitoring.
Used for REST API errors from the GitHub API:
type GitHubAPIError struct {
Message string `json:"message"`
Response *github.Response `json:"-"`
Err error `json:"-"`
}Used for GraphQL API errors from the GitHub API:
type GitHubGraphQLError struct {
Message string `json:"message"`
Err error `json:"-"`
}Instead of directly returning mcp.NewToolResultError(), use:
return ghErrors.NewGitHubAPIErrorResponse(ctx, message, response, err), nilThis function:
- Creates a
GitHubAPIErrorwith the provided message, response, and error - Stores the error in the context for middleware inspection
- Returns an appropriate MCP tool error response
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, message, err), nilTools registered with concrete NewTool[In, Out] types expose output schemas and
structuredContent when the negotiated protocol version is a supported version
2026-07-28 or newer. Older, unknown, or omitted protocol versions retain the
original text responses without these typed-output additions. Unknown versions
are not treated as supported merely because they sort after 2026-07-28.
Ordinary API and validation errors never expose the SDK-generated zero-value
structured output.
Handler-provided structured statuses are distinct from generated error output.
For example, issue_write can return IsError: true with
status: "awaiting_user_submission" while an MCP App form is pending. Modern
clients retain that explicit status and its reason; legacy clients retain the
stop message and error flag without structured content. This status does not
mean the issue was created or updated. The output schema includes a strict
status variant alongside the mutation's id/url variant. Multi-round-trip
InputRequests continue to pass through unchanged.
The consolidated issue tools use method-discriminated object outputs for modern successes. Issue and comment data are projected from the API responses into compact DTOs, excluding API, node, and hypermedia URLs while retaining human links. Legacy text is marshalled separately and keeps its original format and fields.
The error handling system uses context to store errors for later inspection:
// Initialize context with error tracking
ctx = errors.ContextWithGitHubErrors(ctx)
// Retrieve errors for inspection (typically in middleware)
apiErrors, err := errors.GetGitHubAPIErrors(ctx)
graphqlErrors, err := errors.GetGitHubGraphQLErrors(ctx)- User-actionable errors (authentication failures, rate limits, 404s) should be returned as failed tool calls using the error response functions
- Developer errors (JSON marshaling failures, internal logic errors) should be returned as actual Go errors that bubble up through the MCP framework
This approach was designed to work around current limitations in mcp-go where context is not propagated through each step of request processing. By storing errors in context values, middleware can inspect them without requiring context propagation.
Error storage operations in context are designed to fail gracefully - if context storage fails, the tool will still return an appropriate error response to the client.
- Observability: Middleware can inspect the specific types of GitHub API errors occurring
- Debugging: Detailed error information is preserved without exposing potentially sensitive data in logs
- Validation: Remote servers can use error types and HTTP status codes to validate that changes don't break functionality
- Privacy: Error inspection can be done programmatically using
errors.Ischecks without logging PII
func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_issue", /* ... */),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get issue",
resp,
err,
), nil
}
return MarshalledTextResult(issue), nil
}
}This approach ensures that both the client receives an appropriate error response and any middleware can inspect the underlying GitHub API error for monitoring and debugging purposes.