@browserless.io/mcp 1.6.2 → 1.7.1

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 (33) hide show
  1. package/README.md +13 -12
  2. package/build/src/@types/types.d.ts +27 -3
  3. package/build/src/index.js +21 -29
  4. package/build/src/lib/agent-client.d.ts +5 -4
  5. package/build/src/lib/agent-client.js +87 -16
  6. package/build/src/lib/agent-format.d.ts +1 -1
  7. package/build/src/lib/agent-format.js +22 -4
  8. package/build/src/lib/define-tool.d.ts +5 -0
  9. package/build/src/lib/define-tool.js +1 -0
  10. package/build/src/lib/download-store.d.ts +17 -0
  11. package/build/src/lib/download-store.js +84 -0
  12. package/build/src/lib/http-auth.d.ts +22 -0
  13. package/build/src/lib/http-auth.js +33 -0
  14. package/build/src/resources/download-route.d.ts +16 -0
  15. package/build/src/resources/download-route.js +53 -0
  16. package/build/src/resources/upload-route.d.ts +3 -0
  17. package/build/src/resources/upload-route.js +53 -0
  18. package/build/src/skills/auth-profile.md +66 -0
  19. package/build/src/skills/autonomous-login.md +44 -43
  20. package/build/src/skills/file-transfers.md +88 -0
  21. package/build/src/skills/index.js +19 -0
  22. package/build/src/skills/shadow-dom.md +10 -1
  23. package/build/src/skills/system-prompt.d.ts +3 -2
  24. package/build/src/skills/system-prompt.js +32 -2
  25. package/build/src/tools/agent.d.ts +23 -0
  26. package/build/src/tools/agent.js +212 -30
  27. package/build/src/tools/map.js +1 -1
  28. package/build/src/tools/schemas.d.ts +79 -0
  29. package/build/src/tools/schemas.js +126 -3
  30. package/build/src/tools/smartscraper.js +4 -3
  31. package/package.json +5 -3
  32. package/build/src/tools/download.d.ts +0 -11
  33. package/build/src/tools/download.js +0 -92
@@ -130,6 +130,22 @@ const TypeCommandSchema = z.object({
130
130
  text: z.string().describe('Text to type into the element'),
131
131
  }),
132
132
  });
