From 92303d4bcdf62b0888dccd0766fbecc74f811f68 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Fri, 4 Apr 2025 10:20:29 -0700 Subject: [PATCH 01/18] Fix VS Code inputs link (#103) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a52676f0..56d16b60 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The GitHub MCP Server is a [Model Context Protocol (MCP)](https://modelcontextpr server that provides seamless integration with GitHub APIs, enabling advanced automation and interaction capabilities for developers and tools. -[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D&quality=insiders) +[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D&quality=insiders) ## Use Cases From 17e4bd414ed7adcc84812346a2a220a553e82aef Mon Sep 17 00:00:00 2001 From: Ariel Deitcher <1149246+mntlty@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:40:01 -0700 Subject: [PATCH 02/18] add vscode launch configuration (#104) --- .gitignore | 1 + .vscode/launch.json | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index 243e145c..9fb1dca9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ cmd/github-mcp-server/github-mcp-server # Added by goreleaser init: dist/ +__debug_bin* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..cea7fd91 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch stdio server", + "type": "go", + "request": "launch", + "mode": "auto", + "cwd": "${workspaceFolder}", + "program": "cmd/github-mcp-server/main.go", + "args": ["stdio"], + "console": "integratedTerminal", + }, + { + "name": "Launch stdio server (read-only)", + "type": "go", + "request": "launch", + "mode": "auto", + "cwd": "${workspaceFolder}", + "program": "cmd/github-mcp-server/main.go", + "args": ["stdio", "--read-only"], + "console": "integratedTerminal", + } + ] +} \ No newline at end of file From 1a6e33ea0c3d612a180cf03056a1915abdf4eda8 Mon Sep 17 00:00:00 2001 From: Chiedo John <2156688+chiedo@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:52:16 -0400 Subject: [PATCH 03/18] Make pre-requisites more scannable (#107) --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 56d16b60..9330723c 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,11 @@ automation and interaction capabilities for developers and tools. ## Prerequisites -[Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). -The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools. +1. To run the server in a container, you will need to have [Docker](https://www.docker.com/) installed. +2. [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). +The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools (to learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)). -To learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). -To run the server in a container, you will need to have [Docker](https://www.docker.com/) installed. ## Installation From 6716423a15452602d02f8e7a530dbf39bb2e76a6 Mon Sep 17 00:00:00 2001 From: Jeffrey Luszcz <160953253+Jeffrey-Luszcz@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:25:02 -0400 Subject: [PATCH 04/18] Update CONTRIBUTING.md to fix link to LICENSE file (#109) The template pointed to LICENSE.md but this repo is using LICENSE updated link to point to mcp's license file --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75aa991c..7e176e70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. -Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.txt). +Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE). Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. From 86b0dcdd443cf3685ee5315148cc60076a601983 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher <1149246+mntlty@users.noreply.github.com> Date: Fri, 4 Apr 2025 17:45:14 -0700 Subject: [PATCH 05/18] feature request template (#115) --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..1ac04f67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: "⭐ Submit a feature request" +about: Surface a feature or problem that you think should be solved +title: '' +labels: enhancement +assignees: '' + +--- + +### Describe the feature or problem you’d like to solve + +A clear and concise description of what the feature or problem is. + +### Proposed solution + +How will it benefit GitHub MCP Server and its users? + +### Additional context + +Add any other context like screenshots or mockups are helpful, if applicable. \ No newline at end of file From 270bbf7ad4c9ef4ce05cbd3a74eb7150d647b194 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher <1149246+mntlty@users.noreply.github.com> Date: Fri, 4 Apr 2025 17:59:15 -0700 Subject: [PATCH 06/18] pretty print json (#114) --- cmd/github-mcp-server/main.go | 54 +++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 8620a0fa..f3dff6d3 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "context" + "encoding/json" "fmt" "io" stdlog "log" @@ -39,12 +41,20 @@ var ( logFile := viper.GetString("log-file") readOnly := viper.GetBool("read-only") exportTranslations := viper.GetBool("export-translations") + prettyPrintJSON := viper.GetBool("pretty-print-json") logger, err := initLogger(logFile) if err != nil { stdlog.Fatal("Failed to initialize logger:", err) } logCommands := viper.GetBool("enable-command-logging") - if err := runStdioServer(readOnly, logger, logCommands, exportTranslations); err != nil { + cfg := runConfig{ + readOnly: readOnly, + logger: logger, + logCommands: logCommands, + exportTranslations: exportTranslations, + prettyPrintJSON: prettyPrintJSON, + } + if err := runStdioServer(cfg); err != nil { stdlog.Fatal("failed to run stdio server:", err) } }, @@ -60,6 +70,7 @@ func init() { rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file") rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file") rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)") + rootCmd.PersistentFlags().Bool("pretty-print-json", false, "Pretty print JSON output") // Bind flag to viper _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only")) @@ -67,6 +78,7 @@ func init() { _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging")) _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations")) _ = viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host")) + _ = viper.BindPFlag("pretty-print-json", rootCmd.PersistentFlags().Lookup("pretty-print-json")) // Add subcommands rootCmd.AddCommand(stdioCmd) @@ -95,7 +107,28 @@ func initLogger(outPath string) (*log.Logger, error) { return logger, nil } -func runStdioServer(readOnly bool, logger *log.Logger, logCommands bool, exportTranslations bool) error { +type runConfig struct { + readOnly bool + logger *log.Logger + logCommands bool + exportTranslations bool + prettyPrintJSON bool +} + +// JSONPrettyPrintWriter is a Writer that pretty prints input to indented JSON +type JSONPrettyPrintWriter struct { + writer io.Writer +} + +func (j JSONPrettyPrintWriter) Write(p []byte) (n int, err error) { + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, p, "", "\t"); err != nil { + return 0, err + } + return j.writer.Write(prettyJSON.Bytes()) +} + +func runStdioServer(cfg runConfig) error { // Create app context ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -103,7 +136,7 @@ func runStdioServer(readOnly bool, logger *log.Logger, logCommands bool, exportT // Create GH client token := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN") if token == "" { - logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set") + cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set") } ghClient := gogithub.NewClient(nil).WithAuthToken(token) ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version) @@ -125,13 +158,13 @@ func runStdioServer(readOnly bool, logger *log.Logger, logCommands bool, exportT t, dumpTranslations := translations.TranslationHelper() // Create - ghServer := github.NewServer(ghClient, readOnly, t) + ghServer := github.NewServer(ghClient, cfg.readOnly, t) stdioServer := server.NewStdioServer(ghServer) - stdLogger := stdlog.New(logger.Writer(), "stdioserver", 0) + stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) stdioServer.SetErrorLogger(stdLogger) - if exportTranslations { + if cfg.exportTranslations { // Once server is initialized, all translations are loaded dumpTranslations() } @@ -141,11 +174,14 @@ func runStdioServer(readOnly bool, logger *log.Logger, logCommands bool, exportT go func() { in, out := io.Reader(os.Stdin), io.Writer(os.Stdout) - if logCommands { - loggedIO := iolog.NewIOLogger(in, out, logger) + if cfg.logCommands { + loggedIO := iolog.NewIOLogger(in, out, cfg.logger) in, out = loggedIO, loggedIO } + if cfg.prettyPrintJSON { + out = JSONPrettyPrintWriter{writer: out} + } errC <- stdioServer.Listen(ctx, in, out) }() @@ -155,7 +191,7 @@ func runStdioServer(readOnly bool, logger *log.Logger, logCommands bool, exportT // Wait for shutdown signal select { case <-ctx.Done(): - logger.Infof("shutting down server...") + cfg.logger.Infof("shutting down server...") case err := <-errC: if err != nil { return fmt.Errorf("error running server: %w", err) From 6b02799482043cb130da3c67334c5d4f8e0068cc Mon Sep 17 00:00:00 2001 From: Javier Uruen Val Date: Sat, 5 Apr 2025 20:05:35 +0200 Subject: [PATCH 07/18] fix issue with pagination in search_repositories (#129) --- pkg/github/search.go | 2 +- pkg/github/search_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/github/search.go b/pkg/github/search.go index f9a20be1..a98c2643 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -36,7 +36,7 @@ func searchRepositories(client *github.Client, t translations.TranslationHelperF if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + perPage, err := optionalIntParamWithDefault(request, "perPage", 30) if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index 2485c4c2..9d02b3a2 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -72,9 +72,9 @@ func Test_SearchRepositories(t *testing.T) { ), ), requestArgs: map[string]interface{}{ - "query": "golang test", - "page": float64(2), - "per_page": float64(10), + "query": "golang test", + "page": float64(2), + "perPage": float64(10), }, expectError: false, expectedResult: mockSearchResult, From e6b73f7d5a8dc448402cf863c4ef32f52d964d27 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 7 Apr 2025 11:49:32 +0200 Subject: [PATCH 08/18] chore: add code scanning advanced setup yml --- .github/workflows/code-scanning.yml | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/code-scanning.yml diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml new file mode 100644 index 00000000..83d2c30b --- /dev/null +++ b/.github/workflows/code-scanning.yml @@ -0,0 +1,82 @@ +name: "CodeQL" +run-name: ${{ github.event.inputs.code_scanning_run_name }} +on: [push, pull_request, workflow_dispatch] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CODE_SCANNING_REF: ${{ github.event.inputs.code_scanning_ref }} + CODE_SCANNING_BASE_BRANCH: ${{ github.event.inputs.code_scanning_base_branch }} + CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH: ${{ github.event.inputs.code_scanning_is_analyzing_default_branch }} + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ${{ fromJSON(matrix.runner) }} + permissions: + actions: read + contents: read + packages: read + security-events: write + continue-on-error: false + strategy: + fail-fast: false + matrix: + include: + - language: actions + category: /language:actions + build-mode: none + runner: '["ubuntu-22.04"]' + - language: go + category: /language:go + build-mode: autobuild + runner: '["ubuntu-22.04"]' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + dependency-caching: ${{ runner.environment == 'github-hosted' }} + queries: "" # Default query suite + packs: github/ccr-${{ matrix.language }}-queries + config: | + default-setup: + org: + model-packs: [ ${{ github.event.inputs.code_scanning_codeql_packs }} ] + threat-models: [ ] + - name: Setup proxy for registries + id: proxy + uses: github/codeql-action/start-proxy@v3 + with: + registries_credentials: ${{ secrets.GITHUB_REGISTRIES_PROXY }} + language: ${{ matrix.language }} + + - name: Configure + uses: github/codeql-action/resolve-environment@v3 + id: resolve-environment + with: + language: ${{ matrix.language }} + - name: Setup Go + uses: actions/setup-go@v5 + if: matrix.language == 'go' && fromJSON(steps.resolve-environment.outputs.environment).configuration.go.version + with: + go-version: ${{ fromJSON(steps.resolve-environment.outputs.environment).configuration.go.version }} + cache: false + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + env: + CODEQL_PROXY_HOST: ${{ steps.proxy.outputs.proxy_host }} + CODEQL_PROXY_PORT: ${{ steps.proxy.outputs.proxy_port }} + CODEQL_PROXY_CA_CERTIFICATE: ${{ steps.proxy.outputs.proxy_ca_certificate }} + with: + category: ${{ matrix.category }} From c77ea94c0270f213382b06f635b5002170b08d32 Mon Sep 17 00:00:00 2001 From: Matt Metzger Date: Mon, 7 Apr 2025 10:11:00 -0400 Subject: [PATCH 09/18] Add support for retrieving GitHub Issue Comments (#106) * Add support for retrieving GitHub Issue Comments Add new tool 'get_issue_comments' that allows fetching comments associated with GitHub issues. This complements the existing issue retrieval functionality and follows the same patterns as the pull request comments implementation. The implementation includes: - New getIssueComments function in pkg/github/issues.go - Tool registration in server.go - Comprehensive test coverage in issues_test.go * Support pagination for get_issue_comments --------- Co-authored-by: Javier Uruen Val --- README.md | 6 ++ cmd/mcpcurl/README.md | 1 + pkg/github/issues.go | 75 +++++++++++++++++++++ pkg/github/issues_test.go | 134 ++++++++++++++++++++++++++++++++++++++ pkg/github/server.go | 1 + 5 files changed, 217 insertions(+) diff --git a/README.md b/README.md index 9330723c..64303639 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `repo`: Repository name (string, required) - `issue_number`: Issue number (number, required) +- **get_issue_comments** - Get comments for a GitHub issue + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `issue_number`: Issue number (number, required) + - **create_issue** - Create a new issue in a GitHub repository - `owner`: Repository owner (string, required) diff --git a/cmd/mcpcurl/README.md b/cmd/mcpcurl/README.md index 06ce26ee..95e1339a 100644 --- a/cmd/mcpcurl/README.md +++ b/cmd/mcpcurl/README.md @@ -50,6 +50,7 @@ Available Commands: fork_repository Fork a GitHub repository to your account or specified organization get_file_contents Get the contents of a file or directory from a GitHub repository get_issue Get details of a specific issue in a GitHub repository. + get_issue_comments Get comments for a GitHub issue list_commits Get list of commits of a branch in a GitHub repository list_issues List issues in a GitHub repository with filtering options push_files Push multiple files to a GitHub repository in a single commit diff --git a/pkg/github/issues.go b/pkg/github/issues.go index df2f6f58..d5aba2e7 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -597,6 +597,81 @@ func updateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } } +// getIssueComments creates a tool to get comments for a GitHub issue. +func getIssueComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_issue_comments", + mcp.WithDescription(t("TOOL_GET_ISSUE_COMMENTS_DESCRIPTION", "Get comments for a GitHub issue")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Issue number"), + ), + mcp.WithNumber("page", + mcp.Description("Page number"), + ), + mcp.WithNumber("per_page", + mcp.Description("Number of records per page"), + ), + ), + 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 + } + issueNumber, err := requiredInt(request, "issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + page, err := optionalIntParamWithDefault(request, "page", 1) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + opts := &github.IssueListCommentsOptions{ + ListOptions: github.ListOptions{ + Page: page, + PerPage: perPage, + }, + } + + comments, resp, err := client.Issues.ListComments(ctx, owner, repo, issueNumber, opts) + if err != nil { + return nil, fmt.Errorf("failed to get issue comments: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to get issue comments: %s", string(body))), nil + } + + r, err := json.Marshal(comments) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object. // Returns the parsed time or an error if parsing fails. // Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15" diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 5dab1631..0326b9be 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -984,3 +984,137 @@ func Test_ParseISOTimestamp(t *testing.T) { }) } } + +func Test_GetIssueComments(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := getIssueComments(mockClient, translations.NullTranslationHelper) + + assert.Equal(t, "get_issue_comments", 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, "issue_number") + assert.Contains(t, tool.InputSchema.Properties, "page") + assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number"}) + + // Setup mock comments for success case + mockComments := []*github.IssueComment{ + { + ID: github.Ptr(int64(123)), + Body: github.Ptr("This is the first comment"), + User: &github.User{ + Login: github.Ptr("user1"), + }, + CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour * 24)}, + }, + { + ID: github.Ptr(int64(456)), + Body: github.Ptr("This is the second comment"), + User: &github.User{ + Login: github.Ptr("user2"), + }, + CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour)}, + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedComments []*github.IssueComment + expectedErrMsg string + }{ + { + name: "successful comments retrieval", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, + mockComments, + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(42), + }, + expectError: false, + expectedComments: mockComments, + }, + { + name: "successful comments retrieval with pagination", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, + expectQueryParams(t, map[string]string{ + "page": "2", + "per_page": "10", + }).andThen( + mockResponse(t, http.StatusOK, mockComments), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(42), + "page": float64(2), + "per_page": float64(10), + }, + expectError: false, + expectedComments: mockComments, + }, + { + name: "issue not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, + mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(999), + }, + expectError: true, + expectedErrMsg: "failed to get issue comments", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := getIssueComments(client, translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedComments []*github.IssueComment + err = json.Unmarshal([]byte(textContent.Text), &returnedComments) + require.NoError(t, err) + assert.Equal(t, len(tc.expectedComments), len(returnedComments)) + if len(returnedComments) > 0 { + assert.Equal(t, *tc.expectedComments[0].Body, *returnedComments[0].Body) + assert.Equal(t, *tc.expectedComments[0].User.Login, *returnedComments[0].User.Login) + } + }) + } +} diff --git a/pkg/github/server.go b/pkg/github/server.go index 66dbfd1c..18e5da09 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -34,6 +34,7 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH s.AddTool(getIssue(client, t)) s.AddTool(searchIssues(client, t)) s.AddTool(listIssues(client, t)) + s.AddTool(getIssueComments(client, t)) if !readOnly { s.AddTool(createIssue(client, t)) s.AddTool(addIssueComment(client, t)) From 31471a4b4f42b8c55d79a600f7c12a4b47900858 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Mon, 7 Apr 2025 07:20:17 -0700 Subject: [PATCH 10/18] Add line parameter support to create_pull_request_review tool (#118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add line parameter support to create_pull_request_review tool - Updated schema to make path and body the only required fields - Added line parameter as alternative to position for inline comments - Updated handler to accept either position or line based on GitHub API spec - Added new test case that verifies line parameter works properly - Updated error messages for better validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Expand PR review API with multi-line comment support - Added new parameters: line, side, start_line, start_side - Added proper validation for multi-line comment parameters - Improved validation logic to handle parameter combinations - Added test cases for regular and multi-line comments - Updated schema documentation for better tool discoverability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * gofmt --------- Co-authored-by: Claude Co-authored-by: Javier Uruen Val --- README.md | 4 +- pkg/github/pullrequests.go | 71 +++++++++++---- pkg/github/pullrequests_test.go | 147 +++++++++++++++++++++++++++++++- 3 files changed, 205 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 64303639..48bf019d 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,9 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `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, each object with path (string), position (number), and body (string) (array, 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_pull_request** - Create a new pull request diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index c02336ca..a43d5b88 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -593,7 +593,7 @@ func createPullRequestReview(client *github.Client, t translations.TranslationHe map[string]interface{}{ "type": "object", "additionalProperties": false, - "required": []string{"path", "position", "body"}, + "required": []string{"path", "body"}, "properties": map[string]interface{}{ "path": map[string]interface{}{ "type": "string", @@ -601,7 +601,23 @@ func createPullRequestReview(client *github.Client, t translations.TranslationHe }, "position": map[string]interface{}{ "type": "number", - "description": "line number in the file", + "description": "position of the comment in the diff", + }, + "line": map[string]interface{}{ + "type": "number", + "description": "line number in the file to comment on. For multi-line comments, the end of the line range", + }, + "side": map[string]interface{}{ + "type": "string", + "description": "The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. (LEFT or RIGHT)", + }, + "start_line": map[string]interface{}{ + "type": "number", + "description": "The first line of the range to which the comment refers. Required for multi-line comments.", + }, + "start_side": map[string]interface{}{ + "type": "string", + "description": "The side of the diff on which the start line resides for multi-line comments. (LEFT or RIGHT)", }, "body": map[string]interface{}{ "type": "string", @@ -610,7 +626,7 @@ func createPullRequestReview(client *github.Client, t translations.TranslationHe }, }, ), - mcp.Description("Line-specific comments array of objects, each object with path (string), position (number), and body (string)"), + mcp.Description("Line-specific comments array of objects to place comments on pull request changes. Requires path and body. For line comments use line or position. For multi-line comments use start_line and line with optional side parameters."), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -661,7 +677,7 @@ func createPullRequestReview(client *github.Client, t translations.TranslationHe for _, c := range commentsObj { commentMap, ok := c.(map[string]interface{}) if !ok { - return mcp.NewToolResultError("each comment must be an object with path, position, and body"), nil + return mcp.NewToolResultError("each comment must be an object with path and body"), nil } path, ok := commentMap["path"].(string) @@ -669,22 +685,47 @@ func createPullRequestReview(client *github.Client, t translations.TranslationHe return mcp.NewToolResultError("each comment must have a path"), nil } - positionFloat, ok := commentMap["position"].(float64) - if !ok { - return mcp.NewToolResultError("each comment must have a position"), nil - } - position := int(positionFloat) - body, ok := commentMap["body"].(string) if !ok || body == "" { return mcp.NewToolResultError("each comment must have a body"), nil } - comments = append(comments, &github.DraftReviewComment{ - Path: github.Ptr(path), - Position: github.Ptr(position), - Body: github.Ptr(body), - }) + _, hasPosition := commentMap["position"].(float64) + _, hasLine := commentMap["line"].(float64) + _, hasSide := commentMap["side"].(string) + _, hasStartLine := commentMap["start_line"].(float64) + _, hasStartSide := commentMap["start_side"].(string) + + switch { + case !hasPosition && !hasLine: + return mcp.NewToolResultError("each comment must have either position or line"), nil + case hasPosition && (hasLine || hasSide || hasStartLine || hasStartSide): + return mcp.NewToolResultError("position cannot be combined with line, side, start_line, or start_side"), nil + case hasStartSide && !hasSide: + return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil + } + + comment := &github.DraftReviewComment{ + Path: github.Ptr(path), + Body: github.Ptr(body), + } + + if positionFloat, ok := commentMap["position"].(float64); ok { + comment.Position = github.Ptr(int(positionFloat)) + } else if lineFloat, ok := commentMap["line"].(float64); ok { + comment.Line = github.Ptr(int(lineFloat)) + } + if side, ok := commentMap["side"].(string); ok { + comment.Side = github.Ptr(side) + } + if startLineFloat, ok := commentMap["start_line"].(float64); ok { + comment.StartLine = github.Ptr(int(startLineFloat)) + } + if startSide, ok := commentMap["start_side"].(string); ok { + comment.StartSide = github.Ptr(startSide) + } + + comments = append(comments, comment) } reviewRequest.Comments = comments diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index b666e8a8..9e2e9f47 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -1167,7 +1167,152 @@ func Test_CreatePullRequestReview(t *testing.T) { }, }, expectError: false, - expectedErrMsg: "each comment must have a position", + expectedErrMsg: "each comment must have either position or line", + }, + { + name: "successful review creation with line parameter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, + expectRequestBody(t, map[string]interface{}{ + "body": "Code review comments", + "event": "COMMENT", + "comments": []interface{}{ + map[string]interface{}{ + "path": "main.go", + "line": float64(42), + "body": "Consider adding a comment here", + }, + }, + }).andThen( + mockResponse(t, http.StatusOK, mockReview), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "body": "Code review comments", + "event": "COMMENT", + "comments": []interface{}{ + map[string]interface{}{ + "path": "main.go", + "line": float64(42), + "body": "Consider adding a comment here", + }, + }, + }, + expectError: false, + expectedReview: mockReview, + }, + { + name: "successful review creation with multi-line comment", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, + expectRequestBody(t, map[string]interface{}{ + "body": "Multi-line comment review", + "event": "COMMENT", + "comments": []interface{}{ + map[string]interface{}{ + "path": "main.go", + "start_line": float64(10), + "line": float64(15), + "side": "RIGHT", + "body": "This entire block needs refactoring", + }, + }, + }).andThen( + mockResponse(t, http.StatusOK, mockReview), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "body": "Multi-line comment review", + "event": "COMMENT", + "comments": []interface{}{ + map[string]interface{}{ + "path": "main.go", + "start_line": float64(10), + "line": float64(15), + "side": "RIGHT", + "body": "This entire block needs refactoring", + }, + }, + }, + expectError: false, + expectedReview: mockReview, + }, + { + name: "invalid multi-line comment - missing line parameter", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "event": "COMMENT", + "comments": []interface{}{ + map[string]interface{}{ + "path": "main.go", + "start_line": float64(10), + // missing line parameter + "body": "Invalid multi-line comment", + }, + }, + }, + expectError: false, + expectedErrMsg: "each comment must have either position or line", // Updated error message + }, + { + name: "invalid comment - mixing position with line parameters", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.PostReposPullsReviewsByOwnerByRepoByPullNumber, + mockReview, + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "event": "COMMENT", + "comments": []interface{}{ + map[string]interface{}{ + "path": "main.go", + "position": float64(5), + "line": float64(42), + "body": "Invalid parameter combination", + }, + }, + }, + expectError: false, + expectedErrMsg: "position cannot be combined with line, side, start_line, or start_side", + }, + { + name: "invalid multi-line comment - missing side parameter", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "event": "COMMENT", + "comments": []interface{}{ + map[string]interface{}{ + "path": "main.go", + "start_line": float64(10), + "line": float64(15), + "start_side": "LEFT", + // missing side parameter + "body": "Invalid multi-line comment", + }, + }, + }, + expectError: false, + expectedErrMsg: "if start_side is provided, side must also be provided", }, { name: "review creation fails", From e4c2f58ddcf7f2f61c20a175270688944f320bc7 Mon Sep 17 00:00:00 2001 From: Ohki Nozomu Date: Mon, 7 Apr 2025 23:29:30 +0900 Subject: [PATCH 11/18] Use github.Ptr instead of smithy-go/ptr (#147) Co-authored-by: Javier Uruen Val --- go.mod | 1 - go.sum | 2 - pkg/github/repositories.go | 7 +- third-party-licenses.darwin.md | 1 - third-party-licenses.linux.md | 1 - third-party-licenses.windows.md | 1 - .../github.com/aws/smithy-go/ptr/LICENSE | 175 ------------------ .../github.com/aws/smithy-go/ptr/NOTICE | 1 - 8 files changed, 3 insertions(+), 186 deletions(-) delete mode 100644 third-party/github.com/aws/smithy-go/ptr/LICENSE delete mode 100644 third-party/github.com/aws/smithy-go/ptr/NOTICE diff --git a/go.mod b/go.mod index cf96ca5d..858690cd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/github/github-mcp-server go 1.23.7 require ( - github.com/aws/smithy-go v1.22.3 github.com/docker/docker v28.0.4+incompatible github.com/google/go-cmp v0.7.0 github.com/google/go-github/v69 v69.2.0 diff --git a/go.sum b/go.sum index 450f6392..19d368de 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= -github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 112eb374..9c2ec3d8 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -7,7 +7,6 @@ import ( "io" "net/http" - "github.com/aws/smithy-go/ptr" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" @@ -152,9 +151,9 @@ func createOrUpdateFile(client *github.Client, t translations.TranslationHelperF // Create the file options opts := &github.RepositoryContentFileOptions{ - Message: ptr.String(message), + Message: github.Ptr(message), Content: contentBytes, - Branch: ptr.String(branch), + Branch: github.Ptr(branch), } // If SHA is provided, set it (for updates) @@ -163,7 +162,7 @@ func createOrUpdateFile(client *github.Client, t translations.TranslationHelperF return mcp.NewToolResultError(err.Error()), nil } if sha != "" { - opts.SHA = ptr.String(sha) + opts.SHA = github.Ptr(sha) } // Create or update the file diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 354101ed..80c6d1c4 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -7,7 +7,6 @@ The following open source dependencies are used to build the [github/github-mcp- Some packages may only be included on certain architectures or operating systems. - - [github.com/aws/smithy-go/ptr](https://pkg.go.dev/github.com/aws/smithy-go/ptr) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.3/LICENSE)) - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE)) - [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-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)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 354101ed..80c6d1c4 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -7,7 +7,6 @@ The following open source dependencies are used to build the [github/github-mcp- Some packages may only be included on certain architectures or operating systems. - - [github.com/aws/smithy-go/ptr](https://pkg.go.dev/github.com/aws/smithy-go/ptr) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.3/LICENSE)) - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE)) - [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-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)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index ff275242..5fc973d7 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -7,7 +7,6 @@ The following open source dependencies are used to build the [github/github-mcp- Some packages may only be included on certain architectures or operating systems. - - [github.com/aws/smithy-go/ptr](https://pkg.go.dev/github.com/aws/smithy-go/ptr) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.3/LICENSE)) - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE)) - [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-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)) diff --git a/third-party/github.com/aws/smithy-go/ptr/LICENSE b/third-party/github.com/aws/smithy-go/ptr/LICENSE deleted file mode 100644 index 67db8588..00000000 --- a/third-party/github.com/aws/smithy-go/ptr/LICENSE +++ /dev/null @@ -1,175 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. diff --git a/third-party/github.com/aws/smithy-go/ptr/NOTICE b/third-party/github.com/aws/smithy-go/ptr/NOTICE deleted file mode 100644 index 616fc588..00000000 --- a/third-party/github.com/aws/smithy-go/ptr/NOTICE +++ /dev/null @@ -1 +0,0 @@ -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. From 755b816b7c9cd519c8389a3ba50f1f13641c57a6 Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Sun, 6 Apr 2025 18:40:45 +0200 Subject: [PATCH 12/18] pkg/github: fix use of per page parameter Page size tool parameter names were changed to `perPage` within #90 while GitHub API uses `per_page` parameter name. This change fixes overlooked inconsistencies. Follow up on #90 Follow up on #129 Fixes #136 Signed-off-by: Alexander Yastrebov --- README.md | 2 +- pkg/github/issues.go | 8 ++++---- pkg/github/issues_test.go | 16 ++++++++-------- pkg/github/pullrequests.go | 4 ++-- pkg/github/pullrequests_test.go | 4 ++-- pkg/github/repositories.go | 2 +- pkg/github/repositories_test.go | 8 ++++---- pkg/github/search.go | 8 ++++---- pkg/github/search_test.go | 24 ++++++++++++------------ 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 48bf019d..cf53ce2a 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `sort`: Sort field (string, optional) - `order`: Sort order (string, optional) - `page`: Page number (number, optional) - - `per_page`: Results per page (number, optional) + - `perPage`: Results per page (number, optional) ### Pull Requests diff --git a/pkg/github/issues.go b/pkg/github/issues.go index d5aba2e7..05111fac 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -162,7 +162,7 @@ func searchIssues(client *github.Client, t translations.TranslationHelperFunc) ( mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - mcp.WithNumber("per_page", + mcp.WithNumber("perPage", mcp.Description("Results per page (max 100)"), mcp.Min(1), mcp.Max(100), @@ -185,7 +185,7 @@ func searchIssues(client *github.Client, t translations.TranslationHelperFunc) ( if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + perPage, err := optionalIntParamWithDefault(request, "perPage", 30) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -378,7 +378,7 @@ func listIssues(client *github.Client, t translations.TranslationHelperFunc) (to mcp.WithNumber("page", mcp.Description("Page number"), ), - mcp.WithNumber("per_page", + mcp.WithNumber("perPage", mcp.Description("Results per page"), ), ), @@ -432,7 +432,7 @@ func listIssues(client *github.Client, t translations.TranslationHelperFunc) (to opts.Page = int(page) } - if perPage, ok := request.Params.Arguments["per_page"].(float64); ok { + if perPage, ok := request.Params.Arguments["perPage"].(float64); ok { opts.PerPage = int(perPage) } diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 0326b9be..f5b5e163 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -244,7 +244,7 @@ func Test_SearchIssues(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "q") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"q"}) @@ -295,11 +295,11 @@ func Test_SearchIssues(t *testing.T) { ), ), requestArgs: map[string]interface{}{ - "q": "repo:owner/repo is:issue is:open", - "sort": "created", - "order": "desc", - "page": float64(1), - "per_page": float64(30), + "q": "repo:owner/repo is:issue is:open", + "sort": "created", + "order": "desc", + "page": float64(1), + "perPage": float64(30), }, expectError: false, expectedResult: mockSearchResult, @@ -567,7 +567,7 @@ func Test_ListIssues(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "direction") assert.Contains(t, tool.InputSchema.Properties, "since") assert.Contains(t, tool.InputSchema.Properties, "page") - assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) // Setup mock issues for success case @@ -641,7 +641,7 @@ func Test_ListIssues(t *testing.T) { "direction": "desc", "since": "2023-01-01T00:00:00Z", "page": float64(1), - "per_page": float64(30), + "perPage": float64(30), }, expectError: false, expectedIssues: mockIssues, diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index a43d5b88..e046d9bf 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -94,7 +94,7 @@ func listPullRequests(client *github.Client, t translations.TranslationHelperFun mcp.WithString("direction", mcp.Description("Sort direction ('asc', 'desc')"), ), - mcp.WithNumber("per_page", + mcp.WithNumber("perPage", mcp.Description("Results per page (max 100)"), ), mcp.WithNumber("page", @@ -130,7 +130,7 @@ func listPullRequests(client *github.Client, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + perPage, err := optionalIntParamWithDefault(request, "perPage", 30) if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 9e2e9f47..cf1afcdc 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -140,7 +140,7 @@ func Test_ListPullRequests(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "base") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "direction") - assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) @@ -190,7 +190,7 @@ func Test_ListPullRequests(t *testing.T) { "state": "all", "sort": "created", "direction": "desc", - "per_page": float64(30), + "perPage": float64(30), "page": float64(1), }, expectError: false, diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 9c2ec3d8..80d4c986 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -52,7 +52,7 @@ func listCommits(client *github.Client, t translations.TranslationHelperFunc) (t if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + perPage, err := optionalIntParamWithDefault(request, "perPage", 30) if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index bb6579f8..f7ed8e71 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -582,10 +582,10 @@ func Test_ListCommits(t *testing.T) { ), ), requestArgs: map[string]interface{}{ - "owner": "owner", - "repo": "repo", - "page": float64(2), - "per_page": float64(10), + "owner": "owner", + "repo": "repo", + "page": float64(2), + "perPage": float64(10), }, expectError: false, expectedCommits: mockCommits, diff --git a/pkg/github/search.go b/pkg/github/search.go index a98c2643..a8e3e728 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -86,7 +86,7 @@ func searchCode(client *github.Client, t translations.TranslationHelperFunc) (to mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - mcp.WithNumber("per_page", + mcp.WithNumber("perPage", mcp.Description("Results per page (max 100)"), mcp.Min(1), mcp.Max(100), @@ -109,7 +109,7 @@ func searchCode(client *github.Client, t translations.TranslationHelperFunc) (to if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + perPage, err := optionalIntParamWithDefault(request, "perPage", 30) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -166,7 +166,7 @@ func searchUsers(client *github.Client, t translations.TranslationHelperFunc) (t mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - mcp.WithNumber("per_page", + mcp.WithNumber("perPage", mcp.Description("Results per page (max 100)"), mcp.Min(1), mcp.Max(100), @@ -189,7 +189,7 @@ func searchUsers(client *github.Client, t translations.TranslationHelperFunc) (t if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + perPage, err := optionalIntParamWithDefault(request, "perPage", 30) if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index 9d02b3a2..bf1bff45 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -170,7 +170,7 @@ func Test_SearchCode(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "q") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"q"}) @@ -221,11 +221,11 @@ func Test_SearchCode(t *testing.T) { ), ), requestArgs: map[string]interface{}{ - "q": "fmt.Println language:go", - "sort": "indexed", - "order": "desc", - "page": float64(1), - "per_page": float64(30), + "q": "fmt.Println language:go", + "sort": "indexed", + "order": "desc", + "page": float64(1), + "perPage": float64(30), }, expectError: false, expectedResult: mockSearchResult, @@ -321,7 +321,7 @@ func Test_SearchUsers(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "q") assert.Contains(t, tool.InputSchema.Properties, "sort") assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "per_page") + assert.Contains(t, tool.InputSchema.Properties, "perPage") assert.Contains(t, tool.InputSchema.Properties, "page") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"q"}) @@ -376,11 +376,11 @@ func Test_SearchUsers(t *testing.T) { ), ), requestArgs: map[string]interface{}{ - "q": "location:finland language:go", - "sort": "followers", - "order": "desc", - "page": float64(1), - "per_page": float64(30), + "q": "location:finland language:go", + "sort": "followers", + "order": "desc", + "page": float64(1), + "perPage": float64(30), }, expectError: false, expectedResult: mockSearchResult, From a72070069f153bf956b6906181823b7aada20638 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 7 Apr 2025 12:49:04 +0200 Subject: [PATCH 13/18] Test issue search query params --- pkg/github/issues_test.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index f5b5e163..485169fd 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -289,9 +289,20 @@ func Test_SearchIssues(t *testing.T) { { name: "successful issues search with all parameters", mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( + mock.WithRequestMatchHandler( mock.GetSearchIssues, - mockSearchResult, + expectQueryParams( + t, + map[string]string{ + "q": "repo:owner/repo is:issue is:open", + "sort": "created", + "order": "desc", + "page": "1", + "per_page": "30", + }, + ).andThen( + mockResponse(t, http.StatusOK, mockSearchResult), + ), ), ), requestArgs: map[string]interface{}{ From 0f9ef6e708a0271b1e32f70bbf6b102fd3b9add7 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 7 Apr 2025 13:22:53 +0200 Subject: [PATCH 14/18] Add pagination helper --- pkg/github/issues.go | 27 +++---------- pkg/github/pullrequests.go | 17 ++------ pkg/github/repositories.go | 17 ++------ pkg/github/search.go | 57 ++++++-------------------- pkg/github/server.go | 42 +++++++++++++++++++ pkg/github/server_test.go | 83 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 93 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 05111fac..1632e9e8 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -162,15 +162,7 @@ func searchIssues(client *github.Client, t translations.TranslationHelperFunc) ( mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - mcp.WithNumber("perPage", - mcp.Description("Results per page (max 100)"), - mcp.Min(1), - mcp.Max(100), - ), - mcp.WithNumber("page", - mcp.Description("Page number"), - mcp.Min(1), - ), + withPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "q") @@ -185,11 +177,7 @@ func searchIssues(client *github.Client, t translations.TranslationHelperFunc) ( if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "perPage", 30) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - page, err := optionalIntParamWithDefault(request, "page", 1) + pagination, err := optionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -198,8 +186,8 @@ func searchIssues(client *github.Client, t translations.TranslationHelperFunc) ( Sort: sort, Order: order, ListOptions: github.ListOptions{ - PerPage: perPage, - Page: page, + PerPage: pagination.perPage, + Page: pagination.page, }, } @@ -375,12 +363,7 @@ func listIssues(client *github.Client, t translations.TranslationHelperFunc) (to mcp.WithString("since", mcp.Description("Filter by date (ISO 8601 timestamp)"), ), - mcp.WithNumber("page", - mcp.Description("Page number"), - ), - mcp.WithNumber("perPage", - mcp.Description("Results per page"), - ), + withPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index e046d9bf..25090cb7 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -94,12 +94,7 @@ func listPullRequests(client *github.Client, t translations.TranslationHelperFun mcp.WithString("direction", mcp.Description("Sort direction ('asc', 'desc')"), ), - mcp.WithNumber("perPage", - mcp.Description("Results per page (max 100)"), - ), - mcp.WithNumber("page", - mcp.Description("Page number"), - ), + withPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") @@ -130,11 +125,7 @@ func listPullRequests(client *github.Client, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "perPage", 30) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - page, err := optionalIntParamWithDefault(request, "page", 1) + pagination, err := optionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -146,8 +137,8 @@ func listPullRequests(client *github.Client, t translations.TranslationHelperFun Sort: sort, Direction: direction, ListOptions: github.ListOptions{ - PerPage: perPage, - Page: page, + PerPage: pagination.perPage, + Page: pagination.page, }, } diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 80d4c986..5b8725d1 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -28,12 +28,7 @@ func listCommits(client *github.Client, t translations.TranslationHelperFunc) (t mcp.WithString("sha", mcp.Description("Branch name"), ), - mcp.WithNumber("page", - mcp.Description("Page number"), - ), - mcp.WithNumber("perPage", - mcp.Description("Number of records per page"), - ), + withPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") @@ -48,11 +43,7 @@ func listCommits(client *github.Client, t translations.TranslationHelperFunc) (t if err != nil { return mcp.NewToolResultError(err.Error()), nil } - page, err := optionalIntParamWithDefault(request, "page", 1) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - perPage, err := optionalIntParamWithDefault(request, "perPage", 30) + pagination, err := optionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -60,8 +51,8 @@ func listCommits(client *github.Client, t translations.TranslationHelperFunc) (t opts := &github.CommitsListOptions{ SHA: sha, ListOptions: github.ListOptions{ - Page: page, - PerPage: perPage, + Page: pagination.page, + PerPage: pagination.perPage, }, } diff --git a/pkg/github/search.go b/pkg/github/search.go index a8e3e728..117e8298 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -20,31 +20,22 @@ func searchRepositories(client *github.Client, t translations.TranslationHelperF mcp.Required(), mcp.Description("Search query"), ), - mcp.WithNumber("page", - mcp.Description("Page number for pagination"), - ), - mcp.WithNumber("perPage", - mcp.Description("Results per page (max 100)"), - ), + withPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "query") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - page, err := optionalIntParamWithDefault(request, "page", 1) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - perPage, err := optionalIntParamWithDefault(request, "perPage", 30) + pagination, err := optionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } opts := &github.SearchOptions{ ListOptions: github.ListOptions{ - Page: page, - PerPage: perPage, + Page: pagination.page, + PerPage: pagination.perPage, }, } @@ -86,15 +77,7 @@ func searchCode(client *github.Client, t translations.TranslationHelperFunc) (to mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - mcp.WithNumber("perPage", - mcp.Description("Results per page (max 100)"), - mcp.Min(1), - mcp.Max(100), - ), - mcp.WithNumber("page", - mcp.Description("Page number"), - mcp.Min(1), - ), + withPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "q") @@ -109,11 +92,7 @@ func searchCode(client *github.Client, t translations.TranslationHelperFunc) (to if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "perPage", 30) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - page, err := optionalIntParamWithDefault(request, "page", 1) + pagination, err := optionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -122,8 +101,8 @@ func searchCode(client *github.Client, t translations.TranslationHelperFunc) (to Sort: sort, Order: order, ListOptions: github.ListOptions{ - PerPage: perPage, - Page: page, + PerPage: pagination.perPage, + Page: pagination.page, }, } @@ -166,15 +145,7 @@ func searchUsers(client *github.Client, t translations.TranslationHelperFunc) (t mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - mcp.WithNumber("perPage", - mcp.Description("Results per page (max 100)"), - mcp.Min(1), - mcp.Max(100), - ), - mcp.WithNumber("page", - mcp.Description("Page number"), - mcp.Min(1), - ), + withPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "q") @@ -189,11 +160,7 @@ func searchUsers(client *github.Client, t translations.TranslationHelperFunc) (t if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "perPage", 30) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - page, err := optionalIntParamWithDefault(request, "page", 1) + pagination, err := optionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -202,8 +169,8 @@ func searchUsers(client *github.Client, t translations.TranslationHelperFunc) (t Sort: sort, Order: order, ListOptions: github.ListOptions{ - PerPage: perPage, - Page: page, + PerPage: pagination.perPage, + Page: pagination.page, }, } diff --git a/pkg/github/server.go b/pkg/github/server.go index 18e5da09..228be212 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -230,3 +230,45 @@ func optionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, r.Params.Arguments[p]) } } + +// withPagination returns a ToolOption that adds "page" and "perPage" parameters to the tool. +// The "page" parameter is optional, min 1. The "perPage" parameter is optional, min 1, max 100. +func withPagination() mcp.ToolOption { + return func(tool *mcp.Tool) { + mcp.WithNumber("page", + mcp.Description("Page number for pagination (min 1)"), + mcp.Min(1), + )(tool) + + mcp.WithNumber("perPage", + mcp.Description("Results per page for pagination (min 1, max 100)"), + mcp.Min(1), + mcp.Max(100), + )(tool) + } +} + +type paginationParams struct { + page int + perPage int +} + +// optionalPaginationParams returns the "page" and "perPage" parameters from the request, +// or their default values if not present, "page" default is 1, "perPage" default is 30. +// In future, we may want to make the default values configurable, or even have this +// function returned from `withPagination`, where the defaults are provided alongside +// the min/max values. +func optionalPaginationParams(r mcp.CallToolRequest) (paginationParams, error) { + page, err := optionalIntParamWithDefault(r, "page", 1) + if err != nil { + return paginationParams{}, err + } + perPage, err := optionalIntParamWithDefault(r, "perPage", 30) + if err != nil { + return paginationParams{}, err + } + return paginationParams{ + page: page, + perPage: perPage, + }, nil +} diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index beb6ecbb..149fb77a 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -551,3 +551,86 @@ func TestOptionalStringArrayParam(t *testing.T) { }) } } + +func TestOptionalPaginationParams(t *testing.T) { + tests := []struct { + name string + params map[string]any + expected paginationParams + expectError bool + }{ + { + name: "no pagination parameters, default values", + params: map[string]any{}, + expected: paginationParams{ + page: 1, + perPage: 30, + }, + expectError: false, + }, + { + name: "page parameter, default perPage", + params: map[string]any{ + "page": float64(2), + }, + expected: paginationParams{ + page: 2, + perPage: 30, + }, + expectError: false, + }, + { + name: "perPage parameter, default page", + params: map[string]any{ + "perPage": float64(50), + }, + expected: paginationParams{ + page: 1, + perPage: 50, + }, + expectError: false, + }, + { + name: "page and perPage parameters", + params: map[string]any{ + "page": float64(2), + "perPage": float64(50), + }, + expected: paginationParams{ + page: 2, + perPage: 50, + }, + expectError: false, + }, + { + name: "invalid page parameter", + params: map[string]any{ + "page": "not-a-number", + }, + expected: paginationParams{}, + expectError: true, + }, + { + name: "invalid perPage parameter", + params: map[string]any{ + "perPage": "not-a-number", + }, + expected: paginationParams{}, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := createMCPRequest(tc.params) + result, err := optionalPaginationParams(request) + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, result) + } + }) + } +} From ff896d0f33e42d57a719f57ee8be0379bd21ef62 Mon Sep 17 00:00:00 2001 From: Dmytro Shteflyuk Date: Mon, 7 Apr 2025 08:07:29 -0400 Subject: [PATCH 15/18] Removed duplicated createIssue tool definition --- pkg/github/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/github/server.go b/pkg/github/server.go index 228be212..3ca69558 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -38,7 +38,6 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH if !readOnly { s.AddTool(createIssue(client, t)) s.AddTool(addIssueComment(client, t)) - s.AddTool(createIssue(client, t)) s.AddTool(updateIssue(client, t)) } From ff1f16e82919bc51d5c5b266a4c536e7ea3d9ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Sat, 5 Apr 2025 11:31:06 +0200 Subject: [PATCH 16/18] Fix a typos in repository_resource.go --- pkg/github/repository_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 8efb67e6..8b2ba7a7 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -64,7 +64,7 @@ func getRepositoryResourcePrContent(client *github.Client, t translations.Transl func repositoryResourceContentsHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - // the matcher will give []string with one elemenent + // the matcher will give []string with one element // https://github.com/mark3labs/mcp-go/pull/54 o, ok := request.Params.Arguments["owner"].([]string) if !ok || len(o) == 0 { From 8a9f0b06666f0c46662ad916e972f5968b94313d Mon Sep 17 00:00:00 2001 From: Javier Uruen Val Date: Mon, 7 Apr 2025 17:32:29 +0200 Subject: [PATCH 17/18] add script to pretty print server log (#146) --- script/prettyprint-log | 78 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100755 script/prettyprint-log diff --git a/script/prettyprint-log b/script/prettyprint-log new file mode 100755 index 00000000..16c6e4fd --- /dev/null +++ b/script/prettyprint-log @@ -0,0 +1,78 @@ +#!/bin/bash + +# Script to pretty print the output of the github-mcp-server +# log. +# +# It uses colored output when running on a terminal. + +# show script help +show_help() { + cat <&2 + exit 1 + fi + input="$1" +else + input="/dev/stdin" +fi + +# check if we are in a terminal for showing colors +if test -t 1; then + is_terminal="1" +else + is_terminal="0" +fi + +# Processs each log line, print whether is stdin or stdout, using different +# colors if we output to a terminal, and pretty print json data using jq +sed -nE 's/^.*\[(stdin|stdout)\]:.* ([0-9]+) bytes: (.*)\\n"$/\1 \2 \3/p' $input | +while read -r io bytes json; do + # Unescape the JSON string safely + unescaped=$(echo "$json" | awk '{ print "echo -e \"" $0 "\" | jq ." }' | bash) + echo "$(color $io)($bytes bytes):$(reset)" + echo "$unescaped" | jq . + echo +done From 7ab5d968e065f13a660cd123c2c5d0be6d8b6dbd Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 7 Apr 2025 17:51:51 +0200 Subject: [PATCH 18/18] chore: pass version through to server --- cmd/github-mcp-server/main.go | 2 +- pkg/github/server.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index f3dff6d3..dd4d41a7 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -158,7 +158,7 @@ func runStdioServer(cfg runConfig) error { t, dumpTranslations := translations.TranslationHelper() // Create - ghServer := github.NewServer(ghClient, cfg.readOnly, t) + ghServer := github.NewServer(ghClient, version, cfg.readOnly, t) stdioServer := server.NewStdioServer(ghServer) stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) diff --git a/pkg/github/server.go b/pkg/github/server.go index 3ca69558..bf3583b9 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -15,11 +15,11 @@ import ( ) // NewServer creates a new GitHub MCP server with the specified GH client and logger. -func NewServer(client *github.Client, readOnly bool, t translations.TranslationHelperFunc) *server.MCPServer { +func NewServer(client *github.Client, version string, readOnly bool, t translations.TranslationHelperFunc) *server.MCPServer { // Create a new MCP server s := server.NewMCPServer( "github-mcp-server", - "0.0.1", + version, server.WithResourceCapabilities(true, true), server.WithLogging())