import { expect, it } from "@effect/vitest";import { NodeHttpServer } from "@effect/platform-node";import * as NodeServices from "@effect/platform-node/NodeServices";import { EnvironmentId, PreviewTabId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Stream from "effect/Stream";import { McpSchema, McpServer } from "effect/unstable/ai";import { HttpBody, HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http"; import * as McpHttpServer from "./McpHttpServer.ts";import * as McpInvocationContext from "./McpInvocationContext.ts";import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; const environmentId = EnvironmentId.make("environment-mcp-test");const threadId = ThreadId.make("thread-mcp-test");const tabId = PreviewTabId.make("tab-mcp-test");const alternateTabId = PreviewTabId.make("tab-mcp-alternate");const invocation = { environmentId, threadId, providerSessionId: "provider-session-mcp-test", providerInstanceId: ProviderInstanceId.make("codex"), capabilities: new Set(["preview"] as const), issuedAt: 1, expiresAt: Number.MAX_SAFE_INTEGER,};const client = McpSchema.McpServerClient.of({ clientId: 1, initializePayload: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "mcp-test", version: "1.0.0" }, }, getClient: Effect.die("unused"),});const TestLayer = McpHttpServer.PreviewToolkitRegistrationLive.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provideMerge(PreviewAutomationBroker.layer.pipe(Layer.provide(NodeServices.layer))),); it("normalizes empty successful notification responses to accepted", () => { const notificationResponse = McpHttpServer.normalizeMcpHttpResponse( HttpServerResponse.text("", { status: 200, contentType: "application/json" }), ); expect(notificationResponse.status).toBe(202); const resultResponse = McpHttpServer.normalizeMcpHttpResponse( HttpServerResponse.jsonUnsafe({ jsonrpc: "2.0", id: 1, result: {} }), ); expect(resultResponse.status).toBe(200);}); it.effect("returns bounded structural preview snapshot failures", () => Effect.scoped( Effect.gen(function* () { const server = yield* McpServer.McpServer; const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; const events = yield* broker.connect({ clientId: "mcp-failure-client", environmentId, }); yield* Stream.runForEach(events, (event) => event.type === "connected" ? Effect.void : broker.respond({ clientId: "mcp-failure-client", connectionId: event.connectionId, requestId: event.request.requestId, ok: false, error: { _tag: "PreviewAutomationExecutionError", message: "sensitive renderer failure", detail: { consoleOutput: "sensitive browser output" }, }, }), ).pipe(Effect.forkScoped); yield* Effect.yieldNow; const snapshot = yield* server .callTool({ name: "preview_snapshot", arguments: {} }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(snapshot.isError).toBe(true); expect(snapshot.content).toEqual([{ type: "text", text: "Preview snapshot failed." }]); expect(snapshot.structuredContent).toEqual({ error: { _tag: "PreviewAutomationExecutionError", operation: "snapshot", failureCount: 1, }, }); }), ).pipe(Effect.provide(TestLayer)),); it.effect("terminates HTTP MCP sessions with DELETE", () => Effect.scoped( Effect.gen(function* () { const serverLayer = McpServer.layerHttp({ name: "MCP termination test", version: "1.0.0", path: "/mcp", }); yield* HttpRouter.serve(serverLayer, { disableListenLog: true, disableLogger: true, }).pipe(Layer.build); const httpClient = yield* HttpClient.HttpClient; const initializeResponse = yield* httpClient.post("/mcp", { headers: { accept: "application/json, text/event-stream" }, body: HttpBody.text( `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"mcp-test","version":"1.0.0"}}}`, "application/json", ), }); const sessionId = initializeResponse.headers["mcp-session-id"]; expect(initializeResponse.status).toBe(200); expect(sessionId).not.toBeNull(); const missingSessionResponse = yield* httpClient.del("/mcp"); expect(missingSessionResponse.status).toBe(400); const unknownSessionResponse = yield* httpClient.del("/mcp", { headers: { "mcp-session-id": "unknown-session" }, }); expect(unknownSessionResponse.status).toBe(404); const terminateResponse = yield* httpClient.del("/mcp", { headers: { "mcp-session-id": sessionId! }, }); expect(terminateResponse.status).toBe(204); const reusedSessionResponse = yield* httpClient.post("/mcp", { headers: { accept: "application/json, text/event-stream", "mcp-session-id": sessionId!, }, body: HttpBody.text( `{"jsonrpc":"2.0","id":2,"method":"ping","params":{}}`, "application/json", ), }); expect(reusedSessionResponse.status).toBe(404); }), ).pipe(Effect.provide(NodeHttpServer.layerTest)),); it.effect("registers annotated tools and preserves authenticated request context", () => Effect.scoped( Effect.gen(function* () { const server = yield* McpServer.McpServer; const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; const routedRequests: Array<{ readonly operation: string; readonly tabId?: string | undefined; }> = []; const events = yield* broker.connect({ clientId: "mcp-test-client", environmentId, }); yield* Stream.runForEach(events, (event) => { if (event.type === "connected") return Effect.void; routedRequests.push(event.request); return broker.respond({ clientId: "mcp-test-client", connectionId: event.connectionId, requestId: event.request.requestId, ok: true, result: event.request.operation === "snapshot" ? { url: "http://example.test/", title: "Example", loading: false, visibleText: "Example", interactiveElements: [], accessibilityTree: {}, consoleEntries: [], networkEntries: [], actionTimeline: [], screenshot: { mimeType: "image/png", data: Buffer.from("png").toString("base64"), width: 10, height: 5, }, } : event.request.operation === "press" ? undefined : { available: true, visible: true, tabId, url: "http://example.test/", title: "Example", loading: false, }, }); }).pipe(Effect.forkScoped); yield* Effect.yieldNow; const statusTool = server.tools.find(({ tool }) => tool.name === "preview_status"); expect(statusTool?.tool.annotations?.readOnlyHint).toBe(true); expect(statusTool?.tool.annotations?.idempotentHint).toBe(true); expect(statusTool?.tool.annotations?.destructiveHint).toBe(false); const snapshotTool = server.tools.find(({ tool }) => tool.name === "preview_snapshot"); expect(snapshotTool?.tool.annotations?.readOnlyHint).toBe(true); expect(snapshotTool?.tool.annotations?.idempotentHint).toBe(true); expect(snapshotTool?.tool.annotations?.openWorldHint).toBe(true); const clickTool = server.tools.find(({ tool }) => tool.name === "preview_click"); expect(clickTool?.tool.annotations?.readOnlyHint).toBe(false); expect(clickTool?.tool.annotations?.destructiveHint).toBe(true); expect(clickTool?.tool.annotations?.openWorldHint).toBe(true); const navigateTool = server.tools.find(({ tool }) => tool.name === "preview_navigate"); expect(navigateTool?.tool.annotations?.destructiveHint).toBe(false); expect(navigateTool?.tool.annotations?.openWorldHint).toBe(true); const status = yield* server .callTool({ name: "preview_status", arguments: {} }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(status.isError).toBe(false); expect(status.structuredContent).toMatchObject({ available: true, tabId, }); const malformed = yield* server .callTool({ name: "preview_click", arguments: { selector: "" } }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(malformed.isError).toBe(true); const snapshot = yield* server .callTool({ name: "preview_snapshot", arguments: { tabId: alternateTabId } }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(snapshot.isError).toBe(false); expect(snapshot.content.some((content) => content.type === "image")).toBe(true); expect(snapshot.structuredContent).toMatchObject({ screenshot: { mimeType: "image/png", width: 10, height: 5 }, }); expect(routedRequests.find(({ operation }) => operation === "snapshot")?.tabId).toBe( alternateTabId, ); const press = yield* server .callTool({ name: "preview_press", arguments: { key: "Enter" } }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(press.isError).toBe(false); expect(press.structuredContent).toBeNull(); expect(press.content).toEqual([{ type: "text", text: "null" }]); }), ).pipe(Effect.provide(TestLayer)),);
import { expect, it } from "@effect/vitest";
import { NodeHttpServer } from "@effect/platform-node";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { EnvironmentId, PreviewTabId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Stream from "effect/Stream";
import { McpSchema, McpServer } from "effect/unstable/ai";
import { HttpBody, HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http";
import * as McpHttpServer from "./McpHttpServer.ts";
import * as McpInvocationContext from "./McpInvocationContext.ts";
import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";
const environmentId = EnvironmentId.make("environment-mcp-test");
const threadId = ThreadId.make("thread-mcp-test");
const tabId = PreviewTabId.make("tab-mcp-test");
const alternateTabId = PreviewTabId.make("tab-mcp-alternate");
const invocation = {
environmentId,
threadId,
providerSessionId: "provider-session-mcp-test",
providerInstanceId: ProviderInstanceId.make("codex"),
capabilities: new Set(["preview"] as const),
issuedAt: 1,
expiresAt: Number.MAX_SAFE_INTEGER,
};
const client = McpSchema.McpServerClient.of({
clientId: 1,
initializePayload: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "mcp-test", version: "1.0.0" },
},
getClient: Effect.die("unused"),
});
const TestLayer = McpHttpServer.PreviewToolkitRegistrationLive.pipe(
Layer.provideMerge(McpServer.McpServer.layer),
Layer.provideMerge(PreviewAutomationBroker.layer.pipe(Layer.provide(NodeServices.layer))),
);
it("normalizes empty successful notification responses to accepted", () => {
const notificationResponse = McpHttpServer.normalizeMcpHttpResponse(
HttpServerResponse.text("", { status: 200, contentType: "application/json" }),
);
expect(notificationResponse.status).toBe(202);
const resultResponse = McpHttpServer.normalizeMcpHttpResponse(
HttpServerResponse.jsonUnsafe({ jsonrpc: "2.0", id: 1, result: {} }),
);
expect(resultResponse.status).toBe(200);
});
it.effect("returns bounded structural preview snapshot failures", () =>
Effect.scoped(
Effect.gen(function* () {
const server = yield* McpServer.McpServer;
const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
const events = yield* broker.connect({
clientId: "mcp-failure-client",
environmentId,
});
yield* Stream.runForEach(events, (event) =>
event.type === "connected"
? Effect.void
: broker.respond({
clientId: "mcp-failure-client",
connectionId: event.connectionId,
requestId: event.request.requestId,
ok: false,
error: {
_tag: "PreviewAutomationExecutionError",
message: "sensitive renderer failure",
detail: { consoleOutput: "sensitive browser output" },
},
}),
).pipe(Effect.forkScoped);
yield* Effect.yieldNow;
const snapshot = yield* server
.callTool({ name: "preview_snapshot", arguments: {} })
.pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
Effect.provideService(McpSchema.McpServerClient, client),
);
expect(snapshot.isError).toBe(true);
expect(snapshot.content).toEqual([{ type: "text", text: "Preview snapshot failed." }]);
expect(snapshot.structuredContent).toEqual({
error: {
_tag: "PreviewAutomationExecutionError",
operation: "snapshot",
failureCount: 1,
},
});
}),
).pipe(Effect.provide(TestLayer)),
);
it.effect("terminates HTTP MCP sessions with DELETE", () =>
Effect.scoped(
Effect.gen(function* () {
const serverLayer = McpServer.layerHttp({
name: "MCP termination test",
version: "1.0.0",
path: "/mcp",
});
yield* HttpRouter.serve(serverLayer, {
disableListenLog: true,
disableLogger: true,
}).pipe(Layer.build);
const httpClient = yield* HttpClient.HttpClient;
const initializeResponse = yield* httpClient.post("/mcp", {
headers: { accept: "application/json, text/event-stream" },
body: HttpBody.text(
`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"mcp-test","version":"1.0.0"}}}`,
"application/json",
),
});
const sessionId = initializeResponse.headers["mcp-session-id"];
expect(initializeResponse.status).toBe(200);
expect(sessionId).not.toBeNull();
const missingSessionResponse = yield* httpClient.del("/mcp");
expect(missingSessionResponse.status).toBe(400);
const unknownSessionResponse = yield* httpClient.del("/mcp", {
headers: { "mcp-session-id": "unknown-session" },
});
expect(unknownSessionResponse.status).toBe(404);
const terminateResponse = yield* httpClient.del("/mcp", {
headers: { "mcp-session-id": sessionId! },
});
expect(terminateResponse.status).toBe(204);
const reusedSessionResponse = yield* httpClient.post("/mcp", {
headers: {
accept: "application/json, text/event-stream",
"mcp-session-id": sessionId!,
},
body: HttpBody.text(
`{"jsonrpc":"2.0","id":2,"method":"ping","params":{}}`,
"application/json",
),
});
expect(reusedSessionResponse.status).toBe(404);
}),
).pipe(Effect.provide(NodeHttpServer.layerTest)),
);
it.effect("registers annotated tools and preserves authenticated request context", () =>
Effect.scoped(
Effect.gen(function* () {
const server = yield* McpServer.McpServer;
const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
const routedRequests: Array<{
readonly operation: string;
readonly tabId?: string | undefined;
}> = [];
const events = yield* broker.connect({
clientId: "mcp-test-client",
environmentId,
});
yield* Stream.runForEach(events, (event) => {
if (event.type === "connected") return Effect.void;
routedRequests.push(event.request);
return broker.respond({
clientId: "mcp-test-client",
connectionId: event.connectionId,
requestId: event.request.requestId,
ok: true,
result:
event.request.operation === "snapshot"
? {
url: "http://example.test/",
title: "Example",
loading: false,
visibleText: "Example",
interactiveElements: [],
accessibilityTree: {},
consoleEntries: [],
networkEntries: [],
actionTimeline: [],
screenshot: {
mimeType: "image/png",
data: Buffer.from("png").toString("base64"),
width: 10,
height: 5,
},
}
: event.request.operation === "press"
? undefined
: {
available: true,
visible: true,
tabId,
url: "http://example.test/",
title: "Example",
loading: false,
},
});
}).pipe(Effect.forkScoped);
yield* Effect.yieldNow;
const statusTool = server.tools.find(({ tool }) => tool.name === "preview_status");
expect(statusTool?.tool.annotations?.readOnlyHint).toBe(true);
expect(statusTool?.tool.annotations?.idempotentHint).toBe(true);
expect(statusTool?.tool.annotations?.destructiveHint).toBe(false);
const snapshotTool = server.tools.find(({ tool }) => tool.name === "preview_snapshot");
expect(snapshotTool?.tool.annotations?.readOnlyHint).toBe(true);
expect(snapshotTool?.tool.annotations?.idempotentHint).toBe(true);
expect(snapshotTool?.tool.annotations?.openWorldHint).toBe(true);
const clickTool = server.tools.find(({ tool }) => tool.name === "preview_click");
expect(clickTool?.tool.annotations?.readOnlyHint).toBe(false);
expect(clickTool?.tool.annotations?.destructiveHint).toBe(true);
expect(clickTool?.tool.annotations?.openWorldHint).toBe(true);
const navigateTool = server.tools.find(({ tool }) => tool.name === "preview_navigate");
expect(navigateTool?.tool.annotations?.destructiveHint).toBe(false);
expect(navigateTool?.tool.annotations?.openWorldHint).toBe(true);
const status = yield* server
.callTool({ name: "preview_status", arguments: {} })
.pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
Effect.provideService(McpSchema.McpServerClient, client),
);
expect(status.isError).toBe(false);
expect(status.structuredContent).toMatchObject({
available: true,
tabId,
});
const malformed = yield* server
.callTool({ name: "preview_click", arguments: { selector: "" } })
.pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
Effect.provideService(McpSchema.McpServerClient, client),
);
expect(malformed.isError).toBe(true);
const snapshot = yield* server
.callTool({ name: "preview_snapshot", arguments: { tabId: alternateTabId } })
.pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
Effect.provideService(McpSchema.McpServerClient, client),
);
expect(snapshot.isError).toBe(false);
expect(snapshot.content.some((content) => content.type === "image")).toBe(true);
expect(snapshot.structuredContent).toMatchObject({
screenshot: { mimeType: "image/png", width: 10, height: 5 },
});
expect(routedRequests.find(({ operation }) => operation === "snapshot")?.tabId).toBe(
alternateTabId,
);
const press = yield* server
.callTool({ name: "preview_press", arguments: { key: "Enter" } })
.pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
Effect.provideService(McpSchema.McpServerClient, client),
);
expect(press.isError).toBe(false);
expect(press.structuredContent).toBeNull();
expect(press.content).toEqual([{ type: "text", text: "null" }]);
}),
).pipe(Effect.provide(TestLayer)),
);