Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/installation-guides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions docs/installation-guides/install-codex.md
Original file line number Diff line number Diff line change
@@ -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/
```

<details>
<summary><b>Storing Your PAT Securely</b></summary>
<br>

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
```
</details>

## 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)
6 changes: 5 additions & 1 deletion docs/remote-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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"
}
}
```
Expand Down
12 changes: 9 additions & 3 deletions pkg/github/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -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

Expand Down
30 changes: 25 additions & 5 deletions pkg/github/instructions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package github

import (
"os"
"strings"
"testing"
)

Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand All @@ -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)
}
})
}
}
8 changes: 8 additions & 0 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
67 changes: 66 additions & 1 deletion pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading