// ---------------------------------------------------------------------------// Local app × MCP browser resume — real MCP execute + real approval API// ---------------------------------------------------------------------------//// Exercises the browser-approval shape used by CLI/MCP clients that do not// support managed MCP elicitation://// test → MCP SDK Client.callTool("execute") → Executor MCP host// → execution engine → sandbox code → real executor tool elicitations// → local approval API records the user's response// → MCP resume consumes the approved response and returns the result// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";import { randomBytes } from "node:crypto";import { mkdtempSync, rmSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { Effect, Schema } from "effect"; import { createExecutionEngine } from "@executor-js/execution";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";import { collectTables } from "@executor-js/api/server";import { FormElicitation, Subject, Tenant, createExecutor, definePlugin, type Executor,} from "@executor-js/sdk"; import { createMcpRequestHandler } from "./mcp";import { createSqliteFumaDb } from "./db/sqlite-fumadb"; const TEST_BASE_URL = "http://local.test"; const EmptyInputSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(Schema.Struct({})),); const approvalPlugin = definePlugin(() => ({ id: "browser-resume-test" as const, storage: () => ({}), staticSources: () => [ { id: "api", kind: "in-memory", name: "API", tools: [ { name: "singleApproval", description: "Elicit exactly once and return the user's response.", inputSchema: EmptyInputSchema, handler: ({ elicit }) => Effect.gen(function* () { const response = yield* elicit( FormElicitation.make({ message: "Approve browser-resume test call", requestedSchema: { type: "object", properties: { note: { type: "string", title: "Approval note" }, }, required: ["note"], }, }), ); return { ok: true, response }; }), }, ], }, ],})); const makeExecutor = async (tmpDir: string): Promise<Executor> => { const plugins = [approvalPlugin()] as const; const sqlite = await createSqliteFumaDb({ tables: collectTables(), namespace: "executor_local_browser_resume_test", path: join(tmpDir, "data.db"), }); const executor = await Effect.runPromise( createExecutor({ tenant: Tenant.make(`test-${randomBytes(4).toString("hex")}`), subject: Subject.make("local"), db: sqlite.db, plugins, onElicitation: "accept-all", }), ); const close = executor.close; return { ...executor, close: () => Effect.gen(function* () { yield* close(); yield* Effect.promise(() => sqlite.close()); }), };}; const makeMcpFetch = (executor: Executor) => { const engine = createExecutionEngine({ executor, codeExecutor: makeQuickJsExecutor(), }); const mcp = createMcpRequestHandler({ engine }); const fetchImpl: typeof globalThis.fetch = Object.assign( (input: RequestInfo | URL, init?: RequestInit) => { const request = input instanceof Request ? input : new Request(input, init); const url = new URL(request.url); if (url.pathname.startsWith("/mcp")) return mcp.handleRequest(request); if (url.pathname.startsWith("/api/mcp-sessions/")) { return mcp.handleApprovalRequest(request); } return Promise.resolve(new Response("Not found", { status: 404 })); }, { preconnect: globalThis.fetch.preconnect }, ); return { fetch: fetchImpl, dispose: mcp.close };}; const readStructuredRecord = (value: unknown): Record<string, unknown> => { expect(typeof value).toBe("object"); expect(value).not.toBeNull(); return value as Record<string, unknown>;}; const readApproval = (structured: unknown): { readonly executionId: string; readonly url: URL } => { const record = readStructuredRecord(structured); expect(record.status).toBe("user_approval_required"); expect(record).not.toHaveProperty("interaction"); expect(typeof record.executionId).toBe("string"); expect(typeof record.approvalUrl).toBe("string"); expect(record.resumePrompt).toBe( "Return text to the user telling them to approve the action at this approvalUrl. Only after you have prompted the user, call the `resume` tool with this executionId; `resume` will wait for the user's browser decision.", ); const { executionId, approvalUrl } = record as { readonly executionId: string; readonly approvalUrl: string; }; return { executionId, url: new URL(approvalUrl), };}; const readResultArray = (structured: unknown): ReadonlyArray<unknown> => { const record = readStructuredRecord(structured); expect(record.status).toBe("completed"); expect(Array.isArray(record.result)).toBe(true); return record.result as ReadonlyArray<unknown>;}; describe("local MCP browser approval resume", () => { it("continues multiple elicitations from one MCP execute call through the HTTP API path", async () => { const tmpDir = mkdtempSync(join(tmpdir(), "executor-local-browser-resume-")); const executor = await makeExecutor(tmpDir); const { fetch, dispose } = makeMcpFetch(executor); const mcpClient = new Client( { name: "browser-resume-test-client", version: "1.0.0" }, { capabilities: {} }, ); const transport = new StreamableHTTPClientTransport( new URL("/mcp?elicitation_mode=browser", TEST_BASE_URL), { fetch }, ); await mcpClient.connect(transport); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test owns MCP transports, web handler, and executor lifecycle try { const first = await mcpClient.callTool({ name: "execute", arguments: { code: ` return await Promise.all([ tools.api.singleApproval({}), tools.api.singleApproval({}), tools.api.singleApproval({}) ]); `, }, }); expect(first.isError).toBeFalsy(); const firstApproval = readApproval(first.structuredContent); const second = await approveInBrowserThenResume(fetch, mcpClient, firstApproval); const secondApproval = readApproval(second.structuredContent); expect(secondApproval.executionId).not.toBe(firstApproval.executionId); const third = await approveInBrowserThenResume(fetch, mcpClient, secondApproval); const thirdApproval = readApproval(third.structuredContent); expect(thirdApproval.executionId).not.toBe(firstApproval.executionId); expect(thirdApproval.executionId).not.toBe(secondApproval.executionId); const completed = await approveInBrowserThenResume(fetch, mcpClient, thirdApproval); const result = readResultArray(completed.structuredContent); expect(result).toHaveLength(3); expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ data: expect.objectContaining({ response: expect.objectContaining({ action: "accept", content: expect.objectContaining({ note: expect.stringContaining("approved-"), }), }), }), }), ]), ); } finally { await mcpClient.close(); await Effect.runPromise(Effect.ignore(Effect.tryPromise(() => dispose()))); await Effect.runPromise( Effect.ignore(Effect.tryPromise(() => Effect.runPromise(executor.close()))), ); rmSync(tmpDir, { recursive: true, force: true }); } }, 10_000);}); const approveInBrowserThenResume = async ( fetch: typeof globalThis.fetch, client: Client, approval: { readonly executionId: string; readonly url: URL },) => { const sessionId = approval.url.searchParams.get("mcp_session_id"); expect(sessionId).not.toBeNull(); const resume = client.callTool({ name: "resume", arguments: { executionId: approval.executionId }, }); const approvalResponse = await fetch( new URL( `/api/mcp-sessions/${encodeURIComponent(sessionId!)}/executions/${encodeURIComponent(approval.executionId)}/resume`, TEST_BASE_URL, ), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "accept", content: { note: `approved-${approval.executionId}` }, }), }, ); expect(approvalResponse.status).toBe(200); return await resume;};
// ---------------------------------------------------------------------------
// Local app × MCP browser resume — real MCP execute + real approval API
// ---------------------------------------------------------------------------
//
// Exercises the browser-approval shape used by CLI/MCP clients that do not
// support managed MCP elicitation:
//
// test → MCP SDK Client.callTool("execute") → Executor MCP host
// → execution engine → sandbox code → real executor tool elicitations
// → local approval API records the user's response
// → MCP resume consumes the approved response and returns the result
// ---------------------------------------------------------------------------
import { describe, expect, it } from "@effect/vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { randomBytes } from "node:crypto";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Effect, Schema } from "effect";
import { createExecutionEngine } from "@executor-js/execution";
import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";
import { collectTables } from "@executor-js/api/server";
import {
FormElicitation,
Subject,
Tenant,
createExecutor,
definePlugin,
type Executor,
} from "@executor-js/sdk";
import { createMcpRequestHandler } from "./mcp";
import { createSqliteFumaDb } from "./db/sqlite-fumadb";
const TEST_BASE_URL = "http://local.test";
const EmptyInputSchema = Schema.toStandardSchemaV1(
Schema.toStandardJSONSchemaV1(Schema.Struct({})),
);
const approvalPlugin = definePlugin(() => ({
id: "browser-resume-test" as const,
storage: () => ({}),
staticSources: () => [
{
id: "api",
kind: "in-memory",
name: "API",
tools: [
{
name: "singleApproval",
description: "Elicit exactly once and return the user's response.",
inputSchema: EmptyInputSchema,
handler: ({ elicit }) =>
Effect.gen(function* () {
const response = yield* elicit(
FormElicitation.make({
message: "Approve browser-resume test call",
requestedSchema: {
type: "object",
properties: {
note: { type: "string", title: "Approval note" },
},
required: ["note"],
},
}),
);
return { ok: true, response };
}),
},
],
},
],
}));
const makeExecutor = async (tmpDir: string): Promise<Executor> => {
const plugins = [approvalPlugin()] as const;
const sqlite = await createSqliteFumaDb({
tables: collectTables(),
namespace: "executor_local_browser_resume_test",
path: join(tmpDir, "data.db"),
});
const executor = await Effect.runPromise(
createExecutor({
tenant: Tenant.make(`test-${randomBytes(4).toString("hex")}`),
subject: Subject.make("local"),
db: sqlite.db,
plugins,
onElicitation: "accept-all",
}),
);
const close = executor.close;
return {
...executor,
close: () =>
Effect.gen(function* () {
yield* close();
yield* Effect.promise(() => sqlite.close());
}),
};
};
const makeMcpFetch = (executor: Executor) => {
const engine = createExecutionEngine({
executor,
codeExecutor: makeQuickJsExecutor(),
});
const mcp = createMcpRequestHandler({ engine });
const fetchImpl: typeof globalThis.fetch = Object.assign(
(input: RequestInfo | URL, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input, init);
const url = new URL(request.url);
if (url.pathname.startsWith("/mcp")) return mcp.handleRequest(request);
if (url.pathname.startsWith("/api/mcp-sessions/")) {
return mcp.handleApprovalRequest(request);
}
return Promise.resolve(new Response("Not found", { status: 404 }));
},
{ preconnect: globalThis.fetch.preconnect },
);
return { fetch: fetchImpl, dispose: mcp.close };
};
const readStructuredRecord = (value: unknown): Record<string, unknown> => {
expect(typeof value).toBe("object");
expect(value).not.toBeNull();
return value as Record<string, unknown>;
};
const readApproval = (structured: unknown): { readonly executionId: string; readonly url: URL } => {
const record = readStructuredRecord(structured);
expect(record.status).toBe("user_approval_required");
expect(record).not.toHaveProperty("interaction");
expect(typeof record.executionId).toBe("string");
expect(typeof record.approvalUrl).toBe("string");
expect(record.resumePrompt).toBe(
"Return text to the user telling them to approve the action at this approvalUrl. Only after you have prompted the user, call the `resume` tool with this executionId; `resume` will wait for the user's browser decision.",
);
const { executionId, approvalUrl } = record as {
readonly executionId: string;
readonly approvalUrl: string;
};
return {
executionId,
url: new URL(approvalUrl),
};
};
const readResultArray = (structured: unknown): ReadonlyArray<unknown> => {
const record = readStructuredRecord(structured);
expect(record.status).toBe("completed");
expect(Array.isArray(record.result)).toBe(true);
return record.result as ReadonlyArray<unknown>;
};
describe("local MCP browser approval resume", () => {
it("continues multiple elicitations from one MCP execute call through the HTTP API path", async () => {
const tmpDir = mkdtempSync(join(tmpdir(), "executor-local-browser-resume-"));
const executor = await makeExecutor(tmpDir);
const { fetch, dispose } = makeMcpFetch(executor);
const mcpClient = new Client(
{ name: "browser-resume-test-client", version: "1.0.0" },
{ capabilities: {} },
);
const transport = new StreamableHTTPClientTransport(
new URL("/mcp?elicitation_mode=browser", TEST_BASE_URL),
{ fetch },
);
await mcpClient.connect(transport);
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test owns MCP transports, web handler, and executor lifecycle
try {
const first = await mcpClient.callTool({
name: "execute",
arguments: {
code: `
return await Promise.all([
tools.api.singleApproval({}),
tools.api.singleApproval({}),
tools.api.singleApproval({})
]);
`,
},
});
expect(first.isError).toBeFalsy();
const firstApproval = readApproval(first.structuredContent);
const second = await approveInBrowserThenResume(fetch, mcpClient, firstApproval);
const secondApproval = readApproval(second.structuredContent);
expect(secondApproval.executionId).not.toBe(firstApproval.executionId);
const third = await approveInBrowserThenResume(fetch, mcpClient, secondApproval);
const thirdApproval = readApproval(third.structuredContent);
expect(thirdApproval.executionId).not.toBe(firstApproval.executionId);
expect(thirdApproval.executionId).not.toBe(secondApproval.executionId);
const completed = await approveInBrowserThenResume(fetch, mcpClient, thirdApproval);
const result = readResultArray(completed.structuredContent);
expect(result).toHaveLength(3);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
data: expect.objectContaining({
response: expect.objectContaining({
action: "accept",
content: expect.objectContaining({
note: expect.stringContaining("approved-"),
}),
}),
}),
}),
]),
);
} finally {
await mcpClient.close();
await Effect.runPromise(Effect.ignore(Effect.tryPromise(() => dispose())));
await Effect.runPromise(
Effect.ignore(Effect.tryPromise(() => Effect.runPromise(executor.close()))),
);
rmSync(tmpDir, { recursive: true, force: true });
}
}, 10_000);
});
const approveInBrowserThenResume = async (
fetch: typeof globalThis.fetch,
client: Client,
approval: { readonly executionId: string; readonly url: URL },
) => {
const sessionId = approval.url.searchParams.get("mcp_session_id");
expect(sessionId).not.toBeNull();
const resume = client.callTool({
name: "resume",
arguments: { executionId: approval.executionId },
});
const approvalResponse = await fetch(
new URL(
`/api/mcp-sessions/${encodeURIComponent(sessionId!)}/executions/${encodeURIComponent(approval.executionId)}/resume`,
TEST_BASE_URL,
),
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
action: "accept",
content: { note: `approved-${approval.executionId}` },
}),
},
);
expect(approvalResponse.status).toBe(200);
return await resume;
};