import { describe, expect, it } from "@effect/vitest";import { Data, Deferred, Effect } from "effect";import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js";import type * as Cause from "effect/Cause"; import { ElicitationId, FormElicitation, ToolAddress, UrlElicitation } from "@executor-js/sdk";import type { ExecutionEngine, ExecutionResult } from "@executor-js/execution"; import { createExecutorMcpServer, type ExecutorMcpServerConfig } from "./tool-server"; // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- class TestExecutionError extends Data.TaggedError("TestExecutionError")<{ readonly message: string;}> {} const makeStubEngine = <E extends Cause.YieldableError = never>(overrides: { execute?: ExecutionEngine<E>["execute"]; executeWithPause?: ExecutionEngine<E>["executeWithPause"]; resume?: ExecutionEngine<E>["resume"]; description?: string;}): ExecutionEngine<E> => ({ execute: overrides.execute ?? (() => Effect.succeed({ result: "default" })), executeWithPause: overrides.executeWithPause ?? (() => Effect.succeed({ status: "completed", result: { result: "default" } })), resume: overrides.resume ?? (() => Effect.succeed(null)), getPausedExecution: () => Effect.succeed(null), getDescription: Effect.succeed(overrides.description ?? "test executor"),}); /** Connect a real MCP Client to our executor MCP server over in-memory transports. */const withClient = async <E extends Cause.YieldableError>( engine: ExecutionEngine<E>, capabilities: ClientCapabilities, fn: (client: Client) => Promise<void>, config?: Pick<ExecutorMcpServerConfig<E>, "debug" | "elicitationMode" | "browserApprovalStore">,) => { const mcpServer = await Effect.runPromise(createExecutorMcpServer({ engine, ...config })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities }); await mcpServer.connect(serverTransport); await client.connect(clientTransport); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test helper must close MCP transports after async client assertions try { await fn(client); } finally { await clientTransport.close(); await serverTransport.close(); }}; const withNativeClient = async <E extends Cause.YieldableError>( engine: ExecutionEngine<E>, capabilities: ClientCapabilities, fn: (client: Client) => Promise<void>,) => withClient(engine, capabilities, fn, { elicitationMode: { mode: "native" } }); const ELICITATION_CAPS: ClientCapabilities = { elicitation: { form: {}, url: {} },};const FORM_ONLY_CAPS: ClientCapabilities = { elicitation: { form: {} } };const NO_CAPS: ClientCapabilities = {}; /** Extract the first text content from a callTool result. */const textOf = (result: Awaited<ReturnType<Client["callTool"]>>): string => (result.content as Array<{ type: string; text: string }>)[0].text; const STUB_TOOL_ADDRESS = ToolAddress.make("tools.test.org.main.t"); /** Build a stub paused ExecutionResult with the given id and elicitation request. */const makePausedResult = ( id: string, request: FormElicitation | UrlElicitation,): ExecutionResult => ({ status: "paused", execution: { id, elicitationContext: { address: STUB_TOOL_ADDRESS, args: {}, request }, },}); /** Build an engine whose execute triggers one elicitation and returns the handler's result. */const makeElicitingEngine = ( request: FormElicitation | UrlElicitation, formatResult: (response: { action: string; content?: Record<string, unknown> }) => unknown = ( r, ) => r.action,): ExecutionEngine => makeStubEngine({ execute: (_code, { onElicitation }) => Effect.gen(function* () { const response = yield* onElicitation({ address: STUB_TOOL_ADDRESS, args: {}, request, }); return { result: formatResult(response) }; }), }); // ---------------------------------------------------------------------------// Explicit native elicitation mode// --------------------------------------------------------------------------- describe("MCP host server — native elicitation mode", () => { it("execute tool calls engine.execute and returns result", async () => { const engine = makeStubEngine({ execute: (code) => Effect.succeed({ result: `ran: ${code}` }), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "1+1" }, }); expect(result.content).toEqual([{ type: "text", text: "ran: 1+1" }]); expect(result.isError).toBeFalsy(); }); }); it("execute tool surfaces failed engine effects as an opaque generic with correlation id", async () => { const engine = makeStubEngine({ execute: () => Effect.fail(new TestExecutionError({ message: "Unexpected token ':'" })), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "const x: any = 1;" }, }); const text = textOf(result); expect(text).toMatch(/^Error: Internal tool error \[[0-9a-f]{8}\]$/); expect(text).not.toContain("Unexpected token"); const structured = (result.structuredContent as { readonly error?: string }).error ?? ""; expect(structured).toMatch(/^Internal tool error \[[0-9a-f]{8}\]$/); expect(result.isError).toBe(true); }); }); it("execute tool hides defect details in MCP error results", async () => { const engine = makeStubEngine({ // oxlint-disable-next-line executor/no-effect-escape-hatch, executor/no-error-constructor -- boundary: test injects a defect to verify MCP error redaction execute: () => Effect.die(new Error("secret internal detail")), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "run" }, }); const text = textOf(result); expect(text).toMatch(/^Error: Internal tool error \[[0-9a-f]{8}\]$/); // Sensitive internal context must NOT leak through the MCP error path. expect(text).not.toContain("secret internal detail"); expect(result.structuredContent).toMatchObject({ status: "error", }); const structuredError = (result.structuredContent as { readonly error?: string }).error ?? ""; expect(structuredError).toMatch(/^Internal tool error \[[0-9a-f]{8}\]$/); expect(structuredError).not.toContain("secret internal detail"); expect(result.isError).toBe(true); }); }); it("form elicitation is bridged from engine to MCP client and back", async () => { const engine = makeElicitingEngine( FormElicitation.make({ message: "Approve this action?", requestedSchema: { type: "object", properties: { approved: { type: "boolean" } }, }, }), (r) => (r.action === "accept" && r.content?.approved ? "approved" : "denied"), ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async () => ({ action: "accept" as const, content: { approved: true }, })); const result = await client.callTool({ name: "execute", arguments: { code: "do-it" }, }); expect(result.content).toEqual([{ type: "text", text: "approved" }]); }); }); it("form elicitation declined by client → engine sees decline", async () => { const engine = makeElicitingEngine( FormElicitation.make({ message: "Accept?", requestedSchema: {} }), (r) => `action:${r.action}`, ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async () => ({ action: "decline" as const, content: {}, })); const result = await client.callTool({ name: "execute", arguments: { code: "x" }, }); expect(result.content).toEqual([{ type: "text", text: "action:decline" }]); }); }); it("browser approval mode does not auto-switch to native elicitation", async () => { let approvalUrlCalled = false; let executeCalled = false; const engine = makeStubEngine({ execute: () => Effect.sync(() => { executeCalled = true; return { result: "should-not-run" }; }), executeWithPause: () => Effect.sync(() => { return makePausedResult( "exec_browser_1", FormElicitation.make({ message: "Paused", requestedSchema: {} }), ); }), }); await withClient( engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async () => ({ action: "accept" as const, content: {}, })); const { tools } = await client.listTools(); expect(tools.map((t) => t.name)).toContain("resume"); const result = await client.callTool({ name: "execute", arguments: { code: "needs-inline-approval" }, }); expect(result.structuredContent).toMatchObject({ status: "user_approval_required", executionId: "exec_browser_1", approvalUrl: "https://executor.test/resume/exec_browser_1", }); expect(result.structuredContent).not.toHaveProperty("interaction"); expect(executeCalled).toBe(false); expect(approvalUrlCalled).toBe(true); }, { elicitationMode: { mode: "browser", approvalUrl: (executionId) => { approvalUrlCalled = true; return `https://executor.test/resume/${executionId}`; }, }, }, ); }); it("empty form schema gets wrapped with minimal valid schema", async () => { let receivedSchema: unknown; const engine = makeElicitingEngine( FormElicitation.make({ message: "Just approve", requestedSchema: {} }), ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async (request) => { const params = request.params; if ("requestedSchema" in params) { receivedSchema = params.requestedSchema; } return { action: "accept" as const, content: {} }; }); await client.callTool({ name: "execute", arguments: { code: "approve" }, }); expect(receivedSchema).toEqual({ type: "object", properties: {} }); }); }); it("UrlElicitation is sent as native mode:url elicitation", async () => { let receivedParams: Record<string, unknown> | undefined; const engine = makeElicitingEngine( UrlElicitation.make({ message: "Please authenticate", url: "https://example.com/oauth", elicitationId: ElicitationId.make("elic-1"), }), ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async (request) => { receivedParams = request.params as Record<string, unknown>; return { action: "accept" as const, content: {} }; }); await client.callTool({ name: "execute", arguments: { code: "oauth" }, }); expect(receivedParams?.mode).toBe("url"); expect(receivedParams?.message).toBe("Please authenticate"); expect(receivedParams?.url).toBe("https://example.com/oauth"); expect(receivedParams?.elicitationId).toBe("elic-1"); }); }); it("engine error is surfaced as isError result", async () => { const engine = makeStubEngine({ execute: () => Effect.succeed({ result: null, error: "something broke", logs: ["log1"], }), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "bad" }, }); expect(result.isError).toBe(true); expect(textOf(result)).toContain("something broke"); }); }); it("resume tool is hidden in native elicitation mode", async () => { await withNativeClient(makeStubEngine({}), ELICITATION_CAPS, async (client) => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); expect(names).toContain("execute"); expect(names).not.toContain("resume"); }); });}); // ---------------------------------------------------------------------------// Client with form-only elicitation in native mode// --------------------------------------------------------------------------- describe("MCP host server — native form-only elicitation", () => { it("resume tool is hidden in native mode", async () => { await withNativeClient(makeStubEngine({}), FORM_ONLY_CAPS, async (client) => { const { tools } = await client.listTools(); expect(tools.map((t) => t.name)).toContain("execute"); expect(tools.map((t) => t.name)).not.toContain("resume"); }); }); it("uses native elicitation path when client supports form", async () => { const engine = makeStubEngine({ execute: (code) => Effect.succeed({ result: `native: ${code}` }), }); await withNativeClient(engine, FORM_ONLY_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "test" }, }); expect(result.content).toEqual([{ type: "text", text: "native: test" }]); }); }); it("UrlElicitation falls back to form when client lacks url support", async () => { let receivedMessage: string | undefined; const engine = makeElicitingEngine( UrlElicitation.make({ message: "Please authenticate", url: "https://auth.example.com/oauth", elicitationId: ElicitationId.make("elic-1"), }), ); await withNativeClient(engine, FORM_ONLY_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async (request) => { receivedMessage = typeof request.params.message === "string" ? request.params.message : undefined; return { action: "accept" as const, content: {} }; }); const result = await client.callTool({ name: "execute", arguments: { code: "oauth" }, }); expect(result.content).toEqual([{ type: "text", text: "accept" }]); expect(receivedMessage).toContain("https://auth.example.com/oauth"); expect(receivedMessage).toContain("Please authenticate"); }); });}); // ---------------------------------------------------------------------------// Client WITHOUT elicitation (pause/resume path)// --------------------------------------------------------------------------- describe("MCP host server — client without elicitation (pause/resume)", () => { it("completed execution returns result directly", async () => { const engine = makeStubEngine({ executeWithPause: () => Effect.succeed({ status: "completed", result: { result: "done" }, }), }); await withClient(engine, NO_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "ok" }, }); expect(result.content).toEqual([{ type: "text", text: "done" }]); expect(result.isError).toBeFalsy(); }); }); it("both execute and resume tools are visible", async () => { await withClient(makeStubEngine({}), NO_CAPS, async (client) => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); expect(names).toContain("execute"); expect(names).toContain("resume"); }); }); it("browser approval mode requires user approval before resume", async () => { let resumeCalled = false; const engine = makeStubEngine({ resume: () => Effect.sync(() => { resumeCalled = true; return { status: "completed", result: { result: "should-not-run" } }; }), }); await withClient( engine, NO_CAPS, async (client) => { const result = await client.callTool({ name: "resume", arguments: { executionId: "exec_1" }, }); expect(resumeCalled).toBe(false); expect(result.isError).toBeFalsy(); expect(textOf(result)).toContain("User approval required"); expect(textOf(result)).toContain("https://executor.test/resume/exec_1"); expect(textOf(result)).toContain( "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.", ); expect(result.structuredContent).toMatchObject({ status: "user_approval_required", executionId: "exec_1", approvalUrl: "https://executor.test/resume/exec_1", resumePrompt: "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.", }); }, { elicitationMode: { mode: "browser", approvalUrl: (executionId) => `https://executor.test/resume/${executionId}`, }, }, ); }); it("browser approval mode consumes a user-approved response and returns the resumed result", async () => { const approved = new Map<string, { action: "accept"; content?: Record<string, unknown> }>(); const waiter = await Effect.runPromise( Deferred.make<{ action: "accept"; content?: Record<string, unknown> }>(), ); const engine = makeStubEngine({ resume: (executionId, response) => Effect.succeed( executionId === "exec_1" && response.action === "accept" ? { status: "completed", result: { result: "resumed-after-browser" } } : null, ), }); await withClient( engine, NO_CAPS, async (client) => {Lines 1–500 of 875. Load more
import { describe, expect, it } from "@effect/vitest"; import { Data, Deferred, Effect } from "effect"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js"; import type * as Cause from "effect/Cause"; import { ElicitationId, FormElicitation, ToolAddress, UrlElicitation } from "@executor-js/sdk"; import type { ExecutionEngine, ExecutionResult } from "@executor-js/execution"; import { createExecutorMcpServer, type ExecutorMcpServerConfig } from "./tool-server"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- class TestExecutionError extends Data.TaggedError("TestExecutionError")<{ readonly message: string; }> {} const makeStubEngine = <E extends Cause.YieldableError = never>(overrides: { execute?: ExecutionEngine<E>["execute"]; executeWithPause?: ExecutionEngine<E>["executeWithPause"]; resume?: ExecutionEngine<E>["resume"]; description?: string; }): ExecutionEngine<E> => ({ execute: overrides.execute ?? (() => Effect.succeed({ result: "default" })), executeWithPause: overrides.executeWithPause ?? (() => Effect.succeed({ status: "completed", result: { result: "default" } })), resume: overrides.resume ?? (() => Effect.succeed(null)), getPausedExecution: () => Effect.succeed(null), getDescription: Effect.succeed(overrides.description ?? "test executor"), }); /** Connect a real MCP Client to our executor MCP server over in-memory transports. */ const withClient = async <E extends Cause.YieldableError>( engine: ExecutionEngine<E>, capabilities: ClientCapabilities, fn: (client: Client) => Promise<void>, config?: Pick<ExecutorMcpServerConfig<E>, "debug" | "elicitationMode" | "browserApprovalStore">, ) => { const mcpServer = await Effect.runPromise(createExecutorMcpServer({ engine, ...config })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities }); await mcpServer.connect(serverTransport); await client.connect(clientTransport); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: test helper must close MCP transports after async client assertions try { await fn(client); } finally { await clientTransport.close(); await serverTransport.close(); } }; const withNativeClient = async <E extends Cause.YieldableError>( engine: ExecutionEngine<E>, capabilities: ClientCapabilities, fn: (client: Client) => Promise<void>, ) => withClient(engine, capabilities, fn, { elicitationMode: { mode: "native" } }); const ELICITATION_CAPS: ClientCapabilities = { elicitation: { form: {}, url: {} }, }; const FORM_ONLY_CAPS: ClientCapabilities = { elicitation: { form: {} } }; const NO_CAPS: ClientCapabilities = {}; /** Extract the first text content from a callTool result. */ const textOf = (result: Awaited<ReturnType<Client["callTool"]>>): string => (result.content as Array<{ type: string; text: string }>)[0].text; const STUB_TOOL_ADDRESS = ToolAddress.make("tools.test.org.main.t"); /** Build a stub paused ExecutionResult with the given id and elicitation request. */ const makePausedResult = ( id: string, request: FormElicitation | UrlElicitation, ): ExecutionResult => ({ status: "paused", execution: { id, elicitationContext: { address: STUB_TOOL_ADDRESS, args: {}, request }, }, }); /** Build an engine whose execute triggers one elicitation and returns the handler's result. */ const makeElicitingEngine = ( request: FormElicitation | UrlElicitation, formatResult: (response: { action: string; content?: Record<string, unknown> }) => unknown = ( r, ) => r.action, ): ExecutionEngine => makeStubEngine({ execute: (_code, { onElicitation }) => Effect.gen(function* () { const response = yield* onElicitation({ address: STUB_TOOL_ADDRESS, args: {}, request, }); return { result: formatResult(response) }; }), }); // --------------------------------------------------------------------------- // Explicit native elicitation mode // --------------------------------------------------------------------------- describe("MCP host server — native elicitation mode", () => { it("execute tool calls engine.execute and returns result", async () => { const engine = makeStubEngine({ execute: (code) => Effect.succeed({ result: `ran: ${code}` }), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "1+1" }, }); expect(result.content).toEqual([{ type: "text", text: "ran: 1+1" }]); expect(result.isError).toBeFalsy(); }); }); it("execute tool surfaces failed engine effects as an opaque generic with correlation id", async () => { const engine = makeStubEngine({ execute: () => Effect.fail(new TestExecutionError({ message: "Unexpected token ':'" })), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "const x: any = 1;" }, }); const text = textOf(result); expect(text).toMatch(/^Error: Internal tool error \[[0-9a-f]{8}\]$/); expect(text).not.toContain("Unexpected token"); const structured = (result.structuredContent as { readonly error?: string }).error ?? ""; expect(structured).toMatch(/^Internal tool error \[[0-9a-f]{8}\]$/); expect(result.isError).toBe(true); }); }); it("execute tool hides defect details in MCP error results", async () => { const engine = makeStubEngine({ // oxlint-disable-next-line executor/no-effect-escape-hatch, executor/no-error-constructor -- boundary: test injects a defect to verify MCP error redaction execute: () => Effect.die(new Error("secret internal detail")), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "run" }, }); const text = textOf(result); expect(text).toMatch(/^Error: Internal tool error \[[0-9a-f]{8}\]$/); // Sensitive internal context must NOT leak through the MCP error path. expect(text).not.toContain("secret internal detail"); expect(result.structuredContent).toMatchObject({ status: "error", }); const structuredError = (result.structuredContent as { readonly error?: string }).error ?? ""; expect(structuredError).toMatch(/^Internal tool error \[[0-9a-f]{8}\]$/); expect(structuredError).not.toContain("secret internal detail"); expect(result.isError).toBe(true); }); }); it("form elicitation is bridged from engine to MCP client and back", async () => { const engine = makeElicitingEngine( FormElicitation.make({ message: "Approve this action?", requestedSchema: { type: "object", properties: { approved: { type: "boolean" } }, }, }), (r) => (r.action === "accept" && r.content?.approved ? "approved" : "denied"), ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async () => ({ action: "accept" as const, content: { approved: true }, })); const result = await client.callTool({ name: "execute", arguments: { code: "do-it" }, }); expect(result.content).toEqual([{ type: "text", text: "approved" }]); }); }); it("form elicitation declined by client → engine sees decline", async () => { const engine = makeElicitingEngine( FormElicitation.make({ message: "Accept?", requestedSchema: {} }), (r) => `action:${r.action}`, ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async () => ({ action: "decline" as const, content: {}, })); const result = await client.callTool({ name: "execute", arguments: { code: "x" }, }); expect(result.content).toEqual([{ type: "text", text: "action:decline" }]); }); }); it("browser approval mode does not auto-switch to native elicitation", async () => { let approvalUrlCalled = false; let executeCalled = false; const engine = makeStubEngine({ execute: () => Effect.sync(() => { executeCalled = true; return { result: "should-not-run" }; }), executeWithPause: () => Effect.sync(() => { return makePausedResult( "exec_browser_1", FormElicitation.make({ message: "Paused", requestedSchema: {} }), ); }), }); await withClient( engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async () => ({ action: "accept" as const, content: {}, })); const { tools } = await client.listTools(); expect(tools.map((t) => t.name)).toContain("resume"); const result = await client.callTool({ name: "execute", arguments: { code: "needs-inline-approval" }, }); expect(result.structuredContent).toMatchObject({ status: "user_approval_required", executionId: "exec_browser_1", approvalUrl: "https://executor.test/resume/exec_browser_1", }); expect(result.structuredContent).not.toHaveProperty("interaction"); expect(executeCalled).toBe(false); expect(approvalUrlCalled).toBe(true); }, { elicitationMode: { mode: "browser", approvalUrl: (executionId) => { approvalUrlCalled = true; return `https://executor.test/resume/${executionId}`; }, }, }, ); }); it("empty form schema gets wrapped with minimal valid schema", async () => { let receivedSchema: unknown; const engine = makeElicitingEngine( FormElicitation.make({ message: "Just approve", requestedSchema: {} }), ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async (request) => { const params = request.params; if ("requestedSchema" in params) { receivedSchema = params.requestedSchema; } return { action: "accept" as const, content: {} }; }); await client.callTool({ name: "execute", arguments: { code: "approve" }, }); expect(receivedSchema).toEqual({ type: "object", properties: {} }); }); }); it("UrlElicitation is sent as native mode:url elicitation", async () => { let receivedParams: Record<string, unknown> | undefined; const engine = makeElicitingEngine( UrlElicitation.make({ message: "Please authenticate", url: "https://example.com/oauth", elicitationId: ElicitationId.make("elic-1"), }), ); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async (request) => { receivedParams = request.params as Record<string, unknown>; return { action: "accept" as const, content: {} }; }); await client.callTool({ name: "execute", arguments: { code: "oauth" }, }); expect(receivedParams?.mode).toBe("url"); expect(receivedParams?.message).toBe("Please authenticate"); expect(receivedParams?.url).toBe("https://example.com/oauth"); expect(receivedParams?.elicitationId).toBe("elic-1"); }); }); it("engine error is surfaced as isError result", async () => { const engine = makeStubEngine({ execute: () => Effect.succeed({ result: null, error: "something broke", logs: ["log1"], }), }); await withNativeClient(engine, ELICITATION_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "bad" }, }); expect(result.isError).toBe(true); expect(textOf(result)).toContain("something broke"); }); }); it("resume tool is hidden in native elicitation mode", async () => { await withNativeClient(makeStubEngine({}), ELICITATION_CAPS, async (client) => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); expect(names).toContain("execute"); expect(names).not.toContain("resume"); }); }); }); // --------------------------------------------------------------------------- // Client with form-only elicitation in native mode // --------------------------------------------------------------------------- describe("MCP host server — native form-only elicitation", () => { it("resume tool is hidden in native mode", async () => { await withNativeClient(makeStubEngine({}), FORM_ONLY_CAPS, async (client) => { const { tools } = await client.listTools(); expect(tools.map((t) => t.name)).toContain("execute"); expect(tools.map((t) => t.name)).not.toContain("resume"); }); }); it("uses native elicitation path when client supports form", async () => { const engine = makeStubEngine({ execute: (code) => Effect.succeed({ result: `native: ${code}` }), }); await withNativeClient(engine, FORM_ONLY_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "test" }, }); expect(result.content).toEqual([{ type: "text", text: "native: test" }]); }); }); it("UrlElicitation falls back to form when client lacks url support", async () => { let receivedMessage: string | undefined; const engine = makeElicitingEngine( UrlElicitation.make({ message: "Please authenticate", url: "https://auth.example.com/oauth", elicitationId: ElicitationId.make("elic-1"), }), ); await withNativeClient(engine, FORM_ONLY_CAPS, async (client) => { client.setRequestHandler(ElicitRequestSchema, async (request) => { receivedMessage = typeof request.params.message === "string" ? request.params.message : undefined; return { action: "accept" as const, content: {} }; }); const result = await client.callTool({ name: "execute", arguments: { code: "oauth" }, }); expect(result.content).toEqual([{ type: "text", text: "accept" }]); expect(receivedMessage).toContain("https://auth.example.com/oauth"); expect(receivedMessage).toContain("Please authenticate"); }); }); }); // --------------------------------------------------------------------------- // Client WITHOUT elicitation (pause/resume path) // --------------------------------------------------------------------------- describe("MCP host server — client without elicitation (pause/resume)", () => { it("completed execution returns result directly", async () => { const engine = makeStubEngine({ executeWithPause: () => Effect.succeed({ status: "completed", result: { result: "done" }, }), }); await withClient(engine, NO_CAPS, async (client) => { const result = await client.callTool({ name: "execute", arguments: { code: "ok" }, }); expect(result.content).toEqual([{ type: "text", text: "done" }]); expect(result.isError).toBeFalsy(); }); }); it("both execute and resume tools are visible", async () => { await withClient(makeStubEngine({}), NO_CAPS, async (client) => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); expect(names).toContain("execute"); expect(names).toContain("resume"); }); }); it("browser approval mode requires user approval before resume", async () => { let resumeCalled = false; const engine = makeStubEngine({ resume: () => Effect.sync(() => { resumeCalled = true; return { status: "completed", result: { result: "should-not-run" } }; }), }); await withClient( engine, NO_CAPS, async (client) => { const result = await client.callTool({ name: "resume", arguments: { executionId: "exec_1" }, }); expect(resumeCalled).toBe(false); expect(result.isError).toBeFalsy(); expect(textOf(result)).toContain("User approval required"); expect(textOf(result)).toContain("https://executor.test/resume/exec_1"); expect(textOf(result)).toContain( "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.", ); expect(result.structuredContent).toMatchObject({ status: "user_approval_required", executionId: "exec_1", approvalUrl: "https://executor.test/resume/exec_1", resumePrompt: "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.", }); }, { elicitationMode: { mode: "browser", approvalUrl: (executionId) => `https://executor.test/resume/${executionId}`, }, }, ); }); it("browser approval mode consumes a user-approved response and returns the resumed result", async () => { const approved = new Map<string, { action: "accept"; content?: Record<string, unknown> }>(); const waiter = await Effect.runPromise( Deferred.make<{ action: "accept"; content?: Record<string, unknown> }>(), ); const engine = makeStubEngine({ resume: (executionId, response) => Effect.succeed( executionId === "exec_1" && response.action === "accept" ? { status: "completed", result: { result: "resumed-after-browser" } } : null, ), }); await withClient( engine, NO_CAPS, async (client) => {