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.
Currently, users must:
- Generate a Personal Access Token (PAT) manually on GitHub
- Configure the token in their MCP host's configuration (often in plain text)
- Manage token rotation manually
This creates friction for new users and security concerns around token storage.
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:
auth_login- Initiates device flow, returns verification URL and user codeauth_verify- Completes the flow after user authorizes in browser
Once authenticated, the token is held in memory for the session and all regular tools become available.
{
"github": {
"command": "docker",
"args": ["run", "--rm", "-i", "ghcr.io/github/github-mcp-server", "stdio", "--toolsets=all"]
// No token needed! User authenticates via tool call
}
}- User asks agent: "Create an issue on my repo"
- Agent calls
auth_logintool - Tool returns:
To authenticate, visit: https://github.com/login/device Enter code: ABCD-1234 After authorizing, use the auth_verify tool to complete login. - User opens browser, enters code, clicks "Authorize"
- Agent calls
auth_verifytool - Tool returns: "Successfully authenticated as @username"
- Agent proceeds with original request using now-available tools
┌─────────────────────────────────────────────────────────────────┐
│ MCP Server │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Auth State │───▶│ Tool Filter │───▶│ GitHub Clients │ │
│ │ Manager │ │ │ │ (lazy init) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │ │ │
│ │ token │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Device Flow │ │ REST/GraphQL │ │
│ │ Handler │ │ Clients │ │
│ └──────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ UNAUTHENTICATED │ ◀──────────────────────────────┐
│ │ │
│ Tools: auth_* │ │
└────────┬────────┘ │
│ auth_login() │
▼ │
┌─────────────────┐ │
│ PENDING_AUTH │ │
│ │──── timeout/error ─────────────▶│
│ Tools: auth_* │ │
└────────┬────────┘ │
│ auth_verify() success │
▼ │
┌─────────────────┐ │
│ AUTHENTICATED │ │
│ │──── token invalid ─────────────▶│
│ Tools: all │ │
└─────────────────┘
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 |
The device flow requires an OAuth App. Options:
- GitHub-provided OAuth App (recommended) - We register a public OAuth App for this purpose
- User-provided OAuth App - Via
--oauth-client-idflag for enterprise scenarios
Default OAuth App scopes (matching gh CLI minimal scopes):
repo- Full control of private repositoriesread:org- Read org membershipgist- Create gists
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)// 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)When unauthenticated, only auth tools are registered. After successful auth:
- Initialize GitHub clients with new token
- Register all configured toolsets
- Send
tools/list_changednotification to client
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
- Token never in config - Token obtained at runtime, never written to disk (in --rm mode)
- Short-lived session - Token only valid for container lifetime
- Principle of least privilege - Request minimal scopes
- PKCE - Use PKCE extension for additional security (if supported)
- User verification - User explicitly authorizes in browser with full visibility
| 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 |
- Add
pkg/github/auth_state.go- Auth state management - Add
pkg/github/auth_tools.go- Auth tool implementations - Modify
internal/ghmcp/server.go- Support unauthenticated startup - Add device flow endpoint derivation for all host types
- Implement
tools/list_changednotification after auth - Add tool filtering based on auth state
- Update inventory to support dynamic registration
- Add comprehensive error messages
- Update README with new usage
- Add integration tests
- Document OAuth App setup for enterprises
// 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!
# 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{
"servers": {
"github": {
"command": "github-mcp-server",
"args": ["stdio", "--gh-host", "https://github.mycompany.com"],
"type": "stdio"
}
}
}{
"servers": {
"github": {
"command": "github-mcp-server",
"args": ["stdio"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxx"
},
"type": "stdio"
}
}
}- OAuth App ownership - Should GitHub provide a first-party OAuth App, or require users to create their own?
- Token refresh - Should we support refresh tokens for longer sessions, or is re-auth acceptable?
- Scope customization - Should users be able to request additional scopes via tool parameters?
- Persistent storage - Should we support optional persistent token storage for non-Docker installs?
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"
{ "githubz": { "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 } } }