Skip to content

feat(core): support pre-registered OAuth clients for remote MCP - #12983

Merged
saoudrizwan merged 9 commits into
mainfrom
bee/mcp-oauth
Aug 7, 2026
Merged

feat(core): support pre-registered OAuth clients for remote MCP#12983
saoudrizwan merged 9 commits into
mainfrom
bee/mcp-oauth

Conversation

@abeatrix

@abeatrix abeatrix commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

Issue: CLIENTS-74

Description

Enable MCP servers that do not support OAuth Dynamic Client Registration, such as GitHub’s remote MCP server, to authenticate using pre-registered OAuth client credentials.

Problem

The MCP OAuth flow assumed that authorization servers support Dynamic Client Registration. Servers without a registration endpoint failed before browser authorization with:

Incompatible auth server: does not support dynamic client registration

OAuth flow in Desktop are not wired up.

Root cause

The SDK only loaded dynamically registered client information from mutable OAuth state. There was no configuration interface for supplying an existing OAuth client ID and secret.

Fix

  • Add an oauthClient MCP configuration containing a client ID and optional client secret.
  • Prefer configured client credentials during OAuth authorization.
  • Retain Dynamic Client Registration when no client ID is provided.
  • Prompt for pre-registered credentials when configuring OAuth through the CLI.
  • Persist and clear the client configuration alongside the server’s OAuth settings.
  • Export the new configuration type through @cline/core.

This allows services such as GitHub to use their pre-registered OAuth application while preserving the existing flow for servers that support dynamic registration.

Test Procedure

bun run cli mcp install github --transport http https://api.githubcopilot.com/mcp/

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • ♻️ Refactor Changes
  • 💅 Cosmetic Changes
  • 📚 Documentation update
  • 🏃 Workflow Changes

Pre-flight Checklist

  • Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
  • Tests are passing (bun test) and code is formatted and linted (bun run format && bun run lint)
  • I have reviewed contributor guidelines

Screenshots

Connect should open the oauth flow in browser

image

Additional Notes

@cline-for-jetbrains-workflow

Copy link
Copy Markdown

JetBrains Plugin tests failed

⚠️ Action Required: This PR needs to be updated to ensure compatibility with both cline-core and JetBrains.

The changes in this PR are causing test failures in the JetBrains plugin integration. Please review and fix your changes to ensure they work properly with:

  • The cline-core functionality
  • JetBrains IDE integration

Please check the workflow logs for specific test failure details and update your PR accordingly.

Branch: bee/mcp-oauth
Workflow: View run

@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds pre-registered OAuth-client support for remote MCP servers across core, CLI, and the desktop example.

  • Adds configured client credentials and client-bound OAuth persistence.
  • Adds explicit, cancellable desktop browser authorization and passive connection probing.
  • Normalizes remote-proxy installations into native MCP transports.

Confidence Score: 4/5

The PR is not safe to merge until first-time authorization with a pre-registered OAuth client can persist its PKCE verifier.

The MCP SDK saves the PKCE verifier before token exchange, but the provider currently requires matching client information to already be persisted, so a newly configured pre-registered client fails before the browser authorization step.

Files Needing Attention: sdk/packages/core/src/extensions/mcp/oauth.ts

Important Files Changed

Filename Overview
sdk/packages/core/src/extensions/mcp/oauth.ts Adds client-bound, concurrency-safe OAuth persistence, but the initial PKCE verifier write rejects a newly configured pre-registered client before authorization can begin.
sdk/packages/core/src/extensions/mcp/config-loader.ts Adds OAuth-client parsing, guarded state mutation, per-server resolution, and OAuth status projection.
apps/cli/src/wizards/mcp/settings.ts Persists configured OAuth clients and clears old OAuth state when client credentials change.
apps/examples/desktop-app/sidecar/commands.ts Adds explicit desktop OAuth authorization, cancellation, passive probing, and safer MCP settings updates.
apps/examples/desktop-app/webview/components/views/settings/mcp-view.tsx Surfaces OAuth status and explicit connect/cancel controls for remote MCP servers.

Sequence Diagram

sequenceDiagram
  participant Host
  participant Core as MCP OAuth Provider
  participant SDK as MCP SDK
  participant Browser
  Host->>Core: authorize configured server
  Core->>SDK: connect with pre-registered client
  SDK->>Core: saveCodeVerifier
  Core-->>SDK: McpOAuthClientChangedError
  Note over SDK,Browser: Browser authorization never opens
Loading
Prompt To Fix All With AI
### Issue 1
sdk/packages/core/src/extensions/mcp/oauth.ts:270-274
**Initial PKCE verifier is rejected**

