Files
github--github-mcp-server/pkg/github/pullrequests.go
William Martin 4df2d3d803
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
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Lint / lint (push) Has been cancelled
WIP: schema generation
2025-04-11 22:20:59 +02:00

1088 lines
35 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"
)
// GetPullRequest creates a tool to get details of a specific pull request.
func GetPullRequest(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"get_pull_request",
mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_DESCRIPTION", "Get details of a specific pull request")),
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"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: ReadOnly,
Category: CategoryPullRequests,
}
}
// UpdatePullRequest creates a tool to update an existing pull request.
func UpdatePullRequest(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"update_pull_request",
mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository")),
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 ('open' or 'closed')"),
mcp.Enum("open", "closed"),
),
mcp.WithString("base",
mcp.Description("New base branch name"),
),
mcp.WithBoolean("maintainer_can_modify",
mcp.Description("Allow maintainer edits"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: Write,
Category: CategoryPullRequests,
}
}
// ListPullRequests creates a tool to list and filter repository pull requests.
func ListPullRequests(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"list_pull_requests",
mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List and filter repository pull requests")),
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 ('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 ('created', 'updated', 'popularity', 'long-running')"),
),
mcp.WithString("direction",
mcp.Description("Sort direction ('asc', 'desc')"),
),
WithPagination(),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: ReadOnly,
Category: CategoryPullRequests,
}
}
// MergePullRequest creates a tool to merge a pull request.
func MergePullRequest(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"merge_pull_request",
mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request")),
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 ('merge', 'squash', 'rebase')"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: Write,
Category: CategoryPullRequests,
}
}
// GetPullRequestFiles creates a tool to get the list of files changed in a pull request.
func GetPullRequestFiles(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"get_pull_request_files",
mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_FILES_DESCRIPTION", "Get the list of files changed in a pull request")),
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"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: ReadOnly,
Category: CategoryPullRequests,
}
}
// GetPullRequestStatus creates a tool to get the combined status of all status checks for a pull request.
func GetPullRequestStatus(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"get_pull_request_status",
mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_STATUS_DESCRIPTION", "Get the combined status of all status checks for a pull request")),
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"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: ReadOnly,
Category: CategoryPullRequests,
}
}
// UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch.
func UpdatePullRequestBranch(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"update_pull_request_branch",
mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update a pull request branch with the latest changes from the base branch")),
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"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: Write,
Category: CategoryPullRequests,
}
}
// GetPullRequestComments creates a tool to get the review comments on a pull request.
func GetPullRequestComments(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"get_pull_request_comments",
mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION", "Get the review comments on a pull request")),
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"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: ReadOnly,
Category: CategoryPullRequests,
}
}
// GetPullRequestReviews creates a tool to get the reviews on a pull request.
func GetPullRequestReviews(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"get_pull_request_reviews",
mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_REVIEWS_DESCRIPTION", "Get the reviews on a pull request")),
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"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: ReadOnly,
Category: CategoryPullRequests,
}
}
// CreatePullRequestReview creates a tool to submit a review on a pull request.
func CreatePullRequestReview(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"create_pull_request_review",
mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_REVIEW_DESCRIPTION", "Create a review on a pull request")),
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.Required(),
mcp.Description("Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT')"),
),
mcp.WithString("commitId",
mcp.Description("SHA of commit to review"),
),
mcp.WithArray("comments",
mcp.Items(
map[string]interface{}{
"type": "object",
"additionalProperties": false,
"required": []string{"path", "body"},
"properties": map[string]interface{}{
"path": map[string]interface{}{
"type": "string",
"description": "path to the file",
},
"position": map[string]interface{}{
"type": "number",
"description": "position of the comment in the diff",
},
"line": map[string]interface{}{
"type": "number",
"description": "line number in the file to comment on. For multi-line comments, the end of the line range",
},
"side": map[string]interface{}{
"type": "string",
"description": "The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. (LEFT or RIGHT)",
},
"start_line": map[string]interface{}{
"type": "number",
"description": "The first line of the range to which the comment refers. Required for multi-line comments.",
},
"start_side": map[string]interface{}{
"type": "string",
"description": "The side of the diff on which the start line resides for multi-line comments. (LEFT or RIGHT)",
},
"body": map[string]interface{}{
"type": "string",
"description": "comment body",
},
},
},
),
mcp.Description("Line-specific comments array of objects to place comments on pull request changes. Requires path and body. For line comments use line or position. For multi-line comments use start_line and line with optional side parameters."),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
event, err := requiredParam[string](request, "event")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Create review request
reviewRequest := &github.PullRequestReviewRequest{
Event: github.Ptr(event),
}
// Add body if provided
body, err := OptionalParam[string](request, "body")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if body != "" {
reviewRequest.Body = github.Ptr(body)
}
// Add commit ID if provided
commitID, err := OptionalParam[string](request, "commitId")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if commitID != "" {
reviewRequest.CommitID = github.Ptr(commitID)
}
// Add comments if provided
if commentsObj, ok := request.Params.Arguments["comments"].([]interface{}); ok && len(commentsObj) > 0 {
comments := []*github.DraftReviewComment{}
for _, c := range commentsObj {
commentMap, ok := c.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("each comment must be an object with path and body"), nil
}
path, ok := commentMap["path"].(string)
if !ok || path == "" {
return mcp.NewToolResultError("each comment must have a path"), nil
}
body, ok := commentMap["body"].(string)
if !ok || body == "" {
return mcp.NewToolResultError("each comment must have a body"), nil
}
_, hasPosition := commentMap["position"].(float64)
_, hasLine := commentMap["line"].(float64)
_, hasSide := commentMap["side"].(string)
_, hasStartLine := commentMap["start_line"].(float64)
_, hasStartSide := commentMap["start_side"].(string)
switch {
case !hasPosition && !hasLine:
return mcp.NewToolResultError("each comment must have either position or line"), nil
case hasPosition && (hasLine || hasSide || hasStartLine || hasStartSide):
return mcp.NewToolResultError("position cannot be combined with line, side, start_line, or start_side"), nil
case hasStartSide && !hasSide:
return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil
}
comment := &github.DraftReviewComment{
Path: github.Ptr(path),
Body: github.Ptr(body),
}
if positionFloat, ok := commentMap["position"].(float64); ok {
comment.Position = github.Ptr(int(positionFloat))
} else if lineFloat, ok := commentMap["line"].(float64); ok {
comment.Line = github.Ptr(int(lineFloat))
}
if side, ok := commentMap["side"].(string); ok {
comment.Side = github.Ptr(side)
}
if startLineFloat, ok := commentMap["start_line"].(float64); ok {
comment.StartLine = github.Ptr(int(startLineFloat))
}
if startSide, ok := commentMap["start_side"].(string); ok {
comment.StartSide = github.Ptr(startSide)
}
comments = append(comments, comment)
}
reviewRequest.Comments = comments
}
client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
review, resp, err := client.PullRequests.CreateReview(ctx, owner, repo, pullNumber, reviewRequest)
if err != nil {
return nil, fmt.Errorf("failed to create pull request review: %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 create pull request review: %s", string(body))), nil
}
r, err := json.Marshal(review)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}
return mcp.NewToolResultText(string(r)), nil
}
},
Access: Write,
Category: CategoryPullRequests,
}
}
// CreatePullRequest creates a tool to create a new pull request.
func CreatePullRequest(t translations.TranslationHelperFunc) Tool {
return Tool{
Definition: mcp.NewTool(
"create_pull_request",
mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository")),
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"),
),
),
Handler: func(getClient GetClientFn) server.ToolHandlerFunc {
return 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
}
},
Access: Write,
Category: CategoryPullRequests,
}
}