Add MCP elicitation support for URL-mode auth flows#39
Draft
teallarson wants to merge 14 commits intomainfrom
Draft
Add MCP elicitation support for URL-mode auth flows#39teallarson wants to merge 14 commits intomainfrom
teallarson wants to merge 14 commits intomainfrom
Conversation
Enables all three agent templates (ai-sdk, mastra, langchain) to handle MCP elicitation requests from the Arcade Gateway. When a tool requires OAuth authorization, the gateway sends an elicitation/create request with a URL; the agent streams it to the frontend as an "elicitation" event, which the existing auth-prompt UI displays. Each template uses its framework's native elicitation API: - ai-sdk: @ai-sdk/mcp ElicitationRequestSchema + onElicitationRequest - mastra: @mastra/mcp MCPClient.elicitation.onRequest + Agent.stream - langchain: mcp SDK Callbacks(on_elicitation=...) + ElicitResult The existing extractAuthUrlFromToolOutput remains as a fallback for gateways that don't support the elicitation primitive. Note: page.tsx includes some pre-existing dashboard UX changes (dismissible callouts) alongside the onElicitation callback wiring. Made-with: Cursor
The MCP spec requires clients to opt into URL-mode elicitation by
including `url: {}` in the capabilities object. Without this, servers
won't send URL-mode requests (which Arcade uses for OAuth flows).
The Python MCP SDK does this automatically when an elicitation callback
is provided, but the TypeScript side (both @ai-sdk/mcp createMCPClient
and @mastra/mcp MCPClient) requires explicit declaration.
Changes `{ elicitation: {} }` to `{ elicitation: { url: {} } }` in both
the Mastra singleton and the AI SDK client factory.
Made-with: Cursor
Picks up TemplateResponse Starlette fix, favicon, mutation filter, scaffold pain points, and other upstream improvements. Resolves conflicts in dashboard page (keep dismissible callouts + upstream text-left fix + ARCADE_API_KEY removal) and Mastra route (keep elicitation imports + upstream planPrompt export). Made-with: Cursor
…flow
- Replace authProvider with per-request fetch in Mastra MCPClient so the
singleton never triggers its own OAuth handshake and races with the
connect route's initiateOAuth() (fixes double sign-in)
- Make elicitation.onRequest() lazy via ensureElicitationHandler() to
avoid eager connect() at module load time; call it from plan route
- Fix onStepFinish callback: Mastra step toolCalls/toolResults are
ChunkType objects { type, payload: { toolName, ... } } not flat AI SDK
objects, so access toolName via call.payload.toolName (fixes "Calling
other: undefined")
- Remove URL-mode elicitation from AI SDK getArcadeMCPClient(): @ai-sdk/mcp
only supports form-mode, declaring url capability caused gateway to use
elicitation instead of tool output, breaking extractAuthUrlFromToolOutput
- Rename "Why Arcade?" callout to "Why sign into Arcade?" (Next.js + Python)
- Add dismissible callouts with X/localStorage to empty-state,
source-auth-gate, and LangChain dashboard/login templates
Made-with: Cursor
Single-tenant templates store Arcade OAuth tokens in .arcade-auth/tokens.json shared by the whole process. Without clearing on logout, signing out of the local app and signing in as a different user would still use the previous user's Arcade identity. Fix: call clearArcadeTokens() (new export from arcade-oauth-provider.hbs) on local app logout across all three templates: - Next.js (ai-sdk + mastra): new POST /api/auth/arcade/disconnect route, called from handleLogout in dashboard/page.tsx before authClient.signOut() - LangChain: existing POST /api/auth/logout now unlinks tokens.json before clearing the session cookie Made-with: Cursor
Arcade's URL elicitation auth URLs redirect back to app.arcade.dev after
the user authorizes a tool — not back to our local app. This left users
stranded on app.arcade.dev with no automatic redirect back to the template.
Fix: remove `capabilities: { elicitation: { url: {} } }` from the Mastra
MCPClient and remove the ensureElicitationHandler / setElicitationCallback
infrastructure. Auth URLs are now surfaced exclusively via
extractAuthUrlFromToolOutput in onStepFinish, which is the same approach
used by the AI SDK template and works correctly.
Both Mastra and AI SDK now use the tool-output-scraping fallback for
surfacing tool auth URLs, shown to the user as AuthPrompt components
with target="_blank".
Made-with: Cursor
…K template Made-with: Cursor
… plan route Made-with: Cursor
…ition Drop the "I've already signed in — retry" ghost button from all three templates (Next.js shared, LangChain HTML/JS). Fix the Mastra (and AI SDK) reconnect race: verifyExistingConnection() made a live HTTP request on every dashboard load, which could get a transient 401 from the Arcade gateway immediately after a fresh token exchange, causing an unnecessary second sign-in prompt. Replace the fast path with a simple "tokens on disk → connected" trust, matching how the rest of the auth flow works. Expired tokens surface as errors from the plan route instead. Made-with: Cursor
Tool auth redirecting to app.arcade.dev is fine — the frontend opens those
URLs in a new tab. The previous removal was overcorrection; the real issue
was the initial Arcade gateway connection (PKCE) appearing to redirect to
app.arcade.dev, which was a UX confusion, not a technical limit.
Restore capabilities: { elicitation: { url: {} } } on the Mastra MCPClient,
re-add mcpClient.elicitation.onRequest handler, and re-wire
setElicitationCallback from the plan route so URL elicitation events stream
to the frontend as 'elicitation' events.
Also update the AI SDK comment to reflect that Mastra now uses URL-mode
elicitation; the AI SDK still can't because @ai-sdk/mcp only supports
form-mode.
Made-with: Cursor
Two bugs introduced in the previous commit:
1. capabilities: { elicitation: { url: {} } } is not a valid field in
@mastra/mcp MCPClientOptions.servers — removed it.
2. mcpClient.elicitation.onRequest() at the MCPClient level requires
(serverName, handler), not just (handler). Was being called without the
server name, so the handler was never registered. Fix: move registration
into the plan route with await mcpClient.elicitation.onRequest('arcade', ...)
before listToolsets().
Also: clear stored tokens when the plan route catches a 401/invalid_auth
error, so the next connect-route check correctly returns needs_auth instead
of a false-positive connected state.
Made-with: Cursor
Add visibilitychange + 3s interval polling while tool auth prompts are active so the UI reflects authorization within ~3 seconds instead of waiting for the user to manually return to the tab and trigger a focus event. Next.js: use-source-check.ts — replace focus-only listener with focus + visibilitychange + setInterval(3000) while authGateActive. LangChain: dashboard.js — add visibilitychange handler and a startSourceAuthPolling() interval that fires every 3s while auth prompt cards are visible. Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
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
elicitation/createprotocol instead of relying solely on tool output parsing@ai-sdk/mcponElicitationRequest,@mastra/mcpMCPClient.elicitation.onRequest, and PythonmcpSDKCallbacks(on_elicitation=...){ elicitation: { url: {} } }capability so servers know the client supports URL-mode (required by the MCP spec for sensitive flows like OAuth)extractAuthUrlFromToolOutputfallback is preserved for gateways that don't support elicitationChanges
Frontend (shared)
types/inbox.ts— newelicitationevent type inPlanEventunionhooks/use-plan-stream.ts—onElicitationcallback inPlanStreamCallbackscomponents/dashboard/auth-prompt.tsx— "Waiting for authorization..." state after clicking Authorizeapp/dashboard/page.tsx— wiresonElicitationtoaddAuthUrlBackend (shared partials)
arcade-oauth-provider.hbs— elicitation capability declaration + handlers for both AI SDK and Mastra pathsplan-route-body.hbs— conditional Mastra (Agent.stream) vs AI SDK (streamText) implementations with elicitation callback wiringextract-auth-url.hbs— marked as fallbackTemplate-specific
mastra/app/api/plan/route.ts.hbs— updated imports for Mastra-native MCP clientlangchain/app/arcade_oauth.py— elicitation callback bridge +_on_elicitationhandlerlangchain/app/routes/plan.py— drains elicitation events into NDJSON streamDependency bumps
@ai-sdk/mcp^1.0.36,@modelcontextprotocol/sdk^1.27.1@mastra/core^1.24.1,@mastra/mcp^1.4.2,@mastra/ai-sdk^1.3.3langchain-mcp-adapters>=0.2.2,mcp>=1.27Test plan
ai-sdk,mastra,langchain) and verify it builds/type-checksextractAuthUrlFromToolOutputstill works when elicitation is not availableMade with Cursor