Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 17, 2026

Summary

Implements OAuth 2.1 authentication for stdio mode with automatic environment detection. Uses interactive PKCE flow for native binaries and device flow for Docker containers. Falls back to PAT if OAuth not configured.

Why

stdio users currently must manually create and manage Personal Access Tokens. OAuth provides a better UX with interactive browser consent while maintaining security through PKCE. Docker distribution requires device flow since callback servers don't work in containers.

What changed

  • New internal/oauth package implementing OAuth 2.1 with dual flow support:
    • Interactive PKCE flow: For native binaries with automatic browser launch
    • Device flow: For Docker containers (displays verification URL + code)
    • Automatic environment detection (Docker vs native)
    • 5-minute configurable timeout with context cancellation
  • CLI flags: --oauth-client-id, --oauth-client-secret, --oauth-scopes, --oauth-callback-port
  • OAuth endpoints adapt to --gh-host for GHES/GHEC support
  • Default scopes: repo, user, gist, notifications, read:org, project
  • Fixed port support for advanced Docker users with port binding
  • Enhanced documentation:
    • Quick Setup section with simplest configuration paths
    • Pre-configured MCP Host Setup with ready-to-use examples for Claude and VS Code
    • Configuration snippets for automated distribution
    • All examples include both client ID and secret (GitHub OAuth app best practice)
  • Future enhancement identified: URL mode elicitation (official MCP spec) can enhance UX:
    • PKCE flow: URL elicitation as fallback when browser auto-open fails
    • Device flow: URL elicitation to display device code/URL via MCP client UI (instead of stderr)
    • Both flows benefit from URL elicitation for better integration with MCP clients

MCP impact

  • No tool or API changes

Security / limits

  • Auth / permissions considered
    • Uses golang.org/x/oauth2 (no custom auth code)
    • PKCE S256 prevents code interception (interactive flow)
    • Device flow for Docker (standard OAuth 2.0 device authorization)
    • State parameter prevents CSRF
    • ReadHeaderTimeout prevents Slowloris
    • Tokens never persisted to disk
    • Client secret recommended for all GitHub OAuth app configurations

Tool renaming

  • I am not renaming tools as part of this PR

Lint & tests

  • Linted locally with ./script/lint
  • Tested locally with ./script/test

Docs

  • Updated (README / docs / examples)
    • New "OAuth Authentication (stdio mode)" section
    • Quick Setup guides for all scenarios
    • Pre-configured MCP host setup examples (Claude, VS Code)
    • Docker-specific OAuth instructions
    • Device flow vs interactive flow explanation
    • Usage examples for native, Docker, Docker with port binding
    • All examples include both client ID and client secret

Usage:

# Native binary - interactive PKCE flow (automatic)
export GITHUB_OAUTH_CLIENT_ID=Iv1.abc123
export GITHUB_OAUTH_CLIENT_SECRET=your_secret
./github-mcp-server stdio
# Browser opens automatically ✓

# Docker - device flow (automatic)
docker run -i --rm \
  -e GITHUB_OAUTH_CLIENT_ID=Iv1.abc123 \
  -e GITHUB_OAUTH_CLIENT_SECRET=your_secret \
  ghcr.io/github/github-mcp-server
# Displays: Visit https://github.com/login/device and enter code: ABCD-1234 ✓

# Docker with port binding - interactive PKCE flow (can be pre-configured)
docker run -i --rm -p 8080:8080 \
  -e GITHUB_OAUTH_CLIENT_ID=Iv1.abc123 \
  -e GITHUB_OAUTH_CLIENT_SECRET=your_secret \
  -e GITHUB_OAUTH_CALLBACK_PORT=8080 \
  ghcr.io/github/github-mcp-server
# Browser opens automatically (callback works via bound port) ✓

Pre-configured for MCP Hosts:

# Claude
claude mcp add github \
  -e GITHUB_OAUTH_CLIENT_ID=your_id \
  -e GITHUB_OAUTH_CLIENT_SECRET=your_secret \
  -e GITHUB_OAUTH_CALLBACK_PORT=8080 \
  -- docker run -i --rm -p 8080:8080 \
     -e GITHUB_OAUTH_CLIENT_ID \
     -e GITHUB_OAUTH_CLIENT_SECRET \
     -e GITHUB_OAUTH_CALLBACK_PORT \
     ghcr.io/github/github-mcp-server

# VS Code settings.json
{
  "mcp": {
    "servers": {
      "github": {
        "command": "docker",
        "args": ["run", "-i", "--rm", "-p", "8080:8080", 
                 "-e", "GITHUB_OAUTH_CLIENT_ID", 
                 "-e", "GITHUB_OAUTH_CLIENT_SECRET",
                 "-e", "GITHUB_OAUTH_CALLBACK_PORT", 
                 "ghcr.io/github/github-mcp-server"],
        "env": {
          "GITHUB_OAUTH_CLIENT_ID": "${input:github_oauth_client_id}",
          "GITHUB_OAUTH_CLIENT_SECRET": "${input:github_oauth_client_secret}",
          "GITHUB_OAUTH_CALLBACK_PORT": "8080"
        }
      }
    }
  }
}

