From 63b662ca8dd73e8a681af6b17fd96c3216bbddec Mon Sep 17 00:00:00 2001 From: Adin Schmidt <9936213+adinschmidt@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:21:14 -0400 Subject: [PATCH] feat(server): let agents save browser screenshots Agents could save a recording of a collaborative browser tab but had no way to save a still image. preview_snapshot returns a PNG inline for the agent to look at, and the desktop camera button writes a file, but that path was never exposed over MCP. Add a preview_screenshot tool that reuses the existing desktop capture and returns the saved artifact's path, so an agent can hand the user a still the same way it hands over a recording. --- apps/server/src/mcp/toolkits/preview/handlers.ts | 3 +++ apps/server/src/mcp/toolkits/preview/tools.ts | 14 ++++++++++++++ .../components/preview/PreviewAutomationHosts.tsx | 7 +++++++ packages/contracts/src/previewAutomation.ts | 11 +++++++++++ 4 files changed, 35 insertions(+) diff --git a/apps/server/src/mcp/toolkits/preview/handlers.ts b/apps/server/src/mcp/toolkits/preview/handlers.ts index 1c7ff6f9cd9..ac1ad9cd591 100644 --- a/apps/server/src/mcp/toolkits/preview/handlers.ts +++ b/apps/server/src/mcp/toolkits/preview/handlers.ts @@ -5,6 +5,7 @@ import type { PreviewAutomationRecordingArtifact, PreviewAutomationRecordingStatus, PreviewAutomationResizeResult, + PreviewAutomationScreenshotArtifact, PreviewAutomationSetColorSchemeResult, PreviewAutomationSnapshot, PreviewAutomationStatus, @@ -85,6 +86,8 @@ const handlers = { invokeTargeted("recordingStart", input ?? {}), preview_recording_stop: (input) => invokeTargeted("recordingStop", input ?? {}), + preview_screenshot: (input) => + invokeTargeted("screenshot", input ?? {}), } satisfies Parameters[0]; const { preview_snapshot, ...standardHandlers } = handlers; diff --git a/apps/server/src/mcp/toolkits/preview/tools.ts b/apps/server/src/mcp/toolkits/preview/tools.ts index a94d2b056f7..2cfe8f379d4 100644 --- a/apps/server/src/mcp/toolkits/preview/tools.ts +++ b/apps/server/src/mcp/toolkits/preview/tools.ts @@ -9,6 +9,7 @@ import { PreviewAutomationRecordingStatus, PreviewAutomationResizeInput, PreviewAutomationResizeResult, + PreviewAutomationScreenshotArtifact, PreviewAutomationScrollInput, PreviewAutomationSetColorSchemeInput, PreviewAutomationSetColorSchemeResult, @@ -200,6 +201,17 @@ export const PreviewRecordingStopTool = safeBrowserTool( }).annotate(Tool.Title, "Stop browser recording"), ); +export const PreviewScreenshotTool = safeBrowserTool( + Tool.make("preview_screenshot", { + description: + "Save a full-resolution PNG of the collaborative browser tab selected by tabId, or this agent session's current tab when omitted, as a local evidence artifact and return its path. Use this to hand the user a still image; use preview_snapshot instead to look at the page yourself.", + parameters: PreviewAutomationTabTargetInput, + success: PreviewAutomationScreenshotArtifact, + failure: PreviewAutomationError, + dependencies, + }).annotate(Tool.Title, "Save browser screenshot"), +); + export const PreviewToolkit = Toolkit.make( PreviewStatusTool, PreviewOpenTool, @@ -215,6 +227,7 @@ export const PreviewToolkit = Toolkit.make( PreviewWaitForTool, PreviewRecordingStartTool, PreviewRecordingStopTool, + PreviewScreenshotTool, ); export const PreviewStandardToolkit = Toolkit.make( @@ -231,6 +244,7 @@ export const PreviewStandardToolkit = Toolkit.make( PreviewWaitForTool, PreviewRecordingStartTool, PreviewRecordingStopTool, + PreviewScreenshotTool, ); export const PreviewSnapshotToolkit = Toolkit.make(PreviewSnapshotTool); diff --git a/apps/web/src/components/preview/PreviewAutomationHosts.tsx b/apps/web/src/components/preview/PreviewAutomationHosts.tsx index 2a007fb4ce5..33c62171b9d 100644 --- a/apps/web/src/components/preview/PreviewAutomationHosts.tsx +++ b/apps/web/src/components/preview/PreviewAutomationHosts.tsx @@ -661,6 +661,13 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId }) } return { ...artifact, tabId: stopTabId }; } + case "screenshot": { + const ready = await requireReadyTab(); + const artifact = await ready.bridge.captureScreenshot(ready.runtimeTabId); + // The desktop artifact carries the runtime tab id; callers address + // tabs by their server id. + return { ...artifact, tabId: ready.tabId }; + } } } catch (cause) { throw PreviewAutomationOperationError.fromCause({ diff --git a/packages/contracts/src/previewAutomation.ts b/packages/contracts/src/previewAutomation.ts index e33615fa4c0..9c97757b142 100644 --- a/packages/contracts/src/previewAutomation.ts +++ b/packages/contracts/src/previewAutomation.ts @@ -43,6 +43,7 @@ export const PREVIEW_AUTOMATION_OPERATIONS = [ ...PREVIEW_AUTOMATION_V1_OPERATIONS, "resize", "setColorScheme", + "screenshot", ] as const; export const PreviewAutomationOperation = Schema.Literals(PREVIEW_AUTOMATION_OPERATIONS); @@ -563,6 +564,16 @@ export const PreviewAutomationRecordingArtifact = Schema.Struct({ }); export type PreviewAutomationRecordingArtifact = typeof PreviewAutomationRecordingArtifact.Type; +export const PreviewAutomationScreenshotArtifact = Schema.Struct({ + id: Schema.String, + tabId: PreviewTabId, + path: Schema.String, + mimeType: Schema.Literal("image/png"), + sizeBytes: Schema.Int, + createdAt: Schema.String, +}); +export type PreviewAutomationScreenshotArtifact = typeof PreviewAutomationScreenshotArtifact.Type; + export const PreviewAutomationClientId = TrimmedNonEmptyString.check(Schema.isMaxLength(128)); export type PreviewAutomationClientId = typeof PreviewAutomationClientId.Type; export const PreviewAutomationConnectionId = TrimmedNonEmptyString.check(Schema.isMaxLength(64));