package github import ( "context" "encoding/json" "fmt" "io" "net/http" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" ) // 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", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_DESCRIPTION", "Get details of a specific pull request in a GitHub repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get pull request details"), ReadOnlyHint: true, }), 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return nil, fmt.Errorf("failed to get pull request: %w", err) } 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 mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } r, err := json.Marshal(pr) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // CreatePullRequest creates a tool to create a new pull request. func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("create_pull_request", mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), ReadOnlyHint: false, }), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithString("title", mcp.Required(), mcp.Description("PR title"), ), mcp.WithString("body", mcp.Description("PR description"), ), mcp.WithString("head", mcp.Required(), mcp.Description("Branch containing changes"), ), mcp.WithString("base", mcp.Required(), mcp.Description("Branch to merge into"), ), mcp.WithBoolean("draft", mcp.Description("Create as draft PR"), ), mcp.WithBoolean("maintainer_can_modify", mcp.Description("Allow maintainer edits"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } title, err := requiredParam[string](request, "title") if err != nil { return mcp.NewToolResultError(err.Error()), nil } head, err := requiredParam[string](request, "head") if err != nil { return mcp.NewToolResultError(err.Error()), nil } base, err := requiredParam[string](request, "base") if err != nil { return mcp.NewToolResultError(err.Error()), nil } body, err := OptionalParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } draft, err := OptionalParam[bool](request, "draft") if err != nil { return mcp.NewToolResultError(err.Error()), nil } maintainerCanModify, err := OptionalParam[bool](request, "maintainer_can_modify") if err != nil { return mcp.NewToolResultError(err.Error()), 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, fmt.Errorf("failed to get GitHub client: %w", err) } pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR) if err != nil { return nil, fmt.Errorf("failed to create pull request: %w", err) } 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 mcp.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil } r, err := json.Marshal(pr) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // UpdatePullRequest creates a tool to update an existing pull request. func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { 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: 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.WithString("base", mcp.Description("New base branch name"), ), mcp.WithBoolean("maintainer_can_modify", mcp.Description("Allow maintainer edits"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Build the update struct only with provided fields update := &github.PullRequest{} updateNeeded := false if title, ok, err := OptionalParamOK[string](request, "title"); err != nil { return mcp.NewToolResultError(err.Error()), nil } else if ok { update.Title = github.Ptr(title) updateNeeded = true } if body, ok, err := OptionalParamOK[string](request, "body"); err != nil { return mcp.NewToolResultError(err.Error()), nil } else if ok { update.Body = github.Ptr(body) updateNeeded = true } if state, ok, err := OptionalParamOK[string](request, "state"); err != nil { return mcp.NewToolResultError(err.Error()), nil } else if ok { update.State = github.Ptr(state) updateNeeded = true } if base, ok, err := OptionalParamOK[string](request, "base"); err != nil { return mcp.NewToolResultError(err.Error()), nil } else if ok { update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)} updateNeeded = true } if maintainerCanModify, ok, err := OptionalParamOK[bool](request, "maintainer_can_modify"); err != nil { return mcp.NewToolResultError(err.Error()), nil } else if ok { update.MaintainerCanModify = github.Ptr(maintainerCanModify) updateNeeded = true } if !updateNeeded { return mcp.NewToolResultError("No update parameters provided."), nil } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } pr, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) if err != nil { return nil, fmt.Errorf("failed to update pull request: %w", err) } 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 mcp.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil } r, err := json.Marshal(pr) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // ListPullRequests creates a tool to list and filter repository pull requests. func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("list_pull_requests", mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"), ReadOnlyHint: 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } state, err := OptionalParam[string](request, "state") if err != nil { return mcp.NewToolResultError(err.Error()), nil } head, err := OptionalParam[string](request, "head") if err != nil { return mcp.NewToolResultError(err.Error()), nil } base, err := OptionalParam[string](request, "base") if err != nil { return mcp.NewToolResultError(err.Error()), nil } sort, err := OptionalParam[string](request, "sort") if err != nil { return mcp.NewToolResultError(err.Error()), nil } direction, err := OptionalParam[string](request, "direction") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), 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 nil, fmt.Errorf("failed to list pull requests: %w", err) } 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 mcp.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil } r, err := json.Marshal(prs) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // MergePullRequest creates a tool to merge a pull request. func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { 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: 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } commitTitle, err := OptionalParam[string](request, "commit_title") if err != nil { return mcp.NewToolResultError(err.Error()), nil } commitMessage, err := OptionalParam[string](request, "commit_message") if err != nil { return mcp.NewToolResultError(err.Error()), nil } mergeMethod, err := OptionalParam[string](request, "merge_method") if err != nil { return mcp.NewToolResultError(err.Error()), 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 nil, fmt.Errorf("failed to merge pull request: %w", err) } 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 mcp.NewToolResultError(fmt.Sprintf("failed to merge pull request: %s", string(body))), nil } r, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // GetPullRequestFiles creates a tool to get the list of files changed in a pull request. func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_files", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_FILES_DESCRIPTION", "Get the files changed in a specific pull request.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_GET_PULL_REQUEST_FILES_USER_TITLE", "Get pull request files"), ReadOnlyHint: true, }), 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } opts := &github.ListOptions{} files, resp, err := client.PullRequests.ListFiles(ctx, owner, repo, pullNumber, opts) if err != nil { return nil, fmt.Errorf("failed to get pull request files: %w", err) } 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 mcp.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 mcp.NewToolResultText(string(r)), nil } } // GetPullRequestStatus creates a tool to get the combined status of all status checks for a pull request. func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_status", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_STATUS_DESCRIPTION", "Get the status of a specific pull request.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_GET_PULL_REQUEST_STATUS_USER_TITLE", "Get pull request status checks"), ReadOnlyHint: true, }), 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // First get the PR to find the head SHA client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return nil, fmt.Errorf("failed to get pull request: %w", err) } 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 mcp.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 nil, fmt.Errorf("failed to get combined status: %w", err) } 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 mcp.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 mcp.NewToolResultText(string(r)), nil } } // UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { 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: 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } expectedHeadSHA, err := OptionalParam[string](request, "expectedHeadSha") if err != nil { return mcp.NewToolResultError(err.Error()), 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 mcp.NewToolResultText("Pull request branch update is in progress"), nil } return nil, fmt.Errorf("failed to update pull request branch: %w", err) } 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 mcp.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(body))), nil } r, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // GetPullRequestComments creates a tool to get the review comments on a pull request. func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_comments", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION", "Get comments for a specific pull request.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_GET_PULL_REQUEST_COMMENTS_USER_TITLE", "Get pull request comments"), ReadOnlyHint: true, }), 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } opts := &github.PullRequestListCommentsOptions{ ListOptions: github.ListOptions{ PerPage: 100, }, } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts) if err != nil { return nil, fmt.Errorf("failed to get pull request comments: %w", err) } 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 mcp.NewToolResultError(fmt.Sprintf("failed to get pull request comments: %s", string(body))), nil } r, err := json.Marshal(comments) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // AddPullRequestReviewComment creates a tool to add a review comment to a pull request. func AddPullRequestReviewComment(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("add_pull_request_review_comment", mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_DESCRIPTION", "Add a review comment to a pull request.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_USER_TITLE", "Add review comment to pull request"), ReadOnlyHint: false, }), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("pull_number", mcp.Required(), mcp.Description("Pull request number"), ), mcp.WithString("body", mcp.Required(), mcp.Description("The text of the review comment"), ), mcp.WithString("commit_id", mcp.Description("The SHA of the commit to comment on. Required unless in_reply_to is specified."), ), mcp.WithString("path", mcp.Description("The relative path to the file that necessitates a comment. Required unless in_reply_to is specified."), ), mcp.WithString("subject_type", mcp.Description("The level at which the comment is targeted"), mcp.Enum("line", "file"), ), 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"), mcp.Enum("LEFT", "RIGHT"), ), mcp.WithNumber("start_line", mcp.Description("For multi-line comments, the first line of the range that the comment applies to"), ), mcp.WithString("start_side", mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to"), mcp.Enum("LEFT", "RIGHT"), ), mcp.WithNumber("in_reply_to", mcp.Description("The ID of the review comment to reply to. When specified, only body is required and all other parameters are ignored"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pull_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil } body, err := requiredParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } // Check if this is a reply to an existing comment if replyToFloat, ok := request.Params.Arguments["in_reply_to"].(float64); ok { // Use the specialized method for reply comments due to inconsistency in underlying go-github library: https://github.com/google/go-github/pull/950 commentID := int64(replyToFloat) createdReply, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, commentID) if err != nil { return nil, fmt.Errorf("failed to reply to pull request comment: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return mcp.NewToolResultError(fmt.Sprintf("failed to reply to pull request comment: %s", string(respBody))), nil } r, err := json.Marshal(createdReply) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } // This is a new comment, not a reply // Verify required parameters for a new comment commitID, err := requiredParam[string](request, "commit_id") if err != nil { return mcp.NewToolResultError(err.Error()), nil } path, err := requiredParam[string](request, "path") if err != nil { return mcp.NewToolResultError(err.Error()), nil } comment := &github.PullRequestComment{ Body: github.Ptr(body), CommitID: github.Ptr(commitID), Path: github.Ptr(path), } subjectType, err := OptionalParam[string](request, "subject_type") if err != nil { return mcp.NewToolResultError(err.Error()), nil } if subjectType != "file" { line, lineExists := request.Params.Arguments["line"].(float64) startLine, startLineExists := request.Params.Arguments["start_line"].(float64) side, sideExists := request.Params.Arguments["side"].(string) startSide, startSideExists := request.Params.Arguments["start_side"].(string) if !lineExists { return mcp.NewToolResultError("line parameter is required unless using subject_type:file"), nil } comment.Line = github.Ptr(int(line)) if sideExists { comment.Side = github.Ptr(side) } if startLineExists { comment.StartLine = github.Ptr(int(startLine)) } if startSideExists { comment.StartSide = github.Ptr(startSide) } if startLineExists && !lineExists { return mcp.NewToolResultError("if start_line is provided, line must also be provided"), nil } if startSideExists && !sideExists { return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil } } createdComment, resp, err := client.PullRequests.CreateComment(ctx, owner, repo, pullNumber, comment) if err != nil { return nil, fmt.Errorf("failed to create pull request comment: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request comment: %s", string(respBody))), nil } r, err := json.Marshal(createdComment) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // GetPullRequestReviews creates a tool to get the reviews on a pull request. func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_reviews", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_REVIEWS_DESCRIPTION", "Get reviews for a specific pull request.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_GET_PULL_REQUEST_REVIEWS_USER_TITLE", "Get pull request reviews"), ReadOnlyHint: true, }), 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } client, err := getClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } reviews, resp, err := client.PullRequests.ListReviews(ctx, owner, repo, pullNumber, nil) if err != nil { return nil, fmt.Errorf("failed to get pull request reviews: %w", err) } 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 mcp.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 mcp.NewToolResultText(string(r)), nil } } func CreateAndSubmitPullRequestReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("create_and_submit_pull_request_review", mcp.WithDescription(t("TOOL_CREATE_AND_SUBMIT_PULL_REQUEST_REVIEW_DESCRIPTION", "Create and submit a review for a pull request without review comments.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_CREATE_AND_SUBMIT_PULL_REQUEST_REVIEW_USER_TITLE", "Create and submit a pull request review without comments"), ReadOnlyHint: 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("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.Required(), mcp.Description("Review comment text"), ), mcp.WithString("event", mcp.Required(), 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := requiredParam[constrainableInt32](request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } body, err := requiredParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } event, err := requiredParam[string](request, "event") if err != nil { return mcp.NewToolResultError(err.Error()), nil } commitID, err := OptionalParam[string](request, "commitId") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Given our owner, repo and PR number, lookup the GQL ID of the PR. client, err := getGQLClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) } 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(owner), "repo": githubv4.String(repo), "prNum": githubv4.Int(pullNumber), }); err != nil { return mcp.NewToolResultError(err.Error()), 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)"` } if err := client.Mutate( ctx, &addPullRequestReviewMutation, githubv4.AddPullRequestReviewInput{ PullRequestID: getPullRequestQuery.Repository.PullRequest.ID, Body: newGQLStringlike[githubv4.String](body), Event: newGQLStringlike[githubv4.PullRequestReviewEvent](event), CommitOID: newGQLStringlike[githubv4.GitObjectID](commitID), }, nil, ); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Return nothing, 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 mcp.NewToolResultText(""), nil } } // CreatePendingPullRequestReview creates a tool to create a pending review on a pull request. func CreatePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("create_pending_pull_request_review", mcp.WithDescription(t("TOOL_CREATE_PENDING_PULL_REQUEST_REVIEW_DESCRIPTION", "Create a pending review for a pull request.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_CREATE_PENDING_PULL_REQUEST_REVIEW_USER_TITLE", "Create pending pull request review"), ReadOnlyHint: 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("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("commitID", mcp.Description("SHA of commit to review"), ), // Event is omitted here because we always want to create a pending review. // Threads are omitted for the moment, and we'll see if the LLM can use the appropriate tool. ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := requiredParam[constrainableInt32](request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } commitID, err := OptionalParam[string](request, "commitID") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Given our owner, repo and PR number, lookup the GQL ID of the PR. client, err := getGQLClient(ctx) if err != nil { return nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) } 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(owner), "repo": githubv4.String(repo), "prNum": githubv4.Int(pullNumber), }); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Now we have the GQL ID, we can create a pending 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)"` } if err := client.Mutate( ctx, &addPullRequestReviewMutation, githubv4.AddPullRequestReviewInput{ PullRequestID: getPullRequestQuery.Repository.PullRequest.ID, CommitOID: newGQLStringlike[githubv4.GitObjectID](commitID), }, nil, ); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Return nothing, 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 mcp.NewToolResultText(""), nil } } // AddPullRequestReviewCommentToPendingReview creates a tool to add a comment to a pull request review. func AddPullRequestReviewCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("add_pull_request_review_comment_to_pending_review", mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add a comment to the requester's latest pending pull request review.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_TO_PENDING_REVIEW_USER_TITLE", "Add comment to the requester's latest pending pull request review"), ReadOnlyHint: 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"), 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"), mcp.Enum("LEFT", "RIGHT"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := requiredParam[constrainableInt32](request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } path, err := requiredParam[string](request, "path") if err != nil { return mcp.NewToolResultError(err.Error()), nil } body, err := requiredParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } subjectType, err := requiredParam[string](request, "subjectType") if err != nil { return mcp.NewToolResultError(err.Error()), nil } line, err := OptionalParam[constrainableInt32](request, "line") if err != nil { return mcp.NewToolResultError(err.Error()), nil } side, err := OptionalParam[string](request, "side") if err != nil { return mcp.NewToolResultError(err.Error()), nil } startLine, err := OptionalParam[constrainableInt32](request, "startLine") if err != nil { return mcp.NewToolResultError(err.Error()), nil } startSide, err := OptionalParam[string](request, "startSide") if err != nil { return mcp.NewToolResultError(err.Error()), 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 mcp.NewToolResultError(err.Error()), nil } // Then let's get the ID of the review (but maybe we should just get the ID of the review itself: TODO) 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: $number)"` } `graphql:"repository(owner: $owner, name: $name)"` } vars := map[string]interface{}{ "author": githubv4.String(getViewerQuery.Viewer.Login), "owner": githubv4.String(owner), "name": githubv4.String(repo), "number": githubv4.Int(pullNumber), } if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Validate there is one review and the state is pending if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { return mcp.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 mcp.NewToolResultError(errText), 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(path), Body: githubv4.String(body), SubjectType: newGQLStringlike[githubv4.PullRequestReviewThreadSubjectType](subjectType), Line: githubv4.NewInt(githubv4.Int(line)), Side: newGQLStringlike[githubv4.DiffSide](side), StartLine: githubv4.NewInt(githubv4.Int(startLine)), StartSide: newGQLStringlike[githubv4.DiffSide](startSide), PullRequestReviewID: &review.ID, }, nil, ); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Return nothing, 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 mcp.NewToolResultText(""), nil } } // SubmitPendingPullRequestReview creates a tool to submit a pull request review. func SubmitPendingPullRequestReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("submit_pending_pull_request_review", mcp.WithDescription(t("TOOL_SUBMIT_PENDING_PULL_REQUEST_REVIEW_DESCRIPTION", "Submit the requester's latest pending pull request review.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_SUBMIT_PENDING_PULL_REQUEST_REVIEW_USER_TITLE", "Submit the requester's latest pending pull request review"), ReadOnlyHint: 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 submitting // the latest review from a user, since only one can be active at a time. 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("event", mcp.Required(), mcp.Description("The event to perform"), mcp.Enum("APPROVE", "REQUEST_CHANGES", "COMMENT"), ), mcp.WithString("body", mcp.Description("The text of the review comment"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := requiredParam[constrainableInt32](request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } event, err := requiredParam[string](request, "event") if err != nil { return mcp.NewToolResultError(err.Error()), nil } body, err := OptionalParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), 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 mcp.NewToolResultError(err.Error()), nil } // Then let's get the ID of the review (but maybe we should just get the ID of the review itself: TODO) var getLatestReviewForViewerQuery struct { Repository struct { PullRequest struct { Reviews struct { Nodes []struct { ID githubv4.ID Author struct { Login githubv4.String } State githubv4.PullRequestReviewState SubmittedAt githubv4.DateTime Body githubv4.String URL githubv4.URI } } `graphql:"reviews(first: 1, author: $author)"` } `graphql:"pullRequest(number: $number)"` } `graphql:"repository(owner: $owner, name: $name)"` } vars := map[string]interface{}{ "author": githubv4.String(getViewerQuery.Viewer.Login), "owner": githubv4.String(owner), "name": githubv4.String(repo), "number": githubv4.Int(pullNumber), } if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Validate there is one review and the state is pending if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { return mcp.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 mcp.NewToolResultError(errText), nil } // Prepare the mutation var submitPullRequestReviewMutation struct { SubmitPullRequestReview struct { PullRequestReview struct { State githubv4.PullRequestReviewState SubmittedAt githubv4.DateTime } } `graphql:"submitPullRequestReview(input: $input)"` } if err := client.Mutate( ctx, &submitPullRequestReviewMutation, githubv4.SubmitPullRequestReviewInput{ PullRequestReviewID: &review.ID, Event: githubv4.PullRequestReviewEvent(event), Body: newGQLStringlike[githubv4.String](body), }, nil, ); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Return the state and submitted at time of the review as a receipt for the LLM. r, err := json.Marshal(submitPullRequestReviewMutation.SubmitPullRequestReview.PullRequestReview) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } func DeletePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("delete_pending_pull_request_review", mcp.WithDescription(t("TOOL_DELETE_PENDING_PULL_REQUEST_REVIEW_DESCRIPTION", "Delete the requester's latest pending pull request review.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_DELETE_PENDING_PULL_REQUEST_REVIEW_USER_TITLE", "Delete the requester's latest pending pull request review"), ReadOnlyHint: 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 deleting // the latest pending review from a user, since only one can be active at a time. 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return mcp.NewToolResultError(err.Error()), nil } repo, err := requiredParam[string](request, "repo") if err != nil { return mcp.NewToolResultError(err.Error()), nil } pullNumber, err := requiredParam[constrainableInt32](request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), 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 mcp.NewToolResultError(err.Error()), nil } // Then let's get the ID of the review (but maybe we should just get the ID of the review itself: TODO) var getLatestReviewForViewerQuery struct { Repository struct { PullRequest struct { Reviews struct { Nodes []struct { ID githubv4.ID Author struct { Login githubv4.String } State githubv4.PullRequestReviewState SubmittedAt githubv4.DateTime Body githubv4.String URL githubv4.URI } } `graphql:"reviews(first: 1, author: $author)"` } `graphql:"pullRequest(number: $number)"` } `graphql:"repository(owner: $owner, name: $name)"` } vars := map[string]interface{}{ "author": githubv4.String(getViewerQuery.Viewer.Login), "owner": githubv4.String(owner), "name": githubv4.String(repo), "number": githubv4.Int(pullNumber), } if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Validate there is one review and the state is pending if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { return mcp.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 mcp.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 mcp.NewToolResultError(err.Error()), nil } // Return nothing, 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 mcp.NewToolResultText(""), 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 } type requestCopilotReviewArgs struct { Owner string Repo string PullNumber int32 } // TODO: This, and all the param parsing absolutely does not need the MCP request, it just needs the // Argument map. Ideally we would just get the byte array and unmarshal it into the struct but mcp-go // doesn't expose that. func parseRequestCopilotReviewArgs(request mcp.CallToolRequest) (requestCopilotReviewArgs, error) { owner, err := requiredParam[string](request, "owner") if err != nil { return requestCopilotReviewArgs{}, err } repo, err := requiredParam[string](request, "repo") if err != nil { return requestCopilotReviewArgs{}, err } pullNumber, err := requiredParam[constrainableInt32](request, "pullNumber") if err != nil { return requestCopilotReviewArgs{}, err } return requestCopilotReviewArgs{ Owner: owner, Repo: repo, PullNumber: int32(pullNumber), }, nil } // RequestCopilotReview creates a tool to request a Copilot review for a pull request. func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("request_copilot_review", mcp.WithDescription(t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot review for a pull request. Note: This feature depends on GitHub API support and may not be available for all users.")), 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args, err := parseRequestCopilotReviewArgs(request) if err != nil { return nil, err } client, err := getClient(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } if _, _, err := client.PullRequests.RequestReviewers( ctx, args.Owner, args.Repo, int(args.PullNumber), github.ReviewersRequest{ Reviewers: []string{"copilot-pull-request-reviewer[bot]"}, // The login name of the copilot bot. }, ); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Return nothing, just indicate success for the time being. return mcp.NewToolResultText(""), nil } }