133
+ const LoadSecretCommandSchema = z.object({
134
+ method: z.literal('loadSecret'),
135
+ params: z.object({
136
+ ref: z
137
+ .string()
138
+ .describe('The credential reference/alias to inject (e.g. an op:// reference). ' +
139
+ 'The secret value is resolved server-side and typed into the field — ' +
140
+ 'you never see it. Use this for ALL passwords and usernames from a ' +
141
+ 'secrets vault; never put a secret value in `type`.'),
142
+ selector: z
143
+ .string()
144
+ .optional()
145
+ .describe('CSS selector of the input to fill. If omitted, the secret is injected ' +
146
+ 'into the currently focused element (click/focus the field first).'),
147
+ }),
148
+ });
133
149
  const SelectCommandSchema = z.object({
134
150
  method: z.literal('select'),
135
151
  params: z.object({
@@ -392,6 +408,59 @@ const SolveCommandSchema = z.object({
392
408
  .optional()
393
409
  .default({}),
394
410
  });
411
+ const UploadFileCommandSchema = z.object({
412
+ method: z.literal('uploadFile'),
413
+ params: z.object({
414
+ selector: z
415
+ .string()
416
+ .describe('CSS selector of the <input type="file"> element'),
417
+ files: z
418
+ .array(z
419
+ .object({
420
+ content: z
421
+ .string()
422
+ .optional()
423
+ .describe('Base64-encoded file content. LAST RESORT — only for tiny data ' +
424
+ 'you already hold inline. Do NOT read a file into the ' +
425
+ 'conversation, and never split/reassemble base64 by hand: use ' +
426
+ '`path` (stdio) or `handle` so the server moves the bytes.'),
427
+ handle: z
428
+ .string()
429
+ .optional()
430
+ .describe('A download handle from a prior getDownloads (a path in stdio ' +
431
+ 'mode, a `browserless-download://` URI in HTTP mode). The MCP ' +
432
+ 'server reads the stored file — works in both transports and ' +
433
+ 'keeps the bytes out of the conversation. Use this to re-upload ' +
434
+ 'a file you just downloaded.'),
435
+ path: z
436
+ .string()
437
+ .optional()
438
+ .describe('Local filesystem path to read and upload. stdio (local) mode ' +
439
+ 'only — the MCP server reads and base64-encodes it. In HTTP ' +
440
+ 'mode use `handle` or `content` instead.'),
441
+ name: z
442
+ .string()
443
+ .optional()
444
+ .describe('Filename reported to the page. Defaults to the basename of ' +
445
+ '`path`, else "file".'),
446
+ mimeType: z
447
+ .string()
448
+ .optional()
449
+ .describe('MIME type; inferred from the extension when omitted.'),
450
+ })
451
+ .refine((f) => [f.content, f.handle, f.path].filter((s) => s !== undefined)
452
+ .length === 1, {
453
+ message: 'Provide exactly one of "content", "handle", or "path" per file.',
454
+ }))
455
+ .min(1)
456
+ .describe('Files to attach. Combined decoded size is capped (server default ' +
457
+ '10MB, hard max 50MB).'),
458
+ }),
459
+ });
460
+ const GetDownloadsCommandSchema = z.object({
461
+ method: z.literal('getDownloads'),
462
+ params: z.object({}).optional().default({}),
463
+ });
395
464
  const CloseCommandSchema = z.object({
396
465
  method: z.literal('close'),
397
466
  params: z.object({}).optional().default({}),
@@ -410,6 +479,7 @@ const specificCommandSchemas = [
410
479
  CloseTabCommandSchema,
411
480
  ClickCommandSchema,
412
481
  TypeCommandSchema,
482
+ LoadSecretCommandSchema,
413
483
  SelectCommandSchema,
414
484
  CheckboxCommandSchema,
415
485
  HoverCommandSchema,
@@ -425,6 +495,8 @@ const specificCommandSchemas = [
425
495
  LiveURLCommandSchema,
426
496
  SolveCommandSchema,
427
497
  ScreenshotCommandSchema,
498
+ UploadFileCommandSchema,
499
+ GetDownloadsCommandSchema,
428
500
  CloseCommandSchema,
429
501
  ];
430
502
  const KNOWN_METHODS = new Set(specificCommandSchemas.map((schema) => schema.shape.method.value));
@@ -446,7 +518,50 @@ export const AgentCommandSchema = z.union([
446
518
  z.discriminatedUnion('method', specificCommandSchemas),
447
519
  GenericCommandSchema,
448
520
  ]);
449
- export const AgentParamsSchema = z.object({
521
+ // Proxy block for a profile-creation session. Mirrors the POST /profile body
522
+ // proxy shape (type/sticky/country/city/state/preset) so it passes straight
523
+ // through — distinct from the top-level agent proxy fields (proxy/proxyCountry…).
524
+ const CreateProfileProxySchema = z.object({
525
+ type: z
526
+ .literal('residential')
527
+ .optional()
528
+ .describe('Routing tier. Only "residential" is supported today.'),
529
+ sticky: z
530
+ .boolean()
531
+ .optional()
532
+ .describe('Keep the same IP for the lifetime of the creation session.'),
533
+ country: z
534
+ .string()
535
+ .optional()
536
+ .describe('Two-letter country code (e.g. "us").'),
537
+ city: z.string().optional().describe('City-level targeting (plan-gated).'),
538
+ state: z.string().optional().describe('State/region targeting (plan-gated).'),
539
+ preset: z
540
+ .string()
541
+ .optional()
542
+ .describe('Named proxy preset (plan-dependent).'),
543
+ });
544
+ const CreateProfileSchema = z
545
+ .object({
546
+ name: z
547
+ .string()
548
+ .min(1)
549
+ .max(255)
550
+ .refine((s) => /^[^\s/?#]+$/.test(s), {
551
+ message: 'name must match /^[^\\s/?#]+$/ (no whitespace, /, ?, #)',
552
+ })
553
+ .describe('Name to save the profile under. Reused as the saveProfile name.'),
554
+ proxy: CreateProfileProxySchema.optional(),
555
+ browser: z.enum(['chrome', 'chromium', 'stealth']).optional(),
556
+ stealth: z.boolean().optional(),
557
+ })
558
+ .describe('Open this session in profile-creation mode. The MCP tool POSTs /profile ' +
559
+ 'with these params, attaches the agent WS to the returned creation session ' +
560
+ '(non-headless, 10-minute keepalive), and expects a saveProfile call before ' +
561
+ 'close. Mutually exclusive with `profile`. Load the `auth-profile` skill ' +
562
+ '(via browserless_skill) for the full create-then-save recipe.');
563
+ export const AgentParamsSchema = z
564
+ .object({
450
565
  method: z
451
566
  .string()
452
567
  .optional()
@@ -467,8 +582,12 @@ export const AgentParamsSchema = z.object({
467
582
  'type email + type password + click submit). Do NOT batch across navigations.'),
468
583
  proxy: ProxyOptionsSchema.optional().describe('Residential / external proxy config. Read once at session creation. ' +
469
584
  'Changing requires close() + a new session call.'),
470
- profile: profileField('when the agent session connects', ' The profile is fixed for the lifetime of the agent session; ' +
471
- 'passing a different profile value opens a separate browser session.'),
585
+ profile: profileField('when the agent session connects', ' `profile` binds each call to its hydrated session you MUST pass it on ' +
586
+ 'every call in a multi-call flow, not just the first. A call that omits ' +
587
+ '`profile` runs in the default, un-hydrated session and will look logged ' +
588
+ 'out; if that happens, re-issue the call WITH `profile` before concluding ' +
589
+ 'the session expired. A different `profile` value opens a separate session.'),
590
+ createProfile: CreateProfileSchema.optional(),
472
591
  rationale: z
473
592
  .string()
474
593
  .optional()
@@ -484,4 +603,8 @@ export const AgentParamsSchema = z.object({
484
603
  'jargon, raw method names ("evaluate", "click"), JS, full URLs, or ' +
485
604
  'credentials. Include exactly one per `browserless_agent` call, even ' +
486
605
  'when batching commands.'),
606
+ })
607
+ .refine((v) => !(v.profile && v.createProfile), {
608
+ message: '`profile` (hydrate an existing profile) and `createProfile` (author a new ' +
609
+ 'one) cannot both be set',
487
610
  });
@@ -47,9 +47,10 @@ export function registerSmartScraperTool(server, config, analytics) {
47
47
  const cache = new ResponseCache(config.cacheTtlMs);
48
48
  defineTool(server, config, analytics, {
49
49
  name: 'browserless_smartscraper',
50
- description: 'Scrape any webpage using the Browserless smart scraper. ' +
51
- 'Returns page content in requested formats (markdown, html, screenshot, pdf, links). ' +
52
- 'Handles JavaScript-heavy pages, anti-bot measures, and multiple scraping strategies automatically.',
50
+ description: 'Scrape a SINGLE webpage and return its content as markdown or HTML. ' +
51
+ 'Handles JavaScript-heavy pages and anti-bot measures automatically. ' +
52
+ 'For content across MULTIPLE pages of a site, use browserless_crawl; ' +
53
+ "to list a site's URLs, use browserless_map.",
53
54
  parameters: SmartScraperParamsSchema,
54
55
  annotations: {
55
56
  title: 'Browserless Smart Scraper',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserless.io/mcp",
3
- "version": "1.6.2",
3
+ "version": "1.7.1",
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",
@@ -84,7 +84,7 @@
84
84
  "@types/chai": "^5.2.2",
85
85
  "@types/ioredis-mock": "^8.2.7",
86
86
  "@types/mocha": "^10.0.10",
87
- "@types/node": "^25.9.1",
87
+ "@types/node": "^26.0.0",
88
88
  "@types/sinon": "^21.0.1",
89
89
  "@types/ws": "^8.18.1",
90
90
  "c8": "^11.0.0",
@@ -108,6 +108,8 @@
108
108
  "minimatch": "^10.2.1",
109
109
  "glob": "^11.0.0",
110
110
  "serialize-javascript": "^7.0.5"
111
- }
111
+ },
112
+ "hono": "^4.12.26",
113
+ "undici": "^7.28.0"
112
114
  }
113
115
  }
@@ -1,11 +0,0 @@
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 DownloadParamsSchema: z.ZodObject<{
6
- code: z.ZodString;
7
- context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
8
- timeout: z.ZodOptional<z.ZodNumber>;
9
- profile: z.ZodOptional<z.ZodString>;
10
- }, z.core.$strip>;
11
- export declare function registerDownloadTool(server: FastMCP, config: McpConfig, analytics?: AnalyticsHelper): void;
@@ -1,92 +0,0 @@
1
- import { UserError } from 'fastmcp';
2
- import { z } from 'zod';
3
- import { defineTool } from '../lib/define-tool.js';
4
- import { profileField } from './schemas.js';
5
- export const DownloadParamsSchema = z.object({
6
- code: z
7
- .string()
8
- .describe('JavaScript (ESM) code to execute. The default export receives ' +
9
- '{ page, context }. During execution the code should trigger a ' +
10
- 'file download in the browser (e.g. clicking a download link).'),
11
- context: z
12
- .record(z.string(), z.unknown())
13
- .optional()
14
- .describe('Optional context object passed to the function.'),
15
- timeout: z
16
- .number()
17
- .int()
18
- .positive()
19
- .optional()
20
- .describe('Request timeout in milliseconds'),
21
- profile: profileField('before the download script runs'),
22
- });
23
- export function registerDownloadTool(server, config, analytics) {
24
- defineTool(server, config, analytics, {
25
- name: 'browserless_download',
26
- description: 'Run custom Puppeteer code on Browserless and return the file that ' +
27
- 'Chrome downloads during execution. Your code should trigger a file ' +
28
- 'download (e.g. clicking a download link). The downloaded file is ' +
29
- 'returned with its original Content-Type. Useful for downloading ' +
30
- 'CSVs, PDFs, images, or any file from a website.',
31
- parameters: DownloadParamsSchema,
32
- annotations: {
33
- title: 'Browserless Download',
34
- readOnlyHint: false,
35
- destructiveHint: true,
36
- openWorldHint: true,
37
- },
38
- profileNotFoundMessage: (profile) => `Profile "${profile}" was not found for the configured API ` +
39
- `token. Create the profile with Browserless.saveProfile in a ` +
40
- `live session first, or omit the profile parameter to run the ` +
41
- `download anonymously.`,
42
- run: async ({ client, params, log }) => {
43
- const response = await client.download({
44
- code: params.code,
45
- context: params.context,
46
- timeout: params.timeout,
47
- profile: params.profile,
48
- });
49
- log.debug(`Download response: ok=${response.ok}, status=${response.statusCode}, ` +
50
- `contentType=${response.contentType}, size=${response.size}, ` +
51
- `disposition=${response.contentDisposition}`);
52
- return response;
53
- },
54
- analyticsProps: (params, result) => ({
55
- ok: result.ok,
56
- status_code: result.statusCode,
57
- content_type: result.contentType,
58
- size: result.size,
59
- profile_used: !!params.profile,
60
- }),
61
- format: (response) => {
62
- if (!response.ok) {
63
- throw new UserError(`Download failed (status ${response.statusCode}): ${response.data.slice(0, 500)}`);
64
- }
65
- const filenameMatch = response.contentDisposition?.match(/filename[^;=\n]*=["']?([^"';\n]*)["']?/);
66
- const filename = filenameMatch?.[1] ?? 'downloaded-file';
67
- const blocks = [];
68
- if (response.isBinary) {
69
- blocks.push({
70
- type: 'text',
71
- text: `[Downloaded file: "${filename}" – ${response.contentType}, ` +
72
- `${response.size} bytes, base64-encoded]\n${response.data}`,
73
- });
74
- }
75
- else {
76
- blocks.push({ type: 'text', text: response.data });
77
- }
78
- blocks.push({
79
- type: 'text',
80
- text: [
81
- '---',
82
- `Filename: ${filename}`,
83
- `Content-Type: ${response.contentType}`,
84
- `Status: ${response.statusCode}`,
85
- `Size: ${response.size} bytes`,
86
- '---',
87
- ].join('\n'),
88
- });
89
- return blocks;
90
- },
91
- });
92
- }