firecrawl-mcp 3.22.2 → 3.22.3

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 (2) hide show
  1. package/dist/index.js +335 -1586
  2. package/package.json +11 -19
package/dist/index.js CHANGED
@@ -3,1327 +3,14 @@
3
3
  // src/index.ts
4
4
  import FirecrawlApp from "@mendable/firecrawl-js";
5
5
  import dotenv from "dotenv";
6
+ import { FastMCP } from "fastmcp";
6
7
  import { readFile } from "fs/promises";
7
8
  import { createRequire } from "module";
8
9
  import path from "path";
9
- import { z as z4 } from "zod";
10
-
11
- // src/fastmcp/FastMCP.ts
12
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
- import {
15
- CallToolRequestSchema,
16
- CompleteRequestSchema,
17
- ErrorCode,
18
- GetPromptRequestSchema,
19
- ListPromptsRequestSchema,
20
- ListResourcesRequestSchema,
21
- ListResourceTemplatesRequestSchema,
22
- ListToolsRequestSchema,
23
- McpError,
24
- ReadResourceRequestSchema,
25
- RootsListChangedNotificationSchema,
26
- SetLevelRequestSchema
27
- } from "@modelcontextprotocol/sdk/types.js";
28
- import { EventEmitter } from "events";
29
- import Fuse from "fuse.js";
30
- import { startHTTPServer } from "mcp-proxy";
31
- import { setTimeout as delay } from "timers/promises";
32
- import parseURITemplate from "uri-templates";
33
- import { toJsonSchema } from "xsschema";
34
- import { z } from "zod";
35
- var FastMCPError = class extends Error {
36
- constructor(message) {
37
- super(message);
38
- this.name = new.target.name;
39
- }
40
- };
41
- var UnexpectedStateError = class extends FastMCPError {
42
- extras;
43
- constructor(message, extras) {
44
- super(message);
45
- this.name = new.target.name;
46
- this.extras = extras;
47
- }
48
- };
49
- var UserError = class extends UnexpectedStateError {
50
- };
51
- var TextContentZodSchema = z.object({
52
- /**
53
- * The text content of the message.
54
- */
55
- text: z.string(),
56
- type: z.literal("text")
57
- }).strict();
58
- var ImageContentZodSchema = z.object({
59
- /**
60
- * The base64-encoded image data.
61
- */
62
- data: z.string().base64(),
63
- /**
64
- * The MIME type of the image. Different providers may support different image types.
65
- */
66
- mimeType: z.string(),
67
- type: z.literal("image")
68
- }).strict();
69
- var AudioContentZodSchema = z.object({
70
- /**
71
- * The base64-encoded audio data.
72
- */
73
- data: z.string().base64(),
74
- mimeType: z.string(),
75
- type: z.literal("audio")
76
- }).strict();
77
- var ResourceContentZodSchema = z.object({
78
- resource: z.object({
79
- blob: z.string().optional(),
80
- mimeType: z.string().optional(),
81
- text: z.string().optional(),
82
- uri: z.string()
83
- }),
84
- type: z.literal("resource")
85
- }).strict();
86
- var ResourceLinkZodSchema = z.object({
87
- description: z.string().optional(),
88
- mimeType: z.string().optional(),
89
- name: z.string(),
90
- title: z.string().optional(),
91
- type: z.literal("resource_link"),
92
- uri: z.string()
93
- });
94
- var ContentZodSchema = z.discriminatedUnion("type", [
95
- TextContentZodSchema,
96
- ImageContentZodSchema,
97
- AudioContentZodSchema,
98
- ResourceContentZodSchema,
99
- ResourceLinkZodSchema
100
- ]);
101
- var ContentResultZodSchema = z.object({
102
- content: ContentZodSchema.array(),
103
- isError: z.boolean().optional()
104
- }).strict();
105
- var CompletionZodSchema = z.object({
106
- /**
107
- * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.
108
- */
109
- hasMore: z.optional(z.boolean()),
110
- /**
111
- * The total number of completion options available. This can exceed the number of values actually sent in the response.
112
- */
113
- total: z.optional(z.number().int()),
114
- /**
115
- * An array of completion values. Must not exceed 100 items.
116
- */
117
- values: z.array(z.string()).max(100)
118
- });
119
- var FastMCPSessionEventEmitterBase = EventEmitter;
120
- var escapeWWWAuthenticateValue = (value) => {
121
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
122
- };
123
- var FastMCPSessionEventEmitter = class extends FastMCPSessionEventEmitterBase {
124
- };
125
- var FastMCPSession = class extends FastMCPSessionEventEmitter {
126
- get clientCapabilities() {
127
- return this.#clientCapabilities ?? null;
128
- }
129
- get isReady() {
130
- return this.#connectionState === "ready";
131
- }
132
- get loggingLevel() {
133
- return this.#loggingLevel;
134
- }
135
- get roots() {
136
- return this.#roots;
137
- }
138
- get server() {
139
- return this.#server;
140
- }
141
- #auth;
142
- #capabilities = {};
143
- #clientCapabilities;
144
- #connectionState = "connecting";
145
- #logger;
146
- #loggingLevel = "info";
147
- #needsEventLoopFlush = false;
148
- #pingConfig;
149
- #pingInterval = null;
150
- #prompts = [];
151
- #resources = [];
152
- #resourceTemplates = [];
153
- #roots = [];
154
- #rootsConfig;
155
- #server;
156
- #utils;
157
- constructor({
158
- auth,
159
- instructions,
160
- logger,
161
- name,
162
- ping,
163
- prompts,
164
- resources,
165
- resourcesTemplates,
166
- roots,
167
- tools,
168
- transportType,
169
- utils,
170
- version
171
- }) {
172
- super();
173
- this.#auth = auth;
174
- this.#logger = logger;
175
- this.#pingConfig = ping;
176
- this.#rootsConfig = roots;
177
- this.#needsEventLoopFlush = transportType === "httpStream";
178
- if (tools.length) {
179
- this.#capabilities.tools = {};
180
- }
181
- if (resources.length || resourcesTemplates.length) {
182
- this.#capabilities.resources = {};
183
- }
184
- if (prompts.length) {
185
- for (const prompt of prompts) {
186
- this.addPrompt(prompt);
187
- }
188
- this.#capabilities.prompts = {};
189
- }
190
- this.#capabilities.logging = {};
191
- this.#server = new Server(
192
- { name, version },
193
- { capabilities: this.#capabilities, instructions }
194
- );
195
- this.#utils = utils;
196
- this.setupErrorHandling();
197
- this.setupLoggingHandlers();
198
- this.setupRootsHandlers();
199
- this.setupCompleteHandlers();
200
- if (tools.length) {
201
- this.setupToolHandlers(tools);
202
- }
203
- if (resources.length || resourcesTemplates.length) {
204
- for (const resource of resources) {
205
- this.addResource(resource);
206
- }
207
- this.setupResourceHandlers(resources);
208
- if (resourcesTemplates.length) {
209
- for (const resourceTemplate of resourcesTemplates) {
210
- this.addResourceTemplate(resourceTemplate);
211
- }
212
- this.setupResourceTemplateHandlers(resourcesTemplates);
213
- }
214
- }
215
- if (prompts.length) {
216
- this.setupPromptHandlers(prompts);
217
- }
218
- }
219
- async close() {
220
- this.#connectionState = "closed";
221
- if (this.#pingInterval) {
222
- clearInterval(this.#pingInterval);
223
- }
224
- try {
225
- await this.#server.close();
226
- } catch (error) {
227
- this.#logger.error("[FastMCP error]", "could not close server", error);
228
- }
229
- }
230
- async connect(transport) {
231
- if (this.#server.transport) {
232
- throw new UnexpectedStateError("Server is already connected");
233
- }
234
- this.#connectionState = "connecting";
235
- try {
236
- await this.#server.connect(transport);
237
- let attempt = 0;
238
- const maxAttempts = 10;
239
- const retryDelay = 100;
240
- while (attempt++ < maxAttempts) {
241
- const capabilities = this.#server.getClientCapabilities();
242
- if (capabilities) {
243
- this.#clientCapabilities = capabilities;
244
- break;
245
- }
246
- await delay(retryDelay);
247
- }
248
- if (!this.#clientCapabilities) {
249
- this.#logger.warn(
250
- `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`
251
- );
252
- }
253
- if (this.#clientCapabilities?.roots?.listChanged && typeof this.#server.listRoots === "function") {
254
- try {
255
- const roots = await this.#server.listRoots();
256
- this.#roots = roots?.roots || [];
257
- } catch (e) {
258
- if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
259
- this.#logger.debug(
260
- "[FastMCP debug] listRoots method not supported by client"
261
- );
262
- } else {
263
- this.#logger.error(
264
- `[FastMCP error] received error listing roots.
265
-
266
- ${e instanceof Error ? e.stack : JSON.stringify(e)}`
267
- );
268
- }
269
- }
270
- }
271
- if (this.#clientCapabilities) {
272
- const pingConfig = this.#getPingConfig(transport);
273
- if (pingConfig.enabled) {
274
- this.#pingInterval = setInterval(async () => {
275
- try {
276
- await this.#server.ping();
277
- } catch {
278
- const logLevel = pingConfig.logLevel;
279
- if (logLevel === "debug") {
280
- this.#logger.debug("[FastMCP debug] server ping failed");
281
- } else if (logLevel === "warning") {
282
- this.#logger.warn(
283
- "[FastMCP warning] server is not responding to ping"
284
- );
285
- } else if (logLevel === "error") {
286
- this.#logger.error(
287
- "[FastMCP error] server is not responding to ping"
288
- );
289
- } else {
290
- this.#logger.info("[FastMCP info] server ping failed");
291
- }
292
- }
293
- }, pingConfig.intervalMs);
294
- }
295
- }
296
- this.#connectionState = "ready";
297
- this.emit("ready");
298
- } catch (error) {
299
- this.#connectionState = "error";
300
- const errorEvent = {
301
- error: error instanceof Error ? error : new Error(String(error))
302
- };
303
- this.emit("error", errorEvent);
304
- throw error;
305
- }
306
- }
307
- async requestSampling(message, options) {
308
- return this.#server.createMessage(message, options);
309
- }
310
- waitForReady() {
311
- if (this.isReady) {
312
- return Promise.resolve();
313
- }
314
- if (this.#connectionState === "error" || this.#connectionState === "closed") {
315
- return Promise.reject(
316
- new Error(`Connection is in ${this.#connectionState} state`)
317
- );
318
- }
319
- return new Promise((resolve, reject) => {
320
- const timeout = setTimeout(() => {
321
- reject(
322
- new Error(
323
- "Connection timeout: Session failed to become ready within 5 seconds"
324
- )
325
- );
326
- }, 5e3);
327
- this.once("ready", () => {
328
- clearTimeout(timeout);
329
- resolve();
330
- });
331
- this.once("error", (event) => {
332
- clearTimeout(timeout);
333
- reject(event.error);
334
- });
335
- });
336
- }
337
- #getPingConfig(transport) {
338
- const pingConfig = this.#pingConfig || {};
339
- let defaultEnabled = false;
340
- if ("type" in transport) {
341
- if (transport.type === "httpStream") {
342
- defaultEnabled = true;
343
- }
344
- }
345
- return {
346
- enabled: pingConfig.enabled !== void 0 ? pingConfig.enabled : defaultEnabled,
347
- intervalMs: pingConfig.intervalMs || 5e3,
348
- logLevel: pingConfig.logLevel || "debug"
349
- };
350
- }
351
- addPrompt(inputPrompt) {
352
- const completers = {};
353
- const enums = {};
354
- const fuseInstances = {};
355
- for (const argument of inputPrompt.arguments ?? []) {
356
- if (argument.complete) {
357
- completers[argument.name] = argument.complete;
358
- }
359
- if (argument.enum) {
360
- enums[argument.name] = argument.enum;
361
- fuseInstances[argument.name] = new Fuse(argument.enum, {
362
- includeScore: true,
363
- threshold: 0.3
364
- // More flexible matching!
365
- });
366
- }
367
- }
368
- const prompt = {
369
- ...inputPrompt,
370
- complete: async (name, value, auth) => {
371
- if (completers[name]) {
372
- return await completers[name](value, auth);
373
- }
374
- if (fuseInstances[name]) {
375
- const result = fuseInstances[name].search(value);
376
- return {
377
- total: result.length,
378
- values: result.map((item) => item.item)
379
- };
380
- }
381
- return {
382
- values: []
383
- };
384
- }
385
- };
386
- this.#prompts.push(prompt);
387
- }
388
- addResource(inputResource) {
389
- this.#resources.push(inputResource);
390
- }
391
- addResourceTemplate(inputResourceTemplate) {
392
- const completers = {};
393
- for (const argument of inputResourceTemplate.arguments ?? []) {
394
- if (argument.complete) {
395
- completers[argument.name] = argument.complete;
396
- }
397
- }
398
- const resourceTemplate = {
399
- ...inputResourceTemplate,
400
- complete: async (name, value, auth) => {
401
- if (completers[name]) {
402
- return await completers[name](value, auth);
403
- }
404
- return {
405
- values: []
406
- };
407
- }
408
- };
409
- this.#resourceTemplates.push(resourceTemplate);
410
- }
411
- setupCompleteHandlers() {
412
- this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {
413
- if (request.params.ref.type === "ref/prompt") {
414
- const prompt = this.#prompts.find(
415
- (prompt2) => prompt2.name === request.params.ref.name
416
- );
417
- if (!prompt) {
418
- throw new UnexpectedStateError("Unknown prompt", {
419
- request
420
- });
421
- }
422
- if (!prompt.complete) {
423
- throw new UnexpectedStateError("Prompt does not support completion", {
424
- request
425
- });
426
- }
427
- const completion = CompletionZodSchema.parse(
428
- await prompt.complete(
429
- request.params.argument.name,
430
- request.params.argument.value,
431
- this.#auth
432
- )
433
- );
434
- return {
435
- completion
436
- };
437
- }
438
- if (request.params.ref.type === "ref/resource") {
439
- const resource = this.#resourceTemplates.find(
440
- (resource2) => resource2.uriTemplate === request.params.ref.uri
441
- );
442
- if (!resource) {
443
- throw new UnexpectedStateError("Unknown resource", {
444
- request
445
- });
446
- }
447
- if (!("uriTemplate" in resource)) {
448
- throw new UnexpectedStateError("Unexpected resource");
449
- }
450
- if (!resource.complete) {
451
- throw new UnexpectedStateError(
452
- "Resource does not support completion",
453
- {
454
- request
455
- }
456
- );
457
- }
458
- const completion = CompletionZodSchema.parse(
459
- await resource.complete(
460
- request.params.argument.name,
461
- request.params.argument.value,
462
- this.#auth
463
- )
464
- );
465
- return {
466
- completion
467
- };
468
- }
469
- throw new UnexpectedStateError("Unexpected completion request", {
470
- request
471
- });
472
- });
473
- }
474
- setupErrorHandling() {
475
- this.#server.onerror = (error) => {
476
- this.#logger.error("[FastMCP error]", error);
477
- };
478
- }
479
- setupLoggingHandlers() {
480
- this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {
481
- this.#loggingLevel = request.params.level;
482
- return {};
483
- });
484
- }
485
- setupPromptHandlers(prompts) {
486
- this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {
487
- return {
488
- prompts: prompts.map((prompt) => {
489
- return {
490
- arguments: prompt.arguments,
491
- complete: prompt.complete,
492
- description: prompt.description,
493
- name: prompt.name
494
- };
495
- })
496
- };
497
- });
498
- this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {
499
- const prompt = prompts.find(
500
- (prompt2) => prompt2.name === request.params.name
501
- );
502
- if (!prompt) {
503
- throw new McpError(
504
- ErrorCode.MethodNotFound,
505
- `Unknown prompt: ${request.params.name}`
506
- );
507
- }
508
- const args2 = request.params.arguments;
509
- for (const arg of prompt.arguments ?? []) {
510
- if (arg.required && !(args2 && arg.name in args2)) {
511
- throw new McpError(
512
- ErrorCode.InvalidRequest,
513
- `Prompt '${request.params.name}' requires argument '${arg.name}': ${arg.description || "No description provided"}`
514
- );
515
- }
516
- }
517
- let result;
518
- try {
519
- result = await prompt.load(
520
- args2,
521
- this.#auth
522
- );
523
- } catch (error) {
524
- const errorMessage = error instanceof Error ? error.message : String(error);
525
- throw new McpError(
526
- ErrorCode.InternalError,
527
- `Failed to load prompt '${request.params.name}': ${errorMessage}`
528
- );
529
- }
530
- if (typeof result === "string") {
531
- return {
532
- description: prompt.description,
533
- messages: [
534
- {
535
- content: { text: result, type: "text" },
536
- role: "user"
537
- }
538
- ]
539
- };
540
- } else {
541
- return {
542
- description: prompt.description,
543
- messages: result.messages
544
- };
545
- }
546
- });
547
- }
548
- setupResourceHandlers(resources) {
549
- this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {
550
- return {
551
- resources: resources.map((resource) => ({
552
- description: resource.description,
553
- mimeType: resource.mimeType,
554
- name: resource.name,
555
- uri: resource.uri
556
- }))
557
- };
558
- });
559
- this.#server.setRequestHandler(
560
- ReadResourceRequestSchema,
561
- async (request) => {
562
- if ("uri" in request.params) {
563
- const resource = resources.find(
564
- (resource2) => "uri" in resource2 && resource2.uri === request.params.uri
565
- );
566
- if (!resource) {
567
- for (const resourceTemplate of this.#resourceTemplates) {
568
- const uriTemplate = parseURITemplate(
569
- resourceTemplate.uriTemplate
570
- );
571
- const match = uriTemplate.fromUri(request.params.uri);
572
- if (!match) {
573
- continue;
574
- }
575
- const uri = uriTemplate.fill(match);
576
- const result = await resourceTemplate.load(match, this.#auth);
577
- const resources2 = Array.isArray(result) ? result : [result];
578
- return {
579
- contents: resources2.map((resource2) => ({
580
- ...resource2,
581
- description: resourceTemplate.description,
582
- mimeType: resource2.mimeType ?? resourceTemplate.mimeType,
583
- name: resourceTemplate.name,
584
- uri: resource2.uri ?? uri
585
- }))
586
- };
587
- }
588
- throw new McpError(
589
- ErrorCode.MethodNotFound,
590
- `Resource not found: '${request.params.uri}'. Available resources: ${resources.map((r) => r.uri).join(", ") || "none"}`
591
- );
592
- }
593
- if (!("uri" in resource)) {
594
- throw new UnexpectedStateError("Resource does not support reading");
595
- }
596
- let maybeArrayResult;
597
- try {
598
- maybeArrayResult = await resource.load(this.#auth);
599
- } catch (error) {
600
- const errorMessage = error instanceof Error ? error.message : String(error);
601
- throw new McpError(
602
- ErrorCode.InternalError,
603
- `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,
604
- {
605
- uri: resource.uri
606
- }
607
- );
608
- }
609
- const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult];
610
- return {
611
- contents: resourceResults.map((result) => ({
612
- ...result,
613
- mimeType: result.mimeType ?? resource.mimeType,
614
- name: resource.name,
615
- uri: result.uri ?? resource.uri
616
- }))
617
- };
618
- }
619
- throw new UnexpectedStateError("Unknown resource request", {
620
- request
621
- });
622
- }
623
- );
624
- }
625
- setupResourceTemplateHandlers(resourceTemplates) {
626
- this.#server.setRequestHandler(
627
- ListResourceTemplatesRequestSchema,
628
- async () => {
629
- return {
630
- resourceTemplates: resourceTemplates.map((resourceTemplate) => ({
631
- description: resourceTemplate.description,
632
- mimeType: resourceTemplate.mimeType,
633
- name: resourceTemplate.name,
634
- uriTemplate: resourceTemplate.uriTemplate
635
- }))
636
- };
637
- }
638
- );
639
- }
640
- setupRootsHandlers() {
641
- if (this.#rootsConfig?.enabled === false) {
642
- this.#logger.debug(
643
- "[FastMCP debug] roots capability explicitly disabled via config"
644
- );
645
- return;
646
- }
647
- if (typeof this.#server.listRoots === "function") {
648
- this.#server.setNotificationHandler(
649
- RootsListChangedNotificationSchema,
650
- () => {
651
- this.#server.listRoots().then((roots) => {
652
- this.#roots = roots.roots;
653
- this.emit("rootsChanged", {
654
- roots: roots.roots
655
- });
656
- }).catch((error) => {
657
- if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {
658
- this.#logger.debug(
659
- "[FastMCP debug] listRoots method not supported by client"
660
- );
661
- } else {
662
- this.#logger.error(
663
- `[FastMCP error] received error listing roots.
664
-
665
- ${error instanceof Error ? error.stack : JSON.stringify(error)}`
666
- );
667
- }
668
- });
669
- }
670
- );
671
- } else {
672
- this.#logger.debug(
673
- "[FastMCP debug] roots capability not available, not setting up notification handler"
674
- );
675
- }
676
- }
677
- setupToolHandlers(tools) {
678
- this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
679
- return {
680
- tools: await Promise.all(
681
- tools.map(async (tool) => {
682
- return {
683
- annotations: tool.annotations,
684
- description: tool.description,
685
- inputSchema: tool.parameters ? await toJsonSchema(tool.parameters) : {
686
- additionalProperties: false,
687
- properties: {},
688
- type: "object"
689
- },
690
- // More complete schema for Cursor compatibility
691
- name: tool.name
692
- };
693
- })
694
- )
695
- };
696
- });
697
- this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
698
- const tool = tools.find((tool2) => tool2.name === request.params.name);
699
- if (!tool) {
700
- throw new McpError(
701
- ErrorCode.MethodNotFound,
702
- `Unknown tool: ${request.params.name}`
703
- );
704
- }
705
- let args2 = void 0;
706
- if (tool.parameters) {
707
- const parsed = await tool.parameters["~standard"].validate(
708
- request.params.arguments
709
- );
710
- if (parsed.issues) {
711
- const friendlyErrors = this.#utils?.formatInvalidParamsErrorMessage ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues) : parsed.issues.map((issue) => {
712
- const path2 = issue.path?.join(".") || "root";
713
- return `${path2}: ${issue.message}`;
714
- }).join(", ");
715
- throw new McpError(
716
- ErrorCode.InvalidParams,
717
- `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`
718
- );
719
- }
720
- args2 = parsed.value;
721
- }
722
- const progressToken = request.params?._meta?.progressToken;
723
- let result;
724
- try {
725
- const reportProgress = async (progress) => {
726
- try {
727
- await this.#server.notification({
728
- method: "notifications/progress",
729
- params: {
730
- ...progress,
731
- progressToken
732
- }
733
- });
734
- if (this.#needsEventLoopFlush) {
735
- await new Promise((resolve) => setImmediate(resolve));
736
- }
737
- } catch (progressError) {
738
- this.#logger.warn(
739
- `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
740
- progressError instanceof Error ? progressError.message : String(progressError)
741
- );
742
- }
743
- };
744
- const log = {
745
- debug: (message, context) => {
746
- this.#server.sendLoggingMessage({
747
- data: {
748
- context,
749
- message
750
- },
751
- level: "debug"
752
- });
753
- },
754
- error: (message, context) => {
755
- this.#server.sendLoggingMessage({
756
- data: {
757
- context,
758
- message
759
- },
760
- level: "error"
761
- });
762
- },
763
- info: (message, context) => {
764
- this.#server.sendLoggingMessage({
765
- data: {
766
- context,
767
- message
768
- },
769
- level: "info"
770
- });
771
- },
772
- warn: (message, context) => {
773
- this.#server.sendLoggingMessage({
774
- data: {
775
- context,
776
- message
777
- },
778
- level: "warning"
779
- });
780
- }
781
- };
782
- const streamContent = async (content) => {
783
- const contentArray = Array.isArray(content) ? content : [content];
784
- try {
785
- await this.#server.notification({
786
- method: "notifications/tool/streamContent",
787
- params: {
788
- content: contentArray,
789
- toolName: request.params.name
790
- }
791
- });
792
- if (this.#needsEventLoopFlush) {
793
- await new Promise((resolve) => setImmediate(resolve));
794
- }
795
- } catch (streamError) {
796
- this.#logger.warn(
797
- `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
798
- streamError instanceof Error ? streamError.message : String(streamError)
799
- );
800
- }
801
- };
802
- const executeToolPromise = tool.execute(args2, {
803
- client: {
804
- version: this.#server.getClientVersion()
805
- },
806
- log,
807
- reportProgress,
808
- session: this.#auth,
809
- streamContent
810
- });
811
- const maybeStringResult = await (tool.timeoutMs ? Promise.race([
812
- executeToolPromise,
813
- new Promise((_, reject) => {
814
- const timeoutId = setTimeout(() => {
815
- reject(
816
- new UserError(
817
- `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`
818
- )
819
- );
820
- }, tool.timeoutMs);
821
- executeToolPromise.finally(() => clearTimeout(timeoutId));
822
- })
823
- ]) : executeToolPromise);
824
- await delay(1);
825
- if (maybeStringResult === void 0 || maybeStringResult === null) {
826
- result = ContentResultZodSchema.parse({
827
- content: []
828
- });
829
- } else if (typeof maybeStringResult === "string") {
830
- result = ContentResultZodSchema.parse({
831
- content: [{ text: maybeStringResult, type: "text" }]
832
- });
833
- } else if ("type" in maybeStringResult) {
834
- result = ContentResultZodSchema.parse({
835
- content: [maybeStringResult]
836
- });
837
- } else {
838
- result = ContentResultZodSchema.parse(maybeStringResult);
839
- }
840
- } catch (error) {
841
- if (error instanceof UserError) {
842
- return {
843
- content: [{ text: error.message, type: "text" }],
844
- isError: true
845
- };
846
- }
847
- const errorMessage = error instanceof Error ? error.message : String(error);
848
- return {
849
- content: [
850
- {
851
- text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,
852
- type: "text"
853
- }
854
- ],
855
- isError: true
856
- };
857
- }
858
- return result;
859
- });
860
- }
861
- };
862
- function camelToSnakeCase(str) {
863
- return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
864
- }
865
- function convertObjectToSnakeCase(obj) {
866
- const result = {};
867
- for (const [key, value] of Object.entries(obj)) {
868
- const snakeKey = camelToSnakeCase(key);
869
- result[snakeKey] = value;
870
- }
871
- return result;
872
- }
873
- var FastMCPEventEmitterBase = EventEmitter;
874
- var FastMCPEventEmitter = class extends FastMCPEventEmitterBase {
875
- };
876
- var FastMCP = class extends FastMCPEventEmitter {
877
- constructor(options) {
878
- super();
879
- this.options = options;
880
- this.#options = options;
881
- this.#authenticate = options.authenticate;
882
- this.#logger = options.logger || console;
883
- }
884
- options;
885
- get sessions() {
886
- return this.#sessions;
887
- }
888
- #authenticate;
889
- #httpStreamServer = null;
890
- #logger;
891
- #options;
892
- #prompts = [];
893
- #resources = [];
894
- #resourcesTemplates = [];
895
- #sessions = [];
896
- #tools = [];
897
- /**
898
- * Adds a prompt to the server.
899
- */
900
- addPrompt(prompt) {
901
- this.#prompts.push(prompt);
902
- }
903
- /**
904
- * Adds a resource to the server.
905
- */
906
- addResource(resource) {
907
- this.#resources.push(resource);
908
- }
909
- /**
910
- * Adds a resource template to the server.
911
- */
912
- addResourceTemplate(resource) {
913
- this.#resourcesTemplates.push(resource);
914
- }
915
- /**
916
- * Adds a tool to the server.
917
- */
918
- addTool(tool) {
919
- this.#tools.push(tool);
920
- }
921
- /**
922
- * Embeds a resource by URI, making it easy to include resources in tool responses.
923
- *
924
- * @param uri - The URI of the resource to embed
925
- * @returns Promise<ResourceContent> - The embedded resource content
926
- */
927
- async embedded(uri) {
928
- const directResource = this.#resources.find(
929
- (resource) => resource.uri === uri
930
- );
931
- if (directResource) {
932
- const result = await directResource.load();
933
- const results = Array.isArray(result) ? result : [result];
934
- const firstResult = results[0];
935
- const resourceData = {
936
- mimeType: directResource.mimeType,
937
- uri
938
- };
939
- if ("text" in firstResult) {
940
- resourceData.text = firstResult.text;
941
- }
942
- if ("blob" in firstResult) {
943
- resourceData.blob = firstResult.blob;
944
- }
945
- return resourceData;
946
- }
947
- for (const template of this.#resourcesTemplates) {
948
- const templateBase = template.uriTemplate.split("{")[0];
949
- if (uri.startsWith(templateBase)) {
950
- const params = {};
951
- const templateParts = template.uriTemplate.split("/");
952
- const uriParts = uri.split("/");
953
- for (let i = 0; i < templateParts.length; i++) {
954
- const templatePart = templateParts[i];
955
- if (templatePart?.startsWith("{") && templatePart.endsWith("}")) {
956
- const paramName = templatePart.slice(1, -1);
957
- const paramValue = uriParts[i];
958
- if (paramValue) {
959
- params[paramName] = paramValue;
960
- }
961
- }
962
- }
963
- const result = await template.load(
964
- params
965
- );
966
- const resourceData = {
967
- mimeType: template.mimeType,
968
- uri
969
- };
970
- if ("text" in result) {
971
- resourceData.text = result.text;
972
- }
973
- if ("blob" in result) {
974
- resourceData.blob = result.blob;
975
- }
976
- return resourceData;
977
- }
978
- }
979
- throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });
980
- }
981
- /**
982
- * Starts the server.
983
- */
984
- async start(options) {
985
- const config = this.#parseRuntimeConfig(options);
986
- if (config.transportType === "stdio") {
987
- const transport = new StdioServerTransport();
988
- let auth;
989
- if (this.#authenticate) {
990
- try {
991
- auth = await this.#authenticate(
992
- void 0
993
- );
994
- } catch (error) {
995
- this.#logger.error(
996
- "[FastMCP error] Authentication failed for stdio transport:",
997
- error instanceof Error ? error.message : String(error)
998
- );
999
- }
1000
- }
1001
- const session = new FastMCPSession({
1002
- auth,
1003
- instructions: this.#options.instructions,
1004
- logger: this.#logger,
1005
- name: this.#options.name,
1006
- ping: this.#options.ping,
1007
- prompts: this.#prompts,
1008
- resources: this.#resources,
1009
- resourcesTemplates: this.#resourcesTemplates,
1010
- roots: this.#options.roots,
1011
- tools: this.#tools,
1012
- transportType: "stdio",
1013
- utils: this.#options.utils,
1014
- version: this.#options.version
1015
- });
1016
- await session.connect(transport);
1017
- this.#sessions.push(session);
1018
- session.once("error", () => {
1019
- this.#removeSession(session);
1020
- });
1021
- if (transport.onclose) {
1022
- const originalOnClose = transport.onclose;
1023
- transport.onclose = () => {
1024
- this.#removeSession(session);
1025
- if (originalOnClose) {
1026
- originalOnClose();
1027
- }
1028
- };
1029
- } else {
1030
- transport.onclose = () => {
1031
- this.#removeSession(session);
1032
- };
1033
- }
1034
- this.emit("connect", {
1035
- session
1036
- });
1037
- } else if (config.transportType === "httpStream") {
1038
- const httpConfig = config.httpStream;
1039
- if (httpConfig.stateless) {
1040
- this.#logger.info(
1041
- `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
1042
- );
1043
- this.#httpStreamServer = await startHTTPServer({
1044
- createServer: async (request) => {
1045
- const auth = await this.#authenticateRequest(request);
1046
- return this.#createSession(auth);
1047
- },
1048
- enableJsonResponse: httpConfig.enableJsonResponse,
1049
- eventStore: httpConfig.eventStore,
1050
- host: httpConfig.host,
1051
- oauth: this.#options.oauth,
1052
- // In stateless mode, we don't track sessions
1053
- onClose: async () => {
1054
- },
1055
- onConnect: async () => {
1056
- this.#logger.debug(
1057
- `[FastMCP debug] Stateless HTTP Stream request handled`
1058
- );
1059
- },
1060
- onUnhandledRequest: async (req, res) => {
1061
- await this.#handleUnhandledRequest(req, res, true, httpConfig.host);
1062
- },
1063
- port: httpConfig.port,
1064
- sseEndpoint: "/sse",
1065
- // Server-Sent Events endpoint for streaming events
1066
- stateless: true,
1067
- streamEndpoint: httpConfig.endpoint
1068
- });
1069
- } else {
1070
- this.#httpStreamServer = await startHTTPServer({
1071
- createServer: async (request) => {
1072
- const auth = await this.#authenticateRequest(request);
1073
- return this.#createSession(auth);
1074
- },
1075
- enableJsonResponse: httpConfig.enableJsonResponse,
1076
- eventStore: httpConfig.eventStore,
1077
- host: httpConfig.host,
1078
- oauth: this.#options.oauth,
1079
- onClose: async (session) => {
1080
- const sessionIndex = this.#sessions.indexOf(session);
1081
- if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1);
1082
- this.emit("disconnect", {
1083
- session
1084
- });
1085
- },
1086
- onConnect: async (session) => {
1087
- this.#sessions.push(session);
1088
- this.#logger.info(`[FastMCP info] HTTP Stream session established`);
1089
- this.emit("connect", {
1090
- session
1091
- });
1092
- },
1093
- onUnhandledRequest: async (req, res) => {
1094
- await this.#handleUnhandledRequest(
1095
- req,
1096
- res,
1097
- false,
1098
- httpConfig.host
1099
- );
1100
- },
1101
- port: httpConfig.port,
1102
- sseEndpoint: "/sse",
1103
- streamEndpoint: httpConfig.endpoint
1104
- });
1105
- this.#logger.info(
1106
- `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
1107
- );
1108
- this.#logger.info(
1109
- `[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`
1110
- );
1111
- }
1112
- } else {
1113
- throw new Error("Invalid transport type");
1114
- }
1115
- }
1116
- /**
1117
- * Stops the server.
1118
- */
1119
- async stop() {
1120
- if (this.#httpStreamServer) {
1121
- await this.#httpStreamServer.close();
1122
- }
1123
- }
1124
- async #authenticateRequest(request) {
1125
- if (!this.#authenticate) {
1126
- return void 0;
1127
- }
1128
- try {
1129
- return await this.#authenticate(request);
1130
- } catch (error) {
1131
- const response = this.#createOAuthChallengeResponse(error);
1132
- if (response) {
1133
- throw response;
1134
- }
1135
- throw error;
1136
- }
1137
- }
1138
- #createOAuthChallengeResponse(error) {
1139
- if (error instanceof Response) {
1140
- return void 0;
1141
- }
1142
- const resourceMetadataUrl = this.#options.oauth?.protectedResourceMetadataUrl;
1143
- if (!this.#options.oauth?.enabled || !resourceMetadataUrl) {
1144
- return void 0;
1145
- }
1146
- const errorMessage = error instanceof Error ? error.message : String(error || "Unauthorized");
1147
- const wwwAuthenticate = [
1148
- `resource_metadata="${escapeWWWAuthenticateValue(resourceMetadataUrl)}"`,
1149
- `error="invalid_token"`,
1150
- `error_description="${escapeWWWAuthenticateValue(errorMessage)}"`
1151
- ].join(", ");
1152
- return new Response(
1153
- JSON.stringify({
1154
- error: "invalid_token",
1155
- error_description: errorMessage
1156
- }),
1157
- {
1158
- headers: {
1159
- "Content-Type": "application/json",
1160
- "WWW-Authenticate": `Bearer ${wwwAuthenticate}`
1161
- },
1162
- status: 401
1163
- }
1164
- );
1165
- }
1166
- /**
1167
- * Creates a new FastMCPSession instance with the current configuration.
1168
- * Used both for regular sessions and stateless requests.
1169
- */
1170
- #createSession(auth) {
1171
- const allowedTools = auth ? this.#tools.filter(
1172
- (tool) => tool.canAccess ? tool.canAccess(auth) : true
1173
- ) : this.#tools;
1174
- return new FastMCPSession({
1175
- auth,
1176
- instructions: this.#options.instructions,
1177
- logger: this.#logger,
1178
- name: this.#options.name,
1179
- ping: this.#options.ping,
1180
- prompts: this.#prompts,
1181
- resources: this.#resources,
1182
- resourcesTemplates: this.#resourcesTemplates,
1183
- roots: this.#options.roots,
1184
- tools: allowedTools,
1185
- transportType: "httpStream",
1186
- utils: this.#options.utils,
1187
- version: this.#options.version
1188
- });
1189
- }
1190
- /**
1191
- * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
1192
- */
1193
- #handleUnhandledRequest = async (req, res, isStateless = false, host) => {
1194
- const url = new URL(req.url || "", `http://${host}`);
1195
- if (url.pathname === "/messages") {
1196
- return;
1197
- }
1198
- const healthConfig = this.#options.health ?? {};
1199
- const enabled = healthConfig.enabled === void 0 ? true : healthConfig.enabled;
1200
- if (enabled) {
1201
- const path2 = healthConfig.path ?? "/health";
1202
- try {
1203
- if (req.method === "GET" && url.pathname === path2) {
1204
- res.writeHead(healthConfig.status ?? 200, {
1205
- "Content-Type": "text/plain"
1206
- }).end(healthConfig.message ?? "\u2713 Ok");
1207
- return;
1208
- }
1209
- if (req.method === "GET" && url.pathname === "/ready") {
1210
- if (isStateless) {
1211
- const response = {
1212
- mode: "stateless",
1213
- ready: 1,
1214
- status: "ready",
1215
- total: 1
1216
- };
1217
- res.writeHead(200, {
1218
- "Content-Type": "application/json"
1219
- }).end(JSON.stringify(response));
1220
- } else {
1221
- const readySessions = this.#sessions.filter(
1222
- (s) => s.isReady
1223
- ).length;
1224
- const totalSessions = this.#sessions.length;
1225
- const allReady = readySessions === totalSessions && totalSessions > 0;
1226
- const response = {
1227
- ready: readySessions,
1228
- status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing",
1229
- total: totalSessions
1230
- };
1231
- res.writeHead(allReady ? 200 : 503, {
1232
- "Content-Type": "application/json"
1233
- }).end(JSON.stringify(response));
1234
- }
1235
- return;
1236
- }
1237
- } catch (error) {
1238
- this.#logger.error("[FastMCP error] health endpoint error", error);
1239
- }
1240
- }
1241
- const openaiAppsChallengeConfig = this.#options.openaiAppsChallenge;
1242
- const openaiAppsChallengeToken = openaiAppsChallengeConfig?.token?.trim();
1243
- const openaiAppsChallengeEnabled = openaiAppsChallengeConfig?.enabled !== false && !!openaiAppsChallengeToken;
1244
- if (openaiAppsChallengeEnabled && req.method === "GET") {
1245
- const path2 = openaiAppsChallengeConfig?.path ?? "/.well-known/openai-apps-challenge";
1246
- if (url.pathname === path2) {
1247
- res.writeHead(200, {
1248
- "Content-Type": "text/plain"
1249
- }).end(openaiAppsChallengeToken);
1250
- return;
1251
- }
1252
- }
1253
- const oauthConfig = this.#options.oauth;
1254
- if (oauthConfig?.enabled && req.method === "GET") {
1255
- if (url.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer) {
1256
- const metadata = convertObjectToSnakeCase(
1257
- oauthConfig.authorizationServer
1258
- );
1259
- res.writeHead(200, {
1260
- "Content-Type": "application/json"
1261
- }).end(JSON.stringify(metadata));
1262
- return;
1263
- }
1264
- if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) {
1265
- const metadata = convertObjectToSnakeCase(
1266
- oauthConfig.protectedResource
1267
- );
1268
- res.writeHead(200, {
1269
- "Content-Type": "application/json"
1270
- }).end(JSON.stringify(metadata));
1271
- return;
1272
- }
1273
- }
1274
- res.writeHead(404).end();
1275
- };
1276
- #parseRuntimeConfig(overrides) {
1277
- const args2 = process.argv.slice(2);
1278
- const getArg = (name) => {
1279
- const index = args2.findIndex((arg) => arg === `--${name}`);
1280
- return index !== -1 && index + 1 < args2.length ? args2[index + 1] : void 0;
1281
- };
1282
- const transportArg = getArg("transport");
1283
- const portArg = getArg("port");
1284
- const endpointArg = getArg("endpoint");
1285
- const statelessArg = getArg("stateless");
1286
- const hostArg = getArg("host");
1287
- const envTransport = process.env.FASTMCP_TRANSPORT;
1288
- const envPort = process.env.FASTMCP_PORT;
1289
- const envEndpoint = process.env.FASTMCP_ENDPOINT;
1290
- const envStateless = process.env.FASTMCP_STATELESS;
1291
- const envHost = process.env.FASTMCP_HOST;
1292
- const transportType = overrides?.transportType || (transportArg === "http-stream" ? "httpStream" : transportArg) || envTransport || "stdio";
1293
- if (transportType === "httpStream") {
1294
- const port = parseInt(
1295
- overrides?.httpStream?.port?.toString() || portArg || envPort || "8080"
1296
- );
1297
- const host = overrides?.httpStream?.host || hostArg || envHost || "localhost";
1298
- const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
1299
- const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false;
1300
- const stateless = overrides?.httpStream?.stateless || statelessArg === "true" || envStateless === "true" || false;
1301
- return {
1302
- httpStream: {
1303
- enableJsonResponse,
1304
- endpoint,
1305
- host,
1306
- port,
1307
- stateless
1308
- },
1309
- transportType: "httpStream"
1310
- };
1311
- }
1312
- return { transportType: "stdio" };
1313
- }
1314
- #removeSession(session) {
1315
- const sessionIndex = this.#sessions.indexOf(session);
1316
- if (sessionIndex !== -1) {
1317
- this.#sessions.splice(sessionIndex, 1);
1318
- this.emit("disconnect", {
1319
- session
1320
- });
1321
- }
1322
- }
1323
- };
10
+ import { z as z3 } from "zod";
1324
11
 
