@browserless.io/mcp 1.6.2 → 1.7.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.
- package/README.md +13 -12
- package/build/src/@types/types.d.ts +27 -3
- package/build/src/index.js +21 -29
- package/build/src/lib/agent-client.d.ts +5 -4
- package/build/src/lib/agent-client.js +87 -16
- package/build/src/lib/agent-format.d.ts +1 -1
- package/build/src/lib/agent-format.js +22 -4
- package/build/src/lib/define-tool.d.ts +5 -0
- package/build/src/lib/define-tool.js +1 -0
- package/build/src/lib/download-store.d.ts +17 -0
- package/build/src/lib/download-store.js +84 -0
- package/build/src/lib/http-auth.d.ts +22 -0
- package/build/src/lib/http-auth.js +33 -0
- package/build/src/resources/download-route.d.ts +16 -0
- package/build/src/resources/download-route.js +53 -0
- package/build/src/resources/upload-route.d.ts +3 -0
- package/build/src/resources/upload-route.js +53 -0
- package/build/src/skills/auth-profile.md +66 -0
- package/build/src/skills/autonomous-login.md +44 -43
- package/build/src/skills/file-transfers.md +88 -0
- package/build/src/skills/index.js +19 -0
- package/build/src/skills/shadow-dom.md +10 -1
- package/build/src/skills/system-prompt.d.ts +3 -2
- package/build/src/skills/system-prompt.js +32 -2
- package/build/src/tools/agent.d.ts +23 -0
- package/build/src/tools/agent.js +212 -30
- package/build/src/tools/schemas.d.ts +79 -0
- package/build/src/tools/schemas.js +126 -3
- package/package.json +5 -3
- package/build/src/tools/download.d.ts +0 -11
- package/build/src/tools/download.js +0 -92
|
@@ -50,7 +50,10 @@ Load manually via **browserless_skill** if suspected but not injected:
|
|
|
50
50
|
## Selectors
|
|
51
51
|
- Use **ref=** (CSS) or **deep-ref=** (starts \`< \`) exactly as shown in snapshot
|
|
52
52
|
- Example: \`[3] button "Sign In" ref=button#submit\` → \`"button#submit"\`
|
|
53
|
-
- deep-ref for shadow DOM — see \`shadow-dom\` skill
|
|
53
|
+
- deep-ref for shadow DOM / iframes — see \`shadow-dom\` skill
|
|
54
|
+
|
|
55
|
+
## Iframes
|
|
56
|
+
Snapshots include a \`Frames\` list (cross-origin iframes) when present. Elements inside a frame are tagged \`[frame#N]\` and carry a \`deep-ref=< *url* css\` selector that already pierces the frame — pass it as-is to \`click\`/\`type\`/\`hover\`/\`checkbox\`. No frame switching needed. captcha/payment widgets (reCAPTCHA, hCaptcha, Stripe, Turnstile) show up here. \`shadow-dom\` skill auto-loads when frames present.
|
|
54
57
|
|
|
55
58
|
## Tabs
|
|
56
59
|
Snapshots include \`tabs\` + \`activeTargetId\` — no getTabs needed. Multi-tab / \`snapshot { targetId }\` in \`tabs\` skill (auto-loads when >1 tab).
|
|
@@ -66,6 +69,17 @@ Only click when href is \`javascript:\` / \`#\` / missing.
|
|
|
66
69
|
3. **evaluate** { content } — JS (IIFE): \`(() => { return ... })()\`
|
|
67
70
|
4. **html** { selector } — raw HTML
|
|
68
71
|
|
|
72
|
+
## Files (upload / download)
|
|
73
|
+
**To download a file, DRIVE THE BROWSER — do not \`curl\`/\`wget\`/\`fetch\` the file yourself as a first move.** Many real downloads (login/cookie-gated, generated server-side on demand, or triggered by a click whose response headers force the download) have NO fetchable URL — a direct fetch silently gets the wrong bytes, an HTML error page, or 403. Click/goto in the agent and collect from the auto-surfaced ledger. The ONLY time a direct fetch is correct: the ledger hands you a URL to use — the single-use \`/download/<id>\` URL, or an over-cap \`sourceUrl\`. Reaching for \`curl\` first is a bug, not a shortcut.
|
|
74
|
+
**NEVER read a file's bytes or base64 into this conversation, and NEVER split/reassemble/inline base64 by hand.** That is the wrong tool and will stall.
|
|
75
|
+
- **Upload a local file (stdio)**: \`uploadFile { selector, files: [{ path }] }\` — the server reads + encodes it.
|
|
76
|
+
- **Upload a local file (HTTP)**: the server can't read your disk. Stage it once over HTTP, then use the handle:
|
|
77
|
+
\`curl -s -F file=@"/path/to/file" "<MCP_BASE_URL>/upload?token=<TOKEN>"\` → returns \`{ "handle": "browserless-download://…" }\` → \`uploadFile { files: [{ handle }] }\`. (The path-rejection error gives you the exact command with your token + URL filled in.)
|
|
78
|
+
- **Re-upload something from \`getDownloads\`**: pass its \`handle\` (works in both modes).
|
|
79
|
+
- **Download**: just trigger it in the agent (click a download link, or goto the file URL). The captured file **auto-surfaces** as a notification on the agent response (filename/size/handle), never the bytes — the server waits for it to finish (bounded by size), so it usually lands on that same call. stdio: file already saved, you get its path. HTTP: a **single-use** \`curl … /download/<id>?token=\` URL — fetch only if you need it. Files over the cap aren't transferred — you get the source URL to fetch directly. Path/handle reuses in \`uploadFile\`. (No separate download tool — use the agent.)
|
|
80
|
+
- base64 \`content\` is a LAST RESORT — tiny inline data only.
|
|
81
|
+
- Full recipe: \`file-transfers\` skill.
|
|
82
|
+
|
|
69
83
|
## Batching — Maximize Per Call
|
|
70
84
|
Plan ALL actions from snapshot before next snapshot.
|
|
71
85
|
|
|
@@ -112,6 +126,21 @@ Never retry same failed action without re-snapshot.
|
|
|
112
126
|
- See schema for: screenshot, solve, back, forward, reload, click, type, select, checkbox, hover, scroll, text, html, waitForNavigation, waitForTimeout, waitForRequest, liveURL, getTabs, switchTab, closeTab
|
|
113
127
|
|
|
114
128
|
`;
|
|
129
|
+
// Transport-specific file-transfer guidance, appended to the agent tool
|
|
130
|
+
// description so the model knows its mode UP FRONT — instead of guessing (and
|
|
131
|
+
// base64-ing files it should pass by path). The server knows the transport; the
|
|
132
|
+
// model can't introspect it.
|
|
133
|
+
export const fileTransferModeNote = (transport, mcpBaseUrl) => transport === 'stdio'
|
|
134
|
+
? `\n\n## Runtime: LOCAL (stdio)\n` +
|
|
135
|
+
`Before any file transfer, know your mode: this server runs over **stdio**, on the same machine as your files. ` +
|
|
136
|
+
`To UPLOAD a local file, pass its **\`path\`** straight to \`uploadFile\` (\`files: [{ path: "/abs/file" }]\`) — the server reads it. ` +
|
|
137
|
+
`**Do NOT base64 the file or read its bytes into the conversation.** ` +
|
|
138
|
+
`DOWNLOADS are saved to local disk; the agent response gives you the path.`
|
|
139
|
+
: `\n\n## Runtime: REMOTE (HTTP)\n` +
|
|
140
|
+
`Before any file transfer, know your mode: this server runs over **HTTP** and **cannot read your filesystem**. ` +
|
|
141
|
+
`To UPLOAD a local file, stage it once over HTTP, then use the handle:\n` +
|
|
142
|
+
` \`curl -s -F file=@"/abs/file" "${mcpBaseUrl}/upload?token=<YOUR_TOKEN>"\` -> { "handle": "browserless-download://..." } -> \`uploadFile { files: [{ handle }] }\`.\n` +
|
|
143
|
+
`**Never base64 a file through the conversation.** DOWNLOADS come back with a single-use \`${mcpBaseUrl}/download/<id>\` URL.`;
|
|
115
144
|
export const SKILL_TOOL_DESCRIPTION = `Load a Browserless agent skill on demand.
|
|
116
145
|
|
|
117
146
|
Use this when you suspect the page exhibits a non-trivial mechanic but no SKILL block was auto-injected into a previous response. The auto-injection heuristics are conservative; calling this tool is the explicit fallback.
|
|
@@ -125,4 +154,5 @@ Available skills:
|
|
|
125
154
|
- **screenshots** — when to screenshot vs. snapshot, scope and format choices
|
|
126
155
|
- **tabs** — multi-tab workflows, peek-without-switching
|
|
127
156
|
- **autonomous-login** — load before authenticating: when the user asked you to log in, when a wall blocks the task, or as soon as a password input appears. Covers the don't-login-by-default posture, contextual credential matching, MFA/captcha branches, and the required final JSON response shape.
|
|
128
|
-
- **captchas** — the \`solve\` command, response semantics, escalation path (Cloud-only)
|
|
157
|
+
- **captchas** — the \`solve\` command, response semantics, escalation path (Cloud-only)
|
|
158
|
+
- **file-transfers** — \`uploadFile\` / \`getDownloads\`, stdio-path vs. base64 content, size caps`;
|
|
@@ -12,4 +12,27 @@ export { buildCrossOriginNotice, formatConnectError, formatErrorMessage, formatS
|
|
|
12
12
|
export declare const formatScreenshotContent: (result: unknown, cmd: {
|
|
13
13
|
params?: Record<string, unknown>;
|
|
14
14
|
}, caption: string, skills: string) => Content[] | null;
|
|
15
|
+
type DownloadEntry = {
|
|
16
|
+
filename?: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
data?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
maxBytes?: number;
|
|
22
|
+
sourceUrl?: string;
|
|
23
|
+
inProgress?: boolean;
|
|
24
|
+
receivedBytes?: number;
|
|
25
|
+
totalBytes?: number;
|
|
26
|
+
};
|
|
27
|
+
export declare const normalizeUploadCommand: (cmd: {
|
|
28
|
+
method: string;
|
|
29
|
+
params: Record<string, unknown>;
|
|
30
|
+
}, transport: McpConfig["transport"], mcpBaseUrl?: string) => Promise<void>;
|
|
31
|
+
type FormatOpts = {
|
|
32
|
+
transport: McpConfig['transport'];
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
mcpBaseUrl?: string;
|
|
35
|
+
token?: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const formatDownloads: (downloads: DownloadEntry[], prefix: string, skills: string, opts: FormatOpts) => Promise<Content[]>;
|
|
15
38
|
export declare function registerAgentTools(server: FastMCP, config: McpConfig, analytics?: AnalyticsHelper): void;
|
package/build/src/tools/agent.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { UserError } from 'fastmcp';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename } from 'node:path';
|
|
2
4
|
import { z } from 'zod';
|
|
5
|
+
import { downloadUri, getDownload, storeDownload, FILE_TRANSFER_MAX_BYTES, } from '../lib/download-store.js';
|
|
3
6
|
import { getOrCreateSession, send, closeSession, destroySession, isRetryableUpgradeError, } from '../lib/agent-client.js';
|
|
4
7
|
import { classifyAgentError } from '../lib/error-classifier.js';
|
|
5
8
|
import { defineTool } from '../lib/define-tool.js';
|
|
6
9
|
import { detectSkills, markFired, renderSkill, renderSkills, skillsRegistry, } from '../skills/index.js';
|
|
7
10
|
import { AgentParamsSchema } from './schemas.js';
|
|
8
|
-
import { AGENT_SYSTEM_PROMPT, SKILL_TOOL_DESCRIPTION, } from '../skills/system-prompt.js';
|
|
11
|
+
import { AGENT_SYSTEM_PROMPT, SKILL_TOOL_DESCRIPTION, fileTransferModeNote, } from '../skills/system-prompt.js';
|
|
9
12
|
import { buildCrossOriginNotice, formatConnectError, formatErrorMessage, formatSnapshot, } from '../lib/agent-format.js';
|
|
10
13
|
// export schemas, system prompt, and formatters
|
|
11
14
|
export { AgentParamsSchema } from './schemas.js';
|
|
@@ -53,10 +56,127 @@ export const formatScreenshotContent = (result, cmd, caption, skills) => {
|
|
|
53
56
|
content.push({ type: 'text', text: skills });
|
|
54
57
|
return content;
|
|
55
58
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const fmtBytes = (n) => typeof n !== 'number'
|
|
60
|
+
? '?'
|
|
61
|
+
: n >= 1_048_576
|
|
62
|
+
? `${(n / 1_048_576).toFixed(1)}MB`
|
|
63
|
+
: `${Math.round(n / 1024)}KB`;
|
|
64
|
+
// Still-downloading entry: report progress so the caller knows to touch the
|
|
65
|
+
// browser again to collect it (no bytes, nothing to save yet).
|
|
66
|
+
const describeInProgressDownload = (d) => {
|
|
67
|
+
const got = fmtBytes(d.receivedBytes);
|
|
68
|
+
const total = d.totalBytes && d.totalBytes > 0 ? ` / ${fmtBytes(d.totalBytes)}` : '';
|
|
69
|
+
return `${d.filename ?? 'file'} — downloading (${got}${total}); touch the browser again to collect it`;
|
|
70
|
+
};
|
|
71
|
+
// Resolve each uploadFile entry to base64 `content` (from `content`, a prior
|
|
72
|
+
// `handle`, or a local `path` in stdio) so the model never emits base64 itself.
|
|
73
|
+
export const normalizeUploadCommand = async (cmd, transport, mcpBaseUrl) => {
|
|
74
|
+
if (cmd.method !== 'uploadFile')
|
|
75
|
+
return;
|
|
76
|
+
const files = cmd.params.files;
|
|
77
|
+
if (!Array.isArray(files))
|
|
78
|
+
return;
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
if (!file || typeof file !== 'object')
|
|
81
|
+
continue;
|
|
82
|
+
const f = file;
|
|
83
|
+
if (typeof f.content === 'string' && f.content)
|
|
84
|
+
continue;
|
|
85
|
+
let buf;
|
|
86
|
+
let defaultName;
|
|
87
|
+
if (typeof f.handle === 'string' && f.handle) {
|
|
88
|
+
const record = getDownload(f.handle);
|
|
89
|
+
if (!record) {
|
|
90
|
+
throw new UserError(`Unknown upload handle "${f.handle}". Pass a handle returned by ` +
|
|
91
|
+
`getDownloads, or supply base64 "content".`);
|
|
92
|
+
}
|
|
93
|
+
buf = await readFile(record.path);
|
|
94
|
+
defaultName = record.filename;
|
|
95
|
+
delete f.handle;
|
|
96
|
+
}
|
|
97
|
+
else if (typeof f.path === 'string' && f.path) {
|
|
98
|
+
if (transport !== 'stdio') {
|
|
99
|
+
const base = mcpBaseUrl ?? '<MCP_BASE_URL>';
|
|
100
|
+
const tokenQ = '?token=<YOUR_BROWSERLESS_TOKEN>';
|
|
101
|
+
throw new UserError('uploadFile "path" is not available in HTTP mode (the server can\'t ' +
|
|
102
|
+
'read your filesystem). Stage the file once over HTTP, then pass the ' +
|
|
103
|
+
'returned handle — do NOT base64 it through the conversation:\n' +
|
|
104
|
+
` curl -s -F file=@"${f.path}" "${base}/upload${tokenQ}"\n` +
|
|
105
|
+
'then: uploadFile { files: [{ handle: "<handle from the response>" }] }');
|
|
106
|
+
}
|
|
107
|
+
const path = f.path;
|
|
108
|
+
buf = await readFile(path).catch((e) => {
|
|
109
|
+
throw new UserError(`Failed to read upload file "${path}": ` +
|
|
110
|
+
(e instanceof Error ? e.message : String(e)));
|
|
111
|
+
});
|
|
112
|
+
defaultName = basename(path);
|
|
113
|
+
delete f.path;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (buf.byteLength > FILE_TRANSFER_MAX_BYTES) {
|
|
119
|
+
throw new UserError(`Upload file "${defaultName}" is ${buf.byteLength} bytes, over the ` +
|
|
120
|
+
`50MB limit.`);
|
|
121
|
+
}
|
|
122
|
+
f.content = buf.toString('base64');
|
|
123
|
+
if (!f.name)
|
|
124
|
+
f.name = defaultName;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const describeFailedDownload = (d) => {
|
|
128
|
+
let s = `${d.filename ?? 'unknown'}: ${d.error ?? 'no data'}` +
|
|
129
|
+
(d.maxBytes ? ` (max ${d.maxBytes} bytes)` : '');
|
|
130
|
+
// Over-cap files can't go through the transfer flow — point at the source so
|
|
131
|
+
// the caller can fetch it directly (e.g. curl) if it has network access.
|
|
132
|
+
if (d.error === 'FileTooLarge' && d.sourceUrl) {
|
|
133
|
+
s += ` — too large to transfer; fetch directly: ${d.sourceUrl}`;
|
|
134
|
+
}
|
|
135
|
+
return s;
|
|
136
|
+
};
|
|
137
|
+
// Persist a download to the server's filesystem (out of the model's context),
|
|
138
|
+
// tagged to the MCP session for cleanup. Returns null for failed/empty entries.
|
|
139
|
+
const persistDownload = async (d, sessionId) => {
|
|
140
|
+
if (d.error || !d.data || !d.filename)
|
|
141
|
+
return null;
|
|
142
|
+
return storeDownload(d.filename, d.mimeType ?? 'application/octet-stream', Buffer.from(d.data, 'base64'), sessionId);
|
|
143
|
+
};
|
|
144
|
+
// stdio: file is already on the local disk → return its path (reuse as
|
|
145
|
+
// uploadFile { path }). http: return a single-use GET URL + handle; base64
|
|
146
|
+
// never enters context, and fetching consumes the file.
|
|
147
|
+
const describeReadyDownload = (record, opts) => {
|
|
148
|
+
if (opts.transport === 'stdio') {
|
|
149
|
+
return (`${record.path} (${record.mimeType}, ${record.size} bytes) — ` +
|
|
150
|
+
`reuse as uploadFile { path: "${record.path}" }`);
|
|
151
|
+
}
|
|
152
|
+
const base = opts.mcpBaseUrl ?? '<MCP_BASE_URL>';
|
|
153
|
+
const tokenQ = `?token=${opts.token ?? '<YOUR_BROWSERLESS_TOKEN>'}`;
|
|
154
|
+
return (`${record.filename} (${record.mimeType}, ${record.size} bytes)\n` +
|
|
155
|
+
` save it: curl -s "${base}/download/${record.id}${tokenQ}" -o "${record.filename}" (single use)\n` +
|
|
156
|
+
` or reuse: uploadFile { files: [{ handle: "${downloadUri(record.id)}" }] }`);
|
|
157
|
+
};
|
|
158
|
+
// Surface captured downloads as metadata + how to retrieve them (never bytes).
|
|
159
|
+
export const formatDownloads = async (downloads, prefix, skills, opts) => {
|
|
160
|
+
const lines = [];
|
|
161
|
+
for (const d of downloads) {
|
|
162
|
+
if (d.inProgress) {
|
|
163
|
+
lines.push(`- ${describeInProgressDownload(d)}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const record = await persistDownload(d, opts.sessionId);
|
|
167
|
+
lines.push(`- ${record ? describeReadyDownload(record, opts) : describeFailedDownload(d)}`);
|
|
168
|
+
}
|
|
169
|
+
const header = opts.transport === 'stdio'
|
|
170
|
+
? 'Downloads:'
|
|
171
|
+
: 'Downloads (save the ones you need — each GET works once):';
|
|
172
|
+
const text = downloads.length
|
|
173
|
+
? `${prefix}${header}\n${lines.join('\n')}`
|
|
174
|
+
: `${prefix}No new downloads.`;
|
|
175
|
+
const content = [{ type: 'text', text }];
|
|
176
|
+
if (skills)
|
|
177
|
+
content.push({ type: 'text', text: skills });
|
|
178
|
+
return content;
|
|
179
|
+
};
|
|
60
180
|
const SkillIdSchema = z.enum(skillsRegistry.map((s) => s.id));
|
|
61
181
|
const SkillToolParamsSchema = z.object({
|
|
62
182
|
id: SkillIdSchema.describe('The skill to load (see tool description for the full list).'),
|
|
@@ -85,7 +205,8 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
85
205
|
});
|
|
86
206
|
defineTool(server, config, analytics, {
|
|
87
207
|
name: 'browserless_agent',
|
|
88
|
-
description: AGENT_SYSTEM_PROMPT
|
|
208
|
+
description: AGENT_SYSTEM_PROMPT +
|
|
209
|
+
fileTransferModeNote(config.transport, config.mcpBaseUrl),
|
|
89
210
|
parameters: AgentParamsSchema,
|
|
90
211
|
annotations: {
|
|
91
212
|
title: 'Browserless Agent',
|
|
@@ -93,15 +214,16 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
93
214
|
destructiveHint: true,
|
|
94
215
|
openWorldHint: true,
|
|
95
216
|
},
|
|
96
|
-
run: async ({ params, log, analytics, token, apiUrl, sessionId: mcpSessionId, }) => {
|
|
217
|
+
run: async ({ params, log, analytics, token, apiUrl, sessionId: mcpSessionId, attachSessionId, }) => {
|
|
97
218
|
const commands = params.commands && params.commands.length > 0
|
|
98
219
|
? params.commands.map((c) => ({
|
|
99
220
|
method: c.method,
|
|
100
|
-
params:
|
|
221
|
+
params: c.params ?? {},
|
|
101
222
|
}))
|
|
102
|
-
: [{ method: params.method, params:
|
|
223
|
+
: [{ method: params.method, params: params.params ?? {} }];
|
|
103
224
|
const proxy = params.proxy;
|
|
104
225
|
const profile = params.profile;
|
|
226
|
+
const createProfile = params.createProfile;
|
|
105
227
|
const sendAnalytics = (success) => {
|
|
106
228
|
analytics?.fireToolRequest(token, 'browserless_agent', {
|
|
107
229
|
methods: commands.map((c) => c.method).join(','),
|
|
@@ -113,6 +235,7 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
113
235
|
proxy_sticky: !!proxy?.proxySticky,
|
|
114
236
|
proxy_external: !!proxy?.externalProxyServer,
|
|
115
237
|
profile_used: !!profile,
|
|
238
|
+
create_profile: !!createProfile,
|
|
116
239
|
});
|
|
117
240
|
};
|
|
118
241
|
const proxyCmd = commands.find((c) => c.method === 'proxy');
|
|
@@ -122,14 +245,32 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
122
245
|
'Recovery: call `close` to end the current session, then call browserless_agent again with the proxy options set at the top level (alongside `method`/`commands`), e.g. { "proxy": "residential", "proxyCountry": "us", "commands": [ ... ] }.');
|
|
123
246
|
}
|
|
124
247
|
if (commands.length === 1 && commands[0].method === 'close') {
|
|
125
|
-
closeSession(mcpSessionId, token, proxy, profile);
|
|
248
|
+
closeSession(mcpSessionId, token, proxy, profile, createProfile, attachSessionId);
|
|
126
249
|
sendAnalytics(true);
|
|
127
250
|
return [{ type: 'text', text: 'Browser session closed.' }];
|
|
128
251
|
}
|
|
252
|
+
// Open-only call: no real command (e.g. `createProfile`/`profile`/`proxy`
|
|
253
|
+
// set with no method/commands). Dispatching the empty-method default would
|
|
254
|
+
// make the agent route reject it as `Missing required id/method`, so just
|
|
255
|
+
// open (or reuse) the session and report it's ready for follow-up commands.
|
|
256
|
+
if (commands.length === 1 && !commands[0].method) {
|
|
257
|
+
try {
|
|
258
|
+
await getOrCreateSession(mcpSessionId, apiUrl, token, proxy, profile, createProfile, attachSessionId);
|
|
259
|
+
}
|
|
260
|
+
catch (connErr) {
|
|
261
|
+
sendAnalytics(false);
|
|
262
|
+
throw new UserError(formatConnectError(connErr));
|
|
263
|
+
}
|
|
264
|
+
sendAnalytics(true);
|
|
265
|
+
const text = createProfile
|
|
266
|
+
? `Profile-creation session "${createProfile.name}" is open (non-headless). Send commands to drive the login, then call saveProfile.`
|
|
267
|
+
: 'Browser session is open. Send commands to drive it.';
|
|
268
|
+
return [{ type: 'text', text }];
|
|
269
|
+
}
|
|
129
270
|
const runCommands = async (isRetry) => {
|
|
130
271
|
let agentSession;
|
|
131
272
|
try {
|
|
132
|
-
agentSession = await getOrCreateSession(mcpSessionId, apiUrl, token, proxy, profile);
|
|
273
|
+
agentSession = await getOrCreateSession(mcpSessionId, apiUrl, token, proxy, profile, createProfile, attachSessionId);
|
|
133
274
|
}
|
|
134
275
|
catch (connErr) {
|
|
135
276
|
// No retry when the server gave a definitive 4xx — re-attempting
|
|
@@ -138,7 +279,7 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
138
279
|
if (isRetry || !isRetryableUpgradeError(connErr)) {
|
|
139
280
|
throw new UserError(formatConnectError(connErr));
|
|
140
281
|
}
|
|
141
|
-
destroySession(mcpSessionId, token, proxy, profile);
|
|
282
|
+
destroySession(mcpSessionId, token, proxy, profile, createProfile, attachSessionId);
|
|
142
283
|
return runCommands(true);
|
|
143
284
|
}
|
|
144
285
|
// Execute all commands sequentially
|
|
@@ -150,7 +291,7 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
150
291
|
let crossOriginBaseline = agentSession.lastUrl;
|
|
151
292
|
for (const cmd of commands) {
|
|
152
293
|
if (cmd.method === 'close') {
|
|
153
|
-
closeSession(mcpSessionId, token, proxy, profile);
|
|
294
|
+
closeSession(mcpSessionId, token, proxy, profile, createProfile, attachSessionId);
|
|
154
295
|
results.push({ method: 'close', result: { closed: true } });
|
|
155
296
|
closedDuringBatch = true;
|
|
156
297
|
break;
|
|
@@ -162,7 +303,7 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
162
303
|
resp = await send(agentSession, cmd.method, cmd.params);
|
|
163
304
|
}
|
|
164
305
|
catch (sendErr) {
|
|
165
|
-
destroySession(mcpSessionId, token, proxy, profile);
|
|
306
|
+
destroySession(mcpSessionId, token, proxy, profile, createProfile, attachSessionId);
|
|
166
307
|
const errMessage = sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
167
308
|
if (!isRetry) {
|
|
168
309
|
log.warn(`agent: ${cmd.method} failed (first attempt, retrying once): ${errMessage}`);
|
|
@@ -182,7 +323,7 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
182
323
|
if (resp.error) {
|
|
183
324
|
const err = resp.error;
|
|
184
325
|
if (err.code && FATAL_CODES.has(err.code)) {
|
|
185
|
-
destroySession(mcpSessionId, token, proxy, profile);
|
|
326
|
+
destroySession(mcpSessionId, token, proxy, profile, createProfile, attachSessionId);
|
|
186
327
|
if (!isRetry) {
|
|
187
328
|
return runCommands(true);
|
|
188
329
|
}
|
|
@@ -253,13 +394,29 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
253
394
|
if (!last) {
|
|
254
395
|
return [{ type: 'text', text: 'Browser session closed.' }];
|
|
255
396
|
}
|
|
256
|
-
//
|
|
397
|
+
// Auto-surface files Chrome captured this batch so the model needn't call
|
|
398
|
+
// getDownloads. Skipped on explicit drain/close; a failed poll is ignored.
|
|
399
|
+
let autoDownloads = [];
|
|
400
|
+
if (!closedDuringBatch && last.method !== 'getDownloads') {
|
|
401
|
+
try {
|
|
402
|
+
const dl = await send(agentSession, 'getDownloads', {});
|
|
403
|
+
autoDownloads =
|
|
404
|
+
dl.result
|
|
405
|
+
?.downloads ?? [];
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// ignore — downloads will surface on a later call
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const skillsText = triggered.length > 0 ? renderSkills(triggered) : '';
|
|
412
|
+
let baseContent;
|
|
257
413
|
if (lastSnapshot) {
|
|
414
|
+
// Snapshot: compact ref-based text.
|
|
258
415
|
const notice = buildCrossOriginNotice(crossOriginBaseline, lastSnapshot.url);
|
|
259
416
|
const noticeBlock = notice ? `${notice}\n\n` : '';
|
|
260
417
|
if (lastSnapshot.url)
|
|
261
418
|
agentSession.lastUrl = lastSnapshot.url;
|
|
262
|
-
|
|
419
|
+
baseContent = [
|
|
263
420
|
{
|
|
264
421
|
type: 'text',
|
|
265
422
|
text: appendSkills(batchPrefix +
|
|
@@ -269,22 +426,47 @@ export function registerAgentTools(server, config, analytics) {
|
|
|
269
426
|
},
|
|
270
427
|
];
|
|
271
428
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
429
|
+
else if (last.method === 'getDownloads') {
|
|
430
|
+
// Explicit drain.
|
|
431
|
+
const downloads = lastResult?.downloads ?? [];
|
|
432
|
+
const prefix = batchPrefix + (closedSuffix ? `${closedSuffix}\n\n` : '');
|
|
433
|
+
return await formatDownloads(downloads, prefix, skillsText, {
|
|
434
|
+
transport: config.transport,
|
|
435
|
+
sessionId: mcpSessionId,
|
|
436
|
+
mcpBaseUrl: config.mcpBaseUrl,
|
|
437
|
+
token,
|
|
438
|
+
});
|
|
278
439
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
440
|
+
else {
|
|
441
|
+
// Screenshot → image content block; otherwise JSON text.
|
|
442
|
+
const shot = last.method === 'screenshot'
|
|
443
|
+
? formatScreenshotContent(lastResult, lastCmd, batchPrefix, skillsText)
|
|
444
|
+
: null;
|
|
445
|
+
baseContent = shot ?? [
|
|
446
|
+
{
|
|
447
|
+
type: 'text',
|
|
448
|
+
text: appendSkills(batchPrefix + JSON.stringify(lastResult, null, 2), triggered),
|
|
449
|
+
},
|
|
450
|
+
];
|
|
451
|
+
}
|
|
452
|
+
// Append the captured-download notification (metadata only, no bytes).
|
|
453
|
+
if (autoDownloads.length > 0) {
|
|
454
|
+
const notice = await formatDownloads(autoDownloads, '', '', {
|
|
455
|
+
transport: config.transport,
|
|
456
|
+
sessionId: mcpSessionId,
|
|
457
|
+
mcpBaseUrl: config.mcpBaseUrl,
|
|
458
|
+
token,
|
|
459
|
+
});
|
|
460
|
+
baseContent = [...baseContent, ...notice];
|
|
461
|
+
}
|
|
462
|
+
return baseContent;
|
|
286
463
|
};
|
|
287
464
|
try {
|
|
465
|
+
// Resolve any local upload paths to base64 once, before the (possibly
|
|
466
|
+
// retried) send loop runs.
|
|
467
|
+
for (const cmd of commands) {
|
|
468
|
+
await normalizeUploadCommand(cmd, config.transport, config.mcpBaseUrl);
|
|
469
|
+
}
|
|
288
470
|
const result = await runCommands(false);
|
|
289
471
|
sendAnalytics(true);
|
|
290
472
|
return result;
|
|
@@ -84,6 +84,12 @@ export declare const AgentCommandSchema: z.ZodUnion<readonly [z.ZodDiscriminated
|
|
|
84
84
|
selector: z.ZodString;
|
|
85
85
|
text: z.ZodString;
|
|
86
86
|
}, z.core.$strip>;
|
|
87
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
88
|
+
method: z.ZodLiteral<"loadSecret">;
|
|
89
|
+
params: z.ZodObject<{
|
|
90
|
+
ref: z.ZodString;
|
|
91
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$strip>;
|
|
87
93
|
}, z.core.$strip>, z.ZodObject<{
|
|
88
94
|
method: z.ZodLiteral<"select">;
|
|
89
95
|
params: z.ZodObject<{
|
|
@@ -214,6 +220,21 @@ export declare const AgentCommandSchema: z.ZodUnion<readonly [z.ZodDiscriminated
|
|
|
214
220
|
waitForImages: z.ZodOptional<z.ZodBoolean>;
|
|
215
221
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
216
222
|
}, z.core.$strip>>>;
|
|
223
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
224
|
+
method: z.ZodLiteral<"uploadFile">;
|
|
225
|
+
params: z.ZodObject<{
|
|
226
|
+
selector: z.ZodString;
|
|
227
|
+
files: z.ZodArray<z.ZodObject<{
|
|
228
|
+
content: z.ZodOptional<z.ZodString>;
|
|
229
|
+
handle: z.ZodOptional<z.ZodString>;
|
|
230
|
+
path: z.ZodOptional<z.ZodString>;
|
|
231
|
+
name: z.ZodOptional<z.ZodString>;
|
|
232
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
233
|
+
}, z.core.$strip>>;
|
|
234
|
+
}, z.core.$strip>;
|
|
235
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
236
|
+
method: z.ZodLiteral<"getDownloads">;
|
|
237
|
+
params: z.ZodDefault<z.ZodOptional<z.ZodObject<{}, z.core.$strip>>>;
|
|
217
238
|
}, z.core.$strip>, z.ZodObject<{
|
|
218
239
|
method: z.ZodLiteral<"close">;
|
|
219
240
|
params: z.ZodDefault<z.ZodOptional<z.ZodObject<{}, z.core.$strip>>>;
|
|
@@ -221,6 +242,23 @@ export declare const AgentCommandSchema: z.ZodUnion<readonly [z.ZodDiscriminated
|
|
|
221
242
|
method: z.ZodString;
|
|
222
243
|
params: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
223
244
|
}, z.core.$strip>]>;
|
|
245
|
+
declare const CreateProfileSchema: z.ZodObject<{
|
|
246
|
+
name: z.ZodString;
|
|
247
|
+
proxy: z.ZodOptional<z.ZodObject<{
|
|
248
|
+
type: z.ZodOptional<z.ZodLiteral<"residential">>;
|
|
249
|
+
sticky: z.ZodOptional<z.ZodBoolean>;
|
|
250
|
+
country: z.ZodOptional<z.ZodString>;
|
|
251
|
+
city: z.ZodOptional<z.ZodString>;
|
|
252
|
+
state: z.ZodOptional<z.ZodString>;
|
|
253
|
+
preset: z.ZodOptional<z.ZodString>;
|
|
254
|
+
}, z.core.$strip>>;
|
|
255
|
+
browser: z.ZodOptional<z.ZodEnum<{
|
|
256
|
+
chrome: "chrome";
|
|
257
|
+
chromium: "chromium";
|
|
258
|
+
stealth: "stealth";
|
|
259
|
+
}>>;
|
|
260
|
+
stealth: z.ZodOptional<z.ZodBoolean>;
|
|
261
|
+
}, z.core.$strip>;
|
|
224
262
|
export declare const AgentParamsSchema: z.ZodObject<{
|
|
225
263
|
method: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
226
264
|
params: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
@@ -308,6 +346,12 @@ export declare const AgentParamsSchema: z.ZodObject<{
|
|
|
308
346
|
selector: z.ZodString;
|
|
309
347
|
text: z.ZodString;
|
|
310
348
|
}, z.core.$strip>;
|
|
349
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
350
|
+
method: z.ZodLiteral<"loadSecret">;
|
|
351
|
+
params: z.ZodObject<{
|
|
352
|
+
ref: z.ZodString;
|
|
353
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
354
|
+
}, z.core.$strip>;
|
|
311
355
|
}, z.core.$strip>, z.ZodObject<{
|
|
312
356
|
method: z.ZodLiteral<"select">;
|
|
313
357
|
params: z.ZodObject<{
|
|
@@ -438,6 +482,21 @@ export declare const AgentParamsSchema: z.ZodObject<{
|
|
|
438
482
|
waitForImages: z.ZodOptional<z.ZodBoolean>;
|
|
439
483
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
440
484
|
}, z.core.$strip>>>;
|
|
485
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
486
|
+
method: z.ZodLiteral<"uploadFile">;
|
|
487
|
+
params: z.ZodObject<{
|
|
488
|
+
selector: z.ZodString;
|
|
489
|
+
files: z.ZodArray<z.ZodObject<{
|
|
490
|
+
content: z.ZodOptional<z.ZodString>;
|
|
491
|
+
handle: z.ZodOptional<z.ZodString>;
|
|
492
|
+
path: z.ZodOptional<z.ZodString>;
|
|
493
|
+
name: z.ZodOptional<z.ZodString>;
|
|
494
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
495
|
+
}, z.core.$strip>>;
|
|
496
|
+
}, z.core.$strip>;
|
|
497
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
498
|
+
method: z.ZodLiteral<"getDownloads">;
|
|
499
|
+
params: z.ZodDefault<z.ZodOptional<z.ZodObject<{}, z.core.$strip>>>;
|
|
441
500
|
}, z.core.$strip>, z.ZodObject<{
|
|
442
501
|
method: z.ZodLiteral<"close">;
|
|
443
502
|
params: z.ZodDefault<z.ZodOptional<z.ZodObject<{}, z.core.$strip>>>;
|
|
@@ -458,9 +517,29 @@ export declare const AgentParamsSchema: z.ZodObject<{
|
|
|
458
517
|
externalProxyServer: z.ZodOptional<z.ZodString>;
|
|
459
518
|
}, z.core.$strip>>;
|
|
460
519
|
profile: z.ZodOptional<z.ZodString>;
|
|
520
|
+
createProfile: z.ZodOptional<z.ZodObject<{
|
|
521
|
+
name: z.ZodString;
|
|
522
|
+
proxy: z.ZodOptional<z.ZodObject<{
|
|
523
|
+
type: z.ZodOptional<z.ZodLiteral<"residential">>;
|
|
524
|
+
sticky: z.ZodOptional<z.ZodBoolean>;
|
|
525
|
+
country: z.ZodOptional<z.ZodString>;
|
|
526
|
+
city: z.ZodOptional<z.ZodString>;
|
|
527
|
+
state: z.ZodOptional<z.ZodString>;
|
|
528
|
+
preset: z.ZodOptional<z.ZodString>;
|
|
529
|
+
}, z.core.$strip>>;
|
|
530
|
+
browser: z.ZodOptional<z.ZodEnum<{
|
|
531
|
+
chrome: "chrome";
|
|
532
|
+
chromium: "chromium";
|
|
533
|
+
stealth: "stealth";
|
|
534
|
+
}>>;
|
|
535
|
+
stealth: z.ZodOptional<z.ZodBoolean>;
|
|
536
|
+
}, z.core.$strip>>;
|
|
461
537
|
rationale: z.ZodOptional<z.ZodString>;
|
|
462
538
|
}, z.core.$strip>;
|
|
463
539
|
/** A single validated agent command. */
|
|
464
540
|
export type AgentCommand = z.infer<typeof AgentCommandSchema>;
|
|
465
541
|
/** The full `browserless_agent` tool params (single command, batch, proxy, profile). */
|
|
466
542
|
export type AgentParams = z.infer<typeof AgentParamsSchema>;
|
|
543
|
+
/** Params for opening a profile-creation session (POST /profile passthrough). */
|
|
544
|
+
export type CreateProfileParams = z.infer<typeof CreateProfileSchema>;
|
|
545
|
+
export {};
|