-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat: add GitHub App authentication support #2562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pyama86
wants to merge
9
commits into
github:main
Choose a base branch
from
pyama86:feat/github-app-auth
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,140
−14
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
db89352
feat: add GitHub App authentication support
pyama86 f3ad678
fix: use direct NewClient calls instead of gogithub.Option slice
pyama86 6799ef5
fix: address Copilot review feedback
pyama86 a808385
feat: support GitHub App authentication in HTTP mode
go-kazuhiko-yamashita 1413842
Merge branch 'main' into feat/github-app-auth
pyama86 e62232f
Merge branch 'main' into feat/github-app-auth
pyama86 e9625e7
Merge upstream main into feat/github-app-auth
pyama86 fe2da47
Merge remote-tracking branch 'origin/feat/github-app-auth' into feat/…
pyama86 7fab774
Move GitHub App docs out of README
pyama86 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
|
|
||
|
|
@@ -37,6 +38,13 @@ var ( | |
| Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`, | ||
| RunE: func(_ *cobra.Command, _ []string) error { | ||
| token := viper.GetString("personal_access_token") | ||
|
|
||
| // Parse GitHub App authentication config. | ||
| appID, privateKey, installationID, err := parseAppAuthConfig() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| useAppAuth := appID != 0 && len(privateKey) > 0 && installationID != 0 | ||
| oauthClientID := viper.GetString("oauth-client-id") | ||
| oauthClientSecret := viper.GetString("oauth-client-secret") | ||
| // Fall back to the build-time baked-in client (official releases) when none is | ||
|
|
@@ -50,8 +58,8 @@ var ( | |
| oauthClientID = buildinfo.OAuthClientID | ||
| oauthClientSecret = buildinfo.OAuthClientSecret | ||
| } | ||
| if token == "" && oauthClientID == "" { | ||
| return errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, or pass --oauth-client-id to log in via OAuth") | ||
| if token == "" && !useAppAuth && oauthClientID == "" { | ||
| return errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth with GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and GITHUB_APP_PRIVATE_KEY or GITHUB_APP_PRIVATE_KEY_PATH, or pass --oauth-client-id to log in via OAuth") | ||
| } | ||
|
|
||
| // If you're wondering why we're not using viper.GetStringSlice("toolsets"), | ||
|
|
@@ -110,13 +118,16 @@ var ( | |
| InsidersMode: viper.GetBool("insiders"), | ||
| ExcludeTools: excludeTools, | ||
| RepoAccessCacheTTL: &ttl, | ||
| AppID: appID, | ||
| PrivateKey: privateKey, | ||
| InstallationID: installationID, | ||
| } | ||
|
|
||
| // When no static token is provided, log in via OAuth using the given | ||
| // client. The requested scopes default to the full supported set | ||
| // (which filters out no tools); an explicit, narrower --oauth-scopes | ||
| // both narrows the grant and hides tools needing other scopes. | ||
| if token == "" { | ||
| if token == "" && !useAppAuth { | ||
| scopes := ghoauth.SupportedScopes | ||
| if viper.IsSet("oauth-scopes") { | ||
| if err := viper.UnmarshalKey("oauth-scopes", &scopes); err != nil { | ||
|
|
@@ -172,6 +183,11 @@ var ( | |
| } | ||
| } | ||
|
|
||
| appID, privateKey, installationID, err := parseAppAuthConfig() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| ttl := viper.GetDuration("repo-access-cache-ttl") | ||
| httpConfig := ghhttp.ServerConfig{ | ||
| Version: version, | ||
|
|
@@ -194,6 +210,9 @@ var ( | |
| EnabledFeatures: enabledFeatures, | ||
| InsidersMode: viper.GetBool("insiders"), | ||
| TrustProxyHeaders: viper.GetBool("trust-proxy-headers"), | ||
| AppID: appID, | ||
| PrivateKey: privateKey, | ||
| InstallationID: installationID, | ||
| } | ||
|
|
||
| return ghhttp.RunHTTPServer(httpConfig) | ||
|
|
@@ -289,3 +308,53 @@ func wordSepNormalizeFunc(_ *pflag.FlagSet, name string) pflag.NormalizedName { | |
| } | ||
| return pflag.NormalizedName(name) | ||
| } | ||
|
|
||
| // parseAppAuthConfig reads GitHub App authentication config from environment variables. | ||
| // Returns (0, nil, 0, nil) when no App auth is configured. | ||
| func parseAppAuthConfig() (appID int64, privateKey []byte, installationID int64, err error) { | ||
| appIDStr := viper.GetString("app_id") | ||
| installationIDStr := viper.GetString("app_installation_id") | ||
| privateKeyStr := viper.GetString("app_private_key") | ||
| privateKeyPath := viper.GetString("app_private_key_path") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cmd/github-mcp-server/main.go |
||
|
|
||
| // If none are set, App auth is not configured | ||
| if appIDStr == "" && installationIDStr == "" && privateKeyStr == "" && privateKeyPath == "" { | ||
| return 0, nil, 0, nil | ||
| } | ||
|
|
||
| // If some but not all are set, that's a configuration error | ||
| if appIDStr == "" || installationIDStr == "" || (privateKeyStr == "" && privateKeyPath == "") { | ||
| return 0, nil, 0, errors.New("incomplete GitHub App auth config: GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and GITHUB_APP_PRIVATE_KEY or GITHUB_APP_PRIVATE_KEY_PATH are all required") | ||
| } | ||
|
pyama86 marked this conversation as resolved.
|
||
|
|
||
| if privateKeyStr != "" && privateKeyPath != "" { | ||
| return 0, nil, 0, errors.New("GITHUB_APP_PRIVATE_KEY and GITHUB_APP_PRIVATE_KEY_PATH are mutually exclusive") | ||
| } | ||
|
|
||
| appID, err = strconv.ParseInt(appIDStr, 10, 64) | ||
| if err != nil { | ||
| return 0, nil, 0, fmt.Errorf("invalid GITHUB_APP_ID: %w", err) | ||
| } | ||
|
|
||
| installationID, err = strconv.ParseInt(installationIDStr, 10, 64) | ||
| if err != nil { | ||
| return 0, nil, 0, fmt.Errorf("invalid GITHUB_APP_INSTALLATION_ID: %w", err) | ||
| } | ||
|
|
||
| if privateKeyStr != "" { | ||
| // Environment variables often use literal "\n" instead of actual newlines. | ||
| // Only replace when the value has no real newlines to avoid corrupting | ||
| // keys that were correctly passed with actual newlines. | ||
| if !strings.Contains(privateKeyStr, "\n") { | ||
| privateKeyStr = strings.ReplaceAll(privateKeyStr, `\n`, "\n") | ||
| } | ||
| privateKey = []byte(privateKeyStr) | ||
| } else { | ||
| privateKey, err = os.ReadFile(privateKeyPath) | ||
| if err != nil { | ||
| return 0, nil, 0, fmt.Errorf("failed to read private key from %s: %w", privateKeyPath, err) | ||
| } | ||
| } | ||
|
|
||
| return appID, privateKey, installationID, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| # Local Server GitHub App Authentication | ||
|
|
||
| The local GitHub MCP Server can authenticate directly as a GitHub App | ||
| installation instead of a Personal Access Token (PAT) or browser-based OAuth | ||
| login. This works for both the `stdio` and `http` commands. The server signs a | ||
| short-lived JWT with your app's private key, exchanges it for an installation | ||
| access token, and refreshes that installation token automatically before it | ||
| expires. | ||
|
|
||
| > This guide covers installation authentication using `GITHUB_APP_ID`, | ||
| > `GITHUB_APP_INSTALLATION_ID`, and a private key. For browser-based user login | ||
| > via OAuth, see [Local Server OAuth Login](oauth-login.md). The remote server | ||
| > has its own auth model; see [Remote Server](remote-server.md). | ||
|
|
||
| ## Contents | ||
|
|
||
| - [How it works](#how-it-works) | ||
| - [GitHub App setup](#github-app-setup) | ||
| - [Quick start](#quick-start) | ||
| - [Configuration reference](#configuration-reference) | ||
| - [Running in Docker](#running-in-docker) | ||
| - [Using the `http` command](#using-the-http-command) | ||
| - [GitHub Enterprise Server and ghe.com](#github-enterprise-server-and-ghecom) | ||
| - [Troubleshooting](#troubleshooting) | ||
|
|
||
| ## How it works | ||
|
|
||
| When all required app environment variables are present, the local server | ||
| authenticates as the configured GitHub App installation instead of as a user: | ||
|
|
||
| 1. It signs a JWT with the app's private key. | ||
| 2. It exchanges that JWT for an installation access token. | ||
| 3. It reuses that installation token for GitHub API calls and refreshes it | ||
| automatically before it expires. | ||
|
|
||
| Because this is installation authentication, access is controlled by the app's | ||
| granted permissions and repository selection — not by PAT scopes or the OAuth | ||
| scopes described in [Local Server OAuth Login](oauth-login.md). No browser | ||
| callback, OAuth app, or device-code flow is involved. | ||
|
|
||
| ## GitHub App setup | ||
|
|
||
| Before starting the server, create or choose a GitHub App and install it on the | ||
| account or organization that owns the repositories you want to access. | ||
|
|
||
| You will need: | ||
|
|
||
| - The **App ID** | ||
| - The **Installation ID** | ||
| - A **private key** for that app | ||
|
|
||
| Make sure the installation has the repository access and permissions your | ||
| workflows need. For example, repository-writing tools need write permissions on | ||
| the relevant resources; read-only setups can grant narrower access. | ||
|
|
||
| > GitHub App installation auth does **not** require an OAuth callback URL. If | ||
| > you only want installation auth, you do not need to enable device flow or | ||
| > browser login in the app settings. | ||
|
|
||
| ## Quick start | ||
|
|
||
| **Native binary (`stdio`)** using a private key file: | ||
|
|
||
| ```bash | ||
| export GITHUB_APP_ID=12345 | ||
| export GITHUB_APP_INSTALLATION_ID=67890 | ||
| export GITHUB_APP_PRIVATE_KEY_PATH=/path/to/private-key.pem | ||
|
|
||
| github-mcp-server stdio | ||
| ``` | ||
|
|
||
| Using an inline private key (for hosts that only accept environment variables): | ||
|
|
||
| ```bash | ||
| export GITHUB_APP_ID=12345 | ||
| export GITHUB_APP_INSTALLATION_ID=67890 | ||
| export GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----" | ||
|
|
||
| github-mcp-server stdio | ||
| ``` | ||
|
|
||
| Once these variables are set, `GITHUB_PERSONAL_ACCESS_TOKEN` is not required. | ||
|
|
||
| ## Configuration reference | ||
|
|
||
| GitHub App auth is configured with environment variables. The same variables | ||
| apply to both `stdio` and `http`. | ||
|
|
||
| | Environment variable | Description | | ||
| |----------------------|-------------| | ||
| | `GITHUB_APP_ID` | GitHub App ID | | ||
| | `GITHUB_APP_INSTALLATION_ID` | Installation ID of the app installation the server should act as | | ||
| | `GITHUB_APP_PRIVATE_KEY` | PEM-encoded private key inline. If your environment variable system cannot preserve real newlines, use literal `\n` escapes. | | ||
| | `GITHUB_APP_PRIVATE_KEY_PATH` | Path to a PEM private key file | | ||
| | `GITHUB_HOST` | Optional GitHub Enterprise Server / `ghe.com` host. Omit for github.com. | | ||
|
|
||
| Rules: | ||
|
|
||
| - Set **exactly one** of `GITHUB_APP_PRIVATE_KEY` or | ||
| `GITHUB_APP_PRIVATE_KEY_PATH`. | ||
| - `GITHUB_APP_ID`, `GITHUB_APP_INSTALLATION_ID`, and a private key are all | ||
| required together. | ||
| - If none of these variables are set, the server falls back to PAT or OAuth | ||
| auth depending on how you start it. | ||
|
|
||
| ## Running in Docker | ||
|
|
||
| GitHub App auth does **not** use the browser callback flow, so Docker does | ||
| **not** need the OAuth callback port mapping required by | ||
| [Local Server OAuth Login](oauth-login.md). You only need to provide the app | ||
| credentials and, if you use a key file, mount it into the container. | ||
|
|
||
| ```bash | ||
| docker run -i --rm \ | ||
| -e GITHUB_APP_ID=12345 \ | ||
| -e GITHUB_APP_INSTALLATION_ID=67890 \ | ||
| -e GITHUB_APP_PRIVATE_KEY_PATH=/key/private-key.pem \ | ||
| -v /path/to/private-key.pem:/key/private-key.pem:ro \ | ||
| ghcr.io/github/github-mcp-server | ||
| ``` | ||
|
|
||
| VS Code (`.vscode/mcp.json`) with an inline private key: | ||
|
|
||
| ```json | ||
| { | ||
| "servers": { | ||
| "github": { | ||
| "command": "docker", | ||
| "args": [ | ||
| "run", "-i", "--rm", | ||
| "-e", "GITHUB_APP_ID", | ||
| "-e", "GITHUB_APP_INSTALLATION_ID", | ||
| "-e", "GITHUB_APP_PRIVATE_KEY", | ||
| "ghcr.io/github/github-mcp-server" | ||
| ], | ||
| "env": { | ||
| "GITHUB_APP_ID": "12345", | ||
| "GITHUB_APP_INSTALLATION_ID": "67890", | ||
| "GITHUB_APP_PRIVATE_KEY": "-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Using the `http` command | ||
|
|
||
| The same environment variables work for the local HTTP server: | ||
|
|
||
| ```bash | ||
| export GITHUB_APP_ID=12345 | ||
| export GITHUB_APP_INSTALLATION_ID=67890 | ||
| export GITHUB_APP_PRIVATE_KEY_PATH=/path/to/private-key.pem | ||
|
|
||
| github-mcp-server http --port 8082 | ||
| ``` | ||
|
|
||
| This is useful when you want a long-running local MCP HTTP endpoint but still | ||
| want installation-scoped, automatically refreshed credentials. | ||
|
|
||
| ## GitHub Enterprise Server and ghe.com | ||
|
|
||
| GitHub App auth works against GitHub Enterprise Server and `ghe.com` too, as | ||
| long as: | ||
|
|
||
| - the app is registered on that same host, | ||
| - the installation exists on that host, and | ||
| - you set `GITHUB_HOST` / `--gh-host` to that host. | ||
|
|
||
| Example: | ||
|
|
||
| ```bash | ||
| export GITHUB_HOST=https://github.example.com | ||
| export GITHUB_APP_ID=12345 | ||
| export GITHUB_APP_INSTALLATION_ID=67890 | ||
| export GITHUB_APP_PRIVATE_KEY_PATH=/path/to/private-key.pem | ||
|
|
||
| github-mcp-server stdio | ||
| ``` | ||
|
|
||
| The server derives the REST API base URL from `GITHUB_HOST`, so installation | ||
| tokens are requested from the correct instance. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - **"incomplete GitHub App auth config"** — set `GITHUB_APP_ID`, | ||
| `GITHUB_APP_INSTALLATION_ID`, and one private key source together. | ||
| - **"GITHUB_APP_PRIVATE_KEY and GITHUB_APP_PRIVATE_KEY_PATH are mutually | ||
| exclusive"** — keep only one. | ||
| - **403 or 404 from GitHub** — the app is often not installed on the target | ||
| repo/org, or it lacks the needed permissions. | ||
| - **Private key parsing or JWT errors** — make sure the private key matches the | ||
| configured app and is valid PEM. | ||
| - **Unexpected fallback to PAT/OAuth** — confirm the app variables are present | ||
| in the actual server process, not just in your shell. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.