1325
12
  // src/monitor.ts
1326
- import { z as z2 } from "zod";
13
+ import { z } from "zod";
1327
14
  var DEFAULT_API_URL = "https://api.firecrawl.dev";
1328
15
  function resolveAuth(session) {
1329
16
  const apiKey = session?.firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY;
@@ -1365,8 +52,8 @@ async function monitorRequest(session, path2, init = {}) {
1365
52
  function asText(data) {
1366
53
  return JSON.stringify(data, null, 2);
1367
54
  }
1368
- var pageStatusSchema = z2.enum(["same", "new", "changed", "removed", "error"]);
1369
- var checkStatusSchema = z2.enum([
55
+ var pageStatusSchema = z.enum(["same", "new", "changed", "removed", "error"]);
56
+ var checkStatusSchema = z.enum([
1370
57
  "queued",
1371
58
  "running",
1372
59
  "completed",
@@ -1574,22 +261,22 @@ Full \`body\` requests require: \`name\`, \`schedule\` (with \`cron\` or \`text\
1574
261
 
1575
262
  **Mixed mode (JSON + git-diff):** Use \`modes: ["json", "git-diff"]\` to get both per-field diffs and a markdown sidecar. The page is marked \`changed\` whenever either surface changed.
1576
263
  `,
1577
- parameters: z2.object({
1578
- body: z2.record(z2.string(), z2.any()).optional(),
1579
- page: z2.string().optional(),
1580
- pages: z2.array(z2.string()).optional(),
1581
- queries: z2.array(z2.string()).optional(),
1582
- searchWindow: z2.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
1583
- maxResults: z2.number().int().min(1).max(50).optional(),
1584
- includeDomains: z2.array(z2.string()).optional(),
1585
- excludeDomains: z2.array(z2.string()).optional(),
1586
- goal: z2.string().optional(),
1587
- name: z2.string().optional(),
1588
- scheduleText: z2.string().optional(),
1589
- timezone: z2.string().optional(),
1590
- email: z2.string().optional(),
1591
- includeDiffs: z2.boolean().optional(),
1592
- webhookUrl: z2.string().optional()
264
+ parameters: z.object({
265
+ body: z.record(z.string(), z.any()).optional(),
266
+ page: z.string().optional(),
267
+ pages: z.array(z.string()).optional(),
268
+ queries: z.array(z.string()).optional(),
269
+ searchWindow: z.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
270
+ maxResults: z.number().int().min(1).max(50).optional(),
271
+ includeDomains: z.array(z.string()).optional(),
272
+ excludeDomains: z.array(z.string()).optional(),
273
+ goal: z.string().optional(),
274
+ name: z.string().optional(),
275
+ scheduleText: z.string().optional(),
276
+ timezone: z.string().optional(),
277
+ email: z.string().optional(),
278
+ includeDiffs: z.boolean().optional(),
279
+ webhookUrl: z.string().optional()
1593
280
  }),
1594
281
  execute: async (args2, { session, log }) => {
1595
282
  const body = buildMonitorCreateBody(args2);
@@ -1620,11 +307,11 @@ List all Firecrawl monitors for the authenticated account.
1620
307
  { "name": "firecrawl_monitor_list", "arguments": { "limit": 20 } }
1621
308
  \`\`\`
1622
309
  `,
1623
- parameters: z2.object({
1624
- limit: z2.number().int().positive().optional(),
1625
- offset: z2.number().int().nonnegative().optional()
310
+ parameters: z.object({
311
+ limit: z.number().int().positive().optional(),
312
+ offset: z.number().int().nonnegative().optional()
1626
313
  }),
1627
- execute: async (args2, { session, log }) => {
314
+ execute: async (args2, { session }) => {
1628
315
  const { limit, offset } = args2;
1629
316
  const res = await monitorRequest(session, "/monitor", {
1630
317
  query: { limit, offset }
@@ -1651,8 +338,8 @@ Get a single monitor by ID.
1651
338
  { "name": "firecrawl_monitor_get", "arguments": { "id": "mon_abc123" } }
1652
339
  \`\`\`
1653
340
  `,
1654
- parameters: z2.object({ id: z2.string() }),
1655
- execute: async (args2, { session, log }) => {
341
+ parameters: z.object({ id: z.string() }),
342
+ execute: async (args2, { session }) => {
1656
343
  const { id } = args2;
1657
344
  const res = await monitorRequest(
1658
345
  session,
@@ -1686,11 +373,11 @@ Update a monitor. Pass any subset of fields to patch: \`name\`, \`status\` ("act
1686
373
  }
1687
374
  \`\`\`
1688
375
  `,
1689
- parameters: z2.object({
1690
- id: z2.string(),
1691
- body: z2.record(z2.string(), z2.any())
376
+ parameters: z.object({
377
+ id: z.string(),
378
+ body: z.record(z.string(), z.any())
1692
379
  }),
1693
- execute: async (args2, { session, log }) => {
380
+ execute: async (args2, { session }) => {
1694
381
  const { id, body } = args2;
1695
382
  const res = await monitorRequest(
1696
383
  session,
@@ -1719,7 +406,7 @@ Permanently delete a monitor and stop its schedule. This cannot be undone.
1719
406
  { "name": "firecrawl_monitor_delete", "arguments": { "id": "mon_abc123" } }
1720
407
  \`\`\`
1721
408
  `,
1722
- parameters: z2.object({ id: z2.string() }),
409
+ parameters: z.object({ id: z.string() }),
1723
410
  execute: async (args2, { session, log }) => {
1724
411
  const { id } = args2;
1725
412
  log.info("Deleting monitor", { id });
@@ -1750,8 +437,8 @@ Trigger a monitor check immediately, outside its normal schedule. Returns the qu
1750
437
  { "name": "firecrawl_monitor_run", "arguments": { "id": "mon_abc123" } }
1751
438
  \`\`\`
1752
439
  `,
1753
- parameters: z2.object({ id: z2.string() }),
1754
- execute: async (args2, { session, log }) => {
440
+ parameters: z.object({ id: z.string() }),
441
+ execute: async (args2, { session }) => {
1755
442
  const { id } = args2;
1756
443
  const res = await monitorRequest(
1757
444
  session,
@@ -1780,13 +467,13 @@ List historical checks for a monitor.
1780
467
  { "name": "firecrawl_monitor_checks", "arguments": { "id": "mon_abc123", "limit": 10, "status": "completed" } }
1781
468
  \`\`\`
1782
469
  `,
1783
- parameters: z2.object({
1784
- id: z2.string(),
1785
- limit: z2.number().int().positive().optional(),
1786
- offset: z2.number().int().nonnegative().optional(),
470
+ parameters: z.object({
471
+ id: z.string(),
472
+ limit: z.number().int().positive().optional(),
473
+ offset: z.number().int().nonnegative().optional(),
1787
474
  status: checkStatusSchema.optional()
1788
475
  }),
1789
- execute: async (args2, { session, log }) => {
476
+ execute: async (args2, { session }) => {
1790
477
  const { id, limit, offset, status } = args2;
1791
478
  const res = await monitorRequest(
1792
479
  session,
@@ -1863,14 +550,14 @@ The endpoint paginates via a top-level \`next\` URL; this tool returns one page
1863
550
  }
1864
551
  \`\`\`
1865
552
  `,
1866
- parameters: z2.object({
1867
- id: z2.string(),
1868
- checkId: z2.string(),
1869
- limit: z2.number().int().positive().optional(),
1870
- skip: z2.number().int().nonnegative().optional(),
553
+ parameters: z.object({
554
+ id: z.string(),
555
+ checkId: z.string(),
556
+ limit: z.number().int().positive().optional(),
557
+ skip: z.number().int().nonnegative().optional(),
1871
558
  pageStatus: pageStatusSchema.optional()
1872
559
  }),
1873
- execute: async (args2, { session, log }) => {
560
+ execute: async (args2, { session }) => {
1874
561
  const { id, checkId, limit, skip, pageStatus } = args2;
1875
562
  const res = await monitorRequest(
1876
563
  session,
@@ -1883,7 +570,7 @@ The endpoint paginates via a top-level \`next\` URL; this tool returns one page
1883
570
  }
1884
571
 
1885
572
  // src/research.ts
1886
- import { z as z3 } from "zod";
573
+ import { z as z2 } from "zod";
1887
574
  var BASE = "/v2/search/research";
1888
575
  var ORIGIN_HEADERS = { "X-Origin": "mcp-fastmcp" };
1889
576
  function appendParam(params, key, value) {
@@ -1994,26 +681,30 @@ function registerResearchTools(server2, getClient2) {
1994
681
  server2.addTool({
1995
682
  name: "firecrawl_research_search_papers",
1996
683
  annotations: {
1997
- title: "Search arXiv papers",
684
+ title: "Search research papers",
1998
685
  readOnlyHint: true,
1999
- // Semantic search over indexed arXiv metadata; returns ranked results only.
686
+ // Semantic search over indexed paper metadata; returns ranked results only.
2000
687
  openWorldHint: true,
2001
- // Searches the public arXiv research corpus.
688
+ // Searches the Firecrawl research paper index.
2002
689
  destructiveHint: false
2003
- // Query-only; no writes to arXiv or the research index.
690
+ // Query-only; no writes to external sources or the research index.
2004
691
  },
2005
- description: "Primary entry point for finding arXiv papers by topic. Semantic (HyDE) search over arXiv abstracts; returns ranked papers with arXiv id, title, and abstract. The query should be a natural-language description of what you want. Run SEVERAL distinct framings of the question (sibling domains, rival methods, dataset/benchmark names) rather than one query \u2014 recall improves markedly with diverse framings. Returns up to `k` results (default 40).",
2006
- parameters: z3.object({
2007
- query: z3.string().min(1),
2008
- k: z3.number().int().min(1).max(500).optional(),
2009
- authors: z3.array(z3.string()).optional().describe(
692
+ description: "Primary entry point for finding research papers by topic across AI/ML, computer science, math, physics, biomedical, life sciences, and clinical literature. Semantic (HyDE) search over indexed paper metadata and abstracts; returns ranked papers with paper id, title, authors, and abstract. The query should be a natural-language research topic or question. Run SEVERAL distinct framings of the question (sibling domains, rival methods, dataset or benchmark names, conditions, populations, interventions, or outcomes) rather than one query \u2014 recall improves markedly with diverse framings.",
693
+ parameters: z2.object({
694
+ query: z2.string().min(1).describe(
695
+ "Natural-language research topic or question, including methods, systems, conditions, populations, interventions, or outcomes when relevant."
696
+ ),
697
+ k: z2.number().int().min(1).max(500).optional().describe("Number of ranked papers to return (default 40)."),
698
+ authors: z2.array(z2.string()).optional().describe(
2010
699
  "Author substring filter(s); ALL must match (case-insensitive)."
2011
700
  ),
2012
- categories: z3.array(z3.string()).optional().describe("arXiv category filter(s) (e.g. `cs.LG`); ALL must match."),
2013
- from: z3.string().optional().describe(
701
+ categories: z2.array(z2.string()).optional().describe(
702
+ "Paper category filter(s) (e.g. `cs.LG`); ALL provided values must match."
703
+ ),
704
+ from: z2.string().optional().describe(
2014
705
  "Inclusive lower bound on created/updated date (`YYYY-MM-DD`)."
2015
706
  ),
2016
- to: z3.string().optional().describe(
707
+ to: z2.string().optional().describe(
2017
708
  "Inclusive upper bound on created/updated date (`YYYY-MM-DD`)."
2018
709
  )
2019
710
  }),
@@ -2046,8 +737,8 @@ function registerResearchTools(server2, getClient2) {
2046
737
  // Read-only metadata lookup.
2047
738
  },
2048
739
  description: "Fetch canonical metadata for one paper by primaryId or canonical paperId. Use this after search/related results when you need the full title, abstract, authors, categories, source ids, and dates rendered as markdown.",
2049
- parameters: z3.object({
2050
- paperId: z3.string().min(1).describe(
740
+ parameters: z2.object({
741
+ paperId: z2.string().min(1).describe(
2051
742
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
2052
743
  )
2053
744
  }),
@@ -2073,12 +764,12 @@ function registerResearchTools(server2, getClient2) {
2073
764
  // Read-only graph query; no modifications.
2074
765
  },
2075
766
  description: "Expand from anchor papers you have already found, via the citation graph, ranked and filtered to a natural-language `intent`. Pass arXiv ids of your strongest hits as `seed_ids`. Modes: `similar` (cocitation/coupling \u2014 papers in the same niche; the default), `citers` (papers that cite the anchors), `references` (papers the anchors cite). This reaches relevant papers that plain search misses, so use it on your best hits before finishing. A `similar` call already runs a DEEP multi-round expansion internally (re-seeding from each round\u2019s best finds), so one call reaches the wider neighborhood \u2014 no need to chain many. Returns the candidates plus the pool size.",
2076
- parameters: z3.object({
2077
- seed_ids: z3.array(z3.string()).min(1).max(10),
2078
- intent: z3.string().min(1),
2079
- mode: z3.enum(["similar", "citers", "references"]).optional(),
2080
- k: z3.number().int().min(1).max(500).optional(),
2081
- rerank: z3.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
767
+ parameters: z2.object({
768
+ seed_ids: z2.array(z2.string()).min(1).max(10),
769
+ intent: z2.string().min(1),
770
+ mode: z2.enum(["similar", "citers", "references"]).optional(),
771
+ k: z2.number().int().min(1).max(500).optional(),
772
+ rerank: z2.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
2082
773
  }),
2083
774
  execute: async (args2, { session }) => {
2084
775
  const { seed_ids, intent, mode, k, rerank } = args2;
@@ -2115,12 +806,12 @@ note: ${res.data.note}` : "";
2115
806
  // Read-only passage retrieval.
2116
807
  },
2117
808
  description: "Read the most relevant in-body (full-text) passages of ONE specific paper for a question. Use this to VERIFY whether a candidate actually satisfies a constraint before you include or reject it (e.g. 'does this paper actually use technique X / report a score on benchmark Y'). Returns the best-matching passages, or a notice if the paper's full text is unavailable.",
2118
- parameters: z3.object({
2119
- paperId: z3.string().min(1).describe(
809
+ parameters: z2.object({
810
+ paperId: z2.string().min(1).describe(
2120
811
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
2121
812
  ),
2122
- question: z3.string().min(1),
2123
- k: z3.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
813
+ question: z2.string().min(1),
814
+ k: z2.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
2124
815
  }),
2125
816
  execute: async (args2, { session }) => {
2126
817
  const { paperId, question, k } = args2;
@@ -2148,9 +839,9 @@ note: ${res.data.note}` : "";
2148
839
  // Query-only; does not create issues, PRs, or modify repositories.
2149
840
  },
2150
841
  description: "Search GitHub issue/PR history and repository readmes. Returns ranked matches with repo, url, a short snippet, and (when available) the full matched content in markdown.",
2151
- parameters: z3.object({
2152
- query: z3.string().min(1),
2153
- k: z3.number().int().min(1).max(100).optional()
842
+ parameters: z2.object({
843
+ query: z2.string().min(1),
844
+ k: z2.number().int().min(1).max(100).optional()
2154
845
  }),
2155
846
  execute: async (args2, { session }) => {
2156
847
  const { query, k } = args2;
@@ -2171,6 +862,7 @@ note: ${res.data.note}` : "";
2171
862
  dotenv.config({ debug: false, quiet: true });
2172
863
  var require2 = createRequire(import.meta.url);
2173
864
  var { version: packageVersion } = require2("../package.json");
865
+ var authResultByRequest = /* @__PURE__ */ Symbol("firecrawlMcpAuthResult");
2174
866
  function normalizeHeader(value) {
2175
867
  if (value == null) return void 0;
2176
868
  const v = Array.isArray(value) ? value[0] : value;
@@ -2208,6 +900,33 @@ function getMcpResourceUrl() {
2208
900
  function getOAuthProtectedResourceMetadataUrl() {
2209
901
  return `${new URL(getMcpResourceUrl()).origin}/.well-known/oauth-protected-resource`;
2210
902
  }
903
+ function escapeWWWAuthenticateValue(value) {
904
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
905
+ }
906
+ function createOAuthChallengeResponse(error) {
907
+ if (!isMcpOAuthEnabled()) {
908
+ return void 0;
909
+ }
910
+ const errorMessage = error instanceof Error ? error.message : String(error || "Unauthorized");
911
+ const wwwAuthenticate = [
912
+ `resource_metadata="${escapeWWWAuthenticateValue(getOAuthProtectedResourceMetadataUrl())}"`,
913
+ 'error="invalid_token"',
914
+ `error_description="${escapeWWWAuthenticateValue(errorMessage)}"`
915
+ ].join(", ");
916
+ return new Response(
917
+ JSON.stringify({
918
+ error: "invalid_token",
919
+ error_description: errorMessage
920
+ }),
921
+ {
922
+ headers: {
923
+ "Content-Type": "application/json",
924
+ "WWW-Authenticate": `Bearer ${wwwAuthenticate}`
925
+ },
926
+ status: 401
927
+ }
928
+ );
929
+ }
2211
930
  function getOAuthIntrospectionEndpoint() {
2212
931
  return `${getOAuthIssuer()}/api/oauth/introspect`;
2213
932
  }
@@ -2258,6 +977,52 @@ async function resolveCredentialFromHeaders(headers) {
2258
977
  }
2259
978
  return void 0;
2260
979
  }
980
+ async function authenticateRequest(request) {
981
+ const headerCred = request?.headers ? await resolveCredentialFromHeaders(request.headers) : void 0;
982
+ const envCred = resolveCredentialFromEnv();
983
+ if (process.env.CLOUD_SERVICE === "true") {
984
+ if (!headerCred) {
985
+ const clientIp = extractClientIp(request);
986
+ if (process.env.KEYLESS_PROXY_SECRET && clientIp && await keylessEligible(clientIp)) {
987
+ return { firecrawlApiKey: void 0, keylessClientIp: clientIp };
988
+ }
989
+ throw new Error(
990
+ "Firecrawl credentials required: OAuth access token (Authorization: Bearer fco_...) or API key (x-firecrawl-api-key)"
991
+ );
992
+ }
993
+ return { firecrawlApiKey: headerCred };
994
+ }
995
+ const credential = headerCred ?? envCred;
996
+ const httpStreaming = isHttpStreamingTransport();
997
+ if (!httpStreaming && !process.env.FIRECRAWL_API_KEY && !process.env.FIRECRAWL_API_URL) {
998
+ console.error(
999
+ "No FIRECRAWL_API_KEY or FIRECRAWL_API_URL set \u2014 running in keyless mode. firecrawl_scrape and firecrawl_search are free (rate-limited per IP) against the Firecrawl cloud; other tools require an API key (get one free at https://firecrawl.dev)."
1000
+ );
1001
+ }
1002
+ if (httpStreaming && !credential && !process.env.FIRECRAWL_API_URL) {
1003
+ console.error(
1004
+ "HTTP MCP transport requires FIRECRAWL_API_URL and/or credentials (OAuth: Authorization Bearer fco_..., or FIRECRAWL_API_KEY / FIRECRAWL_OAUTH_TOKEN)"
1005
+ );
1006
+ process.exit(1);
1007
+ }
1008
+ return { firecrawlApiKey: credential };
1009
+ }
1010
+ async function authenticateWithOAuthChallenge(request) {
1011
+ if (request?.[authResultByRequest]) {
1012
+ return request[authResultByRequest];
1013
+ }
1014
+ const authResult = authenticateRequest(request).catch((error) => {
1015
+ const oauthChallenge = createOAuthChallengeResponse(error);
1016
+ if (oauthChallenge) {
1017
+ throw oauthChallenge;
1018
+ }
1019
+ throw error;
1020
+ });
1021
+ if (request) {
1022
+ request[authResultByRequest] = authResult;
1023
+ }
1024
+ return authResult;
1025
+ }
2261
1026
  function removeEmptyTopLevel(obj) {
2262
1027
  const out = {};
2263
1028
  for (const [k, v] of Object.entries(obj)) {
@@ -2270,7 +1035,7 @@ function removeEmptyTopLevel(obj) {
2270
1035
  }
2271
1036
  return out;
2272
1037
  }
2273
- var searchDomainSchema = z4.string().trim().toLowerCase().min(1).max(253).regex(
1038
+ var searchDomainSchema = z3.string().trim().toLowerCase().min(1).max(253).regex(
2274
1039
  /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/,
2275
1040
  "Domain must be a valid hostname without protocol or path"
2276
1041
  );
@@ -2330,48 +1095,23 @@ var server = new FastMCP({
2330
1095
  resource: getMcpResourceUrl(),
2331
1096
  resourceName: "Firecrawl MCP",
2332
1097
  scopesSupported: ["firecrawl:global"]
2333
- },
2334
- protectedResourceMetadataUrl: getOAuthProtectedResourceMetadataUrl()
2335
- },
2336
- authenticate: async (request) => {
2337
- const headerCred = request?.headers ? await resolveCredentialFromHeaders(request.headers) : void 0;
2338
- const envCred = resolveCredentialFromEnv();
2339
- if (process.env.CLOUD_SERVICE === "true") {
2340
- if (!headerCred) {
2341
- const clientIp = extractClientIp(request);
2342
- if (process.env.KEYLESS_PROXY_SECRET && clientIp && await keylessEligible(clientIp)) {
2343
- return { firecrawlApiKey: void 0, keylessClientIp: clientIp };
2344
- }
2345
- throw new Error(
2346
- "Firecrawl credentials required: OAuth access token (Authorization: Bearer fco_...) or API key (x-firecrawl-api-key)"
2347
- );
2348
- }
2349
- return { firecrawlApiKey: headerCred };
2350
1098
  }
2351
- const credential = headerCred ?? envCred;
2352
- const httpStreaming = isHttpStreamingTransport();
2353
- if (!httpStreaming && !process.env.FIRECRAWL_API_KEY && !process.env.FIRECRAWL_API_URL) {
2354
- console.error(
2355
- "No FIRECRAWL_API_KEY or FIRECRAWL_API_URL set \u2014 running in keyless mode. firecrawl_scrape and firecrawl_search are free (rate-limited per IP) against the Firecrawl cloud; other tools require an API key (get one free at https://firecrawl.dev)."
2356
- );
2357
- }
2358
- if (httpStreaming && !credential && !process.env.FIRECRAWL_API_URL) {
2359
- console.error(
2360
- "HTTP MCP transport requires FIRECRAWL_API_URL and/or credentials (OAuth: Authorization Bearer fco_..., or FIRECRAWL_API_KEY / FIRECRAWL_OAUTH_TOKEN)"
2361
- );
2362
- process.exit(1);
2363
- }
2364
- return { firecrawlApiKey: credential };
2365
1099
  },
1100
+ authenticate: authenticateWithOAuthChallenge,
2366
1101
  // Lightweight health endpoint for LB checks
2367
1102
  health: {
2368
1103
  enabled: true,
2369
1104
  message: "ok",
2370
1105
  path: "/health",
2371
1106
  status: 200
2372
- },
2373
- ...openAiAppsChallengeToken ? { openaiAppsChallenge: { token: openAiAppsChallengeToken } } : {}
1107
+ }
2374
1108
  });
1109
+ if (openAiAppsChallengeToken) {
1110
+ server.getApp().get(
1111
+ "/.well-known/openai-apps-challenge",
1112
+ (context) => context.text(openAiAppsChallengeToken)
1113
+ );
1114
+ }
2375
1115
  function createClient(apiKey) {
2376
1116
  const config = {
2377
1117
  ...process.env.FIRECRAWL_API_URL && {
@@ -2468,10 +1208,10 @@ function transformScrapeParams(args2) {
2468
1208
  delete out.pdfOptions;
2469
1209
  return out;
2470
1210
  }
2471
- var scrapeParamsSchema = z4.object({
2472
- url: z4.string().url(),
2473
- formats: z4.array(
2474
- z4.enum([
1211
+ var scrapeParamsSchema = z3.object({
1212
+ url: z3.string().url(),
1213
+ formats: z3.array(
1214
+ z3.enum([
2475
1215
  "markdown",
2476
1216
  "html",
2477
1217
  "rawHtml",
@@ -2485,62 +1225,62 @@ var scrapeParamsSchema = z4.object({
2485
1225
  "audio"
2486
1226
  ])
2487
1227
  ).optional(),
2488
- jsonOptions: z4.object({
2489
- prompt: z4.string().optional(),
2490
- schema: z4.record(z4.string(), z4.any()).optional()
1228
+ jsonOptions: z3.object({
1229
+ prompt: z3.string().optional(),
1230
+ schema: z3.record(z3.string(), z3.any()).optional()
2491
1231
  }).optional(),
2492
- queryOptions: z4.object({
2493
- prompt: z4.string().max(1e4),
2494
- mode: z4.enum(["directQuote", "freeform"]).default("freeform")
1232
+ queryOptions: z3.object({
1233
+ prompt: z3.string().max(1e4),
1234
+ mode: z3.enum(["directQuote", "freeform"]).default("freeform")
2495
1235
  }).optional(),
2496
- screenshotOptions: z4.object({
2497
- fullPage: z4.boolean().optional(),
2498
- quality: z4.number().optional(),
2499
- viewport: z4.object({ width: z4.number(), height: z4.number() }).optional()
1236
+ screenshotOptions: z3.object({
1237
+ fullPage: z3.boolean().optional(),
1238
+ quality: z3.number().optional(),
1239
+ viewport: z3.object({ width: z3.number(), height: z3.number() }).optional()
2500
1240
  }).optional(),
2501
- parsers: z4.array(z4.enum(["pdf"])).optional(),
2502
- pdfOptions: z4.object({
2503
- maxPages: z4.number().int().min(1).max(1e4).optional()
1241
+ parsers: z3.array(z3.enum(["pdf"])).optional(),
1242
+ pdfOptions: z3.object({
1243
+ maxPages: z3.number().int().min(1).max(1e4).optional()
2504
1244
  }).optional(),
2505
- onlyMainContent: z4.boolean().optional(),
2506
- redactPII: z4.boolean().optional(),
2507
- includeTags: z4.array(z4.string()).optional(),
2508
- excludeTags: z4.array(z4.string()).optional(),
2509
- waitFor: z4.number().optional(),
1245
+ onlyMainContent: z3.boolean().optional(),
1246
+ redactPII: z3.boolean().optional(),
1247
+ includeTags: z3.array(z3.string()).optional(),
1248
+ excludeTags: z3.array(z3.string()).optional(),
1249
+ waitFor: z3.number().optional(),
2510
1250
  ...SAFE_MODE ? {} : {
2511
- actions: z4.array(
2512
- z4.object({
2513
- type: z4.enum(allowedActionTypes),
2514
- selector: z4.string().optional(),
2515
- milliseconds: z4.number().optional(),
2516
- text: z4.string().optional(),
2517
- key: z4.string().optional(),
2518
- direction: z4.enum(["up", "down"]).optional(),
2519
- script: z4.string().optional(),
2520
- fullPage: z4.boolean().optional()
1251
+ actions: z3.array(
1252
+ z3.object({
1253
+ type: z3.enum(allowedActionTypes),
1254
+ selector: z3.string().optional(),
1255
+ milliseconds: z3.number().optional(),
1256
+ text: z3.string().optional(),
1257
+ key: z3.string().optional(),
1258
+ direction: z3.enum(["up", "down"]).optional(),
1259
+ script: z3.string().optional(),
1260
+ fullPage: z3.boolean().optional()
2521
1261
  })
2522
1262
  ).optional()
2523
1263
  },
2524
- mobile: z4.boolean().optional(),
2525
- skipTlsVerification: z4.boolean().optional(),
2526
- removeBase64Images: z4.boolean().optional(),
2527
- location: z4.object({
2528
- country: z4.string().optional(),
2529
- languages: z4.array(z4.string()).optional()
1264
+ mobile: z3.boolean().optional(),
1265
+ skipTlsVerification: z3.boolean().optional(),
1266
+ removeBase64Images: z3.boolean().optional(),
1267
+ location: z3.object({
1268
+ country: z3.string().optional(),
1269
+ languages: z3.array(z3.string()).optional()
2530
1270
  }).optional(),
2531
- storeInCache: z4.boolean().optional(),
2532
- zeroDataRetention: z4.boolean().optional(),
2533
- maxAge: z4.number().optional(),
2534
- lockdown: z4.boolean().optional(),
2535
- proxy: z4.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
2536
- profile: z4.object({
2537
- name: z4.string(),
2538
- saveChanges: z4.boolean().optional()
1271
+ storeInCache: z3.boolean().optional(),
1272
+ zeroDataRetention: z3.boolean().optional(),
1273
+ maxAge: z3.number().optional(),
1274
+ lockdown: z3.boolean().optional(),
1275
+ proxy: z3.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
1276
+ profile: z3.object({
1277
+ name: z3.string(),
1278
+ saveChanges: z3.boolean().optional()
2539
1279
  }).optional()
2540
1280
  });
2541
- var parseOptionParamsSchema = z4.object({
2542
- formats: z4.array(
2543
- z4.enum([
1281
+ var parseOptionParamsSchema = z3.object({
1282
+ formats: z3.array(
1283
+ z3.enum([
2544
1284
  "markdown",
2545
1285
  "html",
2546
1286
  "rawHtml",
@@ -2550,48 +1290,48 @@ var parseOptionParamsSchema = z4.object({
2550
1290
  "query"
2551
1291
  ])
2552
1292
  ).optional(),
2553
- jsonOptions: z4.object({
2554
- prompt: z4.string().optional(),
2555
- schema: z4.record(z4.string(), z4.any()).optional()
1293
+ jsonOptions: z3.object({
1294
+ prompt: z3.string().optional(),
1295
+ schema: z3.record(z3.string(), z3.any()).optional()
2556
1296
  }).optional(),
2557
- queryOptions: z4.object({
2558
- prompt: z4.string().max(1e4),
2559
- mode: z4.enum(["directQuote", "freeform"]).default("freeform")
1297
+ queryOptions: z3.object({
1298
+ prompt: z3.string().max(1e4),
1299
+ mode: z3.enum(["directQuote", "freeform"]).default("freeform")
2560
1300
  }).optional(),
2561
- parsers: z4.array(z4.enum(["pdf"])).optional(),
2562
- pdfOptions: z4.object({
2563
- maxPages: z4.number().int().min(1).max(1e4).optional()
1301
+ parsers: z3.array(z3.enum(["pdf"])).optional(),
1302
+ pdfOptions: z3.object({
1303
+ maxPages: z3.number().int().min(1).max(1e4).optional()
2564
1304
  }).optional(),
2565
- onlyMainContent: z4.boolean().optional(),
2566
- redactPII: z4.boolean().optional(),
2567
- includeTags: z4.array(z4.string()).optional(),
2568
- excludeTags: z4.array(z4.string()).optional(),
2569
- removeBase64Images: z4.boolean().optional(),
2570
- skipTlsVerification: z4.boolean().optional(),
2571
- storeInCache: z4.boolean().optional(),
2572
- zeroDataRetention: z4.boolean().optional(),
2573
- maxAge: z4.number().optional(),
2574
- proxy: z4.enum(["basic", "auto"]).optional()
1305
+ onlyMainContent: z3.boolean().optional(),
1306
+ redactPII: z3.boolean().optional(),
1307
+ includeTags: z3.array(z3.string()).optional(),
1308
+ excludeTags: z3.array(z3.string()).optional(),
1309
+ removeBase64Images: z3.boolean().optional(),
1310
+ skipTlsVerification: z3.boolean().optional(),
1311
+ storeInCache: z3.boolean().optional(),
1312
+ zeroDataRetention: z3.boolean().optional(),
1313
+ maxAge: z3.number().optional(),
1314
+ proxy: z3.enum(["basic", "auto"]).optional()
2575
1315
  });
2576
1316
  var localParseParamsSchema = parseOptionParamsSchema.extend({
2577
- filePath: z4.string().min(1).describe(
1317
+ filePath: z3.string().min(1).describe(
2578
1318
  "Absolute or relative path to a local file to parse. Supported: .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls"
2579
1319
  ),
2580
- contentType: z4.string().optional().describe(
1320
+ contentType: z3.string().optional().describe(
2581
1321
  "Optional MIME type override. If omitted, the server infers the file kind from the extension."
2582
1322
  )
2583
1323
  });
2584
1324
  var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2585
- filePath: z4.string().min(1).optional().describe(
1325
+ filePath: z3.string().min(1).optional().describe(
2586
1326
  "Phase 1 only: path to the local file on the caller/harness machine. Hosted MCP will not read or stat this path; it is used only to produce upload instructions."
2587
1327
  ),
2588
- uploadRef: z4.string().min(1).optional().describe(
1328
+ uploadRef: z3.string().min(1).optional().describe(
2589
1329
  "Phase 2 only: short-lived upload reference returned by phase 1 after the local PUT upload completes."
2590
1330
  ),
2591
- contentType: z4.string().optional().describe(
1331
+ contentType: z3.string().optional().describe(
2592
1332
  "Phase 1 MIME type override. If omitted, the server infers it from the file extension without reading the file."
2593
1333
  ),
2594
- declaredSizeBytes: z4.number().int().positive().optional().describe(
1334
+ declaredSizeBytes: z3.number().int().positive().optional().describe(
2595
1335
  "Optional phase 1 size declaration. Hosted MCP does not stat the file; provide this only if the caller already knows it."
2596
1336
  )
2597
1337
  }).superRefine((value, ctx) => {
@@ -2599,7 +1339,7 @@ var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2599
1339
  const hasUploadRef = typeof value.uploadRef === "string" && value.uploadRef.length > 0;
2600
1340
  if (hasFilePath === hasUploadRef) {
2601
1341
  ctx.addIssue({
2602
- code: z4.ZodIssueCode.custom,
1342
+ code: z3.ZodIssueCode.custom,
2603
1343
  message: "Hosted firecrawl_parse requires exactly one of filePath (phase 1) or uploadRef (phase 2).",
2604
1344
  path: hasFilePath && hasUploadRef ? ["uploadRef"] : ["filePath"]
2605
1345
  });
@@ -2623,7 +1363,11 @@ function inferContentType(filename) {
2623
1363
  return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
2624
1364
  }
2625
1365
  function extractParseOptions(args2) {
2626
- const { filePath, uploadRef, contentType, declaredSizeBytes, ...options } = args2;
1366
+ const options = { ...args2 };
1367
+ delete options.filePath;
1368
+ delete options.uploadRef;
1369
+ delete options.contentType;
1370
+ delete options.declaredSizeBytes;
2627
1371
  return options;
2628
1372
  }
2629
1373
  function buildParseOptionsPayload(options) {
@@ -2966,13 +1710,13 @@ Map a website to discover all indexed URLs on the site.
2966
1710
  \`\`\`
2967
1711
  **Returns:** Array of URLs found on the site, filtered by search query if provided.
2968
1712
  `,
2969
- parameters: z4.object({
2970
- url: z4.string().url(),
2971
- search: z4.string().optional(),
2972
- sitemap: z4.enum(["include", "skip", "only"]).optional(),
2973
- includeSubdomains: z4.boolean().optional(),
2974
- limit: z4.number().optional(),
2975
- ignoreQueryParameters: z4.boolean().optional()
1713
+ parameters: z3.object({
1714
+ url: z3.string().url(),
1715
+ search: z3.string().optional(),
1716
+ sitemap: z3.enum(["include", "skip", "only"]).optional(),
1717
+ includeSubdomains: z3.boolean().optional(),
1718
+ limit: z3.number().optional(),
1719
+ ignoreQueryParameters: z3.boolean().optional()
2976
1720
  }),
2977
1721
  execute: async (args2, { session, log }) => {
2978
1722
  const { url, ...options } = args2;
@@ -3003,7 +1747,7 @@ Search the web and optionally extract content from search results. This is the m
3003
1747
  The query also supports search operators, that you can use if needed to refine the search:
3004
1748
  | Operator | Functionality | Examples |
3005
1749
  ---|-|-|
3006
- | \`""\` | Non-fuzzy matches a string of text | \`"Firecrawl"\`
1750
+ | \`"\` | Non-fuzzy matches a string of text | \`"Firecrawl"\`
3007
1751
  | \`-\` | Excludes certain keywords or negates other operators | \`-bad\`, \`-site:firecrawl.dev\`
3008
1752
  | \`site:\` | Only returns results from a specified website | \`site:firecrawl.dev\`
3009
1753
  | \`inurl:\` | Only returns results that include a word in the URL | \`inurl:firecrawl\`
@@ -3019,6 +1763,7 @@ The query also supports search operators, that you can use if needed to refine t
3019
1763
  **Common mistakes:** Using crawl or map for open-ended questions (use search instead).
3020
1764
  **Prompt Example:** "Find the latest research papers on AI published in 2023."
3021
1765
  **Sources:** web, images, news, default to web unless needed images or news.
1766
+ **Categories:** Optional filter to limit result types: \`github\` (GitHub repositories, code, issues, and docs), \`research\` (academic and research sources), \`pdf\` (PDF results). Example: \`categories: ["github", "research"]\`.
3022
1767
  **Domain filters:** Use includeDomains to restrict results to specific domains, or excludeDomains to remove domains. Do not use both in the same request. Domains must be hostnames only, without protocol or path.
3023
1768
  **Scrape Options:** Only use scrapeOptions when you think it is absolutely necessary. When you do so default to a lower limit to avoid timeouts, 5 or lower.
3024
1769
  **Optimal Workflow:** Search first using firecrawl_search without formats, then after fetching the results, use the scrape tool to get the content of the relevantpage(s) that you want to scrape
@@ -3045,6 +1790,7 @@ The query also supports search operators, that you can use if needed to refine t
3045
1790
  "arguments": {
3046
1791
  "query": "latest AI research papers 2023",
3047
1792
  "limit": 5,
1793
+ "categories": ["github", "research"],
3048
1794
  "lang": "en",
3049
1795
  "country": "us",
3050
1796
  "sources": [
@@ -3061,17 +1807,20 @@ The query also supports search operators, that you can use if needed to refine t
3061
1807
  \`\`\`
3062
1808
  **Returns:** A JSON envelope of the form \`{ success, data: { web?, images?, news? }, id, creditsUsed }\`. Each result array contains the search results (with optional scraped content). Pass the top-level \`id\` to \`firecrawl_search_feedback\` after you've used the results.
3063
1809
  `,
3064
- parameters: z4.object({
3065
- query: z4.string().min(1),
3066
- limit: z4.number().optional(),
3067
- tbs: z4.string().optional(),
3068
- filter: z4.string().optional(),
3069
- location: z4.string().optional(),
3070
- includeDomains: z4.array(searchDomainSchema).optional(),
3071
- excludeDomains: z4.array(searchDomainSchema).optional(),
3072
- sources: z4.array(z4.object({ type: z4.enum(["web", "images", "news"]) })).optional(),
1810
+ parameters: z3.object({
1811
+ query: z3.string().min(1),
1812
+ limit: z3.number().optional(),
1813
+ tbs: z3.string().optional(),
1814
+ filter: z3.string().optional(),
1815
+ location: z3.string().optional(),
1816
+ includeDomains: z3.array(searchDomainSchema).optional(),
1817
+ excludeDomains: z3.array(searchDomainSchema).optional(),
1818
+ sources: z3.array(z3.object({ type: z3.enum(["web", "images", "news"]) })).optional(),
1819
+ categories: z3.array(z3.enum(["github", "research", "pdf"])).optional().describe(
1820
+ "Limit results to specific source types. `github` searches GitHub repositories, code, issues, and docs; `research` searches academic and research sources; `pdf` searches PDF results."
1821
+ ),
3073
1822
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional(),
3074
- enterprise: z4.array(z4.enum(["default", "anon", "zdr"])).optional()
1823
+ enterprise: z3.array(z3.enum(["default", "anon", "zdr"])).optional()
3075
1824
  }).refine(
3076
1825
  (args2) => !(args2.includeDomains?.length && args2.excludeDomains?.length),
3077
1826
  "includeDomains and excludeDomains cannot both be specified"
@@ -3214,7 +1963,7 @@ async function getCrawlStatusWithOrigin(client, jobId) {
3214
1963
  }
3215
1964
  async function waitForCrawlCompletionWithOrigin(client, jobId, pollInterval = 2, timeout) {
3216
1965
  const startedAt = Date.now();
3217
- while (true) {
1966
+ for (; ; ) {
3218
1967
  const status = await getCrawlStatusWithOrigin(client, jobId);
3219
1968
  if (["completed", "failed", "cancelled"].includes(String(status.status ?? ""))) {
3220
1969
  return status;
@@ -3227,17 +1976,17 @@ async function waitForCrawlCompletionWithOrigin(client, jobId, pollInterval = 2,
3227
1976
  );
3228
1977
  }
3229
1978
  }
3230
- var feedbackIssueSchema = z4.string().trim().min(1).max(80).regex(
1979
+ var feedbackIssueSchema = z3.string().trim().min(1).max(80).regex(
3231
1980
  /^[a-z0-9][a-z0-9_-]*$/,
3232
1981
  "Issue codes must use lowercase letters, numbers, underscores, or hyphens"
3233
1982
  );
3234
- var valuableSourceSchema = z4.object({
3235
- url: z4.string().url(),
3236
- reason: z4.string().max(1e3).optional()
1983
+ var valuableSourceSchema = z3.object({
1984
+ url: z3.string().url(),
1985
+ reason: z3.string().max(1e3).optional()
3237
1986
  });
3238
- var missingContentSchema = z4.object({
3239
- topic: z4.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3240
- description: z4.string().max(2e3).optional()
1987
+ var missingContentSchema = z3.object({
1988
+ topic: z3.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
1989
+ description: z3.string().max(2e3).optional()
3241
1990
  });
3242
1991
  var FEEDBACK_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
3243
1992
  function feedbackEnvEnabled(...keys) {
@@ -3331,24 +2080,24 @@ Pass the \`searchId\` returned by \`firecrawl_search\` (the \`id\` field on the
3331
2080
 
3332
2081
  **Returns:** \`{ success, feedbackId, creditsRefunded, creditsRefundedToday, dailyRefundCap, dailyCapReached?, alreadySubmitted?, warning? }\` JSON.
3333
2082
  `,
3334
- parameters: z4.object({
3335
- searchId: z4.string().uuid("searchId must be the UUID returned by firecrawl_search"),
3336
- rating: z4.enum(["good", "bad", "partial"]),
3337
- valuableSources: z4.array(
3338
- z4.object({
3339
- url: z4.string().url(),
3340
- reason: z4.string().max(1e3).optional()
2083
+ parameters: z3.object({
2084
+ searchId: z3.string().uuid("searchId must be the UUID returned by firecrawl_search"),
2085
+ rating: z3.enum(["good", "bad", "partial"]),
2086
+ valuableSources: z3.array(
2087
+ z3.object({
2088
+ url: z3.string().url(),
2089
+ reason: z3.string().max(1e3).optional()
3341
2090
  })
3342
2091
  ).max(50).optional(),
3343
- missingContent: z4.array(
3344
- z4.object({
3345
- topic: z4.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3346
- description: z4.string().max(2e3).optional()
2092
+ missingContent: z3.array(
2093
+ z3.object({
2094
+ topic: z3.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
2095
+ description: z3.string().max(2e3).optional()
3347
2096
  })
3348
2097
  ).max(20).optional().describe(
3349
2098
  "Array of specific pieces of content the agent expected to find but did not. One entry per distinct topic. Each entry has a short `topic` and optional longer `description`."
3350
2099
  ),
3351
- querySuggestions: z4.string().max(2e3).optional()
2100
+ querySuggestions: z3.string().max(2e3).optional()
3352
2101
  }),
3353
2102
  execute: async (args2, { session, log }) => {
3354
2103
  const {
@@ -3447,19 +2196,19 @@ Do not store multi-MB outputs in feedback. Use concise notes, issue codes, URLs,
3447
2196
 
3448
2197
  **Returns:** \`{ success, feedbackId, creditsRefunded, creditsRefundedToday?, dailyRefundCap?, dailyCapReached?, alreadySubmitted?, warning? }\` JSON.
3449
2198
  `,
3450
- parameters: z4.object({
3451
- endpoint: z4.enum(["search", "scrape", "parse", "map"]),
3452
- jobId: z4.string().uuid("jobId must be the UUID returned by Firecrawl"),
3453
- rating: z4.enum(["good", "bad", "partial"]),
3454
- issues: z4.array(feedbackIssueSchema).max(20).optional(),
3455
- tags: z4.array(feedbackIssueSchema).max(20).optional(),
3456
- note: z4.string().max(4e3).optional(),
3457
- valuableSources: z4.array(valuableSourceSchema).max(50).optional(),
3458
- missingContent: z4.array(missingContentSchema).max(50).optional(),
3459
- querySuggestions: z4.string().max(2e3).optional(),
3460
- url: z4.string().url().optional(),
3461
- pageNumbers: z4.array(z4.number().int().positive()).max(100).optional(),
3462
- metadata: z4.record(z4.string(), z4.unknown()).optional()
2199
+ parameters: z3.object({
2200
+ endpoint: z3.enum(["search", "scrape", "parse", "map"]),
2201
+ jobId: z3.string().uuid("jobId must be the UUID returned by Firecrawl"),
2202
+ rating: z3.enum(["good", "bad", "partial"]),
2203
+ issues: z3.array(feedbackIssueSchema).max(20).optional(),
2204
+ tags: z3.array(feedbackIssueSchema).max(20).optional(),
2205
+ note: z3.string().max(4e3).optional(),
2206
+ valuableSources: z3.array(valuableSourceSchema).max(50).optional(),
2207
+ missingContent: z3.array(missingContentSchema).max(50).optional(),
2208
+ querySuggestions: z3.string().max(2e3).optional(),
2209
+ url: z3.string().url().optional(),
2210
+ pageNumbers: z3.array(z3.number().int().positive()).max(100).optional(),
2211
+ metadata: z3.record(z3.string(), z3.unknown()).optional()
3463
2212
  }),
3464
2213
  execute: async (args2, { session, log }) => {
3465
2214
  const {
@@ -3568,25 +2317,25 @@ server.addTool({
3568
2317
  **Returns:** Final crawl status and data after internal polling, including the crawl id. Use firecrawl_check_crawl_status only when you need to re-check an existing crawl ID later.
3569
2318
  ${SAFE_MODE ? "**Safe Mode:** Read-only crawling. Webhooks and interactive actions are disabled for security." : ""}
3570
2319
  `,
3571
- parameters: z4.object({
3572
- url: z4.string(),
3573
- prompt: z4.string().optional(),
3574
- excludePaths: z4.array(z4.string()).optional(),
3575
- includePaths: z4.array(z4.string()).optional(),
3576
- maxDiscoveryDepth: z4.number().optional(),
3577
- sitemap: z4.enum(["skip", "include", "only"]).optional(),
3578
- limit: z4.number().optional(),
3579
- allowExternalLinks: z4.boolean().optional(),
3580
- allowSubdomains: z4.boolean().optional(),
3581
- crawlEntireDomain: z4.boolean().optional(),
3582
- delay: z4.number().optional(),
3583
- maxConcurrency: z4.number().optional(),
2320
+ parameters: z3.object({
2321
+ url: z3.string(),
2322
+ prompt: z3.string().optional(),
2323
+ excludePaths: z3.array(z3.string()).optional(),
2324
+ includePaths: z3.array(z3.string()).optional(),
2325
+ maxDiscoveryDepth: z3.number().optional(),
2326
+ sitemap: z3.enum(["skip", "include", "only"]).optional(),
2327
+ limit: z3.number().optional(),
2328
+ allowExternalLinks: z3.boolean().optional(),
2329
+ allowSubdomains: z3.boolean().optional(),
2330
+ crawlEntireDomain: z3.boolean().optional(),
2331
+ delay: z3.number().optional(),
2332
+ maxConcurrency: z3.number().optional(),
3584
2333
  ...SAFE_MODE ? {} : {
3585
- webhook: z4.string().optional(),
3586
- webhookHeaders: z4.record(z4.string(), z4.string()).optional()
2334
+ webhook: z3.string().optional(),
2335
+ webhookHeaders: z3.record(z3.string(), z3.string()).optional()
3587
2336
  },
3588
- deduplicateSimilarURLs: z4.boolean().optional(),
3589
- ignoreQueryParameters: z4.boolean().optional(),
2337
+ deduplicateSimilarURLs: z3.boolean().optional(),
2338
+ ignoreQueryParameters: z3.boolean().optional(),
3590
2339
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3591
2340
  }),
3592
2341
  execute: async (args2, { session, log }) => {
@@ -3650,7 +2399,7 @@ Check the status of a crawl job.
3650
2399
  \`\`\`
3651
2400
  **Returns:** Status and progress of the crawl job, including results if available.
3652
2401
  `,
3653
- parameters: z4.object({ id: z4.string() }),
2402
+ parameters: z3.object({ id: z3.string() }),
3654
2403
  execute: async (args2, { session }) => {
3655
2404
  const client = getClient(session);
3656
2405
  const id = args2.id;
@@ -3706,13 +2455,13 @@ Extract structured information from web pages using LLM capabilities. Supports b
3706
2455
  \`\`\`
3707
2456
  **Returns:** Extracted structured data as defined by your schema.
3708
2457
  `,
3709
- parameters: z4.object({
3710
- urls: z4.array(z4.string()),
3711
- prompt: z4.string().optional(),
3712
- schema: z4.record(z4.string(), z4.any()).optional(),
3713
- allowExternalLinks: z4.boolean().optional(),
3714
- enableWebSearch: z4.boolean().optional(),
3715
- includeSubdomains: z4.boolean().optional()
2458
+ parameters: z3.object({
2459
+ urls: z3.array(z3.string()),
2460
+ prompt: z3.string().optional(),
2461
+ schema: z3.record(z3.string(), z3.any()).optional(),
2462
+ allowExternalLinks: z3.boolean().optional(),
2463
+ enableWebSearch: z3.boolean().optional(),
2464
+ includeSubdomains: z3.boolean().optional()
3716
2465
  }),
3717
2466
  execute: async (args2, { session, log }) => {
3718
2467
  const client = getClient(session);
@@ -3813,10 +2562,10 @@ Then poll with \`firecrawl_agent_status\` every 15-30 seconds for at least 2-3 m
3813
2562
  \`\`\`
3814
2563
  **Returns:** Job ID for status checking. Use \`firecrawl_agent_status\` to poll for results.
3815
2564
  `,
3816
- parameters: z4.object({
3817
- prompt: z4.string().min(1).max(1e4),
3818
- urls: z4.array(z4.string().url()).optional(),
3819
- schema: z4.record(z4.string(), z4.any()).optional()
2565
+ parameters: z3.object({
2566
+ prompt: z3.string().min(1).max(1e4),
2567
+ urls: z3.array(z3.string().url()).optional(),
2568
+ schema: z3.record(z3.string(), z3.any()).optional()
3820
2569
  }),
3821
2570
  execute: async (args2, { session, log }) => {
3822
2571
  const client = getClient(session);
@@ -3873,7 +2622,7 @@ Check the status of an agent job and retrieve results when complete. Use this to
3873
2622
 
3874
2623
  **Returns:** Status, progress, and results (if completed) of the agent job.
3875
2624
  `,
3876
- parameters: z4.object({ id: z4.string() }),
2625
+ parameters: z3.object({ id: z3.string() }),
3877
2626
  execute: async (args2, { session, log }) => {
3878
2627
  const client = getClient(session);
3879
2628
  const { id } = args2;
@@ -3937,13 +2686,13 @@ Interact with a page in a live browser session: click buttons, fill forms, extra
3937
2686
  \`\`\`
3938
2687
  **Returns:** Execution result including output, stdout, stderr, exit code, and live view URLs.
3939
2688
  `,
3940
- parameters: z4.object({
3941
- scrapeId: z4.string().trim().min(1).optional(),
3942
- url: z4.string().trim().url().optional(),
3943
- prompt: z4.string().trim().min(1).optional(),
3944
- code: z4.string().trim().min(1).optional(),
3945
- language: z4.enum(["bash", "python", "node"]).optional(),
3946
- timeout: z4.number().min(1).max(300).optional(),
2689
+ parameters: z3.object({
2690
+ scrapeId: z3.string().trim().min(1).optional(),
2691
+ url: z3.string().trim().url().optional(),
2692
+ prompt: z3.string().trim().min(1).optional(),
2693
+ code: z3.string().trim().min(1).optional(),
2694
+ language: z3.enum(["bash", "python", "node"]).optional(),
2695
+ timeout: z3.number().min(1).max(300).optional(),
3947
2696
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3948
2697
  }).refine((data) => Boolean(data.scrapeId) !== Boolean(data.url), {
3949
2698
  message: "Provide either 'url' (interact directly) or 'scrapeId' (reuse a previous scrape), not both."
@@ -4031,8 +2780,8 @@ Stop an interact session for a scraped page. Call this when you are done interac
4031
2780
  \`\`\`
4032
2781
  **Returns:** Success confirmation.
4033
2782
  `,
4034
- parameters: z4.object({
4035
- scrapeId: z4.string()
2783
+ parameters: z3.object({
2784
+ scrapeId: z3.string()
4036
2785
  }),
4037
2786
  execute: async (args2, { session, log }) => {
4038
2787
  const client = getClient(session);