package github import ( "context" "encoding/json" "fmt" "io" "net/http" "github.com/google/go-github/v79/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/sanitize" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" ) // GetPullRequest creates a tool to get details of a specific pull request. func PullRequestRead(getClient GetClientFn, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { tool := mcp.Tool{ Name: "pull_request_read", Description: t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository."), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get details for a single pull request"), ReadOnlyHint: true, }, InputSchema: WithPagination(&jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ "method": { Type: "string", Description: `Action to specify what pull request data needs to be retrieved from GitHub. Possible options: 1. get - Get details of a specific pull request. 2. get_diff - Get the diff of a pull request. 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks. 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned. 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned. 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. `, Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews"}, }, "owner": { Type: "string", Description: "Repository owner", }, "repo": { Type: "string", Description: "Repository name", }, "pullNumber": { Type: "number", Description: "Pull request number", }, }, Required: []string{"method", "owner", "repo", "pullNumber"}, }), } handler := mcp.ToolHandlerFor[map[string]any, any]( func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { method, err := RequiredParam[string](args, "method") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } pagination, err := OptionalPaginationParams(args) if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } var result *mcp.CallToolResult var handlerErr error switch method { case "get": result, handlerErr = GetPullRequest(ctx, client, owner, repo, pullNumber) case "get_diff": result, handlerErr = GetPullRequestDiff(ctx, client, owner, repo, pullNumber) case "get_status": result, handlerErr = GetPullRequestStatus(ctx, client, owner, repo, pullNumber) case "get_files": result, handlerErr = GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination) case "get_review_comments": result, handlerErr = GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) case "get_reviews": result, handlerErr = GetPullRequestReviews(ctx, client, owner, repo, pullNumber) // TODO: Uncomment once issues.go is migrated // case "get_comments": // result, handlerErr = GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) default: return nil, nil, fmt.Errorf("unknown method: %s", method) } return result, nil, handlerErr }, ) return tool, handler } func GetPullRequest(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request", resp, err, ), nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } // sanitize title/body on response if pr != nil { if pr.Title != nil { pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title)) } if pr.Body != nil { pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body)) } } r, err := json.Marshal(pr) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil } func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { raw, resp, err := client.PullRequests.GetRaw( ctx, owner, repo, pullNumber, github.RawOptions{Type: github.Diff}, ) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request diff", resp, err, ), nil } if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil } defer func() { _ = resp.Body.Close() }() // Return the raw response return utils.NewToolResultText(string(raw)), nil } func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request", resp, err, ), nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } // Get combined status for the head SHA status, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, *pr.Head.SHA, nil) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get combined status", resp, err, ), nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil } r, err := json.Marshal(status) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil } func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { opts := &github.ListOptions{ PerPage: pagination.PerPage, Page: pagination.Page, } files, resp, err := client.PullRequests.ListFiles(ctx, owner, repo, pullNumber, opts) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request files", resp, err, ), nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil } r, err := json.Marshal(files) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil } func GetPullRequestReviewComments(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { opts := &github.PullRequestListCommentsOptions{ ListOptions: github.ListOptions{ PerPage: pagination.PerPage, Page: pagination.Page, }, } comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request review comments", resp, err, ), nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil } r, err := json.Marshal(comments) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil } func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { reviews, resp, err := client.PullRequests.ListReviews(ctx, owner, repo, pullNumber, nil) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request reviews", resp, err, ), nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil } r, err := json.Marshal(reviews) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil } // CreatePullRequest creates a tool to create a new pull request. func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { tool := mcp.Tool{ Name: "create_pull_request", Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), ReadOnlyHint: false, }, InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ "owner": { Type: "string", Description: "Repository owner", }, "repo": { Type: "string", Description: "Repository name", }, "title": { Type: "string", Description: "PR title", }, "body": { Type: "string", Description: "PR description", }, "head": { Type: "string", Description: "Branch containing changes", }, "base": { Type: "string", Description: "Branch to merge into", }, "draft": { Type: "boolean", Description: "Create as draft PR", }, "maintainer_can_modify": { Type: "boolean", Description: "Allow maintainer edits", }, }, Required: []string{"owner", "repo", "title", "head", "base"}, }, } handler := mcp.ToolHandlerFor[map[string]any, any]( func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } title, err := RequiredParam[string](args, "title") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } head, err := RequiredParam[string](args, "head") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } base, err := RequiredParam[string](args, "base") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } body, err := OptionalParam[string](args, "body") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } draft, err := OptionalParam[bool](args, "draft") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } maintainerCanModify, err := OptionalParam[bool](args, "maintainer_can_modify") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } newPR := &github.NewPullRequest{ Title: github.Ptr(title), Head: github.Ptr(head), Base: github.Ptr(base), } if body != "" { newPR.Body = github.Ptr(body) } newPR.Draft = github.Ptr(draft) newPR.MaintainerCanModify = github.Ptr(maintainerCanModify) client, err := getClient(ctx) if err != nil { return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to create pull request", resp, err, ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { return nil, nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil, nil } // Return minimal response with just essential information minimalResponse := MinimalResponse{ ID: fmt.Sprintf("%d", pr.GetID()), URL: pr.GetHTMLURL(), } r, err := json.Marshal(minimalResponse) if err != nil { return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil, nil }, ) return tool, handler } // UpdatePullRequest creates a tool to update an existing pull request. // TODO: MIGRATE THIS TOOL /* func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("update_pull_request", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_UPDATE_PULL_REQUEST_USER_TITLE", "Edit pull request"), ReadOnlyHint: ToBoolPtr(false), }), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("pullNumber", mcp.Required(), mcp.Description("Pull request number to update"), ), mcp.WithString("title", mcp.Description("New title"), ), mcp.WithString("body", mcp.Description("New description"), ), mcp.WithString("state", mcp.Description("New state"), mcp.Enum("open", "closed"), ), mcp.WithBoolean("draft", mcp.Description("Mark pull request as draft (true) or ready for review (false)"), ), mcp.WithString("base", mcp.Description("New base branch name"), ), mcp.WithBoolean("maintainer_can_modify", mcp.Description("Allow maintainer edits"), ), mcp.WithArray("reviewers", mcp.Description("GitHub usernames to request reviews from"), mcp.Items(map[string]interface{}{ "type": "string", }), ), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } // Check if draft parameter is provided draftProvided := args["draft"] != nil var draftValue bool if draftProvided { draftValue, err = OptionalParam[bool](args, "draft") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } } // Build the update struct only with provided fields update := &github.PullRequest{} restUpdateNeeded := false if title, ok, err := OptionalParamOK[string](args, "title"); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.Title = github.Ptr(title) restUpdateNeeded = true } if body, ok, err := OptionalParamOK[string](args, "body"); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.Body = github.Ptr(body) restUpdateNeeded = true } if state, ok, err := OptionalParamOK[string](args, "state"); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.State = github.Ptr(state) restUpdateNeeded = true } if base, ok, err := OptionalParamOK[string](args, "base"); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)} restUpdateNeeded = true } if maintainerCanModify, ok, err := OptionalParamOK[bool](args, "maintainer_can_modify"); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.MaintainerCanModify = github.Ptr(maintainerCanModify) restUpdateNeeded = true } // Handle reviewers separately reviewers, err := OptionalStringArrayParam(args, "reviewers") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } // If no updates, no draft change, and no reviewers, return error early if !restUpdateNeeded && !draftProvided && len(reviewers) == 0 { return utils.NewToolResultError("No update parameters provided."), nil, nil } // Handle REST API updates (title, body, state, base, maintainer_can_modify) if restUpdateNeeded { client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } _, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update pull request", resp, err, ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil, nil } } // Handle draft status changes using GraphQL if draftProvided { gqlClient, err := getGQLClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err) } var prQuery struct { Repository struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` } err = gqlClient.Query(ctx, &prQuery, map[string]interface{}{ "owner": githubv4.String(owner), "repo": githubv4.String(repo), "prNum": githubv4.Int(pullNumber), // #nosec G115 - pull request numbers are always small positive integers }) if err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find pull request", err), nil, nil } currentIsDraft := bool(prQuery.Repository.PullRequest.IsDraft) if currentIsDraft != draftValue { if draftValue { // Convert to draft var mutation struct { ConvertPullRequestToDraft struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } } `graphql:"convertPullRequestToDraft(input: $input)"` } err = gqlClient.Mutate(ctx, &mutation, githubv4.ConvertPullRequestToDraftInput{ PullRequestID: prQuery.Repository.PullRequest.ID, }, nil) if err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to convert pull request to draft", err), nil, nil } } else { // Mark as ready for review var mutation struct { MarkPullRequestReadyForReview struct { PullRequest struct { ID githubv4.ID IsDraft githubv4.Boolean } } `graphql:"markPullRequestReadyForReview(input: $input)"` } err = gqlClient.Mutate(ctx, &mutation, githubv4.MarkPullRequestReadyForReviewInput{ PullRequestID: prQuery.Repository.PullRequest.ID, }, nil) if err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to mark pull request ready for review", err), nil, nil } } } } // Handle reviewer requests if len(reviewers) > 0 { client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } reviewersRequest := github.ReviewersRequest{ Reviewers: reviewers, } _, resp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, pullNumber, reviewersRequest) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to request reviewers", resp, err, ), nil, nil } defer func() { if resp != nil && resp.Body != nil { _ = resp.Body.Close() } }() if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil, nil } } // Get the final state of the PR to return client, err := getClient(ctx) if err != nil { return nil, err } finalPR, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "Failed to get pull request", resp, err), nil, nil } defer func() { if resp != nil && resp.Body != nil { _ = resp.Body.Close() } }() // Return minimal response with just essential information minimalResponse := MinimalResponse{ ID: fmt.Sprintf("%d", finalPR.GetID()), URL: finalPR.GetHTMLURL(), } r, err := json.Marshal(minimalResponse) if err != nil { return utils.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil, nil } return utils.NewToolResultText(string(r)), nil, nil } } */ // ListPullRequests creates a tool to list and filter repository pull requests. // TODO: MIGRATE THIS TOOL /* func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("list_pull_requests", mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"), ReadOnlyHint: ToBoolPtr(true), }), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithString("state", mcp.Description("Filter by state"), mcp.Enum("open", "closed", "all"), ), mcp.WithString("head", mcp.Description("Filter by head user/org and branch"), ), mcp.WithString("base", mcp.Description("Filter by base branch"), ), mcp.WithString("sort", mcp.Description("Sort by"), mcp.Enum("created", "updated", "popularity", "long-running"), ), mcp.WithString("direction", mcp.Description("Sort direction"), mcp.Enum("asc", "desc"), ), WithPagination(), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } state, err := OptionalParam[string](args, "state") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } head, err := OptionalParam[string](args, "head") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } base, err := OptionalParam[string](args, "base") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } sort, err := OptionalParam[string](args, "sort") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } direction, err := OptionalParam[string](args, "direction") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } pagination, err := OptionalPaginationParams(args) if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } opts := &github.PullRequestListOptions{ State: state, Head: head, Base: base, Sort: sort, Direction: direction, ListOptions: github.ListOptions{ PerPage: pagination.PerPage, Page: pagination.Page, }, } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list pull requests", resp, err, ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil, nil } // sanitize title/body on each PR for _, pr := range prs { if pr == nil { continue } if pr.Title != nil { pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title)) } if pr.Body != nil { pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body)) } } r, err := json.Marshal(prs) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil, nil } } */ // MergePullRequest creates a tool to merge a pull request. // TODO: MIGRATE THIS TOOL /* func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("merge_pull_request", mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"), ReadOnlyHint: ToBoolPtr(false), }), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("pullNumber", mcp.Required(), mcp.Description("Pull request number"), ), mcp.WithString("commit_title", mcp.Description("Title for merge commit"), ), mcp.WithString("commit_message", mcp.Description("Extra detail for merge commit"), ), mcp.WithString("merge_method", mcp.Description("Merge method"), mcp.Enum("merge", "squash", "rebase"), ), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } commitTitle, err := OptionalParam[string](args, "commit_title") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } commitMessage, err := OptionalParam[string](args, "commit_message") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } mergeMethod, err := OptionalParam[string](args, "merge_method") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } options := &github.PullRequestOptions{ CommitTitle: commitTitle, MergeMethod: mergeMethod, } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } result, resp, err := client.PullRequests.Merge(ctx, owner, repo, pullNumber, commitMessage, options) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to merge pull request", resp, err, ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to merge pull request: %s", string(body))), nil, nil } r, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil, nil } } */ // SearchPullRequests creates a tool to search for pull requests. // TODO: MIGRATE THIS TOOL /* func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_pull_requests", mcp.WithDescription(t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"), ReadOnlyHint: ToBoolPtr(true), }), mcp.WithString("query", mcp.Required(), mcp.Description("Search query using GitHub pull request search syntax"), ), mcp.WithString("owner", mcp.Description("Optional repository owner. If provided with repo, only pull requests for this repository are listed."), ), mcp.WithString("repo", mcp.Description("Optional repository name. If provided with owner, only pull requests for this repository are listed."), ), mcp.WithString("sort", mcp.Description("Sort field by number of matches of categories, defaults to best match"), mcp.Enum( "comments", "reactions", "reactions-+1", "reactions--1", "reactions-smile", "reactions-thinking_face", "reactions-heart", "reactions-tada", "interactions", "created", "updated", ), ), mcp.WithString("order", mcp.Description("Sort order"), mcp.Enum("asc", "desc"), ), WithPagination(), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests") } } */ // UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. // TODO: MIGRATE THIS TOOL /* func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("update_pull_request_branch", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_USER_TITLE", "Update pull request branch"), ReadOnlyHint: ToBoolPtr(false), }), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("pullNumber", mcp.Required(), mcp.Description("Pull request number"), ), mcp.WithString("expectedHeadSha", mcp.Description("The expected SHA of the pull request's HEAD ref"), ), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } expectedHeadSHA, err := OptionalParam[string](args, "expectedHeadSha") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } opts := &github.PullRequestBranchUpdateOptions{} if expectedHeadSHA != "" { opts.ExpectedHeadSHA = github.Ptr(expectedHeadSHA) } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } result, resp, err := client.PullRequests.UpdateBranch(ctx, owner, repo, pullNumber, opts) if err != nil { // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, // and it's not a real error. if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) { return utils.NewToolResultText("Pull request branch update is in progress"), nil, nil } return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update pull request branch", resp, err, ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusAccepted { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(body))), nil, nil } r, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil, nil } } */ type PullRequestReviewWriteParams struct { Method string Owner string Repo string PullNumber int32 Body string Event string CommitID *string } // TODO: MIGRATE THIS TOOL /* func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("pull_request_review_write", mcp.WithDescription(t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request. Available methods: - create: Create a new review of a pull request. If "event" parameter is provided, the review is submitted. If "event" is omitted, a pending review is created. - submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The "body" and "event" parameters are used when submitting the review. - delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. `)), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_PULL_REQUEST_REVIEW_WRITE_USER_TITLE", "Write operations (create, submit, delete) on pull request reviews."), ReadOnlyHint: ToBoolPtr(false), }), // Either we need the PR GQL Id directly, or we need owner, repo and PR number to look it up. // Since our other Pull Request tools are working with the REST Client, will handle the lookup // internally for now. mcp.WithString("method", mcp.Required(), mcp.Description("The write operation to perform on pull request review."), mcp.Enum("create", "submit_pending", "delete_pending"), ), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("pullNumber", mcp.Required(), mcp.Description("Pull request number"), ), mcp.WithString("body", mcp.Description("Review comment text"), ), mcp.WithString("event", mcp.Description("Review action to perform."), mcp.Enum("APPROVE", "REQUEST_CHANGES", "COMMENT"), ), mcp.WithString("commitID", mcp.Description("SHA of commit to review"), ), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { var params PullRequestReviewWriteParams if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } // Given our owner, repo and PR number, lookup the GQL ID of the PR. client, err := getGQLClient(ctx) if err != nil { return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil, nil } switch params.Method { case "create": return CreatePullRequestReview(ctx, client, params) case "submit_pending": return SubmitPendingPullRequestReview(ctx, client, params) case "delete_pending": return DeletePendingPullRequestReview(ctx, client, params) default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil } } } */ func CreatePullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { var getPullRequestQuery struct { Repository struct { PullRequest struct { ID githubv4.ID } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $repo)"` } if err := client.Query(ctx, &getPullRequestQuery, map[string]any{ "owner": githubv4.String(params.Owner), "repo": githubv4.String(params.Repo), "prNum": githubv4.Int(params.PullNumber), }); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get pull request", err, ), nil } // Now we have the GQL ID, we can create a review var addPullRequestReviewMutation struct { AddPullRequestReview struct { PullRequestReview struct { ID githubv4.ID // We don't need this, but a selector is required or GQL complains. } } `graphql:"addPullRequestReview(input: $input)"` } addPullRequestReviewInput := githubv4.AddPullRequestReviewInput{ PullRequestID: getPullRequestQuery.Repository.PullRequest.ID, CommitOID: newGQLStringlikePtr[githubv4.GitObjectID](params.CommitID), } // Event and Body are provided if we submit a review if params.Event != "" { addPullRequestReviewInput.Event = newGQLStringlike[githubv4.PullRequestReviewEvent](params.Event) addPullRequestReviewInput.Body = githubv4.NewString(githubv4.String(params.Body)) } if err := client.Mutate( ctx, &addPullRequestReviewMutation, addPullRequestReviewInput, nil, ); err != nil { return utils.NewToolResultError(err.Error()), nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. if params.Event == "" { return utils.NewToolResultText("pending pull request created"), nil } return utils.NewToolResultText("pull request review submitted successfully"), nil } func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { // First we'll get the current user var getViewerQuery struct { Viewer struct { Login githubv4.String } } if err := client.Query(ctx, &getViewerQuery, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get current user", err, ), nil } var getLatestReviewForViewerQuery struct { Repository struct { PullRequest struct { Reviews struct { Nodes []struct { ID githubv4.ID State githubv4.PullRequestReviewState URL githubv4.URI } } `graphql:"reviews(first: 1, author: $author)"` } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $name)"` } vars := map[string]any{ "author": githubv4.String(getViewerQuery.Viewer.Login), "owner": githubv4.String(params.Owner), "name": githubv4.String(params.Repo), "prNum": githubv4.Int(params.PullNumber), } if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get latest review for current user", err, ), nil } // Validate there is one review and the state is pending if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { return utils.NewToolResultError("No pending review found for the viewer"), nil } review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] if review.State != githubv4.PullRequestReviewStatePending { errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) return utils.NewToolResultError(errText), nil } // Prepare the mutation var submitPullRequestReviewMutation struct { SubmitPullRequestReview struct { PullRequestReview struct { ID githubv4.ID // We don't need this, but a selector is required or GQL complains. } } `graphql:"submitPullRequestReview(input: $input)"` } if err := client.Mutate( ctx, &submitPullRequestReviewMutation, githubv4.SubmitPullRequestReviewInput{ PullRequestReviewID: &review.ID, Event: githubv4.PullRequestReviewEvent(params.Event), Body: newGQLStringlikePtr[githubv4.String](¶ms.Body), }, nil, ); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to submit pull request review", err, ), nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. return utils.NewToolResultText("pending pull request review successfully submitted"), nil } func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { // First we'll get the current user var getViewerQuery struct { Viewer struct { Login githubv4.String } } if err := client.Query(ctx, &getViewerQuery, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get current user", err, ), nil } var getLatestReviewForViewerQuery struct { Repository struct { PullRequest struct { Reviews struct { Nodes []struct { ID githubv4.ID State githubv4.PullRequestReviewState URL githubv4.URI } } `graphql:"reviews(first: 1, author: $author)"` } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $name)"` } vars := map[string]any{ "author": githubv4.String(getViewerQuery.Viewer.Login), "owner": githubv4.String(params.Owner), "name": githubv4.String(params.Repo), "prNum": githubv4.Int(params.PullNumber), } if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get latest review for current user", err, ), nil } // Validate there is one review and the state is pending if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { return utils.NewToolResultError("No pending review found for the viewer"), nil } review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] if review.State != githubv4.PullRequestReviewStatePending { errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) return utils.NewToolResultError(errText), nil } // Prepare the mutation var deletePullRequestReviewMutation struct { DeletePullRequestReview struct { PullRequestReview struct { ID githubv4.ID // We don't need this, but a selector is required or GQL complains. } } `graphql:"deletePullRequestReview(input: $input)"` } if err := client.Mutate( ctx, &deletePullRequestReviewMutation, githubv4.DeletePullRequestReviewInput{ PullRequestReviewID: &review.ID, }, nil, ); err != nil { return utils.NewToolResultError(err.Error()), nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. return utils.NewToolResultText("pending pull request review successfully deleted"), nil } // AddCommentToPendingReview creates a tool to add a comment to a pull request review. // TODO: MIGRATE THIS TOOL /* func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("add_comment_to_pending_review", mcp.WithDescription(t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_USER_TITLE", "Add review comment to the requester's latest pending pull request review"), ReadOnlyHint: ToBoolPtr(false), }), // Ideally, for performance sake this would just accept the pullRequestReviewID. However, we would need to // add a new tool to get that ID for clients that aren't in the same context as the original pending review // creation. So for now, we'll just accept the owner, repo and pull number and assume this is adding a comment // the latest review from a user, since only one can be active at a time. It can later be extended with // a pullRequestReviewID parameter if targeting other reviews is desired: // mcp.WithString("pullRequestReviewID", // mcp.Required(), // mcp.Description("The ID of the pull request review to add a comment to"), // ), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("pullNumber", mcp.Required(), mcp.Description("Pull request number"), ), mcp.WithString("path", mcp.Required(), mcp.Description("The relative path to the file that necessitates a comment"), ), mcp.WithString("body", mcp.Required(), mcp.Description("The text of the review comment"), ), mcp.WithString("subjectType", mcp.Required(), mcp.Description("The level at which the comment is targeted"), mcp.Enum("FILE", "LINE"), ), mcp.WithNumber("line", mcp.Description("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range"), ), mcp.WithString("side", mcp.Description("The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state"), mcp.Enum("LEFT", "RIGHT"), ), mcp.WithNumber("startLine", mcp.Description("For multi-line comments, the first line of the range that the comment applies to"), ), mcp.WithString("startSide", mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state"), mcp.Enum("LEFT", "RIGHT"), ), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { var params struct { Owner string Repo string PullNumber int32 Path string Body string SubjectType string Line *int32 Side *string StartLine *int32 StartSide *string } if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } client, err := getGQLClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) } // First we'll get the current user var getViewerQuery struct { Viewer struct { Login githubv4.String } } if err := client.Query(ctx, &getViewerQuery, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get current user", err, ), nil, nil } var getLatestReviewForViewerQuery struct { Repository struct { PullRequest struct { Reviews struct { Nodes []struct { ID githubv4.ID State githubv4.PullRequestReviewState URL githubv4.URI } } `graphql:"reviews(first: 1, author: $author)"` } `graphql:"pullRequest(number: $prNum)"` } `graphql:"repository(owner: $owner, name: $name)"` } vars := map[string]any{ "author": githubv4.String(getViewerQuery.Viewer.Login), "owner": githubv4.String(params.Owner), "name": githubv4.String(params.Repo), "prNum": githubv4.Int(params.PullNumber), } if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get latest review for current user", err, ), nil, nil } // Validate there is one review and the state is pending if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { return utils.NewToolResultError("No pending review found for the viewer"), nil, nil } review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] if review.State != githubv4.PullRequestReviewStatePending { errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) return utils.NewToolResultError(errText), nil, nil } // Then we can create a new review thread comment on the review. var addPullRequestReviewThreadMutation struct { AddPullRequestReviewThread struct { Thread struct { ID githubv4.ID // We don't need this, but a selector is required or GQL complains. } } `graphql:"addPullRequestReviewThread(input: $input)"` } if err := client.Mutate( ctx, &addPullRequestReviewThreadMutation, githubv4.AddPullRequestReviewThreadInput{ Path: githubv4.String(params.Path), Body: githubv4.String(params.Body), SubjectType: newGQLStringlikePtr[githubv4.PullRequestReviewThreadSubjectType](¶ms.SubjectType), Line: newGQLIntPtr(params.Line), Side: newGQLStringlikePtr[githubv4.DiffSide](params.Side), StartLine: newGQLIntPtr(params.StartLine), StartSide: newGQLStringlikePtr[githubv4.DiffSide](params.StartSide), PullRequestReviewID: &review.ID, }, nil, ); err != nil { return utils.NewToolResultError(err.Error()), nil, nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil, nil } } */ // RequestCopilotReview creates a tool to request a Copilot review for a pull request. // Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this // tool if the configured host does not support it. // TODO: MIGRATE THIS TOOL /* func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("request_copilot_review", mcp.WithDescription(t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_REQUEST_COPILOT_REVIEW_USER_TITLE", "Request Copilot review"), ReadOnlyHint: ToBoolPtr(false), }), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("pullNumber", mcp.Required(), mcp.Description("Pull request number"), ), ), func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } _, resp, err := client.PullRequests.RequestReviewers( ctx, owner, repo, pullNumber, github.ReviewersRequest{ // The login name of the copilot reviewer bot Reviewers: []string{"copilot-pull-request-reviewer[bot]"}, }, ) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to request copilot review", resp, err, ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil, nil } // Return nothing on success, as there's not much value in returning the Pull Request itself return utils.NewToolResultText(""), nil, nil } } */ // newGQLString like takes something that approximates a string (of which there are many types in shurcooL/githubv4) // and constructs a pointer to it, or nil if the string is empty. This is extremely useful because when we parse // params from the MCP request, we need to convert them to types that are pointers of type def strings and it's // not possible to take a pointer of an anonymous value e.g. &githubv4.String("foo"). func newGQLStringlike[T ~string](s string) *T { if s == "" { return nil } stringlike := T(s) return &stringlike } func newGQLStringlikePtr[T ~string](s *string) *T { if s == nil { return nil } stringlike := T(*s) return &stringlike } func newGQLIntPtr(i *int32) *githubv4.Int { if i == nil { return nil } gi := githubv4.Int(*i) return &gi }