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)), );