@browserless.io/mcp 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -22,6 +22,7 @@ Just trigger the download in the agent — navigate to the file URL, or click a
|
|
|
22
22
|
- A short, size-scaled grace wait lets quick downloads land on the **same** call. A slower one shows up as **in-progress with a byte count** ("downloading 2.0MB / 10MB") — just keep using the browser; it'll appear completed on a later response. As long as you keep touching the browser, the download state stays fresh.
|
|
23
23
|
- Files **larger than the cap** aren't transferred: you get a `FileTooLarge` note with the **source URL** — fetch it directly (e.g. `curl`) if you have network access.
|
|
24
24
|
- You decide whether to save each file. (`getDownloads` still exists for an explicit poll, but it's rarely needed.)
|
|
25
|
+
- A **screenshot** can be captured straight to disk with `screenshot { toDisk: true }` instead of returned inline — it then behaves exactly like a download here (same handle/path/URL, same reuse). See the **screenshots** skill.
|
|
25
26
|
|
|
26
27
|
**Local (stdio) mode:** the file is already on the local disk (`BROWSERLESS_DOWNLOAD_DIR`, default a temp dir). The response lists the saved **path** — use/move it, or hand it straight back to `uploadFile { path }`. Nothing more to fetch.
|
|
27
28
|
|
|
@@ -30,6 +30,24 @@ Capture smallest region that answers the question.
|
|
|
30
30
|
- **WebP** — better compression than JPEG
|
|
31
31
|
- **`omitBackground: true`** — for transparent elements
|
|
32
32
|
|
|
33
|
+
## Save to disk instead of seeing it
|
|
34
|
+
|
|
35
|
+
By default a screenshot comes back as an inline image you see right away — that
|
|
36
|
+
costs vision tokens and lives in context. If you only need the file _later_
|
|
37
|
+
(hand it to the user, or re-upload it elsewhere) and don't need to look at it
|
|
38
|
+
now, add **`toDisk: true`**:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "method": "screenshot", "params": { "selector": "#invoice", "toDisk": true } }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You will **not** see the image. The response gives a reusable handle — a local
|
|
45
|
+
path (stdio) or a single-use GET URL (HTTP) — exactly like a download. Reuse it
|
|
46
|
+
with `uploadFile`, or hand the path/URL to the user. See the **file-transfers**
|
|
47
|
+
skill for the handle/path/URL rules and TTL. Note: to actually _look_ at a
|
|
48
|
+
disk-saved shot you'd have to load it back into context — so only use `toDisk`
|
|
49
|
+
when you don't need to view it.
|
|
50
|
+
|
|
33
51
|
## Pattern: capture-after-action
|
|
34
52
|
|
|
35
53
|
```json
|
|
@@ -35,4 +35,7 @@ type FormatOpts = {
|
|
|
35
35
|
token?: string;
|
|
36
36
|
};
|
|
37
37
|
export declare const formatDownloads: (downloads: DownloadEntry[], prefix: string, skills: string, opts: FormatOpts) => Promise<Content[]>;
|
|
38
|
+
export declare const formatScreenshotToDisk: (result: unknown, cmd: {
|
|
39
|
+
params?: Record<string, unknown>;
|
|
40
|
+
}, caption: string, skills: string, opts: FormatOpts) => Promise<Content[] | null>;
|
|
38
41
|
export declare function registerAgentTools(server: FastMCP, config: McpConfig, analytics?: AnalyticsHelper): void;
|
package/build/src/tools/agent.js
CHANGED
|
@@ -25,19 +25,29 @@ const SCREENSHOT_MIME = {
|
|
|
25
25
|
webp: 'image/webp',
|
|
26
26
|
png: 'image/png',
|
|
27
27
|
};
|
|
28
|
+
const getScreenshotPayload = (result, cmd) => {
|
|
29
|
+
const base64 = typeof result?.base64 === 'string'
|
|
30
|
+
? result.base64
|
|
31
|
+
: '';
|
|
32
|
+
if (!base64)
|
|
33
|
+
return null;
|
|
34
|
+
const requestedType = typeof cmd.params?.type === 'string' ? cmd.params.type : 'png';
|
|
35
|
+
return {
|
|
36
|
+
base64,
|
|
37
|
+
mimeType: SCREENSHOT_MIME[requestedType] ?? 'image/png',
|
|
38
|
+
requestedType,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
28
41
|
/**
|
|
29
42
|
* Build the MCP response for a screenshot command, or null when there's no
|
|
30
43
|
* base64 payload (caller falls back to JSON text). Returns the image as a
|
|
31
44
|
* vision content block (~1.5K tokens) vs. ~67K inlining the base64 as text.
|
|
32
45
|
*/
|
|
33
46
|
export const formatScreenshotContent = (result, cmd, caption, skills) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
: '';
|
|
37
|
-
if (!base64)
|
|
47
|
+
const payload = getScreenshotPayload(result, cmd);
|
|
48
|
+
if (!payload)
|
|
38
49
|
return null;
|
|
39
|
-
const
|
|
40
|
-
const mimeType = SCREENSHOT_MIME[requestedType] ?? 'image/png';
|
|
50
|
+
const { base64, mimeType } = payload;
|
|
41
51
|
const decodedBytes = Math.floor(base64.length * 0.75);
|
|
42
52
|
const sizeLabel = decodedBytes >= 1_048_576
|
|
43
53
|
? `${(decodedBytes / 1_048_576).toFixed(1)} MB`
|
|
@@ -177,6 +187,25 @@ export const formatDownloads = async (downloads, prefix, skills, opts) => {
|
|
|
177
187
|
content.push({ type: 'text', text: skills });
|
|
178
188
|
return content;
|
|
179
189
|
};
|
|
190
|
+
// persist the bytes we already got back to the download store and surface a reusable handle
|
|
191
|
+
export const formatScreenshotToDisk = async (result, cmd, caption, skills, opts) => {
|
|
192
|
+
const payload = getScreenshotPayload(result, cmd);
|
|
193
|
+
if (!payload)
|
|
194
|
+
return null;
|
|
195
|
+
const { base64, mimeType, requestedType } = payload;
|
|
196
|
+
const ext = requestedType === 'jpeg' ? 'jpg' : requestedType;
|
|
197
|
+
const record = await storeDownload(`screenshot.${ext}`, mimeType, Buffer.from(base64, 'base64'), opts.sessionId);
|
|
198
|
+
const text = [
|
|
199
|
+
caption.trimEnd(),
|
|
200
|
+
`Screenshot saved to disk (not shown inline):\n- ${describeReadyDownload(record, opts)}`,
|
|
201
|
+
]
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.join('\n\n');
|
|
204
|
+
const content = [{ type: 'text', text }];
|
|
205
|
+
if (skills)
|
|
206
|
+
content.push({ type: 'text', text: skills });
|
|
207
|
+
return content;
|
|
208
|
+
};
|
|
180
209
|
const SkillIdSchema = z.enum(skillsRegistry.map((s) => s.id));
|
|
181
210
|
const SkillToolParamsSchema = z.object({
|
|
182
211
|
id: SkillIdSchema.describe('The skill to load (see tool description for the full list).'),
|
|
@@ -298,9 +327,18 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
298
327
|
}
|
|
299
328
|
log.info(`agent: ${cmd.method} ${JSON.stringify(cmd.params)}`);
|
|
300
329
|
agentSession.skillState.cmdIndex += 1;
|
|
330
|
+
// `toDisk` is a local directive (route the screenshot to the download
|
|
331
|
+
// store); it's not a CDP screenshot param, so strip it before sending
|
|
332
|
+
// or Chrome rejects the unknown key. The original cmd keeps it so the
|
|
333
|
+
// result formatter can tell it should save instead of inline.
|
|
334
|
+
let outboundParams = cmd.params;
|
|
335
|
+
if (cmd.method === 'screenshot' && 'toDisk' in cmd.params) {
|
|
336
|
+
outboundParams = { ...cmd.params };
|
|
337
|
+
delete outboundParams.toDisk;
|
|
338
|
+
}
|
|
301
339
|
let resp;
|
|
302
340
|
try {
|
|
303
|
-
resp = await send(agentSession, cmd.method,
|
|
341
|
+
resp = await send(agentSession, cmd.method, outboundParams);
|
|
304
342
|
}
|
|
305
343
|
catch (sendErr) {
|
|
306
344
|
destroySession(mcpSessionId, token, proxy, profile, createProfile, attachSessionId);
|
|
@@ -372,7 +410,7 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
372
410
|
const reportable = closedDuringBatch ? results.slice(0, -1) : results;
|
|
373
411
|
const last = reportable[reportable.length - 1];
|
|
374
412
|
const lastResult = last.result;
|
|
375
|
-
const lastCmd = commands[
|
|
413
|
+
const lastCmd = commands[reportable.length - 1];
|
|
376
414
|
const closedSuffix = closedDuringBatch
|
|
377
415
|
? '\n\nBrowser session closed.'
|
|
378
416
|
: '';
|
|
@@ -437,6 +475,22 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
437
475
|
token,
|
|
438
476
|
});
|
|
439
477
|
}
|
|
478
|
+
else if (last.method === 'screenshot' &&
|
|
479
|
+
lastCmd.params?.toDisk === true) {
|
|
480
|
+
// Screenshot saved to disk → reusable handle, no inline image.
|
|
481
|
+
const saved = await formatScreenshotToDisk(lastResult, lastCmd, batchPrefix, skillsText, {
|
|
482
|
+
transport: config.transport,
|
|
483
|
+
sessionId: mcpSessionId,
|
|
484
|
+
mcpBaseUrl: config.mcpBaseUrl,
|
|
485
|
+
token,
|
|
486
|
+
});
|
|
487
|
+
baseContent = saved ?? [
|
|
488
|
+
{
|
|
489
|
+
type: 'text',
|
|
490
|
+
text: appendSkills(batchPrefix + JSON.stringify(lastResult, null, 2), triggered),
|
|
491
|
+
},
|
|
492
|
+
];
|
|
493
|
+
}
|
|
440
494
|
else {
|
|
441
495
|
// Screenshot → image content block; otherwise JSON text.
|
|
442
496
|
const shot = last.method === 'screenshot'
|
|
@@ -219,6 +219,7 @@ export declare const AgentCommandSchema: z.ZodUnion<readonly [z.ZodDiscriminated
|
|
|
219
219
|
}, z.core.$strip>>;
|
|
220
220
|
waitForImages: z.ZodOptional<z.ZodBoolean>;
|
|
221
221
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
222
|
+
toDisk: z.ZodOptional<z.ZodBoolean>;
|
|
222
223
|
}, z.core.$strip>>>;
|
|
223
224
|
}, z.core.$strip>, z.ZodObject<{
|
|
224
225
|
method: z.ZodLiteral<"uploadFile">;
|
|
@@ -481,6 +482,7 @@ export declare const AgentParamsSchema: z.ZodObject<{
|
|
|
481
482
|
}, z.core.$strip>>;
|
|
482
483
|
waitForImages: z.ZodOptional<z.ZodBoolean>;
|
|
483
484
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
485
|
+
toDisk: z.ZodOptional<z.ZodBoolean>;
|
|
484
486
|
}, z.core.$strip>>>;
|
|
485
487
|
}, z.core.$strip>, z.ZodObject<{
|
|
486
488
|
method: z.ZodLiteral<"uploadFile">;
|
|
@@ -353,6 +353,14 @@ const ScreenshotCommandSchema = z.object({
|
|
|
353
353
|
.number()
|
|
354
354
|
.optional()
|
|
355
355
|
.describe('Timeout in milliseconds (default 30000)'),
|
|
356
|
+
toDisk: z
|
|
357
|
+
.boolean()
|
|
358
|
+
.optional()
|
|
359
|
+
.describe('Save the screenshot to disk instead of returning it inline. ' +
|
|
360
|
+
'You will NOT see the image; the response gives a reusable handle ' +
|
|
361
|
+
'(local path in stdio, single-use GET URL over HTTP) exactly like a ' +
|
|
362
|
+
'download — reuse it with uploadFile or hand it to the user. Use when ' +
|
|
363
|
+
'you only need the file later, not to look at now (see file-transfers).'),
|
|
356
364
|
})
|
|
357
365
|
.optional()
|
|
358
366
|
.default({})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserless.io/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for the Browserless.io browser automation platform",
|
|
5
5
|
"author": "browserless.io",
|
|
6
6
|
"license": "SSPL-1.0",
|
|
@@ -100,7 +100,8 @@
|
|
|
100
100
|
"typescript-eslint": "^8.60.0"
|
|
101
101
|
},
|
|
102
102
|
"engines": {
|
|
103
|
-
"node": ">=24"
|
|
103
|
+
"node": ">=24",
|
|
104
|
+
"npm": ">=11.10.0"
|
|
104
105
|
},
|
|
105
106
|
"overrides": {
|
|
106
107
|
"mocha": {
|