Skip to content

Commit 31e6e7d

Browse files
committed
Re-enable URL elicitation for Mastra template
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
1 parent cb67ba1 commit 31e6e7d

File tree

3 files changed

+47
-11
lines changed

3 files changed

+47
-11
lines changed

templates/_shared/partials/arcade-oauth-provider.hbs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,15 +181,27 @@ export async function initiateOAuth(): Promise<"AUTHORIZED" | "REDIRECT"> {
181181
// of authProvider, so the singleton never triggers its own OAuth handshake and
182182
// races with the connect route's initiateOAuth() call.
183183
//
184-
// NOTE: URL-mode elicitation is not declared here. Arcade's URL elicitation
185-
// auth URLs redirect back to app.arcade.dev after tool authorization (not to
186-
// our local app), leaving the user stranded. Auth URLs are surfaced instead
187-
// via extractAuthUrlFromToolOutput in onStepFinish — same fallback approach
188-
// used by the AI SDK template.
184+
// URL-mode elicitation is declared so the gateway proactively sends auth URLs
185+
// when a tool needs authorization. The frontend opens these in a new tab
186+
// (pointing to app.arcade.dev) — this is expected for tool auth and is
187+
// separate from the initial Arcade gateway connection, which always redirects
188+
// back to localhost via PKCE.
189+
190+
// Per-request callback bridging the singleton elicitation handler to the
191+
// active response stream. Safe for single-user local dev use.
192+
let _elicitationCallback: ((authUrl: string, message: string) => void) | null = null;
193+
194+
export function setElicitationCallback(
195+
cb: ((authUrl: string, message: string) => void) | null
196+
) {
197+
_elicitationCallback = cb;
198+
}
199+
189200
export const mcpClient = new MCPClient({
190201
servers: {
191202
arcade: {
192203
url: new URL(getGatewayUrl()),
204+
capabilities: { elicitation: { url: {} } },
193205
fetch: async (url: RequestInfo | URL, init?: RequestInit) => {
194206
const tokens = oauthProvider.tokens();
195207
const headers = new Headers(init?.headers);
@@ -202,6 +214,19 @@ export const mcpClient = new MCPClient({
202214
},
203215
});
204216

217+
mcpClient.elicitation.onRequest(async (request) => {
218+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
219+
const params = request.params as any;
220+
// Arcade sends the auth URL as params.url or params.message
221+
const authUrl: string = params?.url ?? params?.message ?? "";
222+
const message: string =
223+
typeof params?.message === "string" ? params.message : "Authorization required";
224+
if (authUrl && _elicitationCallback) {
225+
_elicitationCallback(authUrl, message);
226+
}
227+
return { action: "accept" };
228+
});
229+
205230
{{/if}}
206231
/**
207232
* Create an AI SDK MCP client for Arcade Gateway using stored OAuth tokens.
@@ -213,12 +238,10 @@ export const mcpClient = new MCPClient({
213238
* route tool auth through URL elicitation instead of embedding auth URLs in
214239
* tool output — but the AI SDK client has no handler for URL-mode requests,
215240
* so those requests are silently dropped and tool auth never surfaces to the
216-
* user. Additionally, Arcade's URL elicitation auth URLs redirect back to
217-
* app.arcade.dev after authorization (not to our local app), which would
218-
* leave users stranded there even if we could receive the URLs.
241+
* user. The Mastra template uses URL-mode elicitation instead via
242+
* @mastra/mcp's mcpClient.elicitation.onRequest().
219243
*
220-
* Revisit when @ai-sdk/mcp adds URL-mode elicitation support AND Arcade's
221-
* elicitation redirect_uri points back to the originating app.
244+
* Revisit when @ai-sdk/mcp adds URL-mode elicitation support.
222245
*
223246
* In the meantime, auth URLs are surfaced via extractAuthUrlFromToolOutput
224247
* in onStepFinish, which scrapes them from tool result text.

templates/_shared/partials/plan-route-body.hbs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ export async function POST() {
131131
try {
132132
emit({ type: "status", message: "Connecting to Arcade Gateway..." });
133133

134+
// Wire URL elicitation events from the singleton MCP client to this
135+
// request's stream. Cleared in finally so the next request starts clean.
136+
let elicitationCounter = 0;
137+
setElicitationCallback((authUrl, message) => {
138+
emit({
139+
type: "elicitation",
140+
elicitationId: `elicit-${++elicitationCounter}`,
141+
authUrl,
142+
message,
143+
});
144+
});
145+
134146
const allToolsets = await mcpClient.listToolsets();
135147

136148
// Filter toolsets: keep only read-only triage tools, wrap execute for truncation.
@@ -300,6 +312,7 @@ export async function POST() {
300312
});
301313
emit({ type: "done" });
302314
} finally {
315+
setElicitationCallback(null);
303316
try {
304317
streamController?.close();
305318
} catch {

templates/mastra/app/api/plan/route.ts.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import { Agent } from "@mastra/core/agent";
1212
import { getSession } from "@/lib/auth";
1313
import { getModel, planPrompt } from "@/src/mastra/agents/triage-agent";
14-
import { mcpClient } from "@/src/mastra/tools/arcade";
14+
import { mcpClient, setElicitationCallback } from "@/src/mastra/tools/arcade";
1515
import type { InboxItem } from "@/types/inbox";
1616

1717
{{> plan-route-body}}

0 commit comments

Comments
 (0)