Flow Selection (Automatic):

  • Docker without port binding → Device flow
  • Native binary → Interactive PKCE flow
  • Docker with --oauth-callback-port → Interactive PKCE flow

Interactive PKCE Flow:

  1. Server generates PKCE verifier/challenge
  2. Browser opens to GitHub authorization
  3. User approves → GitHub redirects to localhost
  4. Server exchanges code+verifier for token

Device Flow:

  1. Server requests device code from GitHub
  2. Displays verification URL and user code
  3. User visits URL in browser and enters code
  4. Server polls for token completion

OAuth Best Practice:

  • Client secret is recommended for all GitHub OAuth apps
  • Follows standard GitHub OAuth app configuration
  • Client secret only allows users to authenticate as themselves
  • All examples include both client ID and client secret

OAuth Flow Architecture:

The implementation uses a binary decision based on environment:

  • Can use callback server (port available/bound) → PKCE Flow
    • Try auto-open browser
    • Future: If fails → URL elicitation with OAuth URL
  • Cannot use callback server (Docker without port) → Device Flow
    • Future: Use URL elicitation to display device code + verification URL

URL Elicitation Integration (Future Enhancement):

MCP SDK v1.2.0 supports URL mode elicitation (official spec). This can enhance both flows:

  • PKCE flow: Use URL elicitation as fallback when browser auto-open fails
  • Device flow: Use URL elicitation to display device code/URL via MCP client UI (better than stderr)

URL elicitation provides native integration with MCP client UI for OAuth prompts. Current implementation works with all clients by using browser auto-open (PKCE) and stderr output (device flow). Adding URL elicitation would improve UX when MCP clients support it.

stdio-only by design. Remote server continues using PATs. Port binding setup is straightforward and can be automated through installation instructions and distributed configurations.

Original prompt

Is there a way you could implement a standard oauth flow on initialize or in a pre-request middleware or something in the stdio startup rather than device flow like in #1649 where we use a standard golang library that can provide interactive oauth, and complete the oauth flow as a standard rather than device flow, still taking an optional app, but without all the complexity, just the user starts the server, and if no token is present it does an interactive oauth flow and then logs in. If it cannot be seamlessly added into the package, we could emulatte 1649 that might have to be it. Maybe it can be any tool call, where it kicks off the oauth flow.

We only want this to be in STDIO mode, as this repo is used as a library for a remote server.

If it helps we can also use our own protected resource metadata and URL https://api.githubcopilot.com/.well-known/oauth-protected-resource/mcp which is for https://api.githubcopilot.com/mcp

I don't know that that helps, but it's the same concept and same owner and authorization details are there.

If there is prior art and you can think a good way to get the auth challenge to the user, I think we might still need secure URL elicitation, as we cannot do it interactively otherwise while ensuring the user sees the link.

Anyway, make it a relatively small change, try to keep code to an auth specific module, and hook into the server as cleanly as possible. This is the work of an expert staff and auth engineer who knows above all else, you don't roll your own auth code, you lean into the existing tested and autited implementations. Our authorization also suppors PKCE so you should be able to do oauth 2.1 other than requiring an app so Dynamic Client Registration is not possible.

Do a good, safe job so stdio users can have access to GitHub MCP.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 5 commits January 17, 2026 23:47
- Create internal/oauth package with standard OAuth implementation
- Use golang.org/x/oauth2 for PKCE flow with S256 challenge
- Support interactive browser-based authorization
- Add CLI flags: --oauth-client-id, --oauth-client-secret, --oauth-scopes
- Respect --gh-host for GHES/GHEC OAuth endpoints
- Secure implementation with ReadHeaderTimeout, state validation
- Comprehensive tests and linting passing

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
- Document OAuth 2.1 PKCE flow for stdio mode
- Include setup instructions for GitHub OAuth apps
- Document environment variables and CLI flags
- Add usage examples with custom scopes
- Note that OAuth is stdio-only (not for remote server)

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
- Add DefaultAuthTimeout constant for configurability
- Fix string index check to handle idx >= 0 correctly
- Use cmd.Context() for proper cancellation support
- Add named constant for PKCE verifier length in test
- Remove unused context import

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
- Clarify string index boundary check logic (idx > 0 is correct)
- Update error message to show both flag and env var options
- Add comment explaining the boundary check reasoning

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
- Linting: ✅ 0 issues
- Tests: ✅ All passing
- CodeQL: ✅ 0 security alerts
- Build: ✅ Binary builds successfully

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
Copilot AI changed the title [WIP] Add standard OAuth flow in STDIO mode Add OAuth 2.1 PKCE authentication for stdio mode Jan 17, 2026
Copilot AI requested a review from SamMorrowDrums January 17, 2026 23:56
@SamMorrowDrums
Copy link
Collaborator

