From 7c4ce2830681ac5f2c2662404ed2506b5dbb07b5 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Fri, 2 Oct 2026 16:32:12 +0200 Subject: [PATCH 1/4] refactor(pull-requests): migrate consolidated PR tools to typed inputs and outputs Convert pull_request_read, create_pull_request, update_pull_request, merge_pull_request, update_pull_request_branch, pull_request_review_write (both feature variants), add_comment_to_pending_review and add_reply_to_pull_request_comment to NewTool with concrete inputs and output DTOs. Input normalizers replay legacy handler checks so legacy coercions and error text are preserved. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../add_comment_to_pending_review.snap | 14 +- .../add_comment_to_pending_review_typed.snap | 100 ++ .../add_reply_to_pull_request_comment.snap | 64 +- ...d_reply_to_pull_request_comment_typed.snap | 130 +++ .../__toolsnaps__/create_pull_request.snap | 41 +- .../create_pull_request_typed.snap | 122 +++ .../__toolsnaps__/merge_pull_request.snap | 31 +- .../merge_pull_request_typed.snap | 93 ++ .../__toolsnaps__/pull_request_read.snap | 909 +++++++++++++++- .../pull_request_read_typed.snap | 983 ++++++++++++++++++ .../pull_request_review_write.snap | 14 +- ..._review_write_resolution_reason_typed.snap | 94 ++ .../pull_request_review_write_typed.snap | 90 ++ .../__toolsnaps__/update_pull_request.snap | 41 +- .../update_pull_request_branch.snap | 23 +- .../update_pull_request_branch_typed.snap | 70 ++ .../update_pull_request_typed.snap | 128 +++ pkg/github/consolidated_pull_request_types.go | 502 +++++++++ pkg/github/pullrequests.go | 375 ++++--- ..._consolidated_pull_request_outputs_test.go | 457 ++++++++ 20 files changed, 4131 insertions(+), 150 deletions(-) create mode 100644 pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap create mode 100644 pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap create mode 100644 pkg/github/__toolsnaps__/create_pull_request_typed.snap create mode 100644 pkg/github/__toolsnaps__/merge_pull_request_typed.snap create mode 100644 pkg/github/__toolsnaps__/pull_request_read_typed.snap create mode 100644 pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason_typed.snap create mode 100644 pkg/github/__toolsnaps__/pull_request_review_write_typed.snap create mode 100644 pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap create mode 100644 pkg/github/__toolsnaps__/update_pull_request_typed.snap create mode 100644 pkg/github/consolidated_pull_request_types.go create mode 100644 pkg/github/typed_consolidated_pull_request_outputs_test.go diff --git a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap index 4a6e6ca9ba..a013d81b4b 100644 --- a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap +++ b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap @@ -70,5 +70,17 @@ ], "type": "object" }, - "name": "add_comment_to_pending_review" + "name": "add_comment_to_pending_review", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap b/pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap new file mode 100644 index 0000000000..f533fe8b48 --- /dev/null +++ b/pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap @@ -0,0 +1,100 @@ +{ + "annotations": { + "idempotentHint": false, + "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).", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "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" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "path": { + "description": "The relative path to the file that necessitates a comment", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "side": { + "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" + }, + "startSide": { + "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": { + "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", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap index a86ea3d4b5..88c438d0ed 100644 --- a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap +++ b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap @@ -50,5 +50,67 @@ ], "type": "object" }, - "name": "add_reply_to_pull_request_comment" + "name": "add_reply_to_pull_request_comment", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "comment": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + "reaction": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + } + }, + "required": [ + "comment", + "reaction" + ], + "type": "object" + } + ] + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap new file mode 100644 index 0000000000..b3f95e9a3e --- /dev/null +++ b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap @@ -0,0 +1,130 @@ +{ + "annotations": { + "idempotentHint": false, + "readOnlyHint": false, + "title": "Add reply to pull request comment" + }, + "description": "Add a reply and/or reaction to an existing pull request comment. This can create a new comment linked as a reply to the specified comment, add an emoji reaction to the specified comment, or do both. At least one of body or reaction is required.", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "body": { + "description": "The text of the reply. Required unless reaction is provided.", + "type": "string" + }, + "commentId": { + "description": "The numeric ID of the pull request review comment to reply or react to. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", + "minimum": 1, + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "Pull request number. Required when body is provided.", + "type": "number" + }, + "reaction": { + "description": "Emoji reaction to add. Required unless body is provided.", + "enum": [ + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes" + ], + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "commentId" + ], + "type": "object" + }, + "name": "add_reply_to_pull_request_comment", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "comment": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + "reaction": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + } + }, + "required": [ + "comment", + "reaction" + ], + "type": "object" + } + ] + } +} \ 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 acb74bcd7a..852ef1349d 100644 --- a/pkg/github/__toolsnaps__/create_pull_request.snap +++ b/pkg/github/__toolsnaps__/create_pull_request.snap @@ -65,5 +65,44 @@ ], "type": "object" }, - "name": "create_pull_request" + "name": "create_pull_request", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "reason": { + "type": "string" + }, + "status": { + "enum": [ + "awaiting_user_submission" + ], + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "type": "object" + } + ] + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_pull_request_typed.snap b/pkg/github/__toolsnaps__/create_pull_request_typed.snap new file mode 100644 index 0000000000..c91abdadaf --- /dev/null +++ b/pkg/github/__toolsnaps__/create_pull_request_typed.snap @@ -0,0 +1,122 @@ +{ + "_meta": { + "ui": { + "resourceUri": "ui://github-mcp-server/pr-write", + "visibility": [ + "model", + "app" + ] + } + }, + "annotations": { + "idempotentHint": false, + "readOnlyHint": false, + "title": "Open new pull request" + }, + "description": "Create a new pull request in a GitHub repository.", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "base": { + "description": "Branch to merge into", + "type": "string" + }, + "body": { + "description": "PR description", + "type": "string" + }, + "draft": { + "description": "Create as draft PR", + "type": "boolean" + }, + "head": { + "description": "Branch containing changes", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "reviewers": { + "description": "GitHub usernames or ORG/team-slug team reviewers to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "description": "PR title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], + "type": "object" + }, + "name": "create_pull_request", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "reason": { + "type": "string" + }, + "status": { + "enum": [ + "awaiting_user_submission" + ], + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "type": "object" + } + ] + } +} \ 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 35275cb0e8..57024219fd 100644 --- a/pkg/github/__toolsnaps__/merge_pull_request.snap +++ b/pkg/github/__toolsnaps__/merge_pull_request.snap @@ -60,5 +60,32 @@ ], "type": "object" }, - "name": "merge_pull_request" -} + "name": "merge_pull_request", + "outputSchema": { + "additionalProperties": false, + "properties": { + "merged": { + "type": [ + "null", + "boolean" + ] + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "sha": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "object", + "null" + ] + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/merge_pull_request_typed.snap b/pkg/github/__toolsnaps__/merge_pull_request_typed.snap new file mode 100644 index 0000000000..d9adcfd1f2 --- /dev/null +++ b/pkg/github/__toolsnaps__/merge_pull_request_typed.snap @@ -0,0 +1,93 @@ +{ + "annotations": { + "idempotentHint": false, + "readOnlyHint": false, + "title": "Merge pull request" + }, + "description": "Merge a pull request in a GitHub repository.", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACeElEQVRIibWVTUhUYRSGn/e74+iiQih1F9Vcmj9sptylUVBYkO4jcNeuJBdFKxe1CYQokGrRKjCEdtmqwEVmtqomQWeiUdc2EBUtUufe0yLHn1KLGXtX5zvn4zz3vd8f/Gfp90Qs0drmpA6MT1EveDo1NfV92wB+KnMdo39Nfs4L7eSHD5Nz1QJcJYglWtsw+iUehAuRRjO1g+0KHLerbb4OIHnHAC1FdW129s3XmUJuwnBDoOPbA7BwHsD7QWq1HKYN5msBRCpB1AueLoSROSkciSUyj5ClhE6BLtYC8CpBqVRabNrdMmIiJdQjuUbQ1WI+d78WwIbykxnzU9np7ejlNq2YxQ4ebNtTKyCyWcEgYl55EDj/a7ihFEtkLkr0As2YxjwL+9aem00dCEYNzvnJzLDvH27aaM5y80HEnKGHKGwPnEbT6fSOvzpAmrDQnkncpC7siiUzz2QqIPu25iOuGBorTufO/AJmH0v2ajHwuoHhrQHATOH9rQPJ7IjDLgs6kZ0F6it1AzArVcZLdUE+WnYgmv/uYFmz+dxH4NJGNT+RfYLCE7F4tn0pGkxHy94AmBm8/GfAVvIs7AukUTkbj5YdYIbZ9WJh8m1lzrrbNB4/tD+QuyPsdCibF26gmM/dY/NdRDqd3rEYeN04mswYL+ZXm68DxOPxnWXXMClsp+GGhCWBTtClYj53t1qXK78oVH2XYB/mHZ0pvHsN4Cczzw3rBaoGrJ6D5ZUvN1i+kjI0LWiptjmscbC88hZZCAf2trZeq1v0UsJ6wF7UAlhxUMxPvkW6AboQLbvPcjaO+BIx11cL4I9H308eOiLRQUhpOx79/66fNKzrOCYNDm0AAAAASUVORK5CYII=", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABjElEQVRIibWVPS/DURTGnysSC0HiZdWVrZ28JDaLT8BHaBsMdjqZJDXiAzC2LF5mX6GtATGiIsGARH+Gnj9X8a/kf3uWe3Py3Oc559xz75E6bK7VAWQkzUi6lXTonHsOpgYUgAZfdgmkQpFnjHwb6AemgDpQCiWwYlEPeL4i8JCEt8vb39g67vkmPH8yA3qt5nVgCzi1jLJBBEwkBZSAdxPKAj86LYQQQCU4cYvAKzDUSYF3YC+uRIAD8sA58ACU//VuTODE1n1g+A9c3jBH1tJ1a5TeCPNrdACSCpKeJG1IepN0LKkm6dGDrkqqOOdm7dyUpDNJi865PUnqjsvEObcJHEhaljQnaV5STwvszttXbR2J441KtB4LauLKVpZpYBDYte8mHUogZTWPrAGstTtQBl6AayDX7qHZD7AALMVGDvQBV5ZyETi2qHLtMvmXWRQAk57vBKgl4fV/0+jmq56vImk0icCnAWm7pB3riGngnlADx0TW+T4yL4CxJJy/Df20mkP/TqGHfifsA7INs3X5i3+yAAAAAElFTkSuQmCC", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "commit_message": { + "description": "Extra detail for merge commit", + "type": "string" + }, + "commit_title": { + "description": "Title for merge commit", + "type": "string" + }, + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "merge_method": { + "description": "Merge method", + "enum": [ + "merge", + "squash", + "rebase" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "merge_pull_request", + "outputSchema": { + "additionalProperties": false, + "properties": { + "merged": { + "type": [ + "null", + "boolean" + ] + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "sha": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "object", + "null" + ] + } +} \ 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 d518c7cad9..e284bf9718 100644 --- a/pkg/github/__toolsnaps__/pull_request_read.snap +++ b/pkg/github/__toolsnaps__/pull_request_read.snap @@ -58,5 +58,912 @@ ], "type": "object" }, - "name": "pull_request_read" + "name": "pull_request_read", + "outputSchema": { + "oneOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "additions": { + "type": "integer" + }, + "assignees": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "base": { + "additionalProperties": false, + "properties": { + "ref": { + "type": "string" + }, + "repo": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "full_name": { + "type": "string" + } + }, + "required": [ + "full_name" + ], + "type": [ + "null", + "object" + ] + }, + "sha": { + "type": "string" + } + }, + "required": [ + "ref", + "sha" + ], + "type": [ + "null", + "object" + ] + }, + "body": { + "type": "string" + }, + "changed_files": { + "type": "integer" + }, + "closed_at": { + "type": "string" + }, + "comments": { + "type": "integer" + }, + "commits": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "deletions": { + "type": "integer" + }, + "draft": { + "type": "boolean" + }, + "head": { + "additionalProperties": false, + "properties": { + "ref": { + "type": "string" + }, + "repo": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "full_name": { + "type": "string" + } + }, + "required": [ + "full_name" + ], + "type": [ + "null", + "object" + ] + }, + "sha": { + "type": "string" + } + }, + "required": [ + "ref", + "sha" + ], + "type": [ + "null", + "object" + ] + }, + "html_url": { + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "mergeable_state": { + "type": "string" + }, + "merged": { + "type": "boolean" + }, + "merged_at": { + "type": "string" + }, + "merged_by": { + "type": "string" + }, + "milestone": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "requested_reviewers": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "state": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + } + }, + "required": [ + "number", + "title", + "state", + "draft", + "merged", + "html_url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "diff": { + "type": "string" + } + }, + "required": [ + "diff" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "sha": { + "type": "string" + }, + "state": { + "type": "string" + }, + "statuses": { + "items": { + "additionalProperties": false, + "properties": { + "context": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "type": "string" + }, + "target_url": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "state", + "context" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "state", + "sha", + "total_count", + "statuses" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "pageInfo": { + "additionalProperties": false, + "properties": { + "endCursor": { + "type": "string" + }, + "hasNextPage": { + "type": "boolean" + }, + "hasPreviousPage": { + "type": "boolean" + }, + "startCursor": { + "type": "string" + } + }, + "required": [ + "hasNextPage", + "hasPreviousPage" + ], + "type": "object" + }, + "review_threads": { + "items": { + "additionalProperties": false, + "properties": { + "comments": { + "items": { + "additionalProperties": false, + "properties": { + "author": { + "type": "string" + }, + "body": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "line": { + "type": [ + "null", + "integer" + ] + }, + "original_line": { + "type": [ + "null", + "integer" + ] + }, + "original_start_line": { + "type": [ + "null", + "integer" + ] + }, + "path": { + "type": "string" + }, + "start_line": { + "type": [ + "null", + "integer" + ] + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "path", + "html_url" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "id": { + "type": "string" + }, + "is_collapsed": { + "type": "boolean" + }, + "is_outdated": { + "type": "boolean" + }, + "is_resolved": { + "type": "boolean" + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "id", + "is_resolved", + "is_outdated", + "is_collapsed", + "comments", + "total_count" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "totalCount": { + "type": "integer" + } + }, + "required": [ + "review_threads", + "totalCount", + "pageInfo" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "check_runs": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "type": "string" + }, + "conclusion": { + "type": "string" + }, + "details_url": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "status" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "total_count", + "check_runs" + ], + "type": "object" + }, + { + "anyOf": [ + { + "items": { + "additionalProperties": false, + "properties": { + "additions": { + "type": "integer" + }, + "changes": { + "type": "integer" + }, + "deletions": { + "type": "integer" + }, + "filename": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "previous_filename": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "filename" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + { + "items": { + "additionalProperties": false, + "properties": { + "author": { + "additionalProperties": false, + "properties": { + "date": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": [ + "null", + "object" + ] + }, + "html_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "required": [ + "sha" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + { + "items": { + "additionalProperties": false, + "properties": { + "author_association": { + "type": "string" + }, + "body": { + "type": "string" + }, + "commit_id": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "state": { + "type": "string" + }, + "submitted_at": { + "type": "string" + }, + "user": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + } + }, + "required": [ + "id", + "state", + "html_url" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + { + "items": { + "additionalProperties": false, + "properties": { + "author_association": { + "type": "string" + }, + "body": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "reactions": { + "additionalProperties": false, + "properties": { + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "rocket": { + "type": "integer" + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes" + ], + "type": [ + "null", + "object" + ] + }, + "updated_at": { + "type": "string" + }, + "user": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + } + }, + "required": [ + "id", + "html_url" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + } + ], + "type": "array" + } + ] + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/pull_request_read_typed.snap b/pkg/github/__toolsnaps__/pull_request_read_typed.snap new file mode 100644 index 0000000000..312b55b9cd --- /dev/null +++ b/pkg/github/__toolsnaps__/pull_request_read_typed.snap @@ -0,0 +1,983 @@ +{ + "annotations": { + "idempotentHint": false, + "readOnlyHint": true, + "title": "Get details for a single pull request" + }, + "description": "Get information on a specific pull request in GitHub repository.", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "after": { + "description": "Cursor for pagination, used only by the get_review_comments method. Pass the endCursor from the previous page's PageInfo to fetch the next page.", + "type": "string" + }, + "method": { + "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 combined commit status of a head commit in a pull request.\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_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.\n 6. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns thread metadata and comments with nullable current and original line-range coordinates (line, start_line, original_line, original_start_line). Current coordinates are omitted when unavailable, such as for outdated comments. Use cursor-based pagination (perPage, after) to control results.\n 7. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.\n 8. 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 9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.\n", + "enum": [ + "get", + "get_diff", + "get_status", + "get_files", + "get_commits", + "get_review_comments", + "get_reviews", + "get_comments", + "get_check_runs" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_read", + "outputSchema": { + "oneOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "additions": { + "type": "integer" + }, + "assignees": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "base": { + "additionalProperties": false, + "properties": { + "ref": { + "type": "string" + }, + "repo": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "full_name": { + "type": "string" + } + }, + "required": [ + "full_name" + ], + "type": [ + "null", + "object" + ] + }, + "sha": { + "type": "string" + } + }, + "required": [ + "ref", + "sha" + ], + "type": [ + "null", + "object" + ] + }, + "body": { + "type": "string" + }, + "changed_files": { + "type": "integer" + }, + "closed_at": { + "type": "string" + }, + "comments": { + "type": "integer" + }, + "commits": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "deletions": { + "type": "integer" + }, + "draft": { + "type": "boolean" + }, + "head": { + "additionalProperties": false, + "properties": { + "ref": { + "type": "string" + }, + "repo": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "full_name": { + "type": "string" + } + }, + "required": [ + "full_name" + ], + "type": [ + "null", + "object" + ] + }, + "sha": { + "type": "string" + } + }, + "required": [ + "ref", + "sha" + ], + "type": [ + "null", + "object" + ] + }, + "html_url": { + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "mergeable_state": { + "type": "string" + }, + "merged": { + "type": "boolean" + }, + "merged_at": { + "type": "string" + }, + "merged_by": { + "type": "string" + }, + "milestone": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "requested_reviewers": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "state": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + } + }, + "required": [ + "number", + "title", + "state", + "draft", + "merged", + "html_url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "diff": { + "type": "string" + } + }, + "required": [ + "diff" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "sha": { + "type": "string" + }, + "state": { + "type": "string" + }, + "statuses": { + "items": { + "additionalProperties": false, + "properties": { + "context": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "type": "string" + }, + "target_url": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "state", + "context" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "state", + "sha", + "total_count", + "statuses" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "pageInfo": { + "additionalProperties": false, + "properties": { + "endCursor": { + "type": "string" + }, + "hasNextPage": { + "type": "boolean" + }, + "hasPreviousPage": { + "type": "boolean" + }, + "startCursor": { + "type": "string" + } + }, + "required": [ + "hasNextPage", + "hasPreviousPage" + ], + "type": "object" + }, + "review_threads": { + "items": { + "additionalProperties": false, + "properties": { + "comments": { + "items": { + "additionalProperties": false, + "properties": { + "author": { + "type": "string" + }, + "body": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "line": { + "type": [ + "null", + "integer" + ] + }, + "original_line": { + "type": [ + "null", + "integer" + ] + }, + "original_start_line": { + "type": [ + "null", + "integer" + ] + }, + "path": { + "type": "string" + }, + "start_line": { + "type": [ + "null", + "integer" + ] + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "path", + "html_url" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "id": { + "type": "string" + }, + "is_collapsed": { + "type": "boolean" + }, + "is_outdated": { + "type": "boolean" + }, + "is_resolved": { + "type": "boolean" + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "id", + "is_resolved", + "is_outdated", + "is_collapsed", + "comments", + "total_count" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "totalCount": { + "type": "integer" + } + }, + "required": [ + "review_threads", + "totalCount", + "pageInfo" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "check_runs": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "type": "string" + }, + "conclusion": { + "type": "string" + }, + "details_url": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "status" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "total_count", + "check_runs" + ], + "type": "object" + }, + { + "anyOf": [ + { + "items": { + "additionalProperties": false, + "properties": { + "additions": { + "type": "integer" + }, + "changes": { + "type": "integer" + }, + "deletions": { + "type": "integer" + }, + "filename": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "previous_filename": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "filename" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + { + "items": { + "additionalProperties": false, + "properties": { + "author": { + "additionalProperties": false, + "properties": { + "date": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": [ + "null", + "object" + ] + }, + "html_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "required": [ + "sha" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + { + "items": { + "additionalProperties": false, + "properties": { + "author_association": { + "type": "string" + }, + "body": { + "type": "string" + }, + "commit_id": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "state": { + "type": "string" + }, + "submitted_at": { + "type": "string" + }, + "user": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + } + }, + "required": [ + "id", + "state", + "html_url" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + { + "items": { + "additionalProperties": false, + "properties": { + "author_association": { + "type": "string" + }, + "body": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "reactions": { + "additionalProperties": false, + "properties": { + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "rocket": { + "type": "integer" + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes" + ], + "type": [ + "null", + "object" + ] + }, + "updated_at": { + "type": "string" + }, + "user": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + } + }, + "required": [ + "id", + "html_url" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + } + ], + "type": "array" + } + ] + } +} \ 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 74ef808559..9177599aee 100644 --- a/pkg/github/__toolsnaps__/pull_request_review_write.snap +++ b/pkg/github/__toolsnaps__/pull_request_review_write.snap @@ -60,5 +60,17 @@ ], "type": "object" }, - "name": "pull_request_review_write" + "name": "pull_request_review_write", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason_typed.snap b/pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason_typed.snap new file mode 100644 index 0000000000..c4078e462b --- /dev/null +++ b/pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason_typed.snap @@ -0,0 +1,94 @@ +{ + "annotations": { + "idempotentHint": false, + "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- resolve_thread: Resolve a review thread. Requires only \"threadId\" parameter with the thread's node ID (e.g., PRRT_kwDOxxx). The owner, repo, and pullNumber parameters are not used for this method. Resolving an already-resolved thread is a no-op.\n- unresolve_thread: Unresolve a previously resolved review thread. Requires only \"threadId\" parameter. The owner, repo, and pullNumber parameters are not used for this method. Unresolving an already-unresolved thread is a no-op.\n", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform.", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "method": { + "description": "The write operation to perform on pull request review.", + "enum": [ + "create", + "submit_pending", + "delete_pending", + "resolve_thread", + "unresolve_thread" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "resolutionReason": { + "description": "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.", + "type": "string" + }, + "threadId": { + "description": "The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_review_write", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/pull_request_review_write_typed.snap b/pkg/github/__toolsnaps__/pull_request_review_write_typed.snap new file mode 100644 index 0000000000..605ded7381 --- /dev/null +++ b/pkg/github/__toolsnaps__/pull_request_review_write_typed.snap @@ -0,0 +1,90 @@ +{ + "annotations": { + "idempotentHint": false, + "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- resolve_thread: Resolve a review thread. Requires only \"threadId\" parameter with the thread's node ID (e.g., PRRT_kwDOxxx). The owner, repo, and pullNumber parameters are not used for this method. Resolving an already-resolved thread is a no-op.\n- unresolve_thread: Unresolve a previously resolved review thread. Requires only \"threadId\" parameter. The owner, repo, and pullNumber parameters are not used for this method. Unresolving an already-unresolved thread is a no-op.\n", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform.", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "method": { + "description": "The write operation to perform on pull request review.", + "enum": [ + "create", + "submit_pending", + "delete_pending", + "resolve_thread", + "unresolve_thread" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "threadId": { + "description": "The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_review_write", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ 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 ae037a3e0d..47526b8201 100644 --- a/pkg/github/__toolsnaps__/update_pull_request.snap +++ b/pkg/github/__toolsnaps__/update_pull_request.snap @@ -71,5 +71,44 @@ ], "type": "object" }, - "name": "update_pull_request" + "name": "update_pull_request", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "reason": { + "type": "string" + }, + "status": { + "enum": [ + "awaiting_user_submission" + ], + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "type": "object" + } + ] + } } \ 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 633ce6fb88..fb1e826bc0 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_branch.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_branch.snap @@ -31,5 +31,26 @@ ], "type": "object" }, - "name": "update_pull_request_branch" + "name": "update_pull_request_branch", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "object", + "null" + ] + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap new file mode 100644 index 0000000000..8d2b08025f --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap @@ -0,0 +1,70 @@ +{ + "annotations": { + "idempotentHint": false, + "readOnlyHint": false, + "title": "Update pull request branch" + }, + "description": "Update the branch of a pull request with the latest changes from the base branch.", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request_branch", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "object", + "null" + ] + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_typed.snap new file mode 100644 index 0000000000..1382556fa8 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request_typed.snap @@ -0,0 +1,128 @@ +{ + "_meta": { + "ui": { + "resourceUri": "ui://github-mcp-server/pr-edit", + "visibility": [ + "model", + "app" + ] + } + }, + "annotations": { + "idempotentHint": false, + "readOnlyHint": false, + "title": "Edit pull request" + }, + "description": "Update an existing pull request in a GitHub repository.", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "base": { + "description": "New base branch name", + "type": "string" + }, + "body": { + "description": "New description", + "type": "string" + }, + "draft": { + "description": "Mark pull request as draft (true) or ready for review (false)", + "type": "boolean" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "Pull request number to update", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "reviewers": { + "description": "GitHub usernames or ORG/team-slug team reviewers to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "reason": { + "type": "string" + }, + "status": { + "enum": [ + "awaiting_user_submission" + ], + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "type": "object" + } + ] + } +} \ No newline at end of file diff --git a/pkg/github/consolidated_pull_request_types.go b/pkg/github/consolidated_pull_request_types.go new file mode 100644 index 0000000000..03c54741c2 --- /dev/null +++ b/pkg/github/consolidated_pull_request_types.go @@ -0,0 +1,502 @@ +package github + +import ( + "encoding/json" + "fmt" + "maps" + "slices" + "strings" + + "github.com/github/github-mcp-server/pkg/utils" + "github.com/go-viper/mapstructure/v2" + "github.com/google/go-github/v92/github" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type PullRequestReadInput struct { + Method string `json:"method"` + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber int `json:"pullNumber"` + Page *int `json:"page,omitempty"` + PerPage *int `json:"perPage,omitempty"` + After *string `json:"after,omitempty"` +} + +type CreatePullRequestInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + Title *string `json:"title,omitempty"` + Body *string `json:"body,omitempty"` + Head *string `json:"head,omitempty"` + Base *string `json:"base,omitempty"` + Draft *bool `json:"draft,omitempty"` + MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"` + Reviewers *[]string `json:"reviewers,omitempty"` + UISubmitted *bool `json:"_ui_submitted,omitempty"` +} + +// UpdatePullRequestInput uses pointers because the handler distinguishes +// omitted fields from explicit zero values such as draft=false. +type UpdatePullRequestInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber int `json:"pullNumber"` + Title *string `json:"title,omitempty"` + Body *string `json:"body,omitempty"` + State *string `json:"state,omitempty"` + Draft *bool `json:"draft,omitempty"` + Base *string `json:"base,omitempty"` + MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"` + Reviewers *[]string `json:"reviewers,omitempty"` + UISubmitted *bool `json:"_ui_submitted,omitempty"` +} + +type MergePullRequestInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber int `json:"pullNumber"` + CommitTitle *string `json:"commit_title,omitempty"` + CommitMessage *string `json:"commit_message,omitempty"` + MergeMethod *string `json:"merge_method,omitempty"` + ExpectedHeadSHA *string `json:"expectedHeadSha,omitempty"` +} + +type UpdatePullRequestBranchInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber int `json:"pullNumber"` + ExpectedHeadSHA *string `json:"expectedHeadSha,omitempty"` +} + +// PullRequestReviewWriteInput covers both feature variants; resolutionReason +// is advertised and used only by the resolution-reason variant. +type PullRequestReviewWriteInput struct { + Method string `json:"method"` + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber int `json:"pullNumber"` + Body string `json:"body,omitempty"` + Event string `json:"event,omitempty"` + CommitID *string `json:"commitID,omitempty"` + ThreadID string `json:"threadId,omitempty"` + ResolutionReason *string `json:"resolutionReason,omitempty"` +} + +type AddCommentToPendingReviewInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber int `json:"pullNumber"` + Path string `json:"path"` + Body string `json:"body"` + SubjectType string `json:"subjectType"` + Line *int `json:"line,omitempty"` + Side *string `json:"side,omitempty"` + StartLine *int `json:"startLine,omitempty"` + StartSide *string `json:"startSide,omitempty"` +} + +type AddReplyToPullRequestCommentInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber *int `json:"pullNumber,omitempty"` + CommentID int64 `json:"commentId"` + Body *string `json:"body,omitempty"` + Reaction *string `json:"reaction,omitempty"` +} + +// PullRequestReadOutput mirrors the method-specific top-level shapes that +// pull_request_read has always returned as text. +type PullRequestReadOutput struct { + PullRequest *MinimalPullRequest + Diff *PullRequestDiffOutput + Status *MinimalCombinedStatus + Files *[]MinimalPRFile + Commits *[]MinimalPullRequestCommit + ReviewThreads *MinimalReviewThreadsResponse + Reviews *[]MinimalPullRequestReview + Comments *[]MinimalIssueComment + CheckRuns *MinimalCheckRunsResult +} + +// PullRequestDiffOutput wraps the raw diff, which the text content returns verbatim. +type PullRequestDiffOutput struct { + Diff string `json:"diff"` +} + +func (out PullRequestReadOutput) MarshalJSON() ([]byte, error) { + switch { + case out.PullRequest != nil: + return json.Marshal(out.PullRequest) + case out.Diff != nil: + return json.Marshal(out.Diff) + case out.Status != nil: + return json.Marshal(out.Status) + case out.Files != nil: + return json.Marshal(out.Files) + case out.Commits != nil: + return json.Marshal(out.Commits) + case out.ReviewThreads != nil: + return json.Marshal(out.ReviewThreads) + case out.Reviews != nil: + return json.Marshal(out.Reviews) + case out.Comments != nil: + return json.Marshal(out.Comments) + case out.CheckRuns != nil: + return json.Marshal(out.CheckRuns) + default: + // The SDK validates this zero value even for error results before the + // protocol middleware removes it from the wire result. + return []byte("null"), nil + } +} + +func pullRequestCommentsOutput(output *IssueReadOutput) *PullRequestReadOutput { + if output == nil || output.Comments == nil { + return nil + } + return &PullRequestReadOutput{Comments: output.Comments} +} + +func pullRequestReadOutputSchema() *jsonschema.Schema { + arrays := []*jsonschema.Schema{ + repositoryOutputSchema[[]MinimalPRFile](), + repositoryOutputSchema[[]MinimalPullRequestCommit](), + repositoryOutputSchema[[]MinimalPullRequestReview](), + repositoryOutputSchema[[]MinimalIssueComment](), + } + for _, array := range arrays { + array.Defs = nil + } + // An empty array is valid for every list method, so the list shapes form + // one anyOf array variant instead of ambiguous oneOf branches. + return repositoryUnionSchema( + &jsonschema.Schema{Type: "null"}, + repositoryOutputSchema[MinimalPullRequest](), + repositoryOutputSchema[PullRequestDiffOutput](), + repositoryOutputSchema[MinimalCombinedStatus](), + repositoryOutputSchema[MinimalReviewThreadsResponse](), + repositoryOutputSchema[MinimalCheckRunsResult](), + &jsonschema.Schema{Type: "array", AnyOf: arrays}, + ) +} + +// PullRequestWriteOutput is either the created/updated pull request reference +// or the notice that an interactive form is awaiting user submission. +type PullRequestWriteOutput struct { + PullRequest *MinimalResponse + Awaiting *IssueWriteAwaitingOutput +} + +func (out PullRequestWriteOutput) MarshalJSON() ([]byte, error) { + if out.Awaiting != nil { + return json.Marshal(out.Awaiting) + } + if out.PullRequest != nil { + return json.Marshal(out.PullRequest) + } + return json.Marshal(MinimalResponse{}) +} + +func pullRequestWriteOutputSchema() *jsonschema.Schema { + return issueWriteOutputSchema() +} + +func pullRequestFormArguments(req *mcp.CallToolRequest, args map[string]any) (map[string]any, error) { + // Form deferral depends on which parameters the caller actually sent, + // so inspect the raw request rather than the normalized typed input. + if req == nil || req.Params == nil || len(req.Params.Arguments) == 0 { + return args, nil + } + var formArgs map[string]any + if err := json.Unmarshal(req.Params.Arguments, &formArgs); err != nil { + return nil, err + } + return formArgs, nil +} + +func pullRequestAwaitingFormResult(message string) (*mcp.CallToolResult, *PullRequestWriteOutput, error) { + result := utils.NewToolResultAwaitingFormSubmission(message) + output := &IssueWriteAwaitingOutput{ + Status: "awaiting_user_submission", + Reason: "An interactive form is being shown to the user. The operation has not been performed.", + } + result.StructuredContent = output + return result, &PullRequestWriteOutput{Awaiting: output}, nil +} + +// PullRequestMergeOutput preserves a JSON null merge response rather than +// letting the SDK replace a nil pointer with an empty object. +type PullRequestMergeOutput struct { + Result *github.PullRequestMergeResult +} + +func (out PullRequestMergeOutput) MarshalJSON() ([]byte, error) { + return json.Marshal(out.Result) +} + +func pullRequestMergeOutputSchema() *jsonschema.Schema { + return nullableRepositoryOutputSchema[github.PullRequestMergeResult]() +} + +// PullRequestBranchUpdateOutput carries either the API response or, for an +// accepted asynchronous update, the in-progress message. +type PullRequestBranchUpdateOutput struct { + Result *github.PullRequestBranchUpdateResponse +} + +func (out PullRequestBranchUpdateOutput) MarshalJSON() ([]byte, error) { + return json.Marshal(out.Result) +} + +func pullRequestBranchUpdateOutputSchema() *jsonschema.Schema { + return nullableRepositoryOutputSchema[github.PullRequestBranchUpdateResponse]() +} + +func nullableRepositoryOutputSchema[T any]() *jsonschema.Schema { + schema := repositoryOutputSchema[T]() + schema.Type = "" + schema.Types = []string{"object", "null"} + return schema +} + +// PullRequestCommentReplyOutput is a single reply or reaction reference, or +// both when the caller requested a reply and a reaction together. +type PullRequestCommentReplyOutput struct { + Response *MinimalResponse + ReplyAndReaction *PullRequestReplyAndReactionOutput +} + +// PullRequestReplyAndReactionOutput keeps the legacy map's sorted key order. +type PullRequestReplyAndReactionOutput struct { + Comment MinimalResponse `json:"comment"` + Reaction MinimalResponse `json:"reaction"` +} + +func (out PullRequestCommentReplyOutput) MarshalJSON() ([]byte, error) { + if out.ReplyAndReaction != nil { + return json.Marshal(out.ReplyAndReaction) + } + if out.Response != nil { + return json.Marshal(out.Response) + } + return json.Marshal(MinimalResponse{}) +} + +func pullRequestCommentReplyOutputSchema() *jsonschema.Schema { + return repositoryUnionSchema( + repositoryOutputSchema[MinimalResponse](), + repositoryOutputSchema[PullRequestReplyAndReactionOutput](), + ) +} + +// pullRequestMessageResult exposes the success message of review mutations +// whose exported helpers only return a text result. +func pullRequestMessageResult(result *mcp.CallToolResult, err error) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + if err != nil || result == nil || result.IsError || len(result.Content) != 1 { + return result, nil, err + } + text, ok := result.Content[0].(*mcp.TextContent) + if !ok { + return result, nil, nil + } + return result, &RepositoryMessageOutput{Message: text.Text}, nil +} + +// pullRequestArgumentSpec lists parameters in the order the legacy handler +// read them, so normalization reports the same first error the handler did. +type pullRequestArgumentSpec struct { + required []string + requiredInts []string + ints []string + strings []string + bools []string + stringArrays []string + ignoredStrings []string + // deferred required strings are filled with "" so the handler can still + // show its interactive form before reporting them as missing. + deferred []string + methods []string +} + +var pullRequestArgumentSpecs = map[string]pullRequestArgumentSpec{ + "read": { + required: []string{"method", "owner", "repo"}, requiredInts: []string{"pullNumber"}, + ints: []string{"page", "perPage"}, strings: []string{"after"}, + methods: []string{"get", "get_diff", "get_status", "get_files", "get_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"}, + }, + "create": { + required: []string{"owner", "repo"}, deferred: []string{"title", "head", "base"}, + strings: []string{"title", "head", "base", "body"}, bools: []string{"draft", "maintainer_can_modify", "_ui_submitted"}, + stringArrays: []string{"reviewers"}, + }, + "update": { + required: []string{"owner", "repo"}, requiredInts: []string{"pullNumber"}, + strings: []string{"title", "body", "state", "base"}, bools: []string{"draft", "maintainer_can_modify", "_ui_submitted"}, + stringArrays: []string{"reviewers"}, + }, + "merge": { + required: []string{"owner", "repo"}, requiredInts: []string{"pullNumber"}, + strings: []string{"commit_title", "commit_message", "merge_method", "expectedHeadSha"}, + }, + "branch": { + required: []string{"owner", "repo"}, requiredInts: []string{"pullNumber"}, strings: []string{"expectedHeadSha"}, + }, + "pending_comment": { + required: []string{"owner", "repo"}, requiredInts: []string{"pullNumber"}, + ints: []string{"line", "startLine"}, ignoredStrings: []string{"side", "startSide"}, + }, + "reply": { + required: []string{"owner", "repo"}, strings: []string{"body", "reaction"}, + }, +} + +// normalizePullRequestArguments replays the legacy handler's parameter +// checks before strict schema validation. Legacy handlers bypassed schema +// validation, so they accepted numeric strings and whole floats and reported +// their own error messages; those messages are preserved here. +func normalizePullRequestArguments(kind string) func(json.RawMessage) (json.RawMessage, error) { + spec, ok := pullRequestArgumentSpecs[kind] + if !ok { + panic("unknown pull request argument kind: " + kind) + } + return func(raw json.RawMessage) (json.RawMessage, error) { + var args map[string]any + if err := json.Unmarshal(raw, &args); err != nil { + return nil, err + } + for _, field := range spec.required { + if _, err := RequiredParam[string](args, field); err != nil { + return nil, err + } + } + for _, field := range spec.requiredInts { + value, err := RequiredInt(args, field) + if err != nil { + return nil, err + } + args[field] = value + } + if kind == "pending_comment" { + for _, field := range []string{"path", "body", "subjectType"} { + if _, err := RequiredParam[string](args, field); err != nil { + return nil, err + } + } + } + if kind == "reply" { + commentID, err := RequiredBigInt(args, "commentId") + if err != nil { + return nil, err + } + if commentID < 1 { + return nil, fmt.Errorf("commentId must be greater than 0") + } + args["commentId"] = commentID + // pullNumber is only read, and validated, when a reply body is sent. + if value, exists := args["pullNumber"]; exists { + pullNumber, err := toInt(value) + switch { + case err == nil: + args["pullNumber"] = pullNumber + case args["body"] != nil: + return nil, fmt.Errorf("parameter pullNumber is not a valid number: %w", err) + default: + delete(args, "pullNumber") + } + } + } + for _, field := range spec.deferred { + if _, exists := args[field]; !exists { + args[field] = "" + } + } + for _, field := range spec.strings { + if _, err := OptionalParam[string](args, field); err != nil { + return nil, err + } + } + for _, field := range spec.bools { + if _, err := OptionalParam[bool](args, field); err != nil { + return nil, err + } + } + for _, field := range spec.stringArrays { + if _, exists := args[field]; !exists { + continue + } + values, err := OptionalStringArrayParam(args, field) + if err != nil { + return nil, err + } + args[field] = values + } + for _, field := range spec.ints { + if _, exists := args[field]; !exists { + continue + } + value, err := OptionalIntParam(args, field) + if err != nil { + return nil, err + } + args[field] = value + } + if spec.methods != nil && !slices.Contains(spec.methods, args["method"].(string)) { + return nil, fmt.Errorf("unknown method: %s", args["method"]) + } + for _, field := range spec.ignoredStrings { + if _, isString := args[field].(string); !isString { + delete(args, field) + } + } + return json.Marshal(args) + } +} + +// normalizePullRequestReviewWriteArguments retains the legacy WeakDecode +// semantics: case-insensitive keys, numeric-string conversion and fractional +// truncation of pullNumber. +func normalizePullRequestReviewWriteArguments(raw json.RawMessage) (json.RawMessage, error) { + var args map[string]any + if err := json.Unmarshal(raw, &args); err != nil { + return nil, err + } + var params PullRequestReviewWriteParams + if err := mapstructure.WeakDecode(args, ¶ms); err != nil { + return nil, err + } + if !slices.Contains([]string{"create", "submit_pending", "delete_pending", "resolve_thread", "unresolve_thread"}, params.Method) { + return nil, fmt.Errorf("unknown method: %s", params.Method) + } + canonical := map[string]any{ + "method": params.Method, "owner": params.Owner, "repo": params.Repo, "pullNumber": params.PullNumber, + } + // Empty optional strings are equivalent to omitted ones for the handler, + // but would fail the schema's enum for event. + for key, value := range map[string]string{"body": params.Body, "event": params.Event, "threadId": params.ThreadID} { + if value != "" { + canonical[key] = value + } + } + if params.CommitID != nil { + canonical["commitID"] = *params.CommitID + } + if params.ResolutionReason != nil { + canonical["resolutionReason"] = *params.ResolutionReason + } + for key := range args { + for name := range pullRequestReviewWriteFields { + if strings.EqualFold(key, name) { + delete(args, key) + } + } + } + maps.Copy(args, canonical) + return json.Marshal(args) +} + +var pullRequestReviewWriteFields = map[string]struct{}{ + "method": {}, "owner": {}, "repo": {}, "pullNumber": {}, "body": {}, "event": {}, + "commitID": {}, "threadId": {}, "resolutionReason": {}, +} diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index a8493bc521..c866f31ba1 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -68,11 +68,12 @@ Possible options: Description: "Cursor for pagination, used only by the get_review_comments method. Pass the endCursor from the previous page's PageInfo to fetch the next page.", } - return NewTool( + return NewTool[PullRequestReadInput, *PullRequestReadOutput]( ToolsetMetadataPullRequests, mcp.Tool{ - Name: "pull_request_read", - Description: t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository."), + Name: "pull_request_read", + OutputSchema: pullRequestReadOutputSchema(), + 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, @@ -80,7 +81,11 @@ Possible options: InputSchema: schema, }, scopes.PublicRead(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input PullRequestReadInput) (*mcp.CallToolResult, *PullRequestReadOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } method, err := RequiredParam[string](args, "method") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -120,20 +125,20 @@ Possible options: switch method { case "get": - result, err := GetPullRequest(ctx, client, deps, owner, repo, pullNumber) - return attachIFC(result), nil, err + result, output, err := getPullRequest(ctx, client, deps, owner, repo, pullNumber) + return attachIFC(result), output, err case "get_diff": - result, err := GetPullRequestDiff(ctx, client, deps, owner, repo, pullNumber) - return attachIFC(result), nil, err + result, output, err := getPullRequestDiff(ctx, client, deps, owner, repo, pullNumber) + return attachIFC(result), output, err case "get_status": - result, err := GetPullRequestStatus(ctx, client, owner, repo, pullNumber) - return attachIFC(result), nil, err + result, output, err := getPullRequestStatus(ctx, client, owner, repo, pullNumber) + return attachIFC(result), output, err case "get_files": - result, err := GetPullRequestFiles(ctx, client, deps, owner, repo, pullNumber, pagination) - return attachIFC(result), nil, err + result, output, err := getPullRequestFiles(ctx, client, deps, owner, repo, pullNumber, pagination) + return attachIFC(result), output, err case "get_commits": - result, err := GetPullRequestCommits(ctx, client, deps, owner, repo, pullNumber, pagination) - return attachIFC(result), nil, err + result, output, err := getPullRequestCommits(ctx, client, deps, owner, repo, pullNumber, pagination) + return attachIFC(result), output, err case "get_review_comments": gqlClient, err := deps.GetGQLClient(ctx) if err != nil { @@ -143,27 +148,32 @@ Possible options: if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - result, err := GetPullRequestReviewComments(ctx, gqlClient, deps, owner, repo, pullNumber, cursorPagination) - return attachIFC(result), nil, err + result, output, err := getPullRequestReviewComments(ctx, gqlClient, deps, owner, repo, pullNumber, cursorPagination) + return attachIFC(result), output, err case "get_reviews": - result, err := GetPullRequestReviews(ctx, client, deps, owner, repo, pullNumber, pagination) - return attachIFC(result), nil, err + result, output, err := getPullRequestReviews(ctx, client, deps, owner, repo, pullNumber, pagination) + return attachIFC(result), output, err case "get_comments": - result, err := GetIssueComments(ctx, client, deps, owner, repo, pullNumber, pagination) - return attachIFC(result), nil, err + result, output, err := getIssueComments(ctx, client, deps, owner, repo, pullNumber, pagination) + return attachIFC(result), pullRequestCommentsOutput(output), err case "get_check_runs": - result, err := GetPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination) - return attachIFC(result), nil, err + result, output, err := getPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination) + return attachIFC(result), output, err default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil } - }) + }, normalizePullRequestArguments("read")) } func GetPullRequest(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + result, _, err := getPullRequest(ctx, client, deps, owner, repo, pullNumber) + return result, err +} + +func getPullRequest(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int) (*mcp.CallToolResult, *PullRequestReadOutput, error) { cache, err := deps.GetRepoAccessCache(ctx) if err != nil { - return nil, fmt.Errorf("failed to get repo access cache: %w", err) + return nil, nil, fmt.Errorf("failed to get repo access cache: %w", err) } ff := deps.GetFlags(ctx) @@ -173,27 +183,27 @@ func GetPullRequest(ctx context.Context, client *github.Client, deps ToolDepende "failed to get pull request", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil, nil } if ff.LockdownMode { if restricted, err := authorLockdownResult(ctx, cache, owner, repo, pr.GetUser().GetLogin(), lockdownPullRequestRestrictedMessage); restricted != nil || err != nil { - return restricted, err + return restricted, nil, err } } minimalPR := convertToMinimalPullRequest(pr) - return MarshalledTextResult(minimalPR), nil + return MarshalledTextResult(minimalPR), &PullRequestReadOutput{PullRequest: &minimalPR}, nil } // enforcePullRequestLockdown returns a restricted tool result when lockdown mode is @@ -226,8 +236,13 @@ func enforcePullRequestLockdown(ctx context.Context, client *github.Client, deps } func GetPullRequestDiff(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + result, _, err := getPullRequestDiff(ctx, client, deps, owner, repo, pullNumber) + return result, err +} + +func getPullRequestDiff(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int) (*mcp.CallToolResult, *PullRequestReadOutput, error) { if restricted, err := enforcePullRequestLockdown(ctx, client, deps, owner, repo, pullNumber); restricted != nil || err != nil { - return restricted, err + return restricted, nil, err } raw, resp, err := client.PullRequests.GetRaw( @@ -242,40 +257,45 @@ func GetPullRequestDiff(ctx context.Context, client *github.Client, deps ToolDep "failed to get pull request diff", resp, err, - ), nil + ), nil, nil } if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request diff", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request diff", resp, body), nil, nil } defer func() { _ = resp.Body.Close() }() // Return the raw response - return utils.NewToolResultText(string(raw)), nil + return utils.NewToolResultText(string(raw)), &PullRequestReadOutput{Diff: &PullRequestDiffOutput{Diff: string(raw)}}, nil } func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + result, _, err := getPullRequestStatus(ctx, client, owner, repo, pullNumber) + return result, err +} + +func getPullRequestStatus(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, *PullRequestReadOutput, error) { pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil, nil } // Get combined status for the head SHA @@ -285,27 +305,33 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep "failed to get combined status", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get combined status", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get combined status", resp, body), nil, nil } - r, err := json.Marshal(convertToMinimalCombinedStatus(status)) + minimalStatus := convertToMinimalCombinedStatus(status) + r, err := json.Marshal(minimalStatus) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), &PullRequestReadOutput{Status: &minimalStatus}, nil } func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { + result, _, err := getPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination) + return result, err +} + +func getPullRequestCheckRuns(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, *PullRequestReadOutput, error) { // First get the PR to get the head SHA pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { @@ -313,16 +339,16 @@ func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner, "failed to get pull request", resp, err, - ), nil + ), nil, nil } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil, nil } // Get check runs for the head SHA @@ -339,16 +365,16 @@ func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner, "failed to get check runs", resp, err, - ), nil + ), nil, nil } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get check runs", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get check runs", resp, body), nil, nil } // Convert to minimal check runs to reduce context usage @@ -364,15 +390,20 @@ func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner, r, err := json.Marshal(minimalResult) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), &PullRequestReadOutput{CheckRuns: &minimalResult}, nil } func GetPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { + result, _, err := getPullRequestFiles(ctx, client, deps, owner, repo, pullNumber, pagination) + return result, err +} + +func getPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, *PullRequestReadOutput, error) { if restricted, err := enforcePullRequestLockdown(ctx, client, deps, owner, repo, pullNumber); restricted != nil || err != nil { - return restricted, err + return restricted, nil, err } opts := &github.ListOptions{ @@ -385,29 +416,34 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDe "failed to get pull request files", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request files", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request files", resp, body), nil, nil } minimalFiles := convertToMinimalPRFiles(files) - return MarshalledTextResult(minimalFiles), nil + return MarshalledTextResult(minimalFiles), &PullRequestReadOutput{Files: &minimalFiles}, nil } // GetPullRequestCommits returns the commits on a pull request. Under lockdown // mode it checks the PR author once rather than per commit, since every // commit on the PR belongs to the same untrusted head branch. func GetPullRequestCommits(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { + result, _, err := getPullRequestCommits(ctx, client, deps, owner, repo, pullNumber, pagination) + return result, err +} + +func getPullRequestCommits(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, *PullRequestReadOutput, error) { if restricted, err := enforcePullRequestLockdown(ctx, client, deps, owner, repo, pullNumber); restricted != nil || err != nil { - return restricted, err + return restricted, nil, err } opts := &github.ListOptions{ @@ -420,21 +456,21 @@ func GetPullRequestCommits(ctx context.Context, client *github.Client, deps Tool "failed to get pull request commits", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request commits", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request commits", resp, body), nil, nil } minimalCommits := convertToMinimalPullRequestCommits(commits) - return MarshalledTextResult(minimalCommits), nil + return MarshalledTextResult(minimalCommits), &PullRequestReadOutput{Commits: &minimalCommits}, nil } // GraphQL types for review threads query @@ -485,16 +521,21 @@ type pageInfoFragment struct { } func GetPullRequestReviewComments(ctx context.Context, gqlClient *githubv4.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination CursorPaginationParams) (*mcp.CallToolResult, error) { + result, _, err := getPullRequestReviewComments(ctx, gqlClient, deps, owner, repo, pullNumber, pagination) + return result, err +} + +func getPullRequestReviewComments(ctx context.Context, gqlClient *githubv4.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination CursorPaginationParams) (*mcp.CallToolResult, *PullRequestReadOutput, error) { cache, err := deps.GetRepoAccessCache(ctx) if err != nil { - return nil, fmt.Errorf("failed to get repo access cache: %w", err) + return nil, nil, fmt.Errorf("failed to get repo access cache: %w", err) } ff := deps.GetFlags(ctx) // Convert pagination parameters to GraphQL format gqlParams, err := pagination.ToGraphQLParams() if err != nil { - return utils.NewToolResultError(fmt.Sprintf("invalid pagination parameters: %v", err)), nil + return utils.NewToolResultError(fmt.Sprintf("invalid pagination parameters: %v", err)), nil, nil } // Build variables for GraphQL query @@ -519,13 +560,13 @@ func GetPullRequestReviewComments(ctx context.Context, gqlClient *githubv4.Clien return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get pull request review threads", err, - ), nil + ), nil, nil } // Lockdown mode filtering if ff.LockdownMode { if cache == nil { - return nil, fmt.Errorf("lockdown cache is not configured") + return nil, nil, fmt.Errorf("lockdown cache is not configured") } // Iterate through threads and filter comments @@ -538,7 +579,7 @@ func GetPullRequestReviewComments(ctx context.Context, gqlClient *githubv4.Clien if login != "" { isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo) if err != nil { - return nil, fmt.Errorf("failed to check lockdown mode: %w", err) + return nil, nil, fmt.Errorf("failed to check lockdown mode: %w", err) } if isSafeContent { filteredComments = append(filteredComments, comment) @@ -551,13 +592,19 @@ func GetPullRequestReviewComments(ctx context.Context, gqlClient *githubv4.Clien } } - return MarshalledTextResult(convertToMinimalReviewThreadsResponse(query)), nil + threads := convertToMinimalReviewThreadsResponse(query) + return MarshalledTextResult(threads), &PullRequestReadOutput{ReviewThreads: &threads}, nil } func GetPullRequestReviews(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { + result, _, err := getPullRequestReviews(ctx, client, deps, owner, repo, pullNumber, pagination) + return result, err +} + +func getPullRequestReviews(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, *PullRequestReadOutput, error) { cache, err := deps.GetRepoAccessCache(ctx) if err != nil { - return nil, fmt.Errorf("failed to get repo access cache: %w", err) + return nil, nil, fmt.Errorf("failed to get repo access cache: %w", err) } ff := deps.GetFlags(ctx) @@ -570,21 +617,21 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, deps Tool "failed to get pull request reviews", resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request reviews", resp, body), nil + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request reviews", resp, body), nil, nil } if ff.LockdownMode { if cache == nil { - return nil, fmt.Errorf("lockdown cache is not configured") + return nil, nil, fmt.Errorf("lockdown cache is not configured") } filteredReviews := make([]*github.PullRequestReview, 0, len(reviews)) for _, review := range reviews { @@ -594,7 +641,7 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, deps Tool } isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo) if err != nil { - return nil, fmt.Errorf("failed to check lockdown mode: %w", err) + return nil, nil, fmt.Errorf("failed to check lockdown mode: %w", err) } if isSafeContent { filteredReviews = append(filteredReviews, review) @@ -608,7 +655,7 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, deps Tool minimalReviews = append(minimalReviews, convertToMinimalPullRequestReview(review)) } - return MarshalledTextResult(minimalReviews), nil + return MarshalledTextResult(minimalReviews), &PullRequestReadOutput{Reviews: &minimalReviews}, nil } // PullRequestWriteUIResourceURI is the URI for the create_pull_request tool's MCP App UI resource. @@ -649,11 +696,12 @@ var pullRequestUpdateFormParams = map[string]struct{}{ // CreatePullRequest creates a tool to create a new pull request. func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool { - return NewTool( + return NewTool[CreatePullRequestInput, *PullRequestWriteOutput]( ToolsetMetadataPullRequests, mcp.Tool{ - Name: "create_pull_request", - Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."), + Name: "create_pull_request", + OutputSchema: pullRequestWriteOutputSchema(), + Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), ReadOnlyHint: false, @@ -711,7 +759,11 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo }, }, publicRepositoryWriteScopeAccess(), - func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, input CreatePullRequestInput) (*mcp.CallToolResult, *PullRequestWriteOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -723,15 +775,19 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo // Hand off to the interactive MCP App form unless this call must // execute now (see shouldDeferToForm). - if shouldDeferToForm(ctx, deps, req, args, pullRequestWriteFormParams) { - return utils.NewToolResultAwaitingFormSubmission(fmt.Sprintf( + formArgs, err := pullRequestFormArguments(req, args) + if err != nil { + return nil, nil, err + } + if shouldDeferToForm(ctx, deps, req, formArgs, pullRequestWriteFormParams) { + return pullRequestAwaitingFormResult(fmt.Sprintf( "An interactive form has been shown to the user for creating a new pull request in %s/%s. "+ "STOP — do not call any other tools, do not respond as if the pull request was created, "+ "and do not claim the operation succeeded. The pull request has NOT been created yet; "+ "only the form was rendered. Wait silently for the user to review and click Submit. "+ "When they do, the real result will be delivered to your context automatically.", owner, repo, - )), nil, nil + )) } // When creating PR, title/head/base are required @@ -853,8 +909,8 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - }) + return utils.NewToolResultText(string(r)), &PullRequestWriteOutput{PullRequest: &minimalResponse}, nil + }, normalizePullRequestArguments("create")) } // UpdatePullRequest creates a tool to update an existing pull request. @@ -910,11 +966,12 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo Required: []string{"owner", "repo", "pullNumber"}, } - st := NewTool( + st := NewTool[UpdatePullRequestInput, *PullRequestWriteOutput]( ToolsetMetadataPullRequests, mcp.Tool{ - Name: "update_pull_request", - Description: t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository."), + Name: "update_pull_request", + OutputSchema: pullRequestWriteOutputSchema(), + 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: false, @@ -928,7 +985,11 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo InputSchema: schema, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, input UpdatePullRequestInput) (*mcp.CallToolResult, *PullRequestWriteOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -944,15 +1005,19 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo // Hand off to the interactive MCP App form unless this call must // execute now (see shouldDeferToForm). - if shouldDeferToForm(ctx, deps, req, args, pullRequestUpdateFormParams) { - return utils.NewToolResultAwaitingFormSubmission(fmt.Sprintf( + formArgs, err := pullRequestFormArguments(req, args) + if err != nil { + return nil, nil, err + } + if shouldDeferToForm(ctx, deps, req, formArgs, pullRequestUpdateFormParams) { + return pullRequestAwaitingFormResult(fmt.Sprintf( "An interactive form has been shown to the user for editing pull request #%d in %s/%s. "+ "STOP — do not call any other tools, do not respond as if the pull request was updated, "+ "and do not claim the operation succeeded. The pull request has NOT been updated yet; "+ "only the form was rendered. Wait silently for the user to review and click Submit. "+ "When they do, the real result will be delivered to your context automatically.", pullNumber, owner, repo, - )), nil, nil + )) } _, draftProvided := args["draft"] @@ -1168,8 +1233,8 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo return utils.NewToolResultErrorFromErr("Failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - }) + return utils.NewToolResultText(string(r)), &PullRequestWriteOutput{PullRequest: &minimalResponse}, nil + }, normalizePullRequestArguments("update")) st.FeatureRule = pullRequestsConsolidatedRule return st } @@ -1209,11 +1274,12 @@ func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventor Required: []string{"owner", "repo", "commentId"}, } - return NewTool( + return NewTool[AddReplyToPullRequestCommentInput, *PullRequestCommentReplyOutput]( ToolsetMetadataPullRequests, mcp.Tool{ - Name: "add_reply_to_pull_request_comment", - Description: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a reply and/or reaction to an existing pull request comment. This can create a new comment linked as a reply to the specified comment, add an emoji reaction to the specified comment, or do both. At least one of body or reaction is required."), + Name: "add_reply_to_pull_request_comment", + OutputSchema: pullRequestCommentReplyOutputSchema(), + Description: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a reply and/or reaction to an existing pull request comment. This can create a new comment linked as a reply to the specified comment, add an emoji reaction to the specified comment, or do both. At least one of body or reaction is required."), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_USER_TITLE", "Add reply to pull request comment"), ReadOnlyHint: false, @@ -1221,7 +1287,11 @@ func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventor InputSchema: schema, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input AddReplyToPullRequestCommentInput) (*mcp.CallToolResult, *PullRequestCommentReplyOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -1303,26 +1373,26 @@ func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventor } } - var result any + var output PullRequestCommentReplyOutput switch { case hasBody && hasReaction: - result = map[string]MinimalResponse{ - "comment": *commentResponse, - "reaction": *reactionResponse, + output.ReplyAndReaction = &PullRequestReplyAndReactionOutput{ + Comment: *commentResponse, + Reaction: *reactionResponse, } case hasReaction: - result = reactionResponse + output.Response = reactionResponse default: - result = commentResponse + output.Response = commentResponse } - r, err := json.Marshal(result) + r, err := json.Marshal(output) if err != nil { return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - }) + return utils.NewToolResultText(string(r)), &output, nil + }, normalizePullRequestArguments("reply")) } // ListPullRequests creates a tool to list pull requests in a GitHub repository. @@ -1632,12 +1702,13 @@ func MergePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool Required: []string{"owner", "repo", "pullNumber"}, } - return NewTool( + return NewTool[MergePullRequestInput, *PullRequestMergeOutput]( ToolsetMetadataPullRequests, mcp.Tool{ - Name: "merge_pull_request", - Description: t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository."), - Icons: octicons.Icons("git-merge"), + Name: "merge_pull_request", + OutputSchema: pullRequestMergeOutputSchema(), + Description: t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository."), + Icons: octicons.Icons("git-merge"), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"), ReadOnlyHint: false, @@ -1645,7 +1716,11 @@ func MergePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool InputSchema: schema, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input MergePullRequestInput) (*mcp.CallToolResult, *PullRequestMergeOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -1708,8 +1783,8 @@ func MergePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - }) + return utils.NewToolResultText(string(r)), &PullRequestMergeOutput{Result: result}, nil + }, normalizePullRequestArguments("merge")) } // SearchPullRequests creates a tool to search for pull requests. @@ -1807,11 +1882,12 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser Required: []string{"owner", "repo", "pullNumber"}, } - return NewTool( + return NewTool[UpdatePullRequestBranchInput, *PullRequestBranchUpdateOutput]( ToolsetMetadataPullRequests, 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."), + Name: "update_pull_request_branch", + OutputSchema: pullRequestBranchUpdateOutputSchema(), + 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: false, @@ -1819,7 +1895,11 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser InputSchema: schema, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input UpdatePullRequestBranchInput) (*mcp.CallToolResult, *PullRequestBranchUpdateOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -1850,7 +1930,8 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, // and it's not a real error. if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) { - return utils.NewToolResultText("Pull request branch update is in progress"), nil, nil + message := "Pull request branch update is in progress" + return utils.NewToolResultText(message), &PullRequestBranchUpdateOutput{Result: &github.PullRequestBranchUpdateResponse{Message: &message}}, nil } return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update pull request branch", @@ -1873,8 +1954,8 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - }) + return utils.NewToolResultText(string(r)), &PullRequestBranchUpdateOutput{Result: result}, nil + }, normalizePullRequestArguments("branch")) } type PullRequestReviewWriteParams struct { @@ -1956,10 +2037,11 @@ func pullRequestReviewWrite(t translations.TranslationHelperFunc, withResolution } } - st := NewTool( + st := NewTool[PullRequestReviewWriteInput, *RepositoryMessageOutput]( ToolsetMetadataPullRequests, mcp.Tool{ - Name: "pull_request_review_write", + Name: "pull_request_review_write", + OutputSchema: repositoryOutputSchema[RepositoryMessageOutput](), Description: t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request. Available methods: @@ -1976,7 +2058,11 @@ Available methods: InputSchema: schema, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input PullRequestReviewWriteInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } var params PullRequestReviewWriteParams if err := mapstructure.WeakDecode(args, ¶ms); err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -1988,30 +2074,32 @@ Available methods: return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil, nil } + var result *mcp.CallToolResult + switch params.Method { case "create": - result, err := CreatePullRequestReview(ctx, client, params) - return result, nil, err + result, err = CreatePullRequestReview(ctx, client, params) + return pullRequestMessageResult(result, err) case "submit_pending": - result, err := SubmitPendingPullRequestReview(ctx, client, params) - return result, nil, err + result, err = SubmitPendingPullRequestReview(ctx, client, params) + return pullRequestMessageResult(result, err) case "delete_pending": - result, err := DeletePendingPullRequestReview(ctx, client, params) - return result, nil, err + result, err = DeletePendingPullRequestReview(ctx, client, params) + return pullRequestMessageResult(result, err) case "resolve_thread": if !withResolutionReason { - result, err := ResolveReviewThread(ctx, client, params.ThreadID, true) - return result, nil, err + result, err = ResolveReviewThread(ctx, client, params.ThreadID, true) + return pullRequestMessageResult(result, err) } - result, err := ResolveReviewThreadWithReason(ctx, client, params.ThreadID, params.ResolutionReason, true) - return result, nil, err + result, err = ResolveReviewThreadWithReason(ctx, client, params.ThreadID, params.ResolutionReason, true) + return pullRequestMessageResult(result, err) case "unresolve_thread": - result, err := ResolveReviewThread(ctx, client, params.ThreadID, false) - return result, nil, err + result, err = ResolveReviewThread(ctx, client, params.ThreadID, false) + return pullRequestMessageResult(result, err) default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil } - }) + }, normalizePullRequestReviewWriteArguments) if withResolutionReason { st.FeatureRule = inventory.NewFeatureRule( []inventory.FeatureFlag{FeatureFlagThreadResolutionReason, inventory.FeatureFlag(FeatureFlagPullRequestsGranular)}, @@ -2443,11 +2531,12 @@ func AddCommentToPendingReview(t translations.TranslationHelperFunc) inventory.S Required: []string{"owner", "repo", "pullNumber", "path", "body", "subjectType"}, } - st := NewTool( + st := NewTool[AddCommentToPendingReviewInput, *RepositoryMessageOutput]( ToolsetMetadataPullRequests, 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)."), + Name: "add_comment_to_pending_review", + OutputSchema: repositoryOutputSchema[RepositoryMessageOutput](), + 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, @@ -2455,7 +2544,11 @@ func AddCommentToPendingReview(t translations.TranslationHelperFunc) inventory.S InputSchema: schema, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, input AddCommentToPendingReviewInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + args, err := discussionNotificationArguments(input) + if err != nil { + return nil, nil, err + } owner, err := RequiredParam[string](args, "owner") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -2525,8 +2618,8 @@ func AddCommentToPendingReview(t translations.TranslationHelperFunc) inventory.S StartLine: startLinePtr, StartSide: startSidePtr, }) - return result, nil, err - }) + return pullRequestMessageResult(result, err) + }, normalizePullRequestArguments("pending_comment")) st.FeatureRule = pullRequestsConsolidatedRule return st } diff --git a/pkg/github/typed_consolidated_pull_request_outputs_test.go b/pkg/github/typed_consolidated_pull_request_outputs_test.go new file mode 100644 index 0000000000..d242a60bf1 --- /dev/null +++ b/pkg/github/typed_consolidated_pull_request_outputs_test.go @@ -0,0 +1,457 @@ +package github + +import ( + "context" + "encoding/json" + "maps" + "net/http" + "strings" + "testing" + "time" + + "github.com/github/github-mcp-server/internal/toolsnaps" + ghcontext "github.com/github/github-mcp-server/pkg/context" + "github.com/github/github-mcp-server/pkg/inventory" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/shurcooL/githubv4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var typedPullRequestProtocols = []string{inventory.ProtocolVersionMultiRoundTrip, "2025-11-25", ""} + +func consolidatedPullRequestSession(t *testing.T, deps BaseDeps, protocol string, ui bool, flags ...inventory.FeatureFlag) (*mcp.ClientSession, map[string]*jsonschema.Resolved) { + t.Helper() + tr := translations.NullTranslationHelper + tools := []inventory.ServerTool{ + PullRequestRead(tr), CreatePullRequest(tr), UpdatePullRequest(tr), MergePullRequest(tr), + UpdatePullRequestBranch(tr), PullRequestReviewWrite(tr), PullRequestReviewWriteWithResolutionReason(tr), + AddCommentToPendingReview(tr), AddReplyToPullRequestComment(tr), + } + inv, err := inventory.NewBuilder().SetTools(tools).WithToolsets([]string{"all"}). + WithFeatureChecker(featureCheckerFor(flags...)).Build() + require.NoError(t, err) + server := mcp.NewServer(&mcp.Implementation{Name: "consolidated-pull-requests", Version: "v1"}, nil) + server.AddReceivingMiddleware(InjectDepsMiddleware(deps)) + inv.RegisterTools(context.Background(), server, deps) + server.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, request mcp.Request) (mcp.Result, error) { + if protocol == "" { + switch req := request.(type) { + case *mcp.ListToolsRequest: + req.Params.Meta = mcp.Meta{mcp.MetaKeyProtocolVersion: ""} + case *mcp.CallToolRequest: + req.Params.Meta = mcp.Meta{mcp.MetaKeyProtocolVersion: ""} + } + } + return next(ghcontext.WithUISupport(ctx, ui), method, request) + } + }) + version := protocol + if version == "" { + version = inventory.ProtocolVersionMultiRoundTrip + } + session := connectCommentVisibilityClient(t, server, version) + list, err := session.ListTools(context.Background(), nil) + require.NoError(t, err) + // Exactly one pull_request_review_write variant is exposed for a feature set. + require.Len(t, list.Tools, len(tools)-1) + schemas := make(map[string]*jsonschema.Resolved) + for _, tool := range list.Tools { + if protocol != inventory.ProtocolVersionMultiRoundTrip { + assert.Nil(t, tool.OutputSchema, tool.Name) + continue + } + require.NotNil(t, tool.OutputSchema, tool.Name) + name := tool.Name + if name == "pull_request_review_write" && len(flags) > 0 { + name += "_resolution_reason" + } + require.NoError(t, toolsnaps.Test(name+"_typed", *tool)) + var schema jsonschema.Schema + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, tool.OutputSchema)), &schema)) + resolved, err := schema.Resolve(nil) + require.NoError(t, err) + schemas[tool.Name] = resolved + } + return session, schemas +} + +const ( + typedPRJSON = `{"number":1,"title":"Subject","body":"body","state":"open","draft":false,"merged":false,"mergeable_state":"clean","html_url":"https://github.com/owner/repo/pull/1","user":{"login":"octocat"},"head":{"ref":"feature","sha":"abc"},"base":{"ref":"main","sha":"def"},"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z"}` + typedPRDiff = "diff --git a/file b/file\n+added\n" + typedPRReviewThreads = `{"data":{"repository":{"pullRequest":{"reviewThreads":{"nodes":[{"id":"T_1","isResolved":false,"isOutdated":false,"isCollapsed":false,"comments":{"nodes":[{"id":"C_1","body":"nit","path":"file","line":3,"originalLine":3,"startLine":null,"originalStartLine":null,"author":{"login":"octocat"},"createdAt":"2026-01-01T00:00:00Z","updatedAt":"2026-01-01T00:00:00Z","url":"https://github.com/owner/repo/pull/1#discussion_r1"}],"totalCount":1}}],"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"s","endCursor":"e"},"totalCount":1}}}}}` +) + +func consolidatedPullRequestDeps(t *testing.T) BaseDeps { + t.Helper() + rest := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/api/v3") + write := func(status int, body string) { + w.WriteHeader(status) + _, _ = w.Write([]byte(body)) + } + switch { + case path == "/repos/owner/repo/pulls/1" && strings.Contains(r.Header.Get("Accept"), "diff"): + write(http.StatusOK, typedPRDiff) + case path == "/repos/owner/repo/pulls/1" || path == "/repos/owner/repo/pulls" && r.Method == http.MethodPost: + status := http.StatusOK + if r.Method == http.MethodPost { + status = http.StatusCreated + } + write(status, typedPRJSON) + case path == "/repos/owner/repo/pulls/2": + // A sparse PR without a head SHA still yields a valid status payload. + write(http.StatusOK, `{"number":2,"head":{"sha":"abc"}}`) + case path == "/repos/owner/repo/commits/abc/status": + write(http.StatusOK, `{"state":"success","sha":"abc","total_count":1,"statuses":[{"state":"success","context":"ci","target_url":"https://ci"}]}`) + case path == "/repos/owner/repo/commits/abc/check-runs": + write(http.StatusOK, `{"total_count":1,"check_runs":[{"id":5,"name":"build","status":"completed","conclusion":"success"}]}`) + case path == "/repos/owner/repo/pulls/1/files": + write(http.StatusOK, `[{"filename":"file","status":"modified","additions":1,"changes":1,"patch":"+added"}]`) + case path == "/repos/owner/repo/pulls/2/files", path == "/repos/owner/repo/pulls/2/commits", + path == "/repos/owner/repo/pulls/2/reviews", path == "/repos/owner/repo/issues/2/comments": + write(http.StatusOK, `[]`) + case path == "/repos/owner/repo/pulls/1/commits": + write(http.StatusOK, `[{"sha":"abc","html_url":"https://github.com/owner/repo/commit/abc","commit":{"message":"msg","author":{"name":"Octo","email":"octo@example.com","date":"2026-01-01T00:00:00Z"}}}]`) + case path == "/repos/owner/repo/pulls/1/reviews": + write(http.StatusOK, `[{"id":9,"state":"APPROVED","body":"lgtm","html_url":"https://github.com/owner/repo/pull/1#pullrequestreview-9","user":{"login":"octocat"},"commit_id":"abc","author_association":"MEMBER"}]`) + case path == "/repos/owner/repo/issues/1/comments": + write(http.StatusOK, `[{"id":42,"body":"hello","html_url":"https://github.com/owner/repo/pull/1#issuecomment-42"}]`) + case path == "/repos/owner/repo/pulls/1/merge": + write(http.StatusOK, `{"sha":"abc","merged":true,"message":"Pull Request successfully merged"}`) + case path == "/repos/owner/repo/pulls/2/merge": + write(http.StatusOK, `null`) + case path == "/repos/owner/repo/pulls/1/update-branch": + write(http.StatusAccepted, `{"message":"Updating pull request branch.","url":"https://github.com/owner/repo/pull/1"}`) + case path == "/repos/owner/repo/pulls/2/update-branch": + write(http.StatusUnprocessableEntity, `{"message":"merge conflict between base and head"}`) + case path == "/repos/owner/repo/pulls/comments/42/reactions": + write(http.StatusCreated, `{"id":77,"content":"heart"}`) + case path == "/repos/owner/repo/pulls/1/comments": + write(http.StatusCreated, `{"id":43,"body":"reply","html_url":"https://github.com/owner/repo/pull/1#discussion_r43"}`) + default: + t.Errorf("unexpected REST request: %s %s", r.Method, r.URL.Path) + write(http.StatusInternalServerError, `{"message":"unexpected"}`) + } + })}} + gql := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Query string `json:"query"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + var body string + switch q := req.Query; { + case strings.Contains(q, "addPullRequestReviewThread("): + body = `{"data":{"addPullRequestReviewThread":{"thread":{"id":"T_2"}}}}` + case strings.Contains(q, "addPullRequestReview("): + body = `{"data":{"addPullRequestReview":{"pullRequestReview":{"id":"R_1"}}}}` + case strings.Contains(q, "submitPullRequestReview("): + body = `{"data":{"submitPullRequestReview":{"pullRequestReview":{"id":"R_1"}}}}` + case strings.Contains(q, "deletePullRequestReview("): + body = `{"data":{"deletePullRequestReview":{"pullRequestReview":{"id":"R_1"}}}}` + case strings.Contains(q, "unresolveReviewThread("): + body = `{"data":{"unresolveReviewThread":{"thread":{"id":"T_1","isResolved":false}}}}` + case strings.Contains(q, "resolveReviewThread("): + body = `{"data":{"resolveReviewThread":{"thread":{"id":"T_1","isResolved":true}}}}` + case strings.Contains(q, "reviewThreads("): + body = typedPRReviewThreads + case strings.Contains(q, "reviews(first: 100"): + body = `{"data":{"repository":{"pullRequest":{"reviews":{"nodes":[{"id":"R_1","author":{"userId":"U_1"}}],"pageInfo":{"hasNextPage":false,"endCursor":""}}}}}}` + case strings.Contains(q, "viewer{"): + body = `{"data":{"viewer":{"id":"U_1"}}}` + case strings.Contains(q, "pullRequest(number: $prNum){id}"): + body = `{"data":{"repository":{"pullRequest":{"id":"PR_1"}}}}` + default: + t.Errorf("unexpected GraphQL query: %s", req.Query) + w.WriteHeader(http.StatusInternalServerError) + return + } + _, _ = w.Write([]byte(body)) + })}} + return BaseDeps{ + Client: mustNewGHClient(t, rest), GQLClient: githubv4.NewClient(gql), + RepoAccessCache: stubRepoAccessCache(nil, time.Minute), + } +} + +type typedPullRequestCase struct { + tool string + args map[string]any + text string +} + +func runTypedPullRequestCases(t *testing.T, session *mcp.ClientSession, schemas map[string]*jsonschema.Resolved, cases []typedPullRequestCase) { + t.Helper() + for _, tc := range cases { + args := map[string]any{"owner": "owner", "repo": "repo"} + maps.Copy(args, tc.args) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.tool, Arguments: args}) + require.NoError(t, err) + require.False(t, result.IsError, "%s %v: %s", tc.tool, tc.args, mustMarshalJSON(t, result)) + require.Len(t, result.Content, 1) + text := getTextResult(t, result).Text + assert.Equal(t, tc.text, text, "%s %v", tc.tool, tc.args) + schema := schemas[tc.tool] + if schema == nil { + assert.Nil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) + continue + } + if text == "null" { + assert.Nil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) + continue + } + require.NotNil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) + var value any + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, result.StructuredContent)), &value)) + require.NoError(t, schema.Validate(value), "%s %v", tc.tool, tc.args) + switch { + case tc.args["method"] == "get_diff": + assert.JSONEq(t, mustMarshalJSON(t, PullRequestDiffOutput{Diff: text}), mustMarshalJSON(t, value)) + case tc.tool == "pull_request_review_write" || tc.tool == "add_comment_to_pending_review": + assert.JSONEq(t, mustMarshalJSON(t, RepositoryMessageOutput{Message: text}), mustMarshalJSON(t, value)) + case tc.tool == "update_pull_request_branch": + assert.JSONEq(t, `{"message":"Pull request branch update is in progress"}`, mustMarshalJSON(t, value)) + default: + assert.JSONEq(t, text, mustMarshalJSON(t, value)) + } + } +} + +func TestTypedConsolidatedPullRequestOutputs(t *testing.T) { + const minimalPR = `{"number":1,"title":"Subject","body":"body","state":"open","draft":false,"merged":false,"mergeable_state":"clean","html_url":"https://github.com/owner/repo/pull/1","user":{"login":"octocat"},"head":{"ref":"feature","sha":"abc"},"base":{"ref":"main","sha":"def"},"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z"}` + const prRef = `{"id":"0","url":"https://github.com/owner/repo/pull/1"}` + cases := []typedPullRequestCase{ + {"pull_request_read", map[string]any{"method": "get", "pullNumber": "1"}, minimalPR}, + {"pull_request_read", map[string]any{"method": "get_diff", "pullNumber": 1.0}, typedPRDiff}, + {"pull_request_read", map[string]any{"method": "get_status", "pullNumber": 1}, `{"state":"success","sha":"abc","total_count":1,"statuses":[{"state":"success","context":"ci","target_url":"https://ci"}]}`}, + {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 1, "page": "1", "perPage": 5}, `[{"filename":"file","status":"modified","additions":1,"changes":1,"patch":"+added"}]`}, + {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 2}, `[]`}, + {"pull_request_read", map[string]any{"method": "get_commits", "pullNumber": 1}, `[{"sha":"abc","html_url":"https://github.com/owner/repo/commit/abc","message":"msg","author":{"name":"Octo","email":"octo@example.com","date":"2026-01-01T00:00:00Z"}}]`}, + {"pull_request_read", map[string]any{"method": "get_commits", "pullNumber": 2}, `[]`}, + {"pull_request_read", map[string]any{"method": "get_review_comments", "pullNumber": 1, "perPage": 10, "after": "cursor"}, `{"review_threads":[{"id":"T_1","is_resolved":false,"is_outdated":false,"is_collapsed":false,"comments":[{"body":"nit","path":"file","line":3,"original_line":3,"author":"octocat","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z","html_url":"https://github.com/owner/repo/pull/1#discussion_r1"}],"total_count":1}],"totalCount":1,"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"s","endCursor":"e"}}`}, + {"pull_request_read", map[string]any{"method": "get_reviews", "pullNumber": 1}, `[{"id":9,"state":"APPROVED","body":"lgtm","html_url":"https://github.com/owner/repo/pull/1#pullrequestreview-9","user":{"login":"octocat"},"commit_id":"abc","author_association":"MEMBER"}]`}, + {"pull_request_read", map[string]any{"method": "get_reviews", "pullNumber": 2}, `[]`}, + {"pull_request_read", map[string]any{"method": "get_comments", "pullNumber": 1}, `[{"id":42,"body":"hello","html_url":"https://github.com/owner/repo/pull/1#issuecomment-42"}]`}, + {"pull_request_read", map[string]any{"method": "get_comments", "pullNumber": 2}, `[]`}, + {"pull_request_read", map[string]any{"method": "get_check_runs", "pullNumber": 1}, `{"total_count":1,"check_runs":[{"id":5,"name":"build","status":"completed","conclusion":"success"}]}`}, + {"create_pull_request", map[string]any{"title": "Subject", "head": "feature", "base": "main", "draft": false}, prRef}, + {"update_pull_request", map[string]any{"pullNumber": "1", "title": "Subject", "maintainer_can_modify": false}, prRef}, + {"merge_pull_request", map[string]any{"pullNumber": 1, "merge_method": "squash", "expectedHeadSha": "abc"}, `{"sha":"abc","merged":true,"message":"Pull Request successfully merged"}`}, + {"merge_pull_request", map[string]any{"pullNumber": 2}, `null`}, + {"merge_pull_request", map[string]any{"pullNumber": 1.0, "merge_method": "squash", "expectedHeadSha": "abc"}, `{"sha":"abc","merged":true,"message":"Pull Request successfully merged"}`}, + {"update_pull_request_branch", map[string]any{"pullNumber": "1", "expectedHeadSha": "abc"}, "Pull request branch update is in progress"}, + {"pull_request_review_write", map[string]any{"method": "create", "pullNumber": "1"}, "pending pull request created"}, + {"pull_request_review_write", map[string]any{"method": "create", "pullNumber": 1, "event": "APPROVE", "body": "lgtm", "commitID": "abc"}, "pull request review submitted successfully"}, + {"pull_request_review_write", map[string]any{"method": "submit_pending", "pullNumber": 1, "event": "COMMENT", "body": "done"}, "pending pull request review successfully submitted"}, + {"pull_request_review_write", map[string]any{"method": "delete_pending", "pullNumber": 1}, "pending pull request review successfully deleted"}, + {"pull_request_review_write", map[string]any{"method": "resolve_thread", "threadId": "T_1"}, "review thread resolved successfully"}, + {"pull_request_review_write", map[string]any{"method": "unresolve_thread", "threadId": "T_1"}, "review thread unresolved successfully"}, + {"add_comment_to_pending_review", map[string]any{"pullNumber": "1", "path": "file", "body": "nit", "subjectType": "LINE", "line": "3", "side": "RIGHT", "startLine": 2, "startSide": 7}, "pull request review comment successfully added to pending review"}, + {"add_reply_to_pull_request_comment", map[string]any{"pullNumber": "1", "commentId": "42", "body": "reply"}, `{"id":"43","url":"https://github.com/owner/repo/pull/1#discussion_r43"}`}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "reaction": "heart"}, `{"id":"77","url":"https://api.github.com/repos/owner/repo/pulls/comments/42/reactions/77"}`}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "reaction": "heart"}, `{"id":"77","url":"https://api.github.com/repos/owner/repo/pulls/comments/42/reactions/77"}`}, + {"add_reply_to_pull_request_comment", map[string]any{"pullNumber": 1, "commentId": 42.0, "body": "reply", "reaction": "heart"}, `{"comment":{"id":"43","url":"https://github.com/owner/repo/pull/1#discussion_r43"},"reaction":{"id":"77","url":"https://api.github.com/repos/owner/repo/pulls/comments/42/reactions/77"}}`}, + } + for _, protocol := range typedPullRequestProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + session, schemas := consolidatedPullRequestSession(t, consolidatedPullRequestDeps(t), protocol, false) + runTypedPullRequestCases(t, session, schemas, cases) + }) + } +} + +func TestTypedPullRequestReviewWriteResolutionReasonVariant(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + session, schemas := consolidatedPullRequestSession(t, consolidatedPullRequestDeps(t), protocol, false, FeatureFlagThreadResolutionReason) + list, err := session.ListTools(context.Background(), nil) + require.NoError(t, err) + for _, tool := range list.Tools { + if tool.Name == "pull_request_review_write" { + assert.Contains(t, mustMarshalJSON(t, tool.InputSchema), "resolutionReason") + } + } + runTypedPullRequestCases(t, session, schemas, []typedPullRequestCase{ + {"pull_request_review_write", map[string]any{"Method": "resolve_thread", "ThreadID": "T_1", "resolutionReason": "OUTDATED"}, "review thread resolved successfully"}, + {"pull_request_review_write", map[string]any{"method": "create", "pullNumber": 1.5}, "pending pull request created"}, + }) + }) + } +} + +func TestTypedPullRequestWriteAwaitingForm(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + session, schemas := consolidatedPullRequestSession(t, consolidatedPullRequestDeps(t), protocol, true) + for tool, args := range map[string]map[string]any{ + "create_pull_request": {"owner": "owner", "repo": "repo", "title": "Subject", "head": "feature", "base": "main"}, + "update_pull_request": {"owner": "owner", "repo": "repo", "pullNumber": 1, "title": "Subject"}, + } { + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tool, Arguments: args}) + require.NoError(t, err) + require.True(t, result.IsError, tool) + assert.Contains(t, getErrorResult(t, result).Text, "interactive form") + if protocol == inventory.ProtocolVersionMultiRoundTrip { + require.NotNil(t, result.StructuredContent) + assert.JSONEq(t, `{"status":"awaiting_user_submission","reason":"An interactive form is being shown to the user. The operation has not been performed."}`, mustMarshalJSON(t, result.StructuredContent)) + var value any + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, result.StructuredContent)), &value)) + require.NoError(t, schemas[tool].Validate(value)) + } else { + assert.Nil(t, result.StructuredContent) + } + args["_ui_submitted"] = true + runTypedPullRequestCases(t, session, schemas, []typedPullRequestCase{{tool, args, `{"id":"0","url":"https://github.com/owner/repo/pull/1"}`}}) + } + }) + } +} + +func TestTypedConsolidatedPullRequestErrors(t *testing.T) { + cases := []typedPullRequestCase{ + {"pull_request_read", map[string]any{"pullNumber": 1}, "missing required parameter: method"}, + {"pull_request_read", map[string]any{"method": "get", "pullNumber": 0}, "missing required parameter: pullNumber"}, + {"pull_request_read", map[string]any{"method": "get", "pullNumber": "1.5"}, "parameter pullNumber is not a valid number: non-integer numeric value: 1.5"}, + {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 1, "page": "x"}, "parameter page is not a valid number"}, + {"pull_request_read", map[string]any{"method": "unknown", "pullNumber": 1}, "unknown method: unknown"}, + {"pull_request_read", map[string]any{"method": "get", "pullNumber": 1.5}, "parameter pullNumber is not a valid number: non-integer numeric value: 1.5"}, + {"create_pull_request", map[string]any{"title": "Subject", "head": "feature", "base": "main", "draft": "yes"}, "parameter draft is not of type bool, is string"}, + {"update_pull_request", map[string]any{"pullNumber": 0, "title": "Subject"}, "missing required parameter: pullNumber"}, + {"pull_request_review_write", map[string]any{"method": "nope", "pullNumber": 1}, "unknown method: nope"}, + {"add_comment_to_pending_review", map[string]any{"pullNumber": 1, "body": "nit", "subjectType": "LINE"}, "missing required parameter: path"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 0, "body": "reply"}, "missing required parameter: commentId"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": -1, "body": "reply"}, "commentId must be greater than 0"}, + {"create_pull_request", map[string]any{"head": "feature", "base": "main"}, "missing required parameter: title"}, + {"update_pull_request", map[string]any{"pullNumber": 1}, "No update parameters provided"}, + {"merge_pull_request", map[string]any{"pullNumber": "abc"}, "parameter pullNumber is not a valid number: invalid numeric value: abc"}, + {"update_pull_request_branch", map[string]any{"pullNumber": 2}, "merge conflict between base and head"}, + {"pull_request_review_write", map[string]any{"method": "resolve_thread"}, "threadId is required for resolve_thread and unresolve_thread methods"}, + {"pull_request_review_write", map[string]any{"method": "create", "pullNumber": "x"}, "'PullNumber' cannot parse value as 'int32'"}, + {"add_comment_to_pending_review", map[string]any{"pullNumber": 1, "path": "file", "body": "nit", "subjectType": "LINE", "line": 1.5}, "parameter line is not a valid number"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42}, "at least one of body or reaction is required"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "body": "reply"}, "missing required parameter: pullNumber"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": "1e30", "body": "reply"}, "parameter commentId is not a valid number: numeric value 1e+30 is too large to fit in int64"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "body": "reply"}, "parameter pullNumber is not a valid number: invalid numeric value: x"}, + } + for _, protocol := range typedPullRequestProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + session, _ := consolidatedPullRequestSession(t, consolidatedPullRequestDeps(t), protocol, false) + for _, tc := range cases { + args := map[string]any{"owner": "owner", "repo": "repo"} + maps.Copy(args, tc.args) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.tool, Arguments: args}) + require.NoError(t, err, "%s %v", tc.tool, tc.args) + require.True(t, result.IsError, "%s %v: %s", tc.tool, tc.args, mustMarshalJSON(t, result)) + assert.Contains(t, getErrorResult(t, result).Text, tc.text, "%s %v", tc.tool, tc.args) + assert.Nil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) + } + }) + } +} + +func TestTypedConsolidatedPullRequestAPIErrors(t *testing.T) { + forbidden := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) + })}} + calls := []typedPullRequestCase{ + {"create_pull_request", map[string]any{"title": "Subject", "head": "feature", "base": "main"}, ""}, + {"update_pull_request", map[string]any{"pullNumber": 1, "title": "Subject"}, ""}, + {"merge_pull_request", map[string]any{"pullNumber": 1}, ""}, + {"update_pull_request_branch", map[string]any{"pullNumber": 1}, ""}, + {"pull_request_review_write", map[string]any{"method": "create", "pullNumber": 1}, ""}, + {"pull_request_review_write", map[string]any{"method": "submit_pending", "pullNumber": 1}, ""}, + {"pull_request_review_write", map[string]any{"method": "delete_pending", "pullNumber": 1}, ""}, + {"pull_request_review_write", map[string]any{"method": "resolve_thread", "threadId": "T_1"}, ""}, + {"pull_request_review_write", map[string]any{"method": "unresolve_thread", "threadId": "T_1"}, ""}, + {"add_comment_to_pending_review", map[string]any{"pullNumber": 1, "path": "file", "body": "nit", "subjectType": "FILE"}, ""}, + {"add_reply_to_pull_request_comment", map[string]any{"pullNumber": 1, "commentId": 42, "body": "reply"}, ""}, + } + for _, method := range []string{"get", "get_diff", "get_status", "get_files", "get_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"} { + calls = append(calls, typedPullRequestCase{"pull_request_read", map[string]any{"method": method, "pullNumber": 1}, ""}) + } + for _, protocol := range typedPullRequestProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + deps := BaseDeps{ + Client: mustNewGHClient(t, forbidden), GQLClient: githubv4.NewClient(forbidden), + RepoAccessCache: stubRepoAccessCache(nil, time.Minute), + } + session, _ := consolidatedPullRequestSession(t, deps, protocol, false) + for _, tc := range calls { + args := map[string]any{"owner": "owner", "repo": "repo"} + maps.Copy(args, tc.args) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.tool, Arguments: args}) + require.NoError(t, err, "%s %v", tc.tool, tc.args) + require.True(t, result.IsError, "%s %v: %s", tc.tool, tc.args, mustMarshalJSON(t, result)) + assert.Nil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) + } + }) + } +} + +func TestConsolidatedPullRequestOutputSchemas(t *testing.T) { + for _, tc := range []struct { + schema *jsonschema.Schema + valid []string + invalid []string + }{ + { + pullRequestReadOutputSchema(), + []string{`null`, `[]`, `{"diff":""}`, `{"number":1,"title":"","state":"","draft":false,"merged":false,"html_url":""}`, `{"state":"","sha":"","total_count":0,"statuses":[]}`, `{"total_count":0,"check_runs":[]}`, `{"review_threads":[],"totalCount":0,"pageInfo":{"hasNextPage":false,"hasPreviousPage":false}}`, `[{"filename":"f"}]`, `[{"id":1,"html_url":""}]`}, + []string{`{}`, `"diff"`, `{"diff":1}`, `[{"unknown":1}]`, `{"number":1}`}, + }, + { + pullRequestWriteOutputSchema(), + []string{`{"id":"","url":""}`, `{"status":"awaiting_user_submission","reason":"wait"}`}, + []string{`{}`, `null`, `{"status":"created","reason":"wait"}`}, + }, + { + pullRequestCommentReplyOutputSchema(), + []string{`{"id":"1","url":"u"}`, `{"comment":{"id":"1","url":"u"},"reaction":{"id":"2","url":"v"}}`}, + []string{`{}`, `{"comment":{"id":"1","url":"u"}}`, `null`}, + }, + { + pullRequestMergeOutputSchema(), + []string{`null`, `{}`, `{"sha":"abc","merged":true,"message":"ok"}`}, + []string{`[]`, `{"merged":"yes"}`}, + }, + { + pullRequestBranchUpdateOutputSchema(), + []string{`null`, `{"message":"m"}`, `{"message":"m","url":"u"}`}, + []string{`[]`, `{"message":1}`}, + }, + } { + resolved, err := tc.schema.Resolve(nil) + require.NoError(t, err) + for _, raw := range tc.valid { + var value any + require.NoError(t, json.Unmarshal([]byte(raw), &value)) + require.NoError(t, resolved.Validate(value), raw) + } + for _, raw := range tc.invalid { + var value any + require.NoError(t, json.Unmarshal([]byte(raw), &value)) + require.Error(t, resolved.Validate(value), raw) + } + } + // Zero outputs are validated by the SDK even for errors, so they must conform. + for _, tc := range []struct { + schema *jsonschema.Schema + value any + }{ + {pullRequestReadOutputSchema(), PullRequestReadOutput{}}, + {pullRequestWriteOutputSchema(), PullRequestWriteOutput{}}, + {pullRequestCommentReplyOutputSchema(), PullRequestCommentReplyOutput{}}, + {pullRequestMergeOutputSchema(), PullRequestMergeOutput{}}, + {pullRequestBranchUpdateOutputSchema(), PullRequestBranchUpdateOutput{}}, + } { + resolved, err := tc.schema.Resolve(nil) + require.NoError(t, err) + var value any + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, tc.value)), &value)) + require.NoError(t, resolved.Validate(value), mustMarshalJSON(t, tc.value)) + } +} From d351532b269daf94c9c1d2556b99b512f74e8e62 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 5 Oct 2026 17:34:13 +0200 Subject: [PATCH 2/4] fix(pull-requests): align modern text with typed outputs Use curated output DTOs for modern JSON text while preserving legacy serialization. Cover successful null responses, empty check runs, and Link-header page traversal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../add_comment_to_pending_review.snap | 1 + .../add_comment_to_pending_review_typed.snap | 100 -- .../add_reply_to_pull_request_comment.snap | 24 +- ...d_reply_to_pull_request_comment_typed.snap | 130 --- .../__toolsnaps__/create_pull_request.snap | 8 +- .../create_pull_request_typed.snap | 122 --- .../__toolsnaps__/merge_pull_request.snap | 3 + .../merge_pull_request_typed.snap | 93 -- .../__toolsnaps__/pull_request_read.snap | 182 ++-- .../pull_request_read_typed.snap | 983 ------------------ .../pull_request_review_write.snap | 1 + ...quest_review_write_resolution_reason.snap} | 19 +- .../pull_request_review_write_typed.snap | 90 -- .../__toolsnaps__/update_pull_request.snap | 8 +- .../update_pull_request_branch.snap | 7 +- .../update_pull_request_branch_typed.snap | 70 -- .../update_pull_request_typed.snap | 128 --- pkg/github/consolidated_pull_request_types.go | 139 ++- pkg/github/pullrequests.go | 39 +- ..._consolidated_pull_request_outputs_test.go | 201 +++- 20 files changed, 461 insertions(+), 1887 deletions(-) delete mode 100644 pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap delete mode 100644 pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap delete mode 100644 pkg/github/__toolsnaps__/create_pull_request_typed.snap delete mode 100644 pkg/github/__toolsnaps__/merge_pull_request_typed.snap delete mode 100644 pkg/github/__toolsnaps__/pull_request_read_typed.snap rename pkg/github/__toolsnaps__/{pull_request_review_write_resolution_reason_typed.snap => pull_request_review_write_resolution_reason.snap} (60%) delete mode 100644 pkg/github/__toolsnaps__/pull_request_review_write_typed.snap delete mode 100644 pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap delete mode 100644 pkg/github/__toolsnaps__/update_pull_request_typed.snap diff --git a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap index a013d81b4b..8296a90ccb 100644 --- a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap +++ b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap @@ -75,6 +75,7 @@ "additionalProperties": false, "properties": { "message": { + "description": "Operation result message.", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap b/pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap deleted file mode 100644 index f533fe8b48..0000000000 --- a/pkg/github/__toolsnaps__/add_comment_to_pending_review_typed.snap +++ /dev/null @@ -1,100 +0,0 @@ -{ - "annotations": { - "idempotentHint": false, - "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).", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "body": { - "description": "The text of the review comment", - "type": "string" - }, - "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" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "path": { - "description": "The relative path to the file that necessitates a comment", - "type": "string" - }, - "pullNumber": { - "description": "Pull request number", - "type": "number" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - }, - "side": { - "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" - }, - "startSide": { - "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": { - "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", - "outputSchema": { - "additionalProperties": false, - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - } -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap index 88c438d0ed..8640ba2cf3 100644 --- a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap +++ b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap @@ -56,16 +56,16 @@ { "additionalProperties": false, "properties": { - "id": { + "html_url": { + "description": "Browser URL.", "type": "string" }, - "url": { + "id": { "type": "string" } }, "required": [ - "id", - "url" + "id" ], "type": "object" }, @@ -75,32 +75,32 @@ "comment": { "additionalProperties": false, "properties": { - "id": { + "html_url": { + "description": "Browser URL.", "type": "string" }, - "url": { + "id": { "type": "string" } }, "required": [ - "id", - "url" + "id" ], "type": "object" }, "reaction": { "additionalProperties": false, "properties": { - "id": { + "html_url": { + "description": "Browser URL.", "type": "string" }, - "url": { + "id": { "type": "string" } }, "required": [ - "id", - "url" + "id" ], "type": "object" } diff --git a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap b/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap deleted file mode 100644 index b3f95e9a3e..0000000000 --- a/pkg/github/__toolsnaps__/add_reply_to_pull_request_comment_typed.snap +++ /dev/null @@ -1,130 +0,0 @@ -{ - "annotations": { - "idempotentHint": false, - "readOnlyHint": false, - "title": "Add reply to pull request comment" - }, - "description": "Add a reply and/or reaction to an existing pull request comment. This can create a new comment linked as a reply to the specified comment, add an emoji reaction to the specified comment, or do both. At least one of body or reaction is required.", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "body": { - "description": "The text of the reply. Required unless reaction is provided.", - "type": "string" - }, - "commentId": { - "description": "The numeric ID of the pull request review comment to reply or react to. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", - "minimum": 1, - "type": "number" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "pullNumber": { - "description": "Pull request number. Required when body is provided.", - "type": "number" - }, - "reaction": { - "description": "Emoji reaction to add. Required unless body is provided.", - "enum": [ - "+1", - "-1", - "laugh", - "confused", - "heart", - "hooray", - "rocket", - "eyes" - ], - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - } - }, - "required": [ - "owner", - "repo", - "commentId" - ], - "type": "object" - }, - "name": "add_reply_to_pull_request_comment", - "outputSchema": { - "oneOf": [ - { - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "id", - "url" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "comment": { - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "id", - "url" - ], - "type": "object" - }, - "reaction": { - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "id", - "url" - ], - "type": "object" - } - }, - "required": [ - "comment", - "reaction" - ], - "type": "object" - } - ] - } -} \ 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 852ef1349d..749065d788 100644 --- a/pkg/github/__toolsnaps__/create_pull_request.snap +++ b/pkg/github/__toolsnaps__/create_pull_request.snap @@ -71,16 +71,16 @@ { "additionalProperties": false, "properties": { - "id": { + "html_url": { + "description": "Browser URL.", "type": "string" }, - "url": { + "id": { "type": "string" } }, "required": [ - "id", - "url" + "id" ], "type": "object" }, diff --git a/pkg/github/__toolsnaps__/create_pull_request_typed.snap b/pkg/github/__toolsnaps__/create_pull_request_typed.snap deleted file mode 100644 index c91abdadaf..0000000000 --- a/pkg/github/__toolsnaps__/create_pull_request_typed.snap +++ /dev/null @@ -1,122 +0,0 @@ -{ - "_meta": { - "ui": { - "resourceUri": "ui://github-mcp-server/pr-write", - "visibility": [ - "model", - "app" - ] - } - }, - "annotations": { - "idempotentHint": false, - "readOnlyHint": false, - "title": "Open new pull request" - }, - "description": "Create a new pull request in a GitHub repository.", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "base": { - "description": "Branch to merge into", - "type": "string" - }, - "body": { - "description": "PR description", - "type": "string" - }, - "draft": { - "description": "Create as draft PR", - "type": "boolean" - }, - "head": { - "description": "Branch containing changes", - "type": "string" - }, - "maintainer_can_modify": { - "description": "Allow maintainer edits", - "type": "boolean" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - }, - "reviewers": { - "description": "GitHub usernames or ORG/team-slug team reviewers to request reviews from", - "items": { - "type": "string" - }, - "type": "array" - }, - "title": { - "description": "PR title", - "type": "string" - } - }, - "required": [ - "owner", - "repo", - "title", - "head", - "base" - ], - "type": "object" - }, - "name": "create_pull_request", - "outputSchema": { - "oneOf": [ - { - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "id", - "url" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "reason": { - "type": "string" - }, - "status": { - "enum": [ - "awaiting_user_submission" - ], - "type": "string" - } - }, - "required": [ - "status", - "reason" - ], - "type": "object" - } - ] - } -} \ 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 57024219fd..4f570b1b12 100644 --- a/pkg/github/__toolsnaps__/merge_pull_request.snap +++ b/pkg/github/__toolsnaps__/merge_pull_request.snap @@ -65,18 +65,21 @@ "additionalProperties": false, "properties": { "merged": { + "description": "Whether the pull request was merged.", "type": [ "null", "boolean" ] }, "message": { + "description": "Operation result message.", "type": [ "null", "string" ] }, "sha": { + "description": "Git commit SHA.", "type": [ "null", "string" diff --git a/pkg/github/__toolsnaps__/merge_pull_request_typed.snap b/pkg/github/__toolsnaps__/merge_pull_request_typed.snap deleted file mode 100644 index d9adcfd1f2..0000000000 --- a/pkg/github/__toolsnaps__/merge_pull_request_typed.snap +++ /dev/null @@ -1,93 +0,0 @@ -{ - "annotations": { - "idempotentHint": false, - "readOnlyHint": false, - "title": "Merge pull request" - }, - "description": "Merge a pull request in a GitHub repository.", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACeElEQVRIibWVTUhUYRSGn/e74+iiQih1F9Vcmj9sptylUVBYkO4jcNeuJBdFKxe1CYQokGrRKjCEdtmqwEVmtqomQWeiUdc2EBUtUufe0yLHn1KLGXtX5zvn4zz3vd8f/Gfp90Qs0drmpA6MT1EveDo1NfV92wB+KnMdo39Nfs4L7eSHD5Nz1QJcJYglWtsw+iUehAuRRjO1g+0KHLerbb4OIHnHAC1FdW129s3XmUJuwnBDoOPbA7BwHsD7QWq1HKYN5msBRCpB1AueLoSROSkciSUyj5ClhE6BLtYC8CpBqVRabNrdMmIiJdQjuUbQ1WI+d78WwIbykxnzU9np7ejlNq2YxQ4ebNtTKyCyWcEgYl55EDj/a7ihFEtkLkr0As2YxjwL+9aem00dCEYNzvnJzLDvH27aaM5y80HEnKGHKGwPnEbT6fSOvzpAmrDQnkncpC7siiUzz2QqIPu25iOuGBorTufO/AJmH0v2ajHwuoHhrQHATOH9rQPJ7IjDLgs6kZ0F6it1AzArVcZLdUE+WnYgmv/uYFmz+dxH4NJGNT+RfYLCE7F4tn0pGkxHy94AmBm8/GfAVvIs7AukUTkbj5YdYIbZ9WJh8m1lzrrbNB4/tD+QuyPsdCibF26gmM/dY/NdRDqd3rEYeN04mswYL+ZXm68DxOPxnWXXMClsp+GGhCWBTtClYj53t1qXK78oVH2XYB/mHZ0pvHsN4Cczzw3rBaoGrJ6D5ZUvN1i+kjI0LWiptjmscbC88hZZCAf2trZeq1v0UsJ6wF7UAlhxUMxPvkW6AboQLbvPcjaO+BIx11cL4I9H308eOiLRQUhpOx79/66fNKzrOCYNDm0AAAAASUVORK5CYII=", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABjElEQVRIibWVPS/DURTGnysSC0HiZdWVrZ28JDaLT8BHaBsMdjqZJDXiAzC2LF5mX6GtATGiIsGARH+Gnj9X8a/kf3uWe3Py3Oc559xz75E6bK7VAWQkzUi6lXTonHsOpgYUgAZfdgmkQpFnjHwb6AemgDpQCiWwYlEPeL4i8JCEt8vb39g67vkmPH8yA3qt5nVgCzi1jLJBBEwkBZSAdxPKAj86LYQQQCU4cYvAKzDUSYF3YC+uRIAD8sA58ACU//VuTODE1n1g+A9c3jBH1tJ1a5TeCPNrdACSCpKeJG1IepN0LKkm6dGDrkqqOOdm7dyUpDNJi865PUnqjsvEObcJHEhaljQnaV5STwvszttXbR2J441KtB4LauLKVpZpYBDYte8mHUogZTWPrAGstTtQBl6AayDX7qHZD7AALMVGDvQBV5ZyETi2qHLtMvmXWRQAk57vBKgl4fV/0+jmq56vImk0icCnAWm7pB3riGngnlADx0TW+T4yL4CxJJy/Df20mkP/TqGHfifsA7INs3X5i3+yAAAAAElFTkSuQmCC", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "commit_message": { - "description": "Extra detail for merge commit", - "type": "string" - }, - "commit_title": { - "description": "Title for merge commit", - "type": "string" - }, - "expectedHeadSha": { - "description": "The expected SHA of the pull request's HEAD ref", - "type": "string" - }, - "merge_method": { - "description": "Merge method", - "enum": [ - "merge", - "squash", - "rebase" - ], - "type": "string" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "pullNumber": { - "description": "Pull request number", - "type": "number" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - } - }, - "required": [ - "owner", - "repo", - "pullNumber" - ], - "type": "object" - }, - "name": "merge_pull_request", - "outputSchema": { - "additionalProperties": false, - "properties": { - "merged": { - "type": [ - "null", - "boolean" - ] - }, - "message": { - "type": [ - "null", - "string" - ] - }, - "sha": { - "type": [ - "null", - "string" - ] - } - }, - "type": [ - "object", - "null" - ] - } -} \ 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 e284bf9718..2216e21f3c 100644 --- a/pkg/github/__toolsnaps__/pull_request_read.snap +++ b/pkg/github/__toolsnaps__/pull_request_read.snap @@ -68,6 +68,7 @@ "additionalProperties": false, "properties": { "additions": { + "description": "Number of lines.", "type": "integer" }, "assignees": { @@ -104,6 +105,7 @@ ] }, "sha": { + "description": "Git commit SHA.", "type": "string" } }, @@ -123,6 +125,7 @@ "type": "integer" }, "closed_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "comments": { @@ -132,9 +135,11 @@ "type": "integer" }, "created_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "deletions": { + "description": "Number of lines.", "type": "integer" }, "draft": { @@ -165,6 +170,7 @@ ] }, "sha": { + "description": "Git commit SHA.", "type": "string" } }, @@ -178,6 +184,7 @@ ] }, "html_url": { + "description": "Browser URL.", "type": "string" }, "labels": { @@ -193,9 +200,11 @@ "type": "string" }, "merged": { + "description": "Whether the pull request was merged.", "type": "boolean" }, "merged_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "merged_by": { @@ -217,12 +226,19 @@ ] }, "state": { + "description": "Pull request state; empty for a sparse response.", + "enum": [ + "open", + "closed", + "" + ], "type": "string" }, "title": { "type": "string" }, "updated_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "user": { @@ -244,6 +260,7 @@ "type": "string" }, "created_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "email": { @@ -283,6 +300,7 @@ "type": "string" }, "updated_at": { + "description": "RFC 3339 timestamp.", "type": "string" } }, @@ -332,6 +350,7 @@ "additionalProperties": false, "properties": { "diff": { + "description": "Unified pull request diff.", "type": "string" } }, @@ -344,9 +363,18 @@ "additionalProperties": false, "properties": { "sha": { + "description": "Git commit SHA.", "type": "string" }, "state": { + "description": "Commit status; empty for a sparse response.", + "enum": [ + "error", + "failure", + "pending", + "success", + "" + ], "type": "string" }, "statuses": { @@ -357,18 +385,28 @@ "type": "string" }, "created_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "description": { "type": "string" }, "state": { + "description": "Commit status; empty for a sparse response.", + "enum": [ + "error", + "failure", + "pending", + "success", + "" + ], "type": "string" }, "target_url": { "type": "string" }, "updated_at": { + "description": "RFC 3339 timestamp.", "type": "string" } }, @@ -384,6 +422,7 @@ ] }, "total_count": { + "description": "Total number of results.", "type": "integer" } }, @@ -435,24 +474,29 @@ "type": "string" }, "created_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "html_url": { + "description": "Browser URL.", "type": "string" }, "line": { + "description": "One-based diff line number.", "type": [ "null", "integer" ] }, "original_line": { + "description": "One-based diff line number.", "type": [ "null", "integer" ] }, "original_start_line": { + "description": "One-based diff line number.", "type": [ "null", "integer" @@ -462,12 +506,14 @@ "type": "string" }, "start_line": { + "description": "One-based diff line number.", "type": [ "null", "integer" ] }, "updated_at": { + "description": "RFC 3339 timestamp.", "type": "string" } }, @@ -495,6 +541,7 @@ "type": "boolean" }, "total_count": { + "description": "Total number of results.", "type": "integer" } }, @@ -514,6 +561,7 @@ ] }, "totalCount": { + "description": "Total number of results.", "type": "integer" } }, @@ -532,15 +580,29 @@ "additionalProperties": false, "properties": { "completed_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "conclusion": { + "description": "Completed check outcome.", + "enum": [ + "success", + "failure", + "neutral", + "cancelled", + "skipped", + "timed_out", + "action_required", + "stale", + "startup_failure" + ], "type": "string" }, "details_url": { "type": "string" }, "html_url": { + "description": "Browser URL.", "type": "string" }, "id": { @@ -550,9 +612,20 @@ "type": "string" }, "started_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "status": { + "description": "Check execution status; empty for a sparse response.", + "enum": [ + "queued", + "in_progress", + "completed", + "waiting", + "requested", + "pending", + "" + ], "type": "string" } }, @@ -569,6 +642,7 @@ ] }, "total_count": { + "description": "Total number of results.", "type": "integer" } }, @@ -585,12 +659,15 @@ "additionalProperties": false, "properties": { "additions": { + "description": "Number of lines.", "type": "integer" }, "changes": { + "description": "Number of lines.", "type": "integer" }, "deletions": { + "description": "Number of lines.", "type": "integer" }, "filename": { @@ -603,6 +680,16 @@ "type": "string" }, "status": { + "description": "File change status.", + "enum": [ + "added", + "removed", + "modified", + "renamed", + "copied", + "changed", + "unchanged" + ], "type": "string" } }, @@ -639,12 +726,15 @@ ] }, "html_url": { + "description": "Browser URL.", "type": "string" }, "message": { + "description": "Operation result message.", "type": "string" }, "sha": { + "description": "Git commit SHA.", "type": "string" } }, @@ -669,18 +759,30 @@ "type": "string" }, "commit_id": { + "description": "Git commit SHA.", "type": "string" }, "html_url": { + "description": "Browser URL.", "type": "string" }, "id": { "type": "integer" }, "state": { + "description": "Review state; empty for a sparse response.", + "enum": [ + "APPROVED", + "CHANGES_REQUESTED", + "COMMENTED", + "DISMISSED", + "PENDING", + "" + ], "type": "string" }, "submitted_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "user": { @@ -702,6 +804,7 @@ "type": "string" }, "created_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "email": { @@ -741,6 +844,7 @@ "type": "string" }, "updated_at": { + "description": "RFC 3339 timestamp.", "type": "string" } }, @@ -799,9 +903,11 @@ "type": "string" }, "created_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "html_url": { + "description": "Browser URL.", "type": "string" }, "id": { @@ -835,6 +941,7 @@ "type": "integer" }, "total_count": { + "description": "Total number of results.", "type": "integer" } }, @@ -855,90 +962,17 @@ ] }, "updated_at": { + "description": "RFC 3339 timestamp.", "type": "string" }, "user": { "additionalProperties": false, "properties": { - "avatar_url": { - "type": "string" - }, - "details": { - "additionalProperties": false, - "properties": { - "bio": { - "type": "string" - }, - "blog": { - "type": "string" - }, - "company": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "followers": { - "type": "integer" - }, - "following": { - "type": "integer" - }, - "hireable": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owned_private_repos": { - "type": "integer" - }, - "private_gists": { - "type": "integer" - }, - "public_gists": { - "type": "integer" - }, - "public_repos": { - "type": "integer" - }, - "total_private_repos": { - "type": "integer" - }, - "twitter_username": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "public_repos", - "public_gists", - "followers", - "following", - "created_at", - "updated_at" - ], - "type": [ - "null", - "object" - ] - }, "id": { "type": "integer" }, "login": { "type": "string" - }, - "profile_url": { - "type": "string" } }, "required": [ diff --git a/pkg/github/__toolsnaps__/pull_request_read_typed.snap b/pkg/github/__toolsnaps__/pull_request_read_typed.snap deleted file mode 100644 index 312b55b9cd..0000000000 --- a/pkg/github/__toolsnaps__/pull_request_read_typed.snap +++ /dev/null @@ -1,983 +0,0 @@ -{ - "annotations": { - "idempotentHint": false, - "readOnlyHint": true, - "title": "Get details for a single pull request" - }, - "description": "Get information on a specific pull request in GitHub repository.", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "after": { - "description": "Cursor for pagination, used only by the get_review_comments method. Pass the endCursor from the previous page's PageInfo to fetch the next page.", - "type": "string" - }, - "method": { - "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 combined commit status of a head commit in a pull request.\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_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.\n 6. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns thread metadata and comments with nullable current and original line-range coordinates (line, start_line, original_line, original_start_line). Current coordinates are omitted when unavailable, such as for outdated comments. Use cursor-based pagination (perPage, after) to control results.\n 7. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.\n 8. 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 9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.\n", - "enum": [ - "get", - "get_diff", - "get_status", - "get_files", - "get_commits", - "get_review_comments", - "get_reviews", - "get_comments", - "get_check_runs" - ], - "type": "string" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "page": { - "description": "Page number for pagination (min 1)", - "minimum": 1, - "type": "number" - }, - "perPage": { - "description": "Results per page for pagination (min 1, max 100)", - "maximum": 100, - "minimum": 1, - "type": "number" - }, - "pullNumber": { - "description": "Pull request number", - "type": "number" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - } - }, - "required": [ - "method", - "owner", - "repo", - "pullNumber" - ], - "type": "object" - }, - "name": "pull_request_read", - "outputSchema": { - "oneOf": [ - { - "type": "null" - }, - { - "additionalProperties": false, - "properties": { - "additions": { - "type": "integer" - }, - "assignees": { - "items": { - "type": "string" - }, - "type": [ - "null", - "array" - ] - }, - "base": { - "additionalProperties": false, - "properties": { - "ref": { - "type": "string" - }, - "repo": { - "additionalProperties": false, - "properties": { - "description": { - "type": "string" - }, - "full_name": { - "type": "string" - } - }, - "required": [ - "full_name" - ], - "type": [ - "null", - "object" - ] - }, - "sha": { - "type": "string" - } - }, - "required": [ - "ref", - "sha" - ], - "type": [ - "null", - "object" - ] - }, - "body": { - "type": "string" - }, - "changed_files": { - "type": "integer" - }, - "closed_at": { - "type": "string" - }, - "comments": { - "type": "integer" - }, - "commits": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "deletions": { - "type": "integer" - }, - "draft": { - "type": "boolean" - }, - "head": { - "additionalProperties": false, - "properties": { - "ref": { - "type": "string" - }, - "repo": { - "additionalProperties": false, - "properties": { - "description": { - "type": "string" - }, - "full_name": { - "type": "string" - } - }, - "required": [ - "full_name" - ], - "type": [ - "null", - "object" - ] - }, - "sha": { - "type": "string" - } - }, - "required": [ - "ref", - "sha" - ], - "type": [ - "null", - "object" - ] - }, - "html_url": { - "type": "string" - }, - "labels": { - "items": { - "type": "string" - }, - "type": [ - "null", - "array" - ] - }, - "mergeable_state": { - "type": "string" - }, - "merged": { - "type": "boolean" - }, - "merged_at": { - "type": "string" - }, - "merged_by": { - "type": "string" - }, - "milestone": { - "type": "string" - }, - "number": { - "type": "integer" - }, - "requested_reviewers": { - "items": { - "type": "string" - }, - "type": [ - "null", - "array" - ] - }, - "state": { - "type": "string" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "user": { - "additionalProperties": false, - "properties": { - "avatar_url": { - "type": "string" - }, - "details": { - "additionalProperties": false, - "properties": { - "bio": { - "type": "string" - }, - "blog": { - "type": "string" - }, - "company": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "followers": { - "type": "integer" - }, - "following": { - "type": "integer" - }, - "hireable": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owned_private_repos": { - "type": "integer" - }, - "private_gists": { - "type": "integer" - }, - "public_gists": { - "type": "integer" - }, - "public_repos": { - "type": "integer" - }, - "total_private_repos": { - "type": "integer" - }, - "twitter_username": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "public_repos", - "public_gists", - "followers", - "following", - "created_at", - "updated_at" - ], - "type": [ - "null", - "object" - ] - }, - "id": { - "type": "integer" - }, - "login": { - "type": "string" - }, - "profile_url": { - "type": "string" - } - }, - "required": [ - "login" - ], - "type": [ - "null", - "object" - ] - } - }, - "required": [ - "number", - "title", - "state", - "draft", - "merged", - "html_url" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "diff": { - "type": "string" - } - }, - "required": [ - "diff" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "sha": { - "type": "string" - }, - "state": { - "type": "string" - }, - "statuses": { - "items": { - "additionalProperties": false, - "properties": { - "context": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "state": { - "type": "string" - }, - "target_url": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "state", - "context" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - }, - "total_count": { - "type": "integer" - } - }, - "required": [ - "state", - "sha", - "total_count", - "statuses" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "pageInfo": { - "additionalProperties": false, - "properties": { - "endCursor": { - "type": "string" - }, - "hasNextPage": { - "type": "boolean" - }, - "hasPreviousPage": { - "type": "boolean" - }, - "startCursor": { - "type": "string" - } - }, - "required": [ - "hasNextPage", - "hasPreviousPage" - ], - "type": "object" - }, - "review_threads": { - "items": { - "additionalProperties": false, - "properties": { - "comments": { - "items": { - "additionalProperties": false, - "properties": { - "author": { - "type": "string" - }, - "body": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "html_url": { - "type": "string" - }, - "line": { - "type": [ - "null", - "integer" - ] - }, - "original_line": { - "type": [ - "null", - "integer" - ] - }, - "original_start_line": { - "type": [ - "null", - "integer" - ] - }, - "path": { - "type": "string" - }, - "start_line": { - "type": [ - "null", - "integer" - ] - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "path", - "html_url" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - }, - "id": { - "type": "string" - }, - "is_collapsed": { - "type": "boolean" - }, - "is_outdated": { - "type": "boolean" - }, - "is_resolved": { - "type": "boolean" - }, - "total_count": { - "type": "integer" - } - }, - "required": [ - "id", - "is_resolved", - "is_outdated", - "is_collapsed", - "comments", - "total_count" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - }, - "totalCount": { - "type": "integer" - } - }, - "required": [ - "review_threads", - "totalCount", - "pageInfo" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "check_runs": { - "items": { - "additionalProperties": false, - "properties": { - "completed_at": { - "type": "string" - }, - "conclusion": { - "type": "string" - }, - "details_url": { - "type": "string" - }, - "html_url": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "started_at": { - "type": "string" - }, - "status": { - "type": "string" - } - }, - "required": [ - "id", - "name", - "status" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - }, - "total_count": { - "type": "integer" - } - }, - "required": [ - "total_count", - "check_runs" - ], - "type": "object" - }, - { - "anyOf": [ - { - "items": { - "additionalProperties": false, - "properties": { - "additions": { - "type": "integer" - }, - "changes": { - "type": "integer" - }, - "deletions": { - "type": "integer" - }, - "filename": { - "type": "string" - }, - "patch": { - "type": "string" - }, - "previous_filename": { - "type": "string" - }, - "status": { - "type": "string" - } - }, - "required": [ - "filename" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - }, - { - "items": { - "additionalProperties": false, - "properties": { - "author": { - "additionalProperties": false, - "properties": { - "date": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "type": [ - "null", - "object" - ] - }, - "html_url": { - "type": "string" - }, - "message": { - "type": "string" - }, - "sha": { - "type": "string" - } - }, - "required": [ - "sha" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - }, - { - "items": { - "additionalProperties": false, - "properties": { - "author_association": { - "type": "string" - }, - "body": { - "type": "string" - }, - "commit_id": { - "type": "string" - }, - "html_url": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "state": { - "type": "string" - }, - "submitted_at": { - "type": "string" - }, - "user": { - "additionalProperties": false, - "properties": { - "avatar_url": { - "type": "string" - }, - "details": { - "additionalProperties": false, - "properties": { - "bio": { - "type": "string" - }, - "blog": { - "type": "string" - }, - "company": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "followers": { - "type": "integer" - }, - "following": { - "type": "integer" - }, - "hireable": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owned_private_repos": { - "type": "integer" - }, - "private_gists": { - "type": "integer" - }, - "public_gists": { - "type": "integer" - }, - "public_repos": { - "type": "integer" - }, - "total_private_repos": { - "type": "integer" - }, - "twitter_username": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "public_repos", - "public_gists", - "followers", - "following", - "created_at", - "updated_at" - ], - "type": [ - "null", - "object" - ] - }, - "id": { - "type": "integer" - }, - "login": { - "type": "string" - }, - "profile_url": { - "type": "string" - } - }, - "required": [ - "login" - ], - "type": [ - "null", - "object" - ] - } - }, - "required": [ - "id", - "state", - "html_url" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - }, - { - "items": { - "additionalProperties": false, - "properties": { - "author_association": { - "type": "string" - }, - "body": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "html_url": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "reactions": { - "additionalProperties": false, - "properties": { - "+1": { - "type": "integer" - }, - "-1": { - "type": "integer" - }, - "confused": { - "type": "integer" - }, - "eyes": { - "type": "integer" - }, - "heart": { - "type": "integer" - }, - "hooray": { - "type": "integer" - }, - "laugh": { - "type": "integer" - }, - "rocket": { - "type": "integer" - }, - "total_count": { - "type": "integer" - } - }, - "required": [ - "total_count", - "+1", - "-1", - "laugh", - "confused", - "heart", - "hooray", - "rocket", - "eyes" - ], - "type": [ - "null", - "object" - ] - }, - "updated_at": { - "type": "string" - }, - "user": { - "additionalProperties": false, - "properties": { - "avatar_url": { - "type": "string" - }, - "details": { - "additionalProperties": false, - "properties": { - "bio": { - "type": "string" - }, - "blog": { - "type": "string" - }, - "company": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "followers": { - "type": "integer" - }, - "following": { - "type": "integer" - }, - "hireable": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owned_private_repos": { - "type": "integer" - }, - "private_gists": { - "type": "integer" - }, - "public_gists": { - "type": "integer" - }, - "public_repos": { - "type": "integer" - }, - "total_private_repos": { - "type": "integer" - }, - "twitter_username": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "public_repos", - "public_gists", - "followers", - "following", - "created_at", - "updated_at" - ], - "type": [ - "null", - "object" - ] - }, - "id": { - "type": "integer" - }, - "login": { - "type": "string" - }, - "profile_url": { - "type": "string" - } - }, - "required": [ - "login" - ], - "type": [ - "null", - "object" - ] - } - }, - "required": [ - "id", - "html_url" - ], - "type": "object" - }, - "type": [ - "null", - "array" - ] - } - ], - "type": "array" - } - ] - } -} \ 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 9177599aee..70cf6471c1 100644 --- a/pkg/github/__toolsnaps__/pull_request_review_write.snap +++ b/pkg/github/__toolsnaps__/pull_request_review_write.snap @@ -65,6 +65,7 @@ "additionalProperties": false, "properties": { "message": { + "description": "Operation result message.", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason_typed.snap b/pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason.snap similarity index 60% rename from pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason_typed.snap rename to pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason.snap index c4078e462b..45e19ca7bf 100644 --- a/pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason_typed.snap +++ b/pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason.snap @@ -5,18 +5,6 @@ "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- resolve_thread: Resolve a review thread. Requires only \"threadId\" parameter with the thread's node ID (e.g., PRRT_kwDOxxx). The owner, repo, and pullNumber parameters are not used for this method. Resolving an already-resolved thread is a no-op.\n- unresolve_thread: Unresolve a previously resolved review thread. Requires only \"threadId\" parameter. The owner, repo, and pullNumber parameters are not used for this method. Unresolving an already-unresolved thread is a no-op.\n", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], "inputSchema": { "properties": { "body": { @@ -49,8 +37,7 @@ }, "owner": { "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" + "type": "string" }, "pullNumber": { "description": "Pull request number", @@ -58,8 +45,7 @@ }, "repo": { "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" + "type": "string" }, "resolutionReason": { "description": "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.", @@ -83,6 +69,7 @@ "additionalProperties": false, "properties": { "message": { + "description": "Operation result message.", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/pull_request_review_write_typed.snap b/pkg/github/__toolsnaps__/pull_request_review_write_typed.snap deleted file mode 100644 index 605ded7381..0000000000 --- a/pkg/github/__toolsnaps__/pull_request_review_write_typed.snap +++ /dev/null @@ -1,90 +0,0 @@ -{ - "annotations": { - "idempotentHint": false, - "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- resolve_thread: Resolve a review thread. Requires only \"threadId\" parameter with the thread's node ID (e.g., PRRT_kwDOxxx). The owner, repo, and pullNumber parameters are not used for this method. Resolving an already-resolved thread is a no-op.\n- unresolve_thread: Unresolve a previously resolved review thread. Requires only \"threadId\" parameter. The owner, repo, and pullNumber parameters are not used for this method. Unresolving an already-unresolved thread is a no-op.\n", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "body": { - "description": "Review comment text", - "type": "string" - }, - "commitID": { - "description": "SHA of commit to review", - "type": "string" - }, - "event": { - "description": "Review action to perform.", - "enum": [ - "APPROVE", - "REQUEST_CHANGES", - "COMMENT" - ], - "type": "string" - }, - "method": { - "description": "The write operation to perform on pull request review.", - "enum": [ - "create", - "submit_pending", - "delete_pending", - "resolve_thread", - "unresolve_thread" - ], - "type": "string" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "pullNumber": { - "description": "Pull request number", - "type": "number" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - }, - "threadId": { - "description": "The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments.", - "type": "string" - } - }, - "required": [ - "method", - "owner", - "repo", - "pullNumber" - ], - "type": "object" - }, - "name": "pull_request_review_write", - "outputSchema": { - "additionalProperties": false, - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - } -} \ 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 47526b8201..bc61f3c520 100644 --- a/pkg/github/__toolsnaps__/update_pull_request.snap +++ b/pkg/github/__toolsnaps__/update_pull_request.snap @@ -77,16 +77,16 @@ { "additionalProperties": false, "properties": { - "id": { + "html_url": { + "description": "Browser URL.", "type": "string" }, - "url": { + "id": { "type": "string" } }, "required": [ - "id", - "url" + "id" ], "type": "object" }, diff --git a/pkg/github/__toolsnaps__/update_pull_request_branch.snap b/pkg/github/__toolsnaps__/update_pull_request_branch.snap index fb1e826bc0..83d75b4b37 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_branch.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_branch.snap @@ -36,12 +36,7 @@ "additionalProperties": false, "properties": { "message": { - "type": [ - "null", - "string" - ] - }, - "url": { + "description": "Operation result message.", "type": [ "null", "string" diff --git a/pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap deleted file mode 100644 index 8d2b08025f..0000000000 --- a/pkg/github/__toolsnaps__/update_pull_request_branch_typed.snap +++ /dev/null @@ -1,70 +0,0 @@ -{ - "annotations": { - "idempotentHint": false, - "readOnlyHint": false, - "title": "Update pull request branch" - }, - "description": "Update the branch of a pull request with the latest changes from the base branch.", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "expectedHeadSha": { - "description": "The expected SHA of the pull request's HEAD ref", - "type": "string" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "pullNumber": { - "description": "Pull request number", - "type": "number" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - } - }, - "required": [ - "owner", - "repo", - "pullNumber" - ], - "type": "object" - }, - "name": "update_pull_request_branch", - "outputSchema": { - "additionalProperties": false, - "properties": { - "message": { - "type": [ - "null", - "string" - ] - }, - "url": { - "type": [ - "null", - "string" - ] - } - }, - "type": [ - "object", - "null" - ] - } -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_typed.snap deleted file mode 100644 index 1382556fa8..0000000000 --- a/pkg/github/__toolsnaps__/update_pull_request_typed.snap +++ /dev/null @@ -1,128 +0,0 @@ -{ - "_meta": { - "ui": { - "resourceUri": "ui://github-mcp-server/pr-edit", - "visibility": [ - "model", - "app" - ] - } - }, - "annotations": { - "idempotentHint": false, - "readOnlyHint": false, - "title": "Edit pull request" - }, - "description": "Update an existing pull request in a GitHub repository.", - "icons": [ - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAACwUlEQVRIie2Vz28UZRjHP993pi0QIC3YahNjirtmd3bS3Q1eUHvQEPUiEv8A4kXjwRJ78MCFBLjBBRKCHowHE38cNCbGGx6IUoKiodtNpoNmTJp4oSJNQ3pw29l5POxus2wo3QTwxPc0887zfD7zvu9kXnjEUfdNrjj5vJOmMP4e9JrfR1G02tuQD8tvgpck0dxCPwK30ViqnJTcr4bOmfRlI/PrhUJ5313woDpDpu8ss7f6nYHrvDnGcYlPsoY/bKaXwHY3HWfvgmNnMX0zvMM7069A3c3pkEYWa7UVgFxQPSfs7SSeH3k2rEy5jMubMBoG1yQ+SBbm53of+gCybMkk/H8VAFdbZisZLAFsJ11oyL+BUURcwrjWAZixXeIwxs/5UuVAr0QAYRjubGR+HWy3mb6QCIBXQe8nce0jgIkwfMo3/xLG085x8I9ofkMyUa0O+w2rgS0mcf3lboEDiKJo1cvsIDDr4D1DhTb8407hYhTdTJW+AvrdMnuhG9Je1m9BBzbfjXbyQcXypeqJLQt7+0rVE/mgYr3j7l7FDzOPBY8FDx6vc1EolPeNjI5/Jpgw7Lm9o+Pry//c/K0PhnLFyrSMDxE79jwxvn9079gvt28vrUD7V1EoFHalbltd2C7DfS4sAF4DTSdx7cL96LliZVriPPADuL+geRh0Z8il5SiKVn2ATENvCCYw78U/b8xdBcgHlYuGHQXuK5A4ClxM4vnXW8Lqp5JdWWt6h4CvWnsgxgDSbRZ3Gg0tCJ7sY4nGwDZOt/WBZtzN9FswLgM2sGann5mcPDaw5pWEHQH7cUu86SdkR3LF6tfrA814MNVpwNrM1leUxPXrSKfMeHcwdctyNotY8c3NbMX3LJsB3ZHsymDqlkHvYHYyievXWxPpSj4o75eYIuPWZof+vRKG4c61pncIx6gZsx34/5L/ACy3ElqUYhuvAAAAAElFTkSuQmCC", - "theme": "light" - }, - { - "mimeType": "image/png", - "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABn0lEQVRIie2Vvy5EURDGvyMUkhUU/pS2Q2clwvIKqL0Cm9ArrBcQShJPoLFR2YaIhIjQ7a7E39IiCgqVn8KsPY697E3oTHPumTPf990z986M9Mfm/A0wKGlMUlnSlnPuOQQAE5LOnXOFWErAIvBK1S6BZBAzZ2fzcckHjXwVaAXSwD2wWYN8A2iKK1ABt3m+ZeDRnseIthdgDxioxd1o662tfZIO7Lnf8xcklST1StqRdORxNEualHQIDDvnTmvdIGE5vwdWgLy93bQX0w0UgSdgKMC3AdfA7ndpSgKbduUbYBoI/7Ju4BiYrYFfAl4iBbxAgOyPgV9xWYDQ3xCXKK79C/wL/KJZoeW8QpsJCy0C54AMULaGmQu7sIAW4MpaxTKwbQU3U4dAxmLzwLpxXAIJP2jKgkY8Xx4o1SFwBmx7+7RxTUnVb9Bpa9HDFiR1/SRgWH+6FT3/h2qK6sBpB0aBB7yB880NcpaWtGHXjCsVBmb5PDIvgJ46BJKW84q9AguV87Adp/Q+9O8UMfQjRBKSxiV1SNp3zp3Ug/sVewPruexhKwhGXQAAAABJRU5ErkJggg==", - "theme": "dark" - } - ], - "inputSchema": { - "properties": { - "base": { - "description": "New base branch name", - "type": "string" - }, - "body": { - "description": "New description", - "type": "string" - }, - "draft": { - "description": "Mark pull request as draft (true) or ready for review (false)", - "type": "boolean" - }, - "maintainer_can_modify": { - "description": "Allow maintainer edits", - "type": "boolean" - }, - "owner": { - "description": "Repository owner", - "type": "string", - "x-mcp-header": "owner" - }, - "pullNumber": { - "description": "Pull request number to update", - "type": "number" - }, - "repo": { - "description": "Repository name", - "type": "string", - "x-mcp-header": "repo" - }, - "reviewers": { - "description": "GitHub usernames or ORG/team-slug team reviewers to request reviews from", - "items": { - "type": "string" - }, - "type": "array" - }, - "state": { - "description": "New state", - "enum": [ - "open", - "closed" - ], - "type": "string" - }, - "title": { - "description": "New title", - "type": "string" - } - }, - "required": [ - "owner", - "repo", - "pullNumber" - ], - "type": "object" - }, - "name": "update_pull_request", - "outputSchema": { - "oneOf": [ - { - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "id", - "url" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "reason": { - "type": "string" - }, - "status": { - "enum": [ - "awaiting_user_submission" - ], - "type": "string" - } - }, - "required": [ - "status", - "reason" - ], - "type": "object" - } - ] - } -} \ No newline at end of file diff --git a/pkg/github/consolidated_pull_request_types.go b/pkg/github/consolidated_pull_request_types.go index 03c54741c2..6bee34d975 100644 --- a/pkg/github/consolidated_pull_request_types.go +++ b/pkg/github/consolidated_pull_request_types.go @@ -9,7 +9,6 @@ import ( "github.com/github/github-mcp-server/pkg/utils" "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v92/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -116,7 +115,7 @@ type PullRequestReadOutput struct { Commits *[]MinimalPullRequestCommit ReviewThreads *MinimalReviewThreadsResponse Reviews *[]MinimalPullRequestReview - Comments *[]MinimalIssueComment + Comments *[]IssueReadCommentOutput CheckRuns *MinimalCheckRunsResult } @@ -161,10 +160,10 @@ func pullRequestCommentsOutput(output *IssueReadOutput) *PullRequestReadOutput { func pullRequestReadOutputSchema() *jsonschema.Schema { arrays := []*jsonschema.Schema{ - repositoryOutputSchema[[]MinimalPRFile](), - repositoryOutputSchema[[]MinimalPullRequestCommit](), - repositoryOutputSchema[[]MinimalPullRequestReview](), - repositoryOutputSchema[[]MinimalIssueComment](), + pullRequestOutputSchema[[]MinimalPRFile](), + pullRequestOutputSchema[[]MinimalPullRequestCommit](), + pullRequestOutputSchema[[]MinimalPullRequestReview](), + pullRequestOutputSchema[[]IssueReadCommentOutput](), } for _, array := range arrays { array.Defs = nil @@ -173,11 +172,11 @@ func pullRequestReadOutputSchema() *jsonschema.Schema { // one anyOf array variant instead of ambiguous oneOf branches. return repositoryUnionSchema( &jsonschema.Schema{Type: "null"}, - repositoryOutputSchema[MinimalPullRequest](), - repositoryOutputSchema[PullRequestDiffOutput](), - repositoryOutputSchema[MinimalCombinedStatus](), - repositoryOutputSchema[MinimalReviewThreadsResponse](), - repositoryOutputSchema[MinimalCheckRunsResult](), + pullRequestOutputSchema[MinimalPullRequest](), + pullRequestOutputSchema[PullRequestDiffOutput](), + pullRequestOutputSchema[MinimalCombinedStatus](), + pullRequestOutputSchema[MinimalReviewThreadsResponse](), + pullRequestOutputSchema[MinimalCheckRunsResult](), &jsonschema.Schema{Type: "array", AnyOf: arrays}, ) } @@ -185,7 +184,7 @@ func pullRequestReadOutputSchema() *jsonschema.Schema { // PullRequestWriteOutput is either the created/updated pull request reference // or the notice that an interactive form is awaiting user submission. type PullRequestWriteOutput struct { - PullRequest *MinimalResponse + PullRequest *PullRequestMutationReference Awaiting *IssueWriteAwaitingOutput } @@ -196,11 +195,24 @@ func (out PullRequestWriteOutput) MarshalJSON() ([]byte, error) { if out.PullRequest != nil { return json.Marshal(out.PullRequest) } - return json.Marshal(MinimalResponse{}) + return json.Marshal(PullRequestMutationReference{}) } func pullRequestWriteOutputSchema() *jsonschema.Schema { - return issueWriteOutputSchema() + awaiting := pullRequestOutputSchema[IssueWriteAwaitingOutput]() + awaiting.Properties["status"].Enum = []any{"awaiting_user_submission"} + return repositoryUnionSchema(pullRequestOutputSchema[PullRequestMutationReference](), awaiting) +} + +// PullRequestMutationReference includes a browser URL only when the API supplied one. +// Reactions have no browser URL; their API endpoint is not useful output. +type PullRequestMutationReference struct { + ID string `json:"id"` + HTMLURL string `json:"html_url,omitempty"` +} + +func pullRequestMutationReference(response MinimalResponse) *PullRequestMutationReference { + return &PullRequestMutationReference{ID: response.ID, HTMLURL: response.URL} } func pullRequestFormArguments(req *mcp.CallToolRequest, args map[string]any) (map[string]any, error) { @@ -229,7 +241,13 @@ func pullRequestAwaitingFormResult(message string) (*mcp.CallToolResult, *PullRe // PullRequestMergeOutput preserves a JSON null merge response rather than // letting the SDK replace a nil pointer with an empty object. type PullRequestMergeOutput struct { - Result *github.PullRequestMergeResult + Result *PullRequestMergeResult +} + +type PullRequestMergeResult struct { + SHA *string `json:"sha,omitempty"` + Merged *bool `json:"merged,omitempty"` + Message *string `json:"message,omitempty"` } func (out PullRequestMergeOutput) MarshalJSON() ([]byte, error) { @@ -237,13 +255,17 @@ func (out PullRequestMergeOutput) MarshalJSON() ([]byte, error) { } func pullRequestMergeOutputSchema() *jsonschema.Schema { - return nullableRepositoryOutputSchema[github.PullRequestMergeResult]() + return nullableRepositoryOutputSchema[PullRequestMergeResult]() } // PullRequestBranchUpdateOutput carries either the API response or, for an // accepted asynchronous update, the in-progress message. type PullRequestBranchUpdateOutput struct { - Result *github.PullRequestBranchUpdateResponse + Result *PullRequestBranchUpdateResult +} + +type PullRequestBranchUpdateResult struct { + Message *string `json:"message,omitempty"` } func (out PullRequestBranchUpdateOutput) MarshalJSON() ([]byte, error) { @@ -251,11 +273,11 @@ func (out PullRequestBranchUpdateOutput) MarshalJSON() ([]byte, error) { } func pullRequestBranchUpdateOutputSchema() *jsonschema.Schema { - return nullableRepositoryOutputSchema[github.PullRequestBranchUpdateResponse]() + return nullableRepositoryOutputSchema[PullRequestBranchUpdateResult]() } func nullableRepositoryOutputSchema[T any]() *jsonschema.Schema { - schema := repositoryOutputSchema[T]() + schema := pullRequestOutputSchema[T]() schema.Type = "" schema.Types = []string{"object", "null"} return schema @@ -264,14 +286,14 @@ func nullableRepositoryOutputSchema[T any]() *jsonschema.Schema { // PullRequestCommentReplyOutput is a single reply or reaction reference, or // both when the caller requested a reply and a reaction together. type PullRequestCommentReplyOutput struct { - Response *MinimalResponse + Response *PullRequestMutationReference ReplyAndReaction *PullRequestReplyAndReactionOutput } // PullRequestReplyAndReactionOutput keeps the legacy map's sorted key order. type PullRequestReplyAndReactionOutput struct { - Comment MinimalResponse `json:"comment"` - Reaction MinimalResponse `json:"reaction"` + Comment PullRequestMutationReference `json:"comment"` + Reaction PullRequestMutationReference `json:"reaction"` } func (out PullRequestCommentReplyOutput) MarshalJSON() ([]byte, error) { @@ -281,16 +303,83 @@ func (out PullRequestCommentReplyOutput) MarshalJSON() ([]byte, error) { if out.Response != nil { return json.Marshal(out.Response) } - return json.Marshal(MinimalResponse{}) + return json.Marshal(PullRequestMutationReference{}) } func pullRequestCommentReplyOutputSchema() *jsonschema.Schema { return repositoryUnionSchema( - repositoryOutputSchema[MinimalResponse](), - repositoryOutputSchema[PullRequestReplyAndReactionOutput](), + pullRequestOutputSchema[PullRequestMutationReference](), + pullRequestOutputSchema[PullRequestReplyAndReactionOutput](), ) } +func pullRequestOutputSchema[T any]() *jsonschema.Schema { + schema := repositoryOutputSchema[T]() + describePullRequestOutputSchema(schema) + return schema +} + +// Only newly inferred schemas are customized, once during tool construction. +func describePullRequestOutputSchema(schema *jsonschema.Schema) { + if schema == nil { + return + } + for name, field := range schema.Properties { + switch name { + case "html_url": + field.Description = "Browser URL." + case "sha", "commit_id": + field.Description = "Git commit SHA." + case "created_at", "updated_at", "closed_at", "merged_at", "submitted_at", "started_at", "completed_at": + field.Description = "RFC 3339 timestamp." + case "line", "original_line", "start_line", "original_start_line": + field.Description = "One-based diff line number." + case "additions", "deletions", "changes": + field.Description = "Number of lines." + case "total_count", "totalCount": + field.Description = "Total number of results." + case "message": + field.Description = "Operation result message." + case "merged": + field.Description = "Whether the pull request was merged." + case "diff": + field.Description = "Unified pull request diff." + case "state": + switch { + case schema.Properties["draft"] != nil: + field.Description = "Pull request state; empty for a sparse response." + field.Enum = []any{"open", "closed", ""} + case schema.Properties["statuses"] != nil || schema.Properties["context"] != nil: + field.Description = "Commit status; empty for a sparse response." + field.Enum = []any{"error", "failure", "pending", "success", ""} + case schema.Properties["commit_id"] != nil: + field.Description = "Review state; empty for a sparse response." + field.Enum = []any{"APPROVED", "CHANGES_REQUESTED", "COMMENTED", "DISMISSED", "PENDING", ""} + } + case "status": + switch { + case schema.Properties["filename"] != nil: + field.Description = "File change status." + field.Enum = []any{"added", "removed", "modified", "renamed", "copied", "changed", "unchanged"} + case schema.Properties["conclusion"] != nil: + field.Description = "Check execution status; empty for a sparse response." + field.Enum = []any{"queued", "in_progress", "completed", "waiting", "requested", "pending", ""} + } + case "conclusion": + field.Description = "Completed check outcome." + field.Enum = []any{"success", "failure", "neutral", "cancelled", "skipped", "timed_out", "action_required", "stale", "startup_failure"} + } + describePullRequestOutputSchema(field) + } + describePullRequestOutputSchema(schema.Items) + for _, definition := range schema.Defs { + describePullRequestOutputSchema(definition) + } + for _, variant := range append(slices.Clone(schema.OneOf), schema.AnyOf...) { + describePullRequestOutputSchema(variant) + } +} + // pullRequestMessageResult exposes the success message of review mutations // whose exported helpers only return a text result. func pullRequestMessageResult(result *mcp.CallToolResult, err error) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index c866f31ba1..e376eed5db 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -195,6 +195,10 @@ func getPullRequest(ctx context.Context, client *github.Client, deps ToolDepende return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil, nil } + if pr == nil { + return utils.NewToolResultText("null"), &PullRequestReadOutput{}, nil + } + if ff.LockdownMode { if restricted, err := authorLockdownResult(ctx, cache, owner, repo, pr.GetUser().GetLogin(), lockdownPullRequestRestrictedMessage); restricted != nil || err != nil { return restricted, nil, err @@ -909,7 +913,7 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), &PullRequestWriteOutput{PullRequest: &minimalResponse}, nil + return utils.NewToolResultText(string(r)), &PullRequestWriteOutput{PullRequest: pullRequestMutationReference(minimalResponse)}, nil }, normalizePullRequestArguments("create")) } @@ -1233,7 +1237,7 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo return utils.NewToolResultErrorFromErr("Failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), &PullRequestWriteOutput{PullRequest: &minimalResponse}, nil + return utils.NewToolResultText(string(r)), &PullRequestWriteOutput{PullRequest: pullRequestMutationReference(minimalResponse)}, nil }, normalizePullRequestArguments("update")) st.FeatureRule = pullRequestsConsolidatedRule return st @@ -1374,19 +1378,22 @@ func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventor } var output PullRequestCommentReplyOutput + var r []byte switch { case hasBody && hasReaction: output.ReplyAndReaction = &PullRequestReplyAndReactionOutput{ - Comment: *commentResponse, - Reaction: *reactionResponse, + Comment: *pullRequestMutationReference(*commentResponse), + Reaction: PullRequestMutationReference{ID: reactionResponse.ID}, } + r, err = json.Marshal(map[string]*MinimalResponse{"comment": commentResponse, "reaction": reactionResponse}) case hasReaction: - output.Response = reactionResponse + output.Response = &PullRequestMutationReference{ID: reactionResponse.ID} + r, err = json.Marshal(reactionResponse) default: - output.Response = commentResponse + output.Response = pullRequestMutationReference(*commentResponse) + r, err = json.Marshal(commentResponse) } - r, err := json.Marshal(output) if err != nil { return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } @@ -1783,7 +1790,11 @@ func MergePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), &PullRequestMergeOutput{Result: result}, nil + output := &PullRequestMergeOutput{} + if result != nil { + output.Result = &PullRequestMergeResult{SHA: result.SHA, Merged: result.Merged, Message: result.Message} + } + return utils.NewToolResultText(string(r)), output, nil }, normalizePullRequestArguments("merge")) } @@ -1931,7 +1942,7 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser // and it's not a real error. if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) { message := "Pull request branch update is in progress" - return utils.NewToolResultText(message), &PullRequestBranchUpdateOutput{Result: &github.PullRequestBranchUpdateResponse{Message: &message}}, nil + return utils.NewToolResultText(message), &PullRequestBranchUpdateOutput{Result: &PullRequestBranchUpdateResult{Message: &message}}, nil } return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update pull request branch", @@ -1954,7 +1965,11 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultText(string(r)), &PullRequestBranchUpdateOutput{Result: result}, nil + output := &PullRequestBranchUpdateOutput{} + if result != nil { + output.Result = &PullRequestBranchUpdateResult{Message: result.Message} + } + return utils.NewToolResultText(string(r)), output, nil }, normalizePullRequestArguments("branch")) } @@ -2041,7 +2056,7 @@ func pullRequestReviewWrite(t translations.TranslationHelperFunc, withResolution ToolsetMetadataPullRequests, mcp.Tool{ Name: "pull_request_review_write", - OutputSchema: repositoryOutputSchema[RepositoryMessageOutput](), + OutputSchema: pullRequestOutputSchema[RepositoryMessageOutput](), Description: t("TOOL_PULL_REQUEST_REVIEW_WRITE_DESCRIPTION", `Create and/or submit, delete review of a pull request. Available methods: @@ -2535,7 +2550,7 @@ func AddCommentToPendingReview(t translations.TranslationHelperFunc) inventory.S ToolsetMetadataPullRequests, mcp.Tool{ Name: "add_comment_to_pending_review", - OutputSchema: repositoryOutputSchema[RepositoryMessageOutput](), + OutputSchema: pullRequestOutputSchema[RepositoryMessageOutput](), 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"), diff --git a/pkg/github/typed_consolidated_pull_request_outputs_test.go b/pkg/github/typed_consolidated_pull_request_outputs_test.go index d242a60bf1..f2f500b743 100644 --- a/pkg/github/typed_consolidated_pull_request_outputs_test.go +++ b/pkg/github/typed_consolidated_pull_request_outputs_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "maps" "net/http" + "reflect" + "slices" "strings" "testing" "time" @@ -30,6 +32,19 @@ func consolidatedPullRequestSession(t *testing.T, deps BaseDeps, protocol string UpdatePullRequestBranch(tr), PullRequestReviewWrite(tr), PullRequestReviewWriteWithResolutionReason(tr), AddCommentToPendingReview(tr), AddReplyToPullRequestComment(tr), } + canonicalSchemas := make(map[string]string) + for _, tool := range tools { + if tool.Tool.Name == "pull_request_review_write" { + continue + } + canonicalSchemas[tool.Tool.Name] = mustMarshalJSON(t, tool.Tool.OutputSchema) + } + review := PullRequestReviewWrite(tr) + if slices.Contains(flags, FeatureFlagThreadResolutionReason) { + review = PullRequestReviewWriteWithResolutionReason(tr) + require.NoError(t, toolsnaps.Test("pull_request_review_write_resolution_reason", review.Tool)) + } + canonicalSchemas[review.Tool.Name] = mustMarshalJSON(t, review.Tool.OutputSchema) inv, err := inventory.NewBuilder().SetTools(tools).WithToolsets([]string{"all"}). WithFeatureChecker(featureCheckerFor(flags...)).Build() require.NoError(t, err) @@ -46,7 +61,21 @@ func consolidatedPullRequestSession(t *testing.T, deps BaseDeps, protocol string req.Params.Meta = mcp.Meta{mcp.MetaKeyProtocolVersion: ""} } } - return next(ghcontext.WithUISupport(ctx, ui), method, request) + result, err := next(ghcontext.WithUISupport(ctx, ui), method, request) + if protocol == inventory.ProtocolVersionMultiRoundTrip && method == "tools/call" && err == nil { + if call, ok := result.(*mcp.CallToolResult); ok && !call.IsError && len(call.Content) == 1 { + if text, ok := call.Content[0].(*mcp.TextContent); ok && text.Text == "null" { + // The client decodes explicit null and an omitted field as nil. + // Check the serialized response to distinguish them. + var wire map[string]json.RawMessage + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, call)), &wire)) + structured, present := wire["structuredContent"] + assert.True(t, present, "successful nullable output must include structuredContent") + assert.Equal(t, "null", string(structured)) + } + } + } + return result, err } }) version := protocol @@ -65,11 +94,7 @@ func consolidatedPullRequestSession(t *testing.T, deps BaseDeps, protocol string continue } require.NotNil(t, tool.OutputSchema, tool.Name) - name := tool.Name - if name == "pull_request_review_write" && len(flags) > 0 { - name += "_resolution_reason" - } - require.NoError(t, toolsnaps.Test(name+"_typed", *tool)) + assert.JSONEq(t, canonicalSchemas[tool.Name], mustMarshalJSON(t, tool.OutputSchema), tool.Name) var schema jsonschema.Schema require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, tool.OutputSchema)), &schema)) resolved, err := schema.Resolve(nil) @@ -105,10 +130,17 @@ func consolidatedPullRequestDeps(t *testing.T) BaseDeps { case path == "/repos/owner/repo/pulls/2": // A sparse PR without a head SHA still yields a valid status payload. write(http.StatusOK, `{"number":2,"head":{"sha":"abc"}}`) + case path == "/repos/owner/repo/pulls/3": + // A successful null API payload is normalized to the legacy zero-value PR. + write(http.StatusOK, `null`) case path == "/repos/owner/repo/commits/abc/status": write(http.StatusOK, `{"state":"success","sha":"abc","total_count":1,"statuses":[{"state":"success","context":"ci","target_url":"https://ci"}]}`) case path == "/repos/owner/repo/commits/abc/check-runs": - write(http.StatusOK, `{"total_count":1,"check_runs":[{"id":5,"name":"build","status":"completed","conclusion":"success"}]}`) + if r.URL.Query().Get("page") == "2" { + write(http.StatusOK, `{"total_count":0,"check_runs":[]}`) + } else { + write(http.StatusOK, `{"total_count":1,"check_runs":[{"id":5,"name":"build","status":"completed","conclusion":"success"}]}`) + } case path == "/repos/owner/repo/pulls/1/files": write(http.StatusOK, `[{"filename":"file","status":"modified","additions":1,"changes":1,"patch":"+added"}]`) case path == "/repos/owner/repo/pulls/2/files", path == "/repos/owner/repo/pulls/2/commits", @@ -118,6 +150,9 @@ func consolidatedPullRequestDeps(t *testing.T) BaseDeps { write(http.StatusOK, `[{"sha":"abc","html_url":"https://github.com/owner/repo/commit/abc","commit":{"message":"msg","author":{"name":"Octo","email":"octo@example.com","date":"2026-01-01T00:00:00Z"}}}]`) case path == "/repos/owner/repo/pulls/1/reviews": write(http.StatusOK, `[{"id":9,"state":"APPROVED","body":"lgtm","html_url":"https://github.com/owner/repo/pull/1#pullrequestreview-9","user":{"login":"octocat"},"commit_id":"abc","author_association":"MEMBER"}]`) + case path == "/repos/owner/repo/pulls/3/reviews": + // A successful null list payload is normalized to an empty list. + write(http.StatusOK, `null`) case path == "/repos/owner/repo/issues/1/comments": write(http.StatusOK, `[{"id":42,"body":"hello","html_url":"https://github.com/owner/repo/pull/1#issuecomment-42"}]`) case path == "/repos/owner/repo/pulls/1/merge": @@ -193,29 +228,46 @@ func runTypedPullRequestCases(t *testing.T, session *mcp.ClientSession, schemas require.False(t, result.IsError, "%s %v: %s", tc.tool, tc.args, mustMarshalJSON(t, result)) require.Len(t, result.Content, 1) text := getTextResult(t, result).Text - assert.Equal(t, tc.text, text, "%s %v", tc.tool, tc.args) schema := schemas[tc.tool] if schema == nil { + assert.Equal(t, tc.text, text, "%s %v", tc.tool, tc.args) assert.Nil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) continue } - if text == "null" { + if tc.text == "null" { + assert.Equal(t, "null", text) + // Explicit wire presence is checked by the session middleware. assert.Nil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) + require.NoError(t, schema.Validate(nil)) continue } require.NotNil(t, result.StructuredContent, "%s %v", tc.tool, tc.args) var value any require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, result.StructuredContent)), &value)) require.NoError(t, schema.Validate(value), "%s %v", tc.tool, tc.args) + // The foundation wrapper serializes the compact DTO once for both + // modern JSON text and structuredContent, including diff and message DTOs. + assert.JSONEq(t, mustMarshalJSON(t, value), text, "%s %v", tc.tool, tc.args) switch { case tc.args["method"] == "get_diff": - assert.JSONEq(t, mustMarshalJSON(t, PullRequestDiffOutput{Diff: text}), mustMarshalJSON(t, value)) + assert.JSONEq(t, mustMarshalJSON(t, PullRequestDiffOutput{Diff: tc.text}), mustMarshalJSON(t, value)) case tc.tool == "pull_request_review_write" || tc.tool == "add_comment_to_pending_review": - assert.JSONEq(t, mustMarshalJSON(t, RepositoryMessageOutput{Message: text}), mustMarshalJSON(t, value)) + assert.JSONEq(t, mustMarshalJSON(t, RepositoryMessageOutput{Message: tc.text}), mustMarshalJSON(t, value)) case tc.tool == "update_pull_request_branch": assert.JSONEq(t, `{"message":"Pull request branch update is in progress"}`, mustMarshalJSON(t, value)) + case tc.tool == "create_pull_request" || tc.tool == "update_pull_request": + assert.JSONEq(t, `{"id":"0","html_url":"https://github.com/owner/repo/pull/1"}`, mustMarshalJSON(t, value)) + case tc.tool == "add_reply_to_pull_request_comment": + switch { + case tc.args["body"] != nil && tc.args["reaction"] != nil: + assert.JSONEq(t, `{"comment":{"id":"43","html_url":"https://github.com/owner/repo/pull/1#discussion_r43"},"reaction":{"id":"77"}}`, mustMarshalJSON(t, value)) + case tc.args["body"] != nil: + assert.JSONEq(t, `{"id":"43","html_url":"https://github.com/owner/repo/pull/1#discussion_r43"}`, mustMarshalJSON(t, value)) + default: + assert.JSONEq(t, `{"id":"77"}`, mustMarshalJSON(t, value)) + } default: - assert.JSONEq(t, text, mustMarshalJSON(t, value)) + assert.JSONEq(t, tc.text, mustMarshalJSON(t, value)) } } } @@ -225,6 +277,7 @@ func TestTypedConsolidatedPullRequestOutputs(t *testing.T) { const prRef = `{"id":"0","url":"https://github.com/owner/repo/pull/1"}` cases := []typedPullRequestCase{ {"pull_request_read", map[string]any{"method": "get", "pullNumber": "1"}, minimalPR}, + {"pull_request_read", map[string]any{"method": "get", "pullNumber": 3}, `null`}, {"pull_request_read", map[string]any{"method": "get_diff", "pullNumber": 1.0}, typedPRDiff}, {"pull_request_read", map[string]any{"method": "get_status", "pullNumber": 1}, `{"state":"success","sha":"abc","total_count":1,"statuses":[{"state":"success","context":"ci","target_url":"https://ci"}]}`}, {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 1, "page": "1", "perPage": 5}, `[{"filename":"file","status":"modified","additions":1,"changes":1,"patch":"+added"}]`}, @@ -234,9 +287,11 @@ func TestTypedConsolidatedPullRequestOutputs(t *testing.T) { {"pull_request_read", map[string]any{"method": "get_review_comments", "pullNumber": 1, "perPage": 10, "after": "cursor"}, `{"review_threads":[{"id":"T_1","is_resolved":false,"is_outdated":false,"is_collapsed":false,"comments":[{"body":"nit","path":"file","line":3,"original_line":3,"author":"octocat","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z","html_url":"https://github.com/owner/repo/pull/1#discussion_r1"}],"total_count":1}],"totalCount":1,"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"s","endCursor":"e"}}`}, {"pull_request_read", map[string]any{"method": "get_reviews", "pullNumber": 1}, `[{"id":9,"state":"APPROVED","body":"lgtm","html_url":"https://github.com/owner/repo/pull/1#pullrequestreview-9","user":{"login":"octocat"},"commit_id":"abc","author_association":"MEMBER"}]`}, {"pull_request_read", map[string]any{"method": "get_reviews", "pullNumber": 2}, `[]`}, + {"pull_request_read", map[string]any{"method": "get_reviews", "pullNumber": 3}, `[]`}, {"pull_request_read", map[string]any{"method": "get_comments", "pullNumber": 1}, `[{"id":42,"body":"hello","html_url":"https://github.com/owner/repo/pull/1#issuecomment-42"}]`}, {"pull_request_read", map[string]any{"method": "get_comments", "pullNumber": 2}, `[]`}, {"pull_request_read", map[string]any{"method": "get_check_runs", "pullNumber": 1}, `{"total_count":1,"check_runs":[{"id":5,"name":"build","status":"completed","conclusion":"success"}]}`}, + {"pull_request_read", map[string]any{"method": "get_check_runs", "pullNumber": 1, "page": 2}, `{"total_count":0,"check_runs":[]}`}, {"create_pull_request", map[string]any{"title": "Subject", "head": "feature", "base": "main", "draft": false}, prRef}, {"update_pull_request", map[string]any{"pullNumber": "1", "title": "Subject", "maintainer_can_modify": false}, prRef}, {"merge_pull_request", map[string]any{"pullNumber": 1, "merge_method": "squash", "expectedHeadSha": "abc"}, `{"sha":"abc","merged":true,"message":"Pull Request successfully merged"}`}, @@ -263,6 +318,40 @@ func TestTypedConsolidatedPullRequestOutputs(t *testing.T) { } } +func TestTypedPullRequestFilesLinkPagination(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + var requestedPages []string + rest := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/repos/owner/repo/pulls/1/files", strings.TrimPrefix(r.URL.Path, "/api/v3")) + require.Equal(t, "1", r.URL.Query().Get("per_page")) + page := r.URL.Query().Get("page") + requestedPages = append(requestedPages, page) + switch page { + case "1": + w.Header().Set("Link", `; rel="next"`) + _, _ = w.Write([]byte(`[{"filename":"first","status":"modified","additions":1,"changes":1}]`)) + case "2": + _, _ = w.Write([]byte(`[{"filename":"second","status":"added","additions":1,"changes":1}]`)) + default: + t.Errorf("unexpected page query: %q", page) + w.WriteHeader(http.StatusInternalServerError) + } + })}} + deps := BaseDeps{ + Client: mustNewGHClient(t, rest), GQLClient: githubv4.NewClient(rest), + RepoAccessCache: stubRepoAccessCache(nil, time.Minute), + } + session, schemas := consolidatedPullRequestSession(t, deps, protocol, false) + runTypedPullRequestCases(t, session, schemas, []typedPullRequestCase{ + {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 1, "page": 1, "perPage": 1}, `[{"filename":"first","status":"modified","additions":1,"changes":1}]`}, + {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 1, "page": 2, "perPage": 1}, `[{"filename":"second","status":"added","additions":1,"changes":1}]`}, + }) + assert.Equal(t, []string{"1", "2"}, requestedPages, "explicit page calls must follow the API Link header's next-page target") + }) + } +} + func TestTypedPullRequestReviewWriteResolutionReasonVariant(t *testing.T) { for _, protocol := range typedPullRequestProtocols { t.Run("protocol="+protocol, func(t *testing.T) { @@ -392,6 +481,52 @@ func TestTypedConsolidatedPullRequestAPIErrors(t *testing.T) { } } +func TestTypedPullRequestContentParity(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + for _, permission := range []string{"read", "write"} { + t.Run(protocol+"/"+permission, func(t *testing.T) { + deps := consolidatedPullRequestDeps(t) + deps.Client = mustNewGHClient(t, &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch strings.TrimPrefix(r.URL.Path, "/api/v3") { + case "/repos/owner/repo": + _, _ = w.Write([]byte(`{"private":false}`)) + case "/repos/owner/repo/pulls/1": + _, _ = w.Write([]byte(`{"number":1,"title":"Subject","body":"bo\u200bdy","state":"open","html_url":"https://github.com/owner/repo/pull/1","user":{"login":"octocat"}}`)) + default: + t.Errorf("unexpected request: %s", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })}}) + deps.Flags = stubFeatureFlags(map[string]bool{"lockdown-mode": true}) + deps.RepoAccessCache = stubRepoAccessCache(mockRESTPermissionServer(t, permission, nil), time.Minute) + deps.featureChecker = featureCheckerFor(FeatureFlagIFCLabels) + session, schemas := consolidatedPullRequestSession(t, deps, protocol, false) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: "pull_request_read", + Arguments: map[string]any{"method": "get", "owner": "owner", "repo": "repo", "pullNumber": 1}, + }) + require.NoError(t, err) + if permission == "read" { + require.True(t, result.IsError) + assert.Equal(t, "access to pull request is restricted by lockdown mode", getErrorResult(t, result).Text) + assert.Nil(t, result.StructuredContent) + return + } + require.False(t, result.IsError) + text := getTextResult(t, result).Text + require.NotNil(t, result.Meta["ifc"]) + if protocol == inventory.ProtocolVersionMultiRoundTrip { + assert.JSONEq(t, mustMarshalJSON(t, result.StructuredContent), text) + require.NoError(t, schemas["pull_request_read"].Validate(result.StructuredContent)) + } else { + assert.Nil(t, result.StructuredContent) + assert.Equal(t, `{"number":1,"title":"Subject","body":"body","state":"open","draft":false,"merged":false,"html_url":"https://github.com/owner/repo/pull/1","user":{"login":"octocat"}}`, text) + } + }) + } + } +} + func TestConsolidatedPullRequestOutputSchemas(t *testing.T) { for _, tc := range []struct { schema *jsonschema.Schema @@ -401,17 +536,17 @@ func TestConsolidatedPullRequestOutputSchemas(t *testing.T) { { pullRequestReadOutputSchema(), []string{`null`, `[]`, `{"diff":""}`, `{"number":1,"title":"","state":"","draft":false,"merged":false,"html_url":""}`, `{"state":"","sha":"","total_count":0,"statuses":[]}`, `{"total_count":0,"check_runs":[]}`, `{"review_threads":[],"totalCount":0,"pageInfo":{"hasNextPage":false,"hasPreviousPage":false}}`, `[{"filename":"f"}]`, `[{"id":1,"html_url":""}]`}, - []string{`{}`, `"diff"`, `{"diff":1}`, `[{"unknown":1}]`, `{"number":1}`}, + []string{`{}`, `"diff"`, `{"diff":1}`, `[{"unknown":1}]`, `{"number":1}`, `{"number":1,"title":"","state":"unknown","draft":false,"merged":false,"html_url":""}`}, }, { pullRequestWriteOutputSchema(), - []string{`{"id":"","url":""}`, `{"status":"awaiting_user_submission","reason":"wait"}`}, + []string{`{"id":"","html_url":""}`, `{"status":"awaiting_user_submission","reason":"wait"}`}, []string{`{}`, `null`, `{"status":"created","reason":"wait"}`}, }, { pullRequestCommentReplyOutputSchema(), - []string{`{"id":"1","url":"u"}`, `{"comment":{"id":"1","url":"u"},"reaction":{"id":"2","url":"v"}}`}, - []string{`{}`, `{"comment":{"id":"1","url":"u"}}`, `null`}, + []string{`{"id":"1","html_url":"u"}`, `{"comment":{"id":"1","html_url":"u"},"reaction":{"id":"2"}}`}, + []string{`{}`, `{"comment":{"id":"1","html_url":"u"}}`, `null`, `{"id":"1","url":"https://api.github.com/derivable"}`}, }, { pullRequestMergeOutputSchema(), @@ -420,8 +555,8 @@ func TestConsolidatedPullRequestOutputSchemas(t *testing.T) { }, { pullRequestBranchUpdateOutputSchema(), - []string{`null`, `{"message":"m"}`, `{"message":"m","url":"u"}`}, - []string{`[]`, `{"message":1}`}, + []string{`null`, `{"message":"m"}`}, + []string{`[]`, `{"message":1}`, `{"message":"m","url":"https://api.github.com/derivable"}`}, }, } { resolved, err := tc.schema.Resolve(nil) @@ -431,6 +566,7 @@ func TestConsolidatedPullRequestOutputSchemas(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(raw), &value)) require.NoError(t, resolved.Validate(value), raw) } + for _, raw := range tc.invalid { var value any require.NoError(t, json.Unmarshal([]byte(raw), &value)) @@ -455,3 +591,32 @@ func TestConsolidatedPullRequestOutputSchemas(t *testing.T) { require.NoError(t, resolved.Validate(value), mustMarshalJSON(t, tc.value)) } } + +func TestConsolidatedPullRequestOutputTypesAreCurated(t *testing.T) { + visited := make(map[reflect.Type]bool) + var check func(reflect.Type) + check = func(typ reflect.Type) { + if visited[typ] { + return + } + visited[typ] = true + require.NotContains(t, typ.PkgPath(), "github.com/google/go-github", typ.String()) + switch typ.Kind() { + case reflect.Pointer, reflect.Slice, reflect.Array: + check(typ.Elem()) + case reflect.Struct: + for field := range typ.Fields() { + check(field.Type) + } + case reflect.Map, reflect.Interface: + t.Errorf("non-concrete output field: %s", typ) + } + } + for _, typ := range []reflect.Type{ + reflect.TypeFor[PullRequestReadOutput](), reflect.TypeFor[PullRequestWriteOutput](), + reflect.TypeFor[PullRequestMergeOutput](), reflect.TypeFor[PullRequestBranchUpdateOutput](), + reflect.TypeFor[PullRequestCommentReplyOutput](), + } { + check(typ) + } +} From e0db637a898836762aed125cdae0ee01337e7a6d Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 5 Oct 2026 18:00:29 +0200 Subject: [PATCH 3/4] fix(pull-requests): preserve legacy validation order Replay parameter checks in handler order and scope cursor validation to review-thread reads. Keep commit message schema descriptions distinct from operation messages. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../__toolsnaps__/pull_request_read.snap | 1 - pkg/github/consolidated_pull_request_types.go | 122 +++++++++++++++--- ..._consolidated_pull_request_outputs_test.go | 15 +++ 3 files changed, 118 insertions(+), 20 deletions(-) diff --git a/pkg/github/__toolsnaps__/pull_request_read.snap b/pkg/github/__toolsnaps__/pull_request_read.snap index 2216e21f3c..c99d4276e2 100644 --- a/pkg/github/__toolsnaps__/pull_request_read.snap +++ b/pkg/github/__toolsnaps__/pull_request_read.snap @@ -730,7 +730,6 @@ "type": "string" }, "message": { - "description": "Operation result message.", "type": "string" }, "sha": { diff --git a/pkg/github/consolidated_pull_request_types.go b/pkg/github/consolidated_pull_request_types.go index 6bee34d975..213a7cb7b3 100644 --- a/pkg/github/consolidated_pull_request_types.go +++ b/pkg/github/consolidated_pull_request_types.go @@ -339,7 +339,9 @@ func describePullRequestOutputSchema(schema *jsonschema.Schema) { case "total_count", "totalCount": field.Description = "Total number of results." case "message": - field.Description = "Operation result message." + if schema.Properties["author"] == nil { + field.Description = "Operation result message." + } case "merged": field.Description = "Whether the pull request was merged." case "diff": @@ -412,17 +414,17 @@ type pullRequestArgumentSpec struct { var pullRequestArgumentSpecs = map[string]pullRequestArgumentSpec{ "read": { required: []string{"method", "owner", "repo"}, requiredInts: []string{"pullNumber"}, - ints: []string{"page", "perPage"}, strings: []string{"after"}, + ints: []string{"page", "perPage"}, methods: []string{"get", "get_diff", "get_status", "get_files", "get_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"}, }, "create": { required: []string{"owner", "repo"}, deferred: []string{"title", "head", "base"}, - strings: []string{"title", "head", "base", "body"}, bools: []string{"draft", "maintainer_can_modify", "_ui_submitted"}, + strings: []string{"title", "head", "base", "body"}, bools: []string{"draft", "maintainer_can_modify"}, stringArrays: []string{"reviewers"}, }, "update": { required: []string{"owner", "repo"}, requiredInts: []string{"pullNumber"}, - strings: []string{"title", "body", "state", "base"}, bools: []string{"draft", "maintainer_can_modify", "_ui_submitted"}, + strings: []string{"title", "body", "state", "base"}, bools: []string{"draft", "maintainer_can_modify"}, stringArrays: []string{"reviewers"}, }, "merge": { @@ -483,35 +485,83 @@ func normalizePullRequestArguments(kind string) func(json.RawMessage) (json.RawM return nil, fmt.Errorf("commentId must be greater than 0") } args["commentId"] = commentID - // pullNumber is only read, and validated, when a reply body is sent. - if value, exists := args["pullNumber"]; exists { - pullNumber, err := toInt(value) - switch { - case err == nil: - args["pullNumber"] = pullNumber - case args["body"] != nil: - return nil, fmt.Errorf("parameter pullNumber is not a valid number: %w", err) - default: - delete(args, "pullNumber") - } - } } for _, field := range spec.deferred { if _, exists := args[field]; !exists { args[field] = "" } } - for _, field := range spec.strings { + + stringFields := spec.strings + boolFields := spec.bools + stringArrayFields := spec.stringArrays + if kind == "create" { + stringFields = []string{"title", "head", "base"} + boolFields = []string{"draft", "maintainer_can_modify"} + stringArrayFields = []string{"reviewers"} + } + if kind == "update" { + boolFields = []string{"maintainer_can_modify"} + } + if kind == "reply" { + stringFields = []string{"body", "reaction"} + } + if kind == "update" { + // The update handler checks draft before title/body/state/base. + if _, err := OptionalParam[bool](args, "draft"); err != nil { + return nil, err + } + } + if kind == "create" || kind == "update" { + // This flag is inspected from the raw call only to control form + // deferral; legacy handlers never type-validated it. + if _, err := OptionalParam[bool](args, "_ui_submitted"); err != nil { + delete(args, "_ui_submitted") + } + } + for _, field := range stringFields { if _, err := OptionalParam[string](args, field); err != nil { return nil, err } } - for _, field := range spec.bools { + if kind == "create" { + missingRequired := "" + for _, field := range []string{"title", "head", "base"} { + if args[field] == "" && missingRequired == "" { + missingRequired = field + } + } + if missingRequired != "" { + // The handler may defer to the form before checking these later + // fields. Drop malformed values so strict typed decoding cannot + // change the earlier missing-field result. + if _, err := OptionalParam[string](args, "body"); err != nil { + delete(args, "body") + } + for _, field := range []string{"draft", "maintainer_can_modify", "_ui_submitted"} { + if _, err := OptionalParam[bool](args, field); err != nil { + delete(args, field) + } + } + if _, exists := args["reviewers"]; exists { + if _, err := OptionalStringArrayParam(args, "reviewers"); err != nil { + delete(args, "reviewers") + } + } + boolFields = nil + stringArrayFields = nil + } else { + if _, err := OptionalParam[string](args, "body"); err != nil { + return nil, err + } + } + } + for _, field := range boolFields { if _, err := OptionalParam[bool](args, field); err != nil { return nil, err } } - for _, field := range spec.stringArrays { + for _, field := range stringArrayFields { if _, exists := args[field]; !exists { continue } @@ -534,6 +584,40 @@ func normalizePullRequestArguments(kind string) func(json.RawMessage) (json.RawM if spec.methods != nil && !slices.Contains(spec.methods, args["method"].(string)) { return nil, fmt.Errorf("unknown method: %s", args["method"]) } + if kind == "read" { + if args["method"] == "get_review_comments" { + if _, err := OptionalParam[string](args, "after"); err != nil { + return nil, err + } + } else { + delete(args, "after") + } + } + if kind == "reply" { + _, hasBody := args["body"] + _, hasReaction := args["reaction"] + if !hasBody && !hasReaction { + return nil, fmt.Errorf("at least one of body or reaction is required") + } + if hasBody && args["body"] == "" { + return nil, fmt.Errorf("body cannot be empty when provided") + } + if hasReaction && args["reaction"] == "" { + return nil, fmt.Errorf("reaction cannot be empty when provided") + } + // pullNumber is only read, and validated, when a reply body is sent. + if value, exists := args["pullNumber"]; exists { + pullNumber, err := toInt(value) + switch { + case err == nil && hasBody: + args["pullNumber"] = pullNumber + case err != nil && hasBody: + return nil, fmt.Errorf("parameter pullNumber is not a valid number: %w", err) + default: + delete(args, "pullNumber") + } + } + } for _, field := range spec.ignoredStrings { if _, isString := args[field].(string); !isString { delete(args, field) diff --git a/pkg/github/typed_consolidated_pull_request_outputs_test.go b/pkg/github/typed_consolidated_pull_request_outputs_test.go index f2f500b743..a6142c37b9 100644 --- a/pkg/github/typed_consolidated_pull_request_outputs_test.go +++ b/pkg/github/typed_consolidated_pull_request_outputs_test.go @@ -278,6 +278,7 @@ func TestTypedConsolidatedPullRequestOutputs(t *testing.T) { cases := []typedPullRequestCase{ {"pull_request_read", map[string]any{"method": "get", "pullNumber": "1"}, minimalPR}, {"pull_request_read", map[string]any{"method": "get", "pullNumber": 3}, `null`}, + {"pull_request_read", map[string]any{"method": "get", "pullNumber": 1, "after": 1}, minimalPR}, {"pull_request_read", map[string]any{"method": "get_diff", "pullNumber": 1.0}, typedPRDiff}, {"pull_request_read", map[string]any{"method": "get_status", "pullNumber": 1}, `{"state":"success","sha":"abc","total_count":1,"statuses":[{"state":"success","context":"ci","target_url":"https://ci"}]}`}, {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 1, "page": "1", "perPage": 5}, `[{"filename":"file","status":"modified","additions":1,"changes":1,"patch":"+added"}]`}, @@ -408,7 +409,9 @@ func TestTypedConsolidatedPullRequestErrors(t *testing.T) { {"pull_request_read", map[string]any{"method": "unknown", "pullNumber": 1}, "unknown method: unknown"}, {"pull_request_read", map[string]any{"method": "get", "pullNumber": 1.5}, "parameter pullNumber is not a valid number: non-integer numeric value: 1.5"}, {"create_pull_request", map[string]any{"title": "Subject", "head": "feature", "base": "main", "draft": "yes"}, "parameter draft is not of type bool, is string"}, + {"create_pull_request", map[string]any{"head": "feature", "base": "main", "draft": "yes"}, "missing required parameter: title"}, {"update_pull_request", map[string]any{"pullNumber": 0, "title": "Subject"}, "missing required parameter: pullNumber"}, + {"update_pull_request", map[string]any{"pullNumber": 1, "draft": "yes", "title": 1}, "parameter draft is not of type bool, is string"}, {"pull_request_review_write", map[string]any{"method": "nope", "pullNumber": 1}, "unknown method: nope"}, {"add_comment_to_pending_review", map[string]any{"pullNumber": 1, "body": "nit", "subjectType": "LINE"}, "missing required parameter: path"}, {"add_reply_to_pull_request_comment", map[string]any{"commentId": 0, "body": "reply"}, "missing required parameter: commentId"}, @@ -424,6 +427,8 @@ func TestTypedConsolidatedPullRequestErrors(t *testing.T) { {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "body": "reply"}, "missing required parameter: pullNumber"}, {"add_reply_to_pull_request_comment", map[string]any{"commentId": "1e30", "body": "reply"}, "parameter commentId is not a valid number: numeric value 1e+30 is too large to fit in int64"}, {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "body": "reply"}, "parameter pullNumber is not a valid number: invalid numeric value: x"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "body": ""}, "body cannot be empty when provided"}, + {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "body": "reply", "reaction": 1}, "parameter reaction is not of type string, is float64"}, } for _, protocol := range typedPullRequestProtocols { t.Run("protocol="+protocol, func(t *testing.T) { @@ -592,6 +597,16 @@ func TestConsolidatedPullRequestOutputSchemas(t *testing.T) { } } +func TestPullRequestOutputDescriptionsAreShapeAware(t *testing.T) { + commitSchema := pullRequestOutputSchema[MinimalPullRequestCommit]() + require.Contains(t, commitSchema.Properties, "message") + assert.Empty(t, commitSchema.Properties["message"].Description) + + branchUpdateSchema := pullRequestBranchUpdateOutputSchema() + require.Contains(t, branchUpdateSchema.Properties, "message") + assert.Equal(t, "Operation result message.", branchUpdateSchema.Properties["message"].Description) +} + func TestConsolidatedPullRequestOutputTypesAreCurated(t *testing.T) { visited := make(map[reflect.Type]bool) var check func(reflect.Type) From 13118145be79873c8ae9392113e34faffc6d7807 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 5 Oct 2026 18:12:43 +0200 Subject: [PATCH 4/4] refactor(pull-requests): simplify compatibility validation Remove normalization and test assertions that only pinned which invalid argument wins. Keep per-field validation, accepted coercions, and response-shape behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pkg/github/consolidated_pull_request_types.go | 67 ++----------------- ..._consolidated_pull_request_outputs_test.go | 5 +- 2 files changed, 7 insertions(+), 65 deletions(-) diff --git a/pkg/github/consolidated_pull_request_types.go b/pkg/github/consolidated_pull_request_types.go index 213a7cb7b3..e32976ba50 100644 --- a/pkg/github/consolidated_pull_request_types.go +++ b/pkg/github/consolidated_pull_request_types.go @@ -495,23 +495,9 @@ func normalizePullRequestArguments(kind string) func(json.RawMessage) (json.RawM stringFields := spec.strings boolFields := spec.bools stringArrayFields := spec.stringArrays - if kind == "create" { - stringFields = []string{"title", "head", "base"} - boolFields = []string{"draft", "maintainer_can_modify"} - stringArrayFields = []string{"reviewers"} - } - if kind == "update" { - boolFields = []string{"maintainer_can_modify"} - } if kind == "reply" { stringFields = []string{"body", "reaction"} } - if kind == "update" { - // The update handler checks draft before title/body/state/base. - if _, err := OptionalParam[bool](args, "draft"); err != nil { - return nil, err - } - } if kind == "create" || kind == "update" { // This flag is inspected from the raw call only to control form // deferral; legacy handlers never type-validated it. @@ -524,38 +510,6 @@ func normalizePullRequestArguments(kind string) func(json.RawMessage) (json.RawM return nil, err } } - if kind == "create" { - missingRequired := "" - for _, field := range []string{"title", "head", "base"} { - if args[field] == "" && missingRequired == "" { - missingRequired = field - } - } - if missingRequired != "" { - // The handler may defer to the form before checking these later - // fields. Drop malformed values so strict typed decoding cannot - // change the earlier missing-field result. - if _, err := OptionalParam[string](args, "body"); err != nil { - delete(args, "body") - } - for _, field := range []string{"draft", "maintainer_can_modify", "_ui_submitted"} { - if _, err := OptionalParam[bool](args, field); err != nil { - delete(args, field) - } - } - if _, exists := args["reviewers"]; exists { - if _, err := OptionalStringArrayParam(args, "reviewers"); err != nil { - delete(args, "reviewers") - } - } - boolFields = nil - stringArrayFields = nil - } else { - if _, err := OptionalParam[string](args, "body"); err != nil { - return nil, err - } - } - } for _, field := range boolFields { if _, err := OptionalParam[bool](args, field); err != nil { return nil, err @@ -584,27 +538,12 @@ func normalizePullRequestArguments(kind string) func(json.RawMessage) (json.RawM if spec.methods != nil && !slices.Contains(spec.methods, args["method"].(string)) { return nil, fmt.Errorf("unknown method: %s", args["method"]) } - if kind == "read" { - if args["method"] == "get_review_comments" { - if _, err := OptionalParam[string](args, "after"); err != nil { - return nil, err - } - } else { - delete(args, "after") - } - } if kind == "reply" { _, hasBody := args["body"] _, hasReaction := args["reaction"] if !hasBody && !hasReaction { return nil, fmt.Errorf("at least one of body or reaction is required") } - if hasBody && args["body"] == "" { - return nil, fmt.Errorf("body cannot be empty when provided") - } - if hasReaction && args["reaction"] == "" { - return nil, fmt.Errorf("reaction cannot be empty when provided") - } // pullNumber is only read, and validated, when a reply body is sent. if value, exists := args["pullNumber"]; exists { pullNumber, err := toInt(value) @@ -617,6 +556,12 @@ func normalizePullRequestArguments(kind string) func(json.RawMessage) (json.RawM delete(args, "pullNumber") } } + if hasBody && args["body"] == "" { + return nil, fmt.Errorf("body cannot be empty when provided") + } + if hasReaction && args["reaction"] == "" { + return nil, fmt.Errorf("reaction cannot be empty when provided") + } } for _, field := range spec.ignoredStrings { if _, isString := args[field].(string); !isString { diff --git a/pkg/github/typed_consolidated_pull_request_outputs_test.go b/pkg/github/typed_consolidated_pull_request_outputs_test.go index a6142c37b9..082691bc20 100644 --- a/pkg/github/typed_consolidated_pull_request_outputs_test.go +++ b/pkg/github/typed_consolidated_pull_request_outputs_test.go @@ -278,7 +278,6 @@ func TestTypedConsolidatedPullRequestOutputs(t *testing.T) { cases := []typedPullRequestCase{ {"pull_request_read", map[string]any{"method": "get", "pullNumber": "1"}, minimalPR}, {"pull_request_read", map[string]any{"method": "get", "pullNumber": 3}, `null`}, - {"pull_request_read", map[string]any{"method": "get", "pullNumber": 1, "after": 1}, minimalPR}, {"pull_request_read", map[string]any{"method": "get_diff", "pullNumber": 1.0}, typedPRDiff}, {"pull_request_read", map[string]any{"method": "get_status", "pullNumber": 1}, `{"state":"success","sha":"abc","total_count":1,"statuses":[{"state":"success","context":"ci","target_url":"https://ci"}]}`}, {"pull_request_read", map[string]any{"method": "get_files", "pullNumber": 1, "page": "1", "perPage": 5}, `[{"filename":"file","status":"modified","additions":1,"changes":1,"patch":"+added"}]`}, @@ -409,9 +408,8 @@ func TestTypedConsolidatedPullRequestErrors(t *testing.T) { {"pull_request_read", map[string]any{"method": "unknown", "pullNumber": 1}, "unknown method: unknown"}, {"pull_request_read", map[string]any{"method": "get", "pullNumber": 1.5}, "parameter pullNumber is not a valid number: non-integer numeric value: 1.5"}, {"create_pull_request", map[string]any{"title": "Subject", "head": "feature", "base": "main", "draft": "yes"}, "parameter draft is not of type bool, is string"}, - {"create_pull_request", map[string]any{"head": "feature", "base": "main", "draft": "yes"}, "missing required parameter: title"}, {"update_pull_request", map[string]any{"pullNumber": 0, "title": "Subject"}, "missing required parameter: pullNumber"}, - {"update_pull_request", map[string]any{"pullNumber": 1, "draft": "yes", "title": 1}, "parameter draft is not of type bool, is string"}, + {"update_pull_request", map[string]any{"pullNumber": 1, "draft": "yes"}, "parameter draft is not of type bool, is string"}, {"pull_request_review_write", map[string]any{"method": "nope", "pullNumber": 1}, "unknown method: nope"}, {"add_comment_to_pending_review", map[string]any{"pullNumber": 1, "body": "nit", "subjectType": "LINE"}, "missing required parameter: path"}, {"add_reply_to_pull_request_comment", map[string]any{"commentId": 0, "body": "reply"}, "missing required parameter: commentId"}, @@ -427,7 +425,6 @@ func TestTypedConsolidatedPullRequestErrors(t *testing.T) { {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "body": "reply"}, "missing required parameter: pullNumber"}, {"add_reply_to_pull_request_comment", map[string]any{"commentId": "1e30", "body": "reply"}, "parameter commentId is not a valid number: numeric value 1e+30 is too large to fit in int64"}, {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "body": "reply"}, "parameter pullNumber is not a valid number: invalid numeric value: x"}, - {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "body": ""}, "body cannot be empty when provided"}, {"add_reply_to_pull_request_comment", map[string]any{"commentId": 42, "pullNumber": "x", "body": "reply", "reaction": 1}, "parameter reaction is not of type string, is float64"}, } for _, protocol := range typedPullRequestProtocols {