369 lines · 12.2 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Schema } from "effect";import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";import type { JsonSchemaType } from "@modelcontextprotocol/sdk/validation/types"; import {  AuthTemplateSlug,  ConnectionName,  createExecutor,  FormElicitation,  ElicitationResponse,  IntegrationSlug,  isToolResult,  ToolAddress,  type InvokeOptions,  type Tool,} from "@executor-js/sdk";import {  makeTestConfig,  memoryCredentialsPlugin,  typeCheckOutputTypeScript,} from "@executor-js/sdk/testing"; import { mcpPlugin } from "./plugin";import { makeElicitationMcpServer, serveMcpServer } from "../testing"; const isFormElicitation = Schema.is(FormElicitation); const serveElicitationTestServer = serveMcpServer(makeElicitationMcpServer); const schemaValidator = new CfWorkerJsonSchemaValidator({ shortcircuit: false }); const expectMatchesOutputSchema = (outputSchema: unknown, value: unknown): void => {  expect(outputSchema).toBeDefined();  const result = schemaValidator.getValidator(outputSchema as JsonSchemaType)(value);  expect(result).toEqual({    valid: true,    data: value,    errorMessage: undefined,  });}; const expectToolResultOkData = (result: unknown): unknown => {  expect(isToolResult(result)).toBe(true);  expect(result).toMatchObject({ ok: true });  return (result as { readonly ok: true; readonly data: unknown }).data;}; const INTEG = IntegrationSlug.make("test_mcp");const CONNECTION = ConnectionName.make("main");const TEMPLATE = AuthTemplateSlug.make("none"); // ---------------------------------------------------------------------------// Helper — register an MCP integration pointed at the test server and create// an (open) connection so its tools are produced.// --------------------------------------------------------------------------- const makeTestExecutor = (serverUrl: string) =>  createExecutor(    makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),  ).pipe(    Effect.tap((executor) =>      Effect.gen(function* () {        yield* executor.mcp.addServer({          name: "test-mcp",          endpoint: serverUrl,          slug: String(INTEG),        });        yield* executor.connections.create({          owner: "org",          name: CONNECTION,          integration: INTEG,          template: TEMPLATE,          value: "",        });      }),    ),  ); const findTool = (tools: readonly Tool[], name: string): Tool =>  tools.find((t) => String(t.name) === name)!; // ---------------------------------------------------------------------------// Tests — everything goes through executor.execute()// --------------------------------------------------------------------------- describe("MCP elicitation (end-to-end)", () => {  it.effect("form elicitation accepted → tool returns approved result", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* makeTestExecutor(server.url);       const tools = yield* executor.tools.list();      const gatedEcho = findTool(tools, "gated_echo");       const elicitationMessages: string[] = [];       const options: InvokeOptions = {        onElicitation: (ctx) => {          if (isFormElicitation(ctx.request)) {            elicitationMessages.push(ctx.request.message);          }          return Effect.succeed(            ElicitationResponse.make({              action: "accept",              content: { approved: true },            }),          );        },      };       const result = yield* executor.execute(gatedEcho.address, { value: "hello" }, options);       expect(result).toMatchObject({        ok: true,        data: { content: [{ type: "text", text: "approved:hello" }] },      });      expect(elicitationMessages.length).toBeGreaterThanOrEqual(1);      expect(elicitationMessages.some((m) => m.includes('Approve echo for "hello"?'))).toBe(true);    }),  );   it.effect("form elicitation declined → tool returns denied result", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* makeTestExecutor(server.url);      const tools = yield* executor.tools.list();      const gatedEcho = findTool(tools, "gated_echo");       const result = yield* executor.execute(        gatedEcho.address,        { value: "nope" },        {          onElicitation: () => Effect.succeed(ElicitationResponse.make({ action: "decline" })),        },      );       expect(result).toMatchObject({        ok: true,        data: { content: [{ type: "text", text: "denied:nope" }] },      });    }),  );   it.effect("tool without elicitation works normally", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* makeTestExecutor(server.url);      const tools = yield* executor.tools.list();      const simpleEcho = findTool(tools, "simple_echo");       const result = yield* executor.execute(        simpleEcho.address,        { value: "plain" },        { onElicitation: "accept-all" },      );       expect(result).toMatchObject({        ok: true,        data: { content: [{ type: "text", text: "plain" }] },      });    }),  );   it.effect("registered tools without MCP outputSchema still describe CallToolResult", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* makeTestExecutor(server.url);      const tools = yield* executor.tools.list();      const simpleEcho = findTool(tools, "simple_echo");      const schema = yield* executor.tools.schema(simpleEcho.address);       expect(schema?.outputSchema).toMatchObject({        type: "object",        properties: {          content: { type: "array" },          structuredContent: {},          isError: { const: false },          _meta: { type: "object" },        },        required: ["content"],      });      const outputSchema = schema?.outputSchema as {        readonly properties: {          readonly content: {            readonly items: {              readonly anyOf: readonly unknown[];            };          };        };      };      expect(outputSchema.properties.content.items.anyOf).toEqual(        expect.arrayContaining([          expect.objectContaining({            properties: expect.objectContaining({              type: { const: "text", type: "string" },              text: { type: "string" },            }),            required: ["type", "text"],          }),        ]),      );      expect(schema?.outputTypeScript).toContain('type: "text"');      expect(schema?.outputTypeScript).toContain("structuredContent?: { [k: string]: unknown; }");       const result = yield* executor.execute(        simpleEcho.address,        { value: "plain" },        { onElicitation: "accept-all" },      );       const data = expectToolResultOkData(result);      expectMatchesOutputSchema(schema?.outputSchema, data);      expect(typeCheckOutputTypeScript(schema, data)).toEqual([]);    }),  );   it.effect("successful tool invocation preserves structured MCP result fields", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* makeTestExecutor(server.url);      const tools = yield* executor.tools.list();      const structuredEcho = findTool(tools, "structured_echo");      const schema = yield* executor.tools.schema(structuredEcho.address);       expect(schema?.outputSchema).toMatchObject({        type: "object",        properties: {          content: { type: "array" },          structuredContent: {            type: "object",            properties: {              value: { type: "string" },              upper: { type: "string" },            },          },          _meta: { type: "object" },        },        required: ["content", "structuredContent"],      });      expect(schema?.outputTypeScript).toContain("structuredContent");      expect(schema?.outputTypeScript).toContain("value: string");       const result = yield* executor.execute(        structuredEcho.address,        { value: "plain" },        { onElicitation: "accept-all" },      );       expect(result).toMatchObject({        ok: true,        data: {          content: [{ type: "text", text: "plain" }],          structuredContent: { value: "plain", upper: "PLAIN" },          _meta: { trace: "kept" },        },      });      const data = expectToolResultOkData(result);      expectMatchesOutputSchema(schema?.outputSchema, data);      expect(typeCheckOutputTypeScript(schema, data)).toEqual([]);    }),  );   it.effect("connections.refresh keeps MCP outputSchema nested under structuredContent", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* makeTestExecutor(server.url);       yield* executor.connections.refresh({        owner: "org",        integration: INTEG,        name: CONNECTION,      });       const address = ToolAddress.make("tools.test_mcp.org.main.structured_echo");      const schema = yield* executor.tools.schema(address);      expect(schema?.outputSchema).toMatchObject({        type: "object",        properties: {          content: { type: "array" },          structuredContent: {            type: "object",            properties: {              value: { type: "string" },              upper: { type: "string" },            },          },        },        required: ["content", "structuredContent"],      });      expect(schema?.outputTypeScript).toContain("structuredContent");      expect(schema?.outputTypeScript).toContain("upper: string");       const result = yield* executor.execute(        address,        { value: "plain" },        { onElicitation: "accept-all" },      );      const data = expectToolResultOkData(result);      expect(typeCheckOutputTypeScript(schema, data)).toEqual([]);    }),  );   it.effect("addServer preserves the configured display name as the integration description", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));       yield* executor.mcp.addServer({        name: "Gmail",        endpoint: server.url,        slug: "gmail",      });       const integration = yield* executor.integrations.get(IntegrationSlug.make("gmail"));      expect(integration?.description).toBe("Gmail");    }),  );   it.effect("handler receives correct address, args, and FormElicitation schema", () =>    Effect.gen(function* () {      const server = yield* serveElicitationTestServer;      const executor = yield* makeTestExecutor(server.url);      const tools = yield* executor.tools.list();      const gatedEcho = findTool(tools, "gated_echo");       let capturedAddress: string | undefined;      let capturedArgs: unknown;      let capturedRequest: unknown;       yield* executor.execute(        gatedEcho.address,        { value: "ctx-test" },        {          onElicitation: (ctx) => {            capturedAddress = String(ctx.address);            capturedArgs = ctx.args;            capturedRequest = ctx.request;            return Effect.succeed(              ElicitationResponse.make({                action: "accept",                content: { approved: true },              }),            );          },        },      );       expect(capturedAddress).toBe(String(gatedEcho.address));      expect(capturedArgs).toEqual({ value: "ctx-test" });      expect(isFormElicitation(capturedRequest)).toBe(true);       const form = capturedRequest as FormElicitation;      expect(form.message).toContain('Approve echo for "ctx-test"?');      expect(form.requestedSchema).toEqual({        type: "object",        properties: {          approved: { type: "boolean", title: "Approve" },        },        required: ["approved"],      });    }),  );   // removed: "connection is reused across multiple tool calls" — v2 does not  // cache MCP client connections across invocations (Hyperdrive request-scoped  // rule; each invoke dials and closes its own connection). Session reuse is no  // longer a property of the plugin, so the assertion no longer applies.});