1115 lines · 37.6 KB · TypeScript
RawDownload
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,  ToolResult,  UrlElicitation,} from "@executor-js/sdk";import type { ToolFileValue } 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");const TEST_IMAGE_MIME_TYPE = "image/png";const TEST_IMAGE_PNG_BASE64 =  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/l8N1wwAAAABJRU5ErkJggg=="; /** 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 },  },}); const toolFile = (input: {  readonly name?: string;  readonly mimeType: string;  readonly data: string;  readonly byteLength: number;}): ToolFileValue => ({  _tag: "ToolFile",  ...(input.name ? { name: input.name } : {}),  mimeType: input.mimeType,  encoding: "base64",  data: input.data,  byteLength: input.byteLength,}); /** 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 renders image ToolFile results as MCP images", async () => {    const engine = makeStubEngine({      execute: () =>        Effect.succeed({          result: ToolResult.ok(            toolFile({              name: "photo.png",              mimeType: "image/png",              data: "iVBORw0KGgo=",              byteLength: 8,            }),          ),        }),    });     await withNativeClient(engine, ELICITATION_CAPS, async (client) => {      const result = await client.callTool({        name: "execute",        arguments: { code: "return await tools.gmail.org.main.getAttachment({});" },      });       const content = result.content as Array<Record<string, unknown>>;      expect(content[0]).toMatchObject({        type: "text",        text: "File output: photo.png\nimage/png, 8 bytes",      });      expect(content[1]).toMatchObject({        type: "image",        data: "iVBORw0KGgo=",        mimeType: "image/png",      });      expect(result.structuredContent).toBeUndefined();      expect(result.isError).toBeFalsy();    });  });   it("execute tool preserves upstream MCP image content as native content", async () => {    const engine = makeStubEngine({      execute: () =>        Effect.succeed({          result: ToolResult.ok({            content: [              {                type: "text",                text: "Deterministic image fixture: mcp-image-fixture.png (image/png, 70 bytes)",              },              {                type: "image",                data: TEST_IMAGE_PNG_BASE64,                mimeType: TEST_IMAGE_MIME_TYPE,              },            ],            structuredContent: {              name: "mcp-image-fixture.png",              mimeType: TEST_IMAGE_MIME_TYPE,              byteLength: 70,            },          }),        }),    });     await withNativeClient(engine, ELICITATION_CAPS, async (client) => {      const result = await client.callTool({        name: "execute",        arguments: {          code: "return await tools.image_mcp.org.main.image_fixture_with_metadata({});",        },      });       expect(result.content).toEqual([        {          type: "text",          text: "Deterministic image fixture: mcp-image-fixture.png (image/png, 70 bytes)",        },        {          type: "image",          data: TEST_IMAGE_PNG_BASE64,          mimeType: TEST_IMAGE_MIME_TYPE,        },      ]);      expect(result.structuredContent).toEqual({        name: "mcp-image-fixture.png",        mimeType: TEST_IMAGE_MIME_TYPE,        byteLength: 70,      });      expect(result.isError).toBeFalsy();    });  });   it("execute tool strips nested ToolFile bytes while preserving result metadata", async () => {    const engine = makeStubEngine({      execute: () =>        Effect.succeed({          result: {            subject: "Flight receipt",            attachment: ToolResult.ok(              toolFile({                name: "boarding-pass.png",                mimeType: "image/png",                data: "iVBORw0KGgo=",                byteLength: 8,              }),            ),          },        }),    });     await withNativeClient(engine, ELICITATION_CAPS, async (client) => {      const result = await client.callTool({        name: "execute",        arguments: { code: "return { subject, attachment };" },      });       const content = result.content as Array<Record<string, unknown>>;      const text = String(content[0]?.text ?? "");      expect(text).toContain("File outputs:\n1. boarding-pass.png (image/png, 8 bytes)");      expect(text).toContain("Result metadata:");      expect(text).toContain("Flight receipt");      expect(text).not.toContain("iVBORw0KGgo=");      expect(content[1]).toMatchObject({        type: "image",        data: "iVBORw0KGgo=",        mimeType: "image/png",      });      expect(result.structuredContent).toBeUndefined();      expect(result.isError).toBeFalsy();    });  });   it("execute tool renders text-like ToolFile results as MCP text without structured content", async () => {    const engine = makeStubEngine({      execute: () =>        Effect.succeed({          result: ToolResult.ok(            toolFile({              name: "rows.csv",              mimeType: "text/csv",              data: "YSxiCjEsMgo=",              byteLength: 8,            }),          ),        }),    });     await withNativeClient(engine, ELICITATION_CAPS, async (client) => {      const result = await client.callTool({        name: "execute",        arguments: { code: "return await tools.files.org.main.getCsv({});" },      });       const content = result.content as Array<Record<string, unknown>>;      expect(content[0]).toMatchObject({        type: "text",        text: "File output: rows.csv\ntext/csv, 8 bytes",      });      expect(content[1]).toMatchObject({        type: "text",        text: "a,b\n1,2\n",      });      expect(result.structuredContent).toBeUndefined();      expect(result.isError).toBeFalsy();    });  });   it("execute tool renders opaque binary ToolFile results as embedded MCP resources", async () => {    const engine = makeStubEngine({      execute: () =>        Effect.succeed({          result: ToolResult.ok(            toolFile({              name: "report.pdf",              mimeType: "application/pdf",              data: "JVBERg==",              byteLength: 4,            }),          ),        }),    });     await withNativeClient(engine, ELICITATION_CAPS, async (client) => {      const result = await client.callTool({        name: "execute",        arguments: { code: "return await tools.gmail.org.main.getAttachment({});" },      });       const content = result.content as Array<Record<string, unknown>>;      expect(content[0]).toMatchObject({        type: "text",        text: "File output: report.pdf\napplication/pdf, 4 bytes",      });      expect(content[1]).toMatchObject({        type: "resource",        resource: {          uri: "executor-file:///report.pdf",          mimeType: "application/pdf",          blob: "JVBERg==",        },      });      expect(result.structuredContent).toMatchObject({        status: "completed",        result: {          ok: true,          data: {            _tag: "ToolFile",            name: "report.pdf",            mimeType: "application/pdf",            encoding: "base64",            byteLength: 4,          },        },      });      expect(JSON.stringify(result.structuredContent)).not.toContain("JVBERg==");      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);      },      {

Lines 1–500 of 1115.