feat: add preview tool — navigate + screenshot in one call with optional POST

New MCP tool `preview` combines goto + screenshot with viewport control.
Optionally POSTs result to any HTTP endpoint (e.g. HUD/visor) via previewUrl.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 12:44:52 +02:00
parent 7171fbfc76
commit a80224df36
16 changed files with 332 additions and 272 deletions
+59
View File
@@ -21,6 +21,7 @@ import type {
MetricsData,
NetworkEntry,
PageError,
PreviewCommand,
StorageCommand,
} from './types.js';
@@ -878,6 +879,62 @@ export class ClaudeBrowser {
}
}
private async handlePreviewCommand(cmd: PreviewCommand): Promise<CommandResponse> {
const page = this.ensurePage();
// Resize viewport if dimensions specified
if (cmd.width || cmd.height) {
const current = page.viewportSize();
const width = cmd.width || current?.width || 1280;
const height = cmd.height || current?.height || 800;
await page.setViewportSize({ width, height });
}
// Navigate
const nav = await this.goto(cmd.url);
// Screenshot
const outputPath = resolve(cmd.output || '/tmp/preview.png');
await page.screenshot({ path: outputPath, fullPage: cmd.fullPage || false });
// Optional: POST to preview endpoint
let posted = false;
if (cmd.previewUrl) {
posted = await this.postPreview(outputPath, cmd.previewUrl, cmd.title, cmd.caption);
}
return { ok: true, path: outputPath, url: nav.url, title: nav.title, posted };
}
/**
* POST a screenshot to a preview endpoint.
* Payload: { source: "file:///path", title, caption }
* Silent failure — returns false if endpoint is unreachable.
*/
private async postPreview(
imagePath: string,
previewUrl: string,
title?: string,
caption?: string
): Promise<boolean> {
try {
const payload = JSON.stringify({
source: `file://${resolve(imagePath)}`,
title: title || null,
caption: caption || null,
});
const res = await fetch(previewUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
signal: AbortSignal.timeout(3000),
});
return res.ok;
} catch {
return false;
}
}
private async handleImportCommand(cmd: ImportCommand): Promise<CommandResponse> {
const context = this.getContext();
if (!context) throw new Error('Browser not launched');
@@ -1139,6 +1196,8 @@ export class ClaudeBrowser {
}
case 'import':
return this.handleImportCommand(cmd);
case 'preview':
return this.handlePreviewCommand(cmd);
default: {
const _exhaustive: never = cmd;
return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` };
+34
View File
@@ -255,6 +255,40 @@ server.tool(
})
);
// Preview — navigate + screenshot in one call, optional POST to preview endpoint
server.tool(
'preview',
'Navigate to a URL and take a screenshot in one call. Optionally POST the result to a preview endpoint.',
{
url: z.string().describe('URL or file:///path to preview'),
width: z.number().optional().default(1280).describe('Viewport width'),
height: z.number().optional().default(800).describe('Viewport height'),
fullPage: z.boolean().optional().default(false),
output: z.string().optional().default('/tmp/preview.png').describe('Screenshot output path'),
previewUrl: z.string().optional().describe('HTTP endpoint to POST screenshot result to'),
title: z.string().optional().describe('Title sent with preview POST'),
caption: z.string().optional().describe('Caption sent with preview POST'),
},
withLogging(
'preview',
async ({ url, width, height, fullPage, output, previewUrl, title, caption }) => {
await ensureLaunched();
const result = await browser.executeCommand({
cmd: 'preview',
url,
width,
height,
fullPage,
output,
previewUrl,
title,
caption,
});
return textResult(JSON.stringify(result));
}
)
);
// Eval
server.tool(
'eval',
+16 -1
View File
@@ -347,7 +347,20 @@ export type BrowserCommand =
| ScrollCommand
| ViewportCommand
| EmulateCommand
| ImportCommand;
| ImportCommand
| PreviewCommand;
export interface PreviewCommand {
cmd: 'preview';
url: string;
width?: number;
height?: number;
fullPage?: boolean;
output?: string;
previewUrl?: string;
title?: string;
caption?: string;
}
// Response types
export interface SuccessResponse {
@@ -359,6 +372,8 @@ export interface SuccessResponse {
count?: number;
elements?: ElementInfo[];
result?: unknown;
// Preview fields
posted?: boolean;
// Image processing fields
files?: string[];
outputDir?: string;