From 853323d74fa41a913079b36ffe38d7314b17f9ab Mon Sep 17 00:00:00 2001 From: Dimitrios Philliou Date: Thu, 12 Jun 2025 12:17:11 -0700 Subject: [PATCH 01/19] Update README.md to prompt VS Code version update (#509) Making it clear that you need the latest version of VS Code installed for it work. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73e46cb6..003164e0 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The remote GitHub MCP Server is hosted by GitHub and provides the easiest method ### Usage with VS Code -For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. +For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using the [latest version of VS Code](https://code.visualstudio.com/updates/v1_101) for remote MCP and OAuth support. ### Usage in other MCP Hosts From dc94eaa4f0df8a3eb8912eabcea29d5467e84f09 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Fri, 13 Jun 2025 12:22:30 +0200 Subject: [PATCH 02/19] point to general updates page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 003164e0..a1521208 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The remote GitHub MCP Server is hosted by GitHub and provides the easiest method ### Usage with VS Code -For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using the [latest version of VS Code](https://code.visualstudio.com/updates/v1_101) for remote MCP and OAuth support. +For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using the [latest version of VS Code](https://code.visualstudio.com/updates/) for remote MCP and OAuth support. ### Usage in other MCP Hosts From 5e80be805b62db0d2fbab85c4a4c387cffc7992c Mon Sep 17 00:00:00 2001 From: tonytrg Date: Fri, 13 Jun 2025 12:47:08 +0200 Subject: [PATCH 03/19] add clarification --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1521208..b0c6de69 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The remote GitHub MCP Server is hosted by GitHub and provides the easiest method ### Usage with VS Code -For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using the [latest version of VS Code](https://code.visualstudio.com/updates/) for remote MCP and OAuth support. +For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using [VS Code 1.101](https://code.visualstudio.com/updates/v1_101) or [later](https://code.visualstudio.com/updates) for remote MCP and OAuth support. ### Usage in other MCP Hosts From 8562b1d6c364c60dbcb5524c363c36d3558199f7 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Fri, 13 Jun 2025 15:58:06 +0200 Subject: [PATCH 04/19] point to remote config docs (#513) * point to remote config docs * adding clarification and pointing to remote config * Update README.md Co-authored-by: John Wesley Walker III <81404201+jww3@users.noreply.github.com> --------- Co-authored-by: John Wesley Walker III <81404201+jww3@users.noreply.github.com> --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index b0c6de69..a8d5b255 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ For MCP Hosts that have been [configured to use the remote GitHub MCP Server](do > **Note:** The exact configuration format may vary by host. Refer to your host's documentation for the correct syntax and location for remote MCP server setup. +### Configuration + +See [Remote Server Documentation](docs/remote-server.md) on how to pass configuration settings to the remote GitHub MCP Server. + --- ## Local GitHub MCP Server From e9926b915345be1eacc60b7a2b3c6ff81d333823 Mon Sep 17 00:00:00 2001 From: John Wesley Walker III <81404201+jww3@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:47:47 +0200 Subject: [PATCH 05/19] Add a Remote MCP configuration example that employs a PAT (#514) --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a8d5b255..d40d8aab 100644 --- a/README.md +++ b/README.md @@ -28,28 +28,97 @@ The remote GitHub MCP Server is hosted by GitHub and provides the easiest method For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using [VS Code 1.101](https://code.visualstudio.com/updates/v1_101) or [later](https://code.visualstudio.com/updates) for remote MCP and OAuth support. -### Usage in other MCP Hosts -For MCP Hosts that have been [configured to use the remote GitHub MCP Server](docs/host-integration.md), add the following JSON block to the host's configuration: +Alternatively, to manually configure VS Code, choose the appropriate JSON block from the examples below and add it to your host configuration: + + + + + + + +
Using OAuthUsing a GitHub PAT
VS Code (version 1.101 or greater)
+ ```json { - "mcp": { - "servers": { - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/" + "servers": { + "github-remote": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/" + } + } +} +``` + + + +```json +{ + "servers": { + "github-remote": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer ${input:github_mcp_pat}", } } + }, + "inputs": [ + { + "type": "promptString", + "id": "github_mcp_pat", + "description": "GitHub Personal Access Token", + "password": true + } + ] +} +``` + +
+ +### Usage in other MCP Hosts + +For MCP Hosts that are [Remote MCP-compatible](docs/host-integration.md), choose the appropriate JSON block from the examples below and add it to your host configuration: + + + + + + + +
Using OAuthUsing a GitHub PAT
+ +```json +{ + "mcpServers": { + "github-remote": { + "url": "https://api.githubcopilot.com/mcp/" + } } } ``` + + +```json +{ + "mcpServers": { + "github-remote": { + "url": "https://api.githubcopilot.com/mcp/", + "authorization_token": "Bearer " + } + } +} +``` + +
+ > **Note:** The exact configuration format may vary by host. Refer to your host's documentation for the correct syntax and location for remote MCP server setup. ### Configuration -See [Remote Server Documentation](docs/remote-server.md) on how to pass configuration settings to the remote GitHub MCP Server. +See [Remote Server Documentation](docs/remote-server.md) on how to pass additional configuration settings to the remote GitHub MCP Server. --- From cdab1310a0a9f459c28c91e80bbfbaf6b5215039 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Mon, 16 Jun 2025 09:24:52 +0200 Subject: [PATCH 06/19] Add missing tool descriptions (#515) --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d40d8aab..216e0a61 100644 --- a/README.md +++ b/README.md @@ -491,6 +491,14 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `page`: Page number (number, optional) - `perPage`: Results per page (number, optional) +- **assign_copilot_to_issue** - Assign Copilot to a specific issue in a GitHub repository + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `issueNumber`: Issue number (number, required) + - _Note_: This tool can help with creating a Pull Request with source code changes to resolve the issue. More information can be found at [GitHub Copilot documentation](https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot) + + ### Pull Requests - **get_pull_request** - Get details of a specific pull request @@ -549,6 +557,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `repo`: Repository name (string, required) - `pullNumber`: Pull request number (number, required) +- **get_pull_request_diff** - Get the diff of a pull request + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pullNumber`: Pull request number (number, required) + - **create_pull_request_review** - Create a review on a pull request review - `owner`: Repository owner (string, required) @@ -561,6 +575,53 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - For inline comments: provide `path`, `position` (or `line`), and `body` - For multi-line comments: provide `path`, `start_line`, `line`, optional `side`/`start_side`, and `body` +- **create_pending_pull_request_review** - Create a pending review for a pull request that can be submitted later + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pullNumber`: Pull request number (number, required) + - `commitID`: SHA of commit to review (string, optional) + +- **add_pull_request_review_comment_to_pending_review** - Add a comment to the requester's latest pending pull request review + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pullNumber`: Pull request number (number, required) + - `path`: The relative path to the file that necessitates a comment (string, required) + - `body`: The text of the review comment (string, required) + - `subjectType`: The level at which the comment is targeted (string, required) + - Enum: "FILE", "LINE" + - `line`: The line of the blob in the pull request diff that the comment applies to (number, optional) + - `side`: The side of the diff to comment on (string, optional) + - Enum: "LEFT", "RIGHT" + - `startLine`: For multi-line comments, the first line of the range (number, optional) + - `startSide`: For multi-line comments, the starting side of the diff (string, optional) + - Enum: "LEFT", "RIGHT" + +- **submit_pending_pull_request_review** - Submit the requester's latest pending pull request review + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pullNumber`: Pull request number (number, required) + - `event`: The event to perform (string, required) + - Enum: "APPROVE", "REQUEST_CHANGES", "COMMENT" + - `body`: The text of the review comment (string, optional) + +- **delete_pending_pull_request_review** - Delete the requester's latest pending pull request review + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pullNumber`: Pull request number (number, required) + +- **create_and_submit_pull_request_review** - Create and submit a review for a pull request without review comments + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pullNumber`: Pull request number (number, required) + - `body`: Review comment text (string, required) + - `event`: Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT') (string, required) + - `commitID`: SHA of commit to review (string, optional) + - **create_pull_request** - Create a new pull request - `owner`: Repository owner (string, required) @@ -616,6 +677,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `branch`: Branch name (string, optional) - `sha`: File SHA if updating (string, optional) +- **delete_file** - Delete a file from a GitHub repository + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `path`: Path to the file to delete (string, required) + - `message`: Commit message (string, required) + - `branch`: Branch to delete the file from (string, required) + - **list_branches** - List branches in a GitHub repository - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) @@ -674,6 +742,17 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `page`: Page number, for files in the commit (number, optional) - `perPage`: Results per page, for files in the commit (number, optional) +- **get_tag** - Get details about a specific git tag in a GitHub repository + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `tag`: Tag name (string, required) + +- **list_tags** - List git tags in a GitHub repository + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `page`: Page number (number, optional) + - `perPage`: Results per page (number, optional) + - **search_code** - Search for code across GitHub repositories - `query`: Search query (string, required) - `sort`: Sort field (string, optional) @@ -732,7 +811,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `page`: Page number (number, optional) - `perPage`: Results per page (number, optional) - - **get_notification_details** – Get detailed information for a specific GitHub notification - `notificationID`: The ID of the notification (string, required) From 5502afa6f9c14f3ccc79fda902737b9a9b01b568 Mon Sep 17 00:00:00 2001 From: PierreGode Date: Tue, 17 Jun 2025 09:10:34 +0200 Subject: [PATCH 07/19] Update README.md Fix Uncaught Exception issue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 216e0a61..a5c1e313 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Alternatively, to manually configure VS Code, choose the appropriate JSON block "type": "http", "url": "https://api.githubcopilot.com/mcp/", "headers": { - "Authorization": "Bearer ${input:github_mcp_pat}", + "Authorization": "Bearer ${input:github_mcp_pat}" } } }, From bb24ec0fc38983321e7492798cab37cdfa77da4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:26:42 +0200 Subject: [PATCH 08/19] build(deps): bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 (#529) * build(deps): bump github.com/go-viper/mapstructure/v2 Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.2.1 to 2.3.0. - [Release notes](https://github.com/go-viper/mapstructure/releases) - [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-viper/mapstructure/compare/v2.2.1...v2.3.0) --- updated-dependencies: - dependency-name: github.com/go-viper/mapstructure/v2 dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * adding licenses --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tony Truong --- go.mod | 2 +- go.sum | 4 ++-- third-party-licenses.darwin.md | 2 +- third-party-licenses.linux.md | 2 +- third-party-licenses.windows.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index ab2302ed..d2f28d7d 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 + github.com/go-viper/mapstructure/v2 v2.3.0 github.com/google/go-github/v71 v71.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index e7f6794a..a8a950e9 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 7ba187e1..e182c63c 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -11,7 +11,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE)) - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE)) - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) + - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE)) - [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.1.0/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 7ba187e1..e182c63c 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -11,7 +11,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE)) - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE)) - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) + - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE)) - [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.1.0/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 1c8b6c58..d8bfd492 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -11,7 +11,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE)) - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE)) - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) + - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE)) - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE)) - [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.1.0/LICENSE)) From 41d12695ed69a2b84ef581a0503c8b8cb0d96dd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:26:59 +0200 Subject: [PATCH 09/19] build(deps): bump golang from 1.24.3-alpine to 1.24.4-alpine (#496) Bumps golang from 1.24.3-alpine to 1.24.4-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.24.4-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tony Truong --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1281db4c..a26f19a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24.3-alpine AS build +FROM golang:1.24.4-alpine AS build ARG VERSION="dev" # Set the working directory From 82fe310ccadbaa9982e28d71ed8970b8e5619a61 Mon Sep 17 00:00:00 2001 From: dgiacomo Date: Tue, 17 Jun 2025 16:26:52 -0700 Subject: [PATCH 10/19] Change reference from Anthropic to Github in README.md Noticed README for mcpurl referenced Anthropic's MCP Server when I think intent is to reference Github's - probably some copy pasta origin --- cmd/mcpcurl/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/mcpcurl/README.md b/cmd/mcpcurl/README.md index 493ce5b1..317c2b8e 100644 --- a/cmd/mcpcurl/README.md +++ b/cmd/mcpcurl/README.md @@ -31,7 +31,7 @@ The `--stdio-server-cmd` flag is required for all commands and specifies the com ### Examples -List available tools in Anthropic's MCP server: +List available tools in Github's MCP server: ```console % ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools --help From 1ddb78d0e3f0fc64899ac9be502c396777947ccb Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Wed, 18 Jun 2025 09:36:36 +0200 Subject: [PATCH 11/19] chore: fix e2e tests (#536) Co-authored-by: Sam Morrow --- e2e/e2e_test.go | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index e25dbda4..bc5a3fde 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -4,7 +4,6 @@ package e2e_test import ( "context" - "encoding/base64" "encoding/json" "fmt" "net/http" @@ -508,17 +507,14 @@ func TestFileDeletion(t *testing.T) { require.NoError(t, err, "expected to call 'get_file_contents' tool successfully") require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp)) - textContent, ok = resp.Content[0].(mcp.TextContent) - require.True(t, ok, "expected content to be of type TextContent") + embeddedResource, ok := resp.Content[1].(mcp.EmbeddedResource) + require.True(t, ok, "expected content to be of type EmbeddedResource") - var trimmedGetFileText struct { - Content string `json:"content"` - } - err = json.Unmarshal([]byte(textContent.Text), &trimmedGetFileText) - require.NoError(t, err, "expected to unmarshal text content successfully") - b, err := base64.StdEncoding.DecodeString(trimmedGetFileText.Content) - require.NoError(t, err, "expected to decode base64 content successfully") - require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), string(b), "expected file content to match") + // raw api + textResource, ok := embeddedResource.Resource.(mcp.TextResourceContents) + require.True(t, ok, "expected embedded resource to be of type TextResourceContents") + + require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match") // Delete the file deleteFileRequest := mcp.CallToolRequest{} @@ -703,17 +699,14 @@ func TestDirectoryDeletion(t *testing.T) { require.NoError(t, err, "expected to call 'get_file_contents' tool successfully") require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp)) - textContent, ok = resp.Content[0].(mcp.TextContent) - require.True(t, ok, "expected content to be of type TextContent") + embeddedResource, ok := resp.Content[1].(mcp.EmbeddedResource) + require.True(t, ok, "expected content to be of type EmbeddedResource") - var trimmedGetFileText struct { - Content string `json:"content"` - } - err = json.Unmarshal([]byte(textContent.Text), &trimmedGetFileText) - require.NoError(t, err, "expected to unmarshal text content successfully") - b, err := base64.StdEncoding.DecodeString(trimmedGetFileText.Content) - require.NoError(t, err, "expected to decode base64 content successfully") - require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), string(b), "expected file content to match") + // raw api + textResource, ok := embeddedResource.Resource.(mcp.TextResourceContents) + require.True(t, ok, "expected embedded resource to be of type TextResourceContents") + + require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match") // Delete the directory containing the file deleteFileRequest := mcp.CallToolRequest{} From 3e988d5ab530cd65f038cba0e4cdcc3abfc11196 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 18 Jun 2025 10:18:47 +0200 Subject: [PATCH 12/19] Add toolsnaps for every tool --- .../__toolsnaps__/add_issue_comment.snap | 35 +++++++++ ...uest_review_comment_to_pending_review.snap | 73 +++++++++++++++++++ .../assign_copilot_to_issue.snap | 31 ++++++++ ...create_and_submit_pull_request_review.snap | 49 +++++++++++++ pkg/github/__toolsnaps__/create_branch.snap | 34 +++++++++ pkg/github/__toolsnaps__/create_issue.snap | 52 +++++++++++++ .../__toolsnaps__/create_or_update_file.snap | 49 +++++++++++++ .../create_pending_pull_request_review.snap | 34 +++++++++ .../__toolsnaps__/create_pull_request.snap | 52 +++++++++++++ .../__toolsnaps__/create_repository.snap | 32 ++++++++ pkg/github/__toolsnaps__/delete_file.snap | 41 +++++++++++ .../delete_pending_pull_request_review.snap | 30 ++++++++ .../__toolsnaps__/dismiss_notification.snap | 28 +++++++ pkg/github/__toolsnaps__/fork_repository.snap | 29 ++++++++ .../get_code_scanning_alert.snap | 30 ++++++++ pkg/github/__toolsnaps__/get_commit.snap | 41 +++++++++++ .../__toolsnaps__/get_file_contents.snap | 34 +++++++++ pkg/github/__toolsnaps__/get_issue.snap | 30 ++++++++ .../__toolsnaps__/get_issue_comments.snap | 38 ++++++++++ .../get_notification_details.snap | 20 +++++ .../__toolsnaps__/get_pull_request.snap | 30 ++++++++ .../get_pull_request_comments.snap | 30 ++++++++ .../__toolsnaps__/get_pull_request_diff.snap | 30 ++++++++ .../__toolsnaps__/get_pull_request_files.snap | 30 ++++++++ .../get_pull_request_reviews.snap | 30 ++++++++ .../get_pull_request_status.snap | 30 ++++++++ pkg/github/__toolsnaps__/get_tag.snap | 30 ++++++++ pkg/github/__toolsnaps__/list_branches.snap | 36 +++++++++ .../list_code_scanning_alerts.snap | 57 +++++++++++++++ pkg/github/__toolsnaps__/list_commits.snap | 40 ++++++++++ pkg/github/__toolsnaps__/list_issues.snap | 73 +++++++++++++++++++ .../__toolsnaps__/list_notifications.snap | 49 +++++++++++++ .../__toolsnaps__/list_pull_requests.snap | 71 ++++++++++++++++++ pkg/github/__toolsnaps__/list_tags.snap | 36 +++++++++ .../manage_notification_subscription.snap | 30 ++++++++ ..._repository_notification_subscription.snap | 35 +++++++++ .../mark_all_notifications_read.snap | 25 +++++++ .../__toolsnaps__/merge_pull_request.snap | 47 ++++++++++++ pkg/github/__toolsnaps__/push_files.snap | 58 +++++++++++++++ .../__toolsnaps__/request_copilot_review.snap | 30 ++++++++ pkg/github/__toolsnaps__/search_code.snap | 43 +++++++++++ pkg/github/__toolsnaps__/search_issues.snap | 56 ++++++++++++++ .../__toolsnaps__/search_repositories.snap | 31 ++++++++ pkg/github/__toolsnaps__/search_users.snap | 48 ++++++++++++ .../submit_pending_pull_request_review.snap | 44 +++++++++++ pkg/github/__toolsnaps__/update_issue.snap | 64 ++++++++++++++++ .../__toolsnaps__/update_pull_request.snap | 54 ++++++++++++++ .../update_pull_request_branch.snap | 34 +++++++++ pkg/github/code_scanning_test.go | 3 + pkg/github/issues_test.go | 9 +++ pkg/github/notifications_test.go | 13 ++++ pkg/github/pullrequests_test.go | 18 +++++ pkg/github/repositories_test.go | 13 ++++ pkg/github/search_test.go | 4 + 54 files changed, 1993 insertions(+) create mode 100644 pkg/github/__toolsnaps__/add_issue_comment.snap create mode 100644 pkg/github/__toolsnaps__/add_pull_request_review_comment_to_pending_review.snap create mode 100644 pkg/github/__toolsnaps__/assign_copilot_to_issue.snap create mode 100644 pkg/github/__toolsnaps__/create_and_submit_pull_request_review.snap create mode 100644 pkg/github/__toolsnaps__/create_branch.snap create mode 100644 pkg/github/__toolsnaps__/create_issue.snap create mode 100644 pkg/github/__toolsnaps__/create_or_update_file.snap create mode 100644 pkg/github/__toolsnaps__/create_pending_pull_request_review.snap create mode 100644 pkg/github/__toolsnaps__/create_pull_request.snap create mode 100644 pkg/github/__toolsnaps__/create_repository.snap create mode 100644 pkg/github/__toolsnaps__/delete_file.snap create mode 100644 pkg/github/__toolsnaps__/delete_pending_pull_request_review.snap create mode 100644 pkg/github/__toolsnaps__/dismiss_notification.snap create mode 100644 pkg/github/__toolsnaps__/fork_repository.snap create mode 100644 pkg/github/__toolsnaps__/get_code_scanning_alert.snap create mode 100644 pkg/github/__toolsnaps__/get_commit.snap create mode 100644 pkg/github/__toolsnaps__/get_file_contents.snap create mode 100644 pkg/github/__toolsnaps__/get_issue.snap create mode 100644 pkg/github/__toolsnaps__/get_issue_comments.snap create mode 100644 pkg/github/__toolsnaps__/get_notification_details.snap create mode 100644 pkg/github/__toolsnaps__/get_pull_request.snap create mode 100644 pkg/github/__toolsnaps__/get_pull_request_comments.snap create mode 100644 pkg/github/__toolsnaps__/get_pull_request_diff.snap create mode 100644 pkg/github/__toolsnaps__/get_pull_request_files.snap create mode 100644 pkg/github/__toolsnaps__/get_pull_request_reviews.snap create mode 100644 pkg/github/__toolsnaps__/get_pull_request_status.snap create mode 100644 pkg/github/__toolsnaps__/get_tag.snap create mode 100644 pkg/github/__toolsnaps__/list_branches.snap create mode 100644 pkg/github/__toolsnaps__/list_code_scanning_alerts.snap create mode 100644 pkg/github/__toolsnaps__/list_commits.snap create mode 100644 pkg/github/__toolsnaps__/list_issues.snap create mode 100644 pkg/github/__toolsnaps__/list_notifications.snap create mode 100644 pkg/github/__toolsnaps__/list_pull_requests.snap create mode 100644 pkg/github/__toolsnaps__/list_tags.snap create mode 100644 pkg/github/__toolsnaps__/manage_notification_subscription.snap create mode 100644 pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap create mode 100644 pkg/github/__toolsnaps__/mark_all_notifications_read.snap create mode 100644 pkg/github/__toolsnaps__/merge_pull_request.snap create mode 100644 pkg/github/__toolsnaps__/push_files.snap create mode 100644 pkg/github/__toolsnaps__/request_copilot_review.snap create mode 100644 pkg/github/__toolsnaps__/search_code.snap create mode 100644 pkg/github/__toolsnaps__/search_issues.snap create mode 100644 pkg/github/__toolsnaps__/search_repositories.snap create mode 100644 pkg/github/__toolsnaps__/search_users.snap create mode 100644 pkg/github/__toolsnaps__/submit_pending_pull_request_review.snap create mode 100644 pkg/github/__toolsnaps__/update_issue.snap create mode 100644 pkg/github/__toolsnaps__/update_pull_request.snap create mode 100644 pkg/github/__toolsnaps__/update_pull_request_branch.snap diff --git a/pkg/github/__toolsnaps__/add_issue_comment.snap b/pkg/github/__toolsnaps__/add_issue_comment.snap new file mode 100644 index 00000000..92eeb1ce --- /dev/null +++ b/pkg/github/__toolsnaps__/add_issue_comment.snap @@ -0,0 +1,35 @@ +{ + "annotations": { + "title": "Add comment to issue", + "readOnlyHint": false + }, + "description": "Add a comment to a specific issue in a GitHub repository.", + "inputSchema": { + "properties": { + "body": { + "description": "Comment content", + "type": "string" + }, + "issue_number": { + "description": "Issue number to comment on", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number", + "body" + ], + "type": "object" + }, + "name": "add_issue_comment" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/add_pull_request_review_comment_to_pending_review.snap b/pkg/github/__toolsnaps__/add_pull_request_review_comment_to_pending_review.snap new file mode 100644 index 00000000..454b9d0b --- /dev/null +++ b/pkg/github/__toolsnaps__/add_pull_request_review_comment_to_pending_review.snap @@ -0,0 +1,73 @@ +{ + "annotations": { + "title": "Add comment to the requester's latest pending pull request review", + "readOnlyHint": false + }, + "description": "Add a comment to the requester's latest pending pull request review, a pending review needs to already exist to call this (check with the user if not sure).", + "inputSchema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "line": { + "description": "The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "path": { + "description": "The relative path to the file that necessitates a comment", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "side": { + "description": "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "startLine": { + "description": "For multi-line comments, the first line of the range that the comment applies to", + "type": "number" + }, + "startSide": { + "description": "For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "subjectType": { + "description": "The level at which the comment is targeted", + "enum": [ + "FILE", + "LINE" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], + "type": "object" + }, + "name": "add_pull_request_review_comment_to_pending_review" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap b/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap new file mode 100644 index 00000000..2d61ccfb --- /dev/null +++ b/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap @@ -0,0 +1,31 @@ +{ + "annotations": { + "title": "Assign Copilot to issue", + "readOnlyHint": false, + "idempotentHint": true + }, + "description": "Assign Copilot to a specific issue in a GitHub repository.\n\nThis tool can help with the following outcomes:\n- a Pull Request created with source code changes to resolve the issue\n\n\nMore information can be found at:\n- https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot\n", + "inputSchema": { + "properties": { + "issueNumber": { + "description": "Issue number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issueNumber" + ], + "type": "object" + }, + "name": "assign_copilot_to_issue" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_and_submit_pull_request_review.snap b/pkg/github/__toolsnaps__/create_and_submit_pull_request_review.snap new file mode 100644 index 00000000..85874cfc --- /dev/null +++ b/pkg/github/__toolsnaps__/create_and_submit_pull_request_review.snap @@ -0,0 +1,49 @@ +{ + "annotations": { + "title": "Create and submit a pull request review without comments", + "readOnlyHint": false + }, + "description": "Create and submit a review for a pull request without review comments.", + "inputSchema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "body", + "event" + ], + "type": "object" + }, + "name": "create_and_submit_pull_request_review" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_branch.snap b/pkg/github/__toolsnaps__/create_branch.snap new file mode 100644 index 00000000..d5756fcc --- /dev/null +++ b/pkg/github/__toolsnaps__/create_branch.snap @@ -0,0 +1,34 @@ +{ + "annotations": { + "title": "Create branch", + "readOnlyHint": false + }, + "description": "Create a new branch in a GitHub repository", + "inputSchema": { + "properties": { + "branch": { + "description": "Name for new branch", + "type": "string" + }, + "from_branch": { + "description": "Source branch (defaults to repo default)", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "name": "create_branch" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_issue.snap b/pkg/github/__toolsnaps__/create_issue.snap new file mode 100644 index 00000000..f065b018 --- /dev/null +++ b/pkg/github/__toolsnaps__/create_issue.snap @@ -0,0 +1,52 @@ +{ + "annotations": { + "title": "Open new issue", + "readOnlyHint": false + }, + "description": "Create a new issue in a GitHub repository.", + "inputSchema": { + "properties": { + "assignees": { + "description": "Usernames to assign to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "Issue body content", + "type": "string" + }, + "labels": { + "description": "Labels to apply to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "milestone": { + "description": "Milestone number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "title": { + "description": "Issue title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title" + ], + "type": "object" + }, + "name": "create_issue" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_or_update_file.snap b/pkg/github/__toolsnaps__/create_or_update_file.snap new file mode 100644 index 00000000..53f643df --- /dev/null +++ b/pkg/github/__toolsnaps__/create_or_update_file.snap @@ -0,0 +1,49 @@ +{ + "annotations": { + "title": "Create or update file", + "readOnlyHint": false + }, + "description": "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update.", + "inputSchema": { + "properties": { + "branch": { + "description": "Branch to create/update the file in", + "type": "string" + }, + "content": { + "description": "Content of the file", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path where to create/update the file", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "SHA of file being replaced (for updates)", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "content", + "message", + "branch" + ], + "type": "object" + }, + "name": "create_or_update_file" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_pending_pull_request_review.snap b/pkg/github/__toolsnaps__/create_pending_pull_request_review.snap new file mode 100644 index 00000000..3eea5e6a --- /dev/null +++ b/pkg/github/__toolsnaps__/create_pending_pull_request_review.snap @@ -0,0 +1,34 @@ +{ + "annotations": { + "title": "Create pending pull request review", + "readOnlyHint": false + }, + "description": "Create a pending review for a pull request. Call this first before attempting to add comments to a pending review, and ultimately submitting it. A pending pull request review means a pull request review, it is pending because you create it first and submit it later, and the PR author will not see it until it is submitted.", + "inputSchema": { + "properties": { + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "create_pending_pull_request_review" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_pull_request.snap b/pkg/github/__toolsnaps__/create_pull_request.snap new file mode 100644 index 00000000..44142a79 --- /dev/null +++ b/pkg/github/__toolsnaps__/create_pull_request.snap @@ -0,0 +1,52 @@ +{ + "annotations": { + "title": "Open new pull request", + "readOnlyHint": false + }, + "description": "Create a new pull request in a GitHub repository.", + "inputSchema": { + "properties": { + "base": { + "description": "Branch to merge into", + "type": "string" + }, + "body": { + "description": "PR description", + "type": "string" + }, + "draft": { + "description": "Create as draft PR", + "type": "boolean" + }, + "head": { + "description": "Branch containing changes", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "title": { + "description": "PR title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], + "type": "object" + }, + "name": "create_pull_request" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/create_repository.snap b/pkg/github/__toolsnaps__/create_repository.snap new file mode 100644 index 00000000..aaba75f3 --- /dev/null +++ b/pkg/github/__toolsnaps__/create_repository.snap @@ -0,0 +1,32 @@ +{ + "annotations": { + "title": "Create repository", + "readOnlyHint": false + }, + "description": "Create a new GitHub repository in your account", + "inputSchema": { + "properties": { + "autoInit": { + "description": "Initialize with README", + "type": "boolean" + }, + "description": { + "description": "Repository description", + "type": "string" + }, + "name": { + "description": "Repository name", + "type": "string" + }, + "private": { + "description": "Whether repo should be private", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "create_repository" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_file.snap b/pkg/github/__toolsnaps__/delete_file.snap new file mode 100644 index 00000000..2588ea5c --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_file.snap @@ -0,0 +1,41 @@ +{ + "annotations": { + "title": "Delete file", + "readOnlyHint": false, + "destructiveHint": true + }, + "description": "Delete a file from a GitHub repository", + "inputSchema": { + "properties": { + "branch": { + "description": "Branch to delete the file from", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path to the file to delete", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "message", + "branch" + ], + "type": "object" + }, + "name": "delete_file" +} \ 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 new file mode 100644 index 00000000..9aff7356 --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_pending_pull_request_review.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Delete the requester's latest pending pull request review", + "readOnlyHint": false + }, + "description": "Delete the requester's latest pending pull request review. Use this after the user decides not to submit a pending review, if you don't know if they already created one then check first.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "delete_pending_pull_request_review" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/dismiss_notification.snap b/pkg/github/__toolsnaps__/dismiss_notification.snap new file mode 100644 index 00000000..80646a80 --- /dev/null +++ b/pkg/github/__toolsnaps__/dismiss_notification.snap @@ -0,0 +1,28 @@ +{ + "annotations": { + "title": "Dismiss notification", + "readOnlyHint": false + }, + "description": "Dismiss a notification by marking it as read or done", + "inputSchema": { + "properties": { + "state": { + "description": "The new state of the notification (read/done)", + "enum": [ + "read", + "done" + ], + "type": "string" + }, + "threadID": { + "description": "The ID of the notification thread", + "type": "string" + } + }, + "required": [ + "threadID" + ], + "type": "object" + }, + "name": "dismiss_notification" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/fork_repository.snap b/pkg/github/__toolsnaps__/fork_repository.snap new file mode 100644 index 00000000..6e4d2782 --- /dev/null +++ b/pkg/github/__toolsnaps__/fork_repository.snap @@ -0,0 +1,29 @@ +{ + "annotations": { + "title": "Fork repository", + "readOnlyHint": false + }, + "description": "Fork a GitHub repository to your account or specified organization", + "inputSchema": { + "properties": { + "organization": { + "description": "Organization to fork to", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "fork_repository" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_code_scanning_alert.snap b/pkg/github/__toolsnaps__/get_code_scanning_alert.snap new file mode 100644 index 00000000..eedc20b4 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_code_scanning_alert.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get code scanning alert", + "readOnlyHint": true + }, + "description": "Get details of a specific code scanning alert in a GitHub repository.", + "inputSchema": { + "properties": { + "alertNumber": { + "description": "The number of the alert.", + "type": "number" + }, + "owner": { + "description": "The owner of the repository.", + "type": "string" + }, + "repo": { + "description": "The name of the repository.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "alertNumber" + ], + "type": "object" + }, + "name": "get_code_scanning_alert" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_commit.snap b/pkg/github/__toolsnaps__/get_commit.snap new file mode 100644 index 00000000..af003811 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_commit.snap @@ -0,0 +1,41 @@ +{ + "annotations": { + "title": "Get commit details", + "readOnlyHint": true + }, + "description": "Get details for a commit from a GitHub repository", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch name, or tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "sha" + ], + "type": "object" + }, + "name": "get_commit" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_file_contents.snap b/pkg/github/__toolsnaps__/get_file_contents.snap new file mode 100644 index 00000000..c2c6f19f --- /dev/null +++ b/pkg/github/__toolsnaps__/get_file_contents.snap @@ -0,0 +1,34 @@ +{ + "annotations": { + "title": "Get file or directory contents", + "readOnlyHint": true + }, + "description": "Get the contents of a file or directory from a GitHub repository", + "inputSchema": { + "properties": { + "branch": { + "description": "Branch to get contents from", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path to file/directory (directories must end with a slash '/')", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path" + ], + "type": "object" + }, + "name": "get_file_contents" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_issue.snap b/pkg/github/__toolsnaps__/get_issue.snap new file mode 100644 index 00000000..eab2b872 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_issue.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get issue details", + "readOnlyHint": true + }, + "description": "Get details of a specific issue in a GitHub repository.", + "inputSchema": { + "properties": { + "issue_number": { + "description": "The number of the issue", + "type": "number" + }, + "owner": { + "description": "The owner of the repository", + "type": "string" + }, + "repo": { + "description": "The name of the repository", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "get_issue" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_issue_comments.snap b/pkg/github/__toolsnaps__/get_issue_comments.snap new file mode 100644 index 00000000..fa1fb0d6 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_issue_comments.snap @@ -0,0 +1,38 @@ +{ + "annotations": { + "title": "Get issue comments", + "readOnlyHint": true + }, + "description": "Get comments for a specific issue in a GitHub repository.", + "inputSchema": { + "properties": { + "issue_number": { + "description": "Issue number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number", + "type": "number" + }, + "per_page": { + "description": "Number of records per page", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "get_issue_comments" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_notification_details.snap b/pkg/github/__toolsnaps__/get_notification_details.snap new file mode 100644 index 00000000..62bc6bf1 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_notification_details.snap @@ -0,0 +1,20 @@ +{ + "annotations": { + "title": "Get notification details", + "readOnlyHint": true + }, + "description": "Get detailed information for a specific GitHub notification, always call this tool when the user asks for details about a specific notification, if you don't know the ID list notifications first.", + "inputSchema": { + "properties": { + "notificationID": { + "description": "The ID of the notification", + "type": "string" + } + }, + "required": [ + "notificationID" + ], + "type": "object" + }, + "name": "get_notification_details" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_pull_request.snap b/pkg/github/__toolsnaps__/get_pull_request.snap new file mode 100644 index 00000000..cbcf1f42 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_pull_request.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get pull request details", + "readOnlyHint": true + }, + "description": "Get details of a specific pull request in a GitHub repository.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "get_pull_request" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_pull_request_comments.snap b/pkg/github/__toolsnaps__/get_pull_request_comments.snap new file mode 100644 index 00000000..6699f6d9 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_pull_request_comments.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get pull request comments", + "readOnlyHint": true + }, + "description": "Get comments for a specific pull request.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "get_pull_request_comments" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_pull_request_diff.snap b/pkg/github/__toolsnaps__/get_pull_request_diff.snap new file mode 100644 index 00000000..e054eab9 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_pull_request_diff.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get pull request diff", + "readOnlyHint": true + }, + "description": "Get the diff of a pull request.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "get_pull_request_diff" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_pull_request_files.snap b/pkg/github/__toolsnaps__/get_pull_request_files.snap new file mode 100644 index 00000000..c61f5f35 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_pull_request_files.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get pull request files", + "readOnlyHint": true + }, + "description": "Get the files changed in a specific pull request.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "get_pull_request_files" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_pull_request_reviews.snap b/pkg/github/__toolsnaps__/get_pull_request_reviews.snap new file mode 100644 index 00000000..61dee53e --- /dev/null +++ b/pkg/github/__toolsnaps__/get_pull_request_reviews.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get pull request reviews", + "readOnlyHint": true + }, + "description": "Get reviews for a specific pull request.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "get_pull_request_reviews" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_pull_request_status.snap b/pkg/github/__toolsnaps__/get_pull_request_status.snap new file mode 100644 index 00000000..8ffebc3a --- /dev/null +++ b/pkg/github/__toolsnaps__/get_pull_request_status.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get pull request status checks", + "readOnlyHint": true + }, + "description": "Get the status of a specific pull request.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "get_pull_request_status" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_tag.snap b/pkg/github/__toolsnaps__/get_tag.snap new file mode 100644 index 00000000..42089f87 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_tag.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Get tag details", + "readOnlyHint": true + }, + "description": "Get details about a specific git tag in a GitHub repository", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_tag" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_branches.snap b/pkg/github/__toolsnaps__/list_branches.snap new file mode 100644 index 00000000..492b6d52 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_branches.snap @@ -0,0 +1,36 @@ +{ + "annotations": { + "title": "List branches", + "readOnlyHint": true + }, + "description": "List branches in a GitHub repository", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_branches" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap b/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap new file mode 100644 index 00000000..470f0d01 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap @@ -0,0 +1,57 @@ +{ + "annotations": { + "title": "List code scanning alerts", + "readOnlyHint": true + }, + "description": "List code scanning alerts in a GitHub repository.", + "inputSchema": { + "properties": { + "owner": { + "description": "The owner of the repository.", + "type": "string" + }, + "ref": { + "description": "The Git reference for the results you want to list.", + "type": "string" + }, + "repo": { + "description": "The name of the repository.", + "type": "string" + }, + "severity": { + "description": "Filter code scanning alerts by severity", + "enum": [ + "critical", + "high", + "medium", + "low", + "warning", + "note", + "error" + ], + "type": "string" + }, + "state": { + "default": "open", + "description": "Filter code scanning alerts by state. Defaults to open", + "enum": [ + "open", + "closed", + "dismissed", + "fixed" + ], + "type": "string" + }, + "tool_name": { + "description": "The name of the tool used for code scanning.", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_code_scanning_alerts" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_commits.snap b/pkg/github/__toolsnaps__/list_commits.snap new file mode 100644 index 00000000..7be03a7f --- /dev/null +++ b/pkg/github/__toolsnaps__/list_commits.snap @@ -0,0 +1,40 @@ +{ + "annotations": { + "title": "List commits", + "readOnlyHint": true + }, + "description": "Get list of commits of a branch in a GitHub repository", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "SHA or Branch name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_commits" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_issues.snap b/pkg/github/__toolsnaps__/list_issues.snap new file mode 100644 index 00000000..4fe155f0 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_issues.snap @@ -0,0 +1,73 @@ +{ + "annotations": { + "title": "List issues", + "readOnlyHint": true + }, + "description": "List issues in a GitHub repository.", + "inputSchema": { + "properties": { + "direction": { + "description": "Sort direction", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "labels": { + "description": "Filter by labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "since": { + "description": "Filter by date (ISO 8601 timestamp)", + "type": "string" + }, + "sort": { + "description": "Sort order", + "enum": [ + "created", + "updated", + "comments" + ], + "type": "string" + }, + "state": { + "description": "Filter by state", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_issues" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_notifications.snap b/pkg/github/__toolsnaps__/list_notifications.snap new file mode 100644 index 00000000..92f25eb4 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_notifications.snap @@ -0,0 +1,49 @@ +{ + "annotations": { + "title": "List notifications", + "readOnlyHint": true + }, + "description": "Lists all GitHub notifications for the authenticated user, including unread notifications, mentions, review requests, assignments, and updates on issues or pull requests. Use this tool whenever the user asks what to work on next, requests a summary of their GitHub activity, wants to see pending reviews, or needs to check for new updates or tasks. This tool is the primary way to discover actionable items, reminders, and outstanding work on GitHub. Always call this tool when asked what to work on next, what is pending, or what needs attention in GitHub.", + "inputSchema": { + "properties": { + "before": { + "description": "Only show notifications updated before the given time (ISO 8601 format)", + "type": "string" + }, + "filter": { + "description": "Filter notifications to, use default unless specified. Read notifications are ones that have already been acknowledged by the user. Participating notifications are those that the user is directly involved in, such as issues or pull requests they have commented on or created.", + "enum": [ + "default", + "include_read_notifications", + "only_participating" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only notifications for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only notifications for this repository are listed.", + "type": "string" + }, + "since": { + "description": "Only show notifications updated after the given time (ISO 8601 format)", + "type": "string" + } + }, + "type": "object" + }, + "name": "list_notifications" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_pull_requests.snap b/pkg/github/__toolsnaps__/list_pull_requests.snap new file mode 100644 index 00000000..b8369784 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_pull_requests.snap @@ -0,0 +1,71 @@ +{ + "annotations": { + "title": "List pull requests", + "readOnlyHint": true + }, + "description": "List pull requests in a GitHub repository.", + "inputSchema": { + "properties": { + "base": { + "description": "Filter by base branch", + "type": "string" + }, + "direction": { + "description": "Sort direction", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "head": { + "description": "Filter by head user/org and branch", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sort": { + "description": "Sort by", + "enum": [ + "created", + "updated", + "popularity", + "long-running" + ], + "type": "string" + }, + "state": { + "description": "Filter by state", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_pull_requests" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_tags.snap b/pkg/github/__toolsnaps__/list_tags.snap new file mode 100644 index 00000000..fcb9853f --- /dev/null +++ b/pkg/github/__toolsnaps__/list_tags.snap @@ -0,0 +1,36 @@ +{ + "annotations": { + "title": "List tags", + "readOnlyHint": true + }, + "description": "List git tags in a GitHub repository", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_tags" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/manage_notification_subscription.snap b/pkg/github/__toolsnaps__/manage_notification_subscription.snap new file mode 100644 index 00000000..0f7d9120 --- /dev/null +++ b/pkg/github/__toolsnaps__/manage_notification_subscription.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Manage notification subscription", + "readOnlyHint": false + }, + "description": "Manage a notification subscription: ignore, watch, or delete a notification thread subscription.", + "inputSchema": { + "properties": { + "action": { + "description": "Action to perform: ignore, watch, or delete the notification subscription.", + "enum": [ + "ignore", + "watch", + "delete" + ], + "type": "string" + }, + "notificationID": { + "description": "The ID of the notification thread.", + "type": "string" + } + }, + "required": [ + "notificationID", + "action" + ], + "type": "object" + }, + "name": "manage_notification_subscription" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap b/pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap new file mode 100644 index 00000000..9d09a581 --- /dev/null +++ b/pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap @@ -0,0 +1,35 @@ +{ + "annotations": { + "title": "Manage repository notification subscription", + "readOnlyHint": false + }, + "description": "Manage a repository notification subscription: ignore, watch, or delete repository notifications subscription for the provided repository.", + "inputSchema": { + "properties": { + "action": { + "description": "Action to perform: ignore, watch, or delete the repository notification subscription.", + "enum": [ + "ignore", + "watch", + "delete" + ], + "type": "string" + }, + "owner": { + "description": "The account owner of the repository.", + "type": "string" + }, + "repo": { + "description": "The name of the repository.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "action" + ], + "type": "object" + }, + "name": "manage_repository_notification_subscription" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/mark_all_notifications_read.snap b/pkg/github/__toolsnaps__/mark_all_notifications_read.snap new file mode 100644 index 00000000..5a1fe24a --- /dev/null +++ b/pkg/github/__toolsnaps__/mark_all_notifications_read.snap @@ -0,0 +1,25 @@ +{ + "annotations": { + "title": "Mark all notifications as read", + "readOnlyHint": false + }, + "description": "Mark all notifications as read", + "inputSchema": { + "properties": { + "lastReadAt": { + "description": "Describes the last point that notifications were checked (optional). Default: Now", + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only notifications for this repository are marked as read.", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only notifications for this repository are marked as read.", + "type": "string" + } + }, + "type": "object" + }, + "name": "mark_all_notifications_read" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/merge_pull_request.snap b/pkg/github/__toolsnaps__/merge_pull_request.snap new file mode 100644 index 00000000..a5a1474c --- /dev/null +++ b/pkg/github/__toolsnaps__/merge_pull_request.snap @@ -0,0 +1,47 @@ +{ + "annotations": { + "title": "Merge pull request", + "readOnlyHint": false + }, + "description": "Merge a pull request in a GitHub repository.", + "inputSchema": { + "properties": { + "commit_message": { + "description": "Extra detail for merge commit", + "type": "string" + }, + "commit_title": { + "description": "Title for merge commit", + "type": "string" + }, + "merge_method": { + "description": "Merge method", + "enum": [ + "merge", + "squash", + "rebase" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "merge_pull_request" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/push_files.snap b/pkg/github/__toolsnaps__/push_files.snap new file mode 100644 index 00000000..3ade75ee --- /dev/null +++ b/pkg/github/__toolsnaps__/push_files.snap @@ -0,0 +1,58 @@ +{ + "annotations": { + "title": "Push files to repository", + "readOnlyHint": false + }, + "description": "Push multiple files to a GitHub repository in a single commit", + "inputSchema": { + "properties": { + "branch": { + "description": "Branch to push to", + "type": "string" + }, + "files": { + "description": "Array of file objects to push, each object with path (string) and content (string)", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "file content", + "type": "string" + }, + "path": { + "description": "path to the file", + "type": "string" + } + }, + "required": [ + "path", + "content" + ], + "type": "object" + }, + "type": "array" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch", + "files", + "message" + ], + "type": "object" + }, + "name": "push_files" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/request_copilot_review.snap b/pkg/github/__toolsnaps__/request_copilot_review.snap new file mode 100644 index 00000000..1717ced0 --- /dev/null +++ b/pkg/github/__toolsnaps__/request_copilot_review.snap @@ -0,0 +1,30 @@ +{ + "annotations": { + "title": "Request Copilot review", + "readOnlyHint": false + }, + "description": "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.", + "inputSchema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "request_copilot_review" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_code.snap b/pkg/github/__toolsnaps__/search_code.snap new file mode 100644 index 00000000..c85d6674 --- /dev/null +++ b/pkg/github/__toolsnaps__/search_code.snap @@ -0,0 +1,43 @@ +{ + "annotations": { + "title": "Search code", + "readOnlyHint": true + }, + "description": "Search for code across GitHub repositories", + "inputSchema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "q": { + "description": "Search query using GitHub code search syntax", + "type": "string" + }, + "sort": { + "description": "Sort field ('indexed' only)", + "type": "string" + } + }, + "required": [ + "q" + ], + "type": "object" + }, + "name": "search_code" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_issues.snap b/pkg/github/__toolsnaps__/search_issues.snap new file mode 100644 index 00000000..4e2382a3 --- /dev/null +++ b/pkg/github/__toolsnaps__/search_issues.snap @@ -0,0 +1,56 @@ +{ + "annotations": { + "title": "Search issues", + "readOnlyHint": true + }, + "description": "Search for issues in GitHub repositories.", + "inputSchema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "q": { + "description": "Search query using GitHub issues search syntax", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "q" + ], + "type": "object" + }, + "name": "search_issues" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_repositories.snap b/pkg/github/__toolsnaps__/search_repositories.snap new file mode 100644 index 00000000..b6b6d1d4 --- /dev/null +++ b/pkg/github/__toolsnaps__/search_repositories.snap @@ -0,0 +1,31 @@ +{ + "annotations": { + "title": "Search repositories", + "readOnlyHint": true + }, + "description": "Search for GitHub repositories", + "inputSchema": { + "properties": { + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_repositories" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_users.snap b/pkg/github/__toolsnaps__/search_users.snap new file mode 100644 index 00000000..aad2970b --- /dev/null +++ b/pkg/github/__toolsnaps__/search_users.snap @@ -0,0 +1,48 @@ +{ + "annotations": { + "title": "Search users", + "readOnlyHint": true + }, + "description": "Search for GitHub users", + "inputSchema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "q": { + "description": "Search query using GitHub users search syntax", + "type": "string" + }, + "sort": { + "description": "Sort field by category", + "enum": [ + "followers", + "repositories", + "joined" + ], + "type": "string" + } + }, + "required": [ + "q" + ], + "type": "object" + }, + "name": "search_users" +} \ 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 new file mode 100644 index 00000000..f3541922 --- /dev/null +++ b/pkg/github/__toolsnaps__/submit_pending_pull_request_review.snap @@ -0,0 +1,44 @@ +{ + "annotations": { + "title": "Submit the requester's latest pending pull request review", + "readOnlyHint": false + }, + "description": "Submit the requester's latest pending pull request review, normally this is a final step after creating a pending review, adding comments first, unless you know that the user already did the first two steps, you should check before calling this.", + "inputSchema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "event": { + "description": "The event to perform", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "event" + ], + "type": "object" + }, + "name": "submit_pending_pull_request_review" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_issue.snap b/pkg/github/__toolsnaps__/update_issue.snap new file mode 100644 index 00000000..4bcae7ba --- /dev/null +++ b/pkg/github/__toolsnaps__/update_issue.snap @@ -0,0 +1,64 @@ +{ + "annotations": { + "title": "Edit issue", + "readOnlyHint": false + }, + "description": "Update an existing issue in a GitHub repository.", + "inputSchema": { + "properties": { + "assignees": { + "description": "New assignees", + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "New description", + "type": "string" + }, + "issue_number": { + "description": "Issue number to update", + "type": "number" + }, + "labels": { + "description": "New labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "milestone": { + "description": "New milestone number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "update_issue" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request.snap b/pkg/github/__toolsnaps__/update_pull_request.snap new file mode 100644 index 00000000..765983af --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request.snap @@ -0,0 +1,54 @@ +{ + "annotations": { + "title": "Edit pull request", + "readOnlyHint": false + }, + "description": "Update an existing pull request in a GitHub repository.", + "inputSchema": { + "properties": { + "base": { + "description": "New base branch name", + "type": "string" + }, + "body": { + "description": "New description", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number to update", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_pull_request_branch.snap b/pkg/github/__toolsnaps__/update_pull_request_branch.snap new file mode 100644 index 00000000..60ec9c12 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_pull_request_branch.snap @@ -0,0 +1,34 @@ +{ + "annotations": { + "title": "Update pull request branch", + "readOnlyHint": false + }, + "description": "Update the branch of a pull request with the latest changes from the base branch.", + "inputSchema": { + "properties": { + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request_branch" +} \ No newline at end of file diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go index b5facbf6..5c0131a7 100644 --- a/pkg/github/code_scanning_test.go +++ b/pkg/github/code_scanning_test.go @@ -6,6 +6,7 @@ import ( "net/http" "testing" + "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -17,6 +18,7 @@ func Test_GetCodeScanningAlert(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetCodeScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_code_scanning_alert", tool.Name) assert.NotEmpty(t, tool.Description) @@ -119,6 +121,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := ListCodeScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_code_scanning_alerts", tool.Name) assert.NotEmpty(t, tool.Description) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 251fc32b..7c76d90f 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/github/github-mcp-server/internal/githubv4mock" + "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -21,6 +22,7 @@ func Test_GetIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -117,6 +119,7 @@ func Test_AddIssueComment(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := AddIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "add_issue_comment", tool.Name) assert.NotEmpty(t, tool.Description) @@ -230,6 +233,7 @@ func Test_SearchIssues(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := SearchIssues(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_issues", tool.Name) assert.NotEmpty(t, tool.Description) @@ -386,6 +390,7 @@ func Test_CreateIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := CreateIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -560,6 +565,7 @@ func Test_ListIssues(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) tool, _ := ListIssues(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_issues", tool.Name) assert.NotEmpty(t, tool.Description) @@ -736,6 +742,7 @@ func Test_UpdateIssue(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) tool, _ := UpdateIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "update_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -993,6 +1000,7 @@ func Test_GetIssueComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetIssueComments(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_issue_comments", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1129,6 +1137,7 @@ func TestAssignCopilotToIssue(t *testing.T) { // Verify tool definition mockClient := githubv4.NewClient(nil) tool, _ := AssignCopilotToIssue(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "assign_copilot_to_issue", tool.Name) assert.NotEmpty(t, tool.Description) diff --git a/pkg/github/notifications_test.go b/pkg/github/notifications_test.go index 173f1a78..77372f02 100644 --- a/pkg/github/notifications_test.go +++ b/pkg/github/notifications_test.go @@ -6,6 +6,7 @@ import ( "net/http" "testing" + "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -17,6 +18,8 @@ func Test_ListNotifications(t *testing.T) { // Verify tool definition and schema mockClient := github.NewClient(nil) tool, _ := ListNotifications(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "list_notifications", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "filter") @@ -147,6 +150,8 @@ func Test_ManageNotificationSubscription(t *testing.T) { // Verify tool definition and schema mockClient := github.NewClient(nil) tool, _ := ManageNotificationSubscription(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "manage_notification_subscription", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "notificationID") @@ -283,6 +288,8 @@ func Test_ManageRepositoryNotificationSubscription(t *testing.T) { // Verify tool definition and schema mockClient := github.NewClient(nil) tool, _ := ManageRepositoryNotificationSubscription(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "manage_repository_notification_subscription", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "owner") @@ -444,6 +451,8 @@ func Test_DismissNotification(t *testing.T) { // Verify tool definition and schema mockClient := github.NewClient(nil) tool, _ := DismissNotification(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "dismiss_notification", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "threadID") @@ -574,6 +583,8 @@ func Test_MarkAllNotificationsRead(t *testing.T) { // Verify tool definition and schema mockClient := github.NewClient(nil) tool, _ := MarkAllNotificationsRead(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "mark_all_notifications_read", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "lastReadAt") @@ -672,6 +683,8 @@ func Test_GetNotificationDetails(t *testing.T) { // Verify tool definition and schema mockClient := github.NewClient(nil) tool, _ := GetNotificationDetails(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "get_notification_details", tool.Name) assert.NotEmpty(t, tool.Description) assert.Contains(t, tool.InputSchema.Properties, "notificationID") diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index cdbccc28..144c6b38 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/github/github-mcp-server/internal/githubv4mock" + "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/shurcooL/githubv4" @@ -21,6 +22,7 @@ func Test_GetPullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -133,6 +135,7 @@ func Test_UpdatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := UpdatePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "update_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -315,6 +318,7 @@ func Test_ListPullRequests(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := ListPullRequests(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) @@ -445,6 +449,7 @@ func Test_MergePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := MergePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "merge_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -556,6 +561,7 @@ func Test_GetPullRequestFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestFiles(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_files", tool.Name) assert.NotEmpty(t, tool.Description) @@ -672,6 +678,7 @@ func Test_GetPullRequestStatus(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestStatus(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_status", tool.Name) assert.NotEmpty(t, tool.Description) @@ -833,6 +840,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := UpdatePullRequestBranch(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "update_pull_request_branch", tool.Name) assert.NotEmpty(t, tool.Description) @@ -949,6 +957,7 @@ func Test_GetPullRequestComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestComments(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_comments", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1076,6 +1085,7 @@ func Test_GetPullRequestReviews(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestReviews(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_reviews", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1199,6 +1209,7 @@ func Test_CreatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := CreatePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1358,6 +1369,7 @@ func TestCreateAndSubmitPullRequestReview(t *testing.T) { // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := CreateAndSubmitPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_and_submit_pull_request_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1551,6 +1563,7 @@ func Test_RequestCopilotReview(t *testing.T) { mockClient := github.NewClient(nil) tool, _ := RequestCopilotReview(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "request_copilot_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1661,6 +1674,7 @@ func TestCreatePendingPullRequestReview(t *testing.T) { // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := CreatePendingPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_pending_pull_request_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1843,6 +1857,7 @@ func TestAddPullRequestReviewCommentToPendingReview(t *testing.T) { // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := AddPullRequestReviewCommentToPendingReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "add_pull_request_review_comment_to_pending_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1955,6 +1970,7 @@ func TestSubmitPendingPullRequestReview(t *testing.T) { // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := SubmitPendingPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "submit_pending_pull_request_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -2052,6 +2068,7 @@ func TestDeletePendingPullRequestReview(t *testing.T) { // Verify tool definition once mockClient := githubv4.NewClient(nil) tool, _ := DeletePendingPullRequestReview(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "delete_pending_pull_request_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -2143,6 +2160,7 @@ func TestGetPullRequestDiff(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetPullRequestDiff(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_pull_request_diff", tool.Name) assert.NotEmpty(t, tool.Description) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index c2585341..3ba0f1aa 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" @@ -23,6 +24,7 @@ func Test_GetFileContents(t *testing.T) { mockClient := github.NewClient(nil) mockRawClient := raw.NewClient(mockClient, &url.URL{Scheme: "https", Host: "raw.githubusercontent.com", Path: "/"}) tool, _ := GetFileContents(stubGetClientFn(mockClient), stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_file_contents", tool.Name) assert.NotEmpty(t, tool.Description) @@ -219,6 +221,7 @@ func Test_ForkRepository(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := ForkRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "fork_repository", tool.Name) assert.NotEmpty(t, tool.Description) @@ -317,6 +320,7 @@ func Test_CreateBranch(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := CreateBranch(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_branch", tool.Name) assert.NotEmpty(t, tool.Description) @@ -508,6 +512,7 @@ func Test_GetCommit(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetCommit(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_commit", tool.Name) assert.NotEmpty(t, tool.Description) @@ -633,6 +638,7 @@ func Test_ListCommits(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := ListCommits(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_commits", tool.Name) assert.NotEmpty(t, tool.Description) @@ -807,6 +813,7 @@ func Test_CreateOrUpdateFile(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := CreateOrUpdateFile(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_or_update_file", tool.Name) assert.NotEmpty(t, tool.Description) @@ -970,6 +977,7 @@ func Test_CreateRepository(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := CreateRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "create_repository", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1116,6 +1124,7 @@ func Test_PushFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := PushFiles(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "push_files", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1452,6 +1461,7 @@ func Test_ListBranches(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_branches", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1562,6 +1572,7 @@ func Test_DeleteFile(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := DeleteFile(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "delete_file", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1739,6 +1750,7 @@ func Test_ListTags(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := ListTags(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_tags", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1859,6 +1871,7 @@ func Test_GetTag(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := GetTag(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_tag", tool.Name) assert.NotEmpty(t, tool.Description) diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index 62645e91..b76fe804 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -6,6 +6,7 @@ import ( "net/http" "testing" + "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -17,6 +18,7 @@ func Test_SearchRepositories(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := SearchRepositories(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_repositories", tool.Name) assert.NotEmpty(t, tool.Description) @@ -164,6 +166,7 @@ func Test_SearchCode(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := SearchCode(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_code", tool.Name) assert.NotEmpty(t, tool.Description) @@ -315,6 +318,7 @@ func Test_SearchUsers(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) tool, _ := SearchUsers(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "search_users", tool.Name) assert.NotEmpty(t, tool.Description) From 2765d1dc1ee96bc819e21929dee7db1dfbafd40a Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 18 Jun 2025 10:21:12 +0200 Subject: [PATCH 13/19] Update toolsnap error message with actionable instruction --- internal/toolsnaps/toolsnaps.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/toolsnaps/toolsnaps.go b/internal/toolsnaps/toolsnaps.go index f24ffe58..89d02e1e 100644 --- a/internal/toolsnaps/toolsnaps.go +++ b/internal/toolsnaps/toolsnaps.go @@ -60,7 +60,7 @@ func Test(toolName string, tool any) error { diff := toolNode.Diff(snapNode, jd.SET).Render() if diff != "" { // If there is a difference, we return an error with the diff - return fmt.Errorf("tool schema for %s has changed unexpectedly:\n%s", toolName, diff) + return fmt.Errorf("tool schema for %s has changed unexpectedly:\n%s\nrun with `UPDATE_TOOLSNAPS=true` if this is expected", toolName, diff) } return nil From 846fac61b0f848515c9932a0688858f1b41f504f Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 18 Jun 2025 10:34:01 +0200 Subject: [PATCH 14/19] Ensure UPDATE_TOOLSNAPS doesn't interfere with tests --- internal/toolsnaps/toolsnaps_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/toolsnaps/toolsnaps_test.go b/internal/toolsnaps/toolsnaps_test.go index c664911f..be9cadf7 100644 --- a/internal/toolsnaps/toolsnaps_test.go +++ b/internal/toolsnaps/toolsnaps_test.go @@ -43,6 +43,9 @@ func TestSnapshotDoesNotExistNotInCI(t *testing.T) { func TestSnapshotDoesNotExistInCI(t *testing.T) { withIsolatedWorkingDir(t) + // Ensure that UPDATE_TOOLSNAPS is not set for this test, which it might be if someone is running + // UPDATE_TOOLSNAPS=true go test ./... + t.Setenv("UPDATE_TOOLSNAPS", "false") // Given we are running in CI t.Setenv("GITHUB_ACTIONS", "true") @@ -74,6 +77,9 @@ func TestSnapshotExistsMatch(t *testing.T) { func TestSnapshotExistsDiff(t *testing.T) { withIsolatedWorkingDir(t) + // Ensure that UPDATE_TOOLSNAPS is not set for this test, which it might be if someone is running + // UPDATE_TOOLSNAPS=true go test ./... + t.Setenv("UPDATE_TOOLSNAPS", "false") // Given a non-matching snapshot file exists require.NoError(t, os.MkdirAll("__toolsnaps__", 0700)) @@ -109,6 +115,9 @@ func TestUpdateToolsnaps(t *testing.T) { func TestMalformedSnapshotJSON(t *testing.T) { withIsolatedWorkingDir(t) + // Ensure that UPDATE_TOOLSNAPS is not set for this test, which it might be if someone is running + // UPDATE_TOOLSNAPS=true go test ./... + t.Setenv("UPDATE_TOOLSNAPS", "false") // Given a malformed snapshot file exists require.NoError(t, os.MkdirAll("__toolsnaps__", 0700)) From 7da11c270e9d3ac98b0f517e32f6447ea8ca0ed6 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 18 Jun 2025 13:01:47 +0200 Subject: [PATCH 15/19] docs: suggest shorter name for server --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a5c1e313..5da2b1f8 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Alternatively, to manually configure VS Code, choose the appropriate JSON block ```json { "servers": { - "github-remote": { + "github": { "type": "http", "url": "https://api.githubcopilot.com/mcp/" } @@ -54,7 +54,7 @@ Alternatively, to manually configure VS Code, choose the appropriate JSON block ```json { "servers": { - "github-remote": { + "github": { "type": "http", "url": "https://api.githubcopilot.com/mcp/", "headers": { @@ -89,7 +89,7 @@ For MCP Hosts that are [Remote MCP-compatible](docs/host-integration.md), choose ```json { "mcpServers": { - "github-remote": { + "github": { "url": "https://api.githubcopilot.com/mcp/" } } @@ -102,7 +102,7 @@ For MCP Hosts that are [Remote MCP-compatible](docs/host-integration.md), choose ```json { "mcpServers": { - "github-remote": { + "github": { "url": "https://api.githubcopilot.com/mcp/", "authorization_token": "Bearer " } From da6476def87309db06a8e0a9bf18a003f7a84019 Mon Sep 17 00:00:00 2001 From: Gabor Nyerges Date: Wed, 18 Jun 2025 14:56:58 +0200 Subject: [PATCH 16/19] feat: add GitHub Actions tools for workflow management (#491) * feat: add GitHub Actions tools for workflow management - Introduced new tools for managing GitHub Actions workflows, including listing workflows, running workflows, canceling workflow runs, and retrieving workflow run logs. - Updated README.md to include new `actions` toolset and detailed descriptions of the new tools. - Added comprehensive tests for the new functionality to ensure reliability and correctness. * feat: enhance GitHub Actions toolset with additional workflow management capabilities - Added new tools for managing GitHub Actions, including listing workflows, retrieving workflow run logs, and managing workflow runs. - Integrated the new `actions` toolset into the default toolset group for improved accessibility. * feat: enhance GetJobLogs functionality for improved job log retrieval - Added new tests for GetJobLogs, including scenarios for retrieving logs for both single jobs and failed jobs. - Updated GetJobLogs tool description to clarify its capabilities for fetching logs efficiently. - Implemented error handling for missing required parameters and optimized responses for failed job logs. - Introduced functionality to return actual log content instead of just URLs when requested. * feat: enhance GetJobLogs functionality for improved job log retrieval - Added new tests for GetJobLogs, including scenarios for retrieving logs for both single jobs and failed jobs. - Updated GetJobLogs tool description to clarify its capabilities for fetching logs efficiently. - Implemented error handling for missing required parameters and optimized responses for failed job logs. - Introduced functionality to return actual log content instead of just URLs when requested. * refactor: standardize parameter handling and read-only hints in GitHub Actions tools - Replaced instances of `requiredParam` with `RequiredParam` for consistency across all tools. - Updated `toBoolPtr` to `ToBoolPtr` in tool annotations to maintain uniformity in boolean pointer handling. - Ensured all tools in the GitHub Actions suite adhere to the new naming conventions for improved readability and maintainability. * docs: add missing actions toolset to Available Toolsets table * feat: enhance GitHub Actions tool descriptions with enumerated options - Updated descriptions for workflow run status and job filters to include enumerated options for clarity. - Improved documentation for better usability and understanding of available parameters. * feat: expand event type options in GitHub Actions tool descriptions - Enhanced the event parameter description in the ListWorkflowRuns function to include a comprehensive list of supported event types. - Improved clarity and usability for users by providing enumerated options for event types in the documentation. * feat: add support for running workflows by ID and filename in GitHub Actions tools - Introduced a new tool, RunWorkflowByFileName, to allow users to run workflows using the workflow filename. - Updated the existing RunWorkflow tool to accept a numeric workflow ID instead of a filename. - Enhanced tests to cover scenarios for both running workflows by ID and filename, including error handling for missing parameters. - Improved tool descriptions for clarity and usability. * feat: standardize repository parameter descriptions in GitHub Actions tools - Introduced constants for repository owner and name descriptions to enhance consistency across multiple tools. - Updated all relevant tools to use the new constants for improved clarity and maintainability in parameter descriptions. * feat: enhance GitHub Actions tools with user-friendly titles - Added user-friendly titles to tool annotations for various GitHub Actions tools, improving clarity and usability for end-users. - Updated descriptions for tools including ListWorkflows, ListWorkflowRuns, RunWorkflow, and others to include new titles for better identification and understanding of their functionalities. * feat: unify workflow execution in GitHub Actions tools - Refactored the RunWorkflow tool to accept both numeric workflow IDs and filenames, enhancing flexibility for users. - Updated the corresponding tests to reflect changes in parameter handling and added assertions for workflow type in responses. - Removed the separate RunWorkflowByFileName tool to streamline functionality and improve code maintainability. * fix: linting issues --- README.md | 111 +++- pkg/github/actions.go | 1223 ++++++++++++++++++++++++++++++++++++ pkg/github/actions_test.go | 1097 ++++++++++++++++++++++++++++++++ pkg/github/tools.go | 21 + 4 files changed, 2449 insertions(+), 3 deletions(-) create mode 100644 pkg/github/actions.go create mode 100644 pkg/github/actions_test.go diff --git a/README.md b/README.md index 5da2b1f8..0936749f 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,7 @@ The following sets of tools are available (all are on by default): | Toolset | Description | | ----------------------- | ------------------------------------------------------------- | +| `actions` | GitHub Actions workflows and CI/CD operations | | `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in | | `code_security` | Code scanning alerts and security features | | `issues` | Issue-related tools (create, read, update, comment) | @@ -283,12 +284,12 @@ To specify toolsets you want available to the LLM, you can pass an allow-list in 1. **Using Command Line Argument**: ```bash - github-mcp-server --toolsets repos,issues,pull_requests,code_security + github-mcp-server --toolsets repos,issues,pull_requests,actions,code_security ``` 2. **Using Environment Variable**: ```bash - GITHUB_TOOLSETS="repos,issues,pull_requests,code_security" ./github-mcp-server + GITHUB_TOOLSETS="repos,issues,pull_requests,actions,code_security" ./github-mcp-server ``` The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided. @@ -300,7 +301,7 @@ When using Docker, you can pass the toolsets as environment variables: ```bash docker run -i --rm \ -e GITHUB_PERSONAL_ACCESS_TOKEN= \ - -e GITHUB_TOOLSETS="repos,issues,pull_requests,code_security,experiments" \ + -e GITHUB_TOOLSETS="repos,issues,pull_requests,actions,code_security,experiments" \ ghcr.io/github/github-mcp-server ``` @@ -769,6 +770,110 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `page`: Page number (number, optional) - `perPage`: Results per page (number, optional) +### Actions + +- **list_workflows** - List workflows in a repository + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `page`: Page number (number, optional) + - `perPage`: Results per page (number, optional) + +- **list_workflow_runs** - List workflow runs for a specific workflow + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `workflow_id`: Workflow ID or filename (string, required) + - `branch`: Filter by branch name (string, optional) + - `event`: Filter by event type (string, optional) + - `status`: Filter by run status (string, optional) + - `page`: Page number (number, optional) + - `perPage`: Results per page (number, optional) + +- **run_workflow** - Trigger a workflow via workflow_dispatch event + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `workflow_id`: Workflow ID or filename (string, required) + - `ref`: Git reference (branch, tag, or SHA) (string, required) + - `inputs`: Input parameters for the workflow (object, optional) + +- **get_workflow_run** - Get details of a specific workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + +- **get_workflow_run_logs** - Download logs for a workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + +- **list_workflow_jobs** - List jobs for a workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + - `filter`: Filter by job status (string, optional) + - `page`: Page number (number, optional) + - `perPage`: Results per page (number, optional) + +- **get_job_logs** - Download logs for a specific workflow job or efficiently get all failed job logs for a workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `job_id`: Job ID (number, required for single job logs) + - `run_id`: Workflow run ID (number, required when using failed_only) + - `failed_only`: When true, gets logs for all failed jobs in run_id (boolean, optional) + - `return_content`: Returns actual log content instead of URLs (boolean, optional) + +- **rerun_workflow_run** - Re-run an entire workflow + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + - `enable_debug_logging`: Enable debug logging for the re-run (boolean, optional) + +- **rerun_failed_jobs** - Re-run only the failed jobs in a workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + - `enable_debug_logging`: Enable debug logging for the re-run (boolean, optional) + +- **cancel_workflow_run** - Cancel a running workflow + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + +- **list_workflow_run_artifacts** - List artifacts from a workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + - `page`: Page number (number, optional) + - `perPage`: Results per page (number, optional) + +- **download_workflow_run_artifact** - Get download URL for a specific artifact + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `artifact_id`: Artifact ID (number, required) + +- **delete_workflow_run_logs** - Delete logs for a workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + +- **get_workflow_run_usage** - Get usage metrics for a workflow run + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `run_id`: Workflow run ID (number, required) + ### Code Scanning - **get_code_scanning_alert** - Get a code scanning alert diff --git a/pkg/github/actions.go b/pkg/github/actions.go new file mode 100644 index 00000000..527a426e --- /dev/null +++ b/pkg/github/actions.go @@ -0,0 +1,1223 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v72/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + DescriptionRepositoryOwner = "Repository owner" + DescriptionRepositoryName = "Repository name" +) + +// ListWorkflows creates a tool to list workflows in a repository +func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_workflows", + mcp.WithDescription(t("TOOL_LIST_WORKFLOWS_DESCRIPTION", "List workflows in a repository")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_WORKFLOWS_USER_TITLE", "List workflows"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("per_page", + mcp.Description("The number of results per page (max 100)"), + ), + mcp.WithNumber("page", + mcp.Description("The page number of the results to fetch"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Get optional pagination parameters + perPage, err := OptionalIntParam(request, "per_page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + page, err := OptionalIntParam(request, "page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Set up list options + opts := &github.ListOptions{ + PerPage: perPage, + Page: page, + } + + workflows, resp, err := client.Actions.ListWorkflows(ctx, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("failed to list workflows: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(workflows) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// ListWorkflowRuns creates a tool to list workflow runs for a specific workflow +func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_workflow_runs", + mcp.WithDescription(t("TOOL_LIST_WORKFLOW_RUNS_DESCRIPTION", "List workflow runs for a specific workflow")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_WORKFLOW_RUNS_USER_TITLE", "List workflow runs"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithString("workflow_id", + mcp.Required(), + mcp.Description("The workflow ID or workflow file name"), + ), + mcp.WithString("actor", + mcp.Description("Returns someone's workflow runs. Use the login for the user who created the workflow run."), + ), + mcp.WithString("branch", + mcp.Description("Returns workflow runs associated with a branch. Use the name of the branch."), + ), + mcp.WithString("event", + mcp.Description("Returns workflow runs for a specific event type"), + mcp.Enum( + "branch_protection_rule", + "check_run", + "check_suite", + "create", + "delete", + "deployment", + "deployment_status", + "discussion", + "discussion_comment", + "fork", + "gollum", + "issue_comment", + "issues", + "label", + "merge_group", + "milestone", + "page_build", + "public", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "pull_request_target", + "push", + "registry_package", + "release", + "repository_dispatch", + "schedule", + "status", + "watch", + "workflow_call", + "workflow_dispatch", + "workflow_run", + ), + ), + mcp.WithString("status", + mcp.Description("Returns workflow runs with the check run status"), + mcp.Enum("queued", "in_progress", "completed", "requested", "waiting"), + ), + mcp.WithNumber("per_page", + mcp.Description("The number of results per page (max 100)"), + ), + mcp.WithNumber("page", + mcp.Description("The page number of the results to fetch"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + workflowID, err := RequiredParam[string](request, "workflow_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Get optional filtering parameters + actor, err := OptionalParam[string](request, "actor") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + branch, err := OptionalParam[string](request, "branch") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + event, err := OptionalParam[string](request, "event") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + status, err := OptionalParam[string](request, "status") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Get optional pagination parameters + perPage, err := OptionalIntParam(request, "per_page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + page, err := OptionalIntParam(request, "page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Set up list options + opts := &github.ListWorkflowRunsOptions{ + Actor: actor, + Branch: branch, + Event: event, + Status: status, + ListOptions: github.ListOptions{ + PerPage: perPage, + Page: page, + }, + } + + workflowRuns, resp, err := client.Actions.ListWorkflowRunsByFileName(ctx, owner, repo, workflowID, opts) + if err != nil { + return nil, fmt.Errorf("failed to list workflow runs: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(workflowRuns) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// RunWorkflow creates a tool to run an Actions workflow +func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("run_workflow", + mcp.WithDescription(t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow by workflow ID or filename")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_RUN_WORKFLOW_USER_TITLE", "Run workflow"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithString("workflow_id", + mcp.Required(), + mcp.Description("The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml)"), + ), + mcp.WithString("ref", + mcp.Required(), + mcp.Description("The git reference for the workflow. The reference can be a branch or tag name."), + ), + mcp.WithObject("inputs", + mcp.Description("Inputs the workflow accepts"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + workflowID, err := RequiredParam[string](request, "workflow_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + ref, err := RequiredParam[string](request, "ref") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Get optional inputs parameter + var inputs map[string]interface{} + if requestInputs, ok := request.GetArguments()["inputs"]; ok { + if inputsMap, ok := requestInputs.(map[string]interface{}); ok { + inputs = inputsMap + } + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + event := github.CreateWorkflowDispatchEventRequest{ + Ref: ref, + Inputs: inputs, + } + + var resp *github.Response + var workflowType string + + if workflowIDInt, parseErr := strconv.ParseInt(workflowID, 10, 64); parseErr == nil { + resp, err = client.Actions.CreateWorkflowDispatchEventByID(ctx, owner, repo, workflowIDInt, event) + workflowType = "workflow_id" + } else { + resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event) + workflowType = "workflow_file" + } + + if err != nil { + return nil, fmt.Errorf("failed to run workflow: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + result := map[string]any{ + "message": "Workflow run has been queued", + "workflow_type": workflowType, + "workflow_id": workflowID, + "ref": ref, + "inputs": inputs, + "status": resp.Status, + "status_code": resp.StatusCode, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// GetWorkflowRun creates a tool to get details of a specific workflow run +func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_workflow_run", + mcp.WithDescription(t("TOOL_GET_WORKFLOW_RUN_DESCRIPTION", "Get details of a specific workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_WORKFLOW_RUN_USER_TITLE", "Get workflow run"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + workflowRun, resp, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, runID) + if err != nil { + return nil, fmt.Errorf("failed to get workflow run: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(workflowRun) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// GetWorkflowRunLogs creates a tool to download logs for a specific workflow run +func GetWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_workflow_run_logs", + mcp.WithDescription(t("TOOL_GET_WORKFLOW_RUN_LOGS_DESCRIPTION", "Download logs for a specific workflow run (EXPENSIVE: downloads ALL logs as ZIP. Consider using get_job_logs with failed_only=true for debugging failed jobs)")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_WORKFLOW_RUN_LOGS_USER_TITLE", "Get workflow run logs"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Get the download URL for the logs + url, resp, err := client.Actions.GetWorkflowRunLogs(ctx, owner, repo, runID, 1) + if err != nil { + return nil, fmt.Errorf("failed to get workflow run logs: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + // Create response with the logs URL and information + result := map[string]any{ + "logs_url": url.String(), + "message": "Workflow run logs are available for download", + "note": "The logs_url provides a download link for the complete workflow run logs as a ZIP archive. You can download this archive to extract and examine individual job logs.", + "warning": "This downloads ALL logs as a ZIP file which can be large and expensive. For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id instead.", + "optimization_tip": "Use: get_job_logs with parameters {run_id: " + fmt.Sprintf("%d", runID) + ", failed_only: true} for more efficient failed job debugging", + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// ListWorkflowJobs creates a tool to list jobs for a specific workflow run +func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_workflow_jobs", + mcp.WithDescription(t("TOOL_LIST_WORKFLOW_JOBS_DESCRIPTION", "List jobs for a specific workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_WORKFLOW_JOBS_USER_TITLE", "List workflow jobs"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + mcp.WithString("filter", + mcp.Description("Filters jobs by their completed_at timestamp"), + mcp.Enum("latest", "all"), + ), + mcp.WithNumber("per_page", + mcp.Description("The number of results per page (max 100)"), + ), + mcp.WithNumber("page", + mcp.Description("The page number of the results to fetch"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + // Get optional filtering parameters + filter, err := OptionalParam[string](request, "filter") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Get optional pagination parameters + perPage, err := OptionalIntParam(request, "per_page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + page, err := OptionalIntParam(request, "page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Set up list options + opts := &github.ListWorkflowJobsOptions{ + Filter: filter, + ListOptions: github.ListOptions{ + PerPage: perPage, + Page: page, + }, + } + + jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, opts) + if err != nil { + return nil, fmt.Errorf("failed to list workflow jobs: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + // Add optimization tip for failed job debugging + response := map[string]any{ + "jobs": jobs, + "optimization_tip": "For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id=" + fmt.Sprintf("%d", runID) + " to get logs directly without needing to list jobs first", + } + + r, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// GetJobLogs creates a tool to download logs for a specific workflow job or efficiently get all failed job logs for a workflow run +func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_job_logs", + mcp.WithDescription(t("TOOL_GET_JOB_LOGS_DESCRIPTION", "Download logs for a specific workflow job or efficiently get all failed job logs for a workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get job logs"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("job_id", + mcp.Description("The unique identifier of the workflow job (required for single job logs)"), + ), + mcp.WithNumber("run_id", + mcp.Description("Workflow run ID (required when using failed_only)"), + ), + mcp.WithBoolean("failed_only", + mcp.Description("When true, gets logs for all failed jobs in run_id"), + ), + mcp.WithBoolean("return_content", + mcp.Description("Returns actual log content instead of URLs"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Get optional parameters + jobID, err := OptionalIntParam(request, "job_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID, err := OptionalIntParam(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + failedOnly, err := OptionalParam[bool](request, "failed_only") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + returnContent, err := OptionalParam[bool](request, "return_content") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Validate parameters + if failedOnly && runID == 0 { + return mcp.NewToolResultError("run_id is required when failed_only is true"), nil + } + if !failedOnly && jobID == 0 { + return mcp.NewToolResultError("job_id is required when failed_only is false"), nil + } + + if failedOnly && runID > 0 { + // Handle failed-only mode: get logs for all failed jobs in the workflow run + return handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent) + } else if jobID > 0 { + // Handle single job mode + return handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent) + } + + return mcp.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil + } +} + +// handleFailedJobLogs gets logs for all failed jobs in a workflow run +func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool) (*mcp.CallToolResult, error) { + // First, get all jobs for the workflow run + jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{ + Filter: "latest", + }) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to list workflow jobs: %v", err)), nil + } + defer func() { _ = resp.Body.Close() }() + + // Filter for failed jobs + var failedJobs []*github.WorkflowJob + for _, job := range jobs.Jobs { + if job.GetConclusion() == "failure" { + failedJobs = append(failedJobs, job) + } + } + + if len(failedJobs) == 0 { + result := map[string]any{ + "message": "No failed jobs found in this workflow run", + "run_id": runID, + "total_jobs": len(jobs.Jobs), + "failed_jobs": 0, + } + r, _ := json.Marshal(result) + return mcp.NewToolResultText(string(r)), nil + } + + // Collect logs for all failed jobs + var logResults []map[string]any + for _, job := range failedJobs { + jobResult, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent) + if err != nil { + // Continue with other jobs even if one fails + jobResult = map[string]any{ + "job_id": job.GetID(), + "job_name": job.GetName(), + "error": err.Error(), + } + } + logResults = append(logResults, jobResult) + } + + result := map[string]any{ + "message": fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)), + "run_id": runID, + "total_jobs": len(jobs.Jobs), + "failed_jobs": len(failedJobs), + "logs": logResults, + "return_format": map[string]bool{"content": returnContent, "urls": !returnContent}, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil +} + +// handleSingleJobLogs gets logs for a single job +func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool) (*mcp.CallToolResult, error) { + jobResult, err := getJobLogData(ctx, client, owner, repo, jobID, "", returnContent) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + r, err := json.Marshal(jobResult) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil +} + +// getJobLogData retrieves log data for a single job, either as URL or content +func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool) (map[string]any, error) { + // Get the download URL for the job logs + url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1) + if err != nil { + return nil, fmt.Errorf("failed to get job logs for job %d: %w", jobID, err) + } + defer func() { _ = resp.Body.Close() }() + + result := map[string]any{ + "job_id": jobID, + } + if jobName != "" { + result["job_name"] = jobName + } + + if returnContent { + // Download and return the actual log content + content, err := downloadLogContent(url.String()) + if err != nil { + return nil, fmt.Errorf("failed to download log content for job %d: %w", jobID, err) + } + result["logs_content"] = content + result["message"] = "Job logs content retrieved successfully" + } else { + // Return just the URL + result["logs_url"] = url.String() + result["message"] = "Job logs are available for download" + result["note"] = "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content." + } + + return result, nil +} + +// downloadLogContent downloads the actual log content from a GitHub logs URL +func downloadLogContent(logURL string) (string, error) { + httpResp, err := http.Get(logURL) //nolint:gosec // URLs are provided by GitHub API and are safe + if err != nil { + return "", fmt.Errorf("failed to download logs: %w", err) + } + defer func() { _ = httpResp.Body.Close() }() + + if httpResp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to download logs: HTTP %d", httpResp.StatusCode) + } + + content, err := io.ReadAll(httpResp.Body) + if err != nil { + return "", fmt.Errorf("failed to read log content: %w", err) + } + + // Clean up and format the log content for better readability + logContent := strings.TrimSpace(string(content)) + return logContent, nil +} + +// RerunWorkflowRun creates a tool to re-run an entire workflow run +func RerunWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("rerun_workflow_run", + mcp.WithDescription(t("TOOL_RERUN_WORKFLOW_RUN_DESCRIPTION", "Re-run an entire workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_RERUN_WORKFLOW_RUN_USER_TITLE", "Rerun workflow run"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) + if err != nil { + return nil, fmt.Errorf("failed to rerun workflow run: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + result := map[string]any{ + "message": "Workflow run has been queued for re-run", + "run_id": runID, + "status": resp.Status, + "status_code": resp.StatusCode, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// RerunFailedJobs creates a tool to re-run only the failed jobs in a workflow run +func RerunFailedJobs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("rerun_failed_jobs", + mcp.WithDescription(t("TOOL_RERUN_FAILED_JOBS_DESCRIPTION", "Re-run only the failed jobs in a workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_RERUN_FAILED_JOBS_USER_TITLE", "Rerun failed jobs"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Actions.RerunFailedJobsByID(ctx, owner, repo, runID) + if err != nil { + return nil, fmt.Errorf("failed to rerun failed jobs: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + result := map[string]any{ + "message": "Failed jobs have been queued for re-run", + "run_id": runID, + "status": resp.Status, + "status_code": resp.StatusCode, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// CancelWorkflowRun creates a tool to cancel a workflow run +func CancelWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("cancel_workflow_run", + mcp.WithDescription(t("TOOL_CANCEL_WORKFLOW_RUN_DESCRIPTION", "Cancel a workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_CANCEL_WORKFLOW_RUN_USER_TITLE", "Cancel workflow run"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID) + if err != nil { + return nil, fmt.Errorf("failed to cancel workflow run: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + result := map[string]any{ + "message": "Workflow run has been cancelled", + "run_id": runID, + "status": resp.Status, + "status_code": resp.StatusCode, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// ListWorkflowRunArtifacts creates a tool to list artifacts for a workflow run +func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_workflow_run_artifacts", + mcp.WithDescription(t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_DESCRIPTION", "List artifacts for a workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_USER_TITLE", "List workflow artifacts"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + mcp.WithNumber("per_page", + mcp.Description("The number of results per page (max 100)"), + ), + mcp.WithNumber("page", + mcp.Description("The page number of the results to fetch"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + // Get optional pagination parameters + perPage, err := OptionalIntParam(request, "per_page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + page, err := OptionalIntParam(request, "page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Set up list options + opts := &github.ListOptions{ + PerPage: perPage, + Page: page, + } + + artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, runID, opts) + if err != nil { + return nil, fmt.Errorf("failed to list workflow run artifacts: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(artifacts) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// DownloadWorkflowRunArtifact creates a tool to download a workflow run artifact +func DownloadWorkflowRunArtifact(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("download_workflow_run_artifact", + mcp.WithDescription(t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_DESCRIPTION", "Get download URL for a workflow run artifact")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_USER_TITLE", "Download workflow artifact"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("artifact_id", + mcp.Required(), + mcp.Description("The unique identifier of the artifact"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + artifactIDInt, err := RequiredInt(request, "artifact_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + artifactID := int64(artifactIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Get the download URL for the artifact + url, resp, err := client.Actions.DownloadArtifact(ctx, owner, repo, artifactID, 1) + if err != nil { + return nil, fmt.Errorf("failed to get artifact download URL: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + // Create response with the download URL and information + result := map[string]any{ + "download_url": url.String(), + "message": "Artifact is available for download", + "note": "The download_url provides a download link for the artifact as a ZIP archive. The link is temporary and expires after a short time.", + "artifact_id": artifactID, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// DeleteWorkflowRunLogs creates a tool to delete logs for a workflow run +func DeleteWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("delete_workflow_run_logs", + mcp.WithDescription(t("TOOL_DELETE_WORKFLOW_RUN_LOGS_DESCRIPTION", "Delete logs for a workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_USER_TITLE", "Delete workflow logs"), + ReadOnlyHint: ToBoolPtr(false), + DestructiveHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Actions.DeleteWorkflowRunLogs(ctx, owner, repo, runID) + if err != nil { + return nil, fmt.Errorf("failed to delete workflow run logs: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + result := map[string]any{ + "message": "Workflow run logs have been deleted", + "run_id": runID, + "status": resp.Status, + "status_code": resp.StatusCode, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// GetWorkflowRunUsage creates a tool to get usage metrics for a workflow run +func GetWorkflowRunUsage(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_workflow_run_usage", + mcp.WithDescription(t("TOOL_GET_WORKFLOW_RUN_USAGE_DESCRIPTION", "Get usage metrics for a workflow run")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_WORKFLOW_RUN_USAGE_USER_TITLE", "Get workflow usage"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description(DescriptionRepositoryOwner), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description(DescriptionRepositoryName), + ), + mcp.WithNumber("run_id", + mcp.Required(), + mcp.Description("The unique identifier of the workflow run"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runIDInt, err := RequiredInt(request, "run_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + runID := int64(runIDInt) + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + usage, resp, err := client.Actions.GetWorkflowRunUsageByID(ctx, owner, repo, runID) + if err != nil { + return nil, fmt.Errorf("failed to get workflow run usage: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(usage) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go new file mode 100644 index 00000000..388c0bbe --- /dev/null +++ b/pkg/github/actions_test.go @@ -0,0 +1,1097 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v72/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ListWorkflows(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := ListWorkflows(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "list_workflows", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "page") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful workflow listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsWorkflowsByOwnerByRepo, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + workflows := &github.Workflows{ + TotalCount: github.Ptr(2), + Workflows: []*github.Workflow{ + { + ID: github.Ptr(int64(123)), + Name: github.Ptr("CI"), + Path: github.Ptr(".github/workflows/ci.yml"), + State: github.Ptr("active"), + CreatedAt: &github.Timestamp{}, + UpdatedAt: &github.Timestamp{}, + URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/workflows/123"), + HTMLURL: github.Ptr("https://github.com/owner/repo/actions/workflows/ci.yml"), + BadgeURL: github.Ptr("https://github.com/owner/repo/workflows/CI/badge.svg"), + NodeID: github.Ptr("W_123"), + }, + { + ID: github.Ptr(int64(456)), + Name: github.Ptr("Deploy"), + Path: github.Ptr(".github/workflows/deploy.yml"), + State: github.Ptr("active"), + CreatedAt: &github.Timestamp{}, + UpdatedAt: &github.Timestamp{}, + URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/workflows/456"), + HTMLURL: github.Ptr("https://github.com/owner/repo/actions/workflows/deploy.yml"), + BadgeURL: github.Ptr("https://github.com/owner/repo/workflows/Deploy/badge.svg"), + NodeID: github.Ptr("W_456"), + }, + }, + } + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(workflows) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + }, + expectError: false, + }, + { + name: "missing required parameter owner", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "missing required parameter: owner", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := ListWorkflows(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response github.Workflows + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.NotNil(t, response.TotalCount) + assert.Greater(t, *response.TotalCount, 0) + assert.NotEmpty(t, response.Workflows) + }) + } +} + +func Test_RunWorkflow(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := RunWorkflow(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "run_workflow", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "workflow_id") + assert.Contains(t, tool.InputSchema.Properties, "ref") + assert.Contains(t, tool.InputSchema.Properties, "inputs") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "workflow_id", "ref"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful workflow run", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "workflow_id": "12345", + "ref": "main", + }, + expectError: false, + }, + { + name: "missing required parameter workflow_id", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "ref": "main", + }, + expectError: true, + expectedErrMsg: "missing required parameter: workflow_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := RunWorkflow(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.Equal(t, "Workflow run has been queued", response["message"]) + assert.Contains(t, response, "workflow_type") + }) + } +} + +func Test_RunWorkflow_WithFilename(t *testing.T) { + // Test the unified RunWorkflow function with filenames + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful workflow run by filename", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "workflow_id": "ci.yml", + "ref": "main", + }, + expectError: false, + }, + { + name: "successful workflow run by numeric ID as string", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "workflow_id": "12345", + "ref": "main", + }, + expectError: false, + }, + { + name: "missing required parameter workflow_id", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "ref": "main", + }, + expectError: true, + expectedErrMsg: "missing required parameter: workflow_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := RunWorkflow(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.Equal(t, "Workflow run has been queued", response["message"]) + assert.Contains(t, response, "workflow_type") + }) + } +} + +func Test_CancelWorkflowRun(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := CancelWorkflowRun(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "cancel_workflow_run", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "run_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful workflow run cancellation", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{ + Pattern: "/repos/owner/repo/actions/runs/12345/cancel", + Method: "POST", + }, + "", // Empty response body for 202 Accepted + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(12345), + }, + expectError: false, + }, + { + name: "missing required parameter run_id", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "missing required parameter: run_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := CancelWorkflowRun(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.Equal(t, "Workflow run has been cancelled", response["message"]) + assert.Equal(t, float64(12345), response["run_id"]) + }) + } +} + +func Test_ListWorkflowRunArtifacts(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := ListWorkflowRunArtifacts(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "list_workflow_run_artifacts", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "run_id") + assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "page") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful artifacts listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsRunsArtifactsByOwnerByRepoByRunId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + artifacts := &github.ArtifactList{ + TotalCount: github.Ptr(int64(2)), + Artifacts: []*github.Artifact{ + { + ID: github.Ptr(int64(1)), + NodeID: github.Ptr("A_1"), + Name: github.Ptr("build-artifacts"), + SizeInBytes: github.Ptr(int64(1024)), + URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/1"), + ArchiveDownloadURL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/1/zip"), + Expired: github.Ptr(false), + CreatedAt: &github.Timestamp{}, + UpdatedAt: &github.Timestamp{}, + ExpiresAt: &github.Timestamp{}, + WorkflowRun: &github.ArtifactWorkflowRun{ + ID: github.Ptr(int64(12345)), + RepositoryID: github.Ptr(int64(1)), + HeadRepositoryID: github.Ptr(int64(1)), + HeadBranch: github.Ptr("main"), + HeadSHA: github.Ptr("abc123"), + }, + }, + { + ID: github.Ptr(int64(2)), + NodeID: github.Ptr("A_2"), + Name: github.Ptr("test-results"), + SizeInBytes: github.Ptr(int64(512)), + URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/2"), + ArchiveDownloadURL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/2/zip"), + Expired: github.Ptr(false), + CreatedAt: &github.Timestamp{}, + UpdatedAt: &github.Timestamp{}, + ExpiresAt: &github.Timestamp{}, + WorkflowRun: &github.ArtifactWorkflowRun{ + ID: github.Ptr(int64(12345)), + RepositoryID: github.Ptr(int64(1)), + HeadRepositoryID: github.Ptr(int64(1)), + HeadBranch: github.Ptr("main"), + HeadSHA: github.Ptr("abc123"), + }, + }, + }, + } + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(artifacts) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(12345), + }, + expectError: false, + }, + { + name: "missing required parameter run_id", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "missing required parameter: run_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := ListWorkflowRunArtifacts(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response github.ArtifactList + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.NotNil(t, response.TotalCount) + assert.Greater(t, *response.TotalCount, int64(0)) + assert.NotEmpty(t, response.Artifacts) + }) + } +} + +func Test_DownloadWorkflowRunArtifact(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := DownloadWorkflowRunArtifact(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "download_workflow_run_artifact", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "artifact_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "artifact_id"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful artifact download URL", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/owner/repo/actions/artifacts/123/zip", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // GitHub returns a 302 redirect to the download URL + w.Header().Set("Location", "https://api.github.com/repos/owner/repo/actions/artifacts/123/download") + w.WriteHeader(http.StatusFound) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "artifact_id": float64(123), + }, + expectError: false, + }, + { + name: "missing required parameter artifact_id", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "missing required parameter: artifact_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := DownloadWorkflowRunArtifact(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.Contains(t, response, "download_url") + assert.Contains(t, response, "message") + assert.Equal(t, "Artifact is available for download", response["message"]) + assert.Equal(t, float64(123), response["artifact_id"]) + }) + } +} + +func Test_DeleteWorkflowRunLogs(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := DeleteWorkflowRunLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "delete_workflow_run_logs", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "run_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful logs deletion", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.DeleteReposActionsRunsLogsByOwnerByRepoByRunId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(12345), + }, + expectError: false, + }, + { + name: "missing required parameter run_id", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "missing required parameter: run_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := DeleteWorkflowRunLogs(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.Equal(t, "Workflow run logs have been deleted", response["message"]) + assert.Equal(t, float64(12345), response["run_id"]) + }) + } +} + +func Test_GetWorkflowRunUsage(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := GetWorkflowRunUsage(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "get_workflow_run_usage", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "run_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + }{ + { + name: "successful workflow run usage", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsRunsTimingByOwnerByRepoByRunId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + usage := &github.WorkflowRunUsage{ + Billable: &github.WorkflowRunBillMap{ + "UBUNTU": &github.WorkflowRunBill{ + TotalMS: github.Ptr(int64(120000)), + Jobs: github.Ptr(2), + JobRuns: []*github.WorkflowRunJobRun{ + { + JobID: github.Ptr(1), + DurationMS: github.Ptr(int64(60000)), + }, + { + JobID: github.Ptr(2), + DurationMS: github.Ptr(int64(60000)), + }, + }, + }, + }, + RunDurationMS: github.Ptr(int64(120000)), + } + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(usage) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(12345), + }, + expectError: false, + }, + { + name: "missing required parameter run_id", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "missing required parameter: run_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetWorkflowRunUsage(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + // Unmarshal and verify the result + var response github.WorkflowRunUsage + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.NotNil(t, response.RunDurationMS) + assert.NotNil(t, response.Billable) + }) + } +} + +func Test_GetJobLogs(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := GetJobLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "get_job_logs", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "job_id") + assert.Contains(t, tool.InputSchema.Properties, "run_id") + assert.Contains(t, tool.InputSchema.Properties, "failed_only") + assert.Contains(t, tool.InputSchema.Properties, "return_content") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedErrMsg string + checkResponse func(t *testing.T, response map[string]any) + }{ + { + name: "successful single job logs with URL", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsJobsLogsByOwnerByRepoByJobId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", "https://github.com/logs/job/123") + w.WriteHeader(http.StatusFound) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "job_id": float64(123), + }, + expectError: false, + checkResponse: func(t *testing.T, response map[string]any) { + assert.Equal(t, float64(123), response["job_id"]) + assert.Contains(t, response, "logs_url") + assert.Equal(t, "Job logs are available for download", response["message"]) + assert.Contains(t, response, "note") + }, + }, + { + name: "successful failed jobs logs", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsRunsJobsByOwnerByRepoByRunId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + jobs := &github.Jobs{ + TotalCount: github.Ptr(3), + Jobs: []*github.WorkflowJob{ + { + ID: github.Ptr(int64(1)), + Name: github.Ptr("test-job-1"), + Conclusion: github.Ptr("success"), + }, + { + ID: github.Ptr(int64(2)), + Name: github.Ptr("test-job-2"), + Conclusion: github.Ptr("failure"), + }, + { + ID: github.Ptr(int64(3)), + Name: github.Ptr("test-job-3"), + Conclusion: github.Ptr("failure"), + }, + }, + } + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(jobs) + }), + ), + mock.WithRequestMatchHandler( + mock.GetReposActionsJobsLogsByOwnerByRepoByJobId, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", "https://github.com/logs/job/"+r.URL.Path[len(r.URL.Path)-1:]) + w.WriteHeader(http.StatusFound) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(456), + "failed_only": true, + }, + expectError: false, + checkResponse: func(t *testing.T, response map[string]any) { + assert.Equal(t, float64(456), response["run_id"]) + assert.Equal(t, float64(3), response["total_jobs"]) + assert.Equal(t, float64(2), response["failed_jobs"]) + assert.Contains(t, response, "logs") + assert.Equal(t, "Retrieved logs for 2 failed jobs", response["message"]) + + logs, ok := response["logs"].([]interface{}) + assert.True(t, ok) + assert.Len(t, logs, 2) + }, + }, + { + name: "no failed jobs found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsRunsJobsByOwnerByRepoByRunId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + jobs := &github.Jobs{ + TotalCount: github.Ptr(2), + Jobs: []*github.WorkflowJob{ + { + ID: github.Ptr(int64(1)), + Name: github.Ptr("test-job-1"), + Conclusion: github.Ptr("success"), + }, + { + ID: github.Ptr(int64(2)), + Name: github.Ptr("test-job-2"), + Conclusion: github.Ptr("success"), + }, + }, + } + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(jobs) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(456), + "failed_only": true, + }, + expectError: false, + checkResponse: func(t *testing.T, response map[string]any) { + assert.Equal(t, "No failed jobs found in this workflow run", response["message"]) + assert.Equal(t, float64(456), response["run_id"]) + assert.Equal(t, float64(2), response["total_jobs"]) + assert.Equal(t, float64(0), response["failed_jobs"]) + }, + }, + { + name: "missing job_id when not using failed_only", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "job_id is required when failed_only is false", + }, + { + name: "missing run_id when using failed_only", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "failed_only": true, + }, + expectError: true, + expectedErrMsg: "run_id is required when failed_only is true", + }, + { + name: "missing required parameter owner", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "repo": "repo", + "job_id": float64(123), + }, + expectError: true, + expectedErrMsg: "missing required parameter: owner", + }, + { + name: "missing required parameter repo", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]any{ + "owner": "owner", + "job_id": float64(123), + }, + expectError: true, + expectedErrMsg: "missing required parameter: repo", + }, + { + name: "API error when getting single job logs", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsJobsLogsByOwnerByRepoByJobId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _ = json.NewEncoder(w).Encode(map[string]string{ + "message": "Not Found", + }) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "job_id": float64(999), + }, + expectError: true, + }, + { + name: "API error when listing workflow jobs for failed_only", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsRunsJobsByOwnerByRepoByRunId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _ = json.NewEncoder(w).Encode(map[string]string{ + "message": "Not Found", + }) + }), + ), + ), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(999), + "failed_only": true, + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.Equal(t, tc.expectError, result.IsError) + + // Parse the result and get the text content + textContent := getTextResult(t, result) + + if tc.expectedErrMsg != "" { + assert.Equal(t, tc.expectedErrMsg, textContent.Text) + return + } + + if tc.expectError { + // For API errors, just verify we got an error + assert.True(t, result.IsError) + return + } + + // Unmarshal and verify the result + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + + if tc.checkResponse != nil { + tc.checkResponse(t, response) + } + }) + } +} + +func Test_GetJobLogs_WithContentReturn(t *testing.T) { + // Test the return_content functionality with a mock HTTP server + logContent := "2023-01-01T10:00:00.000Z Starting job...\n2023-01-01T10:00:01.000Z Running tests...\n2023-01-01T10:00:02.000Z Job completed successfully" + + // Create a test server to serve log content + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(logContent)) + })) + defer testServer.Close() + + mockedClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposActionsJobsLogsByOwnerByRepoByJobId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", testServer.URL) + w.WriteHeader(http.StatusFound) + }), + ), + ) + + client := github.NewClient(mockedClient) + _, handler := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper) + + request := createMCPRequest(map[string]any{ + "owner": "owner", + "repo": "repo", + "job_id": float64(123), + "return_content": true, + }) + + result, err := handler(context.Background(), request) + require.NoError(t, err) + require.False(t, result.IsError) + + textContent := getTextResult(t, result) + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + + assert.Equal(t, float64(123), response["job_id"]) + assert.Equal(t, logContent, response["logs_content"]) + assert.Equal(t, "Job logs content retrieved successfully", response["message"]) + assert.NotContains(t, response, "logs_url") // Should not have URL when returning content +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 9569c439..ba540d22 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -111,6 +111,26 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(ManageRepositoryNotificationSubscription(getClient, t)), ) + actions := toolsets.NewToolset("actions", "GitHub Actions workflows and CI/CD operations"). + AddReadTools( + toolsets.NewServerTool(ListWorkflows(getClient, t)), + toolsets.NewServerTool(ListWorkflowRuns(getClient, t)), + toolsets.NewServerTool(GetWorkflowRun(getClient, t)), + toolsets.NewServerTool(GetWorkflowRunLogs(getClient, t)), + toolsets.NewServerTool(ListWorkflowJobs(getClient, t)), + toolsets.NewServerTool(GetJobLogs(getClient, t)), + toolsets.NewServerTool(ListWorkflowRunArtifacts(getClient, t)), + toolsets.NewServerTool(DownloadWorkflowRunArtifact(getClient, t)), + toolsets.NewServerTool(GetWorkflowRunUsage(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(RunWorkflow(getClient, t)), + toolsets.NewServerTool(RerunWorkflowRun(getClient, t)), + toolsets.NewServerTool(RerunFailedJobs(getClient, t)), + toolsets.NewServerTool(CancelWorkflowRun(getClient, t)), + toolsets.NewServerTool(DeleteWorkflowRunLogs(getClient, t)), + ) + // Keep experiments alive so the system doesn't error out when it's always enabled experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet") @@ -125,6 +145,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(issues) tsg.AddToolset(users) tsg.AddToolset(pullRequests) + tsg.AddToolset(actions) tsg.AddToolset(codeSecurity) tsg.AddToolset(secretProtection) tsg.AddToolset(notifications) From f51096d0f1f5ef1431f60d3c15414b746c6dbe80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:07:41 +0200 Subject: [PATCH 17/19] build(deps): bump github.com/mark3labs/mcp-go from 0.31.0 to 0.32.0 (#528) * build(deps): bump github.com/mark3labs/mcp-go from 0.31.0 to 0.32.0 Bumps [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) from 0.31.0 to 0.32.0. - [Release notes](https://github.com/mark3labs/mcp-go/releases) - [Commits](https://github.com/mark3labs/mcp-go/compare/v0.31.0...v0.32.0) --- updated-dependencies: - dependency-name: github.com/mark3labs/mcp-go dependency-version: 0.32.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * updating licenses --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tony Truong --- go.mod | 2 +- go.sum | 4 ++-- third-party-licenses.darwin.md | 2 +- third-party-licenses.linux.md | 2 +- third-party-licenses.windows.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d2f28d7d..9cee56b5 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.7 require ( github.com/google/go-github/v72 v72.0.0 github.com/josephburnett/jd v1.9.2 - github.com/mark3labs/mcp-go v0.31.0 + github.com/mark3labs/mcp-go v0.32.0 github.com/migueleliasweb/go-github-mock v1.3.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index a8a950e9..5e601d90 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4= -github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= +github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88= github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index e182c63c..e616fa56 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -20,7 +20,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE)) - [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md)) - [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.32.0/LICENSE)) - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index e182c63c..e616fa56 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -20,7 +20,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE)) - [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md)) - [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.32.0/LICENSE)) - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index d8bfd492..d34ce244 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -21,7 +21,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE)) - [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md)) - [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.32.0/LICENSE)) - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) From 87a477037fa1b48016635d4447523c65918ef5fe Mon Sep 17 00:00:00 2001 From: Andrew Kwon Date: Wed, 18 Jun 2025 15:15:38 -0700 Subject: [PATCH 18/19] Remove tool desc `add_pull_request_review_comment` and `create_pull_request_review` - Reason for tool removal can be found in https://github.com/github/github-mcp-server/pull/410 --- README.md | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/README.md b/README.md index 0936749f..14596650 100644 --- a/README.md +++ b/README.md @@ -564,18 +564,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `repo`: Repository name (string, required) - `pullNumber`: Pull request number (number, required) -- **create_pull_request_review** - Create a review on a pull request review - - - `owner`: Repository owner (string, required) - - `repo`: Repository name (string, required) - - `pullNumber`: Pull request number (number, required) - - `body`: Review comment text (string, optional) - - `event`: Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT') (string, required) - - `commitId`: SHA of commit to review (string, optional) - - `comments`: Line-specific comments array of objects to place comments on pull request changes (array, optional) - - For inline comments: provide `path`, `position` (or `line`), and `body` - - For multi-line comments: provide `path`, `start_line`, `line`, optional `side`/`start_side`, and `body` - - **create_pending_pull_request_review** - Create a pending review for a pull request that can be submitted later - `owner`: Repository owner (string, required) @@ -634,21 +622,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `draft`: Create as draft PR (boolean, optional) - `maintainer_can_modify`: Allow maintainer edits (boolean, optional) -- **add_pull_request_review_comment** - Add a review comment to a pull request or reply to an existing comment - - - `owner`: Repository owner (string, required) - - `repo`: Repository name (string, required) - - `pull_number`: Pull request number (number, required) - - `body`: The text of the review comment (string, required) - - `commit_id`: The SHA of the commit to comment on (string, required unless using in_reply_to) - - `path`: The relative path to the file that necessitates a comment (string, required unless using in_reply_to) - - `line`: The line of the blob in the pull request diff that the comment applies to (number, optional) - - `side`: The side of the diff to comment on (LEFT or RIGHT) (string, optional) - - `start_line`: For multi-line comments, the first line of the range (number, optional) - - `start_side`: For multi-line comments, the starting side of the diff (LEFT or RIGHT) (string, optional) - - `subject_type`: The level at which the comment is targeted (line or file) (string, optional) - - `in_reply_to`: The ID of the review comment to reply to (number, optional). When specified, only body is required and other parameters are ignored. - - **update_pull_request** - Update an existing pull request in a GitHub repository - `owner`: Repository owner (string, required) From 3fe88ee5e00528ef7416da719a163556307dd169 Mon Sep 17 00:00:00 2001 From: Dimitrios Philliou Date: Fri, 20 Jun 2025 10:43:41 -0700 Subject: [PATCH 19/19] Update feature_request.md Add prompt to feature request template for tool proposals to include example workflows/prompts --- .github/ISSUE_TEMPLATE/feature_request.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 1ac04f67..9b6c6ea8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -15,6 +15,10 @@ A clear and concise description of what the feature or problem is. How will it benefit GitHub MCP Server and its users? +### Example prompts or workflows (for tools/toolsets only) + +If it's a new tool or improvement, share 3–5 example prompts or workflows it would enable. Just enough detail to show the value. Clear, valuable use cases are more likely to get approved. + ### Additional context -Add any other context like screenshots or mockups are helpful, if applicable. \ No newline at end of file +Add any other context like screenshots or mockups are helpful, if applicable.