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
1088 lines
35 KiB
Go
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,
|
|
}
|
|
}
|