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 Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import { McpProtocol, McpSchema, McpServer } from "effect/unstable/ai";import { HttpBody, HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http"; import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts";import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";import * as ServerConfig from "../config.ts";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 decodeJsonText = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));const invocation = { environmentId, threadId, providerSessionId: "provider-session-mcp-test", providerInstanceId: ProviderInstanceId.make("codex"), capabilities: new Set(["preview"] as const), issuedAt: 1,};const client = McpSchema.McpServerClient.of({ clientId: 1, clientCapabilities: {}, clientInfo: { name: "mcp-test", version: "1.0.0" }, protocolVersion: "2025-06-18", initializePayload: { protocolVersion: "2025-06-18", 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), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-http-server-test-" })), Layer.provideMerge(NodeServices.layer),);const PullRequestsTestLayer = McpHttpServer.PullRequestsToolkitRegistrationLive.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide( Layer.mergeAll( Layer.mock(ProjectionSnapshotQuery)({ getThreadShellById: () => Effect.succeed(Option.none()), }), Layer.mock(OrchestrationEngineService)({}), NodeServices.layer, ), ),); const snapshotResult = { 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, },}; /** Answers every snapshot request on a fresh broker host with the given result. */const serveSnapshots = (clientId: string, result: unknown) => Effect.gen(function* () { const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; const connected = yield* Deferred.make<void>(); const inputs: Array<unknown> = []; const events = yield* broker.connect({ clientId, environmentId }); yield* Stream.runForEach(events, (event) => { if (event.type === "connected") return Deferred.succeed(connected, undefined); inputs.push(event.request.input); return broker.respond({ clientId, connectionId: event.connectionId, requestId: event.request.requestId, ok: true, result, }); }).pipe(Effect.forkScoped); yield* Deferred.await(connected); return inputs; }); const callSnapshot = (args: Record<string, unknown>) => Effect.gen(function* () { const server = yield* McpServer.McpServer; return yield* server .callTool({ name: "preview_snapshot", arguments: args }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); }); 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.each([{}, { includeImage: false }])( "returns bounded structural preview snapshot failures %#", (input) => 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: input }) .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: PreviewAutomationExecutionError." }, ]); expect(snapshot.structuredContent).toEqual({ error: { _tag: "PreviewAutomationExecutionError", operation: "snapshot", failureCount: 1, }, }); }), ).pipe(Effect.provide(TestLayer)),); it.effect.each([ { mode: "default", input: {}, images: true }, { mode: "explicit image", input: { includeImage: true }, images: true }, { mode: "text only", input: { includeImage: false }, images: false },])("returns fresh $mode snapshots on repeated MCP calls", ({ input, images }) => Effect.scoped( Effect.gen(function* () { const server = yield* McpServer.McpServer; const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; const connected = yield* Deferred.make<void>(); const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII="; const page = { url: "http://example.test/", loading: false, visibleText: "Save your changes", interactiveElements: [ { tag: "button", role: "button", name: "Save", selector: "#save", x: 0, y: 0, width: 20, height: 10, }, ], accessibilityTree: { role: "document", name: "Example" }, consoleEntries: [], networkEntries: [], actionTimeline: [], }; const screenshot = { mimeType: "image/png", width: 1, height: 1 }; let requests = 0; const events = yield* broker.connect({ clientId: "mcp-image-option-client", environmentId }); yield* Stream.runForEach(events, (event) => { if (event.type === "connected") return Deferred.succeed(connected, undefined); requests += 1; expect(event.request).toMatchObject({ operation: "snapshot", tabId: alternateTabId, threadId, }); expect(event.request.input).toEqual({}); return broker.respond({ clientId: "mcp-image-option-client", connectionId: event.connectionId, requestId: event.request.requestId, ok: true, result: { ...page, title: `Snapshot ${requests}`, screenshot: { ...screenshot, data: png }, }, }); }).pipe(Effect.forkScoped); yield* Deferred.await(connected); for (const call of [1, 2, 3, 4, 5, 6]) { const snapshot = yield* server .callTool({ name: "preview_snapshot", arguments: { ...input, tabId: alternateTabId }, }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); const metadata = { ...page, title: `Snapshot ${call}`, screenshot }; const { accessibilityTree: _tree, ...boundedMetadata } = metadata; expect(snapshot.isError).toBe(false); expect(snapshot.structuredContent).toEqual(metadata); const [identity, text, ...rest] = snapshot.content; expect(identity?.type === "text" ? decodeJsonText(identity.text) : null).toEqual({ url: page.url, }); expect(text?.type === "text" ? decodeJsonText(text.text) : null).toEqual(boundedMetadata); expect(rest).toEqual([ { type: "text", text: "Snapshot text was bounded. Omitted: accessibilityTree (use interactiveElements locators or preview_evaluate).", }, ...(images ? [ { type: "image", mimeType: "image/png", data: new Uint8Array(Buffer.from(png, "base64")), }, ] : []), ]); } // Output selection belongs to this call, not the MCP session's history. const nextDefault = yield* server .callTool({ name: "preview_snapshot", arguments: { tabId: alternateTabId }, }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(nextDefault.content.map((content) => content.type)).toEqual([ "text", "text", "text", "image", ]); expect(nextDefault.structuredContent).toEqual({ ...page, title: "Snapshot 7", screenshot }); expect(requests).toBe(7); }), ).pipe(Effect.provide(TestLayer)),); it.effect("rejects non-boolean snapshot image options before selecting a browser host", () => Effect.gen(function* () { const server = yield* McpServer.McpServer; for (const includeImage of ["false", 0, null]) { const result = yield* server .callTool({ name: "preview_snapshot", arguments: { includeImage }, }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(result.isError).toBe(true); expect(result.content).toEqual([{ type: "text", text: "Preview snapshot failed: AiError." }]); expect(result.structuredContent).toEqual({ error: { _tag: "AiError", operation: "snapshot", failureCount: 1 }, }); } }).pipe(Effect.provide(TestLayer)),); it.effect("saves the snapshot PNG on request and reports its path", () => Effect.scoped( Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const inputs = yield* serveSnapshots("mcp-save-client", snapshotResult); const snapshot = yield* callSnapshot({ save: true }); expect(snapshot.isError).toBe(false); // The browser never receives the server-only `save` flag. expect(inputs).toEqual([{}]); const structured = snapshot.structuredContent as { readonly screenshotPath?: string }; const screenshotPath = structured.screenshotPath; expect(typeof screenshotPath).toBe("string"); expect(path.dirname(screenshotPath!)).toBe(config.browserArtifactsDir); expect(path.basename(screenshotPath!)).toMatch( /^browser-screenshot-example-test-[0-9a-z]+-[0-9a-f]{8}\.png$/, ); expect(Buffer.from(yield* fileSystem.readFile(screenshotPath!)).toString()).toBe("png"); const [, text] = snapshot.content; expect(text?.type === "text" ? text.text : "").toContain(screenshotPath); const unsaved = yield* callSnapshot({}); expect(unsaved.structuredContent).not.toHaveProperty("screenshotPath"); }), ).pipe(Effect.provide(TestLayer)),); it.effect("reports a tagged error when the screenshot cannot be saved", () => Effect.scoped( Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; // A regular file where the artifacts directory should be makes every write fail. yield* fileSystem.writeFileString(config.browserArtifactsDir, ""); yield* serveSnapshots("mcp-save-failure-client", snapshotResult); const snapshot = yield* callSnapshot({ save: true }); expect(snapshot.isError).toBe(true); expect(snapshot.content).toEqual([ { type: "text", text: "Preview snapshot failed: PreviewScreenshotSaveError." }, ]); expect(snapshot.structuredContent).toEqual({ error: { _tag: "PreviewScreenshotSaveError", operation: "snapshot", failureCount: 1 }, }); }), ).pipe(Effect.provide(TestLayer)),); it.effect( "registers the pull request toolkit and surfaces a missing capability as a tool error", () => Effect.gen(function* () { const server = yield* McpServer.McpServer; const names = server.tools.map(({ tool }) => tool.name); expect(names).toEqual( expect.arrayContaining([ "link_pull_request", "unlink_pull_request", "list_thread_pull_requests", ]), ); const linkTool = server.tools.find(({ tool }) => tool.name === "link_pull_request"); expect(linkTool?.tool.annotations?.idempotentHint).toBe(true); expect(linkTool?.tool.annotations?.openWorldHint).toBe(false); expect(linkTool?.tool.description).toContain("Register every pull request you open"); const denied = yield* server .callTool({ name: "list_thread_pull_requests", arguments: {} }) .pipe( // A preview-only credential: the token predates the toolkit or was minted elsewhere. Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(denied.isError).toBe(true); expect(denied.content).toEqual([ { type: "text", text: "MCP credential does not grant the pull-requests capability." }, ]); }).pipe(Effect.provide(PullRequestsTestLayer)),); it.effect("keeps the snapshot text under the agent's output ceiling", () => Effect.scoped( Effect.gen(function* () { // Mirrors the real failure: a [role] container whose innerText is the whole // project list, repeated for several elements, plus a big AX tree. const pageText = "/Users/theo/Code/project\nClaude, Codex · 79 threads\n".repeat(600); const element = (name: string, index: number) => ({ tag: "div", role: "presentation", name, selector: `div:nth-of-type(${index})`, x: 0, y: 0, width: 10, height: 10, }); const oversized = { ...snapshotResult, visibleText: pageText, interactiveElements: [ element(pageText, 1), element(pageText, 2), element(pageText, 3), element("Continue", 4), ], accessibilityTree: { nodes: Array.from({ length: 2_000 }, (_, i) => ({ nodeId: `${i}` })) }, consoleEntries: Array.from({ length: 100 }, (_, i) => ({ level: "log", text: `entry ${i}`, timestamp: "t", })), }; yield* serveSnapshots("mcp-bounded-client", oversized); const snapshot = yield* callSnapshot({ includeImage: false }); expect(snapshot.isError).toBe(false); const [identity, text, notice] = snapshot.content; expect(identity?.type === "text" ? decodeJsonText(identity.text) : null).toEqual({ url: oversized.url, }); expect(text?.type).toBe("text"); const body = text?.type === "text" ? text.text : ""; expect(Buffer.byteLength(body, "utf8")).toBeLessThanOrEqual( McpHttpServer.MAX_SNAPSHOT_TEXT_BYTES, ); const parsed = decodeJsonText(body) as { readonly accessibilityTree?: unknown; readonly visibleText: string; readonly interactiveElements: ReadonlyArray<{ readonly name: string }>; readonly consoleEntries: ReadonlyArray<{ readonly text: string }>; }; expect(parsed.accessibilityTree).toBeUndefined(); expect(parsed.visibleText.length).toBeLessThanOrEqual(8_001); expect(parsed.interactiveElements).toHaveLength(4); expect(parsed.interactiveElements[0]?.name.length).toBeLessThanOrEqual(201); expect(parsed.interactiveElements[3]?.name).toBe("Continue"); expect(parsed.consoleEntries).toHaveLength(40); expect(parsed.consoleEntries[0]?.text).toBe("entry 60"); expect(notice?.type === "text" ? notice.text : "").toContain("accessibilityTree"); expect(notice?.type === "text" ? notice.text : "").toContain("60 older console entries"); // The structured result is untouched; only the text the agent reads is bounded. expect(snapshot.structuredContent).toMatchObject({ accessibilityTree: oversized.accessibilityTree, }); }), ).pipe(Effect.provide(TestLayer)),); it.effect("bounds the snapshot text even when nothing but logs and the title are large", () => Effect.scoped( Effect.gen(function* () { const oversized = { ...snapshotResult, title: "t".repeat(70_000), interactiveElements: [], consoleEntries: [{ level: "log", text: "x".repeat(70_000), timestamp: "t" }], }; yield* serveSnapshots("mcp-bounded-logs-client", oversized); const snapshot = yield* callSnapshot({ includeImage: false }); const [, text] = snapshot.content; const body = text?.type === "text" ? text.text : ""; expect(Buffer.byteLength(body, "utf8")).toBeLessThanOrEqual( McpHttpServer.MAX_SNAPSHOT_TEXT_BYTES, ); const parsed = decodeJsonText(body) as { readonly title: string; readonly consoleEntries: ReadonlyArray<{ readonly text: string }>; }; expect(parsed.title.length).toBe(2_049); expect(parsed.consoleEntries[0]?.text.length).toBe(501); const notice = snapshot.content[2]; const noticeText = notice?.type === "text" ? notice.text : ""; expect(noticeText).toContain("url or title after 2048 characters"); expect(noticeText).toContain("console entries text after 500 characters"); }),Lines 1–500 of 768. Load more
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 Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { McpProtocol, McpSchema, McpServer } from "effect/unstable/ai"; import { HttpBody, HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http"; import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts"; import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts"; import * as ServerConfig from "../config.ts"; 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 decodeJsonText = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); const invocation = { environmentId, threadId, providerSessionId: "provider-session-mcp-test", providerInstanceId: ProviderInstanceId.make("codex"), capabilities: new Set(["preview"] as const), issuedAt: 1, }; const client = McpSchema.McpServerClient.of({ clientId: 1, clientCapabilities: {}, clientInfo: { name: "mcp-test", version: "1.0.0" }, protocolVersion: "2025-06-18", initializePayload: { protocolVersion: "2025-06-18", 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), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-http-server-test-" })), Layer.provideMerge(NodeServices.layer), ); const PullRequestsTestLayer = McpHttpServer.PullRequestsToolkitRegistrationLive.pipe( Layer.provideMerge(McpServer.McpServer.layer), Layer.provide( Layer.mergeAll( Layer.mock(ProjectionSnapshotQuery)({ getThreadShellById: () => Effect.succeed(Option.none()), }), Layer.mock(OrchestrationEngineService)({}), NodeServices.layer, ), ), ); const snapshotResult = { 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, }, }; /** Answers every snapshot request on a fresh broker host with the given result. */ const serveSnapshots = (clientId: string, result: unknown) => Effect.gen(function* () { const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; const connected = yield* Deferred.make<void>(); const inputs: Array<unknown> = []; const events = yield* broker.connect({ clientId, environmentId }); yield* Stream.runForEach(events, (event) => { if (event.type === "connected") return Deferred.succeed(connected, undefined); inputs.push(event.request.input); return broker.respond({ clientId, connectionId: event.connectionId, requestId: event.request.requestId, ok: true, result, }); }).pipe(Effect.forkScoped); yield* Deferred.await(connected); return inputs; }); const callSnapshot = (args: Record<string, unknown>) => Effect.gen(function* () { const server = yield* McpServer.McpServer; return yield* server .callTool({ name: "preview_snapshot", arguments: args }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); }); 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.each([{}, { includeImage: false }])( "returns bounded structural preview snapshot failures %#", (input) => 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: input }) .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: PreviewAutomationExecutionError." }, ]); expect(snapshot.structuredContent).toEqual({ error: { _tag: "PreviewAutomationExecutionError", operation: "snapshot", failureCount: 1, }, }); }), ).pipe(Effect.provide(TestLayer)), ); it.effect.each([ { mode: "default", input: {}, images: true }, { mode: "explicit image", input: { includeImage: true }, images: true }, { mode: "text only", input: { includeImage: false }, images: false }, ])("returns fresh $mode snapshots on repeated MCP calls", ({ input, images }) => Effect.scoped( Effect.gen(function* () { const server = yield* McpServer.McpServer; const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker; const connected = yield* Deferred.make<void>(); const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII="; const page = { url: "http://example.test/", loading: false, visibleText: "Save your changes", interactiveElements: [ { tag: "button", role: "button", name: "Save", selector: "#save", x: 0, y: 0, width: 20, height: 10, }, ], accessibilityTree: { role: "document", name: "Example" }, consoleEntries: [], networkEntries: [], actionTimeline: [], }; const screenshot = { mimeType: "image/png", width: 1, height: 1 }; let requests = 0; const events = yield* broker.connect({ clientId: "mcp-image-option-client", environmentId }); yield* Stream.runForEach(events, (event) => { if (event.type === "connected") return Deferred.succeed(connected, undefined); requests += 1; expect(event.request).toMatchObject({ operation: "snapshot", tabId: alternateTabId, threadId, }); expect(event.request.input).toEqual({}); return broker.respond({ clientId: "mcp-image-option-client", connectionId: event.connectionId, requestId: event.request.requestId, ok: true, result: { ...page, title: `Snapshot ${requests}`, screenshot: { ...screenshot, data: png }, }, }); }).pipe(Effect.forkScoped); yield* Deferred.await(connected); for (const call of [1, 2, 3, 4, 5, 6]) { const snapshot = yield* server .callTool({ name: "preview_snapshot", arguments: { ...input, tabId: alternateTabId }, }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); const metadata = { ...page, title: `Snapshot ${call}`, screenshot }; const { accessibilityTree: _tree, ...boundedMetadata } = metadata; expect(snapshot.isError).toBe(false); expect(snapshot.structuredContent).toEqual(metadata); const [identity, text, ...rest] = snapshot.content; expect(identity?.type === "text" ? decodeJsonText(identity.text) : null).toEqual({ url: page.url, }); expect(text?.type === "text" ? decodeJsonText(text.text) : null).toEqual(boundedMetadata); expect(rest).toEqual([ { type: "text", text: "Snapshot text was bounded. Omitted: accessibilityTree (use interactiveElements locators or preview_evaluate).", }, ...(images ? [ { type: "image", mimeType: "image/png", data: new Uint8Array(Buffer.from(png, "base64")), }, ] : []), ]); } // Output selection belongs to this call, not the MCP session's history. const nextDefault = yield* server .callTool({ name: "preview_snapshot", arguments: { tabId: alternateTabId }, }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(nextDefault.content.map((content) => content.type)).toEqual([ "text", "text", "text", "image", ]); expect(nextDefault.structuredContent).toEqual({ ...page, title: "Snapshot 7", screenshot }); expect(requests).toBe(7); }), ).pipe(Effect.provide(TestLayer)), ); it.effect("rejects non-boolean snapshot image options before selecting a browser host", () => Effect.gen(function* () { const server = yield* McpServer.McpServer; for (const includeImage of ["false", 0, null]) { const result = yield* server .callTool({ name: "preview_snapshot", arguments: { includeImage }, }) .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(result.isError).toBe(true); expect(result.content).toEqual([{ type: "text", text: "Preview snapshot failed: AiError." }]); expect(result.structuredContent).toEqual({ error: { _tag: "AiError", operation: "snapshot", failureCount: 1 }, }); } }).pipe(Effect.provide(TestLayer)), ); it.effect("saves the snapshot PNG on request and reports its path", () => Effect.scoped( Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const inputs = yield* serveSnapshots("mcp-save-client", snapshotResult); const snapshot = yield* callSnapshot({ save: true }); expect(snapshot.isError).toBe(false); // The browser never receives the server-only `save` flag. expect(inputs).toEqual([{}]); const structured = snapshot.structuredContent as { readonly screenshotPath?: string }; const screenshotPath = structured.screenshotPath; expect(typeof screenshotPath).toBe("string"); expect(path.dirname(screenshotPath!)).toBe(config.browserArtifactsDir); expect(path.basename(screenshotPath!)).toMatch( /^browser-screenshot-example-test-[0-9a-z]+-[0-9a-f]{8}\.png$/, ); expect(Buffer.from(yield* fileSystem.readFile(screenshotPath!)).toString()).toBe("png"); const [, text] = snapshot.content; expect(text?.type === "text" ? text.text : "").toContain(screenshotPath); const unsaved = yield* callSnapshot({}); expect(unsaved.structuredContent).not.toHaveProperty("screenshotPath"); }), ).pipe(Effect.provide(TestLayer)), ); it.effect("reports a tagged error when the screenshot cannot be saved", () => Effect.scoped( Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; const fileSystem = yield* FileSystem.FileSystem; // A regular file where the artifacts directory should be makes every write fail. yield* fileSystem.writeFileString(config.browserArtifactsDir, ""); yield* serveSnapshots("mcp-save-failure-client", snapshotResult); const snapshot = yield* callSnapshot({ save: true }); expect(snapshot.isError).toBe(true); expect(snapshot.content).toEqual([ { type: "text", text: "Preview snapshot failed: PreviewScreenshotSaveError." }, ]); expect(snapshot.structuredContent).toEqual({ error: { _tag: "PreviewScreenshotSaveError", operation: "snapshot", failureCount: 1 }, }); }), ).pipe(Effect.provide(TestLayer)), ); it.effect( "registers the pull request toolkit and surfaces a missing capability as a tool error", () => Effect.gen(function* () { const server = yield* McpServer.McpServer; const names = server.tools.map(({ tool }) => tool.name); expect(names).toEqual( expect.arrayContaining([ "link_pull_request", "unlink_pull_request", "list_thread_pull_requests", ]), ); const linkTool = server.tools.find(({ tool }) => tool.name === "link_pull_request"); expect(linkTool?.tool.annotations?.idempotentHint).toBe(true); expect(linkTool?.tool.annotations?.openWorldHint).toBe(false); expect(linkTool?.tool.description).toContain("Register every pull request you open"); const denied = yield* server .callTool({ name: "list_thread_pull_requests", arguments: {} }) .pipe( // A preview-only credential: the token predates the toolkit or was minted elsewhere. Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), Effect.provideService(McpSchema.McpServerClient, client), ); expect(denied.isError).toBe(true); expect(denied.content).toEqual([ { type: "text", text: "MCP credential does not grant the pull-requests capability." }, ]); }).pipe(Effect.provide(PullRequestsTestLayer)), ); it.effect("keeps the snapshot text under the agent's output ceiling", () => Effect.scoped( Effect.gen(function* () { // Mirrors the real failure: a [role] container whose innerText is the whole // project list, repeated for several elements, plus a big AX tree. const pageText = "/Users/theo/Code/project\nClaude, Codex · 79 threads\n".repeat(600); const element = (name: string, index: number) => ({ tag: "div", role: "presentation", name, selector: `div:nth-of-type(${index})`, x: 0, y: 0, width: 10, height: 10, }); const oversized = { ...snapshotResult, visibleText: pageText, interactiveElements: [ element(pageText, 1), element(pageText, 2), element(pageText, 3), element("Continue", 4), ], accessibilityTree: { nodes: Array.from({ length: 2_000 }, (_, i) => ({ nodeId: `${i}` })) }, consoleEntries: Array.from({ length: 100 }, (_, i) => ({ level: "log", text: `entry ${i}`, timestamp: "t", })), }; yield* serveSnapshots("mcp-bounded-client", oversized); const snapshot = yield* callSnapshot({ includeImage: false }); expect(snapshot.isError).toBe(false); const [identity, text, notice] = snapshot.content; expect(identity?.type === "text" ? decodeJsonText(identity.text) : null).toEqual({ url: oversized.url, }); expect(text?.type).toBe("text"); const body = text?.type === "text" ? text.text : ""; expect(Buffer.byteLength(body, "utf8")).toBeLessThanOrEqual( McpHttpServer.MAX_SNAPSHOT_TEXT_BYTES, ); const parsed = decodeJsonText(body) as { readonly accessibilityTree?: unknown; readonly visibleText: string; readonly interactiveElements: ReadonlyArray<{ readonly name: string }>; readonly consoleEntries: ReadonlyArray<{ readonly text: string }>; }; expect(parsed.accessibilityTree).toBeUndefined(); expect(parsed.visibleText.length).toBeLessThanOrEqual(8_001); expect(parsed.interactiveElements).toHaveLength(4); expect(parsed.interactiveElements[0]?.name.length).toBeLessThanOrEqual(201); expect(parsed.interactiveElements[3]?.name).toBe("Continue"); expect(parsed.consoleEntries).toHaveLength(40); expect(parsed.consoleEntries[0]?.text).toBe("entry 60"); expect(notice?.type === "text" ? notice.text : "").toContain("accessibilityTree"); expect(notice?.type === "text" ? notice.text : "").toContain("60 older console entries"); // The structured result is untouched; only the text the agent reads is bounded. expect(snapshot.structuredContent).toMatchObject({ accessibilityTree: oversized.accessibilityTree, }); }), ).pipe(Effect.provide(TestLayer)), ); it.effect("bounds the snapshot text even when nothing but logs and the title are large", () => Effect.scoped( Effect.gen(function* () { const oversized = { ...snapshotResult, title: "t".repeat(70_000), interactiveElements: [], consoleEntries: [{ level: "log", text: "x".repeat(70_000), timestamp: "t" }], }; yield* serveSnapshots("mcp-bounded-logs-client", oversized); const snapshot = yield* callSnapshot({ includeImage: false }); const [, text] = snapshot.content; const body = text?.type === "text" ? text.text : ""; expect(Buffer.byteLength(body, "utf8")).toBeLessThanOrEqual( McpHttpServer.MAX_SNAPSHOT_TEXT_BYTES, ); const parsed = decodeJsonText(body) as { readonly title: string; readonly consoleEntries: ReadonlyArray<{ readonly text: string }>; }; expect(parsed.title.length).toBe(2_049); expect(parsed.consoleEntries[0]?.text.length).toBe(501); const notice = snapshot.content[2]; const noticeText = notice?.type === "text" ? notice.text : ""; expect(noticeText).toContain("url or title after 2048 characters"); expect(noticeText).toContain("console entries text after 500 characters"); }),