Skip to content
Open
11 changes: 7 additions & 4 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"errors"
"fmt"
"os"
"strings"
Expand Down Expand Up @@ -32,10 +31,8 @@ var (
Short: "Start stdio server",
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
RunE: func(_ *cobra.Command, _ []string) error {
// Token is optional - if not provided, server starts in auth mode
token := viper.GetString("personal_access_token")
if token == "" {
return errors.New("GITHUB_PERSONAL_ACCESS_TOKEN not set")
}

// If you're wondering why we're not using viper.GetStringSlice("toolsets"),
// it's because viper doesn't handle comma-separated values correctly for env
Expand Down Expand Up @@ -84,6 +81,8 @@ var (
ContentWindowSize: viper.GetInt("content-window-size"),
LockdownMode: viper.GetBool("lockdown-mode"),
RepoAccessCacheTTL: &ttl,
OAuthClientID: viper.GetString("oauth-client-id"),
OAuthClientSecret: viper.GetString("oauth-client-secret"),
}
return ghmcp.RunStdioServer(stdioServerConfig)
},
Expand All @@ -109,6 +108,8 @@ func init() {
rootCmd.PersistentFlags().Int("content-window-size", 5000, "Specify the content window size")
rootCmd.PersistentFlags().Bool("lockdown-mode", false, "Enable lockdown mode")
rootCmd.PersistentFlags().Duration("repo-access-cache-ttl", 5*time.Minute, "Override the repo access cache TTL (e.g. 1m, 0s to disable)")
rootCmd.PersistentFlags().String("oauth-client-id", "", "OAuth App client ID for device flow authentication (optional, uses default if not provided)")
rootCmd.PersistentFlags().String("oauth-client-secret", "", "OAuth App client secret for device flow authentication (optional, for confidential clients)")

// Bind flag to viper
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
Expand All @@ -123,6 +124,8 @@ func init() {
_ = viper.BindPFlag("content-window-size", rootCmd.PersistentFlags().Lookup("content-window-size"))
_ = viper.BindPFlag("lockdown-mode", rootCmd.PersistentFlags().Lookup("lockdown-mode"))
_ = viper.BindPFlag("repo-access-cache-ttl", rootCmd.PersistentFlags().Lookup("repo-access-cache-ttl"))
_ = viper.BindPFlag("oauth-client-id", rootCmd.PersistentFlags().Lookup("oauth-client-id"))
_ = viper.BindPFlag("oauth-client-secret", rootCmd.PersistentFlags().Lookup("oauth-client-secret"))

// Add subcommands
rootCmd.AddCommand(stdioCmd)
Expand Down
321 changes: 321 additions & 0 deletions docs/auth-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
# OAuth Device Flow Authentication Design

## Overview

This document describes the implementation of OAuth Device Flow authentication for the GitHub MCP Server's stdio transport. The design enables users to authenticate without pre-configuring tokens, making setup significantly simpler.

## Problem Statement

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.

## Proposed Solution

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.
Once authenticated, the token is held in memory for the session and all regular tools become available.

## User Experience

### Before (Current)
```jsonc
{
"githubz": {
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 JSON key. It says "githubz" but should be "github" to be consistent with the "After" example and typical configuration patterns.

Suggested change
"githubz": {
"github": {

Copilot uses AI. Check for mistakes.
"command": "docker",
"args": ["run", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}" // User must create PAT first
}
}
}
```

### After (New)
```jsonc
{
"github": {
"command": "docker",
"args": ["run", "--rm", "-i", "ghcr.io/github/github-mcp-server", "stdio", "--toolsets=all"]
// No token needed! User authenticates via tool call
}
}
```

### 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.
## Technical Design

### Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ MCP Server │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Auth State │───▶│ Tool Filter │───▶│ GitHub Clients │ │
│ │ Manager │ │ │ │ (lazy init) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │ │ │
│ │ token │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Device Flow │ │ REST/GraphQL │ │
│ │ Handler │ │ Clients │ │
│ └──────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```

### State Machine

```
┌─────────────────┐
│ UNAUTHENTICATED │ ◀──────────────────────────────┐
│ │ │
│ Tools: auth_* │ │
└────────┬────────┘ │
│ auth_login() │
▼ │
┌─────────────────┐ │
│ PENDING_AUTH │ │
│ │──── timeout/error ─────────────▶│
│ Tools: auth_* │ │
└────────┬────────┘ │
│ auth_verify() success │
▼ │
┌─────────────────┐ │
│ AUTHENTICATED │ │
│ │──── token invalid ─────────────▶│
│ Tools: all │ │
└─────────────────┘
```
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 state machine diagram includes a "PENDING_AUTH" state, but in the actual auth.go implementation, the state is called "AuthStatePending" (not "PENDING_AUTH"). The diagram should use the actual constant names for accuracy, or the constants should match the design document.

Copilot uses AI. Check for mistakes.

### Host URL Derivation

For different GitHub products, device flow endpoints are derived from the configured host:

| Product | Host Config | Device Code Endpoint |
|---------|-------------|---------------------|
| github.com | (default) | `https://github.com/login/device/code` |
| GHEC | `https://tenant.ghe.com` | `https://tenant.ghe.com/login/device/code` |
| GHES | `https://github.example.com` | `https://github.example.com/login/device/code` |

### OAuth App Requirements

The device flow requires an OAuth App. Options:
1. **GitHub-provided OAuth App** (recommended) - We register a public OAuth App for this purpose
2. **User-provided OAuth App** - Via `--oauth-client-id` flag for enterprise scenarios

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.

```go
type AuthState struct {
mu sync.RWMutex
token string
deviceCode *DeviceCodeResponse
pollInterval time.Duration
expiresAt time.Time
}

func (a *AuthState) IsAuthenticated() bool
func (a *AuthState) GetToken() string
func (a *AuthState) StartDeviceFlow(ctx context.Context, host apiHost, clientID string) (*DeviceCodeResponse, error)
func (a *AuthState) CompleteDeviceFlow(ctx context.Context) (string, error)
```

#### 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.

#### 3. Dynamic Tool Registration

When unauthenticated, only auth tools are registered. After successful auth:
1. Initialize GitHub clients with new token
2. Register all configured toolsets
3. Send `tools/list_changed` notification to client

### Docker Considerations

With `--rm` containers:
- Token lives only in memory for the session duration
- User re-authenticates each time container starts
- This is acceptable UX since device flow is quick (~30 seconds)

For persistent auth (optional future enhancement):
- Mount a config volume: `-v ~/.config/github-mcp-server:/config`
- Server stores encrypted token in volume
- Requires user opt-in for security

### Security Considerations

1. **Token never in config** - Token obtained at runtime, never written to disk (in --rm mode)
2. **Short-lived session** - Token only valid for container lifetime
3. **Principle of least privilege** - Request minimal scopes
4. **PKCE** - Use PKCE extension for additional security (if supported)
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 Security Considerations section mentions "PKCE - Use PKCE extension for additional security (if supported)" but the implementation in auth.go doesn't actually implement PKCE. This security consideration should either be marked as a future enhancement/TODO, or the documentation should be updated to remove this claim since it's not currently implemented.

Suggested change
4. **PKCE** - Use PKCE extension for additional security (if supported)
4. **PKCE (future enhancement)** - Plan to add PKCE extension for additional security in a future implementation

Copilot uses AI. Check for mistakes.
5. **User verification** - User explicitly authorizes in browser with full visibility

### Error Handling

| Scenario | Behavior |
|----------|----------|
| Device flow timeout | Return error, user can retry `auth_login` |
| User denies authorization | Return error explaining denial |
| Network issues during poll | Retry with backoff, eventually timeout |
| Invalid client ID | Clear error message with setup instructions |
| Token expires mid-session | Return 401-like error, prompt re-auth via tools |

## Implementation Plan

### Phase 1: Core Auth Flow
1. Add `pkg/github/auth_state.go` - Auth state management
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 Implementation Plan references "pkg/github/auth_state.go" but the actual file is "pkg/github/auth.go". This should be corrected to reflect the actual filename used in the implementation.

Suggested change
1. Add `pkg/github/auth_state.go` - Auth state management
1. Add `pkg/github/auth.go` - Auth state management

Copilot uses AI. Check for mistakes.
2. Add `pkg/github/auth_tools.go` - Auth tool implementations
3. Modify `internal/ghmcp/server.go` - Support unauthenticated startup
4. Add device flow endpoint derivation for all host types

### Phase 2: Dynamic Tool Registration
1. Implement `tools/list_changed` notification after auth
2. Add tool filtering based on auth state
3. Update inventory to support dynamic registration

### Phase 3: Polish & Documentation
1. Add comprehensive error messages
2. Update README with new usage
3. Add integration tests
4. Document OAuth App setup for enterprises

## Usage Documentation

### Quick Start (New Users)

```jsonc
// VS Code settings.json or mcp.json
{
"servers": {
"github": {
"command": "docker",
"args": ["run", "--rm", "-i", "ghcr.io/github/github-mcp-server", "stdio"],
"type": "stdio"
}
}
}
```

Then just ask your AI assistant to do something with GitHub - it will guide you through authentication!

### Native Installation

```bash
# Install
go install github.com/github/github-mcp-server/cmd/github-mcp-server@latest

# Run (will prompt for auth on first GitHub operation)
github-mcp-server stdio
```

### Enterprise (GHES/GHEC)

```jsonc
{
"servers": {
"github": {
"command": "github-mcp-server",
"args": ["stdio", "--gh-host", "https://github.mycompany.com"],
"type": "stdio"
}
}
}
```

### With Pre-configured Token (Legacy/CI)

```jsonc
{
"servers": {
"github": {
"command": "github-mcp-server",
"args": ["stdio"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxx"
},
"type": "stdio"
}
}
}
```

## Open Questions

1. **OAuth App ownership** - Should GitHub provide a first-party OAuth App, or require users to create their own?
2. **Token refresh** - Should we support refresh tokens for longer sessions, or is re-auth acceptable?
3. **Scope customization** - Should users be able to request additional scopes via tool parameters?
4. **Persistent storage** - Should we support optional persistent token storage for non-Docker installs?

## 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.
Loading