Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f88dc5f
fix(server): isolate browser request timeouts and revoke expired work
hogeheer499-commits Oct 7, 2026
c8201d7
test(server): verify snapshot compatibility with older hosts
hogeheer499-commits Oct 7, 2026
f07b86a
test(server): preserve browser sessions after navigation timeouts
hogeheer499-commits Oct 8, 2026
1ead03a
Merge upstream browser fixes and preserve request timeout isolation
hogeheer499-commits Oct 8, 2026
2368541
Merge browser ownership fixes and preserve timeout isolation
hogeheer499-commits Oct 9, 2026
e9f5f2d
fix(server): cap navigation to remaining request budget
hogeheer499-commits Oct 9, 2026
f4e1cc6
Merge browser reply grace and preserve execution deadlines
hogeheer499-commits Oct 10, 2026
09a2280
Merge current main before narrowing preview request expiry
hogeheer499-commits Oct 13, 2026
912ca8f
fix(server): drop preview actions whose deadline passed while queued
hogeheer499-commits Oct 13, 2026
29770d8
test(server): keep deadline coverage compatible with page continuity
hogeheer499-commits Oct 13, 2026
119e064
Merge upstream main at 8d7523248
hogeheer499-commits Oct 13, 2026
7530cb4
test(server): name the deadline boundary case after what it asserts
hogeheer499-commits Oct 13, 2026
2c73a63
fix(server): let a recording stop run after its deadline
hogeheer499-commits Oct 14, 2026
76614e5
Merge main to resolve queued preview deadline conflicts
hogeheer499-commits Oct 16, 2026
102deab
fix(server): drain initial navigation after an expired open
hogeheer499-commits Oct 16, 2026
30354ec
chore: merge main for preview deadline takeover
juliusmarminge Oct 19, 2026
8edce26
test(server): focus preview deadline regression coverage
juliusmarminge Oct 19, 2026
e7bc457
fix(server): keep initial viewer navigation responsive
juliusmarminge Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions apps/server/src/mcp/McpHttpServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,17 @@ it.effect("tells the agent how to fall back when no desktop app can run the snap
);

