Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pkg/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,11 @@ func NewGitHubGraphQLErrorResponse(ctx context.Context, message string, err erro
}
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 {
err := fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
return NewGitHubAPIErrorResponse(ctx, message, resp, err)
}
27 changes: 27 additions & 0 deletions pkg/errors/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,33 @@ func TestGitHubErrorContext(t *testing.T) {
assert.Equal(t, originalErr, gqlError.Err)
})

t.Run("NewGitHubAPIStatusErrorResponse creates MCP error result from status code", func(t *testing.T) {
// Given a context with GitHub error tracking enabled
ctx := ContextWithGitHubErrors(context.Background())

resp := &github.Response{Response: &http.Response{StatusCode: 422}}
body := []byte(`{"message": "Validation Failed"}`)

// When we create a status error response
result := NewGitHubAPIStatusErrorResponse(ctx, "failed to create issue", resp, body)

// Then it should return an MCP error result
require.NotNil(t, result)
assert.True(t, result.IsError)

// And the error should be stored in the context
apiErrors, err := GetGitHubAPIErrors(ctx)
require.NoError(t, err)
require.Len(t, apiErrors, 1)

apiError := apiErrors[0]
assert.Equal(t, "failed to create issue", apiError.Message)
assert.Equal(t, resp, apiError.Response)
// The synthetic error should contain the status code and body
assert.Contains(t, apiError.Err.Error(), "unexpected status 422")
assert.Contains(t, apiError.Err.Error(), "Validation Failed")
})

t.Run("NewGitHubAPIErrorToCtx with uninitialized context does not error", func(t *testing.T) {
// Given a regular context without GitHub error tracking initialized
ctx := context.Background()
Expand Down
5 changes: 2 additions & 3 deletions pkg/github/code_scanning.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package github
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"

Expand Down Expand Up @@ -80,7 +79,7 @@ func GetCodeScanningAlert(t translations.TranslationHelperFunc) inventory.Server
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get alert", resp, body), nil, nil
}

r, err := json.Marshal(alert)
Expand Down Expand Up @@ -184,7 +183,7 @@ func ListCodeScanningAlerts(t translations.TranslationHelperFunc) inventory.Serv
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list alerts", resp, body), nil, nil
}

r, err := json.Marshal(alerts)
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/dependabot.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func GetDependabotAlert(t translations.TranslationHelperFunc) inventory.ServerTo
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, err
}
return utils.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get alert", resp, body), nil, nil
}

r, err := json.Marshal(alert)
Expand Down Expand Up @@ -172,7 +172,7 @@ func ListDependabotAlerts(t translations.TranslationHelperFunc) inventory.Server
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, err
}
return utils.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list alerts", resp, body), nil, nil
}

r, err := json.Marshal(alerts)
Expand Down
8 changes: 4 additions & 4 deletions pkg/github/gists.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func ListGists(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to list gists: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list gists", resp, body), nil, nil
}

r, err := json.Marshal(gists)
Expand Down Expand Up @@ -149,7 +149,7 @@ func GetGist(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to get gist: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get gist", resp, body), nil, nil
}

r, err := json.Marshal(gist)
Expand Down Expand Up @@ -248,7 +248,7 @@ func CreateGist(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to create gist: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create gist", resp, body), nil, nil
}

