diff --git a/pkg/errors/error.go b/pkg/errors/error.go index 9d81e9010..29d1967e6 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -2,12 +2,15 @@ package errors import ( "context" + "errors" "fmt" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" ) +var errContextMissingGitHubCtxErrors = errors.New("context does not contain GitHubCtxErrors") + type GitHubAPIError struct { Message string `json:"message"` Response *github.Response `json:"-"` @@ -71,7 +74,7 @@ func GetGitHubAPIErrors(ctx context.Context) ([]*GitHubAPIError, error) { if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok { return val.api, nil // return the slice of API errors from the context } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } // GetGitHubGraphQLErrors retrieves the slice of GitHubGraphQLErrors from the context. @@ -79,7 +82,7 @@ func GetGitHubGraphQLErrors(ctx context.Context) ([]*GitHubGraphQLError, error) if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok { return val.graphQL, nil // return the slice of GraphQL errors from the context } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } func NewGitHubAPIErrorToCtx(ctx context.Context, message string, resp *github.Response, err error) (context.Context, error) { @@ -95,7 +98,7 @@ func addGitHubAPIErrorToContext(ctx context.Context, err *GitHubAPIError) (conte val.api = append(val.api, err) // append the error to the existing slice in the context return ctx, nil } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError) (context.Context, error) { @@ -103,7 +106,7 @@ func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError val.graphQL = append(val.graphQL, err) // append the error to the existing slice in the context return ctx, nil } - return nil, fmt.Errorf("context does not contain GitHubCtxErrors") + return nil, errContextMissingGitHubCtxErrors } // NewGitHubAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index a7ec8e20f..11737a497 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -13,6 +13,13 @@ import ( "github.com/shurcooL/githubv4" ) +const ( + descRepositoryOwner = "Repository owner" + descRepositoryName = "Repository name" + errFailedToGetGQLClient = "failed to get GitHub GQL client: %v" + fmtCategoryLabel = "category:%s" +) + func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_discussions", mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")), @@ -22,11 +29,11 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp }), mcp.WithString("owner", mcp.Required(), - mcp.Description("Repository owner"), + mcp.Description(descRepositoryOwner), ), mcp.WithString("repo", mcp.Required(), - mcp.Description("Repository name"), + mcp.Description(descRepositoryName), ), mcp.WithString("category", mcp.Description("Optional filter by discussion category ID. If provided, only discussions with this category are listed."), @@ -51,7 +58,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } // If category filter is specified, use it as the category ID for server-side filtering @@ -98,7 +105,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, Labels: []*github.Label{ { - Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))), + Name: github.Ptr(fmt.Sprintf(fmtCategoryLabel, string(n.Category.Name))), }, }, } @@ -138,7 +145,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, Labels: []*github.Label{ { - Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))), + Name: github.Ptr(fmt.Sprintf(fmtCategoryLabel, string(n.Category.Name))), }, }, } @@ -164,11 +171,11 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper }), mcp.WithString("owner", mcp.Required(), - mcp.Description("Repository owner"), + mcp.Description(descRepositoryOwner), ), mcp.WithString("repo", mcp.Required(), - mcp.Description("Repository name"), + mcp.Description(descRepositoryName), ), mcp.WithNumber("discussionNumber", mcp.Required(), @@ -187,7 +194,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper } client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } var q struct { @@ -221,7 +228,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time}, Labels: []*github.Label{ { - Name: github.Ptr(fmt.Sprintf("category:%s", string(d.Category.Name))), + Name: github.Ptr(fmt.Sprintf(fmtCategoryLabel, string(d.Category.Name))), }, }, } @@ -241,8 +248,8 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati Title: t("TOOL_GET_DISCUSSION_COMMENTS_USER_TITLE", "Get discussion comments"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name")), + mcp.WithString("owner", mcp.Required(), mcp.Description(descRepositoryOwner)), + mcp.WithString("repo", mcp.Required(), mcp.Description(descRepositoryName)), mcp.WithNumber("discussionNumber", mcp.Required(), mcp.Description("Discussion Number")), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -258,7 +265,7 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } var q struct { @@ -303,11 +310,11 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl }), mcp.WithString("owner", mcp.Required(), - mcp.Description("Repository owner"), + mcp.Description(descRepositoryOwner), ), mcp.WithString("repo", mcp.Required(), - mcp.Description("Repository name"), + mcp.Description(descRepositoryName), ), mcp.WithNumber("first", mcp.Description("Number of categories to return per page (min 1, max 100)"), @@ -356,7 +363,7 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } var q struct { Repository struct { diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index bad822b13..15d4766fe 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -17,6 +17,12 @@ import ( "github.com/github/github-mcp-server/pkg/translations" ) +const ( + errFailedToGetPullRequest = "failed to get pull request" + errFailedToGetCurrentUser = "failed to get current user" + errFailedToGetLatestReviewForCurrentUser = "failed to get latest review for current user" +) + // GetPullRequest creates a tool to get details of a specific pull request. func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request", @@ -59,7 +65,7 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, resp, err, ), nil @@ -695,7 +701,7 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, resp, err, ), nil @@ -1025,7 +1031,7 @@ func CreateAndSubmitPullRequestReview(getGQLClient GetGQLClientFn, t translation "prNum": githubv4.Int(params.PullNumber), }); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, err, ), nil } @@ -1119,7 +1125,7 @@ func CreatePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. "prNum": githubv4.Int(params.PullNumber), }); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, err, ), nil } @@ -1240,7 +1246,7 @@ func AddPullRequestReviewCommentToPendingReview(getGQLClient GetGQLClientFn, t t if err := client.Query(ctx, &getViewerQuery, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get current user", + errFailedToGetCurrentUser, err, ), nil } @@ -1268,7 +1274,7 @@ func AddPullRequestReviewCommentToPendingReview(getGQLClient GetGQLClientFn, t t if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get latest review for current user", + errFailedToGetLatestReviewForCurrentUser, err, ), nil } @@ -1377,7 +1383,7 @@ func SubmitPendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. if err := client.Query(ctx, &getViewerQuery, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get current user", + errFailedToGetCurrentUser, err, ), nil } @@ -1405,7 +1411,7 @@ func SubmitPendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get latest review for current user", + errFailedToGetLatestReviewForCurrentUser, err, ), nil } @@ -1501,7 +1507,7 @@ func DeletePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. if err := client.Query(ctx, &getViewerQuery, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get current user", + errFailedToGetCurrentUser, err, ), nil } @@ -1529,7 +1535,7 @@ func DeletePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get latest review for current user", + errFailedToGetLatestReviewForCurrentUser, err, ), nil } diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 29f776a05..a120535f9 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -19,6 +19,12 @@ import ( "github.com/mark3labs/mcp-go/server" ) +const ( + errFailedToGetGitHubClient = "failed to get GitHub client: %w" + errFailedToCreateResourceURI = "failed to create resource URI: %w" + repoURIScheme = "repo://" +) + func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_commit", mcp.WithDescription(t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository")), @@ -65,7 +71,7 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts) if err != nil { @@ -155,7 +161,7 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts) if err != nil { @@ -225,7 +231,7 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) ( client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts) @@ -339,7 +345,7 @@ func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperF // Create or update the file client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts) if err != nil { @@ -417,7 +423,7 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } createdRepo, resp, err := client.Repositories.Create(ctx, "", repo) if err != nil { @@ -503,7 +509,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t // fetch the PR from the API to get the latest commit and use SHA githubClient, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } prNum, err := strconv.Atoi(prNumber) if err != nil { @@ -547,19 +553,19 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t var resourceURI string switch { case sha != "": - resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path) + resourceURI, err = url.JoinPath(repoURIScheme, owner, repo, "sha", sha, "contents", path) if err != nil { - return nil, fmt.Errorf("failed to create resource URI: %w", err) + return nil, fmt.Errorf(errFailedToCreateResourceURI, err) } case ref != "": - resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path) + resourceURI, err = url.JoinPath(repoURIScheme, owner, repo, ref, "contents", path) if err != nil { - return nil, fmt.Errorf("failed to create resource URI: %w", err) + return nil, fmt.Errorf(errFailedToCreateResourceURI, err) } default: - resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path) + resourceURI, err = url.JoinPath(repoURIScheme, owner, repo, "contents", path) if err != nil { - return nil, fmt.Errorf("failed to create resource URI: %w", err) + return nil, fmt.Errorf(errFailedToCreateResourceURI, err) } } @@ -655,7 +661,7 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts) if err != nil { @@ -748,7 +754,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } // Get the reference for the branch @@ -909,7 +915,7 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) ( client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } // Get the source branch SHA @@ -1037,7 +1043,7 @@ func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (too client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } // Get the reference for the branch @@ -1177,7 +1183,7 @@ func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } tags, resp, err := client.Repositories.ListTags(ctx, owner, repo, opts) @@ -1244,7 +1250,7 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(errFailedToGetGitHubClient, err) } // First get the tag reference