2ce83d011b
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Unit Tests / build (macos-latest) (push) Has been cancelled
Unit Tests / build (ubuntu-latest) (push) Has been cancelled
Unit Tests / build (windows-latest) (push) Has been cancelled
License Check / license-check (push) Has been cancelled
Lint / lint (push) Has been cancelled
1809 lines
61 KiB
Go
1809 lines
61 KiB
Go
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
|
|
}
|
|
}
|