Skip to content

Add MCP Connect button OAuth flow#251

Merged
vedworks merged 13 commits into
mainfrom
feat/oauth-mcp
Apr 28, 2026
Merged

Add MCP Connect button OAuth flow#251
vedworks merged 13 commits into
mainfrom
feat/oauth-mcp

Conversation

@vedworks

@vedworks vedworks commented Apr 28, 2026

Copy link
Copy Markdown

Summary

  • Replaces MCP token entry during server creation with a Cursor-style add URL first, then Connect flow.
  • Adds MCP server probing, state persistence, token fallback Connect, and one-click OAuth 2.1 with DCR + PKCE.
  • Adds token refresh on invalid tokens, settings-row hydration probes, telemetry, and edge-case coverage for non-OAuth/DCR servers.

Test plan

  • pnpm --dir apps/web exec vitest run lib/mcp-probe.test.ts lib/mcp-oauth.test.ts lib/mcp-servers.test.ts app/api/settings/mcp/route.test.ts app/api/settings/mcp/probe/route.test.ts app/api/settings/mcp/connect/token/route.test.ts app/api/settings/mcp/connect/start/route.test.ts app/api/settings/mcp/connect/callback/route.test.ts
  • Browser-tested Cloud settings MCP flow with a local mock MCP server: add server, Connect fallback token dialog, token submission, connected tool count, reload persistence, delete cleanup.
  • Browser-tested one-click OAuth path with a local mock auth server: DCR, authorization redirect callback, token exchange, row flips to authenticated with tool count.

Note

High Risk
Adds new OAuth/token-based connection endpoints and local secret/state persistence for MCP servers, which touches authentication headers and token handling. Incorrect validation/storage/probing could break connectivity or leak tokens/state despite added tests and escaping.

Overview
Adds a Cursor-style MCP “Add then Connect” flow: server creation no longer accepts an auth token and now immediately probes the server to populate persisted connection state.

Introduces new Connect APIs: POST /api/settings/mcp/connect/start to probe and (when supported) run OAuth discovery + dynamic client registration + PKCE, GET /api/settings/mcp/connect/callback to exchange the code, persist refresh tokens, update Authorization headers, probe, and postMessage results back to the opener, and POST /api/settings/mcp/connect/token as a manual token fallback that saves the Bearer header then re-probes.

Adds supporting libraries for probing (tools/list over JSON/SSE with RFC 9728 WWW-Authenticate parsing), OAuth client utilities (discovery, DCR, PKCE, code exchange, refresh, expiry computation), and sidecar persistence for secrets (.mcp-secrets.json, 0600) and per-server status (.mcp-states.json), including cleanup on server removal and refresh-on-invalid_token during probes.

Updates the settings UI to show per-server status/tool counts, add a Connect button with OAuth popup handling + timeout, fallback token dialog, and automatic hydration probes; expands test coverage across routes and new libs.

Reviewed by Cursor Bugbot for commit 5da08f9. Bugbot is set up for automated code reviews on this repo. Configure here.

vedworks added 12 commits April 29, 2026 01:05
Add a generic tools/list probe that classifies MCP servers as connected, needing authentication, or errored, including WWW-Authenticate parsing for the OAuth handoff.
Track user-added MCP server status and tool counts in a local sidecar while keeping the runtime server config clean for agent consumption.
Move MCP server creation to a name-and-URL flow, probe immediately after create, and add endpoints for row refreshes and manual Bearer-token Connect.
Replace token entry during add with a row-level Connect action, state badges, tool-count display, retry controls, and the manual token fallback dialog.
Add chmod-protected MCP secret storage plus OAuth discovery, dynamic client registration, PKCE authorization URL construction, and token exchange utilities.
Add the Connect start endpoint that probes for OAuth metadata, performs dynamic client registration, stores PKCE state, and returns the authorization URL.
Handle authorization callbacks by validating state, exchanging PKCE codes, persisting tokens, probing the server, and notifying the settings popup opener.
Refresh expired OAuth access tokens inline when tools/list returns invalid_token, re-probe with the fresh token, and clear stale refresh tokens when recovery fails.
Fan out one probe per configured MCP server after settings load so connection state and tool counts refresh without manual row interaction.
Emit server-side telemetry for OAuth and token Connect starts, completions, and failure fallbacks so the flow is observable in production.
Add edge coverage for authorization servers without dynamic registration and ensure the internally managed composio MCP server cannot be mutated by user-server helpers.
Clear stale hydration tracking for MCP servers that are no longer rendered so deleting and re-adding the same key still triggers automatic probing.
Comment thread apps/web/lib/mcp-servers.ts
Comment thread apps/web/lib/mcp-secrets.ts
@vedworks vedworks self-assigned this Apr 28, 2026
Delete per-server OAuth sidecar entries when MCP servers are removed and clear callback-only redirect URIs after token exchange so stale auth state cannot linger.
@vedworks vedworks merged commit 99b417c into main Apr 28, 2026
3 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: RFC 8414 metadata URL wrongly appended to issuer path
    • Fixed buildMetadataUrls to insert /.well-known/oauth-authorization-server between the origin and path per RFC 8414 §3, instead of appending it to the end of the issuer URL.
  • ✅ Fixed: Callback skips validation of cached.asMetadataUrl before discovery
    • Added !cached.asMetadataUrl to the existing validation guard on line 247, consistent with the probe route's tryRefreshAfterInvalidToken check.