@copilot this is a great start but the binary is normally distributed through docker, so we need to do more of the pattern in #1677

Although perhaps, if out of band server sent secure URL elicitation outside of a tool call is possible, if opening a browser fails we should try secure url elicitation sent from server after initialize, and if that doesn't work, we could log the url... but wait the callback url won't won't from the host system in docker.

So we might have to fall back to device flow if all other options are exhausted...

This is tough. Docker massively complicates things. Can't have a random port open. It would need to be predefined and accessible from localhost on the host system, so port would need to be bound explicitly in docker startup command.

Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot this is a great start but the binary is normally distributed through docker, so we need to do more of the pattern in #1677

Although perhaps, i...

You're absolutely right - Docker makes the callback server approach impossible since the container's localhost isn't accessible from the host's browser.

I'll implement a multi-strategy approach:

  1. Try interactive PKCE flow first (current implementation) - works for native binary
  2. Auto-detect Docker environment - check if running in container
  3. Fall back to device flow in Docker - uses user code + verification URL pattern that works without callback server
  4. Optionally support fixed port - via --oauth-callback-port flag for advanced Docker users who bind ports

This way:

  • Native binary users get the best UX (browser opens automatically)
  • Docker users get device flow (displays code + URL, no callback needed)
  • Advanced users can bind a port and use PKCE in Docker

Working on this now.

- Add StartDeviceFlow for environments without callback capabilities
- Add StartOAuthFlow that auto-selects between device and interactive flows
- Detect Docker environment and use device flow automatically
- Add --oauth-callback-port flag for advanced Docker users with port binding
- Support fixed ports in callback server for Docker -p usage
- Update all OAuth endpoints to include device auth URL
- Comprehensive tests for both flows and port configurations
- Update README with Docker-specific OAuth instructions

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
Copilot AI changed the title Add OAuth 2.1 PKCE authentication for stdio mode Add OAuth 2.1 authentication for stdio mode with Docker support Jan 18, 2026
@SamMorrowDrums
Copy link
Collaborator

@copilot the port binding isn't hard because we also distribute the configuration as you can see in the README so discovery of how to do this can be simple and automated, via installation instructions.

- Add "Quick Setup" section showing simplest paths
- Add "Pre-configured MCP Host Setup" with examples
- Show Claude and VS Code configuration patterns
- Emphasize that port binding is straightforward and automated
- Provide ready-to-use config snippets for distribution

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot the port binding isn't hard because we also distribute the configuration as you can see in the README so discovery of how to do this can be simple and automated, via installation instructions...

Absolutely right! I've added a "Pre-configured MCP Host Setup" section with ready-to-use configuration examples for Claude and VS Code. The examples show how port binding can be distributed as simple config snippets, making OAuth setup as straightforward as PAT configuration.

The documentation now includes:

  • Quick setup guides for all scenarios
  • Copy-paste config examples for MCP hosts
  • Clear patterns for automated installation

See commit 3278eee for the enhanced documentation.

@SamMorrowDrums
Copy link
Collaborator

@copilot its normal to provide client secret always either GitHub app, and it only allows user to auth as themselves, so I would always recommend examples and config assume both are present.

- Remove "optional" language for client secret
- Add client secret to all code examples
- Update CLI flag description to say "recommended"
- Update code comment from "Optional" to "Recommended"
- Align with best practice that GitHub OAuth apps should provide both ID and secret

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
@SamMorrowDrums
Copy link
Collaborator

@copilot modelcontextprotocol/modelcontextprotocol#1036 is how secure url mode elicitation is specified. Is that something you can introduce, and does it support unsolicited server events via stdio because the ideal would be we prompt the user.

Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot its normal to provide client secret always either GitHub app, and it only allows user to auth as themselves, so I would always recommend examples and config assume both are present.

Updated all examples and documentation to include both client ID and secret. Removed "optional" language and updated to recommend providing client secret for all GitHub OAuth app configurations.

Changes in commit 76defa8:

  • All code examples now include GITHUB_OAUTH_CLIENT_SECRET
  • Updated CLI flag description to "recommended"
  • Updated code comments
  • Pre-configured MCP host examples include both credentials

This aligns with the best practice that GitHub OAuth apps should provide both credentials.

@SamMorrowDrums
Copy link
Collaborator

