diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 0d32b3c33a..100e2cc6c7 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -237,15 +237,15 @@ as output formatting) won't appear here. - `path`: The relative path of the file to comment on (string, required) - `pullNumber`: The pull request number (number, required) - `repo`: Repository name (string, required) - - `side`: The side of the diff to comment on (optional) (string, optional) + - `side`: The side of the diff to comment on (LEFT or RIGHT, optional) (string, optional) - `startLine`: The start line of a multi-line comment (optional) (number, optional) - - `startSide`: The start side of a multi-line comment (optional) (string, optional) - - `subjectType`: The subject type of the comment (string, required) + - `startSide`: The start side of a multi-line comment (LEFT or RIGHT, optional) (string, optional) + - `subjectType`: The subject type of the comment (FILE or LINE) (string, required) - **add_pull_request_review_comment_reaction** - Add Pull Request Review Comment Reaction - **OAuth Challenge Scopes**: `repo` - `comment_id`: The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...). (number, required) - - `content`: The emoji reaction type (string, required) + - `content`: The emoji reaction type (+1, -1, laugh, confused, heart, hooray, rocket, or eyes) (string, required) - `owner`: Repository owner (username or organization) (string, required) - `repo`: Repository name (string, required) @@ -253,7 +253,7 @@ as output formatting) won't appear here. - **OAuth Challenge Scopes**: `repo` - `body`: The review body text (optional) (string, optional) - `commitID`: The SHA of the commit to review (optional, defaults to latest) (string, optional) - - `event`: The review action to perform. If omitted, creates a pending review. (string, optional) + - `event`: The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT). If omitted, creates a pending review. (string, optional) - `owner`: Repository owner (username or organization) (string, required) - `pullNumber`: The pull request number (number, required) - `repo`: Repository name (string, required) @@ -300,7 +300,7 @@ as output formatting) won't appear here. - **submit_pending_pull_request_review** - Submit Pending Pull Request Review - **OAuth Challenge Scopes**: `repo` - `body`: The review body text (optional) (string, optional) - - `event`: The review action to perform (string, required) + - `event`: The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT) (string, required) - `owner`: Repository owner (username or organization) (string, required) - `pullNumber`: The pull request number (number, required) - `repo`: Repository name (string, required) @@ -341,7 +341,7 @@ as output formatting) won't appear here. - `owner`: Repository owner (username or organization) (string, required) - `pullNumber`: The pull request number (number, required) - `repo`: Repository name (string, required) - - `state`: The new state for the pull request (string, required) + - `state`: The new state for the pull request (open or closed) (string, required) - **update_pull_request_title** - Update Pull Request Title - **OAuth Challenge Scopes**: `repo` diff --git a/pkg/github/__toolsnaps__/add_pull_request_review_comment.snap b/pkg/github/__toolsnaps__/add_pull_request_review_comment.snap index ae7450539d..9914ae6943 100644 --- a/pkg/github/__toolsnaps__/add_pull_request_review_comment.snap +++ b/pkg/github/__toolsnaps__/add_pull_request_review_comment.snap @@ -27,7 +27,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { @@ -35,11 +34,7 @@ "type": "string" }, "side": { - "description": "The side of the diff to comment on (optional)", - "enum": [ - "LEFT", - "RIGHT" - ], + "description": "The side of the diff to comment on (LEFT or RIGHT, optional)", "type": "string" }, "startLine": { @@ -47,19 +42,11 @@ "type": "number" }, "startSide": { - "description": "The start side of a multi-line comment (optional)", - "enum": [ - "LEFT", - "RIGHT" - ], + "description": "The start side of a multi-line comment (LEFT or RIGHT, optional)", "type": "string" }, "subjectType": { - "description": "The subject type of the comment", - "enum": [ - "FILE", - "LINE" - ], + "description": "The subject type of the comment (FILE or LINE)", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/add_pull_request_review_comment_reaction.snap b/pkg/github/__toolsnaps__/add_pull_request_review_comment_reaction.snap index aec4b38465..11742ec562 100644 --- a/pkg/github/__toolsnaps__/add_pull_request_review_comment_reaction.snap +++ b/pkg/github/__toolsnaps__/add_pull_request_review_comment_reaction.snap @@ -11,21 +11,10 @@ "properties": { "comment_id": { "description": "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", - "minimum": 1, "type": "number" }, "content": { - "description": "The emoji reaction type", - "enum": [ - "+1", - "-1", - "laugh", - "confused", - "heart", - "hooray", - "rocket", - "eyes" - ], + "description": "The emoji reaction type (+1, -1, laugh, confused, heart, hooray, rocket, or eyes)", "type": "string" }, "owner": { diff --git a/pkg/github/__toolsnaps__/add_pull_request_review_comment_reaction_typed.snap b/pkg/github/__toolsnaps__/add_pull_request_review_comment_reaction_typed.snap new file mode 100644 index 0000000000..5189dea14a --- /dev/null +++ b/pkg/github/__toolsnaps__/add_pull_request_review_comment_reaction_typed.snap @@ -0,0 +1,68 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Add Pull Request Review Comment Reaction" + }, + "description": "Add a reaction to a pull request review comment.", + "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": { + "comment_id": { + "description": "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", + "type": "number" + }, + "content": { + "description": "The emoji reaction type (+1, -1, laugh, confused, heart, hooray, rocket, or eyes)", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "comment_id", + "content" + ], + "type": "object" + }, + "name": "add_pull_request_review_comment_reaction", + "outputSchema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/add_pull_request_review_comment_typed.snap b/pkg/github/__toolsnaps__/add_pull_request_review_comment_typed.snap new file mode 100644 index 0000000000..42efa4c14f --- /dev/null +++ b/pkg/github/__toolsnaps__/add_pull_request_review_comment_typed.snap @@ -0,0 +1,90 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Add Pull Request Review Comment" + }, + "description": "Add a review comment to the current user's pending pull request review.", + "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 comment body", + "type": "string" + }, + "line": { + "description": "The line number in the diff to comment on (optional)", + "type": "number" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "path": { + "description": "The relative path of the file to comment on", + "type": "string" + }, + "pullNumber": { + "description": "The 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 or RIGHT, optional)", + "type": "string" + }, + "startLine": { + "description": "The start line of a multi-line comment (optional)", + "type": "number" + }, + "startSide": { + "description": "The start side of a multi-line comment (LEFT or RIGHT, optional)", + "type": "string" + }, + "subjectType": { + "description": "The subject type of the comment (FILE or LINE)", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], + "type": "object" + }, + "name": "add_pull_request_review_comment", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_pull_request_review.snap b/pkg/github/__toolsnaps__/create_pull_request_review.snap index f8bf3f12a8..def275fa0c 100644 --- a/pkg/github/__toolsnaps__/create_pull_request_review.snap +++ b/pkg/github/__toolsnaps__/create_pull_request_review.snap @@ -18,12 +18,7 @@ "type": "string" }, "event": { - "description": "The review action to perform. If omitted, creates a pending review.", - "enum": [ - "APPROVE", - "REQUEST_CHANGES", - "COMMENT" - ], + "description": "The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT). If omitted, creates a pending review.", "type": "string" }, "owner": { @@ -32,7 +27,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/create_pull_request_review_typed.snap b/pkg/github/__toolsnaps__/create_pull_request_review_typed.snap new file mode 100644 index 0000000000..be07893e9e --- /dev/null +++ b/pkg/github/__toolsnaps__/create_pull_request_review_typed.snap @@ -0,0 +1,71 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Create Pull Request Review" + }, + "description": "Create a review on a pull request. If event is provided, the review is submitted immediately; otherwise a pending review is created.", + "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 review body text (optional)", + "type": "string" + }, + "commitID": { + "description": "The SHA of the commit to review (optional, defaults to latest)", + "type": "string" + }, + "event": { + "description": "The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT). If omitted, creates a pending review.", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "create_pull_request_review", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_pending_pull_request_review.snap b/pkg/github/__toolsnaps__/delete_pending_pull_request_review.snap index f99f5d5632..6ff8d5a65b 100644 --- a/pkg/github/__toolsnaps__/delete_pending_pull_request_review.snap +++ b/pkg/github/__toolsnaps__/delete_pending_pull_request_review.snap @@ -15,7 +15,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/delete_pending_pull_request_review_typed.snap b/pkg/github/__toolsnaps__/delete_pending_pull_request_review_typed.snap new file mode 100644 index 0000000000..4ae711a1e4 --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_pending_pull_request_review_typed.snap @@ -0,0 +1,59 @@ +{ + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Delete Pending Pull Request Review" + }, + "description": "Delete a pending pull request review.", + "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": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "delete_pending_pull_request_review", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/remove_pull_request_review_comment_reaction.snap b/pkg/github/__toolsnaps__/remove_pull_request_review_comment_reaction.snap index 57514e6484..9d47996332 100644 --- a/pkg/github/__toolsnaps__/remove_pull_request_review_comment_reaction.snap +++ b/pkg/github/__toolsnaps__/remove_pull_request_review_comment_reaction.snap @@ -11,7 +11,6 @@ "properties": { "comment_id": { "description": "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", - "minimum": 1, "type": "number" }, "owner": { @@ -20,7 +19,6 @@ }, "reaction_id": { "description": "The reaction ID to remove", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/remove_pull_request_review_comment_reaction_typed.snap b/pkg/github/__toolsnaps__/remove_pull_request_review_comment_reaction_typed.snap new file mode 100644 index 0000000000..9ce6a6ec14 --- /dev/null +++ b/pkg/github/__toolsnaps__/remove_pull_request_review_comment_reaction_typed.snap @@ -0,0 +1,64 @@ +{ + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Remove Pull Request Review Comment Reaction" + }, + "description": "Remove a reaction from a pull request review comment.", + "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": { + "comment_id": { + "description": "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", + "type": "number" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "reaction_id": { + "description": "The reaction ID to remove", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "comment_id", + "reaction_id" + ], + "type": "object" + }, + "name": "remove_pull_request_review_comment_reaction", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/request_pull_request_reviewers.snap b/pkg/github/__toolsnaps__/request_pull_request_reviewers.snap index 607f540d1b..e1de2aaa02 100644 --- a/pkg/github/__toolsnaps__/request_pull_request_reviewers.snap +++ b/pkg/github/__toolsnaps__/request_pull_request_reviewers.snap @@ -15,7 +15,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/request_pull_request_reviewers_typed.snap b/pkg/github/__toolsnaps__/request_pull_request_reviewers_typed.snap new file mode 100644 index 0000000000..0b66ab993d --- /dev/null +++ b/pkg/github/__toolsnaps__/request_pull_request_reviewers_typed.snap @@ -0,0 +1,71 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Request Pull Request Reviewers" + }, + "description": "Request reviewers for a pull request.", + "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": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "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" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "reviewers" + ], + "type": "object" + }, + "name": "request_pull_request_reviewers", + "outputSchema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/resolve_review_thread_resolution_reason_typed.snap b/pkg/github/__toolsnaps__/resolve_review_thread_resolution_reason_typed.snap new file mode 100644 index 0000000000..118be96378 --- /dev/null +++ b/pkg/github/__toolsnaps__/resolve_review_thread_resolution_reason_typed.snap @@ -0,0 +1,51 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Resolve Review Thread" + }, + "description": "Resolve a review thread on a pull request. Resolving an already-resolved thread is a no-op.", + "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": { + "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 to resolve (e.g., PRRT_kwDOxxx)", + "type": "string" + } + }, + "required": [ + "threadID" + ], + "type": "object" + }, + "name": "resolve_review_thread", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/resolve_review_thread_typed.snap b/pkg/github/__toolsnaps__/resolve_review_thread_typed.snap new file mode 100644 index 0000000000..22968bfc44 --- /dev/null +++ b/pkg/github/__toolsnaps__/resolve_review_thread_typed.snap @@ -0,0 +1,47 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Resolve Review Thread" + }, + "description": "Resolve a review thread on a pull request. Resolving an already-resolved thread is a no-op.", + "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": { + "threadID": { + "description": "The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx)", + "type": "string" + } + }, + "required": [ + "threadID" + ], + "type": "object" + }, + "name": "resolve_review_thread", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/submit_pending_pull_request_review.snap b/pkg/github/__toolsnaps__/submit_pending_pull_request_review.snap index 12438ffe71..773d0f6429 100644 --- a/pkg/github/__toolsnaps__/submit_pending_pull_request_review.snap +++ b/pkg/github/__toolsnaps__/submit_pending_pull_request_review.snap @@ -14,12 +14,7 @@ "type": "string" }, "event": { - "description": "The review action to perform", - "enum": [ - "APPROVE", - "REQUEST_CHANGES", - "COMMENT" - ], + "description": "The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT)", "type": "string" }, "owner": { @@ -28,7 +23,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/submit_pending_pull_request_review_typed.snap b/pkg/github/__toolsnaps__/submit_pending_pull_request_review_typed.snap new file mode 100644 index 0000000000..d10ac9c877 --- /dev/null +++ b/pkg/github/__toolsnaps__/submit_pending_pull_request_review_typed.snap @@ -0,0 +1,68 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Submit Pending Pull Request Review" + }, + "description": "Submit a pending pull request review.", + "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 review body text (optional)", + "type": "string" + }, + "event": { + "description": "The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT)", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "event" + ], + "type": "object" + }, + "name": "submit_pending_pull_request_review", + "outputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/unresolve_review_thread_typed.snap b/pkg/github/__toolsnaps__/unresolve_review_thread_typed.snap new file mode 100644 index 0000000000..80d561a7d1 --- /dev/null +++ b/pkg/github/__toolsnaps__/unresolve_review_thread_typed.snap @@ -0,0 +1,47 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Unresolve Review Thread" + }, + "description": "Unresolve a previously resolved review thread on a pull request. Unresolving an already-unresolved thread is a no-op.", + "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": { + "threadID": { + "description": "The node ID of the review thread to unresolve (e.g., PRRT_kwDOxxx)", + "type": "string" + } + }, + "required": [ + "threadID" + ], + "type": "object" + }, + "name": "unresolve_review_thread", + "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_body.snap b/pkg/github/__toolsnaps__/update_pull_request_body.snap index 00b4fb978a..1143ec1aa2 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_body.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_body.snap @@ -19,7 +19,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/update_pull_request_body_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_body_typed.snap new file mode 100644 index 0000000000..28835c74e1 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request_body_typed.snap @@ -0,0 +1,68 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Update Pull Request Body" + }, + "description": "Update the body description of an existing pull request.", + "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 new body content for the pull request", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "body" + ], + "type": "object" + }, + "name": "update_pull_request_body", + "outputSchema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request_draft_state.snap b/pkg/github/__toolsnaps__/update_pull_request_draft_state.snap index 653fa3398f..28c299b104 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_draft_state.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_draft_state.snap @@ -19,7 +19,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/update_pull_request_draft_state_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_draft_state_typed.snap new file mode 100644 index 0000000000..c040322050 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request_draft_state_typed.snap @@ -0,0 +1,64 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Update Pull Request Draft State" + }, + "description": "Mark a pull request as draft or ready for review.", + "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": { + "draft": { + "description": "Set to true to convert to draft, false to mark as ready for review", + "type": "boolean" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "draft" + ], + "type": "object" + }, + "name": "update_pull_request_draft_state", + "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_state.snap b/pkg/github/__toolsnaps__/update_pull_request_state.snap index 440533e9b7..d9110c16d0 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_state.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_state.snap @@ -15,7 +15,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { @@ -23,11 +22,7 @@ "type": "string" }, "state": { - "description": "The new state for the pull request", - "enum": [ - "open", - "closed" - ], + "description": "The new state for the pull request (open or closed)", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/update_pull_request_state_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_state_typed.snap new file mode 100644 index 0000000000..a2373c02b5 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request_state_typed.snap @@ -0,0 +1,68 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Update Pull Request State" + }, + "description": "Update the state of an existing pull request (open or closed).", + "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": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "state": { + "description": "The new state for the pull request (open or closed)", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "state" + ], + "type": "object" + }, + "name": "update_pull_request_state", + "outputSchema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request_title.snap b/pkg/github/__toolsnaps__/update_pull_request_title.snap index 1ac5455f77..769df46855 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_title.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_title.snap @@ -15,7 +15,6 @@ }, "pullNumber": { "description": "The pull request number", - "minimum": 1, "type": "number" }, "repo": { diff --git a/pkg/github/__toolsnaps__/update_pull_request_title_typed.snap b/pkg/github/__toolsnaps__/update_pull_request_title_typed.snap new file mode 100644 index 0000000000..580cf9f7e1 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request_title_typed.snap @@ -0,0 +1,68 @@ +{ + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "Update Pull Request Title" + }, + "description": "Update the title of an existing pull request.", + "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": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string", + "x-mcp-header": "owner" + }, + "pullNumber": { + "description": "The pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "title": { + "description": "The new title for the pull request", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "title" + ], + "type": "object" + }, + "name": "update_pull_request_title", + "outputSchema": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "url" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/github/granular_pull_request_types.go b/pkg/github/granular_pull_request_types.go new file mode 100644 index 0000000000..9f493700c0 --- /dev/null +++ b/pkg/github/granular_pull_request_types.go @@ -0,0 +1,208 @@ +package github + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/github/github-mcp-server/pkg/inventory" +) + +type GranularPullRequestCoordinate struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + PullNumber int `json:"pullNumber"` +} + +func (in GranularPullRequestCoordinate) pullRequestCoordinate() GranularPullRequestCoordinate { + return in +} + +type GranularPullRequestTitleInput struct { + GranularPullRequestCoordinate + Title string `json:"title"` +} + +type GranularPullRequestBodyInput struct { + GranularPullRequestCoordinate + Body string `json:"body"` +} + +type GranularPullRequestStateInput struct { + GranularPullRequestCoordinate + State string `json:"state"` +} + +type GranularPullRequestDraftInput struct { + GranularPullRequestCoordinate + Draft bool `json:"draft"` +} + +type GranularPullRequestReviewersInput struct { + GranularPullRequestCoordinate + Reviewers []string `json:"reviewers"` +} + +type GranularCreatePullRequestReviewInput struct { + GranularPullRequestCoordinate + Body string `json:"body,omitempty"` + Event string `json:"event,omitempty"` + CommitID string `json:"commitID,omitempty"` +} + +type GranularSubmitPullRequestReviewInput struct { + GranularPullRequestCoordinate + Event string `json:"event"` + Body string `json:"body,omitempty"` +} + +type GranularReviewThreadInput struct { + ThreadID string `json:"threadID"` +} + +type GranularResolveReviewThreadInput struct { + GranularReviewThreadInput + ResolutionReason *string `json:"resolutionReason,omitempty"` +} + +type GranularAddPullRequestCommentReactionInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + CommentID int64 `json:"comment_id"` + Content string `json:"content"` +} + +type GranularRemovePullRequestCommentReactionInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + CommentID int64 `json:"comment_id"` + ReactionID int64 `json:"reaction_id"` +} + +// normalizeGranularPullRequestArguments replays the untyped parameter checks +// in their original order, including ignored optional-string type errors. +// Schema enums/minima are intentionally descriptive rather than restrictive: +// the legacy handlers left those checks to GitHub, including negative IDs. +func normalizeGranularPullRequestArguments(kind string) inventory.InputNormalizer { + return func(raw json.RawMessage) (json.RawMessage, error) { + if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return nil, &inventory.ToolInputError{Message: "invalid arguments: arguments must be a JSON object"} + } + var args map[string]any + if err := json.Unmarshal(raw, &args); err != nil { + return nil, &inventory.ToolInputError{Message: "invalid arguments: " + err.Error()} + } + + if err := normalizeGranularPullRequestFields(args, kind); err != nil { + return nil, &inventory.ToolInputError{Message: err.Error()} + } + return json.Marshal(args) + } +} + +func normalizeGranularResolveReviewThreadArguments(withResolutionReason bool) inventory.InputNormalizer { + if withResolutionReason { + return normalizeGranularPullRequestArguments("resolve_reason") + } + return normalizeGranularPullRequestArguments("resolve") +} + +func normalizeGranularPullRequestFields(args map[string]any, kind string) error { + requiredString := func(field string) error { + _, err := RequiredParam[string](args, field) + return err + } + ignoredString := func(field string) { + if _, ok := args[field].(string); !ok { + delete(args, field) + } + } + switch kind { + case "resolve", "resolve_reason", "unresolve": + if err := requiredString("threadID"); err != nil { + return err + } + if kind == "resolve_reason" { + _, _, err := OptionalParamOK[string](args, "resolutionReason") + return err + } + delete(args, "resolutionReason") + return nil + } + for _, field := range []string{"owner", "repo"} { + if err := requiredString(field); err != nil { + return err + } + } + if kind == "add_reaction" || kind == "remove_reaction" { + commentID, err := RequiredBigInt(args, "comment_id") + if err != nil { + return err + } + args["comment_id"] = commentID + if kind == "add_reaction" { + return requiredString("content") + } + reactionID, err := RequiredBigInt(args, "reaction_id") + if err != nil { + return err + } + args["reaction_id"] = reactionID + return nil + } + pullNumber, err := RequiredInt(args, "pullNumber") + if err != nil { + return err + } + args["pullNumber"] = pullNumber + switch kind { + case "title", "body", "state": + return requiredString(kind) + case "draft": + if _, ok := args["draft"]; !ok { + return fmt.Errorf("missing required parameter: draft") + } + _, err := OptionalParam[bool](args, "draft") + return err + case "reviewers": + reviewers, err := OptionalStringArrayParam(args, "reviewers") + if err != nil { + return err + } + if len(reviewers) == 0 { + return fmt.Errorf("missing required parameter: reviewers") + } + args["reviewers"] = reviewers + case "create_review": + for _, field := range []string{"body", "event", "commitID"} { + ignoredString(field) + } + case "submit_review": + if err := requiredString("event"); err != nil { + return err + } + ignoredString("body") + case "delete_review": + case "comment": + for _, field := range []string{"path", "body", "subjectType"} { + if err := requiredString(field); err != nil { + return err + } + } + line, err := OptionalIntParam(args, "line") + if err != nil { + return err + } + args["line"] = line + ignoredString("side") + startLine, err := OptionalIntParam(args, "startLine") + if err != nil { + return err + } + args["startLine"] = startLine + ignoredString("startSide") + default: + panic("unknown granular pull request argument kind: " + kind) + } + return nil +} diff --git a/pkg/github/pullrequests_granular.go b/pkg/github/pullrequests_granular.go index c1276a6b86..2f20c9b2de 100644 --- a/pkg/github/pullrequests_granular.go +++ b/pkg/github/pullrequests_granular.go @@ -19,12 +19,15 @@ import ( ) // prUpdateTool is a helper to create single-field pull request update tools via REST. -func prUpdateTool( +func prUpdateTool[In interface { + pullRequestCoordinate() GranularPullRequestCoordinate +}]( t translations.TranslationHelperFunc, name, description, title string, extraProps map[string]*jsonschema.Schema, extraRequired []string, - buildRequest func(args map[string]any) (*gogithub.PullRequest, error), + buildRequest func(args In) *gogithub.PullRequest, + normalizer inventory.InputNormalizer, ) inventory.ServerTool { props := map[string]*jsonschema.Schema{ "owner": { @@ -38,7 +41,6 @@ func prUpdateTool( "pullNumber": { Type: "number", Description: "The pull request number", - Minimum: new(1.0), }, } maps.Copy(props, extraProps) @@ -63,24 +65,10 @@ func prUpdateTool( }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - prReq, err := buildRequest(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args In) (*mcp.CallToolResult, *MinimalResponse, error) { + coordinate := args.pullRequestCoordinate() + owner, repo, pullNumber := coordinate.Owner, coordinate.Repo, coordinate.PullNumber + prReq := buildRequest(args) client, err := deps.GetClient(ctx) if err != nil { @@ -93,15 +81,17 @@ func prUpdateTool( } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(MinimalResponse{ + output := &MinimalResponse{ ID: fmt.Sprintf("%d", pr.GetID()), URL: pr.GetHTMLURL(), - }) + } + 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 }, + normalizer, ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -117,13 +107,10 @@ func GranularUpdatePullRequestTitle(t translations.TranslationHelperFunc) invent "title": {Type: "string", Description: "The new title for the pull request"}, }, []string{"title"}, - func(args map[string]any) (*gogithub.PullRequest, error) { - title, err := RequiredParam[string](args, "title") - if err != nil { - return nil, err - } - return &gogithub.PullRequest{Title: &title}, nil + func(args GranularPullRequestTitleInput) *gogithub.PullRequest { + return &gogithub.PullRequest{Title: &args.Title} }, + normalizeGranularPullRequestArguments("title"), ) } @@ -137,13 +124,10 @@ func GranularUpdatePullRequestBody(t translations.TranslationHelperFunc) invento "body": {Type: "string", Description: "The new body content for the pull request"}, }, []string{"body"}, - func(args map[string]any) (*gogithub.PullRequest, error) { - body, err := RequiredParam[string](args, "body") - if err != nil { - return nil, err - } - return &gogithub.PullRequest{Body: &body}, nil + func(args GranularPullRequestBodyInput) *gogithub.PullRequest { + return &gogithub.PullRequest{Body: &args.Body} }, + normalizeGranularPullRequestArguments("body"), ) } @@ -156,18 +140,14 @@ func GranularUpdatePullRequestState(t translations.TranslationHelperFunc) invent map[string]*jsonschema.Schema{ "state": { Type: "string", - Description: "The new state for the pull request", - Enum: []any{"open", "closed"}, + Description: "The new state for the pull request (open or closed)", }, }, []string{"state"}, - func(args map[string]any) (*gogithub.PullRequest, error) { - state, err := RequiredParam[string](args, "state") - if err != nil { - return nil, err - } - return &gogithub.PullRequest{State: &state}, nil + func(args GranularPullRequestStateInput) *gogithub.PullRequest { + return &gogithub.PullRequest{State: &args.State} }, + normalizeGranularPullRequestArguments("state"), ) } @@ -189,34 +169,15 @@ func GranularUpdatePullRequestDraftState(t translations.TranslationHelperFunc) i Properties: map[string]*jsonschema.Schema{ "owner": {Type: "string", Description: "Repository owner (username or organization)"}, "repo": {Type: "string", Description: "Repository name"}, - "pullNumber": {Type: "number", Description: "The pull request number", Minimum: new(1.0)}, + "pullNumber": {Type: "number", Description: "The pull request number"}, "draft": {Type: "boolean", Description: "Set to true to convert to draft, false to mark as ready for review"}, }, Required: []string{"owner", "repo", "pullNumber", "draft"}, }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - // Use presence check + OptionalParam since RequiredParam rejects false (zero-value for bool) - if _, ok := args["draft"]; !ok { - return utils.NewToolResultError("missing required parameter: draft"), nil, nil - } - draft, err := OptionalParam[bool](args, "draft") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularPullRequestDraftInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + owner, repo, pullNumber, draft := args.Owner, args.Repo, args.PullNumber, args.Draft gqlClient, err := deps.GetGQLClient(ctx) if err != nil { @@ -253,7 +214,7 @@ func GranularUpdatePullRequestDraftState(t translations.TranslationHelperFunc) i }, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to convert to draft", err), nil, nil } - return utils.NewToolResultText("pull request converted to draft"), nil, nil + return pullRequestMessageResult(utils.NewToolResultText("pull request converted to draft"), nil) } var mutation struct { @@ -269,8 +230,9 @@ func GranularUpdatePullRequestDraftState(t translations.TranslationHelperFunc) i }, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to mark ready for review", err), nil, nil } - return utils.NewToolResultText("pull request marked as ready for review"), nil, nil + return pullRequestMessageResult(utils.NewToolResultText("pull request marked as ready for review"), nil) }, + normalizeGranularPullRequestArguments("draft"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -294,7 +256,7 @@ func GranularRequestPullRequestReviewers(t translations.TranslationHelperFunc) i Properties: map[string]*jsonschema.Schema{ "owner": {Type: "string", Description: "Repository owner (username or organization)"}, "repo": {Type: "string", Description: "Repository name"}, - "pullNumber": {Type: "number", Description: "The pull request number", Minimum: new(1.0)}, + "pullNumber": {Type: "number", Description: "The pull request number"}, "reviewers": { Type: "array", Description: "GitHub usernames or ORG/team-slug team reviewers to request reviews from", @@ -305,27 +267,9 @@ func GranularRequestPullRequestReviewers(t translations.TranslationHelperFunc) i }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - reviewers, err := OptionalStringArrayParam(args, "reviewers") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - if len(reviewers) == 0 { - return utils.NewToolResultError("missing required parameter: reviewers"), nil, nil - } - userReviewers, teamReviewers := splitPullRequestReviewers(reviewers) + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularPullRequestReviewersInput) (*mcp.CallToolResult, *MinimalResponse, error) { + owner, repo, pullNumber := args.Owner, args.Repo, args.PullNumber + userReviewers, teamReviewers := splitPullRequestReviewers(args.Reviewers) client, err := deps.GetClient(ctx) if err != nil { @@ -341,15 +285,17 @@ func GranularRequestPullRequestReviewers(t translations.TranslationHelperFunc) i } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(MinimalResponse{ + output := &MinimalResponse{ ID: fmt.Sprintf("%d", pr.GetID()), URL: pr.GetHTMLURL(), - }) + } + 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 }, + normalizeGranularPullRequestArguments("reviewers"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -389,31 +335,18 @@ func GranularCreatePullRequestReview(t translations.TranslationHelperFunc) inven Properties: map[string]*jsonschema.Schema{ "owner": {Type: "string", Description: "Repository owner (username or organization)"}, "repo": {Type: "string", Description: "Repository name"}, - "pullNumber": {Type: "number", Description: "The pull request number", Minimum: new(1.0)}, + "pullNumber": {Type: "number", Description: "The pull request number"}, "body": {Type: "string", Description: "The review body text (optional)"}, - "event": {Type: "string", Description: "The review action to perform. If omitted, creates a pending review.", Enum: []any{"APPROVE", "REQUEST_CHANGES", "COMMENT"}}, + "event": {Type: "string", Description: "The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT). If omitted, creates a pending review."}, "commitID": {Type: "string", Description: "The SHA of the commit to review (optional, defaults to latest)"}, }, Required: []string{"owner", "repo", "pullNumber"}, }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - body, _ := OptionalParam[string](args, "body") - event, _ := OptionalParam[string](args, "event") - commitID, _ := OptionalParam[string](args, "commitID") + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularCreatePullRequestReviewInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + owner, repo, pullNumber := args.Owner, args.Repo, args.PullNumber + body, event, commitID := args.Body, args.Event, args.CommitID gqlClient, err := deps.GetGQLClient(ctx) if err != nil { @@ -433,8 +366,9 @@ func GranularCreatePullRequestReview(t translations.TranslationHelperFunc) inven Event: event, CommitID: commitIDPtr, }) - return result, nil, err + return pullRequestMessageResult(result, err) }, + normalizeGranularPullRequestArguments("create_review"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -458,32 +392,16 @@ func GranularSubmitPendingPullRequestReview(t translations.TranslationHelperFunc Properties: map[string]*jsonschema.Schema{ "owner": {Type: "string", Description: "Repository owner (username or organization)"}, "repo": {Type: "string", Description: "Repository name"}, - "pullNumber": {Type: "number", Description: "The pull request number", Minimum: new(1.0)}, - "event": {Type: "string", Description: "The review action to perform", Enum: []any{"APPROVE", "REQUEST_CHANGES", "COMMENT"}}, + "pullNumber": {Type: "number", Description: "The pull request number"}, + "event": {Type: "string", Description: "The review action to perform (APPROVE, REQUEST_CHANGES, or COMMENT)"}, "body": {Type: "string", Description: "The review body text (optional)"}, }, Required: []string{"owner", "repo", "pullNumber", "event"}, }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - event, err := RequiredParam[string](args, "event") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - body, _ := OptionalParam[string](args, "body") + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularSubmitPullRequestReviewInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + owner, repo, pullNumber, event, body := args.Owner, args.Repo, args.PullNumber, args.Event, args.Body gqlClient, err := deps.GetGQLClient(ctx) if err != nil { @@ -497,8 +415,9 @@ func GranularSubmitPendingPullRequestReview(t translations.TranslationHelperFunc Event: event, Body: body, }) - return result, nil, err + return pullRequestMessageResult(result, err) }, + normalizeGranularPullRequestArguments("submit_review"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -522,25 +441,14 @@ func GranularDeletePendingPullRequestReview(t translations.TranslationHelperFunc Properties: map[string]*jsonschema.Schema{ "owner": {Type: "string", Description: "Repository owner (username or organization)"}, "repo": {Type: "string", Description: "Repository name"}, - "pullNumber": {Type: "number", Description: "The pull request number", Minimum: new(1.0)}, + "pullNumber": {Type: "number", Description: "The pull request number"}, }, Required: []string{"owner", "repo", "pullNumber"}, }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularPullRequestCoordinate) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + owner, repo, pullNumber := args.Owner, args.Repo, args.PullNumber gqlClient, err := deps.GetGQLClient(ctx) if err != nil { @@ -552,8 +460,9 @@ func GranularDeletePendingPullRequestReview(t translations.TranslationHelperFunc Repo: repo, PullNumber: int32(pullNumber), // #nosec G115 - PR numbers are always small positive integers }) - return result, nil, err + return pullRequestMessageResult(result, err) }, + normalizeGranularPullRequestArguments("delete_review"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -577,54 +486,21 @@ func GranularAddPullRequestReviewComment(t translations.TranslationHelperFunc) i Properties: map[string]*jsonschema.Schema{ "owner": {Type: "string", Description: "Repository owner (username or organization)"}, "repo": {Type: "string", Description: "Repository name"}, - "pullNumber": {Type: "number", Description: "The pull request number", Minimum: new(1.0)}, + "pullNumber": {Type: "number", Description: "The pull request number"}, "path": {Type: "string", Description: "The relative path of the file to comment on"}, "body": {Type: "string", Description: "The comment body"}, - "subjectType": {Type: "string", Description: "The subject type of the comment", Enum: []any{"FILE", "LINE"}}, + "subjectType": {Type: "string", Description: "The subject type of the comment (FILE or LINE)"}, "line": {Type: "number", Description: "The line number in the diff to comment on (optional)"}, - "side": {Type: "string", Description: "The side of the diff to comment on (optional)", Enum: []any{"LEFT", "RIGHT"}}, + "side": {Type: "string", Description: "The side of the diff to comment on (LEFT or RIGHT, optional)"}, "startLine": {Type: "number", Description: "The start line of a multi-line comment (optional)"}, - "startSide": {Type: "string", Description: "The start side of a multi-line comment (optional)", Enum: []any{"LEFT", "RIGHT"}}, + "startSide": {Type: "string", Description: "The start side of a multi-line comment (LEFT or RIGHT, optional)"}, }, Required: []string{"owner", "repo", "pullNumber", "path", "body", "subjectType"}, }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pullNumber, err := RequiredInt(args, "pullNumber") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - path, err := RequiredParam[string](args, "path") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - body, err := RequiredParam[string](args, "body") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - subjectType, err := RequiredParam[string](args, "subjectType") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - line, err := OptionalIntParam(args, "line") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - side, _ := OptionalParam[string](args, "side") - startLine, err := OptionalIntParam(args, "startLine") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - startSide, _ := OptionalParam[string](args, "startSide") + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args AddCommentToPendingReviewInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + owner, repo, pullNumber := args.Owner, args.Repo, args.PullNumber gqlClient, err := deps.GetGQLClient(ctx) if err != nil { @@ -633,38 +509,39 @@ func GranularAddPullRequestReviewComment(t translations.TranslationHelperFunc) i // Convert optional int params to *int32 for the helper var linePtr, startLinePtr *int32 - if line != 0 { - l := int32(line) // #nosec G115 + if args.Line != nil && *args.Line != 0 { + l := int32(*args.Line) // #nosec G115 linePtr = &l } - if startLine != 0 { - sl := int32(startLine) // #nosec G115 + if args.StartLine != nil && *args.StartLine != 0 { + sl := int32(*args.StartLine) // #nosec G115 startLinePtr = &sl } // Convert optional string params: pass nil (not empty string) when absent var sidePtr, startSidePtr *string - if side != "" { - sidePtr = &side + if args.Side != nil && *args.Side != "" { + sidePtr = args.Side } - if startSide != "" { - startSidePtr = &startSide + if args.StartSide != nil && *args.StartSide != "" { + startSidePtr = args.StartSide } result, err := AddCommentToPendingReviewCall(ctx, gqlClient, AddCommentToPendingReviewParams{ Owner: owner, Repo: repo, PullNumber: int32(pullNumber), // #nosec G115 - PR numbers are always small positive integers - Path: path, - Body: body, - SubjectType: subjectType, + Path: args.Path, + Body: args.Body, + SubjectType: args.SubjectType, Line: linePtr, Side: sidePtr, StartLine: startLinePtr, StartSide: startSidePtr, }) - return result, nil, err + return pullRequestMessageResult(result, err) }, + normalizeGranularPullRequestArguments("comment"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -719,20 +596,11 @@ func granularResolveReviewThread(t translations.TranslationHelperFunc, withResol }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - threadID, err := RequiredParam[string](args, "threadID") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularResolveReviewThreadInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + threadID := args.ThreadID var resolutionReasonPtr *string if withResolutionReason { - resolutionReason, hasResolutionReason, err := OptionalParamOK[string](args, "resolutionReason") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - if hasResolutionReason { - resolutionReasonPtr = &resolutionReason - } + resolutionReasonPtr = args.ResolutionReason } gqlClient, err := deps.GetGQLClient(ctx) @@ -742,11 +610,12 @@ func granularResolveReviewThread(t translations.TranslationHelperFunc, withResol if !withResolutionReason { result, err := ResolveReviewThread(ctx, gqlClient, threadID, true) - return result, nil, err + return pullRequestMessageResult(result, err) } result, err := ResolveReviewThreadWithReason(ctx, gqlClient, threadID, resolutionReasonPtr, true) - return result, nil, err + return pullRequestMessageResult(result, err) }, + normalizeGranularResolveReviewThreadArguments(withResolutionReason), ) switch { case withResolutionReason: @@ -796,11 +665,8 @@ func GranularUnresolveReviewThread(t translations.TranslationHelperFunc) invento }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - threadID, err := RequiredParam[string](args, "threadID") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularReviewThreadInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + threadID := args.ThreadID gqlClient, err := deps.GetGQLClient(ctx) if err != nil { @@ -808,8 +674,9 @@ func GranularUnresolveReviewThread(t translations.TranslationHelperFunc) invento } result, err := ResolveReviewThread(ctx, gqlClient, threadID, false) - return result, nil, err + return pullRequestMessageResult(result, err) }, + normalizeGranularPullRequestArguments("unresolve"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -842,35 +709,18 @@ func GranularAddPullRequestReviewCommentReaction(t translations.TranslationHelpe "comment_id": { Type: "number", Description: "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", - Minimum: new(1.0), }, "content": { Type: "string", - Description: "The emoji reaction type", - Enum: []any{"+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"}, + Description: "The emoji reaction type (+1, -1, laugh, confused, heart, hooray, rocket, or eyes)", }, }, Required: []string{"owner", "repo", "comment_id", "content"}, }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - commentID, err := RequiredBigInt(args, "comment_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - content, err := RequiredParam[string](args, "content") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularAddPullRequestCommentReactionInput) (*mcp.CallToolResult, *MinimalResponse, error) { + owner, repo, commentID, content := args.Owner, args.Repo, args.CommentID, args.Content client, err := deps.GetClient(ctx) if err != nil { @@ -883,15 +733,17 @@ func GranularAddPullRequestReviewCommentReaction(t translations.TranslationHelpe } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(MinimalResponse{ + output := &MinimalResponse{ ID: fmt.Sprintf("%d", reaction.GetID()), URL: fmt.Sprintf("%srepos/%s/%s/pulls/comments/%d/reactions/%d", client.BaseURL(), owner, repo, commentID, reaction.GetID()), - }) + } + 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 }, + normalizeGranularPullRequestArguments("add_reaction"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st @@ -924,35 +776,18 @@ func GranularRemovePullRequestReviewCommentReaction(t translations.TranslationHe "comment_id": { Type: "number", Description: "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).", - Minimum: new(1.0), }, "reaction_id": { Type: "number", Description: "The reaction ID to remove", - Minimum: new(1.0), }, }, Required: []string{"owner", "repo", "comment_id", "reaction_id"}, }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - commentID, err := RequiredBigInt(args, "comment_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - reactionID, err := RequiredBigInt(args, "reaction_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args GranularRemovePullRequestCommentReactionInput) (*mcp.CallToolResult, *RepositoryMessageOutput, error) { + owner, repo, commentID, reactionID := args.Owner, args.Repo, args.CommentID, args.ReactionID client, err := deps.GetClient(ctx) if err != nil { @@ -967,8 +802,9 @@ func GranularRemovePullRequestReviewCommentReaction(t translations.TranslationHe return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to remove reaction from pull request review comment", resp, err), nil, nil } - return utils.NewToolResultText("reaction successfully removed from pull request review comment"), nil, nil + return pullRequestMessageResult(utils.NewToolResultText("reaction successfully removed from pull request review comment"), nil) }, + normalizeGranularPullRequestArguments("remove_reaction"), ) st.FeatureRule = pullRequestsGranularFeatureRule return st diff --git a/pkg/github/typed_granular_issue_outputs_test.go b/pkg/github/typed_granular_issue_outputs_test.go index ae57b95f9a..f389a9df44 100644 --- a/pkg/github/typed_granular_issue_outputs_test.go +++ b/pkg/github/typed_granular_issue_outputs_test.go @@ -322,6 +322,7 @@ func TestTypedGranularIssueWireErrors(t *testing.T) { {"add_issue_reaction", map[string]any{"content": "HEART"}, "heart"}, {"remove_issue_reaction", map[string]any{"reaction_id": "1.5"}, "non-integer"}, {"add_issue_comment_reaction", map[string]any{"comment_id": 42, "content": "invalid"}, "heart"}, + // Float-to-int overflow diagnostics differ across architectures. {"remove_issue_comment_reaction", map[string]any{"comment_id": "9223372036854775808", "reaction_id": 9}, "comment_id"}, } diff --git a/pkg/github/typed_granular_pull_request_outputs_test.go b/pkg/github/typed_granular_pull_request_outputs_test.go new file mode 100644 index 0000000000..ce1309e2c7 --- /dev/null +++ b/pkg/github/typed_granular_pull_request_outputs_test.go @@ -0,0 +1,464 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "net/http" + "strings" + "testing" + "time" + + "github.com/github/github-mcp-server/internal/toolsnaps" + "github.com/github/github-mcp-server/pkg/inventory" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/github/github-mcp-server/pkg/utils" + gogithub "github.com/google/go-github/v92/github" + "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" +) + +func granularPullRequestTools() []inventory.ServerTool { + t := translations.NullTranslationHelper + return []inventory.ServerTool{ + GranularUpdatePullRequestTitle(t), GranularUpdatePullRequestBody(t), + GranularUpdatePullRequestState(t), GranularUpdatePullRequestDraftState(t), + GranularRequestPullRequestReviewers(t), GranularCreatePullRequestReview(t), + GranularSubmitPendingPullRequestReview(t), GranularDeletePendingPullRequestReview(t), + GranularAddPullRequestReviewComment(t), GranularResolveReviewThread(t), + GranularUnresolveReviewThread(t), GranularAddPullRequestReviewCommentReaction(t), + GranularRemovePullRequestReviewCommentReaction(t), + GranularResolveReviewThreadWithResolutionReason(t), + } +} + +type granularPRCase struct { + name string + args map[string]any + text string + payload string +} + +const granularPRReference = `{"id":"123","url":"https://github.com/owner/repo/pull/1"}` + +func granularPRCases() []granularPRCase { + return []granularPRCase{ + {"update_pull_request_title", map[string]any{"title": "Changed"}, granularPRReference, `{"title":"Changed"}`}, + {"update_pull_request_body", map[string]any{"body": "Body"}, granularPRReference, `{"body":"Body"}`}, + {"update_pull_request_state", map[string]any{"state": "closed"}, granularPRReference, `{"state":"closed"}`}, + {"update_pull_request_state", map[string]any{"state": "other"}, granularPRReference, `{"state":"other"}`}, + {"update_pull_request_draft_state", map[string]any{"draft": true}, "pull request converted to draft", `"pullRequestId":"PR_1"`}, + {"update_pull_request_draft_state", map[string]any{"draft": false}, "pull request marked as ready for review", `"pullRequestId":"PR_1"`}, + {"request_pull_request_reviewers", map[string]any{"reviewers": []string{"octocat", "org/team", "/bad", "org/a/b"}}, granularPRReference, `{"reviewers":["octocat","/bad","org/a/b"],"team_reviewers":["team"]}`}, + {"create_pull_request_review", map[string]any{}, "pending pull request created", `"pullRequestId":"PR_1"`}, + {"create_pull_request_review", map[string]any{"event": "APPROVE", "body": "LGTM", "commitID": "abc"}, "pull request review submitted successfully", `"event":"APPROVE"`}, + {"create_pull_request_review", map[string]any{"event": "COMMENT", "body": "Note"}, "pull request review submitted successfully", `"body":"Note"`}, + {"create_pull_request_review", map[string]any{"event": "REQUEST_CHANGES"}, "pull request review submitted successfully", `"event":"REQUEST_CHANGES"`}, + {"create_pull_request_review", map[string]any{"event": 3, "body": false, "commitID": []any{}}, "pending pull request created", `"pullRequestId":"PR_1"`}, + {"create_pull_request_review", map[string]any{"event": "", "body": "ignored"}, "pending pull request created", `"pullRequestId":"PR_1"`}, + {"submit_pending_pull_request_review", map[string]any{"event": "APPROVE", "body": "LGTM"}, "pending pull request review successfully submitted", `"pullRequestReviewId":"R_1"`}, + {"submit_pending_pull_request_review", map[string]any{"event": "COMMENT", "body": false}, "pending pull request review successfully submitted", `"body":""`}, + {"submit_pending_pull_request_review", map[string]any{"event": "REQUEST_CHANGES"}, "pending pull request review successfully submitted", `"event":"REQUEST_CHANGES"`}, + {"delete_pending_pull_request_review", map[string]any{}, "pending pull request review successfully deleted", `"pullRequestReviewId":"R_1"`}, + {"add_pull_request_review_comment", map[string]any{"path": "file", "body": "nit", "subjectType": "LINE", "line": "3e0", "side": "RIGHT", "startLine": "2.0", "startSide": "LEFT"}, "pull request review comment successfully added to pending review", `"line":3`}, + {"add_pull_request_review_comment", map[string]any{"path": "file", "body": "nit", "subjectType": "FILE", "line": 0, "startLine": 0, "side": 3, "startSide": false}, "pull request review comment successfully added to pending review", `"subjectType":"FILE"`}, + {"resolve_review_thread", map[string]any{"threadID": "T_1"}, "review thread resolved successfully", `"threadId":"T_1"`}, + {"resolve_review_thread", map[string]any{"threadID": "T_1", "resolutionReason": ""}, "review thread resolved successfully", `"threadId":"T_1"`}, + {"unresolve_review_thread", map[string]any{"threadID": "T_1"}, "review thread unresolved successfully", `"threadId":"T_1"`}, + {"add_pull_request_review_comment_reaction", map[string]any{"comment_id": "42.0", "content": "heart"}, `{"id":"77","url":"https://api.github.com/repos/owner/repo/pulls/comments/42/reactions/77"}`, `{"content":"heart"}`}, + {"remove_pull_request_review_comment_reaction", map[string]any{"comment_id": "42", "reaction_id": "77e0"}, "reaction successfully removed from pull request review comment", ""}, + } +} + +// These expectations were run against the untyped handlers at the exact +// parent commit before migrating them. Keep equality, not substring checks. +func granularPRValidationCases() []granularPRCase { + return []granularPRCase{ + {"update_pull_request_title", map[string]any{"title": ""}, "missing required parameter: title", ""}, + {"update_pull_request_body", map[string]any{"body": nil}, "parameter body is not of type string", ""}, + {"update_pull_request_state", map[string]any{}, "missing required parameter: state", ""}, + {"update_pull_request_draft_state", map[string]any{}, "missing required parameter: draft", ""}, + {"update_pull_request_draft_state", map[string]any{"draft": nil}, "parameter draft is not of type bool, is ", ""}, + {"request_pull_request_reviewers", map[string]any{"reviewers": nil}, "missing required parameter: reviewers", ""}, + {"request_pull_request_reviewers", map[string]any{"reviewers": []any{"octocat", false}}, "parameter reviewers is not of type string, is bool", ""}, + {"create_pull_request_review", map[string]any{"pullNumber": 1.5}, "parameter pullNumber is not a valid number: non-integer numeric value: 1.5", ""}, + {"submit_pending_pull_request_review", map[string]any{"event": ""}, "missing required parameter: event", ""}, + {"submit_pending_pull_request_review", map[string]any{"event": false}, "parameter event is not of type string", ""}, + {"delete_pending_pull_request_review", map[string]any{"owner": ""}, "missing required parameter: owner", ""}, + {"add_pull_request_review_comment", map[string]any{"path": "file", "body": "", "subjectType": false}, "missing required parameter: body", ""}, + {"add_pull_request_review_comment", map[string]any{"path": "file", "body": "nit", "subjectType": "LINE", "line": false, "startLine": 1.5}, "parameter line is not a valid number: expected number, got bool", ""}, + {"add_pull_request_review_comment", map[string]any{"path": "file", "body": "nit", "subjectType": "LINE", "startLine": nil}, "parameter startLine is not a valid number: expected number, got ", ""}, + {"resolve_review_thread", map[string]any{"threadID": ""}, "missing required parameter: threadID", ""}, + {"unresolve_review_thread", map[string]any{"threadID": 3}, "parameter threadID is not of type string", ""}, + {"add_pull_request_review_comment_reaction", map[string]any{"comment_id": 0, "content": nil}, "missing required parameter: comment_id", ""}, + {"add_pull_request_review_comment_reaction", map[string]any{"comment_id": "42", "content": false}, "parameter content is not of type string", ""}, + {"remove_pull_request_review_comment_reaction", map[string]any{"comment_id": "42", "reaction_id": "bad"}, "parameter reaction_id is not a valid number: invalid numeric value: bad", ""}, + } +} + +func granularPRArgs(tc granularPRCase) map[string]any { + args := map[string]any{"owner": "owner", "repo": "repo", "pullNumber": "1.0"} + maps.Copy(args, tc.args) + return args +} + +func granularPRDeps(t *testing.T, current **granularPRCase, outcome string) BaseDeps { + t.Helper() + rest := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if outcome == "error" { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) + return + } + if current != nil && *current != nil && (*current).payload != "" { + var payload any + require.NoError(t, json.NewDecoder(r.Body).Decode(&payload)) + assert.JSONEq(t, (*current).payload, mustMarshalJSON(t, payload)) + } + path := r.URL.Path + switch { + case r.Method == http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + case outcome == "empty": + _, _ = w.Write([]byte(`null`)) + case strings.Contains(path, "/reactions"): + _, _ = w.Write([]byte(`{"id":77,"content":"heart"}`)) + default: + _, _ = w.Write([]byte(`{"id":123,"html_url":"https://github.com/owner/repo/pull/1"}`)) + } + })}} + gql := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Query string `json:"query"` + Variables json.RawMessage `json:"variables"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + if outcome == "error" || outcome == "mutation_error" && strings.HasPrefix(req.Query, "mutation") { + _, _ = w.Write([]byte(`{"errors":[{"message":"Forbidden"}]}`)) + return + } + if strings.HasPrefix(req.Query, "mutation") && current != nil && *current != nil { + assert.Contains(t, string(req.Variables), (*current).payload) + } + var body string + switch q := req.Query; { + case strings.Contains(q, "convertPullRequestToDraft("): + body = `{"data":{"convertPullRequestToDraft":{"pullRequest":{"id":"PR_1","isDraft":true}}}}` + case strings.Contains(q, "markPullRequestReadyForReview("): + body = `{"data":{"markPullRequestReadyForReview":{"pullRequest":{"id":"PR_1","isDraft":false}}}}` + 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, "reviews(first: 100"): + body = `{"data":{"repository":{"pullRequest":{"reviews":{"nodes":[{"id":"R_1","author":{"userId":"U_1"}}],"pageInfo":{"hasNextPage":false,"endCursor":""}}}}}}` + if outcome == "no_pending" { + body = `{"data":{"repository":{"pullRequest":{"reviews":{"nodes":[],"pageInfo":{"hasNextPage":false,"endCursor":""}}}}}}` + } + case strings.Contains(q, "viewer{"): + body = `{"data":{"viewer":{"id":"U_1"}}}` + default: + require.Contains(t, q, "pullRequest(number:") + body = `{"data":{"repository":{"pullRequest":{"id":"PR_1"}}}}` + } + if outcome == "empty" { + body = `{"data":{}}` + } + _, _ = w.Write([]byte(body)) + })}} + return BaseDeps{ + Client: mustNewGHClient(t, rest), GQLClient: githubv4.NewClient(gql), + RepoAccessCache: stubRepoAccessCache(nil, time.Minute), + } +} + +func granularPRSession(t *testing.T, deps ToolDependencies, protocol string, reason bool) (*mcp.ClientSession, map[string]*jsonschema.Resolved) { + t.Helper() + tools := granularPullRequestTools() + flags := []inventory.FeatureFlag{inventory.FeatureFlag(FeatureFlagPullRequestsGranular)} + if reason { + flags = append(flags, FeatureFlagThreadResolutionReason) + } + inv, err := inventory.NewBuilder().SetTools(tools).WithToolsets([]string{"all"}). + WithFeatureChecker(featureCheckerFor(flags...)).Build() + require.NoError(t, err) + server := mcp.NewServer(&mcp.Implementation{Name: "granular-pr", Version: "v1"}, nil) + server.AddReceivingMiddleware(InjectDepsMiddleware(deps)) + inv.RegisterTools(context.Background(), server, deps) + if protocol == "" { + server.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) { + switch req := req.(type) { + case *mcp.ListToolsRequest: + req.Params.Meta = mcp.Meta{mcp.MetaKeyProtocolVersion: ""} + case *mcp.CallToolRequest: + req.Params.Meta = mcp.Meta{mcp.MetaKeyProtocolVersion: ""} + } + return next(ctx, method, req) + } + }) + } + version := protocol + if version == "" { + version = inventory.ProtocolVersionMultiRoundTrip + } + session := connectCommentVisibilityClient(t, server, version) + list, err := session.ListTools(context.Background(), nil) + require.NoError(t, err) + require.Len(t, list.Tools, 13) + schemas := make(map[string]*jsonschema.Resolved) + for _, tool := range list.Tools { + name := tool.Name + if name == "resolve_review_thread" && reason { + name += "_resolution_reason" + } + // Resolve the advertised input schema as well as every modern output. + var inputSchema jsonschema.Schema + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, tool.InputSchema)), &inputSchema)) + _, err := inputSchema.Resolve(nil) + require.NoError(t, err, name) + if protocol != inventory.ProtocolVersionMultiRoundTrip { + assert.Nil(t, tool.OutputSchema, name) + continue + } + require.NotNil(t, tool.OutputSchema, name) + 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, name) + schemas[tool.Name] = resolved + } + return session, schemas +} + +func assertGranularPRResult(t *testing.T, result *mcp.CallToolResult, schema *jsonschema.Resolved, text string, isError bool) { + t.Helper() + require.Equal(t, isError, result.IsError, mustMarshalJSON(t, result)) + require.Len(t, result.Content, 1) + require.Equal(t, text, result.Content[0].(*mcp.TextContent).Text) + if schema == nil || isError { + require.Nil(t, result.StructuredContent) + return + } + require.NotNil(t, result.StructuredContent) + var output any + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, result.StructuredContent)), &output)) + require.NoError(t, schema.Validate(output)) + if strings.HasPrefix(text, "{") { + assert.JSONEq(t, text, mustMarshalJSON(t, output)) + } else { + assert.JSONEq(t, mustMarshalJSON(t, RepositoryMessageOutput{Message: text}), mustMarshalJSON(t, output)) + } + for _, raw := range []string{`{}`, `[]`, `{"id":1,"url":false}`, `{"message":1}`} { + var invalid any + require.NoError(t, json.Unmarshal([]byte(raw), &invalid)) + require.Error(t, schema.Validate(invalid), raw) + } +} + +func TestTypedGranularPullRequestWireOutputs(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + for _, reason := range []bool{false, true} { + t.Run("protocol="+protocol+"/reason="+mustMarshalJSON(t, reason), func(t *testing.T) { + var current *granularPRCase + deps := granularPRDeps(t, ¤t, "") + session, schemas := granularPRSession(t, deps, protocol, reason) + for _, tc := range granularPRCases() { + t.Run(tc.name+"/"+tc.text+"/"+mustMarshalJSON(t, tc.args), func(t *testing.T) { + current = &tc + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: granularPRArgs(tc)}) + require.NoError(t, err) + assertGranularPRResult(t, result, schemas[tc.name], tc.text, false) + }) + } + for _, value := range []any{"addressed", "wont-fix", "invalid", "", nil, false} { + tc := granularPRCase{name: "resolve_review_thread", args: map[string]any{"threadID": "T_1", "resolutionReason": value}, text: "review thread resolved successfully"} + current = &tc + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: tc.args}) + require.NoError(t, err) + if reason && (value == nil || value == false) { + typeName := "" + if value == false { + typeName = "bool" + } + assertGranularPRResult(t, result, schemas[tc.name], "parameter resolutionReason is not of type string, is "+typeName, true) + } else { + assertGranularPRResult(t, result, schemas[tc.name], tc.text, false) + } + } + // Legacy handlers sent negative numbers and non-enum strings to GitHub. + for _, tc := range []granularPRCase{ + {"update_pull_request_title", map[string]any{"pullNumber": -1, "title": "Title"}, granularPRReference, `{"title":"Title"}`}, + {"create_pull_request_review", map[string]any{"event": "other"}, "pull request review submitted successfully", `"event":"other"`}, + {"add_pull_request_review_comment", map[string]any{"path": "f", "body": "b", "subjectType": "other", "side": "other", "line": -1}, "pull request review comment successfully added to pending review", `"line":-1`}, + } { + current = &tc + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: granularPRArgs(tc)}) + require.NoError(t, err) + assertGranularPRResult(t, result, schemas[tc.name], tc.text, false) + } + }) + } + } +} + +func TestTypedGranularPullRequestErrorsAndEmptyResults(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + for _, reason := range []bool{false, true} { + t.Run("protocol="+protocol+"/reason="+mustMarshalJSON(t, reason), func(t *testing.T) { + session, schemas := granularPRSession(t, granularPRDeps(t, nil, "error"), protocol, reason) + for _, tc := range granularPRValidationCases() { + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: granularPRArgs(tc)}) + require.NoError(t, err) + assertGranularPRResult(t, result, schemas[tc.name], tc.text, true) + } + for _, tc := range granularPRCases() { + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: granularPRArgs(tc)}) + require.NoError(t, err) + require.True(t, result.IsError, tc.name) + require.Len(t, result.Content, 1) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "Forbidden") + assert.Nil(t, result.StructuredContent) + } + session, _ = granularPRSession(t, granularPRDeps(t, nil, "mutation_error"), protocol, reason) + for _, tc := range granularPRCases() { + if strings.HasPrefix(tc.text, "{") || tc.name == "remove_pull_request_review_comment_reaction" { + continue + } + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: granularPRArgs(tc)}) + require.NoError(t, err) + require.True(t, result.IsError, tc.name) + require.Len(t, result.Content, 1) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "Forbidden") + assert.Nil(t, result.StructuredContent) + } + session, schemas = granularPRSession(t, granularPRDeps(t, nil, "empty"), protocol, reason) + for _, tc := range granularPRCases() { + text := tc.text + isError := false + switch tc.name { + case "update_pull_request_title", "update_pull_request_body", "update_pull_request_state", "request_pull_request_reviewers": + text = `{"id":"0","url":""}` + case "add_pull_request_review_comment_reaction": + text = `{"id":"0","url":"https://api.github.com/repos/owner/repo/pulls/comments/42/reactions/0"}` + case "submit_pending_pull_request_review", "delete_pending_pull_request_review", "add_pull_request_review_comment": + text, isError = "No pending review found for the viewer", true + } + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: granularPRArgs(tc)}) + require.NoError(t, err) + assertGranularPRResult(t, result, schemas[tc.name], text, isError) + } + }) + } + } +} + +func TestGranularPullRequestFeatureVariants(t *testing.T) { + for _, tool := range granularPullRequestTools() { + assert.False(t, tool.IsReadOnly(), tool.Tool.Name) + assert.Equal(t, []string{"repo"}, tool.ScopeAccess.Scopes, tool.Tool.Name) + assert.False(t, tool.FeatureRule.Enabled(func(inventory.FeatureFlag) bool { return false }), tool.Tool.Name) + } + variant := GranularResolveReviewThreadWithResolutionReason(translations.NullTranslationHelper, WithHost(utils.HostTypeGHES)) + require.NotNil(t, variant.Enabled) + enabled, err := variant.Enabled(context.Background()) + require.NoError(t, err) + assert.False(t, enabled) +} + +func TestGranularPullRequestParentBehavior(t *testing.T) { + tools := granularPullRequestTools() + byName := make(map[string]inventory.ServerTool) + for _, tool := range tools[:len(tools)-1] { + byName[tool.Tool.Name] = tool + } + var current *granularPRCase + deps := granularPRDeps(t, ¤t, "") + ctx := ContextWithDeps(context.Background(), deps) + for _, tc := range append(granularPRCases(), granularPRValidationCases()...) { + t.Run(tc.name+"/"+tc.text, func(t *testing.T) { + current = &tc + tool := byName[tc.name] + result, err := tool.Handler(deps)(ctx, &mcp.CallToolRequest{Params: &mcp.CallToolParamsRaw{ + Name: tc.name, Arguments: json.RawMessage(mustMarshalJSON(t, granularPRArgs(tc))), + }}) + require.NoError(t, err) + require.Len(t, result.Content, 1) + assert.Equal(t, tc.text, result.Content[0].(*mcp.TextContent).Text) + }) + } +} + +type granularPRClientErrorDeps struct{ BaseDeps } + +func (granularPRClientErrorDeps) GetClient(context.Context) (*gogithub.Client, error) { + return nil, fmt.Errorf("client unavailable") +} + +func (granularPRClientErrorDeps) GetGQLClient(context.Context) (*githubv4.Client, error) { + return nil, fmt.Errorf("client unavailable") +} + +func TestTypedGranularPullRequestClientErrors(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + for _, reason := range []bool{false, true} { + session, schemas := granularPRSession(t, granularPRClientErrorDeps{}, protocol, reason) + for _, tc := range granularPRCases() { + message := "failed to get GitHub GraphQL client: client unavailable" + if strings.HasPrefix(tc.text, "{") || tc.name == "remove_pull_request_review_comment_reaction" { + message = "failed to get GitHub client: client unavailable" + } + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: granularPRArgs(tc)}) + require.NoError(t, err) + assertGranularPRResult(t, result, schemas[tc.name], message, true) + } + } + } +} + +func TestTypedGranularPullRequestResolutionReasonPayload(t *testing.T) { + for _, protocol := range typedPullRequestProtocols { + for _, reason := range []bool{false, true} { + for _, value := range []string{"addressed", "wont-fix", "invalid", ""} { + client := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request struct { + Variables struct { + Input struct { + ThreadID string `json:"threadId"` + ResolutionReason *string `json:"resolutionReason"` + } `json:"input"` + } `json:"variables"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&request)) + assert.Equal(t, "T_1", request.Variables.Input.ThreadID) + if reason { + require.NotNil(t, request.Variables.Input.ResolutionReason) + assert.Equal(t, value, *request.Variables.Input.ResolutionReason) + } else { + assert.Nil(t, request.Variables.Input.ResolutionReason) + } + _, _ = w.Write([]byte(`{"data":{"resolveReviewThread":{"thread":{"id":"T_1","isResolved":true}}}}`)) + })}} + session, schemas := granularPRSession(t, BaseDeps{GQLClient: githubv4.NewClient(client)}, protocol, reason) + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: "resolve_review_thread", Arguments: map[string]any{"threadID": "T_1", "resolutionReason": value}, + }) + require.NoError(t, err) + assertGranularPRResult(t, result, schemas["resolve_review_thread"], "review thread resolved successfully", false) + } + } + } +} diff --git a/pkg/inventory/input_error.go b/pkg/inventory/input_error.go new file mode 100644 index 0000000000..95965d282d --- /dev/null +++ b/pkg/inventory/input_error.go @@ -0,0 +1,9 @@ +package inventory + +// ToolInputError preserves a migrated handler's user-facing validation message +// when its checks move into an InputNormalizer. +type ToolInputError struct { + Message string +} + +func (err *ToolInputError) Error() string { return err.Message } diff --git a/pkg/inventory/input_error_test.go b/pkg/inventory/input_error_test.go new file mode 100644 index 0000000000..3300fb89ae --- /dev/null +++ b/pkg/inventory/input_error_test.go @@ -0,0 +1,19 @@ +package inventory + +import ( + "fmt" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/assert" +) + +func TestToolInputErrorPreservesText(t *testing.T) { + result := invalidArgumentsResult(fmt.Errorf("normalize tool arguments: %w", &ToolInputError{Message: "missing required parameter: title"})) + assert.True(t, result.IsError) + assert.Equal(t, "missing required parameter: title", result.Content[0].(*mcp.TextContent).Text) + assert.Nil(t, result.StructuredContent) + + result = invalidArgumentsResult(fmt.Errorf("ordinary error")) + assert.Equal(t, "invalid arguments: ordinary error", result.Content[0].(*mcp.TextContent).Text) +} diff --git a/pkg/inventory/server_tool.go b/pkg/inventory/server_tool.go index 473e80320b..ba028a6d95 100644 --- a/pkg/inventory/server_tool.go +++ b/pkg/inventory/server_tool.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "maps" "reflect" @@ -379,6 +380,12 @@ func combineInputNormalizers(normalizers []InputNormalizer) InputNormalizer { } func invalidArgumentsResult(err error) *mcp.CallToolResult { + if inputError, ok := errors.AsType[*ToolInputError](err); ok { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: inputError.Message}}, + IsError: true, + } + } return &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.TextContent{Text: fmt.Sprintf("invalid arguments: %s", err)}, 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))