When a pre-registered client starts its first OAuth authorization with no persisted `clientInformation`, `saveCodeVerifier` compares the empty state against the configured client and throws `McpOAuthClientChangedError`, preventing the browser authorization flow from starting.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (9): Last reviewed commit: "Merge branch 'main' into bee/mcp-oauth" | Re-trigger Greptile

Comment thread apps/cli/src/wizards/mcp/index.ts
Comment thread sdk/packages/core/src/extensions/mcp/oauth.ts
Comment thread sdk/packages/core/src/extensions/mcp/oauth.ts Fixed
@cursor cursor Bot mentioned this pull request Aug 6, 2026
10 tasks
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Status review — two things here:

1. The CodeQL / github-advanced-security alert is stale and should be dismissed. It fired against the intermediate commit 8f2ffa9, which introduced fingerprintOAuthClient (unsalted single-round SHA-256 over client_id + "\0" + client_secret persisted as tokenClientFingerprint). Commit 072dd12 removed the fingerprint entirely and replaced it with the direct isSameOAuthClient comparison against persisted clientInformation — the final diff contains no hashing at all, and the GHAS comment now has an outdated position. The residual fact that clientSecret sits in plaintext in cline_mcp_settings.json is pre-existing behavior (dynamically-registered client_secret and OAuth tokens were already persisted there), not a new regression.

2. One wiring gap I'd like addressed (or explained) before merge: DefaultMcpServerClient.connect() creates its OAuth provider contexts (client.ts ~566 and the handleOperationError fallback ~678) without passing this.registration.oauthClient — only authorizeMcpServerOAuth in oauth.ts wires the configured client in. It works today only because saveTokens persists the configured clientInformation into oauth state during authorize. Two consequences: (a) a user who hand-edits oauthClient in the settings file (outside the CLI wizard) gets state-vs-state comparison and keeps being served the old client's tokens; (b) a 401 on the connect path still attempts dynamic registration instead of using the configured client. Passing the registration's oauthClient into both connect-path contexts would make the invariant hold everywhere.

Minor: clearServerOAuth in apps/cli/src/wizards/mcp/settings.ts now also deletes oauthClient, so toggling a server oauth → headers → oauth forces re-entering the client id/secret. Intentional?

