Skip to content

fix(mcp): remove cf-portal catalog entry — loopback-only redirect URI policy - #89

Merged
jonnyparris merged 1 commit into
mainfrom
oauth/remove-cf-portal-catalog
May 26, 2026
Merged

fix(mcp): remove cf-portal catalog entry — loopback-only redirect URI policy#89
jonnyparris merged 1 commit into
mainfrom
oauth/remove-cf-portal-catalog

Conversation

@jonnyparris

Copy link
Copy Markdown
Owner

What I learned

After 6 PRs of trying to make Dodo connect to cf-portal via OAuth, and finally cross-referencing OpenCode's source (packages/opencode/src/mcp/oauth-provider.ts):

get redirectUrl(): string {
  if (this.config.redirectUri) return this.config.redirectUri
  const port = this.config.callbackPort ?? OAUTH_CALLBACK_PORT  // 19876
  return `http://127.0.0.1:${port}${OAUTH_CALLBACK_PATH}`        // /mcp/oauth/callback
}

OpenCode (and Cursor, Claude Desktop, etc) work with cf-portal because they're local clients using loopback redirect URIs. They spin up an HTTP server on 127.0.0.1:19876 to catch the callback.

cf-portal's authorize endpoint (cf-mcp.cloudflareaccess.com) enforces an allowlist:

  • http://127.0.0.1:*/... — accepted (loopback)
  • ✅ Cloudflare-managed domains (e.g. seal-nightly.cloudflare.dev) — accepted (allowlisted)
  • ❌ Everything else — rejected with Redirect URI not allowed by application configuration

This is sensible OAuth security: treat third-party hosted apps as untrusted clients, only allow callbacks to locally-controlled URIs or known Cloudflare property.

What this means for Dodo

A hosted Worker can't bind 127.0.0.1 from the user's perspective. cf-portal as an OAuth target from Dodo is infeasible by design, not by bug.

Fix

Remove the cf-portal catalog entry. The comment in mcp-catalog.ts documents the reasoning. Test updated to assert the entry's absence.

Workaround for cf-portal access in Dodo (out of scope)

Run a local mcp-remote proxy on the user's machine that does the loopback OAuth, then expose it to Dodo as a static-headers MCP integration. Defeats the point of Dodo being hosted, but it's how Cursor users do it. Not implementing this here.

What stays in place

All the supporting infrastructure from PRs #82#88 remains useful for any future OAuth-MCP entry that allows remote redirect URIs:

  • OAuth start-auth / delete-auth / refresh-state endpoints
  • Popup + polling UX
  • Stale-registration cleanup before retry
  • getAgentByName fix (workerd#2240)
  • Integrations panel moved above Skills/Tools

The browser-rendering and github OAuth entries are still in the catalog. github has its own issue (Incompatible auth server: does not support dynamic client registration — needs a pre-registered client_id which we don't expose yet). browser-rendering has a separate dedicated UI section that wasn't testable in this scope.

Verification

  • npm run typecheck clean
  • npx vitest run 817/817 pass
  • The integrations panel no longer shows cf-portal once redeployed

beep-boop-🤖

…policy)

After end-to-end debugging via chrome-devtools and worker tail, then
cross-referencing OpenCode's source, the root cause is clear:

cf-portal's OAuth authorize endpoint (cf-mcp.cloudflareaccess.com)
only accepts redirect URIs that point at loopback
(http://127.0.0.1:PORT/...) or pre-allowlisted Cloudflare-managed
domains (e.g. seal-nightly.cloudflare.dev).

DCR succeeds for any host, but the /authorize endpoint then rejects
non-allowlisted redirect URIs with:

  error=invalid_request
  error_description=Redirect URI not allowed by application configuration

This is by design — treating third-party hosted apps as untrusted
clients is a sensible default. OpenCode/Cursor/Claude Desktop work
because they run locally and spin up an http://127.0.0.1:19876
callback server. Dodo runs on a Worker; we can't bind 127.0.0.1 from
the user's perspective.

Remove the catalog entry to avoid the UX footgun of users clicking
"Connect with OAuth" only to land on an unrecoverable error. The
comment in mcp-catalog.ts documents the reasoning so the next person
to come looking doesn't re-walk the same path.

To use cf-portal tools in Dodo, the workaround is to run a local
`mcp-remote` proxy and configure it as a static-headers MCP
integration. (Not in scope here.)

The supporting infrastructure (OAuth start/poll routes, popup UX,
stale-registration cleanup, getAgentByName fix, /agents/oauth/callback
path, integrations layout fix) remains in place and is still useful
for any future OAuth-MCP entry that DOES allow remote redirect URIs.

Tests: 817/817 pass. Typecheck clean.

beep-boop-🤖
@jonnyparris
jonnyparris merged commit c4e6e83 into main May 26, 2026
2 checks passed
@jonnyparris
jonnyparris deleted the oauth/remove-cf-portal-catalog branch May 26, 2026 12:47
jonnyparris added a commit that referenced this pull request May 26, 2026
…#93)

Two cleanup fixes for the OAuth integrations UI:

1. Remove GitHub from CORE_MCP_CATALOG (api.githubcopilot.com/mcp/)
   GitHub Copilot's MCP server returns 'Incompatible auth server: does
   not support dynamic client registration' when the SDK-managed OAuth
   path attempts DCR. The catalog entry was advertising a Connect with
   OAuth button that could never succeed. Users who want GitHub MCP
   should add a github_token secret instead — the existing static-
   headers path already handles it and the UI hides any GitHub catalog
   suggestion when a github_token is present.

   The fallback github_token-hides-suggestion code path in
   dodo-settings.js (`if(cat.id==="github"&&hasGithubToken)return`)
   becomes dead with this change but is harmless to leave in place.

2. Always render orphaned OAuth servers
   Before this commit, renderOAuthCard was only called for catalog
   entries with auth_type='oauth' that had a matching SDK-managed
   hub-DO record. When a catalog entry was removed (e.g. cf-portal
   in PR #89) but the user already had a failed/stuck server record
   from a previous attempt, the record was invisible in the UI →
   no way to clear it without poking /api/mcp/delete-auth manually.

   Fix: track which oauthServers got rendered alongside a catalog
   entry; render any unmatched ones via the new renderOrphanedOAuthCard
   with a 'Clear' button (calls existing /api/mcp/delete-auth).

   Also improve renderOAuthCard for non-ready states:
   - 'Refresh' button only shows for state=ready (it was retrying
     against a broken config in other states, which was useless)
   - The remove button reads 'Disconnect' when ready, 'Clear'
     otherwise.

Tests: 832/832 pass. Catalog test count updated to reflect the
removed github entry; new assertions confirm github and cf-portal
are NOT in the catalog so the next person doesn't accidentally
re-add them without reading the history.

Stale UI rendering of an orphaned cf-portal 'authenticating' entry
that surfaced this issue was already wiped manually via
/api/mcp/delete-auth.

beep-boop-🤖
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