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 4cb523f65b..468f100d38 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)) + } +} diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index d67b374b56..817fb7b2a9 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -21,7 +21,7 @@ The following packages are included for the amd64, arm64 architectures. - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE)) - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE)) + - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/LICENSE)) - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.8.0/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index a1abb6821b..ae5e60d8ea 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -21,7 +21,7 @@ The following packages are included for the 386, amd64, arm64 architectures. - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE)) - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE)) + - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/LICENSE)) - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.8.0/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 7e6f57aeb7..adab52cdee 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -22,7 +22,7 @@ The following packages are included for the 386, amd64, arm64 architectures. - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE)) - - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE)) + - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/LICENSE)) - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.8.0/LICENSE))