import { describe, expect, it } from "@effect/vitest";import { Effect, Predicate, Schema, Semaphore } from "effect";import type { JsonSchemaType } from "@modelcontextprotocol/client";import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/client/validators/cf-worker"; 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 elicitationServerSemaphore = Semaphore.makeUnsafe(1);const serveElicitationTestServer = Effect.acquireRelease(elicitationServerSemaphore.take(1), () => elicitationServerSemaphore.release(1).pipe(Effect.asVoid),).pipe(Effect.flatMap(() => 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) => Effect.acquireRelease( createExecutor(makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const })), (executor) => executor.close().pipe(Effect.ignore), ).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("reuses the session while installing each invoke's elicitation handler", () => 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 acceptedMessages: string[] = []; const declinedMessages: string[] = []; let capturedAddress: string | undefined; let capturedArgs: unknown; let capturedRequest: unknown; const options: InvokeOptions = { onElicitation: (ctx) => { capturedAddress = String(ctx.address); capturedArgs = ctx.args; capturedRequest = ctx.request; if (isFormElicitation(ctx.request)) { acceptedMessages.push(ctx.request.message); } return Effect.succeed( ElicitationResponse.make({ action: "accept", content: { approved: true }, }), ); }, }; yield* server.clearRequests; const accepted = yield* executor.execute(gatedEcho.address, { value: "hello" }, options); const declined = yield* executor.execute( gatedEcho.address, { value: "nope" }, { onElicitation: (ctx) => { if (isFormElicitation(ctx.request)) { declinedMessages.push(ctx.request.message); } return Effect.succeed(ElicitationResponse.make({ action: "decline" })); }, }, ); yield* executor.close(); expect(accepted).toMatchObject({ ok: true, data: { content: [{ type: "text", text: "approved:hello" }] }, }); expect(declined).toMatchObject({ ok: true, data: { content: [{ type: "text", text: "denied:nope" }] }, }); expect(acceptedMessages).toEqual(['Approve echo for "hello"?']); expect(declinedMessages).toEqual(['Approve echo for "nope"?']); expect(capturedAddress).toBe(String(gatedEcho.address)); expect(capturedArgs).toEqual({ value: "hello" }); expect(isFormElicitation(capturedRequest)).toBe(true); const form = capturedRequest as FormElicitation; expect(form.message).toContain('Approve echo for "hello"?'); expect(form.requestedSchema).toEqual({ type: "object", properties: { approved: { type: "boolean", title: "Approve" }, }, required: ["approved"], }); const sessionIds = new Set( (yield* server.requests) .map((request) => request.sessionId) .filter(Predicate.isNotUndefined), ); expect(sessionIds.size).toBe(1); }), ); 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?: 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"); }), );});
import { describe, expect, it } from "@effect/vitest";
import { Effect, Predicate, Schema, Semaphore } from "effect";
import type { JsonSchemaType } from "@modelcontextprotocol/client";
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/client/validators/cf-worker";
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 elicitationServerSemaphore = Semaphore.makeUnsafe(1);
const serveElicitationTestServer = Effect.acquireRelease(elicitationServerSemaphore.take(1), () =>
elicitationServerSemaphore.release(1).pipe(Effect.asVoid),
).pipe(Effect.flatMap(() => 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) =>
Effect.acquireRelease(
createExecutor(makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const })),
(executor) => executor.close().pipe(Effect.ignore),
).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("reuses the session while installing each invoke's elicitation handler", () =>
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 acceptedMessages: string[] = [];
const declinedMessages: string[] = [];
let capturedAddress: string | undefined;
let capturedArgs: unknown;
let capturedRequest: unknown;
const options: InvokeOptions = {
onElicitation: (ctx) => {
capturedAddress = String(ctx.address);
capturedArgs = ctx.args;
capturedRequest = ctx.request;
if (isFormElicitation(ctx.request)) {
acceptedMessages.push(ctx.request.message);
}
return Effect.succeed(
ElicitationResponse.make({
action: "accept",
content: { approved: true },
}),
);
},
};
yield* server.clearRequests;
const accepted = yield* executor.execute(gatedEcho.address, { value: "hello" }, options);
const declined = yield* executor.execute(
gatedEcho.address,
{ value: "nope" },
{
onElicitation: (ctx) => {
if (isFormElicitation(ctx.request)) {
declinedMessages.push(ctx.request.message);
}
return Effect.succeed(ElicitationResponse.make({ action: "decline" }));
},
},
);
yield* executor.close();
expect(accepted).toMatchObject({
ok: true,
data: { content: [{ type: "text", text: "approved:hello" }] },
});
expect(declined).toMatchObject({
ok: true,
data: { content: [{ type: "text", text: "denied:nope" }] },
});
expect(acceptedMessages).toEqual(['Approve echo for "hello"?']);
expect(declinedMessages).toEqual(['Approve echo for "nope"?']);
expect(capturedAddress).toBe(String(gatedEcho.address));
expect(capturedArgs).toEqual({ value: "hello" });
expect(isFormElicitation(capturedRequest)).toBe(true);
const form = capturedRequest as FormElicitation;
expect(form.message).toContain('Approve echo for "hello"?');
expect(form.requestedSchema).toEqual({
type: "object",
properties: {
approved: { type: "boolean", title: "Approve" },
},
required: ["approved"],
});
const sessionIds = new Set(
(yield* server.requests)
.map((request) => request.sessionId)
.filter(Predicate.isNotUndefined),
);
expect(sessionIds.size).toBe(1);
}),
);
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?: 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");
}),
);
});