-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat: implement OAuth device flow authentication #1649
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
base: main
Are you sure you want to change the base?
Conversation
When the server starts without a GITHUB_PERSONAL_ACCESS_TOKEN, it now starts in 'unauthenticated mode' with only an auth_login tool available. The auth_login tool: - Initiates the OAuth device flow with GitHub - Uses MCP URL elicitation to show the verification URL and user code - Polls for completion while showing progress notifications - Upon success, dynamically registers all configured GitHub tools This enables a much simpler setup experience - users no longer need to pre-configure a PAT. They can simply start the server and authenticate interactively when prompted. Key changes: - New AuthManager in pkg/github/auth.go handles device flow state - New auth_login tool in pkg/github/auth_tools.go - NewUnauthenticatedMCPServer in internal/ghmcp/server.go for token-less startup - CLI flags --oauth-client-id and --oauth-client-secret for enterprise scenarios - Support for github.com, GHES, and GHEC hosts The token is held in memory for the session duration - no persistent storage, which is ideal for Docker --rm workflows. Closes #132
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements OAuth device flow authentication for the GitHub MCP Server, providing an alternative to Personal Access Tokens (PATs). The implementation allows users to authenticate through the MCP protocol itself rather than pre-configuring tokens, significantly simplifying the initial setup experience while maintaining backwards compatibility with existing PAT-based configurations.
Key Changes:
- Adds OAuth device flow authentication support with unauthenticated server startup mode
- Implements
auth_logintool that uses MCP URL elicitation for seamless browser-based authentication - Provides optional OAuth client ID/secret configuration for enterprise scenarios
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/github/auth.go |
Core authentication manager implementing OAuth device flow state machine, token polling, and host endpoint derivation |
pkg/github/auth_tools.go |
MCP tool implementation for auth_login with URL elicitation, polling, and dynamic tool registration callbacks |
pkg/github/auth_test.go |
Comprehensive unit tests for auth manager covering state transitions, device flow success/failure scenarios |
pkg/github/auth_tools_test.go |
Unit tests for auth tool definitions and toolsnap validation |
pkg/github/__toolsnaps__/auth_login.snap |
Tool schema snapshot documenting the auth_login API surface |
internal/ghmcp/server.go |
New unauthenticated server mode that registers only auth tools initially, then dynamically adds GitHub tools after successful authentication |
cmd/github-mcp-server/main.go |
CLI updates making token optional and adding --oauth-client-id and --oauth-client-secret flags |
docs/auth-design.md |
Comprehensive design document explaining architecture, state machine, security considerations, and usage patterns |
| // This OAuth App is registered by GitHub for use with this server. | ||
| // The client ID is safe to embed in source code per OAuth 2.0 spec for public clients. | ||
| // Users can override this with --oauth-client-id for enterprise scenarios. | ||
| // currently a testing app. | ||
| const DefaultOAuthClientID = "Ov23ctTMsnT9LTRdBYYM" | ||
|
|
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "currently a testing app" but this OAuth client ID is being checked into production code. If this is truly just for testing, it should be clearly documented as temporary with a TODO to replace it. If it's the intended production value, the comment should be removed or updated to reflect that.
| // This OAuth App is registered by GitHub for use with this server. | |
| // The client ID is safe to embed in source code per OAuth 2.0 spec for public clients. | |
| // Users can override this with --oauth-client-id for enterprise scenarios. | |
| // currently a testing app. | |
| const DefaultOAuthClientID = "Ov23ctTMsnT9LTRdBYYM" | |
| // This OAuth App is registered and managed by GitHub for use with this server. | |
| // The client ID is safe to embed in source code per OAuth 2.0 spec for public clients. | |
| // Users can override this with --oauth-client-id for enterprise scenarios. | |
| const DefaultOAuthClientID = "Ov23ctTMsnT9LTRdBYYM" |
docs/auth-design.md
Outdated
| #### 2. Auth Tools (`pkg/github/auth_tools.go`) | ||
|
|
||
| ```go | ||
| // auth_login tool - initiates device flow | ||
| func AuthLogin(ctx context.Context) (*AuthLoginResult, error) | ||
|
|
||
| // auth_verify tool - completes device flow | ||
| func AuthVerify(ctx context.Context) (*AuthVerifyResult, error) | ||
| ``` |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Auth Tools section documents both auth_login and auth_verify as separate tools, but the implementation only has auth_login which handles the complete flow. This section should be updated to show only auth_login and document that it's a blocking call that handles both initiation and completion.
docs/auth-design.md
Outdated
| ## Appendix: Device Flow Sequence | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant User | ||
| participant Agent as AI Agent | ||
| participant MCP as MCP Server | ||
| participant GH as GitHub | ||
| User->>Agent: "Create issue on my repo" | ||
| Agent->>MCP: tools/list | ||
| MCP-->>Agent: [auth_login, auth_verify] | ||
| Agent->>MCP: tools/call auth_login | ||
| MCP->>GH: POST /login/device/code | ||
| GH-->>MCP: device_code, user_code, verification_uri | ||
| MCP-->>Agent: "Visit github.com/login/device, enter ABCD-1234" | ||
| Agent->>User: "Please visit github.com/login/device and enter code ABCD-1234" | ||
| User->>GH: Opens browser, enters code, authorizes | ||
| Agent->>MCP: tools/call auth_verify | ||
| MCP->>GH: POST /login/oauth/access_token (polling) | ||
| GH-->>MCP: access_token | ||
| MCP->>MCP: Initialize GitHub clients | ||
| MCP-->>Agent: notifications/tools/list_changed | ||
| MCP-->>Agent: "Authenticated as @username" | ||
| Agent->>MCP: tools/list | ||
| MCP-->>Agent: [all tools now available] | ||
| Agent->>MCP: tools/call create_issue | ||
| MCP-->>Agent: Issue created! | ||
| Agent->>User: "Done! Created issue #123" | ||
| ``` |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sequence diagram shows two separate tool calls (auth_login and auth_verify), but the actual implementation has auth_login as a single blocking call that polls internally. The diagram should be updated to show auth_login doing the polling loop internally rather than requiring a second auth_verify call.
docs/auth-design.md
Outdated
| Default OAuth App scopes (matching `gh` CLI minimal scopes): | ||
| - `repo` - Full control of private repositories | ||
| - `read:org` - Read org membership | ||
| - `gist` - Create gists | ||
|
|
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation claims default scopes "match gh CLI minimal scopes" but lists far more scopes than just the three mentioned (repo, read:org, gist). The actual DefaultOAuthScopes in auth.go includes 11 scopes. Either the documentation should list all scopes accurately or the claim about matching gh CLI should be verified and updated.
| Default OAuth App scopes (matching `gh` CLI minimal scopes): | |
| - `repo` - Full control of private repositories | |
| - `read:org` - Read org membership | |
| - `gist` - Create gists | |
| Default OAuth App scopes: | |
| - The authoritative list of scopes is defined in `DefaultOAuthScopes` in `pkg/github/auth.go`. | |
| - These scopes form a superset of the `gh` CLI minimal scopes (`repo`, `read:org`, `gist`) to support all GitHub MCP tools while still following least-privilege principles. |
docs/auth-design.md
Outdated
|
|
||
| ### Key Components | ||
|
|
||
| #### 1. Auth State Manager (`pkg/github/auth_state.go`) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation refers to the file as "pkg/github/auth_state.go" but the actual implementation is in "pkg/github/auth.go". The filename in the documentation should be corrected to match the actual implementation.
| #### 1. Auth State Manager (`pkg/github/auth_state.go`) | |
| #### 1. Auth State Manager (`pkg/github/auth.go`) |
|
|
||
| // Create the MCP server with capabilities advertised for dynamic tool registration | ||
| serverOpts := &mcp.ServerOptions{ | ||
| Instructions: "GitHub MCP Server - Authentication Required\n\nYou are not currently authenticated with GitHub. Use the auth_login tool to start the authentication process, then auth_verify to complete it.", |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server instructions still reference "auth_verify" which doesn't exist in the implementation. The instructions should be updated to reflect that only auth_login is needed, and that it's a single blocking call that handles the complete authentication flow.
| Instructions: "GitHub MCP Server - Authentication Required\n\nYou are not currently authenticated with GitHub. Use the auth_login tool to start the authentication process, then auth_verify to complete it.", | |
| Instructions: "GitHub MCP Server - Authentication Required\n\nYou are not currently authenticated with GitHub. Use the auth_login tool to complete authentication. This is a single, blocking call that will guide you through the entire device authorization flow and return once authentication has finished.", |
| - Create and manage repositories | ||
| - Work with issues and pull requests | ||
| - Access your organizations and teams | ||
| - And much more, depending on configuration (mention the success and new tools to the user! |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo in the success message. The text has an unclosed parenthesis and reads "depending on configuration (mention the success..." which appears to be an incomplete instruction to the developer rather than proper user-facing text. This should be completed or removed.
| - And much more, depending on configuration (mention the success and new tools to the user! | |
| - And much more, depending on your GitHub configuration. |
docs/auth-design.md
Outdated
| When the server starts without a `GITHUB_PERSONAL_ACCESS_TOKEN`, instead of failing, it starts in "unauthenticated mode" with only authentication tools available. Users authenticate through MCP tool calls: | ||
|
|
||
| 1. **`auth_login`** - Initiates device flow, returns verification URL and user code | ||
| 2. **`auth_verify`** - Completes the flow after user authorizes in browser | ||
|
|
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation comment mentions "auth_verify" tool but based on the code in auth_tools.go, there is only an "auth_login" tool that handles both initiation and completion of the flow. The design document should be updated to reflect the actual implementation where polling happens within the single auth_login tool call.
docs/auth-design.md
Outdated
| ### Authentication Flow (User Perspective) | ||
|
|
||
| 1. User asks agent: "Create an issue on my repo" | ||
| 2. Agent calls `auth_login` tool | ||
| 3. Tool returns: | ||
| ``` | ||
| To authenticate, visit: https://github.com/login/device | ||
| Enter code: ABCD-1234 | ||
| After authorizing, use the auth_verify tool to complete login. | ||
| ``` | ||
| 4. User opens browser, enters code, clicks "Authorize" | ||
| 5. Agent calls `auth_verify` tool | ||
| 6. Tool returns: "Successfully authenticated as @username" | ||
| 7. Agent proceeds with original request using now-available tools | ||
|
|
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user flow documented here shows a two-step process with auth_verify, but the actual implementation uses a single auth_login tool that blocks and polls. This discrepancy between design documentation and implementation could confuse developers. The documentation should reflect that auth_login is a single blocking call that handles the entire flow.
| if err != nil || u.Hostname() == "" { | ||
| // Fallback: treat as hostname directly (shouldn't happen with scheme added) | ||
| return OAuthHost{ | ||
| DeviceCodeURL: fmt.Sprintf("https://%s/login/device/code", hostname), | ||
| TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", hostname), | ||
| Hostname: hostname, | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the fallback error path when url.Parse fails, the code uses the modified hostname variable (which already has "https://" prepended) in the format string with "https://%s". This would result in invalid URLs like "https://https://hostname/login/device/code". The fallback should use the original hostname without the scheme, or extract just the hostname portion from the modified string.
Summary
This PR implements OAuth device flow authentication for the GitHub MCP Server, enabling a much simpler setup experience where users no longer need to pre-configure a Personal Access Token.
Closes #132
Problem
Currently, users must:
This creates friction for new users and security concerns around token storage.
Solution
When the server starts without a
GITHUB_PERSONAL_ACCESS_TOKEN, instead of failing, it now starts in "unauthenticated mode" with only anauth_logintool available.User Experience
Before:
{ "github": { "command": "docker", "args": ["run", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxx" // User must create PAT first } } }After:
{ "github": { "command": "docker", "args": ["run", "--rm", "-i", "ghcr.io/github/github-mcp-server", "stdio"] // No token needed! User authenticates via tool call } }Authentication Flow
auth_logintoolKey Changes
pkg/github/auth.go: NewAuthManagerthat handles OAuth device flow state machinepkg/github/auth_tools.go: Newauth_logintool that initiates and completes the flowinternal/ghmcp/server.go: NewNewUnauthenticatedMCPServerfor token-less startupcmd/github-mcp-server/main.go: CLI flags--oauth-client-idand--oauth-client-secretfor enterprise scenariosdocs/auth-design.md: Design document explaining the architectureFeatures
--rmTesting
AuthManagerstate transitions and device flowauth_logintool definitionSecurity Considerations
ghCLI for familiarity