From 9b747a6d8a497a88941cc2452ddeff456be29a44 Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Tue, 18 Nov 2025 23:36:52 -0800 Subject: [PATCH 1/4] Add installation guide for OpenAI Codex (#1340) * Add installation guide for OpenAI Codex * updates based on feedback * Remove optional Docker requirement from installation guide for OpenAI Codex * Remove Docker-related troubleshooting and references from installation guide * Update docs/installation-guides/install-codex.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update docs/installation-guides/install-codex.md * Apply suggestions from code review * Update docs/installation-guides/install-codex.md Co-authored-by: Gabriel Peal * updates on feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Gabriel Peal --- docs/installation-guides/README.md | 1 + docs/installation-guides/install-codex.md | 112 ++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 docs/installation-guides/install-codex.md diff --git a/docs/installation-guides/README.md b/docs/installation-guides/README.md index 4406f5b98..097d97b02 100644 --- a/docs/installation-guides/README.md +++ b/docs/installation-guides/README.md @@ -7,6 +7,7 @@ This directory contains detailed installation instructions for the GitHub MCP Se - **[Claude Applications](install-claude.md)** - Installation guide for Claude Web, Claude Desktop and Claude Code CLI - **[Cursor](install-cursor.md)** - Installation guide for Cursor IDE - **[Google Gemini CLI](install-gemini-cli.md)** - Installation guide for Google Gemini CLI +- **[OpenAI Codex](install-codex.md)** - Installation guide for OpenAI Codex - **[Windsurf](install-windsurf.md)** - Installation guide for Windsurf IDE ## Support by Host Application diff --git a/docs/installation-guides/install-codex.md b/docs/installation-guides/install-codex.md new file mode 100644 index 000000000..5f92996bc --- /dev/null +++ b/docs/installation-guides/install-codex.md @@ -0,0 +1,112 @@ +# Install GitHub MCP Server in OpenAI Codex + +## Prerequisites + +1. OpenAI Codex (MCP-enabled) installed / available +2. A [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new) + +> The remote GitHub MCP server is hosted by GitHub at `https://api.githubcopilot.com/mcp/` and supports Streamable HTTP. + +## Remote Configuration + +Edit `~/.codex/config.toml` (shared by CLI and IDE extension) and add: + +```toml +[mcp_servers.github] +url = "https://api.githubcopilot.com/mcp/" +# Replace with your real PAT (least-privilege scopes). Do NOT commit this. +bearer_token_env_var = "GITHUB_PAT_TOKEN" +``` + +You can also add it via the Codex CLI: + +```cli +codex mcp add github --url https://api.githubcopilot.com/mcp/ +``` + +
+Storing Your PAT Securely +
+ +For security, avoid hardcoding your token. One common approach: + +1. Store your token in `.env` file +``` +GITHUB_PAT_TOKEN=ghp_your_token_here +``` + +2. Add to .gitignore +```bash +echo -e ".env" >> .gitignore +``` +
+ +## Local Docker Configuration + +Use this if you prefer a local, self-hosted instance instead of the remote HTTP server, please refer to the [OpenAI documentation for configuration](https://developers.openai.com/codex/mcp). + +## Verification + +After starting Codex (CLI or IDE): +1. Run `/mcp` in the TUI or use the IDE MCP panel; confirm `github` shows tools. +2. Ask: "List my GitHub repositories". +3. If tools are missing: + - Check token validity & scopes. + - Confirm correct table name: `[mcp_servers.github]`. + +## Usage + +After setup, Codex can interact with GitHub directly. It will use the default tool set automatically but can be [configured](../../README.md#default-toolset). Try these example prompts: + +**Repository Operations:** +- "List my GitHub repositories" +- "Show me recent issues in [owner/repo]" +- "Create a new issue in [owner/repo] titled 'Bug: fix login'" + +**Pull Requests:** +- "List open pull requests in [owner/repo]" +- "Show me the diff for PR #123" +- "Add a comment to PR #123: 'LGTM, approved'" + +**Actions & Workflows:** +- "Show me recent workflow runs in [owner/repo]" +- "Trigger the 'deploy' workflow in [owner/repo]" + +**Gists:** +- "Create a gist with this code snippet" +- "List my gists" + +> **Tip**: Use `/mcp` in the Codex UI to see all available GitHub tools and their descriptions. + +## Choosing Scopes for Your PAT + +Minimal useful scopes (adjust as needed): +- `repo` (general repository operations) +- `workflow` (if you want Actions workflow access) +- `read:org` (if accessing org-level resources) +- `project` (for classic project boards) +- `gist` (if using gist tools) + +Use the principle of least privilege: add scopes only when a tool request fails due to permission. + +## Troubleshooting + +| Issue | Possible Cause | Fix | +|-------|----------------|-----| +| Authentication failed | Missing/incorrect PAT scope | Regenerate PAT; ensure `repo` scope present | +| 401 Unauthorized (remote) | Token expired/revoked | Create new PAT; update `bearer_token_env_var` | +| Server not listed | Wrong table name or syntax error | Use `[mcp_servers.github]`; validate TOML | +| Tools missing / zero tools | Insufficient PAT scopes | Add needed scopes (workflow, gist, etc.) | +| Token in file risks leakage | Committed accidentally | Rotate token; add file to `.gitignore` | + +## Security Best Practices +1. Never commit tokens into version control +3. Rotate tokens periodically +4. Restrict scopes up front; expand only when required +5. Remove unused PATs from your GitHub account + +## References +- Remote server URL: `https://api.githubcopilot.com/mcp/` +- Release binaries: [GitHub Releases](https://github.com/github/github-mcp-server/releases) +- OpenAI Codex MCP docs: https://developers.openai.com/codex/mcp +- Main project README: [Advanced configuration options](../../README.md) From f3b9a63311b74f8f3ff024a206fddd492a204874 Mon Sep 17 00:00:00 2001 From: Ksenia Bobrova Date: Wed, 19 Nov 2025 10:11:17 +0100 Subject: [PATCH 2/4] Report error when API silently fails to add review comment (#1441) --- pkg/github/pullrequests.go | 8 ++++ pkg/github/pullrequests_test.go | 67 ++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index e64ae03e4..d8f3b7136 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -1518,6 +1518,14 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans return mcp.NewToolResultError(err.Error()), nil } + if addPullRequestReviewThreadMutation.AddPullRequestReviewThread.Thread.ID == nil { + return mcp.NewToolResultError(`Failed to add comment to pending review. Possible reasons: + - The line number doesn't exist in the pull request diff + - The file path is incorrect + - The side (LEFT/RIGHT) is invalid for the specified line +`), nil + } + // Return nothing interesting, just indicate success for the time being. // In future, we may want to return the review ID, but for the moment, we're not leaking // API implementation details to the LLM. diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 347bce672..b29c743a3 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -2555,10 +2555,75 @@ func TestAddPullRequestReviewCommentToPendingReview(t *testing.T) { PullRequestReviewID: githubv4.NewID("PR_kwDODKw3uc6WYN1T"), }, nil, - githubv4mock.DataResponse(map[string]any{}), + githubv4mock.DataResponse(map[string]any{ + "addPullRequestReviewThread": map[string]any{ + "thread": map[string]any{ + "id": "MDEyOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMTIzNDU2", + }, + }, + }), ), ), }, + { + name: "thread ID is nil - invalid line number", + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "path": "file.go", + "body": "Comment on non-existent line", + "subjectType": "LINE", + "line": float64(999), + "side": "RIGHT", + }, + mockedClient: githubv4mock.NewMockedHTTPClient( + viewerQuery("williammartin"), + getLatestPendingReviewQuery(getLatestPendingReviewQueryParams{ + author: "williammartin", + owner: "owner", + repo: "repo", + prNum: 42, + + reviews: []getLatestPendingReviewQueryReview{ + { + id: "PR_kwDODKw3uc6WYN1T", + state: "PENDING", + url: "https://github.com/owner/repo/pull/42", + }, + }, + }), + githubv4mock.NewMutationMatcher( + struct { + AddPullRequestReviewThread struct { + Thread struct { + ID githubv4.ID + } + } `graphql:"addPullRequestReviewThread(input: $input)"` + }{}, + githubv4.AddPullRequestReviewThreadInput{ + Path: githubv4.String("file.go"), + Body: githubv4.String("Comment on non-existent line"), + SubjectType: githubv4mock.Ptr(githubv4.PullRequestReviewThreadSubjectTypeLine), + Line: githubv4.NewInt(999), + Side: githubv4mock.Ptr(githubv4.DiffSideRight), + StartLine: nil, + StartSide: nil, + PullRequestReviewID: githubv4.NewID("PR_kwDODKw3uc6WYN1T"), + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "addPullRequestReviewThread": map[string]any{ + "thread": map[string]any{ + "id": nil, + }, + }, + }), + ), + ), + expectToolError: true, + expectedToolErrMsg: "Failed to add comment to pending review", + }, } for _, tc := range tests { From 548c27ce621cf1718999c98037a49aac7647d600 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Wed, 19 Nov 2025 11:22:06 +0100 Subject: [PATCH 3/4] adding remote server header for lockdown configuration (#1417) * adding remote server header for lockdown configuration * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/remote-server.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/remote-server.md b/docs/remote-server.md index b263d70aa..5ee6aea64 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -61,6 +61,9 @@ The Remote GitHub MCP server has optional headers equivalent to the Local server - `X-MCP-Readonly`: Enables only "read" tools. - Equivalent to `GITHUB_READ_ONLY` env var for Local server. - If this header is empty, "false", "f", "no", "n", "0", or "off" (ignoring whitespace and case), it will be interpreted as false. All other values are interpreted as true. +- `X-MCP-Lockdown`: Enables lockdown mode, hiding public issue details created by users without push access. + - Equivalent to `GITHUB_LOCKDOWN_MODE` env var for Local server. + - If this header is empty, "false", "f", "no", "n", "0", or "off" (ignoring whitespace and case), it will be interpreted as false. All other values are interpreted as true. Example: @@ -70,7 +73,8 @@ Example: "url": "https://api.githubcopilot.com/mcp/", "headers": { "X-MCP-Toolsets": "repos,issues", - "X-MCP-Readonly": "true" + "X-MCP-Readonly": "true", + "X-MCP-Lockdown": "false" } } ``` From ec6afa776d8bebe0c0ed36926562b411e14c5bf4 Mon Sep 17 00:00:00 2001 From: Ksenia Bobrova Date: Wed, 19 Nov 2025 11:47:31 +0100 Subject: [PATCH 4/4] Instruct LLM to use pull request template when creating PRs (#1442) --- pkg/github/instructions.go | 12 +++++++++--- pkg/github/instructions_test.go | 30 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/pkg/github/instructions.go b/pkg/github/instructions.go index 338b8b987..3a5fb54bb 100644 --- a/pkg/github/instructions.go +++ b/pkg/github/instructions.go @@ -22,7 +22,7 @@ func GenerateInstructions(enabledToolsets []string) string { // Individual toolset instructions for _, toolset := range enabledToolsets { - if inst := getToolsetInstructions(toolset); inst != "" { + if inst := getToolsetInstructions(toolset, enabledToolsets); inst != "" { instructions = append(instructions, inst) } } @@ -48,12 +48,18 @@ Tool usage guidance: } // getToolsetInstructions returns specific instructions for individual toolsets -func getToolsetInstructions(toolset string) string { +func getToolsetInstructions(toolset string, enabledToolsets []string) string { switch toolset { case "pull_requests": - return `## Pull Requests + pullRequestInstructions := `## Pull Requests PR review workflow: Always use 'pull_request_review_write' with method 'create' to create a pending review, then 'add_comment_to_pending_review' to add comments, and finally 'pull_request_review_write' with method 'submit_pending' to submit the review for complex reviews with line-specific comments.` + if slices.Contains(enabledToolsets, "repos") { + pullRequestInstructions += ` + +Before creating a pull request, search for pull request templates in the repository. Template files are called pull_request_template.md or they're located in '.github/PULL_REQUEST_TEMPLATE' directory. Use the template content to structure the PR description and then call create_pull_request tool.` + } + return pullRequestInstructions case "issues": return `## Issues diff --git a/pkg/github/instructions_test.go b/pkg/github/instructions_test.go index f00e0ac74..b8ad2ba8c 100644 --- a/pkg/github/instructions_test.go +++ b/pkg/github/instructions_test.go @@ -2,6 +2,7 @@ package github import ( "os" + "strings" "testing" ) @@ -128,12 +129,23 @@ func TestGenerateInstructionsWithDisableFlag(t *testing.T) { func TestGetToolsetInstructions(t *testing.T) { tests := []struct { - toolset string - expectedEmpty bool + toolset string + expectedEmpty bool + enabledToolsets []string + expectedToContain string + notExpectedToContain string }{ { - toolset: "pull_requests", - expectedEmpty: false, + toolset: "pull_requests", + expectedEmpty: false, + enabledToolsets: []string{"pull_requests", "repos"}, + expectedToContain: "pull_request_template.md", + }, + { + toolset: "pull_requests", + expectedEmpty: false, + enabledToolsets: []string{"pull_requests"}, + notExpectedToContain: "pull_request_template.md", }, { toolset: "issues", @@ -151,7 +163,7 @@ func TestGetToolsetInstructions(t *testing.T) { for _, tt := range tests { t.Run(tt.toolset, func(t *testing.T) { - result := getToolsetInstructions(tt.toolset) + result := getToolsetInstructions(tt.toolset, tt.enabledToolsets) if tt.expectedEmpty { if result != "" { t.Errorf("Expected empty result for toolset '%s', but got: %s", tt.toolset, result) @@ -161,6 +173,14 @@ func TestGetToolsetInstructions(t *testing.T) { t.Errorf("Expected non-empty result for toolset '%s', but got empty", tt.toolset) } } + + if tt.expectedToContain != "" && !strings.Contains(result, tt.expectedToContain) { + t.Errorf("Expected result to contain '%s' for toolset '%s', but it did not. Result: %s", tt.expectedToContain, tt.toolset, result) + } + + if tt.notExpectedToContain != "" && strings.Contains(result, tt.notExpectedToContain) { + t.Errorf("Did not expect result to contain '%s' for toolset '%s', but it did. Result: %s", tt.notExpectedToContain, tt.toolset, result) + } }) } }