// Cloudflare self-host native elicitation through the real Streamable HTTP // transport. A policy gates a built-in read tool, so the test observes both // directions on the same tools/call stream: elicitation/create reaches the // client, and accept/decline decisions return to the execution engine. import { expect } from "@effect/vitest"; import { Effect } from "effect"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { composePluginApi } from "@executor-js/api/server"; import { scenario } from "../src/scenario"; import { Api, Mcp, Target } from "../src/services"; const coreApi = composePluginApi([] as const); const GATED_TOOL = "executor.coreTools.policies.list"; const GATED_CODE = ` const result = await tools.executor.coreTools.policies.list({}); return JSON.stringify(result); `; scenario( "Cloudflare ยท native MCP elicitation carries approval decisions on the tool call stream", { timeout: 120_000 }, Effect.gen(function* () { const target = yield* Target; const api = yield* Api; const mcp = yield* Mcp; const identity = yield* target.newIdentity(); const apiClient = yield* api.client(coreApi, identity); const policy = yield* apiClient.policies.create({ payload: { owner: "org", pattern: GATED_TOOL, action: "require_approval" }, }); yield* Effect.gen(function* () { let decision: "accept" | "decline" | "cancel" = "accept"; let elicitationCount = 0; const client = yield* Effect.acquireRelease( Effect.promise(async () => { const connectedClient = new Client( { name: "executor-cloudflare-native-elicitation-e2e", version: "1.0.0" }, { capabilities: { elicitation: { form: {}, url: {} } } }, ); connectedClient.setRequestHandler(ElicitRequestSchema, async () => { elicitationCount += 1; return decision === "accept" ? { action: "accept" as const, content: {} } : { action: decision }; }); const url = new URL(mcp.url); url.searchParams.set("elicitation_mode", "native"); await connectedClient.connect(new StreamableHTTPClientTransport(url)); return connectedClient; }), (connectedClient) => Effect.promise(() => connectedClient.close()), ); const accepted = yield* Effect.promise(() => client.callTool({ name: "execute", arguments: { code: GATED_CODE } }, undefined, { timeout: 10_000, }), ); expect(elicitationCount, "the native elicitation reached the client").toBe(1); expect(accepted.isError, "accepting lets the gated tool complete").toBeFalsy(); expect( JSON.stringify(accepted.content), "the gated tool returned its policy listing", ).toContain(policy.id); decision = "decline"; const declined = yield* Effect.promise(() => client.callTool({ name: "execute", arguments: { code: GATED_CODE } }, undefined, { timeout: 10_000, }), ); expect(elicitationCount, "the second native elicitation also reached the client").toBe(2); expect(declined.isError, "declining blocks the gated tool").toBe(true); expect( JSON.stringify(declined.content), "the engine reports the client's decline decision", ).toContain("declined by the user"); expect( JSON.stringify(declined.content), "the declined tool did not return its output", ).not.toContain(policy.id); decision = "cancel"; const cancelled = yield* Effect.promise(() => client.callTool({ name: "execute", arguments: { code: GATED_CODE } }, undefined, { timeout: 10_000, }), ); expect(elicitationCount, "the cancel elicitation also reached the client").toBe(3); expect(cancelled.isError, "cancelling blocks the gated tool").toBe(true); expect( JSON.stringify(cancelled.content), "the engine reports the client's cancel decision", ).toContain("cancelled by the user"); }).pipe( Effect.scoped, Effect.ensuring( apiClient.policies .remove({ params: { policyId: policy.id }, payload: { owner: "org" } }) .pipe(Effect.ignore), ), ); }), );