Files
github--github-mcp-server/pkg/github/pullrequests.go
copilot-swe-agent[bot] be1fd87e6b
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
License Check / license-check (push) Has been cancelled
Successfully migrated first 2 tools (PullRequestRead, CreatePullRequest) and fixed helper functions
Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com>
2025-11-20 10:26:55 +00:00

1692 lines
55 KiB
Go

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, &params); 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](&params.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, &params); 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](&params.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
}