Create PR

Or push these changes by commenting:

@cursor push 0ccd44c8f5
Preview (0ccd44c8f5)
diff --git a/apps/web/app/api/settings/mcp/connect/callback/route.ts b/apps/web/app/api/settings/mcp/connect/callback/route.ts
--- a/apps/web/app/api/settings/mcp/connect/callback/route.ts
+++ b/apps/web/app/api/settings/mcp/connect/callback/route.ts
@@ -244,7 +244,7 @@
   }
 
   const cached = getMcpServerSecret(serverKey);
-  if (!cached || !cached.codeVerifier || !cached.redirectUri) {
+  if (!cached || !cached.codeVerifier || !cached.redirectUri || !cached.asMetadataUrl) {
     return renderResultPage(
       {
         kind: "error",

diff --git a/apps/web/lib/mcp-oauth.ts b/apps/web/lib/mcp-oauth.ts
--- a/apps/web/lib/mcp-oauth.ts
+++ b/apps/web/lib/mcp-oauth.ts
@@ -261,10 +261,15 @@
 }
 
 function buildMetadataUrls(issuer: string): string[] {
-  const trimmed = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
+  // RFC 8414 §3: insert /.well-known/oauth-authorization-server between the
+  // origin and the path, e.g. https://example.com/tenant becomes
+  // https://example.com/.well-known/oauth-authorization-server/tenant
+  const url = new URL(issuer);
+  const path = url.pathname.replace(/\/+$/, "");
   return [
-    `${trimmed}/.well-known/oauth-authorization-server`,
-    `${trimmed}/.well-known/openid-configuration`,
+    `${url.origin}/.well-known/oauth-authorization-server${path}`,
+    // OIDC Discovery appends to the issuer per OpenID Connect Discovery §4.
+    `${url.origin}${path}/.well-known/openid-configuration`,
   ];
 }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 5da08f9. Configure here.

Comment thread apps/web/lib/mcp-oauth.ts
`${trimmed}/.well-known/oauth-authorization-server`,
`${trimmed}/.well-known/openid-configuration`,
];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RFC 8414 metadata URL wrongly appended to issuer path

Medium Severity

buildMetadataUrls appends /.well-known/oauth-authorization-server to the end of the issuer URL. Per RFC 8414 §3, for path-based issuers like https://auth.example.com/tenant, the well-known URI is https://auth.example.com/.well-known/oauth-authorization-server/tenant, not https://auth.example.com/tenant/.well-known/oauth-authorization-server. This causes OAuth discovery to fail for any authorization server with a non-root issuer path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5da08f9. Configure here.

},
targetOrigin,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Callback skips validation of cached.asMetadataUrl before discovery

Low Severity

The guard on line 247 checks cached.codeVerifier and cached.redirectUri but not cached.asMetadataUrl. Since emptySecret() initializes asMetadataUrl to "", a corrupted or partially-written secrets entry could pass validation and then feed an empty string to discoverOAuthMetadata, producing a confusing "network_error" instead of a clear "missing_pkce"-style diagnostic. The probe route's tryRefreshAfterInvalidToken correctly checks !cached.asMetadataUrl for the same field.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5da08f9. Configure here.

vedworks added a commit that referenced this pull request Apr 28, 2026
This reverts commit 99b417c, reversing
changes made to 7145fb1.
@cursor

cursor Bot commented Apr 28, 2026

Copy link
Copy Markdown

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: OAuth secrets not cleaned up on server deletion
    • Added a call to deleteMcpServerSecret(normalizedKey) at the end of removeMcpServer so OAuth secrets are cleaned up from .mcp-secrets.json when a server is deleted.
  • ✅ Fixed: Transient redirectUri field not cleared after token exchange
    • Added redirectUri: null to clearTransientOAuthFields so it is cleared alongside codeVerifier and oauthState after token exchange, matching the documented contract.

Create PR

Or push these changes by commenting:

@cursor push dcc2952495
Preview (dcc2952495)
diff --git a/apps/web/lib/mcp-secrets.ts b/apps/web/lib/mcp-secrets.ts
--- a/apps/web/lib/mcp-secrets.ts
+++ b/apps/web/lib/mcp-secrets.ts
@@ -184,6 +184,7 @@
     ...current,
     codeVerifier: null,
     oauthState: null,
+    redirectUri: null,
   };
   writeAll(all);
 }

diff --git a/apps/web/lib/mcp-servers.ts b/apps/web/lib/mcp-servers.ts
--- a/apps/web/lib/mcp-servers.ts
+++ b/apps/web/lib/mcp-servers.ts
@@ -1,5 +1,6 @@
 import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
 import { join } from "node:path";
+import { deleteMcpServerSecret } from "@/lib/mcp-secrets";
 import { resolveOpenClawStateDir } from "@/lib/workspace";
 
 type UnknownRecord = Record<string, unknown>;
@@ -470,4 +471,6 @@
     delete states[normalizedKey];
     writeStatesSidecar(states);
   }
+
+  deleteMcpServerSecret(normalizedKey);
 }

You can send follow-ups to the cloud agent here.

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.

1 participant