Skip to content

Conversation

@SamMorrowDrums
Copy link
Collaborator

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:

  1. Generate a Personal Access Token (PAT) manually on GitHub
  2. Configure the token in their MCP host's configuration (often in plain text)
  3. Manage token rotation manually

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 an auth_login tool 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

  1. User asks agent to do something with GitHub
  2. Agent calls auth_login tool
  3. Tool uses MCP URL elicitation to show verification URL and user code
  4. User opens browser, enters code, clicks "Authorize"
  5. Tool polls for completion with progress notifications
  6. On success, all configured GitHub tools are dynamically registered
  7. Agent proceeds with original request

Key Changes

  • pkg/github/auth.go: New AuthManager that handles OAuth device flow state machine
  • pkg/github/auth_tools.go: New auth_login tool that initiates and completes the flow
  • internal/ghmcp/server.go: New NewUnauthenticatedMCPServer for token-less startup
  • cmd/github-mcp-server/main.go: CLI flags --oauth-client-id and --oauth-client-secret for enterprise scenarios
  • docs/auth-design.md: Design document explaining the architecture

Features

  • ✅ Works with github.com, GHES, and GHEC hosts
  • ✅ Uses MCP URL elicitation for nice UX in supporting clients
  • ✅ Progress notifications during polling
  • ✅ Token held in memory only (no persistent storage) - ideal for Docker --rm
  • ✅ Enterprise support via custom OAuth App client ID
  • ✅ Backwards compatible - existing PAT-based configs continue to work

Testing

  • Unit tests for AuthManager state transitions and device flow
  • Unit tests for auth_login tool definition
  • Toolsnap for API surface documentation

Security Considerations

  • Token obtained at runtime, never written to disk
  • Short-lived session (container lifetime in Docker mode)
  • User explicitly authorizes in browser with full visibility
  • Default scopes match gh CLI for familiarity

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
Copilot AI review requested due to automatic review settings December 19, 2025 00:09
@SamMorrowDrums SamMorrowDrums requested a review from a team as a code owner December 19, 2025 00:09
Copy link
Contributor

Copilot AI left a 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_login tool 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

Comment on lines +111 to +116
// 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"

Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
// 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"

Copilot uses AI. Check for mistakes.
Comment on lines 155 to 163
#### 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)
```
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines 288 to 321
## 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"
```
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines 131 to 135
Default OAuth App scopes (matching `gh` CLI minimal scopes):
- `repo` - Full control of private repositories
- `read:org` - Read org membership
- `gist` - Create gists

Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.

### Key Components

#### 1. Auth State Manager (`pkg/github/auth_state.go`)
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
#### 1. Auth State Manager (`pkg/github/auth_state.go`)
#### 1. Auth State Manager (`pkg/github/auth.go`)

Copilot uses AI. Check for mistakes.

// 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.",
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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.",

Copilot uses AI. Check for mistakes.
- 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!
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
- And much more, depending on configuration (mention the success and new tools to the user!
- And much more, depending on your GitHub configuration.

Copilot uses AI. Check for mistakes.
Comment on lines 18 to 22
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

Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines 51 to 66
### 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

Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +93
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,
}
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non PAT auth method

2 participants