feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: OAuth 2.0 support for HTTP MCP servers - #258

Open
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support
Open

feat: OAuth 2.0 support for HTTP MCP servers#258
NamigGadir wants to merge 9 commits into
philschmid:mainfrom
NamigGadir:feat/oauth-support

Conversation

@NamigGadir

Copy link
Copy Markdown

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

  • Auto-discovery: reads .well-known/oauth-authorization-server (falling back to .well-known/openid-configuration) to find authorization/token/registration endpoints
  • Dynamic Client Registration (RFC 7591): registers an OAuth client automatically when no clientId is configured
  • PKCE authorization code flow with a local callback server and automatic browser launch
  • Non-blocking flow for AI agents: instead of hanging the process waiting for the user to click through a browser, the CLI spawns a background callback server, returns immediately with an auth URL for any server that needs it, and picks up the cached token on the next invocation — so an agent can surface the link, wait for the human, and retry
  • Token + client caching (~/.mcp-cli/tokens/, restrictive permissions) with automatic refresh
  • Stale client detection: re-registers automatically if a cached DCR client's redirect_uri no longer matches (e.g. after a port change)
  • Callback port fallback: prefers a stable default port so registered clients stay valid across runs, falls back to a random port if busy
  • Full test suite (tests/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 — oauth is entirely optional, and servers without it behave exactly as before.

Testing

  • bun run typecheck — passes
  • bun 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 change
  • Manually verified end-to-end against the live Atlassian MCP server (mcp.atlassian.com): discovery → DCR → browser authorization → token exchange → tool listing (31 tools) → a real tool call (atlassianUserInfo), all succeeded

Backward 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).

attehuhtakangasand others added 9 commits January 27, 2026 22:59
- 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>
Sign up for freeto 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.

3 participants

@NamigGadir@attehuhtakangas@philschmid