Skip to content

Commit 7d3657e

Browse files
authored
Merge branch 'main' into lockdown-mode-more-tools
2 parents c46bd2e + ec6afa7 commit 7d3657e

File tree

7 files changed

+226
-10
lines changed

7 files changed

+226
-10
lines changed

docs/installation-guides/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This directory contains detailed installation instructions for the GitHub MCP Se
77
- **[Claude Applications](install-claude.md)** - Installation guide for Claude Web, Claude Desktop and Claude Code CLI
88
- **[Cursor](install-cursor.md)** - Installation guide for Cursor IDE
99
- **[Google Gemini CLI](install-gemini-cli.md)** - Installation guide for Google Gemini CLI
10+
- **[OpenAI Codex](install-codex.md)** - Installation guide for OpenAI Codex
1011
- **[Windsurf](install-windsurf.md)** - Installation guide for Windsurf IDE
1112

1213
## Support by Host Application
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Install GitHub MCP Server in OpenAI Codex
2+
3+
## Prerequisites
4+
5+
1. OpenAI Codex (MCP-enabled) installed / available
6+
2. A [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new)
7+
8+
> The remote GitHub MCP server is hosted by GitHub at `https://api.githubcopilot.com/mcp/` and supports Streamable HTTP.
9+
10+
## Remote Configuration
11+
12+
Edit `~/.codex/config.toml` (shared by CLI and IDE extension) and add:
13+
14+
```toml
15+
[mcp_servers.github]
16+
url = "https://api.githubcopilot.com/mcp/"
17+
# Replace with your real PAT (least-privilege scopes). Do NOT commit this.
18+
bearer_token_env_var = "GITHUB_PAT_TOKEN"
19+
```
20+
21+
You can also add it via the Codex CLI:
22+
23+
```cli
24+
codex mcp add github --url https://api.githubcopilot.com/mcp/
25+
```
26+
27+
<details>
28+
<summary><b>Storing Your PAT Securely</b></summary>
29+
<br>
30+
31+
For security, avoid hardcoding your token. One common approach:
32+
33+
1. Store your token in `.env` file
34+
```
35+
GITHUB_PAT_TOKEN=ghp_your_token_here
36+
```
37+
38+
2. Add to .gitignore
39+
```bash
40+
echo -e ".env" >> .gitignore
41+
```
42+
</details>
43+
44+
## Local Docker Configuration
45+
46+
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).
47+
48+
## Verification
49+
50+
After starting Codex (CLI or IDE):
51+
1. Run `/mcp` in the TUI or use the IDE MCP panel; confirm `github` shows tools.
52+
2. Ask: "List my GitHub repositories".
53+
3. If tools are missing:
54+
- Check token validity & scopes.
55+
- Confirm correct table name: `[mcp_servers.github]`.
56+
57+
## Usage
58+
59+
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:
60+
61+
**Repository Operations:**
62+
- "List my GitHub repositories"
63+
- "Show me recent issues in [owner/repo]"
64+
- "Create a new issue in [owner/repo] titled 'Bug: fix login'"
65+
66+
**Pull Requests:**
67+
- "List open pull requests in [owner/repo]"
68+
- "Show me the diff for PR #123"
69+
- "Add a comment to PR #123: 'LGTM, approved'"
70+
71+
**Actions & Workflows:**
72+
- "Show me recent workflow runs in [owner/repo]"
73+
- "Trigger the 'deploy' workflow in [owner/repo]"
74+
75+
**Gists:**
76+
- "Create a gist with this code snippet"
77+
- "List my gists"
78+
79+
> **Tip**: Use `/mcp` in the Codex UI to see all available GitHub tools and their descriptions.
80+
81+
## Choosing Scopes for Your PAT
82+
83+
Minimal useful scopes (adjust as needed):
84+
- `repo` (general repository operations)
85+
- `workflow` (if you want Actions workflow access)
86+
- `read:org` (if accessing org-level resources)
87+
- `project` (for classic project boards)
88+
- `gist` (if using gist tools)
89+
90+
Use the principle of least privilege: add scopes only when a tool request fails due to permission.
91+
92+
## Troubleshooting
93+
94+
| Issue | Possible Cause | Fix |
95+
|-------|----------------|-----|
96+
| Authentication failed | Missing/incorrect PAT scope | Regenerate PAT; ensure `repo` scope present |
97+
| 401 Unauthorized (remote) | Token expired/revoked | Create new PAT; update `bearer_token_env_var` |
98+
| Server not listed | Wrong table name or syntax error | Use `[mcp_servers.github]`; validate TOML |
99+
| Tools missing / zero tools | Insufficient PAT scopes | Add needed scopes (workflow, gist, etc.) |
100+
| Token in file risks leakage | Committed accidentally | Rotate token; add file to `.gitignore` |
101+
102+
## Security Best Practices
103+
1. Never commit tokens into version control
104+
3. Rotate tokens periodically
105+
4. Restrict scopes up front; expand only when required
106+
5. Remove unused PATs from your GitHub account
107+
108+
## References
109+
- Remote server URL: `https://api.githubcopilot.com/mcp/`
110+
- Release binaries: [GitHub Releases](https://github.com/github/github-mcp-server/releases)
111+
- OpenAI Codex MCP docs: https://developers.openai.com/codex/mcp
112+
- Main project README: [Advanced configuration options](../../README.md)

docs/remote-server.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ The Remote GitHub MCP server has optional headers equivalent to the Local server
6161
- `X-MCP-Readonly`: Enables only "read" tools.
6262
- Equivalent to `GITHUB_READ_ONLY` env var for Local server.
6363
- 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.
64+
- `X-MCP-Lockdown`: Enables lockdown mode, hiding public issue details created by users without push access.
65+
- Equivalent to `GITHUB_LOCKDOWN_MODE` env var for Local server.
66+
- 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.
6467

6568
Example:
6669

@@ -70,7 +73,8 @@ Example:
7073
"url": "https://api.githubcopilot.com/mcp/",
7174
"headers": {
7275
"X-MCP-Toolsets": "repos,issues",
73-
"X-MCP-Readonly": "true"
76+
"X-MCP-Readonly": "true",
77+
"X-MCP-Lockdown": "false"
7478
}
7579
}
7680
```

pkg/github/instructions.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func GenerateInstructions(enabledToolsets []string) string {
2222

2323
// Individual toolset instructions
2424
for _, toolset := range enabledToolsets {
25-
if inst := getToolsetInstructions(toolset); inst != "" {
25+
if inst := getToolsetInstructions(toolset, enabledToolsets); inst != "" {
2626
instructions = append(instructions, inst)
2727
}
2828
}
@@ -48,12 +48,18 @@ Tool usage guidance:
4848
}
4949

5050
// getToolsetInstructions returns specific instructions for individual toolsets
51-
func getToolsetInstructions(toolset string) string {
51+
func getToolsetInstructions(toolset string, enabledToolsets []string) string {
5252
switch toolset {
5353
case "pull_requests":
54-
return `## Pull Requests
54+
pullRequestInstructions := `## Pull Requests
5555
5656
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.`
57+
if slices.Contains(enabledToolsets, "repos") {
58+
pullRequestInstructions += `
59+
60+
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.`
61+
}
62+
return pullRequestInstructions
5763
case "issues":
5864
return `## Issues
5965

pkg/github/instructions_test.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package github
22

33
import (
44
"os"
5+
"strings"
56
"testing"
67
)
78

@@ -128,12 +129,23 @@ func TestGenerateInstructionsWithDisableFlag(t *testing.T) {
128129

129130
func TestGetToolsetInstructions(t *testing.T) {
130131
tests := []struct {
131-
toolset string
132-
expectedEmpty bool
132+
toolset string
133+
expectedEmpty bool
134+
enabledToolsets []string
135+
expectedToContain string
136+
notExpectedToContain string
133137
}{
134138
{
135-
toolset: "pull_requests",
136-
expectedEmpty: false,
139+
toolset: "pull_requests",
140+
expectedEmpty: false,
141+
enabledToolsets: []string{"pull_requests", "repos"},
142+
expectedToContain: "pull_request_template.md",
143+
},
144+
{
145+
toolset: "pull_requests",
146+
expectedEmpty: false,
147+
enabledToolsets: []string{"pull_requests"},
148+
notExpectedToContain: "pull_request_template.md",
137149
},
138150
{
139151
toolset: "issues",
@@ -151,7 +163,7 @@ func TestGetToolsetInstructions(t *testing.T) {
151163

152164
for _, tt := range tests {
153165
t.Run(tt.toolset, func(t *testing.T) {
154-
result := getToolsetInstructions(tt.toolset)
166+
result := getToolsetInstructions(tt.toolset, tt.enabledToolsets)
155167
if tt.expectedEmpty {
156168
if result != "" {
157169
t.Errorf("Expected empty result for toolset '%s', but got: %s", tt.toolset, result)
@@ -161,6 +173,14 @@ func TestGetToolsetInstructions(t *testing.T) {
161173
t.Errorf("Expected non-empty result for toolset '%s', but got empty", tt.toolset)
162174
}
163175
}
176+
177+
if tt.expectedToContain != "" && !strings.Contains(result, tt.expectedToContain) {
178+
t.Errorf("Expected result to contain '%s' for toolset '%s', but it did not. Result: %s", tt.expectedToContain, tt.toolset, result)
179+
}
180+
181+
if tt.notExpectedToContain != "" && strings.Contains(result, tt.notExpectedToContain) {
182+
t.Errorf("Did not expect result to contain '%s' for toolset '%s', but it did. Result: %s", tt.notExpectedToContain, tt.toolset, result)
183+
}
164184
})
165185
}
166186
}

pkg/github/pullrequests.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,14 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans
15721572
return mcp.NewToolResultError(err.Error()), nil
15731573
}
15741574

1575+
if addPullRequestReviewThreadMutation.AddPullRequestReviewThread.Thread.ID == nil {
1576+
return mcp.NewToolResultError(`Failed to add comment to pending review. Possible reasons:
1577+
- The line number doesn't exist in the pull request diff
1578+
- The file path is incorrect
1579+
- The side (LEFT/RIGHT) is invalid for the specified line
1580+
`), nil
1581+
}
1582+
15751583
// Return nothing interesting, just indicate success for the time being.
15761584
// In future, we may want to return the review ID, but for the moment, we're not leaking
15771585
// API implementation details to the LLM.

pkg/github/pullrequests_test.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2555,10 +2555,75 @@ func TestAddPullRequestReviewCommentToPendingReview(t *testing.T) {
25552555
PullRequestReviewID: githubv4.NewID("PR_kwDODKw3uc6WYN1T"),
25562556
},
25572557
nil,
2558-
githubv4mock.DataResponse(map[string]any{}),
2558+
githubv4mock.DataResponse(map[string]any{
2559+
"addPullRequestReviewThread": map[string]any{
2560+
"thread": map[string]any{
2561+
"id": "MDEyOlB1bGxSZXF1ZXN0UmV2aWV3VGhyZWFkMTIzNDU2",
2562+
},
2563+
},
2564+
}),
25592565
),
25602566
),
25612567
},
2568+
{
2569+
name: "thread ID is nil - invalid line number",
2570+
requestArgs: map[string]any{
2571+
"owner": "owner",
2572+
"repo": "repo",
2573+
"pullNumber": float64(42),
2574+
"path": "file.go",
2575+
"body": "Comment on non-existent line",
2576+
"subjectType": "LINE",
2577+
"line": float64(999),
2578+
"side": "RIGHT",
2579+
},
2580+
mockedClient: githubv4mock.NewMockedHTTPClient(
2581+
viewerQuery("williammartin"),
2582+
getLatestPendingReviewQuery(getLatestPendingReviewQueryParams{
2583+
author: "williammartin",
2584+
owner: "owner",
2585+
repo: "repo",
2586+
prNum: 42,
2587+
2588+
reviews: []getLatestPendingReviewQueryReview{
2589+
{
2590+
id: "PR_kwDODKw3uc6WYN1T",
2591+
state: "PENDING",
2592+
url: "https://github.com/owner/repo/pull/42",
2593+
},
2594+
},
2595+
}),
2596+
githubv4mock.NewMutationMatcher(
2597+
struct {
2598+
AddPullRequestReviewThread struct {
2599+
Thread struct {
2600+
ID githubv4.ID
2601+
}
2602+
} `graphql:"addPullRequestReviewThread(input: $input)"`
2603+
}{},
2604+
githubv4.AddPullRequestReviewThreadInput{
2605+
Path: githubv4.String("file.go"),
2606+
Body: githubv4.String("Comment on non-existent line"),
2607+
SubjectType: githubv4mock.Ptr(githubv4.PullRequestReviewThreadSubjectTypeLine),
2608+
Line: githubv4.NewInt(999),
2609+
Side: githubv4mock.Ptr(githubv4.DiffSideRight),
2610+
StartLine: nil,
2611+
StartSide: nil,
2612+
PullRequestReviewID: githubv4.NewID("PR_kwDODKw3uc6WYN1T"),
2613+
},
2614+
nil,
2615+
githubv4mock.DataResponse(map[string]any{
2616+
"addPullRequestReviewThread": map[string]any{
2617+
"thread": map[string]any{
2618+
"id": nil,
2619+
},
2620+
},
2621+
}),
2622+
),
2623+
),
2624+
expectToolError: true,
2625+
expectedToolErrMsg: "Failed to add comment to pending review",
2626+
},
25622627
}
25632628

25642629
for _, tc := range tests {

0 commit comments

Comments
 (0)