feat: OAuth MCP support — all 4 phases (Phase 1-4 complete)#43
Merged
jonnyparris merged 14 commits intomainfrom Apr 24, 2026
Merged
feat: OAuth MCP support — all 4 phases (Phase 1-4 complete)#43jonnyparris merged 14 commits intomainfrom
jonnyparris merged 14 commits intomainfrom
Conversation
… tests incomplete)
…es, callTool signature, mcpGatekeepers option typing
Phase 1 split cloudflare-api into 8 per-service entries but left url pointing at the repo URL for all of them. Phase 2's approved_mcps table uses mcp_url as primary key, so seeding failed with UNIQUE constraint violations. Fix: point each cloudflare-* entry at its actual MCP endpoint. Remove redundant cloudflare-api-browser (conflicts with browser-rendering which uses the same endpoint). Add cloudflare-api-ai-gateway and cloudflare-api-autorag entries missing from Phase 1. Improve descriptions to reflect what each service does.
Phase 4 agent changes had 3 issues: 1. src/mcp.ts: refactored helper signatures to take userEmail as 2nd arg but didn't update all ~30 call sites. Made userEmail optional at end of each signature, falling back to resolveAdminEmail() for backward compat with service-mode DODO_MCP_TOKEN callers. 2. src/index.ts: resolveAdminEmail() returns string | undefined but local resolvedEmail was typed string | null. Added ?? null coercion. 3. src/coding-agent.ts: Phase 3 used this.mcp.removeMcpServer() which doesn't exist — correct SDK method is this.mcp.removeServer(). 4. test/dodo.test.ts: Phase 4 changed test Bearer tokens from 'test-mcp-token' (matching vitest.config.ts) to 'shared-mcp-token' (wrong). Reverted.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
dodo | d3e0fdf | Apr 24 2026, 10:03 AM |
Addresses all 4 critical and 4 high-priority issues from code review. C1 — OAuth state federation (was: broken end-to-end) - CodingAgent.listOAuthTools(): RPC returning serialisable tool metadata from the per-user hub DO's this.mcp manager. - CodingAgent.callOAuthTool(): RPC for executing a tool against the hub. - CodingAgent.loadOAuthToolsFromHub(): called from connectMcpServers; if this DO is a session DO (not keyed by email), fetch the hub's tool list via RPC and cache it in cachedOAuthTools for sync getTools() reads. - CodingAgent.callOAuthToolViaHub(): executor passed to buildToolsForThink so tool calls route back to the hub where OAuth creds live. - buildToolsForThink now takes oauthTools + oauthToolExec instead of peeking at agent.mcp directly. Renamed buildSdkMcpTools -> buildOAuthMcpTools. - Result: OAuth-connected tools are now available in actual chat sessions, not just in the per-user hub DO that OAuth was initiated against. C2 — User-scoped MCP token lookup (was: always failed) - Added mcp_token_index table to SharedIndex with global lookup endpoint. - UserControl.createUserMcpToken / deleteUserMcpToken now sync the pointer to/from SharedIndex so /mcp can resolve tokens without knowing which user's DO to ask. - Extracted resolveMcpToken() helper that tries SharedIndex first, then falls back to DODO_MCP_TOKEN with timing-safe comparison. - /mcp and /mcp/codemode use the helper — removed 30+ lines of duplication. C3 — JWT drift reconciliation on session traffic (was: dead code) - Auth middleware now injects x-owner-email into the incoming request headers after JWT validation so every proxyToAgent forwarder propagates the email without manual threading. - CodingAgent.onRequest reads both x-owner-email and x-dodo-owner-email, normalises (trim + lowercase), and calls reconcileOwnerIdentity on drift. - reconcileOwnerIdentity normalises both sides before comparing to avoid false-positive drift on case changes (Foo@Bar vs foo@bar). C4 — /api/mcp/start-auth input validation (was: unguarded) - URL parsing wrapped in try/catch returning 400 on TypeError. - Rejects non-http(s) protocols. - Calls isHostAllowed before addMcpServer (same rule used for /api/mcp-configs). - addMcpServer wrapped in try/catch returning 502 on OAuth discovery failure. - Same defensive pattern applied to /api/mcp/delete-auth and /refresh-state. - delete-auth and refresh-state now verify the mcpId is in the caller's own getMcpServers().servers before acting (belt-and-braces against cross-user access if DO key scheme ever changes). H3 — Service-mode audit trail - mcpUserEmail now logs a warning when the admin fallback is taken, so operators can audit operations attributed to the admin instead of a real user. (Full signature refactor deferred — behavioural fix matches the audit intent without signature churn.) H4 — Timing-safe DODO_MCP_TOKEN comparison - Added timingSafeEqual helper. Used in both /mcp and /mcp/codemode auth. H5 — MCP-hygiene alarm is now armed - onStart schedules a 24h alarm if one isn't already set. The handler was self-rescheduling but the first alarm was never set, so it was dead code. H6 — OAuth success/error pages exist - /mcp-oauth-success and /mcp-oauth-error routes render minimal HTML with meta-refresh back to /. Previously redirected to 404s. M5 — Cross-user delete protection - delete-auth and refresh-state verify mcpId ownership before acting. M6 — Email case normalisation on drift check - Drift comparison lowercases both sides before comparing. Tests: +3 integration tests covering C2 end-to-end (create token, use, delete, verify rejection after delete) and C4 (malformed URL / non-http protocol / disallowed host). 388 tests pass (was 385), 0 failures. Refs: code review on PR #43.
…-final # Conflicts: # src/agentic.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete OAuth MCP support for Dodo — all 4 phases shipped. Users can connect to OAuth MCP servers without pasting tokens, admins can manage the catalog via D1, MCP tokens get cleared when Access identity changes, and Dodo's own MCP endpoint now resolves to the calling user's identity.
Supersedes #40 (Phase 1 only). Closes #41 (intermediate Phase 2 hotfix — already in this PR).
Phases
Phase 1 — OAuth MCP support via Agents SDK
McpCatalogEntry,McpGatekeeperConfig,mcp_configsD1 table → addedauth_type: "oauth" | "static_headers"field with schema migration.CodingAgent.onStart→this.mcp.configureOAuthCallback({...}).CodingAgent.connectMcpServers→ skips OAuth entries (managed by SDK).CodingAgent.refreshMcpState→ new RPC method for re-auth.src/index.ts→/agents/*callback route +POST /api/mcp/{start-auth,delete-auth,refresh-state}endpoints, all behind existing auth middleware.src/agentic.ts→buildSdkMcpToolshelper that merges SDK tools alongside static gatekeepers, with display-name slug prefix + 64-char cap + dedupe.WORKER_URLenv var added toEnvandwrangler.jsonc(dev defaulthttp://localhost:8787).cloudflare-*entry points to its actual MCP endpoint (not the GitHub repo URL — Phase 1 bug fixed byce2c193).Phase 2 — Admin-managed approved MCP catalog
approved_mcpstable in UserControl with migration + seed fromMCP_CATALOG.listApprovedMcps,createApprovedMcp,updateApprovedMcp,softDeleteApprovedMcp.GET/POST /api/admin/approved-mcps,PUT/DELETE /api/admin/approved-mcps/:url(gated byisAdmin(email, env))./api/mcp-catalognow reads from D1, filters enabled + non-deleted entries, falls back to static array if seed hasn't run.MCP_CATALOGkept as the typed source of truth for seeding.Phase 3 — Revocation hygiene on JWT email drift
CodingAgent.clearAllMcpConnections()→ removes all SDK MCPs + disconnects static gatekeepers.CodingAgent.reconcileOwnerIdentity(incomingEmail)→ compares against storedowner_email, clears MCPs + reconnects on drift./agents/*route now injectsx-dodo-owner-emailheader so the DO can re-validate identity per request.alarm()hook logs daily for future Access revocation webhook integration.Phase 4 — User-scoped Dodo MCP tokens
user_mcp_tokenstable in UserControl (primary keytoken, indexed byemail).createUserMcpToken,lookupUserMcpToken(updateslast_used_at),listUserMcpTokens(returns prefix only),deleteUserMcpToken(cross-user-protected).POST /api/user/mcp-tokens,GET /api/user/mcp-tokens,DELETE /api/user/mcp-tokens/:token./mcpand/mcp/codemodeauth:dodo_*token via UserControl lookup.DODO_MCP_TOKEN(service mode, resolves toADMIN_EMAIL).src/mcp.tshelpers now accept optionaluserEmailparameter (backward-compatible: defaults toresolveAdminEmail()when not passed, preserving existing callers).DODO_MCP_TOKENstill works — no breaking changes for CI/service callers.Verification
npm run typecheck→ cleannpm test→ 385 passed, 2 skipped, 0 failed (out of 387 total)npm run build→ cleanCommit log
12 commits, 722 insertions / 80 deletions across 12 files.
Scope / safety
HttpMcpGatekeeperstatic-headers flow still works unchanged.browser-renderingcatalog entry still functions (now via OAuth).DODO_MCP_TOKENflow preserved for backward compat.*.cfdata.orghostnames or Cloudflare-internal logic in public source — the 8*.mcp.cloudflare.comentries are already public (perdevelopers.cloudflare.com).Reference
~/agent-hq/scratch/chat-flare-mcp-auth-insights.md(internal).Credit
Built across ~15 dispatched Dodo sessions over ~4 hours. Issues #34 (autocompaction) and companion bugs were surfaced and filed during this work and merged in parallel by @jonnyparris, which unblocked the later phases.