Repository navigation
refactor(errors): improve GitHubAPIError handling, unwrap support, and GoDoc documentation - #15
Conversation
Signed-off-by: axe <axeonetwothree@gmail.com>
axeon3
left a comment
There was a problem hiding this comment.
// Package errors provides structured error wrappers, context-based error
// tracking for observability middleware, and MCP tool error response builders
// for GitHub REST, GraphQL, and raw HTTP API calls.
package errors
import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"net/http"
"slices"
"strings"
"sync"
"time"
"github.com/github/github-mcp-server/v2/pkg/sanitize"
"github.com/github/github-mcp-server/v2/pkg/utils"
"github.com/google/go-github/v92/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// GitHubAPIError represents a failed GitHub REST API call made via the
// go-github client. It pairs a human-readable context message with the
// underlying github.Response and error for observability and inspection.
type GitHubAPIError struct {
Message string json:"message"
Response *github.Response json:"-"
Err error json:"-"
}
// newGitHubAPIError creates a new GitHubAPIError with the provided context
// message, GitHub API response, and underlying error.
func newGitHubAPIError(message string, resp *github.Response, err error) *GitHubAPIError {
return &GitHubAPIError{
Message: message,
Response: resp,
Err: err,
}
}
// Error implements the error interface, formatting the context message and
// underlying error.
func (e *GitHubAPIError) Error() string {
if e == nil {
return ""
}
return formatErrorMessage(e.Message, e.Err)
}
// Unwrap returns the underlying error for use with errors.Is and errors.As.
func (e *GitHubAPIError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
// GitHubGraphQLError represents a failed GitHub GraphQL API query or mutation.
// It pairs a human-readable context message with the underlying error.
type GitHubGraphQLError struct {
Message string json:"message"
Err error json:"-"
}
// newGitHubGraphQLError creates a new GitHubGraphQLError with the provided
// context message and underlying error.
func newGitHubGraphQLError(message string, err error) *GitHubGraphQLError {
return &GitHubGraphQLError{
Message: message,
Err: err,
}
}
// Error implements the error interface, formatting the context message and
// underlying error.
func (e *GitHubGraphQLError) Error() string {
if e == nil {
return ""
}
return formatErrorMessage(e.Message, e.Err)
}
// Unwrap returns the underlying error for use with errors.Is and errors.As.
func (e *GitHubGraphQLError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
// GitHubRawAPIError represents a failed raw HTTP request to a GitHub endpoint
// (outside the standard go-github typed client). It retains the raw
// http.Response and underlying error for middleware inspection.
type GitHubRawAPIError struct {
Message string json:"message"
Response *http.Response json:"-"
Err error json:"-"
}
// newGitHubRawAPIError creates a new GitHubRawAPIError with the provided
// context message, raw HTTP response, and underlying error.
func newGitHubRawAPIError(message string, resp *http.Response, err error) *GitHubRawAPIError {
return &GitHubRawAPIError{
Message: message,
Response: resp,
Err: err,
}
}
// Error implements the error interface, formatting the context message and
// underlying error.
func (e *GitHubRawAPIError) Error() string {
if e == nil {
return ""
}
return formatErrorMessage(e.Message, e.Err)
}
// Unwrap returns the underlying error for use with errors.Is and errors.As.
func (e *GitHubRawAPIError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
// formatErrorMessage formats a context message and an optional underlying error.
func formatErrorMessage(message string, err error) string {
switch {
case message != "" && err != nil:
return fmt.Sprintf("%s: %v", message, err)
case err != nil:
return err.Error()
default:
return message
}
}
// GitHubErrorKey is the context key used to store and retrieve GitHubCtxErrors.
type GitHubErrorKey struct{}
// GitHubCtxErrors holds slices of REST, GraphQL, and raw HTTP errors collected
// during the lifecycle of a request context for downstream middleware logging
// and telemetry.
type GitHubCtxErrors struct {
mu sync.Mutex
api []*GitHubAPIError
graphQL []*GitHubGraphQLError
raw []*GitHubRawAPIError
}
// ContextWithGitHubErrors initializes or resets a GitHubCtxErrors container in
// the provided context so middleware and tool handlers can record GitHub API
// errors during request execution.
func ContextWithGitHubErrors(ctx context.Context) context.Context {
if ctx == nil {
ctx = context.Background()
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.api = []*GitHubAPIError{}
val.graphQL = []*GitHubGraphQLError{}
val.raw = []*GitHubRawAPIError{}
val.mu.Unlock()
} else {
ctx = context.WithValue(ctx, GitHubErrorKey{}, &GitHubCtxErrors{})
}
return ctx
}
// GetGitHubAPIErrors retrieves a copy of the GitHubAPIError slice recorded in
// the context. It returns an error if the context is nil or not initialized
// with ContextWithGitHubErrors.
func GetGitHubAPIErrors(ctx context.Context) ([]*GitHubAPIError, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
defer val.mu.Unlock()
return slices.Clone(val.api), nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
// GetGitHubGraphQLErrors retrieves a copy of the GitHubGraphQLError slice
// recorded in the context. It returns an error if the context is nil or not
// initialized with ContextWithGitHubErrors.
func GetGitHubGraphQLErrors(ctx context.Context) ([]*GitHubGraphQLError, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
defer val.mu.Unlock()
return slices.Clone(val.graphQL), nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
// GetGitHubRawAPIErrors retrieves a copy of the GitHubRawAPIError slice
// recorded in the context. It returns an error if the context is nil or not
// initialized with ContextWithGitHubErrors.
func GetGitHubRawAPIErrors(ctx context.Context) ([]*GitHubRawAPIError, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
defer val.mu.Unlock()
return slices.Clone(val.raw), nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
// NewGitHubAPIErrorToCtx records a new GitHubAPIError in the context without
// generating an MCP tool error response. If the context does not contain
// GitHubCtxErrors, the error is silently ignored for graceful degradation.
func NewGitHubAPIErrorToCtx(ctx context.Context, message string, resp *github.Response, err error) (context.Context, error) {
apiErr := newGitHubAPIError(message, resp, err)
if ctx != nil {
_, _ = addGitHubAPIErrorToContext(ctx, apiErr)
}
return ctx, nil
}
// NewGitHubGraphQLErrorToCtx records a new GitHubGraphQLError in the context
// without generating an MCP tool error response. If the context does not
// contain GitHubCtxErrors, the error is silently ignored for graceful
// degradation.
func NewGitHubGraphQLErrorToCtx(ctx context.Context, message string, err error) (context.Context, error) {
graphQLErr := newGitHubGraphQLError(message, err)
if ctx != nil {
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr)
}
return ctx, nil
}
func addGitHubAPIErrorToContext(ctx context.Context, err *GitHubAPIError) (context.Context, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.api = append(val.api, err)
val.mu.Unlock()
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError) (context.Context, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.graphQL = append(val.graphQL, err)
val.mu.Unlock()
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
func addRawAPIErrorToContext(ctx context.Context, err *GitHubRawAPIError) (context.Context, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.raw = append(val.raw, err)
val.mu.Unlock()
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
// NewGitHubAPIErrorResponse records a GitHubAPIError in the context for
// middleware observability and returns a formatted MCP CallToolResult error.
// It provides specialized actionable messages for primary rate limits,
// secondary (abuse) rate limits, and 422 validation errors.
func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github.Response, err error) *mcp.CallToolResult {
apiErr := newGitHubAPIError(message, resp, err)
if ctx != nil {
_, _ = addGitHubAPIErrorToContext(ctx, apiErr)
}
var rateLimitErr *github.RateLimitError
if stderrors.As(err, &rateLimitErr) && rateLimitErr != nil {
resetTime := rateLimitErr.Rate.Reset.Time
if !resetTime.IsZero() {
retryIn := time.Until(resetTime).Round(time.Second)
if retryIn > 0 {
return utils.NewToolResultError(fmt.Sprintf(
"%s: GitHub API rate limit exceeded. Retry after %v.", message, retryIn))
}
}
return utils.NewToolResultError(fmt.Sprintf(
"%s: GitHub API rate limit exceeded. Wait before retrying.", message))
}
var abuseErr *github.AbuseRateLimitError
if stderrors.As(err, &abuseErr) && abuseErr != nil {
if abuseErr.RetryAfter != nil {
retryAfter := abuseErr.RetryAfter.Round(time.Second)
if retryAfter > 0 {
return utils.NewToolResultError(fmt.Sprintf(
"%s: GitHub secondary rate limit exceeded. Retry after %v.",
message, retryAfter))
}
}
return utils.NewToolResultError(fmt.Sprintf(
"%s: GitHub secondary rate limit exceeded. Wait before retrying.", message))
}
return utils.NewToolResultErrorFromErr(message, formatGitHubValidationError(resp, err))
}
// formatGitHubValidationError extracts and sanitizes field-level validation
// details from HTTP 422 (Unprocessable Entity) GitHub ErrorResponses while
// omitting raw HTTP exchange metadata.
func formatGitHubValidationError(resp *github.Response, err error) error {
var ghErr *github.ErrorResponse
if !stderrors.As(err, &ghErr) || ghErr == nil {
return err
}
statusCode := 0
if ghErr.Response != nil {
statusCode = ghErr.Response.StatusCode
}
if statusCode == 0 && resp != nil && resp.Response != nil {
statusCode = resp.StatusCode
}
if statusCode != http.StatusUnprocessableEntity {
return err
}
parts := make([]string, 0, len(ghErr.Errors)+1)
if summary := sanitizeGitHubValidationText(ghErr.Message); summary != "" {
parts = append(parts, summary)
}
for _, validationErr := range ghErr.Errors {
if detail := formatGitHubValidationDetail(validationErr); detail != "" {
parts = append(parts, detail)
}
}
if len(parts) == 0 {
return stderrors.New("GitHub API validation failed")
}
return stderrors.New(strings.Join(parts, "\n"))
}
// formatGitHubValidationDetail formats a single github.Error entry into a
// concise "Resource.field (code): message" string.
func formatGitHubValidationDetail(validationErr github.Error) string {
resource := sanitizeGitHubValidationText(validationErr.Resource)
field := sanitizeGitHubValidationText(validationErr.Field)
code := sanitizeGitHubValidationText(validationErr.Code)
message := sanitizeGitHubValidationText(validationErr.Message)
location := strings.Trim(strings.Join([]string{resource, field}, "."), ".")
switch {
case location != "" && code != "":
location += " (" + code + ")"
case location == "":
location = code
}
switch {
case location != "" && message != "":
return location + ": " + message
case message != "":
return message
default:
return location
}
}
// sanitizeGitHubValidationText strips control/unsafe characters and collapses
// whitespace in validation error fields.
func sanitizeGitHubValidationText(value string) string {
sanitized := sanitize.PlainText(value)
return strings.Join(strings.Fields(sanitized), " ")
}
// NewGitHubGraphQLErrorResponse records a GitHubGraphQLError in the context
// for middleware observability and returns an MCP CallToolResult error.
func NewGitHubGraphQLErrorResponse(ctx context.Context, message string, err error) *mcp.CallToolResult {
graphQLErr := newGitHubGraphQLError(message, err)
if ctx != nil {
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr)
}
return utils.NewToolResultErrorFromErr(message, err)
}
// NewGitHubRawAPIErrorResponse records a GitHubRawAPIError in the context for
// middleware observability and returns an MCP CallToolResult error.
func NewGitHubRawAPIErrorResponse(ctx context.Context, message string, resp *http.Response, err error) *mcp.CallToolResult {
rawErr := newGitHubRawAPIError(message, resp, err)
if ctx != nil {
_, _ = addRawAPIErrorToContext(ctx, rawErr)
}
return utils.NewToolResultErrorFromErr(message, err)
}
// NewGitHubAPIStatusErrorResponse handles cases where a GitHub REST API call
// returns a nil client error but an unexpected HTTP status code. It synthesizes
// an error from the status code and body, records it in the context, and
// returns an MCP CallToolResult error.
func NewGitHubAPIStatusErrorResponse(ctx context.Context, message string, resp *github.Response, body []byte) *mcp.CallToolResult {
var err error
if resp != nil && resp.Response != nil {
err = fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
} else {
err = fmt.Errorf("unexpected status: %s", string(body))
}
return NewGitHubAPIErrorResponse(ctx, message, resp, err)
}
// StructuredResolutionError is a machine-readable error returned by name-resolution
// helpers (e.g. resolving a project field or single-select option by name). Agents
// can parse the JSON body to self-correct without re-prompting.
//
// Kind values:
// - "field_not_found" — no project field matches the supplied name
// - "field_ambiguous" — more than one project field shares the supplied name
// - "option_not_found" — no option on the resolved single-select field matches
// - "option_ambiguous" — duplicate option names on the resolved field
// - "item_not_in_project" — the issue/PR exists but is not an item on the project
// - "wrong_field_type" — the named field is not the data type the caller expected
type StructuredResolutionError struct {
Kind string json:"error"
Name string json:"name,omitempty"
Field string json:"field,omitempty"
Candidates []any json:"candidates,omitempty"
Hint string json:"hint,omitempty"
}
// Error implements the error interface; the message is the JSON body so that the
// downstream tool result also carries the structured payload as plain text.
func (e *StructuredResolutionError) Error() string {
if e == nil {
return {"error":"unknown"}
}
b, err := json.Marshal(e)
if err != nil {
return fmt.Sprintf({"error":%q,"name":%q}, e.Kind, e.Name)
}
return string(b)
}
// NewStructuredResolutionError constructs a StructuredResolutionError.
func NewStructuredResolutionError(kind, name, hint string, candidates []any) *StructuredResolutionError {
return &StructuredResolutionError{
Kind: kind,
Name: name,
Hint: hint,
Candidates: candidates,
}
}
// NewStructuredResolutionErrorResponse returns an mcp.CallToolResult whose text body
// is the JSON-serialised StructuredResolutionError, suitable for agent self-correction.
func NewStructuredResolutionErrorResponse(err *StructuredResolutionError) *mcp.CallToolResult {
if err == nil {
return utils.NewToolResultError({"error":"unknown"})
}
return utils.NewToolResultError(err.Error())
}
03892c1
into
agentic-autofix/fix-code-scanning-alerts
Refactors the errors package to standardize error formatting, support Go's error unwrapping chain, guard against nil-pointer dereferences and concurrent context mutations, and add complete GoDoc comments across all exported and internal types/helpers.
Changes
package errors fixed:
import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"net/http"
"slices"
"strings"
"sync"
"time"
)
type GitHubAPIError struct {
Message string
json:"message"Response *github.Response
json:"-"Err error
json:"-"}
// newGitHubAPIError creates a new GitHubAPIError with the provided message, response, and error.
func newGitHubAPIError(message string, resp *github.Response, err error) *GitHubAPIError {
return &GitHubAPIError{
Message: message,
Response: resp,
Err: err,
}
}
func (e *GitHubAPIError) Error() string {
if e == nil {
return ""
}
if e.Err == nil {
return e.Message
}
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
func (e *GitHubAPIError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
type GitHubGraphQLError struct {
Message string
json:"message"Err error
json:"-"}
func newGitHubGraphQLError(message string, err error) *GitHubGraphQLError {
return &GitHubGraphQLError{
Message: message,
Err: err,
}
}
func (e *GitHubGraphQLError) Error() string {
if e == nil {
return ""
}
if e.Err == nil {
return e.Message
}
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
func (e *GitHubGraphQLError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
type GitHubRawAPIError struct {
Message string
json:"message"Response *http.Response
json:"-"Err error
json:"-"}
func newGitHubRawAPIError(message string, resp *http.Response, err error) *GitHubRawAPIError {
return &GitHubRawAPIError{
Message: message,
Response: resp,
Err: err,
}
}
func (e *GitHubRawAPIError) Error() string {
if e == nil {
return ""
}
if e.Err == nil {
return e.Message
}
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
func (e *GitHubRawAPIError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
type GitHubErrorKey struct{}
type GitHubCtxErrors struct {
mu sync.Mutex
api []*GitHubAPIError
graphQL []*GitHubGraphQLError
raw []*GitHubRawAPIError
}
// ContextWithGitHubErrors updates or creates a context with a pointer to GitHub error information (to be used by middleware).
func ContextWithGitHubErrors(ctx context.Context) context.Context {
if ctx == nil {
ctx = context.Background()
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.api = []*GitHubAPIError{}
val.graphQL = []*GitHubGraphQLError{}
val.raw = []*GitHubRawAPIError{}
val.mu.Unlock()
} else {
ctx = context.WithValue(ctx, GitHubErrorKey{}, &GitHubCtxErrors{})
}
}
// GetGitHubAPIErrors retrieves the slice of GitHubAPIErrors from the context.
func GetGitHubAPIErrors(ctx context.Context) ([]*GitHubAPIError, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
defer val.mu.Unlock()
return slices.Clone(val.api), nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
// GetGitHubGraphQLErrors retrieves the slice of GitHubGraphQLErrors from the context.
func GetGitHubGraphQLErrors(ctx context.Context) ([]*GitHubGraphQLError, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
defer val.mu.Unlock()
return slices.Clone(val.graphQL), nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
// GetGitHubRawAPIErrors retrieves the slice of GitHubRawAPIErrors from the context.
func GetGitHubRawAPIErrors(ctx context.Context) ([]*GitHubRawAPIError, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
defer val.mu.Unlock()
return slices.Clone(val.raw), nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
func NewGitHubAPIErrorToCtx(ctx context.Context, message string, resp *github.Response, err error) (context.Context, error) {
apiErr := newGitHubAPIError(message, resp, err)
if ctx != nil {
_, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling
}
return ctx, nil
}
func NewGitHubGraphQLErrorToCtx(ctx context.Context, message string, err error) (context.Context, error) {
graphQLErr := newGitHubGraphQLError(message, err)
if ctx != nil {
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr) // Explicitly ignore error for graceful handling
}
return ctx, nil
}
func addGitHubAPIErrorToContext(ctx context.Context, err *GitHubAPIError) (context.Context, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.api = append(val.api, err)
val.mu.Unlock()
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError) (context.Context, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.graphQL = append(val.graphQL, err)
val.mu.Unlock()
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
func addRawAPIErrorToContext(ctx context.Context, err *GitHubRawAPIError) (context.Context, error) {
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok && val != nil {
val.mu.Lock()
val.raw = append(val.raw, err)
val.mu.Unlock()
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
}
// NewGitHubAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github.Response, err error) *mcp.CallToolResult {
apiErr := newGitHubAPIError(message, resp, err)
if ctx != nil {
_, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling
}
}
// formatGitHubValidationError exposes the parsed fields of 422 responses without
// including request or response metadata from the underlying HTTP exchange.
func formatGitHubValidationError(resp *github.Response, err error) error {
var ghErr *github.ErrorResponse
if !stderrors.As(err, &ghErr) || ghErr == nil {
return err
}
}
func formatGitHubValidationDetail(validationErr github.Error) string {
resource := sanitizeGitHubValidationText(validationErr.Resource)
field := sanitizeGitHubValidationText(validationErr.Field)
code := sanitizeGitHubValidationText(validationErr.Code)
message := sanitizeGitHubValidationText(validationErr.Message)
}
func sanitizeGitHubValidationText(value string) string {
sanitized := sanitize.PlainText(value)
return strings.Join(strings.Fields(sanitized), " ")
}
// NewGitHubGraphQLErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
func NewGitHubGraphQLErrorResponse(ctx context.Context, message string, err error) *mcp.CallToolResult {
graphQLErr := newGitHubGraphQLError(message, err)
if ctx != nil {
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr) // Explicitly ignore error for graceful handling
}
return utils.NewToolResultErrorFromErr(message, err)
}
// NewGitHubRawAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
func NewGitHubRawAPIErrorResponse(ctx context.Context, message string, resp *http.Response, err error) *mcp.CallToolResult {
rawErr := newGitHubRawAPIError(message, resp, err)
if ctx != nil {
_, _ = addRawAPIErrorToContext(ctx, rawErr) // Explicitly ignore error for graceful handling
}
return utils.NewToolResultErrorFromErr(message, err)
}
// NewGitHubAPIStatusErrorResponse handles cases where the API call succeeds (err == nil)
// but returns an unexpected HTTP status code. It creates a synthetic error from the
// status code and response body, then records it in context for observability tracking.
func NewGitHubAPIStatusErrorResponse(ctx context.Context, message string, resp *github.Response, body []byte) *mcp.CallToolResult {
var err error
if resp != nil && resp.Response != nil {
err = fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
} else {
err = fmt.Errorf("unexpected status: %s", string(body))
}
return NewGitHubAPIErrorResponse(ctx, message, resp, err)
}
// StructuredResolutionError is a machine-readable error returned by name-resolution
// helpers (e.g. resolving a project field or single-select option by name). Agents
// can parse the JSON body to self-correct without re-prompting.
//
// Kind values:
// - "field_not_found" — no project field matches the supplied name
// - "field_ambiguous" — more than one project field shares the supplied name
// - "option_not_found" — no option on the resolved single-select field matches
// - "option_ambiguous" — duplicate option names on the resolved field
// - "item_not_in_project" — the issue/PR exists but is not an item on the project
// - "wrong_field_type" — the named field is not the data type the caller expected
type StructuredResolutionError struct {
Kind string
json:"error"Name string
json:"name,omitempty"Field string
json:"field,omitempty"Candidates []any
json:"candidates,omitempty"Hint string
json:"hint,omitempty"}
// Error implements the error interface; the message is the JSON body so that the
// downstream tool result also carries the structured payload as plain text.
func (e *StructuredResolutionError) Error() string {
if e == nil {
return
{"error":"unknown"}}
b, err := json.Marshal(e)
if err != nil {
return fmt.Sprintf(
{"error":%q,"name":%q}, e.Kind, e.Name)}
return string(b)
}
// NewStructuredResolutionError constructs a StructuredResolutionError.
func NewStructuredResolutionError(kind, name, hint string, candidates []any) *StructuredResolutionError {
return &StructuredResolutionError{
Kind: kind,
Name: name,
Hint: hint,
Candidates: candidates,
}
}
// NewStructuredResolutionErrorResponse returns an mcp.CallToolResult whose text body
// is the JSON-serialised StructuredResolutionError, suitable for agent self-correction.
func NewStructuredResolutionErrorResponse(err *StructuredResolutionError) *mcp.CallToolResult {
if err == nil {
return utils.NewToolResultError(
{"error":"unknown"})}
return utils.NewToolResultError(err.Error())
}