diff --git a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap index 08fa42df5..78795c096 100644 --- a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap +++ b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap @@ -1,73 +1,72 @@ { "annotations": { - "title": "Add review comment to the requester's latest pending pull request review", - "readOnlyHint": false + "title": "Add review comment to the requester's latest pending pull request 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).", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], "properties": { "body": { - "description": "The text of the review comment", - "type": "string" + "type": "string", + "description": "The text of the review comment" }, "line": { - "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", - "type": "number" + "type": "number", + "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" }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "path": { - "description": "The relative path to the file that necessitates a comment", - "type": "string" + "type": "string", + "description": "The relative path to the file that necessitates a comment" }, "pullNumber": { - "description": "Pull request number", - "type": "number" + "type": "number", + "description": "Pull request number" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" }, "side": { + "type": "string", "description": "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", "enum": [ "LEFT", "RIGHT" - ], - "type": "string" + ] }, "startLine": { - "description": "For multi-line comments, the first line of the range that the comment applies to", - "type": "number" + "type": "number", + "description": "For multi-line comments, the first line of the range that the comment applies to" }, "startSide": { + "type": "string", "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", "enum": [ "LEFT", "RIGHT" - ], - "type": "string" + ] }, "subjectType": { + "type": "string", "description": "The level at which the comment is targeted", "enum": [ "FILE", "LINE" - ], - "type": "string" + ] } - }, - "required": [ - "owner", - "repo", - "pullNumber", - "path", - "body", - "subjectType" - ], - "type": "object" + } }, "name": "add_comment_to_pending_review" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_pull_request.snap b/pkg/github/__toolsnaps__/create_pull_request.snap index 44142a79e..80f0b9863 100644 --- a/pkg/github/__toolsnaps__/create_pull_request.snap +++ b/pkg/github/__toolsnaps__/create_pull_request.snap @@ -1,52 +1,51 @@ { "annotations": { - "title": "Open new pull request", - "readOnlyHint": false + "title": "Open new pull request" }, "description": "Create a new pull request in a GitHub repository.", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], "properties": { "base": { - "description": "Branch to merge into", - "type": "string" + "type": "string", + "description": "Branch to merge into" }, "body": { - "description": "PR description", - "type": "string" + "type": "string", + "description": "PR description" }, "draft": { - "description": "Create as draft PR", - "type": "boolean" + "type": "boolean", + "description": "Create as draft PR" }, "head": { - "description": "Branch containing changes", - "type": "string" + "type": "string", + "description": "Branch containing changes" }, "maintainer_can_modify": { - "description": "Allow maintainer edits", - "type": "boolean" + "type": "boolean", + "description": "Allow maintainer edits" }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" }, "title": { - "description": "PR title", - "type": "string" + "type": "string", + "description": "PR title" } - }, - "required": [ - "owner", - "repo", - "title", - "head", - "base" - ], - "type": "object" + } }, "name": "create_pull_request" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_pull_requests.snap b/pkg/github/__toolsnaps__/list_pull_requests.snap index fee7e2ff1..ae90c3fe0 100644 --- a/pkg/github/__toolsnaps__/list_pull_requests.snap +++ b/pkg/github/__toolsnaps__/list_pull_requests.snap @@ -1,71 +1,71 @@ { "annotations": { - "title": "List pull requests", - "readOnlyHint": true + "readOnlyHint": true, + "title": "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.", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo" + ], "properties": { "base": { - "description": "Filter by base branch", - "type": "string" + "type": "string", + "description": "Filter by base branch" }, "direction": { + "type": "string", "description": "Sort direction", "enum": [ "asc", "desc" - ], - "type": "string" + ] }, "head": { - "description": "Filter by head user/org and branch", - "type": "string" + "type": "string", + "description": "Filter by head user/org and branch" }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "page": { + "type": "number", "description": "Page number for pagination (min 1)", - "minimum": 1, - "type": "number" + "minimum": 1 }, "perPage": { + "type": "number", "description": "Results per page for pagination (min 1, max 100)", - "maximum": 100, "minimum": 1, - "type": "number" + "maximum": 100 }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" }, "sort": { + "type": "string", "description": "Sort by", "enum": [ "created", "updated", "popularity", "long-running" - ], - "type": "string" + ] }, "state": { + "type": "string", "description": "Filter by state", "enum": [ "open", "closed", "all" - ], - "type": "string" + ] } - }, - "required": [ - "owner", - "repo" - ], - "type": "object" + } }, "name": "list_pull_requests" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/merge_pull_request.snap b/pkg/github/__toolsnaps__/merge_pull_request.snap index a5a1474cb..50d040f2a 100644 --- a/pkg/github/__toolsnaps__/merge_pull_request.snap +++ b/pkg/github/__toolsnaps__/merge_pull_request.snap @@ -1,47 +1,46 @@ { "annotations": { - "title": "Merge pull request", - "readOnlyHint": false + "title": "Merge pull request" }, "description": "Merge a pull request in a GitHub repository.", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "pullNumber" + ], "properties": { "commit_message": { - "description": "Extra detail for merge commit", - "type": "string" + "type": "string", + "description": "Extra detail for merge commit" }, "commit_title": { - "description": "Title for merge commit", - "type": "string" + "type": "string", + "description": "Title for merge commit" }, "merge_method": { + "type": "string", "description": "Merge method", "enum": [ "merge", "squash", "rebase" - ], - "type": "string" + ] }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "pullNumber": { - "description": "Pull request number", - "type": "number" + "type": "number", + "description": "Pull request number" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" } - }, - "required": [ - "owner", - "repo", - "pullNumber" - ], - "type": "object" + } }, "name": "merge_pull_request" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/pull_request_read.snap b/pkg/github/__toolsnaps__/pull_request_read.snap index be9661aae..434fba348 100644 --- a/pkg/github/__toolsnaps__/pull_request_read.snap +++ b/pkg/github/__toolsnaps__/pull_request_read.snap @@ -1,12 +1,20 @@ { "annotations": { - "title": "Get details for a single pull request", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Get details for a single pull request" }, "description": "Get information on a specific pull request in GitHub repository.", "inputSchema": { + "type": "object", + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], "properties": { "method": { + "type": "string", "description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 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.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 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.\n", "enum": [ "get", @@ -16,40 +24,32 @@ "get_review_comments", "get_reviews", "get_comments" - ], - "type": "string" + ] }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "page": { + "type": "number", "description": "Page number for pagination (min 1)", - "minimum": 1, - "type": "number" + "minimum": 1 }, "perPage": { + "type": "number", "description": "Results per page for pagination (min 1, max 100)", - "maximum": 100, "minimum": 1, - "type": "number" + "maximum": 100 }, "pullNumber": { - "description": "Pull request number", - "type": "number" + "type": "number", + "description": "Pull request number" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" } - }, - "required": [ - "method", - "owner", - "repo", - "pullNumber" - ], - "type": "object" + } }, "name": "pull_request_read" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/pull_request_review_write.snap b/pkg/github/__toolsnaps__/pull_request_review_write.snap index e1702787c..92cc19924 100644 --- a/pkg/github/__toolsnaps__/pull_request_review_write.snap +++ b/pkg/github/__toolsnaps__/pull_request_review_write.snap @@ -1,57 +1,56 @@ { "annotations": { - "title": "Write operations (create, submit, delete) on pull request reviews.", - "readOnlyHint": false + "title": "Write operations (create, submit, delete) on pull request reviews." }, "description": "Create and/or submit, delete review of a pull request.\n\nAvailable methods:\n- 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.\n- 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.\n- 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.\n", "inputSchema": { + "type": "object", + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], "properties": { "body": { - "description": "Review comment text", - "type": "string" + "type": "string", + "description": "Review comment text" }, "commitID": { - "description": "SHA of commit to review", - "type": "string" + "type": "string", + "description": "SHA of commit to review" }, "event": { + "type": "string", "description": "Review action to perform.", "enum": [ "APPROVE", "REQUEST_CHANGES", "COMMENT" - ], - "type": "string" + ] }, "method": { + "type": "string", "description": "The write operation to perform on pull request review.", "enum": [ "create", "submit_pending", "delete_pending" - ], - "type": "string" + ] }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "pullNumber": { - "description": "Pull request number", - "type": "number" + "type": "number", + "description": "Pull request number" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" } - }, - "required": [ - "method", - "owner", - "repo", - "pullNumber" - ], - "type": "object" + } }, "name": "pull_request_review_write" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/request_copilot_review.snap b/pkg/github/__toolsnaps__/request_copilot_review.snap index 1717ced01..b967b51cc 100644 --- a/pkg/github/__toolsnaps__/request_copilot_review.snap +++ b/pkg/github/__toolsnaps__/request_copilot_review.snap @@ -1,30 +1,29 @@ { "annotations": { - "title": "Request Copilot review", - "readOnlyHint": false + "title": "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.", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "pullNumber" + ], "properties": { "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "pullNumber": { - "description": "Pull request number", - "type": "number" + "type": "number", + "description": "Pull request number" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" } - }, - "required": [ - "owner", - "repo", - "pullNumber" - ], - "type": "object" + } }, "name": "request_copilot_review" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_pull_requests.snap b/pkg/github/__toolsnaps__/search_pull_requests.snap index 811aa1322..2013f5c08 100644 --- a/pkg/github/__toolsnaps__/search_pull_requests.snap +++ b/pkg/github/__toolsnaps__/search_pull_requests.snap @@ -1,43 +1,48 @@ { "annotations": { - "title": "Search pull requests", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Search pull requests" }, "description": "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr", "inputSchema": { + "type": "object", + "required": [ + "query" + ], "properties": { "order": { + "type": "string", "description": "Sort order", "enum": [ "asc", "desc" - ], - "type": "string" + ] }, "owner": { - "description": "Optional repository owner. If provided with repo, only pull requests for this repository are listed.", - "type": "string" + "type": "string", + "description": "Optional repository owner. If provided with repo, only pull requests for this repository are listed." }, "page": { + "type": "number", "description": "Page number for pagination (min 1)", - "minimum": 1, - "type": "number" + "minimum": 1 }, "perPage": { + "type": "number", "description": "Results per page for pagination (min 1, max 100)", - "maximum": 100, "minimum": 1, - "type": "number" + "maximum": 100 }, "query": { - "description": "Search query using GitHub pull request search syntax", - "type": "string" + "type": "string", + "description": "Search query using GitHub pull request search syntax" }, "repo": { - "description": "Optional repository name. If provided with owner, only pull requests for this repository are listed.", - "type": "string" + "type": "string", + "description": "Optional repository name. If provided with owner, only pull requests for this repository are listed." }, "sort": { + "type": "string", "description": "Sort field by number of matches of categories, defaults to best match", "enum": [ "comments", @@ -51,14 +56,9 @@ "interactions", "created", "updated" - ], - "type": "string" + ] } - }, - "required": [ - "query" - ], - "type": "object" + } }, "name": "search_pull_requests" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request.snap b/pkg/github/__toolsnaps__/update_pull_request.snap index 25170ed5f..6dec2c01f 100644 --- a/pkg/github/__toolsnaps__/update_pull_request.snap +++ b/pkg/github/__toolsnaps__/update_pull_request.snap @@ -1,65 +1,64 @@ { "annotations": { - "title": "Edit pull request", - "readOnlyHint": false + "title": "Edit pull request" }, "description": "Update an existing pull request in a GitHub repository.", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "pullNumber" + ], "properties": { "base": { - "description": "New base branch name", - "type": "string" + "type": "string", + "description": "New base branch name" }, "body": { - "description": "New description", - "type": "string" + "type": "string", + "description": "New description" }, "draft": { - "description": "Mark pull request as draft (true) or ready for review (false)", - "type": "boolean" + "type": "boolean", + "description": "Mark pull request as draft (true) or ready for review (false)" }, "maintainer_can_modify": { - "description": "Allow maintainer edits", - "type": "boolean" + "type": "boolean", + "description": "Allow maintainer edits" }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "pullNumber": { - "description": "Pull request number to update", - "type": "number" + "type": "number", + "description": "Pull request number to update" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" }, "reviewers": { + "type": "array", "description": "GitHub usernames to request reviews from", "items": { "type": "string" - }, - "type": "array" + } }, "state": { + "type": "string", "description": "New state", "enum": [ "open", "closed" - ], - "type": "string" + ] }, "title": { - "description": "New title", - "type": "string" + "type": "string", + "description": "New title" } - }, - "required": [ - "owner", - "repo", - "pullNumber" - ], - "type": "object" + } }, "name": "update_pull_request" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request_branch.snap b/pkg/github/__toolsnaps__/update_pull_request_branch.snap index 60ec9c126..9be1cb002 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_branch.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_branch.snap @@ -1,34 +1,33 @@ { "annotations": { - "title": "Update pull request branch", - "readOnlyHint": false + "title": "Update pull request branch" }, "description": "Update the branch of a pull request with the latest changes from the base branch.", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "pullNumber" + ], "properties": { "expectedHeadSha": { - "description": "The expected SHA of the pull request's HEAD ref", - "type": "string" + "type": "string", + "description": "The expected SHA of the pull request's HEAD ref" }, "owner": { - "description": "Repository owner", - "type": "string" + "type": "string", + "description": "Repository owner" }, "pullNumber": { - "description": "Pull request number", - "type": "number" + "type": "number", + "description": "Pull request number" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" } - }, - "required": [ - "owner", - "repo", - "pullNumber" - ], - "type": "object" + } }, "name": "update_pull_request_branch" } \ No newline at end of file diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 69af69af7..b2aa08eed 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -11,27 +9,25 @@ 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. -Possible options: +// PullRequestRead 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]) { + schema := &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. @@ -39,70 +35,87 @@ 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"}, + } + WithPagination(schema) - 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") + return 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: schema, + }, + 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 utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } switch method { - case "get": - return GetPullRequest(ctx, client, owner, repo, pullNumber) + result, err := GetPullRequest(ctx, client, owner, repo, pullNumber) + return result, nil, err case "get_diff": - return GetPullRequestDiff(ctx, client, owner, repo, pullNumber) + result, err := GetPullRequestDiff(ctx, client, owner, repo, pullNumber) + return result, nil, err case "get_status": - return GetPullRequestStatus(ctx, client, owner, repo, pullNumber) + result, err := GetPullRequestStatus(ctx, client, owner, repo, pullNumber) + return result, nil, err case "get_files": - return GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination) + result, err := GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination) + return result, nil, err case "get_review_comments": - return GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) + result, err := GetPullRequestReviewComments(ctx, client, owner, repo, pullNumber, pagination) + return result, nil, err case "get_reviews": - return GetPullRequestReviews(ctx, client, owner, repo, pullNumber) + result, err := GetPullRequestReviews(ctx, client, owner, repo, pullNumber) + return result, nil, err case "get_comments": - return GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) + result, err := GetIssueComments(ctx, client, owner, repo, pullNumber, pagination, flags) + return result, nil, err default: - return nil, fmt.Errorf("unknown method: %s", method) + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil } } } @@ -123,7 +136,7 @@ func GetPullRequest(ctx context.Context, client *github.Client, owner, repo stri if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } // sanitize title/body on response @@ -141,7 +154,7 @@ func GetPullRequest(ctx context.Context, client *github.Client, owner, repo stri return 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) { @@ -165,13 +178,13 @@ func GetPullRequestDiff(ctx context.Context, client *github.Client, owner, repo if err != nil { return 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) { @@ -190,7 +203,7 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil } // Get combined status for the head SHA @@ -209,7 +222,7 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil } r, err := json.Marshal(status) @@ -217,7 +230,7 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep return 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) { @@ -240,7 +253,7 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil } r, err := json.Marshal(files) @@ -248,7 +261,7 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo return 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) { @@ -274,7 +287,7 @@ func GetPullRequestReviewComments(ctx context.Context, client *github.Client, ow if err != nil { return 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) @@ -282,7 +295,7 @@ func GetPullRequestReviewComments(ctx context.Context, client *github.Client, ow return 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) { @@ -301,7 +314,7 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, re if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil } r, err := json.Marshal(reviews) @@ -309,82 +322,94 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, owner, re return 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{ +func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &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"}, + } + + return 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: 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") + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + 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 utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } 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) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - 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(bodyBytes))), nil, nil } // Return minimal response with just essential information @@ -430,138 +455,152 @@ 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 utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } // UpdatePullRequest creates a tool to update an existing pull request. -func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("update_pull_request", - mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number to update", + }, + "title": { + Type: "string", + Description: "New title", + }, + "body": { + Type: "string", + Description: "New description", + }, + "state": { + Type: "string", + Description: "New state", + Enum: []any{"open", "closed"}, + }, + "draft": { + Type: "boolean", + Description: "Mark pull request as draft (true) or ready for review (false)", + }, + "base": { + Type: "string", + Description: "New base branch name", + }, + "maintainer_can_modify": { + Type: "boolean", + Description: "Allow maintainer edits", + }, + "reviewers": { + Type: "array", + Description: "GitHub usernames to request reviews from", + Items: &jsonschema.Schema{ + Type: "string", + }, + }, + }, + Required: []string{"owner", "repo", "pullNumber"}, + } + + return mcp.Tool{ + Name: "update_pull_request", + Description: t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + 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 } - 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 } - // Check if draft parameter is provided - draftProvided := request.GetArguments()["draft"] != nil + _, draftProvided := args["draft"] 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, nil } } - // Build the update struct only with provided fields 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, 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, 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, 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, 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, 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, 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, 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 utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } _, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) @@ -570,16 +609,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) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - 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(bodyBytes))), nil, nil } } @@ -587,7 +626,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 utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil } var prQuery struct { @@ -605,7 +644,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) @@ -626,7 +665,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 @@ -643,7 +682,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 } } } @@ -653,7 +692,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 utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } reviewersRequest := github.ReviewersRequest{ @@ -666,7 +705,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 { @@ -675,23 +714,23 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra }() if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - return mcp.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(bodyBytes))), nil, nil } } // Get the final state of the PR to return client, err := getClient(ctx) if err != nil { - return nil, err + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } 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 { @@ -707,82 +746,97 @@ 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.NewToolResultErrorFromErr("Failed to marshal response", err), nil, nil } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } // ListPullRequests creates a tool to list and filter repository pull requests. -func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("list_pull_requests", - mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "state": { + Type: "string", + Description: "Filter by state", + Enum: []any{"open", "closed", "all"}, + }, + "head": { + Type: "string", + Description: "Filter by head user/org and branch", + }, + "base": { + Type: "string", + Description: "Filter by base branch", + }, + "sort": { + Type: "string", + Description: "Sort by", + Enum: []any{"created", "updated", "popularity", "long-running"}, + }, + "direction": { + Type: "string", + Description: "Sort direction", + Enum: []any{"asc", "desc"}, + }, + }, + Required: []string{"owner", "repo"}, + } + WithPagination(schema) + + return mcp.Tool{ + Name: "list_pull_requests", + Description: 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."), + Annotations: &mcp.ToolAnnotations{ 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + ReadOnlyHint: true, + }, + InputSchema: schema, + }, + 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 } - 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, 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, 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, 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, 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, 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 } + opts := &github.PullRequestListOptions{ State: state, Head: head, @@ -797,7 +851,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 utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts) if err != nil { @@ -805,16 +859,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) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - 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(bodyBytes))), nil, nil } // sanitize title/body on each PR @@ -832,68 +886,80 @@ 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 utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } // MergePullRequest creates a tool to merge a pull request. -func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("merge_pull_request", - mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number", + }, + "commit_title": { + Type: "string", + Description: "Title for merge commit", + }, + "commit_message": { + Type: "string", + Description: "Extra detail for merge commit", + }, + "merge_method": { + Type: "string", + Description: "Merge method", + Enum: []any{"merge", "squash", "rebase"}, + }, + }, + Required: []string{"owner", "repo", "pullNumber"}, + } + + return mcp.Tool{ + Name: "merge_pull_request", + Description: t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository."), + Annotations: &mcp.ToolAnnotations{ 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + 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 } - 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 } - 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, 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, 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, nil } options := &github.PullRequestOptions{ @@ -903,7 +969,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 utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } result, resp, err := client.PullRequests.Merge(ctx, owner, repo, pullNumber, commitMessage, options) if err != nil { @@ -911,48 +977,48 @@ 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) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - 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(bodyBytes))), nil, nil } r, err := json.Marshal(result) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } // SearchPullRequests creates a tool to search for pull requests. -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( +func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "query": { + Type: "string", + Description: "Search query using GitHub pull request search syntax", + }, + "owner": { + Type: "string", + Description: "Optional repository owner. If provided with repo, only pull requests for this repository are listed.", + }, + "repo": { + Type: "string", + Description: "Optional repository name. If provided with owner, only pull requests for this repository are listed.", + }, + "sort": { + Type: "string", + Description: "Sort field by number of matches of categories, defaults to best match", + Enum: []any{ "comments", "reactions", "reactions-+1", @@ -964,59 +1030,83 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF "interactions", "created", "updated", - ), - ), - mcp.WithString("order", - mcp.Description("Sort order"), - mcp.Enum("asc", "desc"), - ), - WithPagination(), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests") + }, + }, + "order": { + Type: "string", + Description: "Sort order", + Enum: []any{"asc", "desc"}, + }, + }, + Required: []string{"query"}, + } + WithPagination(schema) + + return mcp.Tool{ + Name: "search_pull_requests", + Description: t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"), + ReadOnlyHint: true, + }, + InputSchema: schema, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + result, err := searchHandler(ctx, getClient, args, "pr", "failed to search pull requests") + return result, nil, err } } // UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. -func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("update_pull_request_branch", - mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number", + }, + "expectedHeadSha": { + Type: "string", + Description: "The expected SHA of the pull request's HEAD ref", + }, + }, + Required: []string{"owner", "repo", "pullNumber"}, + } + + return mcp.Tool{ + Name: "update_pull_request_branch", + Description: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch."), + Annotations: &mcp.ToolAnnotations{ 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + 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 } - 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 } - 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, nil } opts := &github.PullRequestBranchUpdateOptions{} if expectedHeadSHA != "" { @@ -1025,37 +1115,37 @@ 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 utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } 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, 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) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - 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(bodyBytes))), nil, nil } r, err := json.Marshal(result) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } @@ -1069,71 +1159,86 @@ type PullRequestReviewWriteParams struct { CommitID *string } -func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - 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. +func PullRequestReviewWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + // 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. + "method": { + Type: "string", + Description: `The write operation to perform on pull request review.`, + Enum: []any{"create", "submit_pending", "delete_pending"}, + }, + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number", + }, + "body": { + Type: "string", + Description: "Review comment text", + }, + "event": { + Type: "string", + Description: "Review action to perform.", + Enum: []any{"APPROVE", "REQUEST_CHANGES", "COMMENT"}, + }, + "commitID": { + Type: "string", + Description: "SHA of commit to review", + }, + }, + Required: []string{"method", "owner", "repo", "pullNumber"}, + } + + return mcp.Tool{ + Name: "pull_request_review_write", + Description: 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{ +`), + Annotations: &mcp.ToolAnnotations{ 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + 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 + if err := mapstructure.Decode(args, ¶ms); err != nil { + return utils.NewToolResultError(err.Error()), nil, nil } // Given our owner, repo and PR number, lookup the GQL ID of the PR. client, err := getGQLClient(ctx) if err != nil { - return 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, nil } switch params.Method { case "create": - return CreatePullRequestReview(ctx, client, params) + result, err := CreatePullRequestReview(ctx, client, params) + return result, nil, err case "submit_pending": - return SubmitPendingPullRequestReview(ctx, client, params) + result, err := SubmitPendingPullRequestReview(ctx, client, params) + return result, nil, err case "delete_pending": - return DeletePendingPullRequestReview(ctx, client, params) + result, err := DeletePendingPullRequestReview(ctx, client, params) + return result, nil, err default: - return mcp.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil } } } @@ -1184,16 +1289,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 +1346,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 +1383,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 +1431,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,23 +1457,20 @@ 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) { - 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), - }), +func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ // 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 @@ -1378,47 +1480,63 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans // 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number", + }, + "path": { + Type: "string", + Description: "The relative path to the file that necessitates a comment", + }, + "body": { + Type: "string", + Description: "The text of the review comment", + }, + "subjectType": { + Type: "string", + Description: "The level at which the comment is targeted", + Enum: []any{"FILE", "LINE"}, + }, + "line": { + Type: "number", + 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", + }, + "side": { + Type: "string", + Description: "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", + Enum: []any{"LEFT", "RIGHT"}, + }, + "startLine": { + Type: "number", + Description: "For multi-line comments, the first line of the range that the comment applies to", + }, + "startSide": { + Type: "string", + 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", + Enum: []any{"LEFT", "RIGHT"}, + }, + }, + Required: []string{"owner", "repo", "pullNumber", "path", "body", "subjectType"}, + } + + return mcp.Tool{ + Name: "add_comment_to_pending_review", + Description: 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)."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_USER_TITLE", "Add review comment to the requester's latest pending pull request review"), + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { var params struct { Owner string Repo string @@ -1431,13 +1549,13 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans StartLine *int32 StartSide *string } - if err := mapstructure.Decode(request.Params.Arguments, ¶ms); err != nil { - return mcp.NewToolResultError(err.Error()), nil + if err := mapstructure.Decode(args, ¶ms); err != nil { + return utils.NewToolResultError(err.Error()), nil, nil } client, err := getGQLClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub GQL client: %w", err) + return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil } // First we'll get the current user @@ -1451,7 +1569,7 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get current user", err, - ), nil + ), nil, nil } var getLatestReviewForViewerQuery struct { @@ -1479,18 +1597,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 mcp.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 mcp.NewToolResultError(errText), nil + return utils.NewToolResultError(errText), nil, nil } // Then we can create a new review thread comment on the review. @@ -1517,58 +1635,67 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans }, nil, ); err != nil { - return mcp.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 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, 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) { - 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{ +func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "pullNumber": { + Type: "number", + Description: "Pull request number", + }, + }, + Required: []string{"owner", "repo", "pullNumber"}, + } + + return mcp.Tool{ + Name: "request_copilot_review", + Description: 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."), + Annotations: &mcp.ToolAnnotations{ 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, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + 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 } - 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 } client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } _, resp, err := client.PullRequests.RequestReviewers( @@ -1586,20 +1713,20 @@ 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) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - 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(bodyBytes))), nil, 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, nil } } diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 02eaadf32..f9b439d41 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -13,6 +11,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" + "github.com/google/jsonschema-go/jsonschema" "github.com/shurcooL/githubv4" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -28,11 +27,12 @@ func Test_GetPullRequest(t *testing.T) { assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR for success case mockPR := &github.PullRequest{ @@ -110,7 +110,7 @@ func Test_GetPullRequest(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -147,17 +147,18 @@ func Test_UpdatePullRequest(t *testing.T) { assert.Equal(t, "update_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "draft") - assert.Contains(t, tool.InputSchema.Properties, "title") - assert.Contains(t, tool.InputSchema.Properties, "body") - assert.Contains(t, tool.InputSchema.Properties, "state") - assert.Contains(t, tool.InputSchema.Properties, "base") - assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify") - assert.Contains(t, tool.InputSchema.Properties, "reviewers") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "draft") + assert.Contains(t, schema.Properties, "title") + assert.Contains(t, schema.Properties, "body") + assert.Contains(t, schema.Properties, "state") + assert.Contains(t, schema.Properties, "base") + assert.Contains(t, schema.Properties, "maintainer_can_modify") + assert.Contains(t, schema.Properties, "reviewers") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR for success case mockUpdatedPR := &github.PullRequest{ @@ -367,7 +368,7 @@ func Test_UpdatePullRequest(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError || tc.expectedErrMsg != "" { @@ -551,7 +552,7 @@ func Test_UpdatePullRequest_Draft(t *testing.T) { request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) if tc.expectError || tc.expectedErrMsg != "" { require.NoError(t, err) @@ -585,16 +586,17 @@ func Test_ListPullRequests(t *testing.T) { assert.Equal(t, "list_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "state") - assert.Contains(t, tool.InputSchema.Properties, "head") - assert.Contains(t, tool.InputSchema.Properties, "base") - assert.Contains(t, tool.InputSchema.Properties, "sort") - assert.Contains(t, tool.InputSchema.Properties, "direction") - assert.Contains(t, tool.InputSchema.Properties, "perPage") - assert.Contains(t, tool.InputSchema.Properties, "page") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "state") + assert.Contains(t, schema.Properties, "head") + assert.Contains(t, schema.Properties, "base") + assert.Contains(t, schema.Properties, "sort") + assert.Contains(t, schema.Properties, "direction") + assert.Contains(t, schema.Properties, "perPage") + assert.Contains(t, schema.Properties, "page") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"}) // Setup mock PRs for success case mockPRs := []*github.PullRequest{ @@ -679,7 +681,7 @@ func Test_ListPullRequests(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -719,13 +721,14 @@ func Test_MergePullRequest(t *testing.T) { assert.Equal(t, "merge_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "commit_title") - assert.Contains(t, tool.InputSchema.Properties, "commit_message") - assert.Contains(t, tool.InputSchema.Properties, "merge_method") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "commit_title") + assert.Contains(t, schema.Properties, "commit_message") + assert.Contains(t, schema.Properties, "merge_method") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock merge result for success case mockMergeResult := &github.PullRequestMergeResult{ @@ -798,7 +801,7 @@ func Test_MergePullRequest(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -833,14 +836,15 @@ func Test_SearchPullRequests(t *testing.T) { assert.Equal(t, "search_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "query") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "sort") - assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "perPage") - assert.Contains(t, tool.InputSchema.Properties, "page") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "query") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "sort") + assert.Contains(t, schema.Properties, "order") + assert.Contains(t, schema.Properties, "perPage") + assert.Contains(t, schema.Properties, "page") + assert.ElementsMatch(t, schema.Required, []string{"query"}) mockSearchResult := &github.IssuesSearchResult{ Total: github.Ptr(2), @@ -1099,12 +1103,15 @@ func Test_SearchPullRequests(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrMsg) + require.NoError(t, err) + require.NotNil(t, result) + require.True(t, result.IsError) + textContent := getErrorResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedErrMsg) return } @@ -1140,13 +1147,14 @@ func Test_GetPullRequestFiles(t *testing.T) { assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "page") - assert.Contains(t, tool.InputSchema.Properties, "perPage") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "page") + assert.Contains(t, schema.Properties, "perPage") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR files for success case mockFiles := []*github.CommitFile{ @@ -1244,7 +1252,7 @@ func Test_GetPullRequestFiles(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -1284,11 +1292,12 @@ func Test_GetPullRequestStatus(t *testing.T) { assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR for successful PR fetch mockPR := &github.PullRequest{ @@ -1412,7 +1421,7 @@ func Test_GetPullRequestStatus(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -1453,11 +1462,12 @@ func Test_UpdatePullRequestBranch(t *testing.T) { assert.Equal(t, "update_pull_request_branch", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "expectedHeadSha") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "expectedHeadSha") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock update result for success case mockUpdateResult := &github.PullRequestBranchUpdateResponse{ @@ -1543,7 +1553,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -1573,11 +1583,12 @@ func Test_GetPullRequestComments(t *testing.T) { assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR comments for success case mockComments := []*github.PullRequestComment{ @@ -1666,7 +1677,7 @@ func Test_GetPullRequestComments(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -1707,11 +1718,12 @@ func Test_GetPullRequestReviews(t *testing.T) { assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) // Setup mock PR reviews for success case mockReviews := []*github.PullRequestReview{ @@ -1796,7 +1808,7 @@ func Test_GetPullRequestReviews(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -1837,15 +1849,16 @@ func Test_CreatePullRequest(t *testing.T) { assert.Equal(t, "create_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "title") - assert.Contains(t, tool.InputSchema.Properties, "body") - assert.Contains(t, tool.InputSchema.Properties, "head") - assert.Contains(t, tool.InputSchema.Properties, "base") - assert.Contains(t, tool.InputSchema.Properties, "draft") - assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "title", "head", "base"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "title") + assert.Contains(t, schema.Properties, "body") + assert.Contains(t, schema.Properties, "head") + assert.Contains(t, schema.Properties, "base") + assert.Contains(t, schema.Properties, "draft") + assert.Contains(t, schema.Properties, "maintainer_can_modify") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "title", "head", "base"}) // Setup mock PR for success case mockPR := &github.PullRequest{ @@ -1951,7 +1964,7 @@ func Test_CreatePullRequest(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -1990,14 +2003,15 @@ func TestCreateAndSubmitPullRequestReview(t *testing.T) { assert.Equal(t, "pull_request_review_write", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "body") - assert.Contains(t, tool.InputSchema.Properties, "event") - assert.Contains(t, tool.InputSchema.Properties, "commitID") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "body") + assert.Contains(t, schema.Properties, "event") + assert.Contains(t, schema.Properties, "commitID") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) tests := []struct { name string @@ -2162,7 +2176,7 @@ func TestCreateAndSubmitPullRequestReview(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) @@ -2188,10 +2202,11 @@ func Test_RequestCopilotReview(t *testing.T) { assert.Equal(t, "request_copilot_review", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber"}) // Setup mock PR for success case mockPR := &github.PullRequest{ @@ -2271,7 +2286,7 @@ func Test_RequestCopilotReview(t *testing.T) { request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) if tc.expectError { require.NoError(t, err) @@ -2302,12 +2317,13 @@ func TestCreatePendingPullRequestReview(t *testing.T) { assert.Equal(t, "pull_request_review_write", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "commitID") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "commitID") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) tests := []struct { name string @@ -2462,7 +2478,7 @@ func TestCreatePendingPullRequestReview(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) @@ -2489,17 +2505,18 @@ func TestAddPullRequestReviewCommentToPendingReview(t *testing.T) { assert.Equal(t, "add_comment_to_pending_review", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "path") - assert.Contains(t, tool.InputSchema.Properties, "body") - assert.Contains(t, tool.InputSchema.Properties, "subjectType") - assert.Contains(t, tool.InputSchema.Properties, "line") - assert.Contains(t, tool.InputSchema.Properties, "side") - assert.Contains(t, tool.InputSchema.Properties, "startLine") - assert.Contains(t, tool.InputSchema.Properties, "startSide") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber", "path", "body", "subjectType"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "path") + assert.Contains(t, schema.Properties, "body") + assert.Contains(t, schema.Properties, "subjectType") + assert.Contains(t, schema.Properties, "line") + assert.Contains(t, schema.Properties, "side") + assert.Contains(t, schema.Properties, "startLine") + assert.Contains(t, schema.Properties, "startSide") + assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber", "path", "body", "subjectType"}) tests := []struct { name string @@ -2575,7 +2592,7 @@ func TestAddPullRequestReviewCommentToPendingReview(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) @@ -2602,13 +2619,14 @@ func TestSubmitPendingPullRequestReview(t *testing.T) { assert.Equal(t, "pull_request_review_write", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.Contains(t, tool.InputSchema.Properties, "event") - assert.Contains(t, tool.InputSchema.Properties, "body") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.Contains(t, schema.Properties, "event") + assert.Contains(t, schema.Properties, "body") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) tests := []struct { name string @@ -2675,7 +2693,7 @@ func TestSubmitPendingPullRequestReview(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) @@ -2702,11 +2720,12 @@ func TestDeletePendingPullRequestReview(t *testing.T) { assert.Equal(t, "pull_request_review_write", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) tests := []struct { name string @@ -2769,7 +2788,7 @@ func TestDeletePendingPullRequestReview(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) @@ -2796,11 +2815,12 @@ func TestGetPullRequestDiff(t *testing.T) { assert.Equal(t, "pull_request_read", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "pullNumber") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "pullNumber"}) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.Contains(t, schema.Properties, "owner") + assert.Contains(t, schema.Properties, "repo") + assert.Contains(t, schema.Properties, "pullNumber") + assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"}) stubbedDiff := `diff --git a/README.md b/README.md index 5d6e7b2..8a4f5c3 100644 @@ -2855,7 +2875,7 @@ index 5d6e7b2..8a4f5c3 100644 request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index d80d16fd7..cf3a5d85f 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -223,23 +223,23 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // AddReadTools( // toolsets.NewServerTool(SearchOrgs(getClient, t)), // ) - // pullRequests := toolsets.NewToolset(ToolsetMetadataPullRequests.ID, ToolsetMetadataPullRequests.Description). - // AddReadTools( - // toolsets.NewServerTool(PullRequestRead(getClient, t, flags)), - // toolsets.NewServerTool(ListPullRequests(getClient, t)), - // toolsets.NewServerTool(SearchPullRequests(getClient, t)), - // ). - // AddWriteTools( - // toolsets.NewServerTool(MergePullRequest(getClient, t)), - // toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), - // toolsets.NewServerTool(CreatePullRequest(getClient, t)), - // toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)), - // toolsets.NewServerTool(RequestCopilotReview(getClient, t)), - - // // Reviews - // toolsets.NewServerTool(PullRequestReviewWrite(getGQLClient, t)), - // toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)), - // ) + pullRequests := toolsets.NewToolset(ToolsetMetadataPullRequests.ID, ToolsetMetadataPullRequests.Description). + AddReadTools( + toolsets.NewServerTool(PullRequestRead(getClient, t, flags)), + toolsets.NewServerTool(ListPullRequests(getClient, t)), + toolsets.NewServerTool(SearchPullRequests(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(MergePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), + toolsets.NewServerTool(CreatePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)), + toolsets.NewServerTool(RequestCopilotReview(getClient, t)), + + // Reviews + toolsets.NewServerTool(PullRequestReviewWrite(getGQLClient, t)), + toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)), + ) codeSecurity := toolsets.NewToolset(ToolsetMetadataCodeSecurity.ID, ToolsetMetadataCodeSecurity.Description). AddReadTools( toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), @@ -365,7 +365,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(issues) // tsg.AddToolset(orgs) // tsg.AddToolset(users) - // tsg.AddToolset(pullRequests) + tsg.AddToolset(pullRequests) // tsg.AddToolset(actions) tsg.AddToolset(codeSecurity) tsg.AddToolset(dependabot)