@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,125 @@
1
+ const RECOVERY = {
2
+ SELECTOR_MISS: 'Re-snapshot — the element is not in the current DOM. If you have not tried it yet, retry with a deep selector "< selector" in case the element is inside a shadow root.',
3
+ SESSION_LOST: 'A fresh session was opened automatically. Re-run goto then snapshot — page state from before the failure is gone.',
4
+ UNAUTHORIZED: 'The server returned 401. Authentication is missing or invalid; the page is not reachable from this session. Do not retry the prior selector.',
5
+ FORBIDDEN: 'The server returned 403. Cookies/auth may be missing or invalid, or the resource is geo/IP-blocked. Do not retry the prior selector.',
6
+ NOT_FOUND: 'The server returned 404. The URL no longer exists; pick a different navigation target.',
7
+ SERVER_ERROR: 'The origin returned a 5xx error. Back off briefly, then retry once. If it persists, choose a different path.',
8
+ NAVIGATION_FAILED: 'A network/DNS error prevented navigation. Verify the URL is correct and reachable.',
9
+ TIMEOUT: 'The page or wait condition did not resolve in time. Try a longer waitFor, a different signal (waitForResponse with a known URL), or re-snapshot to confirm current state.',
10
+ INVALID_PARAMS: 'The parameters were rejected. The schema is authoritative — fix the params; do not blind-retry.',
11
+ UNKNOWN: 'Re-snapshot and re-plan from the current page state.',
12
+ };
13
+ const FATAL_SESSION_CODES = new Set(['BROWSER_CRASHED']);
14
+ const NAVIGATION_FAIL_PATTERNS = [
15
+ /net::ERR_/i,
16
+ /\bECONNREFUSED\b/,
17
+ /\bENOTFOUND\b/,
18
+ /\bEAI_AGAIN\b/,
19
+ /\bECONNRESET\b/,
20
+ /navigation aborted/i,
21
+ /failed to navigate/i,
22
+ ];
23
+ const WS_LOSS_PATTERNS = [
24
+ /WebSocket closed/i,
25
+ /WebSocket connection failed/i,
26
+ /Agent WebSocket connection failed/i,
27
+ ];
28
+ const TIMEOUT_PATTERNS = [/timed out/i, /\btimeout\b/i];
29
+ const extractStatus = (err) => {
30
+ if (typeof err.status === 'number') {
31
+ return err.status;
32
+ }
33
+ const match = err.message?.match(/\b(401|403|404|5\d\d)\b/);
34
+ if (match)
35
+ return Number(match[1]);
36
+ return undefined;
37
+ };
38
+ const fromStatus = (status) => {
39
+ if (status === 401)
40
+ return 'UNAUTHORIZED';
41
+ if (status === 403)
42
+ return 'FORBIDDEN';
43
+ if (status === 404)
44
+ return 'NOT_FOUND';
45
+ if (status >= 500 && status <= 599)
46
+ return 'SERVER_ERROR';
47
+ return undefined;
48
+ };
49
+ const INVALID_PARAMS_PATTERNS = [
50
+ /\bInvalid parameters?\b/i,
51
+ /\bFailed to deserialize\b/i,
52
+ ];
53
+ export const classifyAgentError = (input) => {
54
+ const { err, cmd } = input;
55
+ const code = err.code;
56
+ const message = err.message ?? '';
57
+ // waitForSelector failures are timeouts in intent: the agent reports
58
+ // SELECTOR_NOT_FOUND, but the user asked to wait, so the actionable signal
59
+ // is "the wait expired", not "the element is missing right now".
60
+ if (cmd?.method === 'waitForSelector' && code === 'SELECTOR_NOT_FOUND') {
61
+ return { category: 'TIMEOUT', code, recovery: RECOVERY.TIMEOUT };
62
+ }
63
+ if (code === 'SELECTOR_NOT_FOUND') {
64
+ return {
65
+ category: 'SELECTOR_MISS',
66
+ code,
67
+ recovery: RECOVERY.SELECTOR_MISS,
68
+ };
69
+ }
70
+ // Authoritative upstream codes win first.
71
+ if (code === 'INVALID_PARAMS') {
72
+ return {
73
+ category: 'INVALID_PARAMS',
74
+ code,
75
+ recovery: RECOVERY.INVALID_PARAMS,
76
+ };
77
+ }
78
+ if (code && FATAL_SESSION_CODES.has(code)) {
79
+ return { category: 'SESSION_LOST', code, recovery: RECOVERY.SESSION_LOST };
80
+ }
81
+ // HTTP status before the INVALID_PARAMS *message-pattern* heuristic so a
82
+ // message that happens to mention 4xx/5xx isn't swallowed by it.
83
+ const status = extractStatus(err);
84
+ if (status !== undefined) {
85
+ const fromCode = fromStatus(status);
86
+ if (fromCode) {
87
+ return {
88
+ category: fromCode,
89
+ code,
90
+ status,
91
+ recovery: RECOVERY[fromCode],
92
+ };
93
+ }
94
+ }
95
+ if (INVALID_PARAMS_PATTERNS.some((re) => re.test(message))) {
96
+ return {
97
+ category: 'INVALID_PARAMS',
98
+ code,
99
+ recovery: RECOVERY.INVALID_PARAMS,
100
+ };
101
+ }
102
+ const isTimeout = TIMEOUT_PATTERNS.some((re) => re.test(message));
103
+ if (/Agent WebSocket connection timed out/i.test(message)) {
104
+ return { category: 'TIMEOUT', code, recovery: RECOVERY.TIMEOUT };
105
+ }
106
+ if (WS_LOSS_PATTERNS.some((re) => re.test(message))) {
107
+ return { category: 'SESSION_LOST', code, recovery: RECOVERY.SESSION_LOST };
108
+ }
109
+ if (NAVIGATION_FAIL_PATTERNS.some((re) => re.test(message))) {
110
+ return {
111
+ category: 'NAVIGATION_FAILED',
112
+ code,
113
+ recovery: RECOVERY.NAVIGATION_FAILED,
114
+ };
115
+ }
116
+ if (isTimeout) {
117
+ return { category: 'TIMEOUT', code, recovery: RECOVERY.TIMEOUT };
118
+ }
119
+ return { category: 'UNKNOWN', code, recovery: RECOVERY.UNKNOWN };
120
+ };
121
+ export const formatClassifiedError = (classified, bodyLines) => {
122
+ const parts = [`Category: ${classified.category}`, ...bodyLines];
123
+ parts.push(`Recovery: ${classified.recovery}`);
124
+ return parts.join('\n\n');
125
+ };
@@ -0,0 +1,13 @@
1
+ import { Redis } from 'ioredis';
2
+ import { OAuthProxy, type AuthorizationParams, type DCRRequest, type DCRResponse, type OAuthProxyConfig, type TokenRequest, type TokenResponse } from 'fastmcp/auth';
3
+ export declare class RedisOAuthProxy extends OAuthProxy {
4
+ private redis;
5
+ constructor(config: OAuthProxyConfig, redis: Redis);
6
+ private get _internal();
7
+ registerClient(request: DCRRequest): Promise<DCRResponse>;
8
+ private isClientRegistered;
9
+ authorize(params: AuthorizationParams): Promise<Response>;
10
+ handleCallback(request: Request): Promise<Response>;
11
+ exchangeAuthorizationCode(request: TokenRequest): Promise<TokenResponse>;
12
+ destroy(): void;
13
+ }
@@ -0,0 +1,214 @@
1
+ import { OAuthProxy, OAuthProxyError, } from 'fastmcp/auth';
2
+ /**
3
+ * Redis-backed OAuthProxy using Redis as the single source of truth for OAuth
4
+ * flow state (transactions, authorization codes, DCRs), so the steps of one
5
+ * flow can land on different instances behind a load balancer.
6
+ *
7
+ * fastmcp v4 (CWE-601 hardening) made DCR state load-bearing for authorize(),
8
+ * handleCallback(), and handleConsent(), all checking a process-local Map.
9
+ * That breaks multi-instance (DCR on A, authorize on B → B rejects a valid
10
+ * redirect_uri), so the overrides here validate against Redis instead and
11
+ * ignore the parent's Map (super.registerClient still fills it; we never read it).
12
+ *
13
+ * Consent is NOT supported — the parent's handleConsent uses a process-local
14
+ * Map; the constructor throws if it's enabled. Requires Redis 6.2+ / Valkey 7+
15
+ * for GETDEL (atomic one-time-use of authorization codes across instances).
16
+ */
17
+ const KEY_PREFIX = 'mcp:oauth:';
18
+ const TX_PREFIX = `${KEY_PREFIX}tx:`;
19
+ const CODE_PREFIX = `${KEY_PREFIX}code:`;
20
+ const CLIENT_PREFIX = `${KEY_PREFIX}client:`;
21
+ const DEFAULT_TRANSACTION_TTL = 600;
22
+ const DEFAULT_CODE_TTL = 300;
23
+ const DEFAULT_CLIENT_TTL = 3600;
24
+ const DATE_FIELDS = new Set(['createdAt', 'expiresAt', 'issuedAt']);
25
+ function serialize(obj) {
26
+ return JSON.stringify(obj, (_key, value) => {
27
+ if (value instanceof Date)
28
+ return { __date: value.toISOString() };
29
+ return value;
30
+ });
31
+ }
32
+ function deserialize(json) {
33
+ return JSON.parse(json, (key, value) => {
34
+ if (value && typeof value === 'object' && '__date' in value) {
35
+ return new Date(value.__date);
36
+ }
37
+ if (DATE_FIELDS.has(key) && typeof value === 'string') {
38
+ return new Date(value);
39
+ }
40
+ return value;
41
+ });
42
+ }
43
+ export class RedisOAuthProxy extends OAuthProxy {
44
+ redis;
45
+ constructor(config, redis) {
46
+ super(config);
47
+ this.redis = redis;
48
+ // Our authorize() override short-circuits the parent's consent branch,
49
+ // and the parent's handleConsent reads transactions from a process-local
50
+ // Map which breaks in multi-instance. Fail fast if a caller opts in.
51
+ if (this.config.consentRequired) {
52
+ throw new Error('RedisOAuthProxy requires consentRequired: false — consent flow is not supported in multi-instance mode');
53
+ }
54
+ }
55
+ get _internal() {
56
+ return this;
57
+ }
58
+ async registerClient(request) {
59
+ // Delegate validation/response to the parent, then mirror the accepted
60
+ // URIs into Redis so every instance can honor the v4 redirect_uri check.
61
+ // (The parent's in-memory Map is also populated but we never read it.)
62
+ const response = await super.registerClient(request);
63
+ const ttl = this._internal.config.clientRegistrationTtl ?? DEFAULT_CLIENT_TTL;
64
+ // Snapshot pre-existence so rollback doesn't DEL a valid prior
65
+ // registration of the same URI (two DCR calls sharing a redirect_uri).
66
+ // allSettled → a probe failure is fail-fast with no writes attempted.
67
+ const probes = await Promise.allSettled(response.redirect_uris.map(async (uri) => ({
68
+ uri,
69
+ existed: (await this.redis.exists(`${CLIENT_PREFIX}${uri}`)) > 0,
70
+ })));
71
+ const probeFailed = probes.find((p) => p.status === 'rejected');
72
+ if (probeFailed) {
73
+ throw probeFailed.reason;
74
+ }
75
+ const redisPreExisting = new Set(probes
76
+ .filter((p) => p.status === 'fulfilled' && p.value.existed)
77
+ .map((p) => p.value.uri));
78
+ const writes = await Promise.allSettled(response.redirect_uris.map((uri) => this.redis.set(`${CLIENT_PREFIX}${uri}`, '1', 'EX', ttl)));
79
+ const writeFailed = writes.find((w) => w.status === 'rejected');
80
+ if (writeFailed) {
81
+ // Best-effort cleanup of Redis keys this call introduced; if these
82
+ // deletes also fail the originating error still wins.
83
+ await Promise.allSettled(response.redirect_uris
84
+ .filter((uri) => !redisPreExisting.has(uri))
85
+ .map((uri) => this.redis.del(`${CLIENT_PREFIX}${uri}`)));
86
+ throw writeFailed.reason;
87
+ }
88
+ return response;
89
+ }
90
+ async isClientRegistered(uri) {
91
+ return (await this.redis.exists(`${CLIENT_PREFIX}${uri}`)) === 1;
92
+ }
93
+ async authorize(params) {
94
+ if (!params.client_id || !params.redirect_uri || !params.response_type) {
95
+ throw new OAuthProxyError('invalid_request', 'Missing required parameters');
96
+ }
97
+ if (params.response_type !== 'code') {
98
+ throw new OAuthProxyError('unsupported_response_type', "Only 'code' response type is supported");
99
+ }
100
+ // RFC 6749 §5.2 — reject any client_id other than the single upstream
101
+ // identity this proxy fronts. Ported from fastmcp v4 OAuthProxy.authorize.
102
+ if (params.client_id !== this._internal.config.upstreamClientId) {
103
+ throw new OAuthProxyError('invalid_client', 'Unknown client_id');
104
+ }
105
+ // RFC 6749 §3.1.2.3 / RFC 6819 §4.1.5 — redirect_uri must be one
106
+ // previously registered via DCR; skipping this is CWE-601 (auth-code
107
+ // theft). We read the shared Redis registry so cross-instance DCR counts.
108
+ if (!(await this.isClientRegistered(params.redirect_uri))) {
109
+ throw new OAuthProxyError('invalid_request', 'redirect_uri is not registered for this client');
110
+ }
111
+ if (params.code_challenge && !params.code_challenge_method) {
112
+ throw new OAuthProxyError('invalid_request', 'code_challenge_method required when code_challenge is present');
113
+ }
114
+ const transaction = await this._internal.createTransaction(params);
115
+ const ttl = this._internal.config.transactionTtl || DEFAULT_TRANSACTION_TTL;
116
+ await this.redis.set(`${TX_PREFIX}${transaction.id}`, serialize(transaction), 'EX', ttl);
117
+ return this._internal.redirectToUpstream(transaction);
118
+ }
119
+ async handleCallback(request) {
120
+ const url = new URL(request.url);
121
+ const code = url.searchParams.get('code');
122
+ const state = url.searchParams.get('state');
123
+ const error = url.searchParams.get('error');
124
+ if (error) {
125
+ throw new OAuthProxyError(error, url.searchParams.get('error_description') || undefined);
126
+ }
127
+ if (!code || !state) {
128
+ throw new OAuthProxyError('invalid_request', 'Missing code or state parameter');
129
+ }
130
+ const txJson = await this.redis.get(`${TX_PREFIX}${state}`);
131
+ if (!txJson) {
132
+ throw new OAuthProxyError('invalid_request', 'Invalid or expired state');
133
+ }
134
+ const transaction = deserialize(txJson);
135
+ // Defense-in-depth: reject if the transaction's stored callback URL is no
136
+ // longer registered. Guards against DCR revocation mid-flow and any path
137
+ // that could have persisted an unvalidated URI.
138
+ if (!(await this.isClientRegistered(transaction.clientCallbackUrl))) {
139
+ await this.redis.del(`${TX_PREFIX}${state}`);
140
+ throw new OAuthProxyError('invalid_request', 'Transaction callback URL is not registered');
141
+ }
142
+ const upstreamTokens = await this._internal.exchangeUpstreamCode(code, transaction);
143
+ // generateAuthorizationCode writes to the in-memory clientCodes Map.
144
+ // We read from it, persist to Redis, then clean up the Map entry.
145
+ const clientCode = this._internal.generateAuthorizationCode(transaction, upstreamTokens);
146
+ const codeData = this._internal.clientCodes.get(clientCode);
147
+ if (codeData) {
148
+ const codeTtl = this._internal.config.authorizationCodeTtl || DEFAULT_CODE_TTL;
149
+ await this.redis.set(`${CODE_PREFIX}${clientCode}`, serialize(codeData), 'EX', codeTtl);
150
+ this._internal.clientCodes.delete(clientCode);
151
+ }
152
+ // Remove consumed transaction
153
+ await this.redis.del(`${TX_PREFIX}${state}`);
154
+ const redirectUrl = new URL(transaction.clientCallbackUrl);
155
+ redirectUrl.searchParams.set('code', clientCode);
156
+ redirectUrl.searchParams.set('state', transaction.state);
157
+ return new Response(null, {
158
+ headers: { Location: redirectUrl.toString() },
159
+ status: 302,
160
+ });
161
+ }
162
+ async exchangeAuthorizationCode(request) {
163
+ if (request.grant_type !== 'authorization_code') {
164
+ throw new OAuthProxyError('unsupported_grant_type', 'Only authorization_code grant type is supported');
165
+ }
166
+ // RFC 6749 §5.2 — reject unknown clients at token exchange too, so a
167
+ // stolen authorization code cannot be redeemed by an arbitrary caller.
168
+ // Ported from fastmcp v4 OAuthProxy.exchangeAuthorizationCode.
169
+ if (request.client_id !== this._internal.config.upstreamClientId) {
170
+ throw new OAuthProxyError('invalid_client', 'Unknown client_id');
171
+ }
172
+ // Atomically read-and-delete the code. The parent's in-memory `used` flag
173
+ // can't work across instances (two concurrent redemptions on different
174
+ // instances both see it valid); GETDEL makes the consume race-free.
175
+ const codeJson = await this.redis.getdel(`${CODE_PREFIX}${request.code}`);
176
+ if (!codeJson) {
177
+ throw new OAuthProxyError('invalid_grant', 'Invalid or expired authorization code');
178
+ }
179
+ const clientCode = deserialize(codeJson);
180
+ if (clientCode.clientId !== request.client_id) {
181
+ throw new OAuthProxyError('invalid_client', 'Client ID mismatch');
182
+ }
183
+ if (clientCode.codeChallenge && !request.code_verifier) {
184
+ throw new OAuthProxyError('invalid_request', 'code_verifier required for PKCE');
185
+ }
186
+ if (clientCode.codeChallenge && request.code_verifier) {
187
+ // Delegate PKCE validation to parent by placing code in Map temporarily.
188
+ // Redis key is already consumed by GETDEL above, so no additional del
189
+ // is needed in finally — only the local Map cleanup.
190
+ this._internal.clientCodes.set(request.code, clientCode);
191
+ try {
192
+ return await super.exchangeAuthorizationCode(request);
193
+ }
194
+ finally {
195
+ this._internal.clientCodes.delete(request.code);
196
+ }
197
+ }
198
+ const response = {
199
+ access_token: clientCode.upstreamTokens.accessToken,
200
+ expires_in: clientCode.upstreamTokens.expiresIn,
201
+ token_type: clientCode.upstreamTokens.tokenType,
202
+ };
203
+ if (clientCode.upstreamTokens.refreshToken) {
204
+ response.refresh_token = clientCode.upstreamTokens.refreshToken;
205
+ }
206
+ if (clientCode.upstreamTokens.scope?.length > 0) {
207
+ response.scope = clientCode.upstreamTokens.scope.join(' ');
208
+ }
209
+ return response;
210
+ }
211
+ destroy() {
212
+ super.destroy();
213
+ }
214
+ }
@@ -0,0 +1,2 @@
1
+ import type { RetryOptions } from '../@types/types.js';
2
+ export declare function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
@@ -0,0 +1,19 @@
1
+ export async function retryWithBackoff(fn, options) {
2
+ const { maxRetries, baseDelayMs, shouldRetry } = options;
3
+ let lastError;
4
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
5
+ try {
6
+ return await fn();
7
+ }
8
+ catch (err) {
9
+ lastError = err;
10
+ if (attempt === maxRetries)
11
+ break;
12
+ if (shouldRetry && !shouldRetry(lastError))
13
+ break;
14
+ const delay = baseDelayMs * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5);
15
+ await new Promise((resolve) => setTimeout(resolve, delay));
16
+ }
17
+ }
18
+ throw lastError;
19
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Build the schema for an optional profile field. The NUL refinement protects
4
+ * the session-key separator used in agent-client.ts — a profile name
5
+ * containing NUL could otherwise collide with another key.
6
+ *
7
+ * Dependency-clean (zod only) so it can be shared by the server tools and the
8
+ * published `@browserless.io/mcp/schemas` surface without pulling in fastmcp.
9
+ */
10
+ export declare function profileField(whenLoaded: string, extra?: string): z.ZodOptional<z.ZodString>;
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ // NUL is the session-key separator (KEY_SEP) in agent-client.ts. Computed via
3
+ // fromCharCode so the literal control character never appears in source.
4
+ const NUL = String.fromCharCode(0);
5
+ /**
6
+ * Build the schema for an optional profile field. The NUL refinement protects
7
+ * the session-key separator used in agent-client.ts — a profile name
8
+ * containing NUL could otherwise collide with another key.
9
+ *
10
+ * Dependency-clean (zod only) so it can be shared by the server tools and the
11
+ * published `@browserless.io/mcp/schemas` surface without pulling in fastmcp.
12
+ */
13
+ export function profileField(whenLoaded, extra = '') {
14
+ const description = `Optional name of an authentication profile to hydrate into the browser ${whenLoaded}. ` +
15
+ "The profile's cookies, localStorage, and IndexedDB are restored into the session before the request runs. " +
16
+ 'The profile must already exist for the API token in use — create one with Browserless.saveProfile in a live agent session first.' +
17
+ extra;
18
+ return z
19
+ .string()
20
+ .trim()
21
+ .min(1)
22
+ .refine((v) => !v.includes(NUL), {
23
+ message: 'profile must not contain NUL characters',
24
+ })
25
+ .optional()
26
+ .describe(description);
27
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Patch `globalThis.fetch` to extend `expires_in` on Supabase OAuth token
3
+ * responses so clients don't thrash refresh against the ~60s default. Global
4
+ * because FastMCP's OAuthProxy has no fetch hook; matched origin/path-exact.
5
+ */
6
+ export declare function installSupabaseTokenTtlPatch(supabaseUrl: string, ttlSeconds: number): void;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Patch `globalThis.fetch` to extend `expires_in` on Supabase OAuth token
3
+ * responses so clients don't thrash refresh against the ~60s default. Global
4
+ * because FastMCP's OAuthProxy has no fetch hook; matched origin/path-exact.
5
+ */
6
+ export function installSupabaseTokenTtlPatch(supabaseUrl, ttlSeconds) {
7
+ const supabaseOrigin = new URL(supabaseUrl).origin;
8
+ const TOKEN_PATHNAME = '/auth/v1/oauth/token';
9
+ const originalFetch = globalThis.fetch;
10
+ globalThis.fetch = async (...args) => {
11
+ const response = await originalFetch(...args);
12
+ const url = typeof args[0] === 'string'
13
+ ? args[0]
14
+ : args[0] instanceof URL
15
+ ? args[0].toString()
16
+ : args[0].url;
17
+ const reqUrl = new URL(url);
18
+ if (!response.ok ||
19
+ reqUrl.origin !== supabaseOrigin ||
20
+ reqUrl.pathname !== TOKEN_PATHNAME) {
21
+ return response;
22
+ }
23
+ const body = (await response.json());
24
+ if (typeof body.expires_in === 'number' && body.expires_in < ttlSeconds) {
25
+ body.expires_in = ttlSeconds;
26
+ }
27
+ return new Response(JSON.stringify(body), {
28
+ status: response.status,
29
+ statusText: response.statusText,
30
+ headers: response.headers,
31
+ });
32
+ };
33
+ }
@@ -0,0 +1,27 @@
1
+ import type { SupabaseJwtPayload } from '../@types/types.js';
2
+ /**
3
+ * 64-bit truncation of SHA-256 — wide enough to make accidental collisions
4
+ * astronomically unlikely, unlike a 32-bit djb2. Used to fingerprint tokens,
5
+ * proxy configs, and other identifiers we want stable but unguessable.
6
+ */
7
+ export declare function hashToken(input: string): string;
8
+ /**
9
+ * djb2 string hash. Matches the Browserless backend's session ID hashing —
10
+ * kept compatible so analytics `user_id` values stay consistent across services.
11
+ */
12
+ export declare function djb2(str: string): number;
13
+ /** Content-Types that should be treated as text (not base64-encoded). */
14
+ export declare const TEXT_CONTENT_TYPES: string[];
15
+ export declare function isTextContentType(ct: string): boolean;
16
+ /** Strip undefined entries so JSON.stringify omits them from the wire body. */
17
+ export declare function compact<T extends Record<string, unknown>>(obj: T): Partial<T>;
18
+ /** Split a comma-separated env-var value into a trimmed, non-empty list. */
19
+ export declare function parseCsv(raw: string | undefined): string[];
20
+ /** Decode the payload segment of a JWT. Throws on malformed input. */
21
+ export declare function decodeJwtPayload(jwt: string): SupabaseJwtPayload;
22
+ /**
23
+ * Reject server bodies that are obviously not a real message — empty, just
24
+ * whitespace, or a literal `null`/`undefined` from a misbehaving JSON layer.
25
+ * Callers usually fall back to a canned message when this returns false.
26
+ */
27
+ export declare function isMeaningfulBody(s: string): boolean;
@@ -0,0 +1,67 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * 64-bit truncation of SHA-256 — wide enough to make accidental collisions
4
+ * astronomically unlikely, unlike a 32-bit djb2. Used to fingerprint tokens,
5
+ * proxy configs, and other identifiers we want stable but unguessable.
6
+ */
7
+ export function hashToken(input) {
8
+ return createHash('sha256').update(input).digest('hex').slice(0, 16);
9
+ }
10
+ /**
11
+ * djb2 string hash. Matches the Browserless backend's session ID hashing —
12
+ * kept compatible so analytics `user_id` values stay consistent across services.
13
+ */
14
+ export function djb2(str) {
15
+ let hash = 5381;
16
+ for (let i = 0; i < str.length; i++) {
17
+ hash = (hash * 33) ^ str.charCodeAt(i);
18
+ }
19
+ return hash >>> 0;
20
+ }
21
+ /** Content-Types that should be treated as text (not base64-encoded). */
22
+ export const TEXT_CONTENT_TYPES = [
23
+ 'text/',
24
+ 'application/json',
25
+ 'application/javascript',
26
+ 'application/xml',
27
+ 'application/xhtml+xml',
28
+ 'application/ld+json',
29
+ ];
30
+ export function isTextContentType(ct) {
31
+ const lower = ct.toLowerCase();
32
+ return TEXT_CONTENT_TYPES.some((prefix) => lower.includes(prefix));
33
+ }
34
+ /** Strip undefined entries so JSON.stringify omits them from the wire body. */
35
+ export function compact(obj) {
36
+ const out = {};
37
+ for (const [k, v] of Object.entries(obj)) {
38
+ if (v !== undefined)
39
+ out[k] = v;
40
+ }
41
+ return out;
42
+ }
43
+ /** Split a comma-separated env-var value into a trimmed, non-empty list. */
44
+ export function parseCsv(raw) {
45
+ return (raw ?? '')
46
+ .split(',')
47
+ .map((s) => s.trim())
48
+ .filter((s) => s.length > 0);
49
+ }
50
+ /** Decode the payload segment of a JWT. Throws on malformed input. */
51
+ export function decodeJwtPayload(jwt) {
52
+ const parts = jwt.split('.');
53
+ if (parts.length !== 3) {
54
+ throw new Error('Invalid JWT format');
55
+ }
56
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
57
+ return JSON.parse(payload);
58
+ }
59
+ /**
60
+ * Reject server bodies that are obviously not a real message — empty, just
61
+ * whitespace, or a literal `null`/`undefined` from a misbehaving JSON layer.
62
+ * Callers usually fall back to a canned message when this returns false.
63
+ */
64
+ export function isMeaningfulBody(s) {
65
+ const normalized = s.trim();
66
+ return normalized.length > 0 && !/^(?:null|undefined)$/i.test(normalized);
67
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastMCP } from 'fastmcp';
2
+ export declare function registerExtractContentPrompt(server: FastMCP): void;
@@ -0,0 +1,33 @@
1
+ export function registerExtractContentPrompt(server) {
2
+ server.addPrompt({
3
+ name: 'extract-content',
4
+ description: 'Extract specific information from a webpage using the smart scraper',
5
+ arguments: [
6
+ {
7
+ name: 'url',
8
+ description: 'The URL to extract content from',
9
+ required: true,
10
+ },
11
+ {
12
+ name: 'instructions',
13
+ description: 'What information to extract from the page',
14
+ required: true,
15
+ },
16
+ ],
17
+ load: async ({ url, instructions }) => {
18
+ return {
19
+ messages: [
20
+ {
21
+ role: 'user',
22
+ content: {
23
+ type: 'text',
24
+ text: `Use the browserless_smartscraper tool to scrape: ${url}\n` +
25
+ `Then extract the following information: ${instructions}\n` +
26
+ 'Return the extracted information in a structured format.',
27
+ },
28
+ },
29
+ ],
30
+ };
31
+ },
32
+ });
33
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastMCP } from 'fastmcp';
2
+ export declare function registerScrapeUrlPrompt(server: FastMCP): void;
@@ -0,0 +1,36 @@
1
+ export function registerScrapeUrlPrompt(server) {
2
+ server.addPrompt({
3
+ name: 'scrape-url',
4
+ description: 'Scrape a webpage and return its content as markdown with metadata',
5
+ arguments: [
6
+ {
7
+ name: 'url',
8
+ description: 'The URL to scrape',
9
+ required: true,
10
+ },
11
+ {
12
+ name: 'includeScreenshot',
13
+ description: 'Whether to capture a screenshot (true/false)',
14
+ required: false,
15
+ },
16
+ ],
17
+ load: async ({ url, includeScreenshot }) => {
18
+ const formats = includeScreenshot === 'true'
19
+ ? '["markdown", "screenshot"]'
20
+ : '["markdown"]';
21
+ return {
22
+ messages: [
23
+ {
24
+ role: 'user',
25
+ content: {
26
+ type: 'text',
27
+ text: `Use the browserless_smartscraper tool to scrape the following URL: ${url}\n` +
28
+ `Options: formats=${formats}\n` +
29
+ 'Return the markdown content and summarize the key information found on the page.',
30
+ },
31
+ },
32
+ ],
33
+ };
34
+ },
35
+ });
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { FastMCP } from 'fastmcp';
2
+ import type { McpConfig } from '../@types/types.js';
3
+ export declare function registerApiDocsResource(server: FastMCP, config: McpConfig): void;