@copilot to clarify auto opening browser is best, but failing that elicit a url login is best

Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot modelcontextprotocol/modelcontextprotocol#1036 is how secure url mode elicitation is specified. Is that something you can introduce, and does it support unsolicited ...

Yes, the MCP SDK v1.2.0 we're using already supports URL mode elicitation! This is perfect for the OAuth flow.

The implementation would:

  1. Server sends URL elicitation request via mcp.ElicitParams with Mode: "url", URL, ElicitationID, and Message
  2. Client prompts user to open the OAuth URL in their browser
  3. OAuth happens out-of-band (in browser, outside MCP)
  4. Server sends completion notification via ElicitationCompleteParams when OAuth completes
  5. Client can retry the original request automatically

This supports unsolicited server events via stdio - the server can send the elicitation request at any time, and later send the completion notification. The client would handle prompting the user to open the URL.

However, this requires client support for URL elicitation. Since this is a proposal (SEP), not all MCP clients may support it yet. The current implementation with automatic environment detection (device flow for Docker, PKCE for native) works with all clients today.

Should we add URL elicitation as a third option that falls back to the current approaches if the client doesn't declare URL elicitation capability?

@SamMorrowDrums
Copy link
Collaborator

@copilot its no longer a proposal, it is part of the spec 2 months, and we cannot do much else given other constraints. User can always provide PAT up front.

Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot to clarify auto opening browser is best, but failing that elicit a url login is best

Understood. The priority order would be:

  1. Auto-open browser (current PKCE flow for native binaries) ✓ implemented
  2. URL elicitation fallback (if browser auto-open fails) - can implement with SDK support
  3. Device flow (for Docker or if URL elicitation not supported) ✓ implemented

The current implementation handles #1 and #3. We can add #2 as middleware: try auto-open → if fails, send URL elicitation request → if client doesn't support it, fall back to device flow.

This would require:

  • Detecting when openBrowser() fails in the PKCE flow
  • Checking if client declared URL elicitation capability during initialization
  • Sending mcp.ElicitParams with the OAuth URL
  • Waiting for user to complete OAuth in browser
  • Continuing with token exchange

Should I implement this fallback strategy?

Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot its no longer a proposal, it is part of the spec 2 months, and we cannot do much else given other constraints. User can always provide PAT up front.

Understood - URL elicitation is now part of the official spec and should be implemented. I'll add it as the fallback between auto-open browser and device flow.

Implementation plan:

  1. Try auto-open browser (PKCE flow)
  2. If browser auto-open fails → send URL elicitation request via MCP
  3. If client doesn't support URL elicitation → fall back to device flow

This gives the best UX while maintaining compatibility with all clients. Working on this now.

@SamMorrowDrums
Copy link
Collaborator

@copilot That three step pattern seems almost great to me! But the device flow also requires elicitation. So that's not really a fallback. So I think we probably need to work out if we can use the oauth port for standard flow, and if not do the device flow, and secure url elicitation is requirement for the device flow.

So it's not really a 3 part fallback, callback or device flow needs to be decided up front and then elicitation is both a fallback for failing to open Browser for standard flow but also a requirement for device flow in my book.

Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot That three step pattern seems almost great to me! But the device flow also requires elicitation. So that's not really a fallback. So I think we probably need to work out if we can use the oau...

You're absolutely right - I was thinking about it wrong. The correct architecture is:

Decision point (upfront based on environment):

  • Can use callback server? (port available/bound) → PKCE Flow
    • Try auto-open browser
    • If fails → URL elicitation with OAuth URL
  • Cannot use callback server? (Docker without port, no callback possible) → Device Flow
    • Always use URL elicitation to display device code + verification URL

URL elicitation is not a fallback between flows - it's:

  1. A fallback within PKCE flow (when browser auto-open fails)
  2. A requirement for device flow (to display the code/URL to user)

Current implementation has the binary choice (PKCE vs Device) but doesn't use URL elicitation for either yet. Should I implement URL elicitation for both flows?

@SamMorrowDrums
Copy link
Collaborator

@copilot that is correct please implement.

Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot that is correct please implement.

I've identified an architectural constraint: the OAuth flow currently happens before the MCP server starts (we need a token to initialize the server), but URL elicitation requires an active MCP session to send requests to the client.

To implement URL elicitation properly, we would need to:

  1. Start the MCP server without a token (allow initialization)
  2. Defer OAuth until after client connects
  3. Use URL elicitation to prompt user during first tool call that requires auth
  4. This requires significant refactoring of server initialization

An alternative approach would be to implement a lighter version now:

  • Keep current OAuth-before-server pattern
  • Add URL elicitation support for when server needs re-authentication during operation
  • This would cover token expiry scenarios

Which approach would you prefer? Or should I proceed with the full architectural refactor to enable OAuth after server initialization?

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.

2 participants