@browserless.io/mcp 1.6.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.
Files changed (81) hide show
  1. package/LICENSE +557 -0
  2. package/README.md +280 -0
  3. package/bin/cli.js +2 -0
  4. package/build/src/@types/types.d.ts +538 -0
  5. package/build/src/config.d.ts +3 -0
  6. package/build/src/config.js +42 -0
  7. package/build/src/index.d.ts +4 -0
  8. package/build/src/index.js +153 -0
  9. package/build/src/lib/account-resolver.d.ts +17 -0
  10. package/build/src/lib/account-resolver.js +78 -0
  11. package/build/src/lib/agent-client.d.ts +58 -0
  12. package/build/src/lib/agent-client.js +530 -0
  13. package/build/src/lib/agent-format.d.ts +35 -0
  14. package/build/src/lib/agent-format.js +155 -0
  15. package/build/src/lib/amplitude.d.ts +11 -0
  16. package/build/src/lib/amplitude.js +65 -0
  17. package/build/src/lib/analytics.d.ts +18 -0
  18. package/build/src/lib/analytics.js +79 -0
  19. package/build/src/lib/api-client.d.ts +17 -0
  20. package/build/src/lib/api-client.js +357 -0
  21. package/build/src/lib/bounded-event-store.d.ts +22 -0
  22. package/build/src/lib/bounded-event-store.js +69 -0
  23. package/build/src/lib/cache.d.ts +12 -0
  24. package/build/src/lib/cache.js +49 -0
  25. package/build/src/lib/define-tool.d.ts +71 -0
  26. package/build/src/lib/define-tool.js +71 -0
  27. package/build/src/lib/error-classifier.d.ts +4 -0
  28. package/build/src/lib/error-classifier.js +125 -0
  29. package/build/src/lib/redis-oauth-proxy.d.ts +13 -0
  30. package/build/src/lib/redis-oauth-proxy.js +214 -0
  31. package/build/src/lib/retry.d.ts +2 -0
  32. package/build/src/lib/retry.js +19 -0
  33. package/build/src/lib/schema-fields.d.ts +10 -0
  34. package/build/src/lib/schema-fields.js +27 -0
  35. package/build/src/lib/supabase-token-patch.d.ts +6 -0
  36. package/build/src/lib/supabase-token-patch.js +33 -0
  37. package/build/src/lib/utils.d.ts +27 -0
  38. package/build/src/lib/utils.js +67 -0
  39. package/build/src/prompts/extract-content.d.ts +2 -0
  40. package/build/src/prompts/extract-content.js +33 -0
  41. package/build/src/prompts/scrape-url.d.ts +2 -0
  42. package/build/src/prompts/scrape-url.js +36 -0
  43. package/build/src/resources/api-docs.d.ts +3 -0
  44. package/build/src/resources/api-docs.js +54 -0
  45. package/build/src/resources/status.d.ts +3 -0
  46. package/build/src/resources/status.js +30 -0
  47. package/build/src/skills/autonomous-login.md +95 -0
  48. package/build/src/skills/captchas.md +48 -0
  49. package/build/src/skills/cookie-consent.md +50 -0
  50. package/build/src/skills/dynamic-content.md +72 -0
  51. package/build/src/skills/index.d.ts +9 -0
  52. package/build/src/skills/index.js +221 -0
  53. package/build/src/skills/modals.md +56 -0
  54. package/build/src/skills/screenshots.md +53 -0
  55. package/build/src/skills/shadow-dom.md +64 -0
  56. package/build/src/skills/snapshot-misses.md +67 -0
  57. package/build/src/skills/system-prompt.d.ts +2 -0
  58. package/build/src/skills/system-prompt.js +128 -0
  59. package/build/src/skills/tabs.md +77 -0
  60. package/build/src/tools/agent.d.ts +15 -0
  61. package/build/src/tools/agent.js +299 -0
  62. package/build/src/tools/crawl.d.ts +75 -0
  63. package/build/src/tools/crawl.js +426 -0
  64. package/build/src/tools/download.d.ts +11 -0
  65. package/build/src/tools/download.js +92 -0
  66. package/build/src/tools/export.d.ts +28 -0
  67. package/build/src/tools/export.js +129 -0
  68. package/build/src/tools/function.d.ts +24 -0
  69. package/build/src/tools/function.js +144 -0
  70. package/build/src/tools/map.d.ts +23 -0
  71. package/build/src/tools/map.js +129 -0
  72. package/build/src/tools/performance.d.ts +25 -0
  73. package/build/src/tools/performance.js +103 -0
  74. package/build/src/tools/schemas.d.ts +466 -0
  75. package/build/src/tools/schemas.js +487 -0
  76. package/build/src/tools/search.d.ts +67 -0
  77. package/build/src/tools/search.js +184 -0
  78. package/build/src/tools/smartscraper.d.ts +42 -0
  79. package/build/src/tools/smartscraper.js +136 -0
  80. package/package.json +111 -0
  81. package/patches/mcp-proxy+6.4.0.patch +31 -0
