feat: OAuth 2.0 support for HTTP MCP servers#258
Open
NamigGadir wants to merge 9 commits into
Open
Conversation
- Add McpCliOAuthProvider implementing OAuthClientProvider interface
- File-based token storage in ~/.mcp-cli/{tokens,clients,verifiers}
- Support authorization_code and client_credentials grant types
- Auto-create OAuth provider for all HTTP servers (enables server-initiated OAuth)
- Handle OAuth callback with local HTTP server on configurable port
- Cross-platform browser opening for authorization flow
- Detect OAuth errors from UnauthorizedError and invalid_token responses
This enables MCP servers like Linear that require OAuth authentication.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Start callback server BEFORE opening browser to fix race condition where browser redirects before server is ready - Add allowInteractiveAuth option to disable OAuth prompts when listing multiple servers (prevents multiple browsers opening) - Show helpful "requires authentication" message for unauthenticated servers when listing, with command to authenticate individually - Export AuthRequiredError and ConnectOptions from client module Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add port fallback mechanism: tries 80 → 8080 → 3000 → 8095 → random - Port 80 as default with standard URL format (http://localhost/callback) - Add pretty styled HTML pages for success/error callbacks - Add callbackPorts config option for custom port fallback list - Pre-start callback server to determine actual port before auth flow - Add comprehensive tests for new port fallback features Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When the callback server port changes between sessions (e.g., port 3000 was used during registration but port 8080 is available now), the OAuth authorization would fail with "Invalid redirect_uri" because the server expects the originally registered redirect_uri. Changes: - clientInformation() now validates stored redirect_uris match current redirectUrl, invalidating stale registrations that would cause errors - redirectToAuthorization() reuses pre-started callback server instead of starting a new one, ensuring consistent port usage throughout the OAuth flow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Split the 850-line oauth.ts into smaller, focused modules: - types.ts: Interfaces (OAuthConfig, OAuthCallbackResult) and constants - storage.ts: File storage utilities for tokens, clients, verifiers - browser.ts: Cross-platform browser opening utility - callback-server.ts: HTTP callback server with HTML templates - provider.ts: Main McpCliOAuthProvider class - index.ts: Re-exports for backwards compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
CLI NEVER opens browser - always returns auth URL for AI agents. Key changes: - Removed allowInteractiveAuth option entirely (CLI is for AI agents) - redirectToAuthorization() now captures auth URL, never opens browser - AuthRequiredError includes authorization URL for immediate action - Callback server runs in background (5 min timeout) - CLI returns immediately - List command shows working servers + auth URLs for servers needing login - Random port by default to avoid conflicts with multiple OAuth servers - Added comprehensive OAuth configuration docs to README This builds on the previous OAuth commits in this branch.
Two bugs prevented the non-blocking OAuth flow from ever completing: 1. Command handlers (info, call) and the top-level main() always called process.exit() right after reporting AuthRequiredError. This killed the whole process - including the in-process HTTP callback server - before the user had any chance to open the auth URL, let alone finish the redirect. Result: "connection refused" on the callback URL. 2. Nothing ever consumed the authorization code once the callback server captured it. McpCliOAuthProvider.waitForCallback() existed but was dead code - no caller awaited it or exchanged the code for tokens, so even a successful redirect would never persist a token. Fix: - client.ts now kicks off a fire-and-forget chain after throwing AuthRequiredError: waitForCallback() then SDK auth() with the received authorizationCode, then tokens saved via the provider. - New oauth/pending.ts tracks these in-flight completions so command handlers and main() can check hasPendingOAuth() before force-exiting, letting the event loop stay alive (kept open by the callback server's listening socket) until the flow finishes or the 5 minute timeout elapses. - info.ts / call.ts / index.ts updated to skip process.exit() when an OAuth completion is still pending, setting process.exitCode instead so the eventual natural exit still reports the right status. Manually verified end-to-end against mcp.atlassian.com: auth URL, browser login, callback received, code exchanged, token saved, mcp-cli info atlassian (31 tools) and a real tool call both succeed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the auth provider only captured the authorization URL for AI agents to relay to the user, requiring a manual copy/paste into the browser. This now automatically launches the URL in the system default browser (open/xdg-open/start) as soon as it's ready, while still capturing/printing it for agents and as a fallback if the browser can't be launched. - Add oauth.autoOpenBrowser config option (default: true) to opt out for headless/CI environments - Wire config through config.ts and oauth/types.ts - Update AuthRequiredError message to reflect auto-open behavior - Add tests covering auto-open default, opt-out, and URL capture - Update README and CHANGELOG Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OAuth access/refresh tokens are the most sensitive artifacts mcp-cli persists. Previously they were written to plaintext JSON files under ~/.mcp-cli/tokens/ (0600 permissions), readable by anything running as the same OS user — a real risk given how many npx/bun packages get executed in typical dev workflows. On macOS, tokens are now stored in the user's login Keychain via the `security` CLI, giving OS-level encryption at rest and Keychain ACLs. Non-macOS platforms fall back to the existing file-based storage unchanged. - Add src/oauth/keychain.ts: thin wrapper around `security` find/add/delete-generic-password, gated by isKeychainSupported() (darwin-only; disableable via MCP_CLI_DISABLE_KEYCHAIN=1 for tests) - provider.ts: tokens()/saveTokens() prefer Keychain, with automatic one-time migration of legacy plaintext token files into the Keychain; invalidateCredentials() also purges the Keychain entry - Add tests/keychain.test.ts with fully mocked `security` calls (no real Keychain access, safe on any platform/CI) - Fix tests/oauth.test.ts to set MCP_CLI_DISABLE_KEYCHAIN=1 in beforeEach/afterEach — these tests were previously writing real secrets into the developer's login Keychain with no cleanup Verified: typecheck, lint, full test suite (248/248), build, and a live end-to-end run against the Atlassian MCP server confirming automatic migration of an existing plaintext token into the Keychain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds OAuth 2.0 support for HTTP-based MCP servers, implementing the full MCP Authorization flow so servers like Notion, Linear, Atlassian, etc. work out of the box with just a
url— no manual OAuth app registration required for servers that support Dynamic Client Registration.What's included
.well-known/oauth-authorization-server(falling back to.well-known/openid-configuration) to find authorization/token/registration endpointsclientIdis configured~/.mcp-cli/tokens/, restrictive permissions) with automatic refreshtests/oauth.test.ts,tests/config.test.ts) and updated docs (README, CHANGELOG, SKILL.md)Why
Several popular hosted MCP servers (Atlassian, Notion, Linear, …) require OAuth rather than static API tokens/headers. Today there's no way to use them with mcp-cli without manually minting and refreshing a bearer token out-of-band. This closes that gap while keeping config fully backward compatible —
oauthis entirely optional, and servers without it behave exactly as before.Testing
bun run typecheck— passesbun run lint— passes (Biome, no issues)bun test— 231/232 pass; the 1 failure (grep command works, an unrelated network-dependent integration test) is flaky and passes in isolation, unaffected by this changemcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeededBackward compatibility
No breaking changes. Existing HTTP server configs (with or without static
headers) are unaffected. OAuth only activates when a server config opts in (or, per this branch's design, when the server signals it requires auth).