it.effect.each([
{ mode: "default", input: {}, images: false },
{ 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 }) =>
{ mode: "default", input: {}, images: false, legacyHost: false },
{ mode: "default from an older host", input: {}, images: false, legacyHost: true },
{ mode: "explicit image", input: { includeImage: true }, images: true, legacyHost: false },
{ mode: "text only", input: { includeImage: false }, images: false, legacyHost: false },
{
mode: "text only from an older host",
input: { includeImage: false },
images: false,
legacyHost: true,
},
])("returns fresh $mode snapshots on repeated MCP calls", ({ input, images, legacyHost }) =>
Effect.scoped(
Effect.gen(function* () {
const server = yield* McpServer.McpServer;
Expand Down Expand Up @@ -287,7 +294,8 @@ it.effect.each([
tabId: alternateTabId,
threadId,
});
expect(event.request.input).toEqual({});
const capture = requests <= 6 && images;
expect(event.request.input).toEqual(capture ? {} : { includeImage: false });
return broker.respond({
clientId: "mcp-image-option-client",
connectionId: event.connectionId,
Expand All @@ -296,7 +304,8 @@ it.effect.each([
result: {
...page,
title: `Snapshot ${requests}`,
screenshot: { ...screenshot, data: png },
// Older hosts ignore the capture preference and still return a full PNG.
...(capture || legacyHost ? { screenshot: { ...screenshot, data: png } } : {}),
},
});
}).pipe(Effect.forkScoped);
Expand All @@ -312,7 +321,11 @@ it.effect.each([
Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
Effect.provideService(McpSchema.McpServerClient, client),
);
const metadata = { ...page, title: `Snapshot ${call}`, screenshot };
const metadata = {
...page,
title: `Snapshot ${call}`,
...(images || legacyHost ? { screenshot } : {}),
};
const { accessibilityTree: _tree, ...boundedMetadata } = metadata;
expect(snapshot.isError).toBe(false);
expect(snapshot.structuredContent).toEqual({
Expand Down Expand Up @@ -352,7 +365,10 @@ it.effect.each([
Effect.provideService(McpSchema.McpServerClient, client),
);
expect(nextDefault.content.map((content) => content.type)).toEqual(["text", "text", "text"]);
expect(nextDefault.structuredContent).toMatchObject({ title: "Snapshot 7", screenshot });
expect(nextDefault.structuredContent).toMatchObject({ title: "Snapshot 7" });
if (legacyHost)
expect(nextDefault.structuredContent).toHaveProperty("screenshot", screenshot);
else expect(nextDefault.structuredContent).not.toHaveProperty("screenshot");
expect(nextDefault.structuredContent).not.toHaveProperty("accessibilityTree");
expect(requests).toBe(7);
}),
Expand Down Expand Up @@ -439,6 +455,20 @@ it.effect("saves the snapshot PNG on request and reports its path", () =>
const [only, ...others] = pathOnly.content;
expect(others).toEqual([]);
expect(only?.type === "text" ? decodeJsonText(only.text) : null).toEqual(saved);
const explicitPathOnly = yield* callSnapshot({ save: true, includeImage: false });
expect(explicitPathOnly.structuredContent).toEqual({
url: snapshotResult.url,
screenshotPath: expect.any(String),
});
expect(explicitPathOnly.content.map((content) => content.type)).toEqual(["text"]);
const explicitSaved = explicitPathOnly.structuredContent as {
readonly screenshotPath: string;
};
expect(Buffer.from(yield* fileSystem.readFile(explicitSaved.screenshotPath)).toString()).toBe(
"png",
);
// Saves always capture; the unsaved default call is text-only.
expect(inputs).toEqual([{}, { includeImage: false }, {}, {}]);
}),
).pipe(Effect.provide(layerTest)),
);
Expand Down Expand Up @@ -850,6 +880,9 @@ it.effect("registers annotated tools and preserves authenticated request context
expect(snapshotTool?.tool.annotations?.readOnlyHint).toBe(false);
expect(snapshotTool?.tool.annotations?.idempotentHint).toBe(false);
expect(snapshotTool?.tool.annotations?.openWorldHint).toBe(true);
// This formatter advertises no required PNG output schema to MCP clients.
// PreviewAutomationSnapshot validates the host result inside this server.
expect(snapshotTool?.tool.outputSchema).toBeUndefined();

const clickTool = server.tools.find(({ tool }) => tool.name === "preview_click");
expect(clickTool?.tool.annotations?.readOnlyHint).toBe(false);
Expand Down
27 changes: 18 additions & 9 deletions apps/server/src/mcp/McpHttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,17 +492,22 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
Effect.gen(function* () {
const snapshot = encodedResult as SnapshotMetadata & {
readonly url: string;
readonly screenshot: {
readonly screenshot?: {
readonly mimeType: "image/png";
readonly data: string;
readonly width: number;
readonly height: number;
};
};
const { screenshot, ...page } = snapshot;
const png = new Uint8Array(Buffer.from(screenshot.data, "base64"));
const png =
screenshot === undefined
? undefined
: new Uint8Array(Buffer.from(screenshot.data, "base64"));
const screenshotPath =
payload?.save === true ? yield* saveScreenshot(snapshot.url, png) : undefined;
payload?.save === true && png !== undefined
? yield* saveScreenshot(snapshot.url, png)
: undefined;
// Images stay out of tool history unless asked for: providers replay them on every
// later request, and some reject inline images outright.
const includeImage = payload?.includeImage === true;
Expand All @@ -520,11 +525,15 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
}
const metadata = {
...page,
screenshot: {
mimeType: screenshot.mimeType,
width: screenshot.width,
height: screenshot.height,
},
...(screenshot === undefined
? {}
: {
screenshot: {
mimeType: screenshot.mimeType,
width: screenshot.width,
height: screenshot.height,
},
}),
...(screenshotPath === undefined ? {} : { screenshotPath }),
};
const bounded = boundSnapshotMetadata(metadata);
Expand All @@ -551,7 +560,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
text: `Snapshot text was bounded. Omitted: ${bounded.omitted.join("; ")}.`,
},
]),
...(includeImage
...(includeImage && screenshot !== undefined && png !== undefined
? [{ type: "image" as const, data: png, mimeType: screenshot.mimeType }]
: []),
],
Expand Down
Loading
Loading