Also — has the full browser round-trip been run against a real pre-registered client (e.g. GitHub's remote MCP)? The test procedure in the description only shows mcp install.

@saoudrizwan

Copy link
Copy Markdown
Contributor

Reviewed the full diff at eb57dc2 (all 7 commits), with focus on the desktop wiring. Overall this is in good shape: the architecture split between passive connection and explicit browser authorization is the right design, and the later commits addressed the earlier review threads properly. Findings below, roughly by severity.

What holds together well

  • The passive/explicit split is clean: SdkUrlMcpClient.connect omits the OAuth provider when there are no stored tokens so the SDK cannot silently start discovery/registration, the 401-to-UnauthorizedError translation at the fetch boundary gives the UI a typed signal, and only the explicit authorize_mcp_server_oauth command can open a browser.
  • Cancellation is plumbed end to end: AbortSignal through authorizeMcpServerOAuth, cancelWait on the callback server, per-server dedupe in runCancellableMcpOAuthAuthorization, and owner-scoped cleanup when the webview connection drops (mirroring the provider OAuth pattern in server.ts).
  • The OAuth state guards are thorough: file-level expectedOAuthClient assertion inside the locked mutator, per-write assertOAuthClientUnchanged, token invalidation when the configured client changes, and the callback state parameter check against getLastOAuthState. The earlier fingerprint approach (which triggered the CodeQL insecure-hash alert) is gone from the branch head, so that alert is stale and can be dismissed.
  • resetInteractiveState correctly handles switching from dynamic registration to a configured oauthClient (replaces clientInformation, clears tokens) so the explicit flow cannot mix credentials across clients.

Issues

1. One malformed settings entry now blanks the entire desktop MCP list (moderate)

readMcpServersResponse in apps/examples/desktop-app/sidecar/commands.ts now calls resolveMcpServerRegistrations and listMcpServerOAuthStatuses, both of which strictly zod-parse the whole file and throw on any invalid entry. I verified against the built branch: a file with one valid server and one hand-edited entry lacking transport/command/url throws Invalid MCP settings at ...: mcpServers.broken: Invalid input, so list_mcp_servers fails and the UI shows only an error banner. The previous implementation read the raw JSON leniently and rendered everything it could. Since the view offers "Open settings file" for hand editing, one typo now hides all servers, including the ones the user would want to edit or delete to fix the problem. Suggest parsing per-entry and degrading gracefully (skip or flag the bad entry) rather than failing the whole response.

2. Editing an enabled remote server can silently disable it (moderate)

upsert_mcp_server now writes remote servers with disabled: true, probes, and only re-enables when the probe connects. For a newly added server that is good behavior. But editing an already-enabled, working server (even just its metadata) goes through the same path: if the probe fails transiently (offline, slow endpoint, server blip), the previously-working server ends up disabled with no explicit indication that the save is what turned it off. The save dialog also now blocks on the probe for up to the MCP request timeout. Consider skipping the probe (or restoring the enabled state on probe failure) when the server was already enabled and the transport identity is unchanged, i.e. reuse the mcpTransportIdentity comparison you already compute for oauth preservation.

3. Connect then Cancel flips an enabled server to disabled (minor, possibly intended)

authorize_mcp_server_oauth disables the server before authorizing and re-enables only on success. A server can be enabled while authorizationRequired is true (enable-toggle probe failed with 401), so clicking Connect and then Cancel leaves the toggle off even though the user never touched it. The banner text does say the server stays off until authorization succeeds, so this may be deliberate; flagging in case the toggle change on cancel is surprising.

4. mcp-remote rewrite drops env (minor)

resolveMcpRemoteProxyTransport only matches the exact no-flag npx [-y] mcp-remote <url> shape, which safely excludes --header usage, but it does not check transport.env. An entry using env for proxy configuration (HTTP_PROXY, MCP_REMOTE_CONFIG_DIR) still gets rewritten to the native transport where that env no longer applies. Cheap fix: bail out of the rewrite when env is non-empty.

5. Shared button.tsx sizing change is global (minor)

Removing the fixed h-8 from the sm variant and changing xs padding affects every size="sm"/size="xs" button in the desktop app, not just the MCP view. Worth a quick visual pass over other screens that use these sizes, or splitting this into its own change.

6. Stale error banner on disabled servers (nit)

The card shows oauthStatus.lastError as "Connection failed" even when the server is toggled off, since persisted lastError only clears on a successful connect or authorize. Consider suppressing the banner for disabled servers.

Verification

  • Ran the branch locally: core MCP suite via vitest (62 passed), desktop sidecar mcp-oauth.test.ts (3 passed), CLI mcp.test.ts + wizards/mcp/settings.test.ts (20 passed).
  • Full @cline/core unit run had 2 failures, both unrelated: the documented cloud-VM workspace-manifest git-remote artifact, and a flaky 10s timeout in checkpoint-restore.test.ts that hits a different test each run and passes on main.
  • All PR checks are green, including platform integration and the JetBrains-relevant suites; the JetBrains failure comment predates the last four commits.

@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Reviewed and end-to-end tested this PR against a fake GitHub-style remote MCP server: an OAuth authorization server with no registration_endpoint, a pre-registered confidential client (id + secret), PKCE (S256) enforced at the token endpoint, and a bearer-protected streamable HTTP MCP endpoint.

What was tested (CLI wizard + real browser flow):

  • DCR failure preserved: cline mcp install fake-github --transport http ... with an empty client ID still goes through dynamic registration and fails against the non-DCR server with the exact pre-fix error (Incompatible auth server: does not support dynamic client registration), and the server entry is kept for retry.
  • Pre-registered client flow works: entering the client ID + secret in the wizard produced an authorization URL carrying client_id=fake-github-client-id (DCR skipped), the browser consent page was approved, the token exchange authenticated via client_secret_basic with PKCE verified server-side, and MCP server "fake-github" OAuth authorization completed. The persisted settings contained oauthClient plus oauth.clientInformation bound to the tokens.
  • Runtime path works: tool calls through createDefaultMcpServerClientFactory (listTools, callTool) succeeded using the saved tokens.

One gap found — already fixed by the newer commits on this branch: at the initial revision (d850a15), SdkUrlMcpClient in client.ts created its OAuth provider without the configured oauthClient, so the token-to-client binding check was a no-op at connection time. Empirically: rotating oauthClient.clientId directly in the settings file (bypassing the wizard) left the runtime client silently reusing tokens minted under the old client, and a later refresh would have used stale credentials. The current head (eb57dc2) passes createMcpOAuthClientInformation(registration.oauthClient) at both provider call sites, which resolves this. Re-verified after rebuild:

# config clientId matches persisted tokens -> works
whoami: "Authenticated as fake-user via pre-registered OAuth client (no dynamic client registration)."

# config clientId rotated in the file -> fails closed, stale token never sent (server saw an unauthenticated 401)
Error: MCP server "fake-github" requires OAuth authorization. Run authorizeMcpServerOAuth for this server.

All src/extensions/mcp/ tests pass under vitest (62/62), as do the full @cline/cli unit suites.

Minor notes (non-blocking):

  • cline mcp install --yes (non-interactive) has no flags for oauthClient; automation must use the wizard or edit the settings JSON. Could be a follow-up.
  • The client secret is stored in plaintext in cline_mcp_settings.json — consistent with the tokens already stored there, but worth mentioning in the feature docs.

@saoudrizwan
saoudrizwan merged commit 6f7f817 into main Aug 7, 2026
17 checks passed
@saoudrizwan
saoudrizwan deleted the bee/mcp-oauth branch August 7, 2026 01:58
Comment on lines +270 to +274
assertOAuthClientUnchanged(
options.serverName,
current,
clientInformation,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Initial PKCE verifier is rejected

When a pre-registered client starts its first OAuth authorization with no persisted clientInformation, saveCodeVerifier compares the empty state against the configured client and throws McpOAuthClientChangedError, preventing the browser authorization flow from starting.

Knowledge Base Used: SDK Core (@cline/core)

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/packages/core/src/extensions/mcp/oauth.ts
Line: 270-274

Comment:
**Initial PKCE verifier is rejected**

When a pre-registered client starts its first OAuth authorization with no persisted `clientInformation`, `saveCodeVerifier` compares the empty state against the configured client and throws `McpOAuthClientChangedError`, preventing the browser authorization flow from starting.

**Knowledge Base Used:** [SDK Core (`@cline/core`)](https://app.greptile.com/cline-org-2/-/custom-context/knowledge-base/cline/cline/-/docs/sdk-core.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

teknium1 added a commit to NousResearch/hermes-agent that referenced this pull request Aug 13, 2026
Port from cline/cline#12983 (the 'invalidate tokens when OAuth client
changes' invariant): tokens are minted for a specific client_id, so after
a user edits oauth.client_id / oauth.client_secret in config.yaml the old
tokens can only fail with invalid_client. Pre-registered clients are
deliberately exempt from the invalid_client auto-poison path, so the stale
tokens wedged every request until ~/.hermes/mcp-tokens/<server>.* was
wiped by hand.

_maybe_preregister_client() now compares the on-disk client.json identity
against the incoming config identity before overwriting it and discards
tokens.json + meta.json on a mismatch (with a log line pointing at
hermes mcp login). Unchanged identity is a strict no-op.

Proven live on main with an isolated-HERMES_HOME E2E probe; regression
tests sabotage-verified (fail without the wiring line).
TrungKiencding pushed a commit to TrungKiencding/AgentX-Workmate that referenced this pull request Aug 25, 2026
Port from cline/cline#12983 (the 'invalidate tokens when OAuth client
changes' invariant): tokens are minted for a specific client_id, so after
a user edits oauth.client_id / oauth.client_secret in config.yaml the old
tokens can only fail with invalid_client. Pre-registered clients are
deliberately exempt from the invalid_client auto-poison path, so the stale
tokens wedged every request until ~/.hermes/mcp-tokens/<server>.* was
wiped by hand.

_maybe_preregister_client() now compares the on-disk client.json identity
against the incoming config identity before overwriting it and discards
tokens.json + meta.json on a mismatch (with a log line pointing at
hermes mcp login). Unchanged identity is a strict no-op.

Proven live on main with an isolated-HERMES_HOME E2E probe; regression
tests sabotage-verified (fail without the wiring line).

(cherry picked from commit 3eac116b9d56dbcb7ed2a8624fcf28477d12f280)
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
Port from cline/cline#12983 (the 'invalidate tokens when OAuth client
changes' invariant): tokens are minted for a specific client_id, so after
a user edits oauth.client_id / oauth.client_secret in config.yaml the old
tokens can only fail with invalid_client. Pre-registered clients are
deliberately exempt from the invalid_client auto-poison path, so the stale
tokens wedged every request until ~/.hermes/mcp-tokens/<server>.* was
wiped by hand.

_maybe_preregister_client() now compares the on-disk client.json identity
against the incoming config identity before overwriting it and discards
tokens.json + meta.json on a mismatch (with a log line pointing at
hermes mcp login). Unchanged identity is a strict no-op.

Proven live on main with an isolated-HERMES_HOME E2E probe; regression
tests sabotage-verified (fail without the wiring line).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants