// Cloud: browser-mode MCP approval must let the resume page load the paused // execution from the approval URL the MCP tool returned. // // This drives the user-reported path without mcporter: // 1. A real StreamableHTTP MCP session runs with ?elicitation_mode=browser. // 2. A gated execute call returns approvalUrl and executionId. // 3. The cloud resume page's API GET loads that paused execution by // mcp_session_id and executionId. // 4. Posting the browser approval lets the model-side browser resume tool // consume the decision and complete the gated call. import { expect } from "@effect/vitest"; import { Effect } from "effect"; import { composePluginApi } from "@executor-js/api/server"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { scenario } from "../src/scenario"; import { Api, Browser, Mcp, Target } from "../src/services"; import { parseBrowserApproval } from "../src/surfaces/mcp"; import type { Identity } from "../src/target"; import { visit } from "../src/surfaces/browser"; const coreApi = composePluginApi([] as const); const GATE_TOOL = "executor.coreTools.policies.list"; const UNAVAILABLE_COPY = "This paused execution is no longer available"; const GATED_CODE = ` const result = await tools.executor.coreTools.policies.list({}); return JSON.stringify(result); `; type Connected = { readonly client: Client; readonly transport: StreamableHTTPClientTransport; }; const emailOf = (identity: Identity): string => identity.credentials?.email ?? identity.label; const textOf = (result: unknown): string => ( ((result as { readonly content?: unknown }).content ?? []) as ReadonlyArray<{ readonly type: string; readonly text?: string; }> ) .filter((part) => part.type === "text") .map((part) => part.text ?? "") .join("\n"); const openBrowserApprovalSession = async (mcpUrl: string, bearer: string): Promise => { const url = new URL(mcpUrl); url.searchParams.set("elicitation_mode", "browser"); const client = new Client( { name: "executor-e2e-browser-resume", version: "0.0.1" }, { capabilities: {} }, ); const transport = new StreamableHTTPClientTransport(url, { requestInit: { headers: { authorization: `Bearer ${bearer}` } }, }); await client.connect(transport); return { client, transport }; }; const closeQuietly = (connected: Connected): Effect.Effect => Effect.promise(() => connected.client.close().catch(() => undefined)); const pathWithSearch = (url: string): string => { const parsed = new URL(url); return `${parsed.pathname}${parsed.search}`; }; const authenticatedFetch = ( identity: Identity, input: URL, init: RequestInit = {}, ): Promise => fetch(input, { ...init, headers: { ...(identity.headers ?? {}), ...(init.headers ?? {}), }, }); scenario( "MCP approval · browser resume page loads the paused execution from its approval URL", { timeout: 180_000 }, Effect.gen(function* () { const target = yield* Target; const { client: apiClient } = yield* Api; const mcp = yield* Mcp; const identity = yield* target.newIdentity(); const bearer = yield* mcp.mintBearer(emailOf(identity)); const api = yield* apiClient(coreApi, identity); const policy = yield* api.policies.create({ payload: { owner: "org", pattern: GATE_TOOL, action: "require_approval" }, }); yield* Effect.gen(function* () { const session = yield* Effect.promise(() => openBrowserApprovalSession(target.mcpUrl, bearer), ); yield* Effect.gen(function* () { const paused = yield* Effect.promise(() => session.client.callTool({ name: "execute", arguments: { code: GATED_CODE } }), ); const approval = parseBrowserApproval({ raw: paused, text: textOf(paused), ok: paused.isError !== true, }); const approvalUrl = new URL(approval.approvalUrl); const mcpSessionId = approvalUrl.searchParams.get("mcp_session_id"); expect( mcpSessionId, "approval URL carries the MCP session id that the resume page will query", ).toEqual(expect.any(String)); expect(mcpSessionId, "approval URL points at the session that paused").toBe( session.transport.sessionId, ); const pausedApiUrl = new URL( `/api/mcp-sessions/${encodeURIComponent(mcpSessionId ?? "")}/executions/${encodeURIComponent(approval.executionId)}`, target.baseUrl, ); const pausedResponse = yield* Effect.promise(() => authenticatedFetch(identity, pausedApiUrl), ); const pausedBody = yield* Effect.promise(() => pausedResponse.text()); expect( pausedResponse.status, `resume page paused-execution GET response: ${pausedBody}`, ).toBe(200); const resumeApiUrl = new URL(`${pausedApiUrl.pathname}/resume`, target.baseUrl); const resumeResponse = yield* Effect.promise(() => authenticatedFetch(identity, resumeApiUrl, { method: "POST", headers: { "content-type": "application/json", origin: new URL(target.baseUrl).origin, }, body: JSON.stringify({ action: "accept", content: {} }), }), ); const resumeBody = yield* Effect.promise(() => resumeResponse.text()); expect(resumeResponse.status, `resume page approval POST response: ${resumeBody}`).toBe( 200, ); expect(JSON.parse(resumeBody), "resume page approval POST body").toMatchObject({ status: "completed", structured: { status: "approved", executionId: approval.executionId }, isError: false, }); const resumed = yield* Effect.promise(() => session.client.callTool({ name: "resume", arguments: { executionId: approval.executionId }, }), ); expect(resumed.isError, "browser-mode resume consumes the page decision").not.toBe(true); expect(textOf(resumed), "the gated tool completed after approval").toContain(policy.id); }).pipe(Effect.ensuring(closeQuietly(session))); }).pipe( Effect.ensuring( api.policies .remove({ params: { policyId: policy.id }, payload: { owner: "org" } }) .pipe(Effect.ignore), ), ); }), ); scenario( "MCP approval · browser resume page approves a paused execution through the UI", { timeout: 180_000 }, Effect.gen(function* () { const target = yield* Target; const { client: apiClient } = yield* Api; const browser = yield* Browser; const mcp = yield* Mcp; const identity = yield* target.newIdentity(); const bearer = yield* mcp.mintBearer(emailOf(identity)); const api = yield* apiClient(coreApi, identity); const policy = yield* api.policies.create({ payload: { owner: "org", pattern: GATE_TOOL, action: "require_approval" }, }); yield* Effect.gen(function* () { const session = yield* Effect.promise(() => openBrowserApprovalSession(target.mcpUrl, bearer), ); yield* Effect.gen(function* () { const paused = yield* Effect.promise(() => session.client.callTool({ name: "execute", arguments: { code: GATED_CODE } }), ); const approval = parseBrowserApproval({ raw: paused, text: textOf(paused), ok: paused.isError !== true, }); const approvalUrl = new URL(approval.approvalUrl); const mcpSessionId = approvalUrl.searchParams.get("mcp_session_id"); expect( mcpSessionId, "approval URL carries the MCP session id that the browser page will query", ).toEqual(expect.any(String)); expect(mcpSessionId, "approval URL points at the session that paused").toBe( session.transport.sessionId, ); const [resumed] = yield* Effect.all( [ Effect.promise(() => session.client.callTool({ name: "resume", arguments: { executionId: approval.executionId }, }), ), browser.session(identity, async ({ page, step }) => { await step("Open the paused execution approval page", async () => { await visit(page, pathWithSearch(approval.approvalUrl)); await page.getByText("User approval required").waitFor(); }); await step("Review the paused tool call details", async () => { await page.getByText("Pending request").waitFor(); await page.getByText(/Approve executor\.coreTools\.policies\.list\?/).waitFor(); const approve = page.getByRole("button", { name: "Approve" }); await approve.waitFor(); expect( await approve.isEnabled(), "the approve control is enabled for the paused execution", ).toBe(true); expect( await page.getByText(UNAVAILABLE_COPY).count(), "the resume page does not show the expired-session failure copy", ).toBe(0); }); await step("Approve the paused tool call", async () => { await page.getByRole("button", { name: "Approve" }).click(); await page.getByText("Approve sent").waitFor(); }); }), ], { concurrency: "unbounded" }, ); expect(resumed.isError, "browser-mode resume completed after the UI approval").not.toBe( true, ); expect(textOf(resumed), "the gated tool completed after approval").toContain(policy.id); }).pipe(Effect.ensuring(closeQuietly(session))); }).pipe( Effect.ensuring( api.policies .remove({ params: { policyId: policy.id }, payload: { owner: "org" } }) .pipe(Effect.ignore), ), ); }), );