firecrawl-mcp 3.22.1 → 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.
- package/dist/index.js +342 -1586
- 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
|
|
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
|
|
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 =
|
|
1369
|
-
var checkStatusSchema =
|
|
55
|
+
var pageStatusSchema = z.enum(["same", "new", "changed", "removed", "error"]);
|
|
56
|
+
var checkStatusSchema = z.enum([
|
|
1370
57
|
"queued",
|
|
1371
58
|
"running",
|
|
1372
59
|
"completed",
|
|
@@ -1495,6 +182,13 @@ Goal guidance:
|
|
|
1495
182
|
- If the user says they do not care about something, include that explicitly. It is okay to ask whether they want to ignore specific noise when it is likely to matter.
|
|
1496
183
|
- Do not invent page-specific sections, thresholds, entities, or business rules unless the user mentioned them.
|
|
1497
184
|
|
|
185
|
+
Query guidance (web monitors): \`queries\` control recall (what search retrieves) and \`goal\` controls precision (which results alert) \u2014 tune both.
|
|
186
|
+
- Write keywords, not sentences: \`OpenAI new model release\`, not \`tell me when OpenAI releases a new model\`.
|
|
187
|
+
- Quote multi-word entities (\`"Llama 4"\`); group synonyms with \`OR\` (\`launch OR release OR announcement\`).
|
|
188
|
+
- Keep each query tight (~2-6 terms). One broad query usually beats several narrow ones \u2014 extra queries split the \`maxResults\` budget. Use one query per distinct entity; do not emit one per facet of a single subject.
|
|
189
|
+
- Keep \`site:\` operators out of queries \u2014 use \`includeDomains\` / \`excludeDomains\`.
|
|
190
|
+
- A healthy web monitor mostly returns \`new: 0\` and alerts only on genuinely new, on-goal results. Many \`ignored\` results \u21D2 queries too broad (tighten them); nothing for long stretches \u21D2 queries too narrow or window too tight (broaden); dismissed alerts \u21D2 goal too broad (add an intent-specific Ignore). Aim for high precision with enough recall.
|
|
191
|
+
|
|
1498
192
|
Full \`body\` requests require: \`name\`, \`schedule\` (with \`cron\` or \`text\`), and \`targets\` (one or more \`{ type: 'scrape', urls: [...] }\`, \`{ type: 'crawl', url: '...' }\`, or \`{ type: 'search', queries: [...], searchWindow?, maxResults?, includeDomains?, excludeDomains? }\`). Optional: \`goal\` (required when any search target is present), \`judgeEnabled\`, \`webhook\`, \`notification\`, \`retentionDays\`.
|
|
1499
193
|
|
|
1500
194
|
**Markdown-mode (default):** Each check produces a unified text diff of the page's markdown. No extra configuration needed.
|
|
@@ -1567,22 +261,22 @@ Full \`body\` requests require: \`name\`, \`schedule\` (with \`cron\` or \`text\
|
|
|
1567
261
|
|
|
1568
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.
|
|
1569
263
|
`,
|
|
1570
|
-
parameters:
|
|
1571
|
-
body:
|
|
1572
|
-
page:
|
|
1573
|
-
pages:
|
|
1574
|
-
queries:
|
|
1575
|
-
searchWindow:
|
|
1576
|
-
maxResults:
|
|
1577
|
-
includeDomains:
|
|
1578
|
-
excludeDomains:
|
|
1579
|
-
goal:
|
|
1580
|
-
name:
|
|
1581
|
-
scheduleText:
|
|
1582
|
-
timezone:
|
|
1583
|
-
email:
|
|
1584
|
-
includeDiffs:
|
|
1585
|
-
webhookUrl:
|
|
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()
|
|
1586
280
|
}),
|
|
1587
281
|
execute: async (args2, { session, log }) => {
|
|
1588
282
|
const body = buildMonitorCreateBody(args2);
|
|
@@ -1613,11 +307,11 @@ List all Firecrawl monitors for the authenticated account.
|
|
|
1613
307
|
{ "name": "firecrawl_monitor_list", "arguments": { "limit": 20 } }
|
|
1614
308
|
\`\`\`
|
|
1615
309
|
`,
|
|
1616
|
-
parameters:
|
|
1617
|
-
limit:
|
|
1618
|
-
offset:
|
|
310
|
+
parameters: z.object({
|
|
311
|
+
limit: z.number().int().positive().optional(),
|
|
312
|
+
offset: z.number().int().nonnegative().optional()
|
|
1619
313
|
}),
|
|
1620
|
-
execute: async (args2, { session
|
|
314
|
+
execute: async (args2, { session }) => {
|
|
1621
315
|
const { limit, offset } = args2;
|
|
1622
316
|
const res = await monitorRequest(session, "/monitor", {
|
|
1623
317
|
query: { limit, offset }
|
|
@@ -1644,8 +338,8 @@ Get a single monitor by ID.
|
|
|
1644
338
|
{ "name": "firecrawl_monitor_get", "arguments": { "id": "mon_abc123" } }
|
|
1645
339
|
\`\`\`
|
|
1646
340
|
`,
|
|
1647
|
-
parameters:
|
|
1648
|
-
execute: async (args2, { session
|
|
341
|
+
parameters: z.object({ id: z.string() }),
|
|
342
|
+
execute: async (args2, { session }) => {
|
|
1649
343
|
const { id } = args2;
|
|
1650
344
|
const res = await monitorRequest(
|
|
1651
345
|
session,
|
|
@@ -1679,11 +373,11 @@ Update a monitor. Pass any subset of fields to patch: \`name\`, \`status\` ("act
|
|
|
1679
373
|
}
|
|
1680
374
|
\`\`\`
|
|
1681
375
|
`,
|
|
1682
|
-
parameters:
|
|
1683
|
-
id:
|
|
1684
|
-
body:
|
|
376
|
+
parameters: z.object({
|
|
377
|
+
id: z.string(),
|
|
378
|
+
body: z.record(z.string(), z.any())
|
|
1685
379
|
}),
|
|
1686
|
-
execute: async (args2, { session
|
|
380
|
+
execute: async (args2, { session }) => {
|
|
1687
381
|
const { id, body } = args2;
|
|
1688
382
|
const res = await monitorRequest(
|
|
1689
383
|
session,
|
|
@@ -1712,7 +406,7 @@ Permanently delete a monitor and stop its schedule. This cannot be undone.
|
|
|
1712
406
|
{ "name": "firecrawl_monitor_delete", "arguments": { "id": "mon_abc123" } }
|
|
1713
407
|
\`\`\`
|
|
1714
408
|
`,
|
|
1715
|
-
parameters:
|
|
409
|
+
parameters: z.object({ id: z.string() }),
|
|
1716
410
|
execute: async (args2, { session, log }) => {
|
|
1717
411
|
const { id } = args2;
|
|
1718
412
|
log.info("Deleting monitor", { id });
|
|
@@ -1743,8 +437,8 @@ Trigger a monitor check immediately, outside its normal schedule. Returns the qu
|
|
|
1743
437
|
{ "name": "firecrawl_monitor_run", "arguments": { "id": "mon_abc123" } }
|
|
1744
438
|
\`\`\`
|
|
1745
439
|
`,
|
|
1746
|
-
parameters:
|
|
1747
|
-
execute: async (args2, { session
|
|
440
|
+
parameters: z.object({ id: z.string() }),
|
|
441
|
+
execute: async (args2, { session }) => {
|
|
1748
442
|
const { id } = args2;
|
|
1749
443
|
const res = await monitorRequest(
|
|
1750
444
|
session,
|
|
@@ -1773,13 +467,13 @@ List historical checks for a monitor.
|
|
|
1773
467
|
{ "name": "firecrawl_monitor_checks", "arguments": { "id": "mon_abc123", "limit": 10, "status": "completed" } }
|
|
1774
468
|
\`\`\`
|
|
1775
469
|
`,
|
|
1776
|
-
parameters:
|
|
1777
|
-
id:
|
|
1778
|
-
limit:
|
|
1779
|
-
offset:
|
|
470
|
+
parameters: z.object({
|
|
471
|
+
id: z.string(),
|
|
472
|
+
limit: z.number().int().positive().optional(),
|
|
473
|
+
offset: z.number().int().nonnegative().optional(),
|
|
1780
474
|
status: checkStatusSchema.optional()
|
|
1781
475
|
}),
|
|
1782
|
-
execute: async (args2, { session
|
|
476
|
+
execute: async (args2, { session }) => {
|
|
1783
477
|
const { id, limit, offset, status } = args2;
|
|
1784
478
|
const res = await monitorRequest(
|
|
1785
479
|
session,
|
|
@@ -1856,14 +550,14 @@ The endpoint paginates via a top-level \`next\` URL; this tool returns one page
|
|
|
1856
550
|
}
|
|
1857
551
|
\`\`\`
|
|
1858
552
|
`,
|
|
1859
|
-
parameters:
|
|
1860
|
-
id:
|
|
1861
|
-
checkId:
|
|
1862
|
-
limit:
|
|
1863
|
-
skip:
|
|
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(),
|
|
1864
558
|
pageStatus: pageStatusSchema.optional()
|
|
1865
559
|
}),
|
|
1866
|
-
execute: async (args2, { session
|
|
560
|
+
execute: async (args2, { session }) => {
|
|
1867
561
|
const { id, checkId, limit, skip, pageStatus } = args2;
|
|
1868
562
|
const res = await monitorRequest(
|
|
1869
563
|
session,
|
|
@@ -1876,7 +570,7 @@ The endpoint paginates via a top-level \`next\` URL; this tool returns one page
|
|
|
1876
570
|
}
|
|
1877
571
|
|
|
1878
572
|
// src/research.ts
|
|
1879
|
-
import { z as
|
|
573
|
+
import { z as z2 } from "zod";
|
|
1880
574
|
var BASE = "/v2/search/research";
|
|
1881
575
|
var ORIGIN_HEADERS = { "X-Origin": "mcp-fastmcp" };
|
|
1882
576
|
function appendParam(params, key, value) {
|
|
@@ -1987,26 +681,30 @@ function registerResearchTools(server2, getClient2) {
|
|
|
1987
681
|
server2.addTool({
|
|
1988
682
|
name: "firecrawl_research_search_papers",
|
|
1989
683
|
annotations: {
|
|
1990
|
-
title: "Search
|
|
684
|
+
title: "Search research papers",
|
|
1991
685
|
readOnlyHint: true,
|
|
1992
|
-
// Semantic search over indexed
|
|
686
|
+
// Semantic search over indexed paper metadata; returns ranked results only.
|
|
1993
687
|
openWorldHint: true,
|
|
1994
|
-
// Searches the
|
|
688
|
+
// Searches the Firecrawl research paper index.
|
|
1995
689
|
destructiveHint: false
|
|
1996
|
-
// Query-only; no writes to
|
|
690
|
+
// Query-only; no writes to external sources or the research index.
|
|
1997
691
|
},
|
|
1998
|
-
description: "Primary entry point for finding
|
|
1999
|
-
parameters:
|
|
2000
|
-
query:
|
|
2001
|
-
|
|
2002
|
-
|
|
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(
|
|
2003
699
|
"Author substring filter(s); ALL must match (case-insensitive)."
|
|
2004
700
|
),
|
|
2005
|
-
categories:
|
|
2006
|
-
|
|
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(
|
|
2007
705
|
"Inclusive lower bound on created/updated date (`YYYY-MM-DD`)."
|
|
2008
706
|
),
|
|
2009
|
-
to:
|
|
707
|
+
to: z2.string().optional().describe(
|
|
2010
708
|
"Inclusive upper bound on created/updated date (`YYYY-MM-DD`)."
|
|
2011
709
|
)
|
|
2012
710
|
}),
|
|
@@ -2039,8 +737,8 @@ function registerResearchTools(server2, getClient2) {
|
|
|
2039
737
|
// Read-only metadata lookup.
|
|
2040
738
|
},
|
|
2041
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.",
|
|
2042
|
-
parameters:
|
|
2043
|
-
paperId:
|
|
740
|
+
parameters: z2.object({
|
|
741
|
+
paperId: z2.string().min(1).describe(
|
|
2044
742
|
"Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
|
|
2045
743
|
)
|
|
2046
744
|
}),
|
|
@@ -2066,12 +764,12 @@ function registerResearchTools(server2, getClient2) {
|
|
|
2066
764
|
// Read-only graph query; no modifications.
|
|
2067
765
|
},
|
|
2068
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.",
|
|
2069
|
-
parameters:
|
|
2070
|
-
seed_ids:
|
|
2071
|
-
intent:
|
|
2072
|
-
mode:
|
|
2073
|
-
k:
|
|
2074
|
-
rerank:
|
|
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.")
|
|
2075
773
|
}),
|
|
2076
774
|
execute: async (args2, { session }) => {
|
|
2077
775
|
const { seed_ids, intent, mode, k, rerank } = args2;
|
|
@@ -2108,12 +806,12 @@ note: ${res.data.note}` : "";
|
|
|
2108
806
|
// Read-only passage retrieval.
|
|
2109
807
|
},
|
|
2110
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.",
|
|
2111
|
-
parameters:
|
|
2112
|
-
paperId:
|
|
809
|
+
parameters: z2.object({
|
|
810
|
+
paperId: z2.string().min(1).describe(
|
|
2113
811
|
"Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
|
|
2114
812
|
),
|
|
2115
|
-
question:
|
|
2116
|
-
k:
|
|
813
|
+
question: z2.string().min(1),
|
|
814
|
+
k: z2.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
|
|
2117
815
|
}),
|
|
2118
816
|
execute: async (args2, { session }) => {
|
|
2119
817
|
const { paperId, question, k } = args2;
|
|
@@ -2141,9 +839,9 @@ note: ${res.data.note}` : "";
|
|
|
2141
839
|
// Query-only; does not create issues, PRs, or modify repositories.
|
|
2142
840
|
},
|
|
2143
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.",
|
|
2144
|
-
parameters:
|
|
2145
|
-
query:
|
|
2146
|
-
k:
|
|
842
|
+
parameters: z2.object({
|
|
843
|
+
query: z2.string().min(1),
|
|
844
|
+
k: z2.number().int().min(1).max(100).optional()
|
|
2147
845
|
}),
|
|
2148
846
|
execute: async (args2, { session }) => {
|
|
2149
847
|
const { query, k } = args2;
|
|
@@ -2164,6 +862,7 @@ note: ${res.data.note}` : "";
|
|
|
2164
862
|
dotenv.config({ debug: false, quiet: true });
|
|
2165
863
|
var require2 = createRequire(import.meta.url);
|
|
2166
864
|
var { version: packageVersion } = require2("../package.json");
|
|
865
|
+
var authResultByRequest = /* @__PURE__ */ Symbol("firecrawlMcpAuthResult");
|
|
2167
866
|
function normalizeHeader(value) {
|
|
2168
867
|
if (value == null) return void 0;
|
|
2169
868
|
const v = Array.isArray(value) ? value[0] : value;
|
|
@@ -2201,6 +900,33 @@ function getMcpResourceUrl() {
|
|
|
2201
900
|
function getOAuthProtectedResourceMetadataUrl() {
|
|
2202
901
|
return `${new URL(getMcpResourceUrl()).origin}/.well-known/oauth-protected-resource`;
|
|
2203
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
|
+
}
|
|
2204
930
|
function getOAuthIntrospectionEndpoint() {
|
|
2205
931
|
return `${getOAuthIssuer()}/api/oauth/introspect`;
|
|
2206
932
|
}
|
|
@@ -2251,6 +977,52 @@ async function resolveCredentialFromHeaders(headers) {
|
|
|
2251
977
|
}
|
|
2252
978
|
return void 0;
|
|
2253
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
|
+
}
|
|
2254
1026
|
function removeEmptyTopLevel(obj) {
|
|
2255
1027
|
const out = {};
|
|
2256
1028
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -2263,7 +1035,7 @@ function removeEmptyTopLevel(obj) {
|
|
|
2263
1035
|
}
|
|
2264
1036
|
return out;
|
|
2265
1037
|
}
|
|
2266
|
-
var searchDomainSchema =
|
|
1038
|
+
var searchDomainSchema = z3.string().trim().toLowerCase().min(1).max(253).regex(
|
|
2267
1039
|
/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/,
|
|
2268
1040
|
"Domain must be a valid hostname without protocol or path"
|
|
2269
1041
|
);
|
|
@@ -2323,48 +1095,23 @@ var server = new FastMCP({
|
|
|
2323
1095
|
resource: getMcpResourceUrl(),
|
|
2324
1096
|
resourceName: "Firecrawl MCP",
|
|
2325
1097
|
scopesSupported: ["firecrawl:global"]
|
|
2326
|
-
},
|
|
2327
|
-
protectedResourceMetadataUrl: getOAuthProtectedResourceMetadataUrl()
|
|
2328
|
-
},
|
|
2329
|
-
authenticate: async (request) => {
|
|
2330
|
-
const headerCred = request?.headers ? await resolveCredentialFromHeaders(request.headers) : void 0;
|
|
2331
|
-
const envCred = resolveCredentialFromEnv();
|
|
2332
|
-
if (process.env.CLOUD_SERVICE === "true") {
|
|
2333
|
-
if (!headerCred) {
|
|
2334
|
-
const clientIp = extractClientIp(request);
|
|
2335
|
-
if (process.env.KEYLESS_PROXY_SECRET && clientIp && await keylessEligible(clientIp)) {
|
|
2336
|
-
return { firecrawlApiKey: void 0, keylessClientIp: clientIp };
|
|
2337
|
-
}
|
|
2338
|
-
throw new Error(
|
|
2339
|
-
"Firecrawl credentials required: OAuth access token (Authorization: Bearer fco_...) or API key (x-firecrawl-api-key)"
|
|
2340
|
-
);
|
|
2341
|
-
}
|
|
2342
|
-
return { firecrawlApiKey: headerCred };
|
|
2343
1098
|
}
|
|
2344
|
-
const credential = headerCred ?? envCred;
|
|
2345
|
-
const httpStreaming = isHttpStreamingTransport();
|
|
2346
|
-
if (!httpStreaming && !process.env.FIRECRAWL_API_KEY && !process.env.FIRECRAWL_API_URL) {
|
|
2347
|
-
console.error(
|
|
2348
|
-
"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)."
|
|
2349
|
-
);
|
|
2350
|
-
}
|
|
2351
|
-
if (httpStreaming && !credential && !process.env.FIRECRAWL_API_URL) {
|
|
2352
|
-
console.error(
|
|
2353
|
-
"HTTP MCP transport requires FIRECRAWL_API_URL and/or credentials (OAuth: Authorization Bearer fco_..., or FIRECRAWL_API_KEY / FIRECRAWL_OAUTH_TOKEN)"
|
|
2354
|
-
);
|
|
2355
|
-
process.exit(1);
|
|
2356
|
-
}
|
|
2357
|
-
return { firecrawlApiKey: credential };
|
|
2358
1099
|
},
|
|
1100
|
+
authenticate: authenticateWithOAuthChallenge,
|
|
2359
1101
|
// Lightweight health endpoint for LB checks
|
|
2360
1102
|
health: {
|
|
2361
1103
|
enabled: true,
|
|
2362
1104
|
message: "ok",
|
|
2363
1105
|
path: "/health",
|
|
2364
1106
|
status: 200
|
|
2365
|
-
}
|
|
2366
|
-
...openAiAppsChallengeToken ? { openaiAppsChallenge: { token: openAiAppsChallengeToken } } : {}
|
|
1107
|
+
}
|
|
2367
1108
|
});
|
|
1109
|
+
if (openAiAppsChallengeToken) {
|
|
1110
|
+
server.getApp().get(
|
|
1111
|
+
"/.well-known/openai-apps-challenge",
|
|
1112
|
+
(context) => context.text(openAiAppsChallengeToken)
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
2368
1115
|
function createClient(apiKey) {
|
|
2369
1116
|
const config = {
|
|
2370
1117
|
...process.env.FIRECRAWL_API_URL && {
|
|
@@ -2461,10 +1208,10 @@ function transformScrapeParams(args2) {
|
|
|
2461
1208
|
delete out.pdfOptions;
|
|
2462
1209
|
return out;
|
|
2463
1210
|
}
|
|
2464
|
-
var scrapeParamsSchema =
|
|
2465
|
-
url:
|
|
2466
|
-
formats:
|
|
2467
|
-
|
|
1211
|
+
var scrapeParamsSchema = z3.object({
|
|
1212
|
+
url: z3.string().url(),
|
|
1213
|
+
formats: z3.array(
|
|
1214
|
+
z3.enum([
|
|
2468
1215
|
"markdown",
|
|
2469
1216
|
"html",
|
|
2470
1217
|
"rawHtml",
|
|
@@ -2478,62 +1225,62 @@ var scrapeParamsSchema = z4.object({
|
|
|
2478
1225
|
"audio"
|
|
2479
1226
|
])
|
|
2480
1227
|
).optional(),
|
|
2481
|
-
jsonOptions:
|
|
2482
|
-
prompt:
|
|
2483
|
-
schema:
|
|
1228
|
+
jsonOptions: z3.object({
|
|
1229
|
+
prompt: z3.string().optional(),
|
|
1230
|
+
schema: z3.record(z3.string(), z3.any()).optional()
|
|
2484
1231
|
}).optional(),
|
|
2485
|
-
queryOptions:
|
|
2486
|
-
prompt:
|
|
2487
|
-
mode:
|
|
1232
|
+
queryOptions: z3.object({
|
|
1233
|
+
prompt: z3.string().max(1e4),
|
|
1234
|
+
mode: z3.enum(["directQuote", "freeform"]).default("freeform")
|
|
2488
1235
|
}).optional(),
|
|
2489
|
-
screenshotOptions:
|
|
2490
|
-
fullPage:
|
|
2491
|
-
quality:
|
|
2492
|
-
viewport:
|
|
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()
|
|
2493
1240
|
}).optional(),
|
|
2494
|
-
parsers:
|
|
2495
|
-
pdfOptions:
|
|
2496
|
-
maxPages:
|
|
1241
|
+
parsers: z3.array(z3.enum(["pdf"])).optional(),
|
|
1242
|
+
pdfOptions: z3.object({
|
|
1243
|
+
maxPages: z3.number().int().min(1).max(1e4).optional()
|
|
2497
1244
|
}).optional(),
|
|
2498
|
-
onlyMainContent:
|
|
2499
|
-
redactPII:
|
|
2500
|
-
includeTags:
|
|
2501
|
-
excludeTags:
|
|
2502
|
-
waitFor:
|
|
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(),
|
|
2503
1250
|
...SAFE_MODE ? {} : {
|
|
2504
|
-
actions:
|
|
2505
|
-
|
|
2506
|
-
type:
|
|
2507
|
-
selector:
|
|
2508
|
-
milliseconds:
|
|
2509
|
-
text:
|
|
2510
|
-
key:
|
|
2511
|
-
direction:
|
|
2512
|
-
script:
|
|
2513
|
-
fullPage:
|
|
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()
|
|
2514
1261
|
})
|
|
2515
1262
|
).optional()
|
|
2516
1263
|
},
|
|
2517
|
-
mobile:
|
|
2518
|
-
skipTlsVerification:
|
|
2519
|
-
removeBase64Images:
|
|
2520
|
-
location:
|
|
2521
|
-
country:
|
|
2522
|
-
languages:
|
|
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()
|
|
2523
1270
|
}).optional(),
|
|
2524
|
-
storeInCache:
|
|
2525
|
-
zeroDataRetention:
|
|
2526
|
-
maxAge:
|
|
2527
|
-
lockdown:
|
|
2528
|
-
proxy:
|
|
2529
|
-
profile:
|
|
2530
|
-
name:
|
|
2531
|
-
saveChanges:
|
|
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()
|
|
2532
1279
|
}).optional()
|
|
2533
1280
|
});
|
|
2534
|
-
var parseOptionParamsSchema =
|
|
2535
|
-
formats:
|
|
2536
|
-
|
|
1281
|
+
var parseOptionParamsSchema = z3.object({
|
|
1282
|
+
formats: z3.array(
|
|
1283
|
+
z3.enum([
|
|
2537
1284
|
"markdown",
|
|
2538
1285
|
"html",
|
|
2539
1286
|
"rawHtml",
|
|
@@ -2543,48 +1290,48 @@ var parseOptionParamsSchema = z4.object({
|
|
|
2543
1290
|
"query"
|
|
2544
1291
|
])
|
|
2545
1292
|
).optional(),
|
|
2546
|
-
jsonOptions:
|
|
2547
|
-
prompt:
|
|
2548
|
-
schema:
|
|
1293
|
+
jsonOptions: z3.object({
|
|
1294
|
+
prompt: z3.string().optional(),
|
|
1295
|
+
schema: z3.record(z3.string(), z3.any()).optional()
|
|
2549
1296
|
}).optional(),
|
|
2550
|
-
queryOptions:
|
|
2551
|
-
prompt:
|
|
2552
|
-
mode:
|
|
1297
|
+
queryOptions: z3.object({
|
|
1298
|
+
prompt: z3.string().max(1e4),
|
|
1299
|
+
mode: z3.enum(["directQuote", "freeform"]).default("freeform")
|
|
2553
1300
|
}).optional(),
|
|
2554
|
-
parsers:
|
|
2555
|
-
pdfOptions:
|
|
2556
|
-
maxPages:
|
|
1301
|
+
parsers: z3.array(z3.enum(["pdf"])).optional(),
|
|
1302
|
+
pdfOptions: z3.object({
|
|
1303
|
+
maxPages: z3.number().int().min(1).max(1e4).optional()
|
|
2557
1304
|
}).optional(),
|
|
2558
|
-
onlyMainContent:
|
|
2559
|
-
redactPII:
|
|
2560
|
-
includeTags:
|
|
2561
|
-
excludeTags:
|
|
2562
|
-
removeBase64Images:
|
|
2563
|
-
skipTlsVerification:
|
|
2564
|
-
storeInCache:
|
|
2565
|
-
zeroDataRetention:
|
|
2566
|
-
maxAge:
|
|
2567
|
-
proxy:
|
|
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()
|
|
2568
1315
|
});
|
|
2569
1316
|
var localParseParamsSchema = parseOptionParamsSchema.extend({
|
|
2570
|
-
filePath:
|
|
1317
|
+
filePath: z3.string().min(1).describe(
|
|
2571
1318
|
"Absolute or relative path to a local file to parse. Supported: .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls"
|
|
2572
1319
|
),
|
|
2573
|
-
contentType:
|
|
1320
|
+
contentType: z3.string().optional().describe(
|
|
2574
1321
|
"Optional MIME type override. If omitted, the server infers the file kind from the extension."
|
|
2575
1322
|
)
|
|
2576
1323
|
});
|
|
2577
1324
|
var hostedParseParamsSchema = parseOptionParamsSchema.extend({
|
|
2578
|
-
filePath:
|
|
1325
|
+
filePath: z3.string().min(1).optional().describe(
|
|
2579
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."
|
|
2580
1327
|
),
|
|
2581
|
-
uploadRef:
|
|
1328
|
+
uploadRef: z3.string().min(1).optional().describe(
|
|
2582
1329
|
"Phase 2 only: short-lived upload reference returned by phase 1 after the local PUT upload completes."
|
|
2583
1330
|
),
|
|
2584
|
-
contentType:
|
|
1331
|
+
contentType: z3.string().optional().describe(
|
|
2585
1332
|
"Phase 1 MIME type override. If omitted, the server infers it from the file extension without reading the file."
|
|
2586
1333
|
),
|
|
2587
|
-
declaredSizeBytes:
|
|
1334
|
+
declaredSizeBytes: z3.number().int().positive().optional().describe(
|
|
2588
1335
|
"Optional phase 1 size declaration. Hosted MCP does not stat the file; provide this only if the caller already knows it."
|
|
2589
1336
|
)
|
|
2590
1337
|
}).superRefine((value, ctx) => {
|
|
@@ -2592,7 +1339,7 @@ var hostedParseParamsSchema = parseOptionParamsSchema.extend({
|
|
|
2592
1339
|
const hasUploadRef = typeof value.uploadRef === "string" && value.uploadRef.length > 0;
|
|
2593
1340
|
if (hasFilePath === hasUploadRef) {
|
|
2594
1341
|
ctx.addIssue({
|
|
2595
|
-
code:
|
|
1342
|
+
code: z3.ZodIssueCode.custom,
|
|
2596
1343
|
message: "Hosted firecrawl_parse requires exactly one of filePath (phase 1) or uploadRef (phase 2).",
|
|
2597
1344
|
path: hasFilePath && hasUploadRef ? ["uploadRef"] : ["filePath"]
|
|
2598
1345
|
});
|
|
@@ -2616,7 +1363,11 @@ function inferContentType(filename) {
|
|
|
2616
1363
|
return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
2617
1364
|
}
|
|
2618
1365
|
function extractParseOptions(args2) {
|
|
2619
|
-
const
|
|
1366
|
+
const options = { ...args2 };
|
|
1367
|
+
delete options.filePath;
|
|
1368
|
+
delete options.uploadRef;
|
|
1369
|
+
delete options.contentType;
|
|
1370
|
+
delete options.declaredSizeBytes;
|
|
2620
1371
|
return options;
|
|
2621
1372
|
}
|
|
2622
1373
|
function buildParseOptionsPayload(options) {
|
|
@@ -2959,13 +1710,13 @@ Map a website to discover all indexed URLs on the site.
|
|
|
2959
1710
|
\`\`\`
|
|
2960
1711
|
**Returns:** Array of URLs found on the site, filtered by search query if provided.
|
|
2961
1712
|
`,
|
|
2962
|
-
parameters:
|
|
2963
|
-
url:
|
|
2964
|
-
search:
|
|
2965
|
-
sitemap:
|
|
2966
|
-
includeSubdomains:
|
|
2967
|
-
limit:
|
|
2968
|
-
ignoreQueryParameters:
|
|
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()
|
|
2969
1720
|
}),
|
|
2970
1721
|
execute: async (args2, { session, log }) => {
|
|
2971
1722
|
const { url, ...options } = args2;
|
|
@@ -2996,7 +1747,7 @@ Search the web and optionally extract content from search results. This is the m
|
|
|
2996
1747
|
The query also supports search operators, that you can use if needed to refine the search:
|
|
2997
1748
|
| Operator | Functionality | Examples |
|
|
2998
1749
|
---|-|-|
|
|
2999
|
-
| \`"
|
|
1750
|
+
| \`"\` | Non-fuzzy matches a string of text | \`"Firecrawl"\`
|
|
3000
1751
|
| \`-\` | Excludes certain keywords or negates other operators | \`-bad\`, \`-site:firecrawl.dev\`
|
|
3001
1752
|
| \`site:\` | Only returns results from a specified website | \`site:firecrawl.dev\`
|
|
3002
1753
|
| \`inurl:\` | Only returns results that include a word in the URL | \`inurl:firecrawl\`
|
|
@@ -3012,6 +1763,7 @@ The query also supports search operators, that you can use if needed to refine t
|
|
|
3012
1763
|
**Common mistakes:** Using crawl or map for open-ended questions (use search instead).
|
|
3013
1764
|
**Prompt Example:** "Find the latest research papers on AI published in 2023."
|
|
3014
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"]\`.
|
|
3015
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.
|
|
3016
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.
|
|
3017
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
|
|
@@ -3038,6 +1790,7 @@ The query also supports search operators, that you can use if needed to refine t
|
|
|
3038
1790
|
"arguments": {
|
|
3039
1791
|
"query": "latest AI research papers 2023",
|
|
3040
1792
|
"limit": 5,
|
|
1793
|
+
"categories": ["github", "research"],
|
|
3041
1794
|
"lang": "en",
|
|
3042
1795
|
"country": "us",
|
|
3043
1796
|
"sources": [
|
|
@@ -3054,17 +1807,20 @@ The query also supports search operators, that you can use if needed to refine t
|
|
|
3054
1807
|
\`\`\`
|
|
3055
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.
|
|
3056
1809
|
`,
|
|
3057
|
-
parameters:
|
|
3058
|
-
query:
|
|
3059
|
-
limit:
|
|
3060
|
-
tbs:
|
|
3061
|
-
filter:
|
|
3062
|
-
location:
|
|
3063
|
-
includeDomains:
|
|
3064
|
-
excludeDomains:
|
|
3065
|
-
sources:
|
|
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
|
+
),
|
|
3066
1822
|
scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional(),
|
|
3067
|
-
enterprise:
|
|
1823
|
+
enterprise: z3.array(z3.enum(["default", "anon", "zdr"])).optional()
|
|
3068
1824
|
}).refine(
|
|
3069
1825
|
(args2) => !(args2.includeDomains?.length && args2.excludeDomains?.length),
|
|
3070
1826
|
"includeDomains and excludeDomains cannot both be specified"
|
|
@@ -3207,7 +1963,7 @@ async function getCrawlStatusWithOrigin(client, jobId) {
|
|
|
3207
1963
|
}
|
|
3208
1964
|
async function waitForCrawlCompletionWithOrigin(client, jobId, pollInterval = 2, timeout) {
|
|
3209
1965
|
const startedAt = Date.now();
|
|
3210
|
-
|
|
1966
|
+
for (; ; ) {
|
|
3211
1967
|
const status = await getCrawlStatusWithOrigin(client, jobId);
|
|
3212
1968
|
if (["completed", "failed", "cancelled"].includes(String(status.status ?? ""))) {
|
|
3213
1969
|
return status;
|
|
@@ -3220,17 +1976,17 @@ async function waitForCrawlCompletionWithOrigin(client, jobId, pollInterval = 2,
|
|
|
3220
1976
|
);
|
|
3221
1977
|
}
|
|
3222
1978
|
}
|
|
3223
|
-
var feedbackIssueSchema =
|
|
1979
|
+
var feedbackIssueSchema = z3.string().trim().min(1).max(80).regex(
|
|
3224
1980
|
/^[a-z0-9][a-z0-9_-]*$/,
|
|
3225
1981
|
"Issue codes must use lowercase letters, numbers, underscores, or hyphens"
|
|
3226
1982
|
);
|
|
3227
|
-
var valuableSourceSchema =
|
|
3228
|
-
url:
|
|
3229
|
-
reason:
|
|
1983
|
+
var valuableSourceSchema = z3.object({
|
|
1984
|
+
url: z3.string().url(),
|
|
1985
|
+
reason: z3.string().max(1e3).optional()
|
|
3230
1986
|
});
|
|
3231
|
-
var missingContentSchema =
|
|
3232
|
-
topic:
|
|
3233
|
-
description:
|
|
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()
|
|
3234
1990
|
});
|
|
3235
1991
|
var FEEDBACK_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
3236
1992
|
function feedbackEnvEnabled(...keys) {
|
|
@@ -3324,24 +2080,24 @@ Pass the \`searchId\` returned by \`firecrawl_search\` (the \`id\` field on the
|
|
|
3324
2080
|
|
|
3325
2081
|
**Returns:** \`{ success, feedbackId, creditsRefunded, creditsRefundedToday, dailyRefundCap, dailyCapReached?, alreadySubmitted?, warning? }\` JSON.
|
|
3326
2082
|
`,
|
|
3327
|
-
parameters:
|
|
3328
|
-
searchId:
|
|
3329
|
-
rating:
|
|
3330
|
-
valuableSources:
|
|
3331
|
-
|
|
3332
|
-
url:
|
|
3333
|
-
reason:
|
|
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()
|
|
3334
2090
|
})
|
|
3335
2091
|
).max(50).optional(),
|
|
3336
|
-
missingContent:
|
|
3337
|
-
|
|
3338
|
-
topic:
|
|
3339
|
-
description:
|
|
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()
|
|
3340
2096
|
})
|
|
3341
2097
|
).max(20).optional().describe(
|
|
3342
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`."
|
|
3343
2099
|
),
|
|
3344
|
-
querySuggestions:
|
|
2100
|
+
querySuggestions: z3.string().max(2e3).optional()
|
|
3345
2101
|
}),
|
|
3346
2102
|
execute: async (args2, { session, log }) => {
|
|
3347
2103
|
const {
|
|
@@ -3440,19 +2196,19 @@ Do not store multi-MB outputs in feedback. Use concise notes, issue codes, URLs,
|
|
|
3440
2196
|
|
|
3441
2197
|
**Returns:** \`{ success, feedbackId, creditsRefunded, creditsRefundedToday?, dailyRefundCap?, dailyCapReached?, alreadySubmitted?, warning? }\` JSON.
|
|
3442
2198
|
`,
|
|
3443
|
-
parameters:
|
|
3444
|
-
endpoint:
|
|
3445
|
-
jobId:
|
|
3446
|
-
rating:
|
|
3447
|
-
issues:
|
|
3448
|
-
tags:
|
|
3449
|
-
note:
|
|
3450
|
-
valuableSources:
|
|
3451
|
-
missingContent:
|
|
3452
|
-
querySuggestions:
|
|
3453
|
-
url:
|
|
3454
|
-
pageNumbers:
|
|
3455
|
-
metadata:
|
|
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()
|
|
3456
2212
|
}),
|
|
3457
2213
|
execute: async (args2, { session, log }) => {
|
|
3458
2214
|
const {
|
|
@@ -3561,25 +2317,25 @@ server.addTool({
|
|
|
3561
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.
|
|
3562
2318
|
${SAFE_MODE ? "**Safe Mode:** Read-only crawling. Webhooks and interactive actions are disabled for security." : ""}
|
|
3563
2319
|
`,
|
|
3564
|
-
parameters:
|
|
3565
|
-
url:
|
|
3566
|
-
prompt:
|
|
3567
|
-
excludePaths:
|
|
3568
|
-
includePaths:
|
|
3569
|
-
maxDiscoveryDepth:
|
|
3570
|
-
sitemap:
|
|
3571
|
-
limit:
|
|
3572
|
-
allowExternalLinks:
|
|
3573
|
-
allowSubdomains:
|
|
3574
|
-
crawlEntireDomain:
|
|
3575
|
-
delay:
|
|
3576
|
-
maxConcurrency:
|
|
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(),
|
|
3577
2333
|
...SAFE_MODE ? {} : {
|
|
3578
|
-
webhook:
|
|
3579
|
-
webhookHeaders:
|
|
2334
|
+
webhook: z3.string().optional(),
|
|
2335
|
+
webhookHeaders: z3.record(z3.string(), z3.string()).optional()
|
|
3580
2336
|
},
|
|
3581
|
-
deduplicateSimilarURLs:
|
|
3582
|
-
ignoreQueryParameters:
|
|
2337
|
+
deduplicateSimilarURLs: z3.boolean().optional(),
|
|
2338
|
+
ignoreQueryParameters: z3.boolean().optional(),
|
|
3583
2339
|
scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
|
|
3584
2340
|
}),
|
|
3585
2341
|
execute: async (args2, { session, log }) => {
|
|
@@ -3643,7 +2399,7 @@ Check the status of a crawl job.
|
|
|
3643
2399
|
\`\`\`
|
|
3644
2400
|
**Returns:** Status and progress of the crawl job, including results if available.
|
|
3645
2401
|
`,
|
|
3646
|
-
parameters:
|
|
2402
|
+
parameters: z3.object({ id: z3.string() }),
|
|
3647
2403
|
execute: async (args2, { session }) => {
|
|
3648
2404
|
const client = getClient(session);
|
|
3649
2405
|
const id = args2.id;
|
|
@@ -3699,13 +2455,13 @@ Extract structured information from web pages using LLM capabilities. Supports b
|
|
|
3699
2455
|
\`\`\`
|
|
3700
2456
|
**Returns:** Extracted structured data as defined by your schema.
|
|
3701
2457
|
`,
|
|
3702
|
-
parameters:
|
|
3703
|
-
urls:
|
|
3704
|
-
prompt:
|
|
3705
|
-
schema:
|
|
3706
|
-
allowExternalLinks:
|
|
3707
|
-
enableWebSearch:
|
|
3708
|
-
includeSubdomains:
|
|
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()
|
|
3709
2465
|
}),
|
|
3710
2466
|
execute: async (args2, { session, log }) => {
|
|
3711
2467
|
const client = getClient(session);
|
|
@@ -3806,10 +2562,10 @@ Then poll with \`firecrawl_agent_status\` every 15-30 seconds for at least 2-3 m
|
|
|
3806
2562
|
\`\`\`
|
|
3807
2563
|
**Returns:** Job ID for status checking. Use \`firecrawl_agent_status\` to poll for results.
|
|
3808
2564
|
`,
|
|
3809
|
-
parameters:
|
|
3810
|
-
prompt:
|
|
3811
|
-
urls:
|
|
3812
|
-
schema:
|
|
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()
|
|
3813
2569
|
}),
|
|
3814
2570
|
execute: async (args2, { session, log }) => {
|
|
3815
2571
|
const client = getClient(session);
|
|
@@ -3866,7 +2622,7 @@ Check the status of an agent job and retrieve results when complete. Use this to
|
|
|
3866
2622
|
|
|
3867
2623
|
**Returns:** Status, progress, and results (if completed) of the agent job.
|
|
3868
2624
|
`,
|
|
3869
|
-
parameters:
|
|
2625
|
+
parameters: z3.object({ id: z3.string() }),
|
|
3870
2626
|
execute: async (args2, { session, log }) => {
|
|
3871
2627
|
const client = getClient(session);
|
|
3872
2628
|
const { id } = args2;
|
|
@@ -3930,13 +2686,13 @@ Interact with a page in a live browser session: click buttons, fill forms, extra
|
|
|
3930
2686
|
\`\`\`
|
|
3931
2687
|
**Returns:** Execution result including output, stdout, stderr, exit code, and live view URLs.
|
|
3932
2688
|
`,
|
|
3933
|
-
parameters:
|
|
3934
|
-
scrapeId:
|
|
3935
|
-
url:
|
|
3936
|
-
prompt:
|
|
3937
|
-
code:
|
|
3938
|
-
language:
|
|
3939
|
-
timeout:
|
|
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(),
|
|
3940
2696
|
scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
|
|
3941
2697
|
}).refine((data) => Boolean(data.scrapeId) !== Boolean(data.url), {
|
|
3942
2698
|
message: "Provide either 'url' (interact directly) or 'scrapeId' (reuse a previous scrape), not both."
|
|
@@ -4024,8 +2780,8 @@ Stop an interact session for a scraped page. Call this when you are done interac
|
|
|
4024
2780
|
\`\`\`
|
|
4025
2781
|
**Returns:** Success confirmation.
|
|
4026
2782
|
`,
|
|
4027
|
-
parameters:
|
|
4028
|
-
scrapeId:
|
|
2783
|
+
parameters: z3.object({
|
|
2784
|
+
scrapeId: z3.string()
|
|
4029
2785
|
}),
|
|
4030
2786
|
execute: async (args2, { session, log }) => {
|
|
4031
2787
|
const client = getClient(session);
|