From 964457dc60a7f78c7925fc4b60c6f3f3dbafa029 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:09:33 +0000 Subject: [PATCH 1/3] Initial plan From 4706206967364ece1769ddaa444746fee2784932 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:19:05 +0000 Subject: [PATCH 2/3] WIP: Partial migration of pullrequests toolset - imports and first 2 tools complete Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> --- pkg/github/pullrequests.go | 560 +++++----- pkg/github/pullrequests_marked.go | 1668 +++++++++++++++++++++++++++++ pkg/github/pullrequests_test.go | 2 - pkg/github/search_utils.go | 35 +- 4 files changed, 1979 insertions(+), 286 deletions(-) create mode 100644 pkg/github/pullrequests_marked.go diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 69af69af7..6073229de 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -11,26 +9,31 @@ import ( "github.com/go-viper/mapstructure/v2" "github.com/google/go-github/v79/github" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/sanitize" "github.com/github/github-mcp-server/pkg/translations" + "github.com/github/github-mcp-server/pkg/utils" ) // GetPullRequest creates a tool to get details of a specific pull request. -func PullRequestRead(getClient GetClientFn, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("pull_request_read", - mcp.WithDescription(t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get details for a single pull request"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("method", - mcp.Required(), - mcp.Description(`Action to specify what pull request data needs to be retrieved from GitHub. +func PullRequestRead(getClient GetClientFn, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "pull_request_read", + Description: t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get details for a single pull request"), + ReadOnlyHint: true, + }, + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "method": { + Type: "string", + Description: `Action to specify what pull request data needs to be retrieved from GitHub. Possible options: 1. get - Get details of a specific pull request. 2. get_diff - Get the diff of a pull request. @@ -39,72 +42,82 @@ Possible options: 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned. 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned. -`), +`, + Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments"}, + }, + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number", + }, + }, + Required: []string{"method", "owner", "repo", "pullNumber"}, + }), + } - mcp.Enum("get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments"), - ), - 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"), - ), - WithPagination(), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := RequiredParam[string](request, "method") + handler := mcp.ToolHandlerFor[map[string]any, any]( + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + method, err := RequiredParam[string](args, "method") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - owner, err := RequiredParam[string](request, "owner") + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pullNumber, err := RequiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pagination, err := OptionalPaginationParams(request) + pagination, err := OptionalPaginationParams(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } - switch method { + var result *mcp.CallToolResult + var handlerErr error + switch method { case "get": - return GetPullRequest(ctx, client, owner, repo, pullNumber) + result, handlerErr = GetPullRequest(ctx, client, owner, repo, pullNumber) case "get_diff": - return GetPullRequestDiff(ctx, client, owner, repo, pullNumber) + result, handlerErr = GetPullRequestDiff(ctx, client, owner, repo, pullNumber) case "get_status": - return GetPullRequestStatus(ctx, client, owner, repo, pullNumber) + result, handlerErr = GetPullRequestStatus(ctx, client, owner, repo, pullNumber) case "get_files": - return GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination) + result, handlerErr = GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination) case "get_review_comments": - return GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) + result, handlerErr = GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) case "get_reviews": - return GetPullRequestReviews(ctx, client, owner, repo, pullNumber) + result, handlerErr = GetPullRequestReviews(ctx, client, owner, repo, pullNumber) case "get_comments": - return GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) + result, handlerErr = GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) default: - return nil, fmt.Errorf("unknown method: %s", method) + return nil, nil, fmt.Errorf("unknown method: %s", method) } - } + + return result, nil, handlerErr + }, + ) + + return tool, handler } func GetPullRequest(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { @@ -121,9 +134,9 @@ func GetPullRequest(ctx context.Context, client *github.Client, owner, repo stri 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } // sanitize title/body on response @@ -138,10 +151,10 @@ func GetPullRequest(ctx context.Context, client *github.Client, owner, repo stri r, err := json.Marshal(pr) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { @@ -163,15 +176,15 @@ func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil } defer func() { _ = resp.Body.Close() }() // Return the raw response - return mcp.NewToolResultText(string(raw)), nil + return utils.NewToolResultText(string(raw)), nil } func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { @@ -188,9 +201,9 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } // Get combined status for the head SHA @@ -207,17 +220,17 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil } r, err := json.Marshal(status) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { @@ -238,17 +251,17 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo 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 nil, 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 + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil } r, err := json.Marshal(files) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } func GetPullRequestReviewComments(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { @@ -272,17 +285,17 @@ func GetPullRequestReviewComments(ctx context.Context, client *github.Client, ow 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil } r, err := json.Marshal(comments) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { @@ -299,92 +312,104 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, re 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 nil, 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 + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil } r, err := json.Marshal(reviews) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } // CreatePullRequest creates a tool to create a new pull request. -func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, 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: ToBoolPtr(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") +func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "create_pull_request", + Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "title": { + Type: "string", + Description: "PR title", + }, + "body": { + Type: "string", + Description: "PR description", + }, + "head": { + Type: "string", + Description: "Branch containing changes", + }, + "base": { + Type: "string", + Description: "Branch to merge into", + }, + "draft": { + Type: "boolean", + Description: "Create as draft PR", + }, + "maintainer_can_modify": { + Type: "boolean", + Description: "Allow maintainer edits", + }, + }, + Required: []string{"owner", "repo", "title", "head", "base"}, + }, + } + + handler := mcp.ToolHandlerFor[map[string]any, any]( + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - title, err := RequiredParam[string](request, "title") + title, err := RequiredParam[string](args, "title") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - head, err := RequiredParam[string](request, "head") + head, err := RequiredParam[string](args, "head") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - base, err := RequiredParam[string](request, "base") + base, err := RequiredParam[string](args, "base") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - body, err := OptionalParam[string](request, "body") + body, err := OptionalParam[string](args, "body") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - draft, err := OptionalParam[bool](request, "draft") + draft, err := OptionalParam[bool](args, "draft") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - maintainerCanModify, err := OptionalParam[bool](request, "maintainer_can_modify") + maintainerCanModify, err := OptionalParam[bool](args, "maintainer_can_modify") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } newPR := &github.NewPullRequest{ @@ -402,7 +427,7 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR) if err != nil { @@ -410,16 +435,16 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu "failed to create pull request", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil, nil } // Return minimal response with just essential information @@ -430,15 +455,18 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu r, err := json.Marshal(minimalResponse) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil - } + return utils.NewToolResultText(string(r)), nil, nil + }, + ) + + return tool, handler } // UpdatePullRequest creates a tool to update an existing pull request. -func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { +func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("update_pull_request", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ @@ -483,27 +511,27 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra }), ), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - pullNumber, err := RequiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } // Check if draft parameter is provided - draftProvided := request.GetArguments()["draft"] != nil + draftProvided := args["draft"] != nil var draftValue bool if draftProvided { - draftValue, err = OptionalParam[bool](request, "draft") + draftValue, err = OptionalParam[bool](args, "draft") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } } @@ -511,57 +539,57 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra update := &github.PullRequest{} restUpdateNeeded := false - if title, ok, err := OptionalParamOK[string](request, "title"); err != nil { - return mcp.NewToolResultError(err.Error()), nil + if title, ok, err := OptionalParamOK[string](args, "title"); err != nil { + return utils.NewToolResultError(err.Error()), nil } else if ok { update.Title = github.Ptr(title) restUpdateNeeded = true } - if body, ok, err := OptionalParamOK[string](request, "body"); err != nil { - return mcp.NewToolResultError(err.Error()), nil + if body, ok, err := OptionalParamOK[string](args, "body"); err != nil { + return utils.NewToolResultError(err.Error()), nil } else if ok { update.Body = github.Ptr(body) restUpdateNeeded = true } - if state, ok, err := OptionalParamOK[string](request, "state"); err != nil { - return mcp.NewToolResultError(err.Error()), nil + if state, ok, err := OptionalParamOK[string](args, "state"); err != nil { + return utils.NewToolResultError(err.Error()), nil } else if ok { update.State = github.Ptr(state) restUpdateNeeded = true } - if base, ok, err := OptionalParamOK[string](request, "base"); err != nil { - return mcp.NewToolResultError(err.Error()), nil + if base, ok, err := OptionalParamOK[string](args, "base"); err != nil { + return utils.NewToolResultError(err.Error()), nil } else if ok { update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)} restUpdateNeeded = true } - if maintainerCanModify, ok, err := OptionalParamOK[bool](request, "maintainer_can_modify"); err != nil { - return mcp.NewToolResultError(err.Error()), nil + if maintainerCanModify, ok, err := OptionalParamOK[bool](args, "maintainer_can_modify"); err != nil { + return utils.NewToolResultError(err.Error()), nil } else if ok { update.MaintainerCanModify = github.Ptr(maintainerCanModify) restUpdateNeeded = true } // Handle reviewers separately - reviewers, err := OptionalStringArrayParam(request, "reviewers") + reviewers, err := OptionalStringArrayParam(args, "reviewers") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } // If no updates, no draft change, and no reviewers, return error early if !restUpdateNeeded && !draftProvided && len(reviewers) == 0 { - return mcp.NewToolResultError("No update parameters provided."), nil + return utils.NewToolResultError("No update parameters provided."), nil } // Handle REST API updates (title, body, state, base, maintainer_can_modify) if restUpdateNeeded { client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } _, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) @@ -577,9 +605,9 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil } } @@ -587,7 +615,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra if draftProvided { gqlClient, err := getGQLClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err) } var prQuery struct { @@ -653,7 +681,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra if len(reviewers) > 0 { client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } reviewersRequest := github.ReviewersRequest{ @@ -677,9 +705,9 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil } } @@ -707,15 +735,15 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra r, err := json.Marshal(minimalResponse) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil + return utils.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil } - return mcp.NewToolResultText(string(r)), nil + return utils.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) { +func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("list_pull_requests", mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ @@ -750,38 +778,38 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun ), WithPagination(), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - state, err := OptionalParam[string](request, "state") + state, err := OptionalParam[string](args, "state") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - head, err := OptionalParam[string](request, "head") + head, err := OptionalParam[string](args, "head") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - base, err := OptionalParam[string](request, "base") + base, err := OptionalParam[string](args, "base") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - sort, err := OptionalParam[string](request, "sort") + sort, err := OptionalParam[string](args, "sort") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - direction, err := OptionalParam[string](request, "direction") + direction, err := OptionalParam[string](args, "direction") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - pagination, err := OptionalPaginationParams(request) + pagination, err := OptionalPaginationParams(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } opts := &github.PullRequestListOptions{ State: state, @@ -797,7 +825,7 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) } prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts) if err != nil { @@ -812,9 +840,9 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil } // sanitize title/body on each PR @@ -832,15 +860,15 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun r, err := json.Marshal(prs) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } } // MergePullRequest creates a tool to merge a pull request. -func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { +func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("merge_pull_request", mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ @@ -870,30 +898,30 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun mcp.Enum("merge", "squash", "rebase"), ), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - pullNumber, err := RequiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - commitTitle, err := OptionalParam[string](request, "commit_title") + commitTitle, err := OptionalParam[string](args, "commit_title") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - commitMessage, err := OptionalParam[string](request, "commit_message") + commitMessage, err := OptionalParam[string](args, "commit_message") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - mergeMethod, err := OptionalParam[string](request, "merge_method") + mergeMethod, err := OptionalParam[string](args, "merge_method") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } options := &github.PullRequestOptions{ @@ -903,7 +931,7 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, 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 { @@ -918,17 +946,17 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to merge pull request: %s", string(body))), nil + return utils.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 nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } } @@ -972,13 +1000,13 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF ), WithPagination(), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests") } } // UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. -func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { +func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("update_pull_request_branch", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ @@ -1001,22 +1029,22 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe 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") + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - pullNumber, err := RequiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - expectedHeadSHA, err := OptionalParam[string](request, "expectedHeadSha") + expectedHeadSHA, err := OptionalParam[string](args, "expectedHeadSha") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } opts := &github.PullRequestBranchUpdateOptions{} if expectedHeadSHA != "" { @@ -1025,14 +1053,14 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, 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 utils.NewToolResultText("Pull request branch update is in progress"), nil } return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update pull request branch", @@ -1045,17 +1073,17 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe 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 nil, 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 + return utils.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 nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } } @@ -1069,7 +1097,7 @@ type PullRequestReviewWriteParams struct { CommitID *string } -func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { +func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("pull_request_review_write", mcp.WithDescription(t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request. @@ -1113,16 +1141,16 @@ Available methods: mcp.Description("SHA of commit to review"), ), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { var params PullRequestReviewWriteParams if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.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 mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil } switch params.Method { @@ -1133,7 +1161,7 @@ Available methods: case "delete_pending": return DeletePendingPullRequestReview(ctx, client, params) default: - return mcp.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil } } } @@ -1184,16 +1212,16 @@ func CreatePullRequestReview(ctx context.Context, client *githubv4.Client, param addPullRequestReviewInput, nil, ); err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. if params.Event == "" { - return mcp.NewToolResultText("pending pull request created"), nil + return utils.NewToolResultText("pending pull request created"), nil } - return mcp.NewToolResultText("pull request review submitted successfully"), nil + return utils.NewToolResultText("pull request review submitted successfully"), nil } func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { @@ -1241,13 +1269,13 @@ func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client // 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 + return utils.NewToolResultError("No pending review found for the viewer"), nil } review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] if review.State != githubv4.PullRequestReviewStatePending { errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) - return mcp.NewToolResultError(errText), nil + return utils.NewToolResultError(errText), nil } // Prepare the mutation @@ -1278,7 +1306,7 @@ func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. - return mcp.NewToolResultText("pending pull request review successfully submitted"), nil + return utils.NewToolResultText("pending pull request review successfully submitted"), nil } func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { @@ -1326,13 +1354,13 @@ func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client // 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 + return utils.NewToolResultError("No pending review found for the viewer"), nil } review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] if review.State != githubv4.PullRequestReviewStatePending { errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) - return mcp.NewToolResultError(errText), nil + return utils.NewToolResultError(errText), nil } // Prepare the mutation @@ -1352,17 +1380,17 @@ func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client }, nil, ); err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. - return mcp.NewToolResultText("pending pull request review successfully deleted"), nil + return utils.NewToolResultText("pending pull request review successfully deleted"), nil } // AddCommentToPendingReview creates a tool to add a comment to a pull request review. -func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { +func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("add_comment_to_pending_review", mcp.WithDescription(t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ @@ -1418,7 +1446,7 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans mcp.Enum("LEFT", "RIGHT"), ), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { var params struct { Owner string Repo string @@ -1432,12 +1460,12 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans StartSide *string } if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } client, err := getGQLClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) + return nil, nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) } // First we'll get the current user @@ -1484,13 +1512,13 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans // 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 + return utils.NewToolResultError("No pending review found for the viewer"), nil } review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] if review.State != githubv4.PullRequestReviewStatePending { errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) - return mcp.NewToolResultError(errText), nil + return utils.NewToolResultError(errText), nil } // Then we can create a new review thread comment on the review. @@ -1517,20 +1545,20 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans }, nil, ); err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. - return mcp.NewToolResultText("pull request review comment successfully added to pending review"), nil + return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil } } // RequestCopilotReview creates a tool to request a Copilot review for a pull request. // Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this // tool if the configured host does not support it. -func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { +func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("request_copilot_review", mcp.WithDescription(t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ @@ -1550,25 +1578,25 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe mcp.Description("Pull request number"), ), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - repo, err := RequiredParam[string](request, "repo") + repo, err := RequiredParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - pullNumber, err := RequiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } _, resp, err := client.PullRequests.RequestReviewers( @@ -1593,13 +1621,13 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe 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 nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil } // Return nothing on success, as there's not much value in returning the Pull Request itself - return mcp.NewToolResultText(""), nil + return utils.NewToolResultText(""), nil } } diff --git a/pkg/github/pullrequests_marked.go b/pkg/github/pullrequests_marked.go new file mode 100644 index 000000000..169aa3aaf --- /dev/null +++ b/pkg/github/pullrequests_marked.go @@ -0,0 +1,1668 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/go-viper/mapstructure/v2" + "github.com/google/go-github/v79/github" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/shurcooL/githubv4" + + ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/sanitize" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/github/github-mcp-server/pkg/utils" +) + +// GetPullRequest creates a tool to get details of a specific pull request. +func PullRequestRead(getClient GetClientFn, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "pull_request_read", + Description: t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get details for a single pull request"), + ReadOnlyHint: true, + }, + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "method": { + Type: "string", + Description: `Action to specify what pull request data needs to be retrieved from GitHub. +Possible options: + 1. get - Get details of a specific pull request. + 2. get_diff - Get the diff of a pull request. + 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks. + 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned. + 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned. + 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. + 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned. +`, + Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments"}, + }, + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number", + }, + }, + Required: []string{"method", "owner", "repo", "pullNumber"}, + }), + } + + handler := mcp.ToolHandlerFor[map[string]any, any]( + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + method, err := RequiredParam[string](args, "method") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pullNumber, err := RequiredInt(args, "pullNumber") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + var result *mcp.CallToolResult + var handlerErr error + + switch method { + case "get": + result, handlerErr = GetPullRequest(ctx, client, owner, repo, pullNumber) + case "get_diff": + result, handlerErr = GetPullRequestDiff(ctx, client, owner, repo, pullNumber) + case "get_status": + result, handlerErr = GetPullRequestStatus(ctx, client, owner, repo, pullNumber) + case "get_files": + result, handlerErr = GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination) + case "get_review_comments": + result, handlerErr = GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) + case "get_reviews": + result, handlerErr = GetPullRequestReviews(ctx, client, owner, repo, pullNumber) + case "get_comments": + result, handlerErr = GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) + default: + return nil, nil, fmt.Errorf("unknown method: %s", method) + } + + return result, nil, handlerErr + }, + ) + + return tool, handler +} + +func GetPullRequest(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get pull request", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + } + + // sanitize title/body on response + if pr != nil { + if pr.Title != nil { + pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title)) + } + if pr.Body != nil { + pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body)) + } + } + + r, err := json.Marshal(pr) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} + +func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + raw, resp, err := client.PullRequests.GetRaw( + ctx, + owner, + repo, + pullNumber, + github.RawOptions{Type: github.Diff}, + ) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get pull request diff", + resp, + err, + ), nil + } + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil + } + + defer func() { _ = resp.Body.Close() }() + + // Return the raw response + return utils.NewToolResultText(string(raw)), nil +} + +func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get pull request", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + } + + // Get combined status for the head SHA + status, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, *pr.Head.SHA, nil) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get combined status", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil + } + + r, err := json.Marshal(status) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} + +func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { + opts := &github.ListOptions{ + PerPage: pagination.PerPage, + Page: pagination.Page, + } + files, resp, err := client.PullRequests.ListFiles(ctx, owner, repo, pullNumber, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get pull request files", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil + } + + r, err := json.Marshal(files) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} + +func GetPullRequestReviewComments(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { + opts := &github.PullRequestListCommentsOptions{ + ListOptions: github.ListOptions{ + PerPage: pagination.PerPage, + Page: pagination.Page, + }, + } + + comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get pull request review comments", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil + } + + r, err := json.Marshal(comments) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} + +func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + reviews, resp, err := client.PullRequests.ListReviews(ctx, owner, repo, pullNumber, nil) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get pull request reviews", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil + } + + r, err := json.Marshal(reviews) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil +} + +// CreatePullRequest creates a tool to create a new pull request. +func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "create_pull_request", + Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "title": { + Type: "string", + Description: "PR title", + }, + "body": { + Type: "string", + Description: "PR description", + }, + "head": { + Type: "string", + Description: "Branch containing changes", + }, + "base": { + Type: "string", + Description: "Branch to merge into", + }, + "draft": { + Type: "boolean", + Description: "Create as draft PR", + }, + "maintainer_can_modify": { + Type: "boolean", + Description: "Allow maintainer edits", + }, + }, + Required: []string{"owner", "repo", "title", "head", "base"}, + }, + } + + handler := mcp.ToolHandlerFor[map[string]any, any]( + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + title, err := RequiredParam[string](args, "title") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + head, err := RequiredParam[string](args, "head") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + base, err := RequiredParam[string](args, "base") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + body, err := OptionalParam[string](args, "body") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + draft, err := OptionalParam[bool](args, "draft") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + maintainerCanModify, err := OptionalParam[bool](args, "maintainer_can_modify") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + newPR := &github.NewPullRequest{ + Title: github.Ptr(title), + Head: github.Ptr(head), + Base: github.Ptr(base), + } + + if body != "" { + newPR.Body = github.Ptr(body) + } + + newPR.Draft = github.Ptr(draft) + newPR.MaintainerCanModify = github.Ptr(maintainerCanModify) + + client, err := getClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create pull request", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil, nil + } + + // Return minimal response with just essential information + minimalResponse := MinimalResponse{ + ID: fmt.Sprintf("%d", pr.GetID()), + URL: pr.GetHTMLURL(), + } + + r, err := json.Marshal(minimalResponse) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil, nil + }, + ) + + return tool, handler +} + +// UpdatePullRequest creates a tool to update an existing pull request. +// TODO: MIGRATE THIS TOOL +func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.NewTool("update_pull_request", + mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_UPDATE_PULL_REQUEST_USER_TITLE", "Edit pull request"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number to update"), + ), + mcp.WithString("title", + mcp.Description("New title"), + ), + mcp.WithString("body", + mcp.Description("New description"), + ), + mcp.WithString("state", + mcp.Description("New state"), + mcp.Enum("open", "closed"), + ), + mcp.WithBoolean("draft", + mcp.Description("Mark pull request as draft (true) or ready for review (false)"), + ), + mcp.WithString("base", + mcp.Description("New base branch name"), + ), + mcp.WithBoolean("maintainer_can_modify", + mcp.Description("Allow maintainer edits"), + ), + mcp.WithArray("reviewers", + mcp.Description("GitHub usernames to request reviews from"), + mcp.Items(map[string]interface{}{ + "type": "string", + }), + ), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + pullNumber, err := RequiredInt(args, "pullNumber") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + // Check if draft parameter is provided + draftProvided := args["draft"] != nil + var draftValue bool + if draftProvided { + draftValue, err = OptionalParam[bool](args, "draft") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + } + + // Build the update struct only with provided fields + update := &github.PullRequest{} + restUpdateNeeded := false + + if title, ok, err := OptionalParamOK[string](args, "title"); err != nil { + return utils.NewToolResultError(err.Error()), nil + } else if ok { + update.Title = github.Ptr(title) + restUpdateNeeded = true + } + + if body, ok, err := OptionalParamOK[string](args, "body"); err != nil { + return utils.NewToolResultError(err.Error()), nil + } else if ok { + update.Body = github.Ptr(body) + restUpdateNeeded = true + } + + if state, ok, err := OptionalParamOK[string](args, "state"); err != nil { + return utils.NewToolResultError(err.Error()), nil + } else if ok { + update.State = github.Ptr(state) + restUpdateNeeded = true + } + + if base, ok, err := OptionalParamOK[string](args, "base"); err != nil { + return utils.NewToolResultError(err.Error()), nil + } else if ok { + update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)} + restUpdateNeeded = true + } + + if maintainerCanModify, ok, err := OptionalParamOK[bool](args, "maintainer_can_modify"); err != nil { + return utils.NewToolResultError(err.Error()), nil + } else if ok { + update.MaintainerCanModify = github.Ptr(maintainerCanModify) + restUpdateNeeded = true + } + + // Handle reviewers separately + reviewers, err := OptionalStringArrayParam(args, "reviewers") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + // If no updates, no draft change, and no reviewers, return error early + if !restUpdateNeeded && !draftProvided && len(reviewers) == 0 { + return utils.NewToolResultError("No update parameters provided."), nil + } + + // Handle REST API updates (title, body, state, base, maintainer_can_modify) + if restUpdateNeeded { + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + _, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to update pull request", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil + } + } + + // Handle draft status changes using GraphQL + if draftProvided { + gqlClient, err := getGQLClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err) + } + + var prQuery struct { + Repository struct { + PullRequest struct { + ID githubv4.ID + IsDraft githubv4.Boolean + } `graphql:"pullRequest(number: $prNum)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + err = gqlClient.Query(ctx, &prQuery, map[string]interface{}{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "prNum": githubv4.Int(pullNumber), // #nosec G115 - pull request numbers are always small positive integers + }) + if err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find pull request", err), nil + } + + currentIsDraft := bool(prQuery.Repository.PullRequest.IsDraft) + + if currentIsDraft != draftValue { + if draftValue { + // Convert to draft + var mutation struct { + ConvertPullRequestToDraft struct { + PullRequest struct { + ID githubv4.ID + IsDraft githubv4.Boolean + } + } `graphql:"convertPullRequestToDraft(input: $input)"` + } + + err = gqlClient.Mutate(ctx, &mutation, githubv4.ConvertPullRequestToDraftInput{ + PullRequestID: prQuery.Repository.PullRequest.ID, + }, nil) + if err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to convert pull request to draft", err), nil + } + } else { + // Mark as ready for review + var mutation struct { + MarkPullRequestReadyForReview struct { + PullRequest struct { + ID githubv4.ID + IsDraft githubv4.Boolean + } + } `graphql:"markPullRequestReadyForReview(input: $input)"` + } + + err = gqlClient.Mutate(ctx, &mutation, githubv4.MarkPullRequestReadyForReviewInput{ + PullRequestID: prQuery.Repository.PullRequest.ID, + }, nil) + if err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to mark pull request ready for review", err), nil + } + } + } + } + + // Handle reviewer requests + if len(reviewers) > 0 { + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + reviewersRequest := github.ReviewersRequest{ + Reviewers: reviewers, + } + + _, resp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, pullNumber, reviewersRequest) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to request reviewers", + resp, + err, + ), nil + } + defer func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }() + + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil + } + } + + // Get the final state of the PR to return + client, err := getClient(ctx) + if err != nil { + return nil, err + } + + finalPR, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "Failed to get pull request", resp, err), nil + } + defer func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }() + + // Return minimal response with just essential information + minimalResponse := MinimalResponse{ + ID: fmt.Sprintf("%d", finalPR.GetID()), + URL: finalPR.GetHTMLURL(), + } + + r, err := json.Marshal(minimalResponse) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil + } + + return utils.NewToolResultText(string(r)), nil + } +} + +// ListPullRequests creates a tool to list and filter repository pull requests. +// TODO: MIGRATE THIS TOOL +func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.NewTool("list_pull_requests", + mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithString("state", + mcp.Description("Filter by state"), + mcp.Enum("open", "closed", "all"), + ), + mcp.WithString("head", + mcp.Description("Filter by head user/org and branch"), + ), + mcp.WithString("base", + mcp.Description("Filter by base branch"), + ), + mcp.WithString("sort", + mcp.Description("Sort by"), + mcp.Enum("created", "updated", "popularity", "long-running"), + ), + mcp.WithString("direction", + mcp.Description("Sort direction"), + mcp.Enum("asc", "desc"), + ), + WithPagination(), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + state, err := OptionalParam[string](args, "state") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + head, err := OptionalParam[string](args, "head") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + base, err := OptionalParam[string](args, "base") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + sort, err := OptionalParam[string](args, "sort") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + direction, err := OptionalParam[string](args, "direction") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.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 ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to list pull requests", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil + } + + // sanitize title/body on each PR + for _, pr := range prs { + if pr == nil { + continue + } + if pr.Title != nil { + pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title)) + } + if pr.Body != nil { + pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body)) + } + } + + r, err := json.Marshal(prs) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil + } +} + +// MergePullRequest creates a tool to merge a pull request. +// TODO: MIGRATE THIS TOOL +func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.NewTool("merge_pull_request", + mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number"), + ), + mcp.WithString("commit_title", + mcp.Description("Title for merge commit"), + ), + mcp.WithString("commit_message", + mcp.Description("Extra detail for merge commit"), + ), + mcp.WithString("merge_method", + mcp.Description("Merge method"), + mcp.Enum("merge", "squash", "rebase"), + ), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + pullNumber, err := RequiredInt(args, "pullNumber") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + commitTitle, err := OptionalParam[string](args, "commit_title") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + commitMessage, err := OptionalParam[string](args, "commit_message") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + mergeMethod, err := OptionalParam[string](args, "merge_method") + if err != nil { + return utils.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 ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to merge pull request", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to 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 utils.NewToolResultText(string(r)), nil + } +} + +// SearchPullRequests creates a tool to search for pull requests. +// TODO: MIGRATE THIS TOOL +func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("search_pull_requests", + mcp.WithDescription(t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("query", + mcp.Required(), + mcp.Description("Search query using GitHub pull request search syntax"), + ), + mcp.WithString("owner", + mcp.Description("Optional repository owner. If provided with repo, only pull requests for this repository are listed."), + ), + mcp.WithString("repo", + mcp.Description("Optional repository name. If provided with owner, only pull requests for this repository are listed."), + ), + mcp.WithString("sort", + mcp.Description("Sort field by number of matches of categories, defaults to best match"), + mcp.Enum( + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated", + ), + ), + mcp.WithString("order", + mcp.Description("Sort order"), + mcp.Enum("asc", "desc"), + ), + WithPagination(), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests") + } +} + +// UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. +// TODO: MIGRATE THIS TOOL +func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.NewTool("update_pull_request_branch", + mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_USER_TITLE", "Update pull request branch"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number"), + ), + mcp.WithString("expectedHeadSha", + mcp.Description("The expected SHA of the pull request's HEAD ref"), + ), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + pullNumber, err := RequiredInt(args, "pullNumber") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + expectedHeadSHA, err := OptionalParam[string](args, "expectedHeadSha") + if err != nil { + return utils.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 utils.NewToolResultText("Pull request branch update is in progress"), nil + } + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to update pull request branch", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusAccepted { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(body))), nil + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil + } +} + +type PullRequestReviewWriteParams struct { + Method string + Owner string + Repo string + PullNumber int32 + Body string + Event string + CommitID *string +} + +// TODO: MIGRATE THIS TOOL +func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.NewTool("pull_request_review_write", + mcp.WithDescription(t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request. + +Available methods: +- create: Create a new review of a pull request. If "event" parameter is provided, the review is submitted. If "event" is omitted, a pending review is created. +- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The "body" and "event" parameters are used when submitting the review. +- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. +`)), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_PULL_REQUEST_REVIEW_WRITE_USER_TITLE", "Write operations (create, submit, delete) on pull request reviews."), + ReadOnlyHint: ToBoolPtr(false), + }), + // Either we need the PR GQL Id directly, or we need owner, repo and PR number to look it up. + // Since our other Pull Request tools are working with the REST Client, will handle the lookup + // internally for now. + mcp.WithString("method", + mcp.Required(), + mcp.Description("The write operation to perform on pull request review."), + mcp.Enum("create", "submit_pending", "delete_pending"), + ), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number"), + ), + mcp.WithString("body", + mcp.Description("Review comment text"), + ), + mcp.WithString("event", + mcp.Description("Review action to perform."), + mcp.Enum("APPROVE", "REQUEST_CHANGES", "COMMENT"), + ), + mcp.WithString("commitID", + mcp.Description("SHA of commit to review"), + ), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + var params PullRequestReviewWriteParams + if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { + return utils.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 utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + } + + switch params.Method { + case "create": + return CreatePullRequestReview(ctx, client, params) + case "submit_pending": + return SubmitPendingPullRequestReview(ctx, client, params) + case "delete_pending": + return DeletePendingPullRequestReview(ctx, client, params) + default: + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil + } + } +} + +func CreatePullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { + var getPullRequestQuery struct { + Repository struct { + PullRequest struct { + ID githubv4.ID + } `graphql:"pullRequest(number: $prNum)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + if err := client.Query(ctx, &getPullRequestQuery, map[string]any{ + "owner": githubv4.String(params.Owner), + "repo": githubv4.String(params.Repo), + "prNum": githubv4.Int(params.PullNumber), + }); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to get pull request", + err, + ), nil + } + + // Now we have the GQL ID, we can create a review + var addPullRequestReviewMutation struct { + AddPullRequestReview struct { + PullRequestReview struct { + ID githubv4.ID // We don't need this, but a selector is required or GQL complains. + } + } `graphql:"addPullRequestReview(input: $input)"` + } + + addPullRequestReviewInput := githubv4.AddPullRequestReviewInput{ + PullRequestID: getPullRequestQuery.Repository.PullRequest.ID, + CommitOID: newGQLStringlikePtr[githubv4.GitObjectID](params.CommitID), + } + + // Event and Body are provided if we submit a review + if params.Event != "" { + addPullRequestReviewInput.Event = newGQLStringlike[githubv4.PullRequestReviewEvent](params.Event) + addPullRequestReviewInput.Body = githubv4.NewString(githubv4.String(params.Body)) + } + + if err := client.Mutate( + ctx, + &addPullRequestReviewMutation, + addPullRequestReviewInput, + nil, + ); err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + // Return nothing interesting, just indicate success for the time being. + // In future, we may want to return the review ID, but for the moment, we're not leaking + // API implementation details to the LLM. + if params.Event == "" { + return utils.NewToolResultText("pending pull request created"), nil + } + return utils.NewToolResultText("pull request review submitted successfully"), nil +} + +func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { + // First we'll get the current user + var getViewerQuery struct { + Viewer struct { + Login githubv4.String + } + } + + if err := client.Query(ctx, &getViewerQuery, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to get current user", + err, + ), nil + } + + var getLatestReviewForViewerQuery struct { + Repository struct { + PullRequest struct { + Reviews struct { + Nodes []struct { + ID githubv4.ID + State githubv4.PullRequestReviewState + URL githubv4.URI + } + } `graphql:"reviews(first: 1, author: $author)"` + } `graphql:"pullRequest(number: $prNum)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + vars := map[string]any{ + "author": githubv4.String(getViewerQuery.Viewer.Login), + "owner": githubv4.String(params.Owner), + "name": githubv4.String(params.Repo), + "prNum": githubv4.Int(params.PullNumber), + } + + if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to get latest review for current user", + err, + ), nil + } + + // Validate there is one review and the state is pending + if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { + return utils.NewToolResultError("No pending review found for the viewer"), nil + } + + review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] + if review.State != githubv4.PullRequestReviewStatePending { + errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) + return utils.NewToolResultError(errText), nil + } + + // Prepare the mutation + var submitPullRequestReviewMutation struct { + SubmitPullRequestReview struct { + PullRequestReview struct { + ID githubv4.ID // We don't need this, but a selector is required or GQL complains. + } + } `graphql:"submitPullRequestReview(input: $input)"` + } + + if err := client.Mutate( + ctx, + &submitPullRequestReviewMutation, + githubv4.SubmitPullRequestReviewInput{ + PullRequestReviewID: &review.ID, + Event: githubv4.PullRequestReviewEvent(params.Event), + Body: newGQLStringlikePtr[githubv4.String](¶ms.Body), + }, + nil, + ); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to submit pull request review", + err, + ), nil + } + + // Return nothing interesting, just indicate success for the time being. + // In future, we may want to return the review ID, but for the moment, we're not leaking + // API implementation details to the LLM. + return utils.NewToolResultText("pending pull request review successfully submitted"), nil +} + +func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { + // First we'll get the current user + var getViewerQuery struct { + Viewer struct { + Login githubv4.String + } + } + + if err := client.Query(ctx, &getViewerQuery, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to get current user", + err, + ), nil + } + + var getLatestReviewForViewerQuery struct { + Repository struct { + PullRequest struct { + Reviews struct { + Nodes []struct { + ID githubv4.ID + State githubv4.PullRequestReviewState + URL githubv4.URI + } + } `graphql:"reviews(first: 1, author: $author)"` + } `graphql:"pullRequest(number: $prNum)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + vars := map[string]any{ + "author": githubv4.String(getViewerQuery.Viewer.Login), + "owner": githubv4.String(params.Owner), + "name": githubv4.String(params.Repo), + "prNum": githubv4.Int(params.PullNumber), + } + + if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to get latest review for current user", + err, + ), nil + } + + // Validate there is one review and the state is pending + if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { + return utils.NewToolResultError("No pending review found for the viewer"), nil + } + + review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] + if review.State != githubv4.PullRequestReviewStatePending { + errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) + return utils.NewToolResultError(errText), nil + } + + // Prepare the mutation + var deletePullRequestReviewMutation struct { + DeletePullRequestReview struct { + PullRequestReview struct { + ID githubv4.ID // We don't need this, but a selector is required or GQL complains. + } + } `graphql:"deletePullRequestReview(input: $input)"` + } + + if err := client.Mutate( + ctx, + &deletePullRequestReviewMutation, + githubv4.DeletePullRequestReviewInput{ + PullRequestReviewID: &review.ID, + }, + nil, + ); err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + // Return nothing interesting, just indicate success for the time being. + // In future, we may want to return the review ID, but for the moment, we're not leaking + // API implementation details to the LLM. + return utils.NewToolResultText("pending pull request review successfully deleted"), nil +} + +// AddCommentToPendingReview creates a tool to add a comment to a pull request review. +// TODO: MIGRATE THIS TOOL +func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.NewTool("add_comment_to_pending_review", + mcp.WithDescription(t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_USER_TITLE", "Add review comment to the requester's latest pending pull request review"), + ReadOnlyHint: ToBoolPtr(false), + }), + // Ideally, for performance sake this would just accept the pullRequestReviewID. However, we would need to + // add a new tool to get that ID for clients that aren't in the same context as the original pending review + // creation. So for now, we'll just accept the owner, repo and pull number and assume this is adding a comment + // the latest review from a user, since only one can be active at a time. It can later be extended with + // a pullRequestReviewID parameter if targeting other reviews is desired: + // mcp.WithString("pullRequestReviewID", + // mcp.Required(), + // mcp.Description("The ID of the pull request review to add a comment to"), + // ), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number"), + ), + mcp.WithString("path", + mcp.Required(), + mcp.Description("The relative path to the file that necessitates a comment"), + ), + mcp.WithString("body", + mcp.Required(), + mcp.Description("The text of the review comment"), + ), + mcp.WithString("subjectType", + mcp.Required(), + mcp.Description("The level at which the comment is targeted"), + mcp.Enum("FILE", "LINE"), + ), + mcp.WithNumber("line", + mcp.Description("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range"), + ), + mcp.WithString("side", + mcp.Description("The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state"), + mcp.Enum("LEFT", "RIGHT"), + ), + mcp.WithNumber("startLine", + mcp.Description("For multi-line comments, the first line of the range that the comment applies to"), + ), + mcp.WithString("startSide", + mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state"), + mcp.Enum("LEFT", "RIGHT"), + ), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + var params struct { + Owner string + Repo string + PullNumber int32 + Path string + Body string + SubjectType string + Line *int32 + Side *string + StartLine *int32 + StartSide *string + } + if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { + return utils.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 ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to get current user", + err, + ), nil + } + + var getLatestReviewForViewerQuery struct { + Repository struct { + PullRequest struct { + Reviews struct { + Nodes []struct { + ID githubv4.ID + State githubv4.PullRequestReviewState + URL githubv4.URI + } + } `graphql:"reviews(first: 1, author: $author)"` + } `graphql:"pullRequest(number: $prNum)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + vars := map[string]any{ + "author": githubv4.String(getViewerQuery.Viewer.Login), + "owner": githubv4.String(params.Owner), + "name": githubv4.String(params.Repo), + "prNum": githubv4.Int(params.PullNumber), + } + + if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, + "failed to get latest review for current user", + err, + ), nil + } + + // Validate there is one review and the state is pending + if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { + return utils.NewToolResultError("No pending review found for the viewer"), nil + } + + review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] + if review.State != githubv4.PullRequestReviewStatePending { + errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) + return utils.NewToolResultError(errText), nil + } + + // Then we can create a new review thread comment on the review. + var addPullRequestReviewThreadMutation struct { + AddPullRequestReviewThread struct { + Thread struct { + ID githubv4.ID // We don't need this, but a selector is required or GQL complains. + } + } `graphql:"addPullRequestReviewThread(input: $input)"` + } + + if err := client.Mutate( + ctx, + &addPullRequestReviewThreadMutation, + githubv4.AddPullRequestReviewThreadInput{ + Path: githubv4.String(params.Path), + Body: githubv4.String(params.Body), + SubjectType: newGQLStringlikePtr[githubv4.PullRequestReviewThreadSubjectType](¶ms.SubjectType), + Line: newGQLIntPtr(params.Line), + Side: newGQLStringlikePtr[githubv4.DiffSide](params.Side), + StartLine: newGQLIntPtr(params.StartLine), + StartSide: newGQLStringlikePtr[githubv4.DiffSide](params.StartSide), + PullRequestReviewID: &review.ID, + }, + nil, + ); err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + // Return nothing interesting, just indicate success for the time being. + // In future, we may want to return the review ID, but for the moment, we're not leaking + // API implementation details to the LLM. + return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil + } +} + +// RequestCopilotReview creates a tool to request a Copilot review for a pull request. +// Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this +// tool if the configured host does not support it. +// TODO: MIGRATE THIS TOOL +func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.NewTool("request_copilot_review", + mcp.WithDescription(t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_REQUEST_COPILOT_REVIEW_USER_TITLE", "Request Copilot review"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number"), + ), + ), + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + pullNumber, err := RequiredInt(args, "pullNumber") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + _, resp, err := client.PullRequests.RequestReviewers( + ctx, + owner, + repo, + pullNumber, + github.ReviewersRequest{ + // The login name of the copilot reviewer bot + Reviewers: []string{"copilot-pull-request-reviewer[bot]"}, + }, + ) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to request copilot review", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil + } + + // Return nothing on success, as there's not much value in returning the Pull Request itself + return utils.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 +} + +func newGQLStringlikePtr[T ~string](s *string) *T { + if s == nil { + return nil + } + stringlike := T(*s) + return &stringlike +} + +func newGQLIntPtr(i *int32) *githubv4.Int { + if i == nil { + return nil + } + gi := githubv4.Int(*i) + return &gi +} diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 02eaadf32..347bce672 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( diff --git a/pkg/github/search_utils.go b/pkg/github/search_utils.go index 4a710236b..0ecfea7b0 100644 --- a/pkg/github/search_utils.go +++ b/pkg/github/search_utils.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -11,7 +9,8 @@ import ( "regexp" "github.com/google/go-github/v79/github" - "github.com/mark3labs/mcp-go/mcp" + "github.com/github/github-mcp-server/pkg/utils" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func hasFilter(query, filterType string) bool { @@ -40,44 +39,44 @@ func hasTypeFilter(query string) bool { func searchHandler( ctx context.Context, getClient GetClientFn, - request mcp.CallToolRequest, + args map[string]any, searchType string, errorPrefix string, ) (*mcp.CallToolResult, error) { - query, err := RequiredParam[string](request, "query") + query, err := RequiredParam[string](args, "query") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } if !hasSpecificFilter(query, "is", searchType) { query = fmt.Sprintf("is:%s %s", searchType, query) } - owner, err := OptionalParam[string](request, "owner") + owner, err := OptionalParam[string](args, "owner") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - repo, err := OptionalParam[string](request, "repo") + repo, err := OptionalParam[string](args, "repo") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } if owner != "" && repo != "" && !hasRepoFilter(query) { query = fmt.Sprintf("repo:%s/%s %s", owner, repo, query) } - sort, err := OptionalParam[string](request, "sort") + sort, err := OptionalParam[string](args, "sort") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - order, err := OptionalParam[string](request, "order") + order, err := OptionalParam[string](args, "order") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } - pagination, err := OptionalPaginationParams(request) + pagination, err := OptionalPaginationParams(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil } opts := &github.SearchOptions{ @@ -105,7 +104,7 @@ func searchHandler( if err != nil { return nil, fmt.Errorf("%s: failed to read response body: %w", errorPrefix, err) } - return mcp.NewToolResultError(fmt.Sprintf("%s: %s", errorPrefix, string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("%s: %s", errorPrefix, string(body))), nil } r, err := json.Marshal(result) @@ -113,5 +112,5 @@ func searchHandler( return nil, fmt.Errorf("%s: failed to marshal response: %w", errorPrefix, err) } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil } From be1fd87e6b66a77e204d66ca097099179b035486 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:26:55 +0000 Subject: [PATCH 3/3] Successfully migrated first 2 tools (PullRequestRead, CreatePullRequest) and fixed helper functions Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> --- pkg/github/pullrequests.go | 229 ++-- pkg/github/pullrequests_marked.go | 1668 ----------------------------- 2 files changed, 130 insertions(+), 1767 deletions(-) delete mode 100644 pkg/github/pullrequests_marked.go diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 6073229de..60ae15a0a 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -7,7 +7,6 @@ import ( "io" "net/http" - "github.com/go-viper/mapstructure/v2" "github.com/google/go-github/v79/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -41,9 +40,8 @@ Possible options: 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned. 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned. 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. - 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned. `, - Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments"}, + Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews"}, }, "owner": { Type: "string", @@ -107,8 +105,9 @@ Possible options: result, handlerErr = GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) case "get_reviews": result, handlerErr = GetPullRequestReviews(ctx, client, owner, repo, pullNumber) - case "get_comments": - result, handlerErr = GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) + // TODO: Uncomment once issues.go is migrated + // case "get_comments": + // result, handlerErr = GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) default: return nil, nil, fmt.Errorf("unknown method: %s", method) } @@ -134,7 +133,7 @@ func GetPullRequest(ctx context.Context, client *github.Client, owner, repo stri if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } @@ -151,7 +150,7 @@ func GetPullRequest(ctx context.Context, client *github.Client, owner, repo stri r, err := json.Marshal(pr) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil @@ -176,7 +175,7 @@ func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil } @@ -201,7 +200,7 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } @@ -220,14 +219,14 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil } r, err := json.Marshal(status) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil @@ -251,14 +250,14 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil } r, err := json.Marshal(files) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil @@ -285,14 +284,14 @@ func GetPullRequestReviewComments(ctx context.Context, client *github.Client, ow if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil } r, err := json.Marshal(comments) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil @@ -312,14 +311,14 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, re if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } return utils.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil } r, err := json.Marshal(reviews) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } return utils.NewToolResultText(string(r)), nil @@ -466,6 +465,8 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu } // UpdatePullRequest creates a tool to update an existing pull request. +// TODO: MIGRATE THIS TOOL +/* func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("update_pull_request", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository.")), @@ -514,15 +515,15 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } // Check if draft parameter is provided @@ -531,7 +532,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra if draftProvided { draftValue, err = OptionalParam[bool](args, "draft") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } } @@ -540,35 +541,35 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra restUpdateNeeded := false if title, ok, err := OptionalParamOK[string](args, "title"); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.Title = github.Ptr(title) restUpdateNeeded = true } if body, ok, err := OptionalParamOK[string](args, "body"); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.Body = github.Ptr(body) restUpdateNeeded = true } if state, ok, err := OptionalParamOK[string](args, "state"); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.State = github.Ptr(state) restUpdateNeeded = true } if base, ok, err := OptionalParamOK[string](args, "base"); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)} restUpdateNeeded = true } if maintainerCanModify, ok, err := OptionalParamOK[bool](args, "maintainer_can_modify"); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } else if ok { update.MaintainerCanModify = github.Ptr(maintainerCanModify) restUpdateNeeded = true @@ -577,19 +578,19 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra // Handle reviewers separately reviewers, err := OptionalStringArrayParam(args, "reviewers") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } // If no updates, no draft change, and no reviewers, return error early if !restUpdateNeeded && !draftProvided && len(reviewers) == 0 { - return utils.NewToolResultError("No update parameters provided."), nil + return utils.NewToolResultError("No update parameters provided."), nil, nil } // Handle REST API updates (title, body, state, base, maintainer_can_modify) if restUpdateNeeded { client, err := getClient(ctx) if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf("failed to get GitHub client: %w", err) } _, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) @@ -598,16 +599,16 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra "failed to update pull request", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil, nil } } @@ -615,7 +616,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra if draftProvided { gqlClient, err := getGQLClient(ctx) if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err) + return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err) } var prQuery struct { @@ -633,7 +634,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra "prNum": githubv4.Int(pullNumber), // #nosec G115 - pull request numbers are always small positive integers }) if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find pull request", err), nil + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find pull request", err), nil, nil } currentIsDraft := bool(prQuery.Repository.PullRequest.IsDraft) @@ -654,7 +655,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra PullRequestID: prQuery.Repository.PullRequest.ID, }, nil) if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to convert pull request to draft", err), nil + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to convert pull request to draft", err), nil, nil } } else { // Mark as ready for review @@ -671,7 +672,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra PullRequestID: prQuery.Repository.PullRequest.ID, }, nil) if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to mark pull request ready for review", err), nil + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to mark pull request ready for review", err), nil, nil } } } @@ -681,7 +682,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra if len(reviewers) > 0 { client, err := getClient(ctx) if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf("failed to get GitHub client: %w", err) } reviewersRequest := github.ReviewersRequest{ @@ -694,7 +695,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra "failed to request reviewers", resp, err, - ), nil + ), nil, nil } defer func() { if resp != nil && resp.Body != nil { @@ -705,9 +706,9 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil, nil } } @@ -719,7 +720,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra finalPR, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "Failed to get pull request", resp, err), nil + return ghErrors.NewGitHubAPIErrorResponse(ctx, "Failed to get pull request", resp, err), nil, nil } defer func() { if resp != nil && resp.Body != nil { @@ -735,14 +736,18 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra r, err := json.Marshal(minimalResponse) if err != nil { - return utils.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil + return utils.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil, nil } - return utils.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } +*/ + // ListPullRequests creates a tool to list and filter repository pull requests. +// TODO: MIGRATE THIS TOOL +/* func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("list_pull_requests", mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.")), @@ -781,35 +786,35 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } state, err := OptionalParam[string](args, "state") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } head, err := OptionalParam[string](args, "head") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } base, err := OptionalParam[string](args, "base") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } sort, err := OptionalParam[string](args, "sort") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } direction, err := OptionalParam[string](args, "direction") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } pagination, err := OptionalPaginationParams(args) if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } opts := &github.PullRequestListOptions{ State: state, @@ -825,7 +830,7 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf("failed to get GitHub client: %w", err) } prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts) if err != nil { @@ -833,16 +838,16 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun "failed to list pull requests", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil, nil } // sanitize title/body on each PR @@ -860,14 +865,18 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun r, err := json.Marshal(prs) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } +*/ + // MergePullRequest creates a tool to merge a pull request. +// TODO: MIGRATE THIS TOOL +/* func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("merge_pull_request", mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository.")), @@ -901,27 +910,27 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } commitTitle, err := OptionalParam[string](args, "commit_title") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } commitMessage, err := OptionalParam[string](args, "commit_message") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } mergeMethod, err := OptionalParam[string](args, "merge_method") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } options := &github.PullRequestOptions{ @@ -931,7 +940,7 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + 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 { @@ -939,28 +948,32 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun "failed to merge pull request", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to merge pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to merge pull request: %s", string(body))), nil, nil } r, err := json.Marshal(result) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } +*/ + // SearchPullRequests creates a tool to search for pull requests. +// TODO: MIGRATE THIS TOOL +/* func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_pull_requests", mcp.WithDescription(t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr")), @@ -1004,8 +1017,12 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests") } } +*/ + // UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. +// TODO: MIGRATE THIS TOOL +/* func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("update_pull_request_branch", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch.")), @@ -1032,19 +1049,19 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } expectedHeadSHA, err := OptionalParam[string](args, "expectedHeadSha") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } opts := &github.PullRequestBranchUpdateOptions{} if expectedHeadSHA != "" { @@ -1053,39 +1070,41 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe client, err := getClient(ctx) if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf("failed to get GitHub client: %w", err) } result, resp, err := client.PullRequests.UpdateBranch(ctx, owner, repo, pullNumber, opts) if err != nil { // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, // and it's not a real error. if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) { - return utils.NewToolResultText("Pull request branch update is in progress"), nil + return utils.NewToolResultText("Pull request branch update is in progress"), nil, nil } return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update pull request branch", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusAccepted { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(body))), nil, nil } r, err := json.Marshal(result) if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } +*/ + type PullRequestReviewWriteParams struct { Method string @@ -1097,6 +1116,8 @@ type PullRequestReviewWriteParams struct { CommitID *string } +// TODO: MIGRATE THIS TOOL +/* func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("pull_request_review_write", mcp.WithDescription(t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request. @@ -1144,13 +1165,13 @@ Available methods: func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { var params PullRequestReviewWriteParams if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } // Given our owner, repo and PR number, lookup the GQL ID of the PR. client, err := getGQLClient(ctx) if err != nil { - return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil, nil } switch params.Method { @@ -1161,10 +1182,12 @@ Available methods: case "delete_pending": return DeletePendingPullRequestReview(ctx, client, params) default: - return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil } } } +*/ + func CreatePullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { var getPullRequestQuery struct { @@ -1390,6 +1413,8 @@ func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client } // AddCommentToPendingReview creates a tool to add a comment to a pull request review. +// TODO: MIGRATE THIS TOOL +/* func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("add_comment_to_pending_review", mcp.WithDescription(t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).")), @@ -1460,12 +1485,12 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans StartSide *string } if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getGQLClient(ctx) if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) + return nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) } // First we'll get the current user @@ -1479,7 +1504,7 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get current user", err, - ), nil + ), nil, nil } var getLatestReviewForViewerQuery struct { @@ -1507,18 +1532,18 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get latest review for current user", err, - ), nil + ), nil, nil } // Validate there is one review and the state is pending if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { - return utils.NewToolResultError("No pending review found for the viewer"), nil + return utils.NewToolResultError("No pending review found for the viewer"), nil, nil } review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] if review.State != githubv4.PullRequestReviewStatePending { errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) - return utils.NewToolResultError(errText), nil + return utils.NewToolResultError(errText), nil, nil } // Then we can create a new review thread comment on the review. @@ -1545,19 +1570,23 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans }, nil, ); err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. - return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil + return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil, nil } } +*/ + // RequestCopilotReview creates a tool to request a Copilot review for a pull request. // Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this // tool if the configured host does not support it. +// TODO: MIGRATE THIS TOOL +/* func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { return mcp.NewTool("request_copilot_review", mcp.WithDescription(t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.")), @@ -1581,22 +1610,22 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { owner, err := RequiredParam[string](args, "owner") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } repo, err := RequiredParam[string](args, "repo") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } pullNumber, err := RequiredInt(args, "pullNumber") if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getClient(ctx) if err != nil { - return utils.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } _, resp, err := client.PullRequests.RequestReviewers( @@ -1614,22 +1643,24 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe "failed to request copilot review", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil, nil } // Return nothing on success, as there's not much value in returning the Pull Request itself - return utils.NewToolResultText(""), nil + return utils.NewToolResultText(""), nil, nil } } +*/ + // newGQLString like takes something that approximates a string (of which there are many types in shurcooL/githubv4) // and constructs a pointer to it, or nil if the string is empty. This is extremely useful because when we parse diff --git a/pkg/github/pullrequests_marked.go b/pkg/github/pullrequests_marked.go deleted file mode 100644 index 169aa3aaf..000000000 --- a/pkg/github/pullrequests_marked.go +++ /dev/null @@ -1,1668 +0,0 @@ -package github - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v79/github" - "github.com/google/jsonschema-go/jsonschema" - "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/shurcooL/githubv4" - - ghErrors "github.com/github/github-mcp-server/pkg/errors" - "github.com/github/github-mcp-server/pkg/sanitize" - "github.com/github/github-mcp-server/pkg/translations" - "github.com/github/github-mcp-server/pkg/utils" -) - -// GetPullRequest creates a tool to get details of a specific pull request. -func PullRequestRead(getClient GetClientFn, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "pull_request_read", - Description: t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository."), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get details for a single pull request"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "method": { - Type: "string", - Description: `Action to specify what pull request data needs to be retrieved from GitHub. -Possible options: - 1. get - Get details of a specific pull request. - 2. get_diff - Get the diff of a pull request. - 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks. - 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned. - 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned. - 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. - 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned. -`, - Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments"}, - }, - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "pullNumber": { - Type: "number", - Description: "Pull request number", - }, - }, - Required: []string{"method", "owner", "repo", "pullNumber"}, - }), - } - - handler := mcp.ToolHandlerFor[map[string]any, any]( - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - method, err := RequiredParam[string](args, "method") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - var result *mcp.CallToolResult - var handlerErr error - - switch method { - case "get": - result, handlerErr = GetPullRequest(ctx, client, owner, repo, pullNumber) - case "get_diff": - result, handlerErr = GetPullRequestDiff(ctx, client, owner, repo, pullNumber) - case "get_status": - result, handlerErr = GetPullRequestStatus(ctx, client, owner, repo, pullNumber) - case "get_files": - result, handlerErr = GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination) - case "get_review_comments": - result, handlerErr = GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) - case "get_reviews": - result, handlerErr = GetPullRequestReviews(ctx, client, owner, repo, pullNumber) - case "get_comments": - result, handlerErr = GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) - default: - return nil, nil, fmt.Errorf("unknown method: %s", method) - } - - return result, nil, handlerErr - }, - ) - - return tool, handler -} - -func GetPullRequest(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { - pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil - } - - // sanitize title/body on response - if pr != nil { - if pr.Title != nil { - pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title)) - } - if pr.Body != nil { - pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body)) - } - } - - r, err := json.Marshal(pr) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil -} - -func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { - raw, resp, err := client.PullRequests.GetRaw( - ctx, - owner, - repo, - pullNumber, - github.RawOptions{Type: github.Diff}, - ) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request diff", - resp, - err, - ), nil - } - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil - } - - defer func() { _ = resp.Body.Close() }() - - // Return the raw response - return utils.NewToolResultText(string(raw)), nil -} - -func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { - pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil - } - - // Get combined status for the head SHA - status, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, *pr.Head.SHA, nil) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get combined status", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil - } - - r, err := json.Marshal(status) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil -} - -func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { - opts := &github.ListOptions{ - PerPage: pagination.PerPage, - Page: pagination.Page, - } - files, resp, err := client.PullRequests.ListFiles(ctx, owner, repo, pullNumber, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request files", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil - } - - r, err := json.Marshal(files) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil -} - -func GetPullRequestReviewComments(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { - opts := &github.PullRequestListCommentsOptions{ - ListOptions: github.ListOptions{ - PerPage: pagination.PerPage, - Page: pagination.Page, - }, - } - - comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request review comments", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil - } - - r, err := json.Marshal(comments) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil -} - -func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { - reviews, resp, err := client.PullRequests.ListReviews(ctx, owner, repo, pullNumber, nil) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request reviews", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil - } - - r, err := json.Marshal(reviews) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil -} - -// CreatePullRequest creates a tool to create a new pull request. -func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "create_pull_request", - Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "title": { - Type: "string", - Description: "PR title", - }, - "body": { - Type: "string", - Description: "PR description", - }, - "head": { - Type: "string", - Description: "Branch containing changes", - }, - "base": { - Type: "string", - Description: "Branch to merge into", - }, - "draft": { - Type: "boolean", - Description: "Create as draft PR", - }, - "maintainer_can_modify": { - Type: "boolean", - Description: "Allow maintainer edits", - }, - }, - Required: []string{"owner", "repo", "title", "head", "base"}, - }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any]( - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - title, err := RequiredParam[string](args, "title") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - head, err := RequiredParam[string](args, "head") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - base, err := RequiredParam[string](args, "base") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - body, err := OptionalParam[string](args, "body") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - draft, err := OptionalParam[bool](args, "draft") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - maintainerCanModify, err := OptionalParam[bool](args, "maintainer_can_modify") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - newPR := &github.NewPullRequest{ - Title: github.Ptr(title), - Head: github.Ptr(head), - Base: github.Ptr(base), - } - - if body != "" { - newPR.Body = github.Ptr(body) - } - - newPR.Draft = github.Ptr(draft) - newPR.MaintainerCanModify = github.Ptr(maintainerCanModify) - - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create pull request", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil, nil - } - - // Return minimal response with just essential information - minimalResponse := MinimalResponse{ - ID: fmt.Sprintf("%d", pr.GetID()), - URL: pr.GetHTMLURL(), - } - - r, err := json.Marshal(minimalResponse) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - - return tool, handler -} - -// UpdatePullRequest creates a tool to update an existing pull request. -// TODO: MIGRATE THIS TOOL -func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.NewTool("update_pull_request", - mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_UPDATE_PULL_REQUEST_USER_TITLE", "Edit pull request"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number to update"), - ), - mcp.WithString("title", - mcp.Description("New title"), - ), - mcp.WithString("body", - mcp.Description("New description"), - ), - mcp.WithString("state", - mcp.Description("New state"), - mcp.Enum("open", "closed"), - ), - mcp.WithBoolean("draft", - mcp.Description("Mark pull request as draft (true) or ready for review (false)"), - ), - mcp.WithString("base", - mcp.Description("New base branch name"), - ), - mcp.WithBoolean("maintainer_can_modify", - mcp.Description("Allow maintainer edits"), - ), - mcp.WithArray("reviewers", - mcp.Description("GitHub usernames to request reviews from"), - mcp.Items(map[string]interface{}{ - "type": "string", - }), - ), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - // Check if draft parameter is provided - draftProvided := args["draft"] != nil - var draftValue bool - if draftProvided { - draftValue, err = OptionalParam[bool](args, "draft") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - } - - // Build the update struct only with provided fields - update := &github.PullRequest{} - restUpdateNeeded := false - - if title, ok, err := OptionalParamOK[string](args, "title"); err != nil { - return utils.NewToolResultError(err.Error()), nil - } else if ok { - update.Title = github.Ptr(title) - restUpdateNeeded = true - } - - if body, ok, err := OptionalParamOK[string](args, "body"); err != nil { - return utils.NewToolResultError(err.Error()), nil - } else if ok { - update.Body = github.Ptr(body) - restUpdateNeeded = true - } - - if state, ok, err := OptionalParamOK[string](args, "state"); err != nil { - return utils.NewToolResultError(err.Error()), nil - } else if ok { - update.State = github.Ptr(state) - restUpdateNeeded = true - } - - if base, ok, err := OptionalParamOK[string](args, "base"); err != nil { - return utils.NewToolResultError(err.Error()), nil - } else if ok { - update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)} - restUpdateNeeded = true - } - - if maintainerCanModify, ok, err := OptionalParamOK[bool](args, "maintainer_can_modify"); err != nil { - return utils.NewToolResultError(err.Error()), nil - } else if ok { - update.MaintainerCanModify = github.Ptr(maintainerCanModify) - restUpdateNeeded = true - } - - // Handle reviewers separately - reviewers, err := OptionalStringArrayParam(args, "reviewers") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - // If no updates, no draft change, and no reviewers, return error early - if !restUpdateNeeded && !draftProvided && len(reviewers) == 0 { - return utils.NewToolResultError("No update parameters provided."), nil - } - - // Handle REST API updates (title, body, state, base, maintainer_can_modify) - if restUpdateNeeded { - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - _, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to update pull request", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil - } - } - - // Handle draft status changes using GraphQL - if draftProvided { - gqlClient, err := getGQLClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err) - } - - var prQuery struct { - Repository struct { - PullRequest struct { - ID githubv4.ID - IsDraft githubv4.Boolean - } `graphql:"pullRequest(number: $prNum)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - - err = gqlClient.Query(ctx, &prQuery, map[string]interface{}{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - "prNum": githubv4.Int(pullNumber), // #nosec G115 - pull request numbers are always small positive integers - }) - if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find pull request", err), nil - } - - currentIsDraft := bool(prQuery.Repository.PullRequest.IsDraft) - - if currentIsDraft != draftValue { - if draftValue { - // Convert to draft - var mutation struct { - ConvertPullRequestToDraft struct { - PullRequest struct { - ID githubv4.ID - IsDraft githubv4.Boolean - } - } `graphql:"convertPullRequestToDraft(input: $input)"` - } - - err = gqlClient.Mutate(ctx, &mutation, githubv4.ConvertPullRequestToDraftInput{ - PullRequestID: prQuery.Repository.PullRequest.ID, - }, nil) - if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to convert pull request to draft", err), nil - } - } else { - // Mark as ready for review - var mutation struct { - MarkPullRequestReadyForReview struct { - PullRequest struct { - ID githubv4.ID - IsDraft githubv4.Boolean - } - } `graphql:"markPullRequestReadyForReview(input: $input)"` - } - - err = gqlClient.Mutate(ctx, &mutation, githubv4.MarkPullRequestReadyForReviewInput{ - PullRequestID: prQuery.Repository.PullRequest.ID, - }, nil) - if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to mark pull request ready for review", err), nil - } - } - } - } - - // Handle reviewer requests - if len(reviewers) > 0 { - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - reviewersRequest := github.ReviewersRequest{ - Reviewers: reviewers, - } - - _, resp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, pullNumber, reviewersRequest) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to request reviewers", - resp, - err, - ), nil - } - defer func() { - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - }() - - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil - } - } - - // Get the final state of the PR to return - client, err := getClient(ctx) - if err != nil { - return nil, err - } - - finalPR, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "Failed to get pull request", resp, err), nil - } - defer func() { - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - }() - - // Return minimal response with just essential information - minimalResponse := MinimalResponse{ - ID: fmt.Sprintf("%d", finalPR.GetID()), - URL: finalPR.GetHTMLURL(), - } - - r, err := json.Marshal(minimalResponse) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil - } - - return utils.NewToolResultText(string(r)), nil - } -} - -// ListPullRequests creates a tool to list and filter repository pull requests. -// TODO: MIGRATE THIS TOOL -func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.NewTool("list_pull_requests", - mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("state", - mcp.Description("Filter by state"), - mcp.Enum("open", "closed", "all"), - ), - mcp.WithString("head", - mcp.Description("Filter by head user/org and branch"), - ), - mcp.WithString("base", - mcp.Description("Filter by base branch"), - ), - mcp.WithString("sort", - mcp.Description("Sort by"), - mcp.Enum("created", "updated", "popularity", "long-running"), - ), - mcp.WithString("direction", - mcp.Description("Sort direction"), - mcp.Enum("asc", "desc"), - ), - WithPagination(), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - state, err := OptionalParam[string](args, "state") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - head, err := OptionalParam[string](args, "head") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - base, err := OptionalParam[string](args, "base") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - sort, err := OptionalParam[string](args, "sort") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - direction, err := OptionalParam[string](args, "direction") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.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 ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list pull requests", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil - } - - // sanitize title/body on each PR - for _, pr := range prs { - if pr == nil { - continue - } - if pr.Title != nil { - pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title)) - } - if pr.Body != nil { - pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body)) - } - } - - r, err := json.Marshal(prs) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil - } -} - -// MergePullRequest creates a tool to merge a pull request. -// TODO: MIGRATE THIS TOOL -func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.NewTool("merge_pull_request", - mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), - mcp.WithString("commit_title", - mcp.Description("Title for merge commit"), - ), - mcp.WithString("commit_message", - mcp.Description("Extra detail for merge commit"), - ), - mcp.WithString("merge_method", - mcp.Description("Merge method"), - mcp.Enum("merge", "squash", "rebase"), - ), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - commitTitle, err := OptionalParam[string](args, "commit_title") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - commitMessage, err := OptionalParam[string](args, "commit_message") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - mergeMethod, err := OptionalParam[string](args, "merge_method") - if err != nil { - return utils.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 ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to merge pull request", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to 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 utils.NewToolResultText(string(r)), nil - } -} - -// SearchPullRequests creates a tool to search for pull requests. -// TODO: MIGRATE THIS TOOL -func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("search_pull_requests", - mcp.WithDescription(t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("query", - mcp.Required(), - mcp.Description("Search query using GitHub pull request search syntax"), - ), - mcp.WithString("owner", - mcp.Description("Optional repository owner. If provided with repo, only pull requests for this repository are listed."), - ), - mcp.WithString("repo", - mcp.Description("Optional repository name. If provided with owner, only pull requests for this repository are listed."), - ), - mcp.WithString("sort", - mcp.Description("Sort field by number of matches of categories, defaults to best match"), - mcp.Enum( - "comments", - "reactions", - "reactions-+1", - "reactions--1", - "reactions-smile", - "reactions-thinking_face", - "reactions-heart", - "reactions-tada", - "interactions", - "created", - "updated", - ), - ), - mcp.WithString("order", - mcp.Description("Sort order"), - mcp.Enum("asc", "desc"), - ), - WithPagination(), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests") - } -} - -// UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. -// TODO: MIGRATE THIS TOOL -func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.NewTool("update_pull_request_branch", - mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_USER_TITLE", "Update pull request branch"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), - mcp.WithString("expectedHeadSha", - mcp.Description("The expected SHA of the pull request's HEAD ref"), - ), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - expectedHeadSHA, err := OptionalParam[string](args, "expectedHeadSha") - if err != nil { - return utils.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 utils.NewToolResultText("Pull request branch update is in progress"), nil - } - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to update pull request branch", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusAccepted { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(body))), nil - } - - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil - } -} - -type PullRequestReviewWriteParams struct { - Method string - Owner string - Repo string - PullNumber int32 - Body string - Event string - CommitID *string -} - -// TODO: MIGRATE THIS TOOL -func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.NewTool("pull_request_review_write", - mcp.WithDescription(t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request. - -Available methods: -- create: Create a new review of a pull request. If "event" parameter is provided, the review is submitted. If "event" is omitted, a pending review is created. -- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The "body" and "event" parameters are used when submitting the review. -- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. -`)), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_PULL_REQUEST_REVIEW_WRITE_USER_TITLE", "Write operations (create, submit, delete) on pull request reviews."), - ReadOnlyHint: ToBoolPtr(false), - }), - // Either we need the PR GQL Id directly, or we need owner, repo and PR number to look it up. - // Since our other Pull Request tools are working with the REST Client, will handle the lookup - // internally for now. - mcp.WithString("method", - mcp.Required(), - mcp.Description("The write operation to perform on pull request review."), - mcp.Enum("create", "submit_pending", "delete_pending"), - ), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), - mcp.WithString("body", - mcp.Description("Review comment text"), - ), - mcp.WithString("event", - mcp.Description("Review action to perform."), - mcp.Enum("APPROVE", "REQUEST_CHANGES", "COMMENT"), - ), - mcp.WithString("commitID", - mcp.Description("SHA of commit to review"), - ), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - var params PullRequestReviewWriteParams - if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { - return utils.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 utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil - } - - switch params.Method { - case "create": - return CreatePullRequestReview(ctx, client, params) - case "submit_pending": - return SubmitPendingPullRequestReview(ctx, client, params) - case "delete_pending": - return DeletePendingPullRequestReview(ctx, client, params) - default: - return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil - } - } -} - -func CreatePullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { - var getPullRequestQuery struct { - Repository struct { - PullRequest struct { - ID githubv4.ID - } `graphql:"pullRequest(number: $prNum)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - - if err := client.Query(ctx, &getPullRequestQuery, map[string]any{ - "owner": githubv4.String(params.Owner), - "repo": githubv4.String(params.Repo), - "prNum": githubv4.Int(params.PullNumber), - }); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get pull request", - err, - ), nil - } - - // Now we have the GQL ID, we can create a review - var addPullRequestReviewMutation struct { - AddPullRequestReview struct { - PullRequestReview struct { - ID githubv4.ID // We don't need this, but a selector is required or GQL complains. - } - } `graphql:"addPullRequestReview(input: $input)"` - } - - addPullRequestReviewInput := githubv4.AddPullRequestReviewInput{ - PullRequestID: getPullRequestQuery.Repository.PullRequest.ID, - CommitOID: newGQLStringlikePtr[githubv4.GitObjectID](params.CommitID), - } - - // Event and Body are provided if we submit a review - if params.Event != "" { - addPullRequestReviewInput.Event = newGQLStringlike[githubv4.PullRequestReviewEvent](params.Event) - addPullRequestReviewInput.Body = githubv4.NewString(githubv4.String(params.Body)) - } - - if err := client.Mutate( - ctx, - &addPullRequestReviewMutation, - addPullRequestReviewInput, - nil, - ); err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - // Return nothing interesting, just indicate success for the time being. - // In future, we may want to return the review ID, but for the moment, we're not leaking - // API implementation details to the LLM. - if params.Event == "" { - return utils.NewToolResultText("pending pull request created"), nil - } - return utils.NewToolResultText("pull request review submitted successfully"), nil -} - -func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { - // First we'll get the current user - var getViewerQuery struct { - Viewer struct { - Login githubv4.String - } - } - - if err := client.Query(ctx, &getViewerQuery, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get current user", - err, - ), nil - } - - var getLatestReviewForViewerQuery struct { - Repository struct { - PullRequest struct { - Reviews struct { - Nodes []struct { - ID githubv4.ID - State githubv4.PullRequestReviewState - URL githubv4.URI - } - } `graphql:"reviews(first: 1, author: $author)"` - } `graphql:"pullRequest(number: $prNum)"` - } `graphql:"repository(owner: $owner, name: $name)"` - } - - vars := map[string]any{ - "author": githubv4.String(getViewerQuery.Viewer.Login), - "owner": githubv4.String(params.Owner), - "name": githubv4.String(params.Repo), - "prNum": githubv4.Int(params.PullNumber), - } - - if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get latest review for current user", - err, - ), nil - } - - // Validate there is one review and the state is pending - if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { - return utils.NewToolResultError("No pending review found for the viewer"), nil - } - - review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] - if review.State != githubv4.PullRequestReviewStatePending { - errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) - return utils.NewToolResultError(errText), nil - } - - // Prepare the mutation - var submitPullRequestReviewMutation struct { - SubmitPullRequestReview struct { - PullRequestReview struct { - ID githubv4.ID // We don't need this, but a selector is required or GQL complains. - } - } `graphql:"submitPullRequestReview(input: $input)"` - } - - if err := client.Mutate( - ctx, - &submitPullRequestReviewMutation, - githubv4.SubmitPullRequestReviewInput{ - PullRequestReviewID: &review.ID, - Event: githubv4.PullRequestReviewEvent(params.Event), - Body: newGQLStringlikePtr[githubv4.String](¶ms.Body), - }, - nil, - ); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to submit pull request review", - err, - ), nil - } - - // Return nothing interesting, just indicate success for the time being. - // In future, we may want to return the review ID, but for the moment, we're not leaking - // API implementation details to the LLM. - return utils.NewToolResultText("pending pull request review successfully submitted"), nil -} - -func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) { - // First we'll get the current user - var getViewerQuery struct { - Viewer struct { - Login githubv4.String - } - } - - if err := client.Query(ctx, &getViewerQuery, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get current user", - err, - ), nil - } - - var getLatestReviewForViewerQuery struct { - Repository struct { - PullRequest struct { - Reviews struct { - Nodes []struct { - ID githubv4.ID - State githubv4.PullRequestReviewState - URL githubv4.URI - } - } `graphql:"reviews(first: 1, author: $author)"` - } `graphql:"pullRequest(number: $prNum)"` - } `graphql:"repository(owner: $owner, name: $name)"` - } - - vars := map[string]any{ - "author": githubv4.String(getViewerQuery.Viewer.Login), - "owner": githubv4.String(params.Owner), - "name": githubv4.String(params.Repo), - "prNum": githubv4.Int(params.PullNumber), - } - - if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get latest review for current user", - err, - ), nil - } - - // Validate there is one review and the state is pending - if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { - return utils.NewToolResultError("No pending review found for the viewer"), nil - } - - review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] - if review.State != githubv4.PullRequestReviewStatePending { - errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) - return utils.NewToolResultError(errText), nil - } - - // Prepare the mutation - var deletePullRequestReviewMutation struct { - DeletePullRequestReview struct { - PullRequestReview struct { - ID githubv4.ID // We don't need this, but a selector is required or GQL complains. - } - } `graphql:"deletePullRequestReview(input: $input)"` - } - - if err := client.Mutate( - ctx, - &deletePullRequestReviewMutation, - githubv4.DeletePullRequestReviewInput{ - PullRequestReviewID: &review.ID, - }, - nil, - ); err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - // Return nothing interesting, just indicate success for the time being. - // In future, we may want to return the review ID, but for the moment, we're not leaking - // API implementation details to the LLM. - return utils.NewToolResultText("pending pull request review successfully deleted"), nil -} - -// AddCommentToPendingReview creates a tool to add a comment to a pull request review. -// TODO: MIGRATE THIS TOOL -func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.NewTool("add_comment_to_pending_review", - mcp.WithDescription(t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_USER_TITLE", "Add review comment to the requester's latest pending pull request review"), - ReadOnlyHint: ToBoolPtr(false), - }), - // Ideally, for performance sake this would just accept the pullRequestReviewID. However, we would need to - // add a new tool to get that ID for clients that aren't in the same context as the original pending review - // creation. So for now, we'll just accept the owner, repo and pull number and assume this is adding a comment - // the latest review from a user, since only one can be active at a time. It can later be extended with - // a pullRequestReviewID parameter if targeting other reviews is desired: - // mcp.WithString("pullRequestReviewID", - // mcp.Required(), - // mcp.Description("The ID of the pull request review to add a comment to"), - // ), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), - mcp.WithString("path", - mcp.Required(), - mcp.Description("The relative path to the file that necessitates a comment"), - ), - mcp.WithString("body", - mcp.Required(), - mcp.Description("The text of the review comment"), - ), - mcp.WithString("subjectType", - mcp.Required(), - mcp.Description("The level at which the comment is targeted"), - mcp.Enum("FILE", "LINE"), - ), - mcp.WithNumber("line", - mcp.Description("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range"), - ), - mcp.WithString("side", - mcp.Description("The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state"), - mcp.Enum("LEFT", "RIGHT"), - ), - mcp.WithNumber("startLine", - mcp.Description("For multi-line comments, the first line of the range that the comment applies to"), - ), - mcp.WithString("startSide", - mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state"), - mcp.Enum("LEFT", "RIGHT"), - ), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - var params struct { - Owner string - Repo string - PullNumber int32 - Path string - Body string - SubjectType string - Line *int32 - Side *string - StartLine *int32 - StartSide *string - } - if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { - return utils.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 ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get current user", - err, - ), nil - } - - var getLatestReviewForViewerQuery struct { - Repository struct { - PullRequest struct { - Reviews struct { - Nodes []struct { - ID githubv4.ID - State githubv4.PullRequestReviewState - URL githubv4.URI - } - } `graphql:"reviews(first: 1, author: $author)"` - } `graphql:"pullRequest(number: $prNum)"` - } `graphql:"repository(owner: $owner, name: $name)"` - } - - vars := map[string]any{ - "author": githubv4.String(getViewerQuery.Viewer.Login), - "owner": githubv4.String(params.Owner), - "name": githubv4.String(params.Repo), - "prNum": githubv4.Int(params.PullNumber), - } - - if err := client.Query(context.Background(), &getLatestReviewForViewerQuery, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get latest review for current user", - err, - ), nil - } - - // Validate there is one review and the state is pending - if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 { - return utils.NewToolResultError("No pending review found for the viewer"), nil - } - - review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0] - if review.State != githubv4.PullRequestReviewStatePending { - errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL) - return utils.NewToolResultError(errText), nil - } - - // Then we can create a new review thread comment on the review. - var addPullRequestReviewThreadMutation struct { - AddPullRequestReviewThread struct { - Thread struct { - ID githubv4.ID // We don't need this, but a selector is required or GQL complains. - } - } `graphql:"addPullRequestReviewThread(input: $input)"` - } - - if err := client.Mutate( - ctx, - &addPullRequestReviewThreadMutation, - githubv4.AddPullRequestReviewThreadInput{ - Path: githubv4.String(params.Path), - Body: githubv4.String(params.Body), - SubjectType: newGQLStringlikePtr[githubv4.PullRequestReviewThreadSubjectType](¶ms.SubjectType), - Line: newGQLIntPtr(params.Line), - Side: newGQLStringlikePtr[githubv4.DiffSide](params.Side), - StartLine: newGQLIntPtr(params.StartLine), - StartSide: newGQLStringlikePtr[githubv4.DiffSide](params.StartSide), - PullRequestReviewID: &review.ID, - }, - nil, - ); err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - // Return nothing interesting, just indicate success for the time being. - // In future, we may want to return the review ID, but for the moment, we're not leaking - // API implementation details to the LLM. - return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil - } -} - -// RequestCopilotReview creates a tool to request a Copilot review for a pull request. -// Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this -// tool if the configured host does not support it. -// TODO: MIGRATE THIS TOOL -func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.NewTool("request_copilot_review", - mcp.WithDescription(t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_REQUEST_COPILOT_REVIEW_USER_TITLE", "Request Copilot review"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), - ), - func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil - } - - _, resp, err := client.PullRequests.RequestReviewers( - ctx, - owner, - repo, - pullNumber, - github.ReviewersRequest{ - // The login name of the copilot reviewer bot - Reviewers: []string{"copilot-pull-request-reviewer[bot]"}, - }, - ) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to request copilot review", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil - } - - // Return nothing on success, as there's not much value in returning the Pull Request itself - return utils.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 -} - -func newGQLStringlikePtr[T ~string](s *string) *T { - if s == nil { - return nil - } - stringlike := T(*s) - return &stringlike -} - -func newGQLIntPtr(i *int32) *githubv4.Int { - if i == nil { - return nil - } - gi := githubv4.Int(*i) - return &gi -}