From 3f1367d9252cfb1125c0f24311cc4eb5e4f9746d Mon Sep 17 00:00:00 2001 From: Quinn Stobbs Date: Fri, 20 Feb 2026 10:52:02 +1100 Subject: [PATCH] Improve API error messages with centralized error handling Introduces user-friendly error messages for common Buildkite API failures, particularly authentication (401) and authorization (403) errors. Users now receive clear, actionable guidance instead of raw HTTP error responses. Changes: - Add handleAPIError() function for centralized error handling in pkg/buildkite/errors.go - Add comprehensive test coverage in pkg/buildkite/errors_test.go - Update all 14 tool implementations to use handleAPIError() - Provide specific messages for 401 (invalid/expired token) and 403 (insufficient permissions) - Include detailed error information from API RawBody when available This improves the developer experience by making it immediately clear when API token issues occur and what steps to take to resolve them. Co-Authored-By: Claude Sonnet 4.5 --- pkg/buildkite/access_token.go | 2 +- pkg/buildkite/annotations.go | 10 +-- pkg/buildkite/artifacts.go | 26 +++--- pkg/buildkite/builds.go | 55 ++---------- pkg/buildkite/cluster_queue.go | 16 ++-- pkg/buildkite/clusters.go | 12 +-- pkg/buildkite/errors.go | 67 +++++++++++++++ pkg/buildkite/errors_test.go | 139 +++++++++++++++++++++++++++++++ pkg/buildkite/joblogs.go | 2 +- pkg/buildkite/jobs.go | 10 +-- pkg/buildkite/organizations.go | 2 +- pkg/buildkite/pipelines.go | 37 +------- pkg/buildkite/test_executions.go | 8 +- pkg/buildkite/test_runs.go | 16 ++-- pkg/buildkite/tests.go | 8 +- pkg/buildkite/user.go | 2 +- 16 files changed, 269 insertions(+), 143 deletions(-) create mode 100644 pkg/buildkite/errors.go create mode 100644 pkg/buildkite/errors_test.go diff --git a/pkg/buildkite/access_token.go b/pkg/buildkite/access_token.go index 4f2ec7b..136991a 100644 --- a/pkg/buildkite/access_token.go +++ b/pkg/buildkite/access_token.go @@ -26,7 +26,7 @@ func AccessToken(client AccessTokenClient) (tool mcp.Tool, handler server.ToolHa token, _, err := client.Get(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &token) diff --git a/pkg/buildkite/annotations.go b/pkg/buildkite/annotations.go index 69343bf..0158106 100644 --- a/pkg/buildkite/annotations.go +++ b/pkg/buildkite/annotations.go @@ -39,22 +39,22 @@ func ListAnnotations(client AnnotationsClient) (tool mcp.Tool, handler server.To orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } pipelineSlug, err := request.RequireString("pipeline_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } buildNumber, err := request.RequireString("build_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } paginationParams, err := optionalPaginationParams(request) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( @@ -69,7 +69,7 @@ func ListAnnotations(client AnnotationsClient) (tool mcp.Tool, handler server.To ListOptions: paginationParams, }) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } result := PaginatedResult[buildkite.Annotation]{ diff --git a/pkg/buildkite/artifacts.go b/pkg/buildkite/artifacts.go index 9ace1f6..748267e 100644 --- a/pkg/buildkite/artifacts.go +++ b/pkg/buildkite/artifacts.go @@ -103,22 +103,22 @@ func ListArtifactsForBuild(client ArtifactsClient) (tool mcp.Tool, handler serve orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } pipelineSlug, err := request.RequireString("pipeline_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } buildNumber, err := request.RequireString("build_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } paginationParams, err := optionalPaginationParams(request) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( @@ -133,7 +133,7 @@ func ListArtifactsForBuild(client ArtifactsClient) (tool mcp.Tool, handler serve ListOptions: paginationParams, }) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } result := PaginatedResult[buildkite.Artifact]{ @@ -184,27 +184,27 @@ func ListArtifactsForJob(client ArtifactsClient) (tool mcp.Tool, handler server. orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } pipelineSlug, err := request.RequireString("pipeline_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } buildNumber, err := request.RequireString("build_number") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } jobID, err := request.RequireString("job_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } paginationParams, err := optionalPaginationParams(request) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( @@ -220,7 +220,7 @@ func ListArtifactsForJob(client ArtifactsClient) (tool mcp.Tool, handler server. ListOptions: paginationParams, }) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } result := PaginatedResult[buildkite.Artifact]{ @@ -261,7 +261,7 @@ func GetArtifact(client ArtifactsClient) (tool mcp.Tool, handler server.ToolHand artifactURL, err := request.RequireString("url") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } // Validate the URL format @@ -275,7 +275,7 @@ func GetArtifact(client ArtifactsClient) (tool mcp.Tool, handler server.ToolHand var buffer bytes.Buffer resp, err := client.DownloadArtifactByURL(ctx, artifactURL, &buffer) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("response failed with error %s", err.Error())), nil + return handleAPIError(err), nil } // Create a response with the artifact data encoded safely for JSON diff --git a/pkg/buildkite/builds.go b/pkg/buildkite/builds.go index a401fbe..df8e85d 100644 --- a/pkg/buildkite/builds.go +++ b/pkg/buildkite/builds.go @@ -3,7 +3,6 @@ package buildkite import ( "context" "encoding/json" - "errors" "fmt" "strings" "time" @@ -259,14 +258,7 @@ func ListBuilds(client BuildsClient) (tool mcp.Tool, handler mcp.TypedToolHandle builds, resp, err := client.ListByPipeline(ctx, args.OrgSlug, args.PipelineSlug, options) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } headers := map[string]string{ @@ -340,14 +332,7 @@ func GetBuildTestEngineRuns(client BuildsClient) (tool mcp.Tool, handler mcp.Typ IncludeTestEngine: true, }) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } // Extract just the test engine runs data @@ -423,14 +408,7 @@ func GetBuild(client BuildsClient) (tool mcp.Tool, handler mcp.TypedToolHandlerF build, _, err := client.Get(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, options) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } // Parse job states filter @@ -602,14 +580,7 @@ func CreateBuild(client BuildsClient) (tool mcp.Tool, handler mcp.TypedToolHandl build, _, err := client.Create(ctx, args.OrgSlug, args.PipelineSlug, createBuild) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &build) @@ -668,14 +639,7 @@ func WaitForBuild(client BuildsClient) (tool mcp.Tool, handler mcp.TypedToolHand build, _, err := client.Get(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, &buildkite.BuildGetOptions{}) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } // wait for the build to enter a terminal state @@ -702,14 +666,7 @@ func WaitForBuild(client BuildsClient) (tool mcp.Tool, handler mcp.TypedToolHand case <-ticker.C: build, _, err = client.Get(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, nil) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } log.Ctx(ctx).Info().Str("build_id", build.ID).Str("state", build.State).Int("job_count", len(build.Jobs)).Msg("Build status checked") diff --git a/pkg/buildkite/cluster_queue.go b/pkg/buildkite/cluster_queue.go index cdb668f..c00c47b 100644 --- a/pkg/buildkite/cluster_queue.go +++ b/pkg/buildkite/cluster_queue.go @@ -35,17 +35,17 @@ func ListClusterQueues(client ClusterQueuesClient) (tool mcp.Tool, handler serve orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } clusterID, err := request.RequireString("cluster_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } paginationParams, err := optionalPaginationParams(request) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( attribute.String("org_slug", orgSlug), @@ -58,7 +58,7 @@ func ListClusterQueues(client ClusterQueuesClient) (tool mcp.Tool, handler serve ListOptions: paginationParams, }) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } if len(queues) == 0 { @@ -102,17 +102,17 @@ func GetClusterQueue(client ClusterQueuesClient) (tool mcp.Tool, handler server. orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } clusterID, err := request.RequireString("cluster_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } queueID, err := request.RequireString("queue_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( attribute.String("org_slug", orgSlug), @@ -122,7 +122,7 @@ func GetClusterQueue(client ClusterQueuesClient) (tool mcp.Tool, handler server. queue, _, err := client.Get(ctx, orgSlug, clusterID, queueID) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &queue) diff --git a/pkg/buildkite/clusters.go b/pkg/buildkite/clusters.go index 78a7f4a..c4a0654 100644 --- a/pkg/buildkite/clusters.go +++ b/pkg/buildkite/clusters.go @@ -32,12 +32,12 @@ func ListClusters(client ClustersClient) (tool mcp.Tool, handler server.ToolHand orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } paginationParams, err := optionalPaginationParams(request) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( attribute.String("org_slug", orgSlug), @@ -49,7 +49,7 @@ func ListClusters(client ClustersClient) (tool mcp.Tool, handler server.ToolHand ListOptions: paginationParams, }) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } if len(clusters) == 0 { @@ -90,12 +90,12 @@ func GetCluster(client ClustersClient) (tool mcp.Tool, handler server.ToolHandle orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } clusterID, err := request.RequireString("cluster_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( attribute.String("org_slug", orgSlug), @@ -104,7 +104,7 @@ func GetCluster(client ClustersClient) (tool mcp.Tool, handler server.ToolHandle cluster, _, err := client.Get(ctx, orgSlug, clusterID) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &cluster) diff --git a/pkg/buildkite/errors.go b/pkg/buildkite/errors.go new file mode 100644 index 0000000..9a0e52d --- /dev/null +++ b/pkg/buildkite/errors.go @@ -0,0 +1,67 @@ +package buildkite + +import ( + "errors" + "fmt" + "net/http" + + "github.com/buildkite/go-buildkite/v4" + "github.com/mark3labs/mcp-go/mcp" +) + +// handleAPIError converts a Buildkite API error into an MCP tool result error +// with user-friendly messages for common error cases like authentication failures. +func handleAPIError(err error) *mcp.CallToolResult { + if err == nil { + return nil + } + + var errResp *buildkite.ErrorResponse + if errors.As(err, &errResp) { + // Check for authentication/authorization errors + if errResp.Response != nil { + statusCode := errResp.Response.StatusCode + + switch statusCode { + case http.StatusUnauthorized: + return mcp.NewToolResultError( + "Authentication failed: Your API token is invalid or has expired. " + + "Please check your BUILDKITE_API_TOKEN and ensure it's still valid.", + ) + case http.StatusForbidden: + // Try to get detailed error from RawBody or Message + detailedMsg := getDetailedErrorMessage(errResp) + return mcp.NewToolResultError( + fmt.Sprintf( + "Permission denied: Your API token doesn't have the required permissions for this operation. %s", + detailedMsg, + ), + ) + } + } + + // For other errors, return the raw body if available (usually has detailed error info) + if errResp.RawBody != nil { + return mcp.NewToolResultError(string(errResp.RawBody)) + } + + // Fall back to the message field + if errResp.Message != "" { + return mcp.NewToolResultError(errResp.Message) + } + } + + // Default: return the error string + return mcp.NewToolResultError(err.Error()) +} + +// getDetailedErrorMessage extracts a detailed error message from ErrorResponse +func getDetailedErrorMessage(errResp *buildkite.ErrorResponse) string { + if errResp.RawBody != nil { + return string(errResp.RawBody) + } + if errResp.Message != "" { + return errResp.Message + } + return "" +} diff --git a/pkg/buildkite/errors_test.go b/pkg/buildkite/errors_test.go new file mode 100644 index 0000000..e3cf400 --- /dev/null +++ b/pkg/buildkite/errors_test.go @@ -0,0 +1,139 @@ +package buildkite + +import ( + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/buildkite/go-buildkite/v4" + "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/require" +) + +func TestHandleAPIError_Nil(t *testing.T) { + result := handleAPIError(nil) + require.Nil(t, result) +} + +func TestHandleAPIError_Unauthorized(t *testing.T) { + assert := require.New(t) + + resp := &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: io.NopCloser(strings.NewReader("Unauthorized")), + } + err := &buildkite.ErrorResponse{ + Response: resp, + Message: "Unauthorized", + } + + result := handleAPIError(err) + assert.NotNil(result) + assert.True(result.IsError) + assert.Contains(result.Content[0].(mcp.TextContent).Text, "Authentication failed") + assert.Contains(result.Content[0].(mcp.TextContent).Text, "API token is invalid or has expired") + assert.Contains(result.Content[0].(mcp.TextContent).Text, "BUILDKITE_API_TOKEN") +} + +func TestHandleAPIError_Forbidden(t *testing.T) { + assert := require.New(t) + + resp := &http.Response{ + StatusCode: http.StatusForbidden, + Body: io.NopCloser(strings.NewReader("Forbidden")), + } + err := &buildkite.ErrorResponse{ + Response: resp, + Message: "Insufficient permissions", + RawBody: []byte(`{"message":"Missing required scope: write_builds"}`), + } + + result := handleAPIError(err) + assert.NotNil(result) + assert.True(result.IsError) + assert.Contains(result.Content[0].(mcp.TextContent).Text, "Permission denied") + assert.Contains(result.Content[0].(mcp.TextContent).Text, "required permissions") + assert.Contains(result.Content[0].(mcp.TextContent).Text, "write_builds") +} + +func TestHandleAPIError_WithRawBody(t *testing.T) { + assert := require.New(t) + + resp := &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader("Not Found")), + } + err := &buildkite.ErrorResponse{ + Response: resp, + Message: "Not Found", + RawBody: []byte(`{"message":"Pipeline not found"}`), + } + + result := handleAPIError(err) + assert.NotNil(result) + assert.True(result.IsError) + assert.Contains(result.Content[0].(mcp.TextContent).Text, "Pipeline not found") +} + +func TestHandleAPIError_WithMessage(t *testing.T) { + assert := require.New(t) + + resp := &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(strings.NewReader("Internal Server Error")), + } + err := &buildkite.ErrorResponse{ + Response: resp, + Message: "Internal Server Error", + } + + result := handleAPIError(err) + assert.NotNil(result) + assert.True(result.IsError) + assert.Contains(result.Content[0].(mcp.TextContent).Text, "Internal Server Error") +} + +func TestHandleAPIError_NonBuildkiteError(t *testing.T) { + assert := require.New(t) + + err := errors.New("generic error message") + + result := handleAPIError(err) + assert.NotNil(result) + assert.True(result.IsError) + assert.Equal("generic error message", result.Content[0].(mcp.TextContent).Text) +} + +func TestGetDetailedErrorMessage_RawBody(t *testing.T) { + assert := require.New(t) + + errResp := &buildkite.ErrorResponse{ + RawBody: []byte("detailed error from raw body"), + Message: "should not use this", + } + + msg := getDetailedErrorMessage(errResp) + assert.Equal("detailed error from raw body", msg) +} + +func TestGetDetailedErrorMessage_MessageOnly(t *testing.T) { + assert := require.New(t) + + errResp := &buildkite.ErrorResponse{ + Message: "error message", + } + + msg := getDetailedErrorMessage(errResp) + assert.Equal("error message", msg) +} + +func TestGetDetailedErrorMessage_Empty(t *testing.T) { + assert := require.New(t) + + errResp := &buildkite.ErrorResponse{} + + msg := getDetailedErrorMessage(errResp) + assert.Equal("", msg) +} diff --git a/pkg/buildkite/joblogs.go b/pkg/buildkite/joblogs.go index bb9dafe..50e19ef 100644 --- a/pkg/buildkite/joblogs.go +++ b/pkg/buildkite/joblogs.go @@ -202,7 +202,7 @@ func SearchLogs(client BuildkiteLogsClient) (tool mcp.Tool, handler mcp.TypedToo ) if err := validateSearchPattern(params.Pattern); err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } reader, err := newParquetReader(ctx, client, params.JobLogsBaseParams) diff --git a/pkg/buildkite/jobs.go b/pkg/buildkite/jobs.go index b0586f7..a3bef44 100644 --- a/pkg/buildkite/jobs.go +++ b/pkg/buildkite/jobs.go @@ -2,7 +2,6 @@ package buildkite import ( "context" - "errors" "github.com/buildkite/buildkite-mcp-server/pkg/trace" "github.com/buildkite/go-buildkite/v4" @@ -88,14 +87,7 @@ func UnblockJob(client JobsClient) (tool mcp.Tool, handler mcp.TypedToolHandlerF // Unblock the job job, _, err := client.UnblockJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, &unblockOptions) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &job) diff --git a/pkg/buildkite/organizations.go b/pkg/buildkite/organizations.go index c79ea50..ea6e0f9 100644 --- a/pkg/buildkite/organizations.go +++ b/pkg/buildkite/organizations.go @@ -26,7 +26,7 @@ func UserTokenOrganization(client OrganizationsClient) (tool mcp.Tool, handler s orgs, _, err := client.List(ctx, &buildkite.OrganizationListOptions{}) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } if len(orgs) == 0 { diff --git a/pkg/buildkite/pipelines.go b/pkg/buildkite/pipelines.go index 4efe1b9..cfc84d7 100644 --- a/pkg/buildkite/pipelines.go +++ b/pkg/buildkite/pipelines.go @@ -2,7 +2,6 @@ package buildkite import ( "context" - "errors" "fmt" "net/url" @@ -97,14 +96,7 @@ func ListPipelines(client PipelinesClient) (tool mcp.Tool, handler mcp.TypedTool Repository: args.Repository, }) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } headers := map[string]string{"Link": resp.Header.Get("Link")} @@ -174,14 +166,7 @@ func GetPipeline(client PipelinesClient) (tool mcp.Tool, handler mcp.TypedToolHa pipeline, _, err := client.Get(ctx, args.OrgSlug, args.PipelineSlug) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } var result any @@ -392,14 +377,7 @@ func CreatePipeline(client PipelinesClient) (tool mcp.Tool, handler mcp.TypedToo pipeline, _, err := client.Create(ctx, args.OrgSlug, create) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } if args.CreateWebhook { @@ -524,14 +502,7 @@ func UpdatePipeline(client PipelinesClient) (mcp.Tool, mcp.TypedToolHandlerFunc[ pipeline, _, err := client.Update(ctx, args.OrgSlug, args.PipelineSlug, update) if err != nil { - var errResp *buildkite.ErrorResponse - if errors.As(err, &errResp) { - if errResp.RawBody != nil { - return mcp.NewToolResultError(string(errResp.RawBody)), nil - } - } - - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &pipeline) diff --git a/pkg/buildkite/test_executions.go b/pkg/buildkite/test_executions.go index cbb6606..c840893 100644 --- a/pkg/buildkite/test_executions.go +++ b/pkg/buildkite/test_executions.go @@ -41,17 +41,17 @@ func GetFailedTestExecutions(client TestExecutionsClient) (tool mcp.Tool, handle orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } testSuiteSlug, err := request.RequireString("test_suite_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } runID, err := request.RequireString("run_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } includeFailureExpanded := request.GetBool("include_failure_expanded", false) @@ -74,7 +74,7 @@ func GetFailedTestExecutions(client TestExecutionsClient) (tool mcp.Tool, handle failedExecutions, _, err := client.GetFailedExecutions(ctx, orgSlug, testSuiteSlug, runID, options) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } // Always apply client-side pagination diff --git a/pkg/buildkite/test_runs.go b/pkg/buildkite/test_runs.go index a5a8190..e83eb1e 100644 --- a/pkg/buildkite/test_runs.go +++ b/pkg/buildkite/test_runs.go @@ -42,17 +42,17 @@ func ListTestRuns(client TestRunsClient) (tool mcp.Tool, handler server.ToolHand orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } testSuiteSlug, err := request.RequireString("test_suite_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } paginationParams, err := optionalPaginationParams(request) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( @@ -68,7 +68,7 @@ func ListTestRuns(client TestRunsClient) (tool mcp.Tool, handler server.ToolHand testRuns, resp, err := client.List(ctx, orgSlug, testSuiteSlug, options) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } result := PaginatedResult[buildkite.TestRun]{ @@ -115,17 +115,17 @@ func GetTestRun(client TestRunsClient) (tool mcp.Tool, handler server.ToolHandle orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } testSuiteSlug, err := request.RequireString("test_suite_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } runID, err := request.RequireString("run_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( @@ -136,7 +136,7 @@ func GetTestRun(client TestRunsClient) (tool mcp.Tool, handler server.ToolHandle testRun, resp, err := client.Get(ctx, orgSlug, testSuiteSlug, runID) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } if resp.StatusCode != http.StatusOK { diff --git a/pkg/buildkite/tests.go b/pkg/buildkite/tests.go index c4943b8..cd9581d 100644 --- a/pkg/buildkite/tests.go +++ b/pkg/buildkite/tests.go @@ -37,17 +37,17 @@ func GetTest(client TestsClient) (tool mcp.Tool, handler server.ToolHandlerFunc, orgSlug, err := request.RequireString("org_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } testSuiteSlug, err := request.RequireString("test_suite_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } testID, err := request.RequireString("test_id") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } span.SetAttributes( @@ -58,7 +58,7 @@ func GetTest(client TestsClient) (tool mcp.Tool, handler server.ToolHandlerFunc, test, _, err := client.Get(ctx, orgSlug, testSuiteSlug, testID) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &test) diff --git a/pkg/buildkite/user.go b/pkg/buildkite/user.go index c72aea2..b83a587 100644 --- a/pkg/buildkite/user.go +++ b/pkg/buildkite/user.go @@ -27,7 +27,7 @@ func CurrentUser(client UserClient) (tool mcp.Tool, handler server.ToolHandlerFu user, _, err := client.CurrentUser(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return handleAPIError(err), nil } return mcpTextResult(span, &user)