Migrate from storing GitHub API errors in context via middleware to embedding typed errors directly in CallToolResult using the Go SDK v1.3.0 SetError/GetError API. Key changes: - NewGitHubAPIErrorResponse/NewGitHubGraphQLErrorResponse/ NewGitHubRawAPIErrorResponse now use result.SetError() with typed errors instead of storing in context - Add Unwrap() to all error types for errors.As/errors.Is support - Export error constructors (NewGitHubAPIError, etc.) for direct use - Remove context-based error infrastructure (ContextWithGitHubErrors, GetGitHubAPIErrors, GitHubCtxErrors, etc.) - Remove addGitHubAPIErrorToContext middleware from server.go - Remove NewGitHubAPIErrorToCtx/NewGitHubGraphQLErrorToCtx and their callers in repositories_helper.go and actions.go - Update tests to verify SetError/GetError and errors.As extraction - Update error-handling.md documentation Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
5.0 KiB
Error Handling
This document describes the error handling patterns used in the GitHub MCP Server, specifically how we handle GitHub API errors using the MCP SDK's SetError/GetError mechanism.
Overview
The GitHub MCP Server uses the Go SDK's CallToolResult.SetError() to embed typed GitHub API errors directly in tool results. This approach enables:
- Tool Response Generation: Return appropriate MCP tool error responses to clients
- Error Type Inspection: Consumers can use
result.GetError()witherrors.Asto extract typed errors for analysis
This is powered by the Go SDK v1.3.0+ SetError/GetError methods on CallToolResult, which embed a Go error in the result alongside the error text content.
Error Types
GitHubAPIError
Used for REST API errors from the GitHub API:
type GitHubAPIError struct {
Message string `json:"message"`
Response *github.Response `json:"-"`
Err error `json:"-"`
}
GitHubGraphQLError
Used for GraphQL API errors from the GitHub API:
type GitHubGraphQLError struct {
Message string `json:"message"`
Err error `json:"-"`
}
GitHubRawAPIError
Used for raw HTTP API errors from the GitHub API:
type GitHubRawAPIError struct {
Message string `json:"message"`
Response *http.Response `json:"-"`
Err error `json:"-"`
}
Usage Patterns
For GitHub REST API Errors
When a GitHub REST API call fails, use:
return ghErrors.NewGitHubAPIErrorResponse(ctx, message, response, err), nil
This function:
- Creates a
GitHubAPIErrorwith the provided message, response, and error - Calls
result.SetError()to embed the typed error in the tool result - Returns a
CallToolResultwithIsError: trueand error text content
For GitHub GraphQL API Errors
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, message, err), nil
For Raw HTTP API Errors
return ghErrors.NewGitHubRawAPIErrorResponse(ctx, message, response, err), nil
Extracting Errors from Results
Consumers (such as middleware or the remote server) can extract typed errors from results:
if err := result.GetError(); err != nil {
var apiErr *errors.GitHubAPIError
if errors.As(err, &apiErr) {
// Access apiErr.Response.StatusCode, apiErr.Message, etc.
}
var gqlErr *errors.GitHubGraphQLError
if errors.As(err, &gqlErr) {
// Access gqlErr.Message, gqlErr.Err, etc.
}
}
Design Principles
User-Actionable vs. Developer Errors
- 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
Type Safety with SetError/GetError
All GitHub API error types implement the error interface with Unwrap() support, enabling:
errors.As()to extract the specific error type (e.g.,*GitHubAPIError)errors.Is()to check for the underlying cause- Standard Go error handling patterns
Benefits
- Type Safety: Errors are embedded in the result as typed Go errors, not just strings
- Observability: Middleware can inspect the specific types of GitHub API errors using
errors.As - Simplicity: No context-based error storage or middleware setup required
- Debugging: Detailed error information (HTTP status codes, response objects) is preserved
- Privacy: Error inspection can be done programmatically using
errors.Is/errors.Aschecks
Example Implementation
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
}
}
The error can then be inspected by consumers:
result, err := handler(ctx, request)
if err == nil && result.IsError {
if apiErr := result.GetError(); apiErr != nil {
var ghErr *errors.GitHubAPIError
if errors.As(apiErr, &ghErr) {
log.Printf("GitHub API error: status=%d message=%s",
ghErr.Response.StatusCode, ghErr.Message)
}
}
}