Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ea4e396

Browse filesBrowse files
refactor(auth): isolate GitHub App auth to stdio startup
Keep PEM loading and installation-token provider construction at the CLI leaf, then pass a generic refreshing token provider through the existing HTTP transports. Rebase the feature onto current main and keep the HTTP command unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 646357dd-c89f-4973-9a5c-e6c5fc18818c
1 parent e05a384 commit ea4e396
Copy full SHA for ea4e396

11 files changed

+183-441Lines changed: 183 additions & 441 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎README.md‎

Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ Add one of the following JSON blocks to your IDE's MCP settings.
311311

312312
See **[Local Server OAuth Login](docs/oauth-login.md)** for the native-binary flow (no fixed port needed), the headless/device-code fallback, GitHub Enterprise Server / `ghe.com`, and bringing your own OAuth or GitHub App.
313313

314-
**Running headless (CI, Kubernetes, background agents)?** The stdio server can authenticate as a **GitHub App installation** with no browser, device code, or elicitation — see **[GitHub App Server-to-Server Authentication](docs/github-app-auth.md)**. This injects a high-privilege credential alongside the agent, so read the security guidance there first; it is not recommended without an independent security review.
314+
For non-interactive stdio deployments, see **[GitHub App Authentication](docs/github-app-auth.md)**.
315315

316316
**Or authenticate with a Personal Access Token.** Set `GITHUB_PERSONAL_ACCESS_TOKEN` instead (it takes precedence over OAuth):
317317

Collapse file

‎cmd/github-mcp-server/main.go‎