@@ -0,0 +1,299 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getOrCreateSession, send, closeSession, destroySession, isRetryableUpgradeError, } from '../lib/agent-client.js';
4
+ import { classifyAgentError } from '../lib/error-classifier.js';
5
+ import { defineTool } from '../lib/define-tool.js';
6
+ import { detectSkills, markFired, renderSkill, renderSkills, skillsRegistry, } from '../skills/index.js';
7
+ import { AgentParamsSchema } from './schemas.js';
8
+ import { AGENT_SYSTEM_PROMPT, SKILL_TOOL_DESCRIPTION, } from '../skills/system-prompt.js';
9
+ import { buildCrossOriginNotice, formatConnectError, formatErrorMessage, formatSnapshot, } from '../lib/agent-format.js';
10
+ // export schemas, system prompt, and formatters
11
+ export { AgentParamsSchema } from './schemas.js';
12
+ export { buildCrossOriginNotice, formatConnectError, formatErrorMessage, formatSnapshot, sanitizeUpgradeBody, } from '../lib/agent-format.js';
13
+ const SNAPSHOT_METHOD = 'snapshot';
14
+ const FATAL_CODES = new Set(['BROWSER_CRASHED']);
15
+ const appendSkills = (base, ids) => {
16
+ if (ids.length === 0)
17
+ return base;
18
+ return `${base}\n\n${renderSkills(ids)}`;
19
+ };
20
+ const SCREENSHOT_MIME = {
21
+ jpeg: 'image/jpeg',
22
+ webp: 'image/webp',
23
+ png: 'image/png',
24
+ };
25
+ /**
26
+ * Build the MCP response for a screenshot command, or null when there's no
27
+ * base64 payload (caller falls back to JSON text). Returns the image as a
28
+ * vision content block (~1.5K tokens) vs. ~67K inlining the base64 as text.
29
+ */
30
+ export const formatScreenshotContent = (result, cmd, caption, skills) => {
31
+ const base64 = typeof result?.base64 === 'string'
32
+ ? result.base64
33
+ : '';
34
+ if (!base64)
35
+ return null;
36
+ const requestedType = typeof cmd.params?.type === 'string' ? cmd.params.type : 'png';
37
+ const mimeType = SCREENSHOT_MIME[requestedType] ?? 'image/png';
38
+ const decodedBytes = Math.floor(base64.length * 0.75);
39
+ const sizeLabel = decodedBytes >= 1_048_576
40
+ ? `${(decodedBytes / 1_048_576).toFixed(1)} MB`
41
+ : `${Math.round(decodedBytes / 1024)} KB`;
42
+ const captionText = [
43
+ caption.trimEnd(),
44
+ `Screenshot captured (${mimeType}, ~${sizeLabel}).`,
45
+ ]
46
+ .filter(Boolean)
47
+ .join('\n\n');
48
+ const content = [
49
+ { type: 'text', text: captionText },
50
+ { type: 'image', data: base64, mimeType },
51
+ ];
52
+ if (skills)
53
+ content.push({ type: 'text', text: skills });
54
+ return content;
55
+ };
56
+ // Zod parses params at the tool boundary, so this only needs to supply the {}
57
+ // default when the field was omitted — the schema never delivers a string,
58
+ // array, or null here.
59
+ const coerceParams = (params) => params ?? {};
60
+ const SkillIdSchema = z.enum(skillsRegistry.map((s) => s.id));
61
+ const SkillToolParamsSchema = z.object({
62
+ id: SkillIdSchema.describe('The skill to load (see tool description for the full list).'),
63
+ });
64
+ export function registerAgentTools(server, config, analytics) {
65
+ defineTool(server, config, analytics, {
66
+ name: 'browserless_skill',
67
+ description: SKILL_TOOL_DESCRIPTION,
68
+ parameters: SkillToolParamsSchema,
69
+ annotations: {
70
+ title: 'Load Browserless Skill',
71
+ readOnlyHint: true,
72
+ destructiveHint: false,
73
+ openWorldHint: false,
74
+ },
75
+ run: async ({ params }) => renderSkill(params.id),
76
+ analyticsProps: (params, body) => ({
77
+ skill: params.id,
78
+ success: !!body,
79
+ }),
80
+ format: (body, params) => {
81
+ if (!body)
82
+ throw new UserError(`Unknown skill id: ${params.id}`);
83
+ return [{ type: 'text', text: body }];
84
+ },
85
+ });
86
+ defineTool(server, config, analytics, {
87
+ name: 'browserless_agent',
88
+ description: AGENT_SYSTEM_PROMPT,
89
+ parameters: AgentParamsSchema,
90
+ annotations: {
91
+ title: 'Browserless Agent',
92
+ readOnlyHint: false,
93
+ destructiveHint: true,
94
+ openWorldHint: true,
95
+ },
96
+ run: async ({ params, log, analytics, token, apiUrl, sessionId: mcpSessionId, }) => {
97
+ const commands = params.commands && params.commands.length > 0
98
+ ? params.commands.map((c) => ({
99
+ method: c.method,
100
+ params: coerceParams(c.params),
101
+ }))
102
+ : [{ method: params.method, params: coerceParams(params.params) }];
103
+ const proxy = params.proxy;
104
+ const profile = params.profile;
105
+ const sendAnalytics = (success) => {
106
+ analytics?.fireToolRequest(token, 'browserless_agent', {
107
+ methods: commands.map((c) => c.method).join(','),
108
+ command_count: commands.length,
109
+ api_url: apiUrl,
110
+ success,
111
+ proxy_tier: proxy?.proxy ?? null,
112
+ proxy_country: proxy?.proxyCountry ?? null,
113
+ proxy_sticky: !!proxy?.proxySticky,
114
+ proxy_external: !!proxy?.externalProxyServer,
115
+ profile_used: !!profile,
116
+ });
117
+ };
118
+ const proxyCmd = commands.find((c) => c.method === 'proxy');
119
+ if (proxyCmd) {
120
+ sendAnalytics(false);
121
+ throw new UserError('Invalid command: "proxy" is not a BQL mutation. Proxy config is a top-level tool argument (proxy, proxyCountry, proxyState, proxyCity, proxySticky, proxyLocaleMatch, proxyPreset, externalProxyServer) and is read once at session creation. ' +
122
+ '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
+ }
124
+ if (commands.length === 1 && commands[0].method === 'close') {
125
+ closeSession(mcpSessionId, token, proxy, profile);
126
+ sendAnalytics(true);
127
+ return [{ type: 'text', text: 'Browser session closed.' }];
128
+ }
129
+ const runCommands = async (isRetry) => {
130
+ let agentSession;
131
+ try {
132
+ agentSession = await getOrCreateSession(mcpSessionId, apiUrl, token, proxy, profile);
133
+ }
134
+ catch (connErr) {
135
+ // No retry when the server gave a definitive 4xx — re-attempting
136
+ // with the same (bad token / wrong profile / unsupported params)
137
+ // will just produce the same response and waste time.
138
+ if (isRetry || !isRetryableUpgradeError(connErr)) {
139
+ throw new UserError(formatConnectError(connErr));
140
+ }
141
+ destroySession(mcpSessionId, token, proxy, profile);
142
+ return runCommands(true);
143
+ }
144
+ // Execute all commands sequentially
145
+ const results = [];
146
+ let closedDuringBatch = false;
147
+ // Cross-origin baseline: prefer the URL from the previous snapshot,
148
+ // else the first URL seen this batch — so [goto A, goto B, snapshot]
149
+ // still detects the A→snapshot cross-origin transition.
150
+ let crossOriginBaseline = agentSession.lastUrl;
151
+ for (const cmd of commands) {
152
+ if (cmd.method === 'close') {
153
+ closeSession(mcpSessionId, token, proxy, profile);
154
+ results.push({ method: 'close', result: { closed: true } });
155
+ closedDuringBatch = true;
156
+ break;
157
+ }
158
+ log.info(`agent: ${cmd.method} ${JSON.stringify(cmd.params)}`);
159
+ agentSession.skillState.cmdIndex += 1;
160
+ let resp;
161
+ try {
162
+ resp = await send(agentSession, cmd.method, cmd.params);
163
+ }
164
+ catch (sendErr) {
165
+ destroySession(mcpSessionId, token, proxy, profile);
166
+ const errMessage = sendErr instanceof Error ? sendErr.message : String(sendErr);
167
+ if (!isRetry) {
168
+ log.warn(`agent: ${cmd.method} failed (first attempt, retrying once): ${errMessage}`);
169
+ return runCommands(true);
170
+ }
171
+ const classified = classifyAgentError({
172
+ err: { message: errMessage },
173
+ cmd,
174
+ });
175
+ throw new UserError(formatErrorMessage({
176
+ category: classified.category,
177
+ prefix: `${cmd.method} failed: `,
178
+ message: errMessage,
179
+ recovery: classified.recovery,
180
+ }));
181
+ }
182
+ if (resp.error) {
183
+ const err = resp.error;
184
+ if (err.code && FATAL_CODES.has(err.code)) {
185
+ destroySession(mcpSessionId, token, proxy, profile);
186
+ if (!isRetry) {
187
+ return runCommands(true);
188
+ }
189
+ }
190
+ const classified = classifyAgentError({ err, cmd });
191
+ const prefix = commands.length > 1
192
+ ? `Batch failed at "${cmd.method}" (after ${results.map((r) => r.method).join(' → ') || 'start'}): `
193
+ : `${cmd.method} failed: `;
194
+ let suggestion;
195
+ if (err.code === 'SELECTOR_NOT_FOUND' &&
196
+ cmd.params.selector &&
197
+ typeof cmd.params.selector === 'string' &&
198
+ !cmd.params.selector.startsWith('< ')) {
199
+ suggestion = `Retry with deep selector "< ${cmd.params.selector}" — the element is likely inside a shadow DOM.`;
200
+ }
201
+ else if (err.suggestion) {
202
+ suggestion = err.suggestion;
203
+ }
204
+ const body = formatErrorMessage({
205
+ category: classified.category,
206
+ code: err.code,
207
+ prefix,
208
+ message: err.message,
209
+ suggestion,
210
+ recovery: classified.recovery,
211
+ snapshotText: err.snapshot
212
+ ? formatSnapshot(err.snapshot)
213
+ : undefined,
214
+ });
215
+ const triggered = detectSkills({ snapshot: err.snapshot, error: err, cmd, apiUrl }, agentSession.skillState);
216
+ markFired(agentSession.skillState, triggered);
217
+ throw new UserError(appendSkills(body, triggered));
218
+ }
219
+ // Capture the first URL we observe in the batch as a fallback
220
+ // baseline for the cross-origin notice.
221
+ if (!crossOriginBaseline) {
222
+ const r = resp.result;
223
+ if (r && typeof r.url === 'string') {
224
+ crossOriginBaseline = r.url;
225
+ }
226
+ }
227
+ results.push({ method: cmd.method, result: resp.result });
228
+ }
229
+ // If the batch ended with close, format the result around the
230
+ // command before close (close itself has no useful payload).
231
+ const reportable = closedDuringBatch ? results.slice(0, -1) : results;
232
+ const last = reportable[reportable.length - 1];
233
+ const lastResult = last.result;
234
+ const lastCmd = commands[commands.length - 1];
235
+ const closedSuffix = closedDuringBatch
236
+ ? '\n\nBrowser session closed.'
237
+ : '';
238
+ const batchPrefix = commands.length > 1
239
+ ? `Executed: ${results.map((r) => r.method).join(' → ')}\n\n`
240
+ : '';
241
+ const lastSnapshot = last.method === SNAPSHOT_METHOD
242
+ ? lastResult
243
+ : undefined;
244
+ const triggered = detectSkills({
245
+ snapshot: lastSnapshot,
246
+ cmd: lastCmd,
247
+ resp: lastResult,
248
+ apiUrl,
249
+ }, agentSession.skillState);
250
+ markFired(agentSession.skillState, triggered);
251
+ // The whole batch was just `close` (or close-only after a no-op
252
+ // prefix that produced nothing reportable).
253
+ if (!last) {
254
+ return [{ type: 'text', text: 'Browser session closed.' }];
255
+ }
256
+ // Snapshot: format as compact ref-based text
257
+ if (lastSnapshot) {
258
+ const notice = buildCrossOriginNotice(crossOriginBaseline, lastSnapshot.url);
259
+ const noticeBlock = notice ? `${notice}\n\n` : '';
260
+ if (lastSnapshot.url)
261
+ agentSession.lastUrl = lastSnapshot.url;
262
+ return [
263
+ {
264
+ type: 'text',
265
+ text: appendSkills(batchPrefix +
266
+ noticeBlock +
267
+ formatSnapshot(lastSnapshot) +
268
+ closedSuffix, triggered),
269
+ },
270
+ ];
271
+ }
272
+ // Screenshot: return as image content block (vision input ≈ 1.5K tokens
273
+ // vs. ~67K tokens if we dumped the base64 inline as text).
274
+ if (last.method === 'screenshot') {
275
+ const content = formatScreenshotContent(lastResult, lastCmd, batchPrefix, triggered.length > 0 ? renderSkills(triggered) : '');
276
+ if (content)
277
+ return content;
278
+ }
279
+ // Everything else: return as JSON text
280
+ return [
281
+ {
282
+ type: 'text',
283
+ text: appendSkills(batchPrefix + JSON.stringify(lastResult, null, 2), triggered),
284
+ },
285
+ ];
286
+ };
287
+ try {
288
+ const result = await runCommands(false);
289
+ sendAnalytics(true);
290
+ return result;
291
+ }
292
+ catch (err) {
293
+ sendAnalytics(false);
294
+ throw err;
295
+ }
296
+ },
297
+ format: (content) => content,
298
+ });
299
+ }
@@ -0,0 +1,75 @@
1
+ import { FastMCP } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { AnalyticsHelper } from '../lib/analytics.js';
4
+ import type { McpConfig } from '../@types/types.js';
5
+ export declare const CrawlStatusSchema: z.ZodEnum<{
6
+ "in-progress": "in-progress";
7
+ completed: "completed";
8
+ failed: "failed";
9
+ cancelled: "cancelled";
10
+ }>;
11
+ export declare const PageStatusSchema: z.ZodEnum<{
12
+ "in-progress": "in-progress";
13
+ completed: "completed";
14
+ failed: "failed";
15
+ cancelled: "cancelled";
16
+ queued: "queued";
17
+ }>;
18
+ export declare const CrawlSitemapModeSchema: z.ZodEnum<{
19
+ skip: "skip";
20
+ auto: "auto";
21
+ force: "force";
22
+ }>;
23
+ export declare const CrawlFormatSchema: z.ZodEnum<{
24
+ markdown: "markdown";
25
+ html: "html";
26
+ rawText: "rawText";
27
+ }>;
28
+ export declare const CrawlScrapeOptionsSchema: z.ZodObject<{
29
+ formats: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEnum<{
30
+ markdown: "markdown";
31
+ html: "html";
32
+ rawText: "rawText";
33
+ }>>>>;
34
+ onlyMainContent: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
35
+ includeTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
36
+ excludeTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
37
+ waitFor: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
38
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
39
+ timeout: z.ZodOptional<z.ZodNumber>;
40
+ }, z.core.$strip>;
41
+ export declare const CrawlParamsSchema: z.ZodObject<{
42
+ url: z.ZodURL;
43
+ limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
44
+ maxDepth: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
45
+ maxRetries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
46
+ allowExternalLinks: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
47
+ allowSubdomains: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
48
+ sitemap: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
49
+ skip: "skip";
50
+ auto: "auto";
51
+ force: "force";
52
+ }>>>;
53
+ includePaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ excludePaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
55
+ delay: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
56
+ scrapeOptions: z.ZodOptional<z.ZodObject<{
57
+ formats: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEnum<{
58
+ markdown: "markdown";
59
+ html: "html";
60
+ rawText: "rawText";
61
+ }>>>>;
62
+ onlyMainContent: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
63
+ includeTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
64
+ excludeTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
65
+ waitFor: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
66
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
67
+ timeout: z.ZodOptional<z.ZodNumber>;
68
+ }, z.core.$strip>>;
69
+ waitForCompletion: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
70
+ pollInterval: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
71
+ maxWaitTime: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
72
+ timeout: z.ZodOptional<z.ZodNumber>;
73
+ profile: z.ZodOptional<z.ZodString>;
74
+ }, z.core.$strip>;
75
+ export declare function registerCrawlTool(server: FastMCP, config: McpConfig, analytics?: AnalyticsHelper): void;