minimalResponse := MinimalResponse{
Expand Down Expand Up @@ -350,7 +350,7 @@ func UpdateGist(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to update gist: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to update gist", resp, body), nil, nil
}

minimalResponse := MinimalResponse{
Expand Down
20 changes: 10 additions & 10 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func GetIssue(ctx context.Context, client *github.Client, cache *lockdown.RepoAc
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get issue", resp, body), nil
}

if flags.LockdownMode {
Expand Down Expand Up @@ -396,7 +396,7 @@ func GetIssueComments(ctx context.Context, client *github.Client, cache *lockdow
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to get issue comments: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get issue comments", resp, body), nil
}
if flags.LockdownMode {
if cache == nil {
Expand Down Expand Up @@ -455,7 +455,7 @@ func GetSubIssues(ctx context.Context, client *github.Client, cache *lockdown.Re
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to list sub-issues: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list sub-issues", resp, body), nil
}

if featureFlags.LockdownMode {
Expand Down Expand Up @@ -588,7 +588,7 @@ func ListIssueTypes(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to list issue types: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list issue types", resp, body), nil, nil
}

r, err := json.Marshal(issueTypes)
Expand Down Expand Up @@ -673,7 +673,7 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to create comment: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create comment", resp, body), nil, nil
}

r, err := json.Marshal(createdComment)
Expand Down Expand Up @@ -823,7 +823,7 @@ func AddSubIssue(ctx context.Context, client *github.Client, owner string, repo
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to add sub-issue: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to add sub-issue", resp, body), nil
}

r, err := json.Marshal(subIssue)
Expand Down Expand Up @@ -855,7 +855,7 @@ func RemoveSubIssue(ctx context.Context, client *github.Client, owner string, re
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to remove sub-issue: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to remove sub-issue", resp, body), nil
}

r, err := json.Marshal(subIssue)
Expand Down Expand Up @@ -904,7 +904,7 @@ func ReprioritizeSubIssue(ctx context.Context, client *github.Client, owner stri
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to reprioritize sub-issue: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to reprioritize sub-issue", resp, body), nil
}

r, err := json.Marshal(subIssue)
Expand Down Expand Up @@ -1195,7 +1195,7 @@ func CreateIssue(ctx context.Context, client *github.Client, owner string, repo
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to create issue: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create issue", resp, body), nil
}

// Return minimal response with just essential information
Expand Down Expand Up @@ -1256,7 +1256,7 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to update issue: %s", string(body))), nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to update issue", resp, body), nil
}

// Use GraphQL API for state updates
Expand Down
12 changes: 6 additions & 6 deletions pkg/github/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func ListNotifications(t translations.TranslationHelperFunc) inventory.ServerToo
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to get notifications: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get notifications", resp, body), nil, nil
}

// Marshal response to JSON
Expand Down Expand Up @@ -236,7 +236,7 @@ func DismissNotification(t translations.TranslationHelperFunc) inventory.ServerT
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to mark notification as %s: %s", state, string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, fmt.Sprintf("failed to mark notification as %s", state), resp, body), nil, nil
}

return utils.NewToolResultText(fmt.Sprintf("Notification marked as %s", state)), nil, nil
Expand Down Expand Up @@ -329,7 +329,7 @@ func MarkAllNotificationsRead(t translations.TranslationHelperFunc) inventory.Se
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to mark all notifications as read: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to mark all notifications as read", resp, body), nil, nil
}

return utils.NewToolResultText("All notifications marked as read"), nil, nil
Expand Down Expand Up @@ -387,7 +387,7 @@ func GetNotificationDetails(t translations.TranslationHelperFunc) inventory.Serv
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to get notification details: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get notification details", resp, body), nil, nil
}

r, err := json.Marshal(thread)
Expand Down Expand Up @@ -481,7 +481,7 @@ func ManageNotificationSubscription(t translations.TranslationHelperFunc) invent

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return utils.NewToolResultError(fmt.Sprintf("failed to %s notification subscription: %s", action, string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, fmt.Sprintf("failed to %s notification subscription", action), resp, body), nil, nil
}

if action == NotificationActionDelete {
Expand Down Expand Up @@ -589,7 +589,7 @@ func ManageRepositoryNotificationSubscription(t translations.TranslationHelperFu
// Handle non-2xx status codes
if resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) {
body, _ := io.ReadAll(resp.Body)
return utils.NewToolResultError(fmt.Sprintf("failed to %s repository subscription: %s", action, string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, fmt.Sprintf("failed to %s repository subscription", action), resp, body), nil, nil
}

if action == RepositorySubscriptionActionDelete {
Expand Down
10 changes: 5 additions & 5 deletions pkg/github/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func GetProject(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to get project: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get project", resp, body), nil, nil
}

minimalProject := convertToMinimalProject(project)
Expand Down Expand Up @@ -423,7 +423,7 @@ func GetProjectField(t translations.TranslationHelperFunc) inventory.ServerTool
if err != nil {
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("failed to get project field: %s", string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get project field", resp, body), nil, nil
}
r, err := json.Marshal(projectField)
if err != nil {
Expand Down Expand Up @@ -782,7 +782,7 @@ func AddProjectItem(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectAddFailedError, string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectAddFailedError, resp, body), nil, nil
}
r, err := json.Marshal(addedItem)
if err != nil {
Expand Down Expand Up @@ -896,7 +896,7 @@ func UpdateProjectItem(t translations.TranslationHelperFunc) inventory.ServerToo
if err != nil {
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectUpdateFailedError, string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectUpdateFailedError, resp, body), nil, nil
}
r, err := json.Marshal(updatedItem)
if err != nil {
Expand Down Expand Up @@ -988,7 +988,7 @@ func DeleteProjectItem(t translations.TranslationHelperFunc) inventory.ServerToo
if err != nil {
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
}
return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectDeleteFailedError, string(body))), nil, nil
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectDeleteFailedError, resp, body), nil, nil
}
return utils.NewToolResultText("project item successfully deleted"), nil, nil
}
Expand Down
Loading