Copy file name to clipboardExpand all lines: cmd/github-mcp-server/main.go
+12-38Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ var (
4040
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
4141
RunE: func(_ *cobra.Command, _ []string) error {
4242
token := viper.GetString("personal_access_token")
43-
44-
// GitHub App server-to-server auth (non-interactive). It is detected
45-
// when any app-* setting is present; a partial configuration yields a
46-
// clear error from the loader/validator below rather than silently
47-
// falling back to another mode.
4843
appID := viper.GetString("app-id")
4944
appInstallationID := viper.GetString("app-installation-id")
5045
appPrivateKeyPath := viper.GetString("app-private-key-path")
@@ -59,14 +54,13 @@ var (
5954
// --oauth-client-id. Recognizing the host via NormalizeHost means an explicit
6055
// GITHUB_HOST=github.com (or api.github.com) still counts as the default and keeps
6156
// zero-config login working. The secret tracks the id, so an explicitly provided
62-
// id with no secret never picks up the baked-in secret. App auth opts out of this
63-
// default so configuring an app never accidentally enables OAuth login too.
57+
// id with no secret never picks up the baked-in secret.
6458
if oauthClientID == "" && !appAuthRequested && oauth.NormalizeHost(viper.GetString("host")) == "https://github.com" {
6559
oauthClientID = buildinfo.OAuthClientID
6660
oauthClientSecret = buildinfo.OAuthClientSecret
6761
}
6862
if token == "" && !appAuthRequested && oauthClientID == "" {
69-
return errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth (GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID and GITHUB_APP_PRIVATE_KEY_PATH), or pass --oauth-client-id to log in via OAuth")
63+
return errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth, or pass --oauth-client-id to log in via OAuth")
7064
}
7165
if appAuthRequested && token != "" {
7266
return errors.New("GitHub App authentication and GITHUB_PERSONAL_ACCESS_TOKEN are mutually exclusive: set only one")
@@ -137,7 +131,6 @@ var (
137131
// client. The requested scopes default to the full supported set
138132
// (which filters out no tools); an explicit, narrower --oauth-scopes
139133
// both narrows the grant and hides tools needing other scopes.
140-
// Skipped for GitHub App auth, which sources tokens non-interactively.
141134
if token == "" && !appAuthRequested {
142135
scopes := ghoauth.SupportedScopes
143136
if viper.IsSet("oauth-scopes") {
@@ -156,15 +149,12 @@ var (
156149
stdioServerConfig.OAuthScopes = scopes
157150
}
158151

159-
// GitHub App server-to-server auth: load and parse the private key,
160-
// then resolve the REST base URL so the server can mint installation
161-
// tokens for the configured host (github.com, GHES, or ghe.com).
162152
if appAuthRequested {
163-
appConfig, err := buildAppAuthConfig(appID, appInstallationID, appPrivateKeyPath, appPrivateKeyInline, viper.GetString("host"))
153+
tokenProvider, err := newGitHubAppTokenProvider(appID, appInstallationID, appPrivateKeyPath, appPrivateKeyInline, viper.GetString("host"))
164154
if err != nil {
165155
return err
166156
}
167-
stdioServerConfig.AppAuth = appConfig
157+
stdioServerConfig.TokenProvider = tokenProvider
168158
}
169159

170160
return ghmcp.RunStdioServer(stdioServerConfig)
@@ -263,11 +253,7 @@ func init() {
263253
stdioCmd.Flags().StringSlice("oauth-scopes", nil, "Comma-separated OAuth scopes to request; also filters tools to those scopes. Defaults to the full supported set")
264254
stdioCmd.Flags().Int("oauth-callback-port", 0, "Fixed local port for the OAuth callback server. Defaults to a random port; set a fixed port when mapping it through Docker")
265255

266-
// stdio-specific GitHub App (server-to-server) flags. Provide an app ID,
267-
// installation ID, and private key to authenticate non-interactively — no
268-
// browser, device code, or elicitation. Intended for headless deployments.
269-
// The private key itself has no flag (only GITHUB_APP_PRIVATE_KEY): a flag
270-
// would place the key in the process arguments. Prefer the key file path.
256+
// The private key has no flag because passing it in argv would expose it.
271257
stdioCmd.Flags().String("app-id", "", "GitHub App ID or client ID, enabling non-interactive server-to-server authentication")
272258
stdioCmd.Flags().String("app-installation-id", "", "GitHub App installation ID to mint installation access tokens for")
273259
stdioCmd.Flags().String("app-private-key-path", "", "Path to the GitHub App private key (PEM). Preferred over GITHUB_APP_PRIVATE_KEY: keeps the key off the command line and out of the environment")
@@ -326,19 +312,11 @@ func main() {
326312
}
327313
}
328314

329-
// buildAppAuthConfig assembles the GitHub App server-to-server configuration:
330-
// it loads and parses the private key and resolves the REST base URL for the
331-
// configured host. The private key is read from a file (preferred) or an inline
332-
// environment value; a missing or partial configuration yields a clear error.
333-
func buildAppAuthConfig(appID, installationID, keyPath, keyInline, host string) (*githubapp.Config, error) {
315+
func newGitHubAppTokenProvider(appID, installationID, keyPath, keyInline, host string) (func() string, error) {
334316
keyBytes, err := loadAppPrivateKey(keyPath, keyInline)
335317
if err != nil {
336318
return nil, err
337319
}
338-
privateKey, err := githubapp.ParsePrivateKey(keyBytes)
339-
if err != nil {
340-
return nil, fmt.Errorf("invalid GitHub App private key: %w", err)
341-
}
342320

343321
apiHost, err := utils.NewAPIHost(host)
344322
if err != nil {
@@ -349,22 +327,18 @@ func buildAppAuthConfig(appID, installationID, keyPath, keyInline, host string)
349327
return nil, fmt.Errorf("failed to resolve REST URL for GitHub App authentication: %w", err)
350328
}
351329

352-
cfg := &githubapp.Config{
330+
provider, err := githubapp.NewProvider(githubapp.Config{
353331
AppID: appID,
354332
InstallationID: installationID,
355-
PrivateKey: privateKey,
333+
PrivateKeyPEM: keyBytes,
356334
BaseRESTURL: restURL.String(),
335+
}, nil)
336+
if err != nil {
337+
return nil, fmt.Errorf("failed to configure GitHub App authentication: %w", err)
357338
}
358-
if err := cfg.Validate(); err != nil {
359-
return nil, err
360-
}
361-
return cfg, nil
339+
return provider.AccessToken, nil
362340
}
363341

364-
// loadAppPrivateKey returns the GitHub App private key bytes from a file path
365-
// (preferred — it keeps the key off argv and out of the environment) or from an
366-
// inline value. The inline form tolerates literal "\n" escapes so a PEM survives
367-
// being carried in a single-line environment variable.
368342
func loadAppPrivateKey(path, inline string) ([]byte, error) {
369343
switch {
370344
case path != "":
Collapse file

‎cmd/github-mcp-server/main_test.go‎

Copy file name to clipboard
+38Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestLoadAppPrivateKey(t *testing.T) {
13+
t.Run("file", func(t *testing.T) {
14+
path := filepath.Join(t.TempDir(), "app.pem")
15+
require.NoError(t, os.WriteFile(path, []byte("from-file"), 0o600))
16+
17+
key, err := loadAppPrivateKey(path, "from-inline")
18+
require.NoError(t, err)
19+
assert.Equal(t, []byte("from-file"), key)
20+
})
21+
22+
t.Run("inline", func(t *testing.T) {
23+
key, err := loadAppPrivateKey("", `first\nsecond`)
24+
require.NoError(t, err)
25+
assert.Equal(t, []byte("first\nsecond"), key)
26+
})
27+
28+
t.Run("missing", func(t *testing.T) {
29+
_, err := loadAppPrivateKey("", "")
30+
require.Error(t, err)
31+
assert.Contains(t, err.Error(), "private key")
32+
})
33+
}
34+
35+
func TestGitHubAppFlagsAreStdioOnly(t *testing.T) {
36+
assert.NotNil(t, stdioCmd.Flags().Lookup("app-id"))
37+
assert.Nil(t, httpCmd.Flags().Lookup("app-id"))
38+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.