firecrawl-mcp 3.27.1 → 3.27.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/README.md +17 -18
- package/dist/index.js +2400 -361
- package/package.json +8 -1
package/dist/index.js
CHANGED
|
@@ -76,19 +76,2004 @@ var alexandriaSessionFeedbackSchema = z.strictObject({
|
|
|
76
76
|
rating: z.enum(["good", "bad", "partial"]),
|
|
77
77
|
...alexandriaFeedbackFields,
|
|
78
78
|
requestedWebsite: alexandriaFeedbackFields.requestedWebsite.unwrap(),
|
|
79
|
-
rationale: detail
|
|
80
|
-
objective: detail
|
|
79
|
+
rationale: detail
|
|
81
80
|
});
|
|
82
81
|
|
|
83
82
|
// src/index.ts
|
|
84
83
|
import FirecrawlApp from "firecrawl";
|
|
85
84
|
import dotenv from "dotenv";
|
|
86
|
-
|
|
85
|
+
|
|
86
|
+
// node_modules/.pnpm/fastmcp@4.3.2_patch_hash=08f77b633292d9ae4cb022c0fa5bd722dbd5c1aaaad600204f8ed38339bfd37b/node_modules/fastmcp/dist/chunk-LWU5CQGW.js
|
|
87
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
88
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
89
|
+
import {
|
|
90
|
+
CallToolRequestSchema,
|
|
91
|
+
CompleteRequestSchema,
|
|
92
|
+
ErrorCode,
|
|
93
|
+
GetPromptRequestSchema,
|
|
94
|
+
ListPromptsRequestSchema,
|
|
95
|
+
ListResourcesRequestSchema,
|
|
96
|
+
ListResourceTemplatesRequestSchema,
|
|
97
|
+
ListToolsRequestSchema,
|
|
98
|
+
McpError,
|
|
99
|
+
ReadResourceRequestSchema,
|
|
100
|
+
RootsListChangedNotificationSchema,
|
|
101
|
+
SetLevelRequestSchema
|
|
102
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
103
|
+
import { EventEmitter } from "events";
|
|
104
|
+
import Fuse from "fuse.js";
|
|
105
|
+
import { Hono } from "hono";
|
|
106
|
+
import { startHTTPServer } from "mcp-proxy";
|
|
107
|
+
import { setTimeout as delay } from "timers/promises";
|
|
108
|
+
import { fetch as fetch2 } from "undici";
|
|
109
|
+
import parseURITemplate from "uri-templates";
|
|
110
|
+
import { strictJsonSchema, toJsonSchema } from "xsschema";
|
|
111
|
+
import { z as z2 } from "zod";
|
|
112
|
+
var FastMCPError = class extends Error {
|
|
113
|
+
constructor(message) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = new.target.name;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var UnexpectedStateError = class extends FastMCPError {
|
|
119
|
+
extras;
|
|
120
|
+
constructor(message, extras) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.name = new.target.name;
|
|
123
|
+
this.extras = extras;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var UserError = class extends UnexpectedStateError {
|
|
127
|
+
};
|
|
128
|
+
var TextContentZodSchema = z2.object({
|
|
129
|
+
/**
|
|
130
|
+
* The text content of the message.
|
|
131
|
+
*/
|
|
132
|
+
text: z2.string(),
|
|
133
|
+
type: z2.literal("text")
|
|
134
|
+
}).strict();
|
|
135
|
+
var ImageContentZodSchema = z2.object({
|
|
136
|
+
/**
|
|
137
|
+
* The base64-encoded image data.
|
|
138
|
+
*/
|
|
139
|
+
data: z2.string().base64(),
|
|
140
|
+
/**
|
|
141
|
+
* The MIME type of the image. Different providers may support different image types.
|
|
142
|
+
*/
|
|
143
|
+
mimeType: z2.string(),
|
|
144
|
+
type: z2.literal("image")
|
|
145
|
+
}).strict();
|
|
146
|
+
var AudioContentZodSchema = z2.object({
|
|
147
|
+
/**
|
|
148
|
+
* The base64-encoded audio data.
|
|
149
|
+
*/
|
|
150
|
+
data: z2.string().base64(),
|
|
151
|
+
mimeType: z2.string(),
|
|
152
|
+
type: z2.literal("audio")
|
|
153
|
+
}).strict();
|
|
154
|
+
var ResourceContentZodSchema = z2.object({
|
|
155
|
+
resource: z2.object({
|
|
156
|
+
blob: z2.string().optional(),
|
|
157
|
+
mimeType: z2.string().optional(),
|
|
158
|
+
text: z2.string().optional(),
|
|
159
|
+
uri: z2.string()
|
|
160
|
+
}),
|
|
161
|
+
type: z2.literal("resource")
|
|
162
|
+
}).strict();
|
|
163
|
+
var ResourceLinkZodSchema = z2.object({
|
|
164
|
+
description: z2.string().optional(),
|
|
165
|
+
mimeType: z2.string().optional(),
|
|
166
|
+
name: z2.string(),
|
|
167
|
+
title: z2.string().optional(),
|
|
168
|
+
type: z2.literal("resource_link"),
|
|
169
|
+
uri: z2.string()
|
|
170
|
+
});
|
|
171
|
+
var ContentZodSchema = z2.discriminatedUnion("type", [
|
|
172
|
+
TextContentZodSchema,
|
|
173
|
+
ImageContentZodSchema,
|
|
174
|
+
AudioContentZodSchema,
|
|
175
|
+
ResourceContentZodSchema,
|
|
176
|
+
ResourceLinkZodSchema
|
|
177
|
+
]);
|
|
178
|
+
var ContentResultZodSchema = z2.object({
|
|
179
|
+
content: ContentZodSchema.array(),
|
|
180
|
+
isError: z2.boolean().optional(),
|
|
181
|
+
structuredContent: z2.record(z2.string(), z2.unknown()).optional()
|
|
182
|
+
}).strict();
|
|
183
|
+
var CompletionZodSchema = z2.object({
|
|
184
|
+
/**
|
|
185
|
+
* Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.
|
|
186
|
+
*/
|
|
187
|
+
hasMore: z2.optional(z2.boolean()),
|
|
188
|
+
/**
|
|
189
|
+
* The total number of completion options available. This can exceed the number of values actually sent in the response.
|
|
190
|
+
*/
|
|
191
|
+
total: z2.optional(z2.number().int()),
|
|
192
|
+
/**
|
|
193
|
+
* An array of completion values. Must not exceed 100 items.
|
|
194
|
+
*/
|
|
195
|
+
values: z2.array(z2.string()).max(100)
|
|
196
|
+
});
|
|
197
|
+
var FastMCPSessionEventEmitterBase = EventEmitter;
|
|
198
|
+
var FastMCPSessionEventEmitter = class extends FastMCPSessionEventEmitterBase {
|
|
199
|
+
};
|
|
200
|
+
var FastMCPSession = class extends FastMCPSessionEventEmitter {
|
|
201
|
+
get clientCapabilities() {
|
|
202
|
+
return this.#clientCapabilities ?? null;
|
|
203
|
+
}
|
|
204
|
+
get isReady() {
|
|
205
|
+
return this.#connectionState === "ready";
|
|
206
|
+
}
|
|
207
|
+
get loggingLevel() {
|
|
208
|
+
return this.#loggingLevel;
|
|
209
|
+
}
|
|
210
|
+
get roots() {
|
|
211
|
+
return this.#roots;
|
|
212
|
+
}
|
|
213
|
+
get server() {
|
|
214
|
+
return this.#server;
|
|
215
|
+
}
|
|
216
|
+
get sessionId() {
|
|
217
|
+
return this.#sessionId;
|
|
218
|
+
}
|
|
219
|
+
set sessionId(value) {
|
|
220
|
+
this.#sessionId = value;
|
|
221
|
+
}
|
|
222
|
+
#auth;
|
|
223
|
+
#capabilities = {};
|
|
224
|
+
#clientCapabilities;
|
|
225
|
+
#connectionState = "connecting";
|
|
226
|
+
#logger;
|
|
227
|
+
#loggingLevel = "info";
|
|
228
|
+
#needsEventLoopFlush = false;
|
|
229
|
+
#onToolCall;
|
|
230
|
+
#pingConfig;
|
|
231
|
+
#pingInterval = null;
|
|
232
|
+
#prompts = /* @__PURE__ */ new Map();
|
|
233
|
+
#resources = /* @__PURE__ */ new Map();
|
|
234
|
+
#resourceTemplates = /* @__PURE__ */ new Map();
|
|
235
|
+
#roots = [];
|
|
236
|
+
#rootsConfig;
|
|
237
|
+
#server;
|
|
238
|
+
/**
|
|
239
|
+
* Session ID from the Mcp-Session-Id header (HTTP transports only).
|
|
240
|
+
* Used to track per-session state across multiple requests.
|
|
241
|
+
*/
|
|
242
|
+
#sessionId;
|
|
243
|
+
#utils;
|
|
244
|
+
constructor({
|
|
245
|
+
auth,
|
|
246
|
+
instructions,
|
|
247
|
+
logger,
|
|
248
|
+
name: name2,
|
|
249
|
+
onToolCall,
|
|
250
|
+
ping,
|
|
251
|
+
prompts,
|
|
252
|
+
resources,
|
|
253
|
+
resourcesTemplates,
|
|
254
|
+
roots,
|
|
255
|
+
sessionId,
|
|
256
|
+
tools,
|
|
257
|
+
transportType,
|
|
258
|
+
utils,
|
|
259
|
+
version
|
|
260
|
+
}) {
|
|
261
|
+
super();
|
|
262
|
+
this.#auth = auth;
|
|
263
|
+
this.#logger = logger;
|
|
264
|
+
this.#onToolCall = onToolCall;
|
|
265
|
+
this.#pingConfig = ping;
|
|
266
|
+
this.#rootsConfig = roots;
|
|
267
|
+
this.#sessionId = sessionId;
|
|
268
|
+
this.#needsEventLoopFlush = transportType === "httpStream";
|
|
269
|
+
if (tools.length) {
|
|
270
|
+
this.#capabilities.tools = {};
|
|
271
|
+
}
|
|
272
|
+
if (resources.length || resourcesTemplates.length) {
|
|
273
|
+
this.#capabilities.resources = {};
|
|
274
|
+
}
|
|
275
|
+
if (prompts.length) {
|
|
276
|
+
for (const prompt of prompts) {
|
|
277
|
+
this.addPrompt(prompt);
|
|
278
|
+
}
|
|
279
|
+
this.#capabilities.prompts = {};
|
|
280
|
+
}
|
|
281
|
+
this.#capabilities.logging = {};
|
|
282
|
+
this.#capabilities.completions = {};
|
|
283
|
+
this.#server = new Server(
|
|
284
|
+
{ name: name2, version },
|
|
285
|
+
{ capabilities: this.#capabilities, instructions }
|
|
286
|
+
);
|
|
287
|
+
this.#utils = utils;
|
|
288
|
+
this.setupErrorHandling();
|
|
289
|
+
this.setupLoggingHandlers();
|
|
290
|
+
this.setupRootsHandlers();
|
|
291
|
+
this.setupCompleteHandlers();
|
|
292
|
+
if (tools.length) {
|
|
293
|
+
this.setupToolHandlers(tools);
|
|
294
|
+
}
|
|
295
|
+
if (resources.length || resourcesTemplates.length) {
|
|
296
|
+
for (const resource of resources) {
|
|
297
|
+
this.addResource(resource);
|
|
298
|
+
}
|
|
299
|
+
this.setupResourceHandlers();
|
|
300
|
+
if (resourcesTemplates.length) {
|
|
301
|
+
for (const resourceTemplate of resourcesTemplates) {
|
|
302
|
+
this.addResourceTemplate(resourceTemplate);
|
|
303
|
+
}
|
|
304
|
+
this.setupResourceTemplateHandlers();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (prompts.length) {
|
|
308
|
+
this.setupPromptHandlers();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async close() {
|
|
312
|
+
this.#connectionState = "closed";
|
|
313
|
+
if (this.#pingInterval) {
|
|
314
|
+
clearInterval(this.#pingInterval);
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
await this.#server.close();
|
|
318
|
+
} catch (error2) {
|
|
319
|
+
this.#logger.error("[FastMCP error]", "could not close server", error2);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async connect(transport) {
|
|
323
|
+
if (this.#server.transport) {
|
|
324
|
+
throw new UnexpectedStateError("Server is already connected");
|
|
325
|
+
}
|
|
326
|
+
this.#connectionState = "connecting";
|
|
327
|
+
try {
|
|
328
|
+
await this.#server.connect(transport);
|
|
329
|
+
if ("sessionId" in transport) {
|
|
330
|
+
const transportWithSessionId = transport;
|
|
331
|
+
if (typeof transportWithSessionId.sessionId === "string") {
|
|
332
|
+
this.#sessionId = transportWithSessionId.sessionId;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
let attempt = 0;
|
|
336
|
+
const maxAttempts = 10;
|
|
337
|
+
const retryDelay = 100;
|
|
338
|
+
while (attempt++ < maxAttempts) {
|
|
339
|
+
const capabilities = this.#server.getClientCapabilities();
|
|
340
|
+
if (capabilities) {
|
|
341
|
+
this.#clientCapabilities = capabilities;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
await delay(retryDelay);
|
|
345
|
+
}
|
|
346
|
+
if (!this.#clientCapabilities) {
|
|
347
|
+
this.#logger.warn(
|
|
348
|
+
`[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (this.#rootsConfig?.enabled !== false && this.#clientCapabilities?.roots?.listChanged && typeof this.#server.listRoots === "function") {
|
|
352
|
+
try {
|
|
353
|
+
const roots = await this.#server.listRoots();
|
|
354
|
+
this.#roots = roots?.roots || [];
|
|
355
|
+
} catch (e) {
|
|
356
|
+
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
|
|
357
|
+
this.#logger.debug(
|
|
358
|
+
"[FastMCP debug] listRoots method not supported by client"
|
|
359
|
+
);
|
|
360
|
+
} else {
|
|
361
|
+
this.#logger.error(
|
|
362
|
+
`[FastMCP error] received error listing roots.
|
|
363
|
+
|
|
364
|
+
${e instanceof Error ? e.stack : JSON.stringify(e)}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (this.#clientCapabilities) {
|
|
370
|
+
const pingConfig = this.#getPingConfig(transport);
|
|
371
|
+
if (pingConfig.enabled) {
|
|
372
|
+
this.#pingInterval = setInterval(async () => {
|
|
373
|
+
try {
|
|
374
|
+
await this.#server.ping();
|
|
375
|
+
} catch {
|
|
376
|
+
const logLevel = pingConfig.logLevel;
|
|
377
|
+
if (logLevel === "debug") {
|
|
378
|
+
this.#logger.debug("[FastMCP debug] server ping failed");
|
|
379
|
+
} else if (logLevel === "warning") {
|
|
380
|
+
this.#logger.warn(
|
|
381
|
+
"[FastMCP warning] server is not responding to ping"
|
|
382
|
+
);
|
|
383
|
+
} else if (logLevel === "error") {
|
|
384
|
+
this.#logger.error(
|
|
385
|
+
"[FastMCP error] server is not responding to ping"
|
|
386
|
+
);
|
|
387
|
+
} else {
|
|
388
|
+
this.#logger.info("[FastMCP info] server ping failed");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}, pingConfig.intervalMs);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
this.#connectionState = "ready";
|
|
395
|
+
this.emit("ready");
|
|
396
|
+
} catch (error2) {
|
|
397
|
+
this.#connectionState = "error";
|
|
398
|
+
const errorEvent = {
|
|
399
|
+
error: error2 instanceof Error ? error2 : new Error(String(error2))
|
|
400
|
+
};
|
|
401
|
+
this.emit("error", errorEvent);
|
|
402
|
+
throw error2;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
promptsListChanged(prompts) {
|
|
406
|
+
this.#prompts.clear();
|
|
407
|
+
for (const prompt of prompts) {
|
|
408
|
+
this.addPrompt(prompt);
|
|
409
|
+
}
|
|
410
|
+
this.setupPromptHandlers();
|
|
411
|
+
this.triggerListChangedNotification("notifications/prompts/list_changed");
|
|
412
|
+
}
|
|
413
|
+
async requestSampling(message, options) {
|
|
414
|
+
return this.#server.createMessage(message, options);
|
|
415
|
+
}
|
|
416
|
+
resourcesListChanged(resources) {
|
|
417
|
+
this.#resources.clear();
|
|
418
|
+
for (const resource of resources) {
|
|
419
|
+
this.addResource(resource);
|
|
420
|
+
}
|
|
421
|
+
this.setupResourceHandlers();
|
|
422
|
+
this.triggerListChangedNotification("notifications/resources/list_changed");
|
|
423
|
+
}
|
|
424
|
+
resourceTemplatesListChanged(resourceTemplates) {
|
|
425
|
+
this.#resourceTemplates.clear();
|
|
426
|
+
for (const resourceTemplate of resourceTemplates) {
|
|
427
|
+
this.addResourceTemplate(resourceTemplate);
|
|
428
|
+
}
|
|
429
|
+
this.setupResourceTemplateHandlers();
|
|
430
|
+
this.triggerListChangedNotification("notifications/resources/list_changed");
|
|
431
|
+
}
|
|
432
|
+
toolsListChanged(tools) {
|
|
433
|
+
const allowedTools = tools.filter(
|
|
434
|
+
(tool) => tool.canAccess ? tool.canAccess(this.#auth) : true
|
|
435
|
+
);
|
|
436
|
+
this.setupToolHandlers(allowedTools);
|
|
437
|
+
this.triggerListChangedNotification("notifications/tools/list_changed");
|
|
438
|
+
}
|
|
439
|
+
async triggerListChangedNotification(method) {
|
|
440
|
+
try {
|
|
441
|
+
await this.#server.notification({
|
|
442
|
+
method
|
|
443
|
+
});
|
|
444
|
+
} catch (error2) {
|
|
445
|
+
this.#logger.error(
|
|
446
|
+
`[FastMCP error] failed to send ${method} notification.
|
|
447
|
+
|
|
448
|
+
${error2 instanceof Error ? error2.stack : JSON.stringify(error2)}`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Update the session's authentication context.
|
|
454
|
+
* Called by mcp-proxy when a new token is validated on subsequent requests.
|
|
455
|
+
*/
|
|
456
|
+
updateAuth(auth) {
|
|
457
|
+
this.#auth = auth;
|
|
458
|
+
}
|
|
459
|
+
waitForReady() {
|
|
460
|
+
if (this.isReady) {
|
|
461
|
+
return Promise.resolve();
|
|
462
|
+
}
|
|
463
|
+
if (this.#connectionState === "error" || this.#connectionState === "closed") {
|
|
464
|
+
return Promise.reject(
|
|
465
|
+
new Error(`Connection is in ${this.#connectionState} state`)
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
const timeout = setTimeout(() => {
|
|
470
|
+
reject(
|
|
471
|
+
new Error(
|
|
472
|
+
"Connection timeout: Session failed to become ready within 5 seconds"
|
|
473
|
+
)
|
|
474
|
+
);
|
|
475
|
+
}, 5e3);
|
|
476
|
+
this.once("ready", () => {
|
|
477
|
+
clearTimeout(timeout);
|
|
478
|
+
resolve();
|
|
479
|
+
});
|
|
480
|
+
this.once("error", (event) => {
|
|
481
|
+
clearTimeout(timeout);
|
|
482
|
+
reject(event.error);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
#formatSchemaIssues(issues) {
|
|
487
|
+
return this.#utils?.formatInvalidParamsErrorMessage ? this.#utils.formatInvalidParamsErrorMessage(issues) : issues.map((issue) => {
|
|
488
|
+
const path2 = issue.path?.join(".") || "root";
|
|
489
|
+
return `${path2}: ${issue.message}`;
|
|
490
|
+
}).join(", ");
|
|
491
|
+
}
|
|
492
|
+
#getPingConfig(transport) {
|
|
493
|
+
const pingConfig = this.#pingConfig || {};
|
|
494
|
+
let defaultEnabled = false;
|
|
495
|
+
if ("type" in transport) {
|
|
496
|
+
if (transport.type === "httpStream") {
|
|
497
|
+
defaultEnabled = true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
enabled: pingConfig.enabled !== void 0 ? pingConfig.enabled : defaultEnabled,
|
|
502
|
+
intervalMs: pingConfig.intervalMs || 5e3,
|
|
503
|
+
logLevel: pingConfig.logLevel || "debug"
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
async #validateStructuredContent(tool, value, toolName) {
|
|
507
|
+
if (!tool.outputSchema) {
|
|
508
|
+
return value;
|
|
509
|
+
}
|
|
510
|
+
const parsed = await tool.outputSchema["~standard"].validate(value);
|
|
511
|
+
if (parsed.issues) {
|
|
512
|
+
throw new UserError(
|
|
513
|
+
`Tool '${toolName}' structured output validation failed: ${this.#formatSchemaIssues(parsed.issues)}. Please check the result matches the tool's outputSchema.`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return parsed.value;
|
|
517
|
+
}
|
|
518
|
+
addPrompt(inputPrompt) {
|
|
519
|
+
const completers = {};
|
|
520
|
+
const enums = {};
|
|
521
|
+
const fuseInstances = {};
|
|
522
|
+
for (const argument of inputPrompt.arguments ?? []) {
|
|
523
|
+
if (argument.complete) {
|
|
524
|
+
completers[argument.name] = argument.complete;
|
|
525
|
+
}
|
|
526
|
+
if (argument.enum) {
|
|
527
|
+
enums[argument.name] = argument.enum;
|
|
528
|
+
fuseInstances[argument.name] = new Fuse(argument.enum, {
|
|
529
|
+
includeScore: true,
|
|
530
|
+
threshold: 0.3
|
|
531
|
+
// More flexible matching!
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const prompt = {
|
|
536
|
+
...inputPrompt,
|
|
537
|
+
complete: async (name2, value, auth) => {
|
|
538
|
+
if (completers[name2]) {
|
|
539
|
+
return await completers[name2](value, auth);
|
|
540
|
+
}
|
|
541
|
+
if (inputPrompt.complete) {
|
|
542
|
+
return await inputPrompt.complete(name2, value, auth);
|
|
543
|
+
}
|
|
544
|
+
if (fuseInstances[name2]) {
|
|
545
|
+
const result = fuseInstances[name2].search(value);
|
|
546
|
+
return {
|
|
547
|
+
total: result.length,
|
|
548
|
+
values: result.map((item) => item.item)
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
values: []
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
this.#prompts.set(prompt.name, prompt);
|
|
557
|
+
}
|
|
558
|
+
addResource(inputResource) {
|
|
559
|
+
this.#resources.set(inputResource.uri, inputResource);
|
|
560
|
+
}
|
|
561
|
+
addResourceTemplate(inputResourceTemplate) {
|
|
562
|
+
const completers = {};
|
|
563
|
+
for (const argument of inputResourceTemplate.arguments ?? []) {
|
|
564
|
+
if (argument.complete) {
|
|
565
|
+
completers[argument.name] = argument.complete;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const resourceTemplate = {
|
|
569
|
+
...inputResourceTemplate,
|
|
570
|
+
complete: async (name2, value, auth) => {
|
|
571
|
+
if (completers[name2]) {
|
|
572
|
+
return await completers[name2](value, auth);
|
|
573
|
+
}
|
|
574
|
+
if (inputResourceTemplate.complete) {
|
|
575
|
+
return await inputResourceTemplate.complete(name2, value, auth);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
values: []
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
this.#resourceTemplates.set(resourceTemplate.name, resourceTemplate);
|
|
583
|
+
}
|
|
584
|
+
setupCompleteHandlers() {
|
|
585
|
+
this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
586
|
+
if (request.params.ref.type === "ref/prompt") {
|
|
587
|
+
const ref = request.params.ref;
|
|
588
|
+
const prompt = "name" in ref && this.#prompts.get(ref.name);
|
|
589
|
+
if (!prompt) {
|
|
590
|
+
throw new UnexpectedStateError("Unknown prompt", {
|
|
591
|
+
request
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
if (!prompt.complete) {
|
|
595
|
+
throw new UnexpectedStateError("Prompt does not support completion", {
|
|
596
|
+
request
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
const completion = CompletionZodSchema.parse(
|
|
600
|
+
await prompt.complete(
|
|
601
|
+
request.params.argument.name,
|
|
602
|
+
request.params.argument.value,
|
|
603
|
+
this.#auth
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
return {
|
|
607
|
+
completion
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (request.params.ref.type === "ref/resource") {
|
|
611
|
+
const ref = request.params.ref;
|
|
612
|
+
const resource = "uri" in ref && Array.from(this.#resourceTemplates.values()).find(
|
|
613
|
+
(resource2) => resource2.uriTemplate === ref.uri
|
|
614
|
+
);
|
|
615
|
+
if (!resource) {
|
|
616
|
+
throw new UnexpectedStateError("Unknown resource", {
|
|
617
|
+
request
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
if (!("uriTemplate" in resource)) {
|
|
621
|
+
throw new UnexpectedStateError("Unexpected resource");
|
|
622
|
+
}
|
|
623
|
+
if (!resource.complete) {
|
|
624
|
+
throw new UnexpectedStateError(
|
|
625
|
+
"Resource does not support completion",
|
|
626
|
+
{
|
|
627
|
+
request
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const completion = CompletionZodSchema.parse(
|
|
632
|
+
await resource.complete(
|
|
633
|
+
request.params.argument.name,
|
|
634
|
+
request.params.argument.value,
|
|
635
|
+
this.#auth
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
return {
|
|
639
|
+
completion
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
throw new UnexpectedStateError("Unexpected completion request", {
|
|
643
|
+
request
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
setupErrorHandling() {
|
|
648
|
+
this.#server.onerror = (error2) => {
|
|
649
|
+
this.#logger.error("[FastMCP error]", error2);
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
setupLoggingHandlers() {
|
|
653
|
+
this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {
|
|
654
|
+
this.#loggingLevel = request.params.level;
|
|
655
|
+
return {};
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
setupPromptHandlers() {
|
|
659
|
+
let cachedPromptsList = null;
|
|
660
|
+
this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
661
|
+
if (cachedPromptsList) {
|
|
662
|
+
return {
|
|
663
|
+
prompts: cachedPromptsList
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
cachedPromptsList = Array.from(this.#prompts.values()).map((prompt) => {
|
|
667
|
+
return {
|
|
668
|
+
arguments: prompt.arguments,
|
|
669
|
+
complete: prompt.complete,
|
|
670
|
+
description: prompt.description,
|
|
671
|
+
name: prompt.name
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
return {
|
|
675
|
+
prompts: cachedPromptsList
|
|
676
|
+
};
|
|
677
|
+
});
|
|
678
|
+
this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
679
|
+
const prompt = this.#prompts.get(request.params.name);
|
|
680
|
+
if (!prompt) {
|
|
681
|
+
throw new McpError(
|
|
682
|
+
ErrorCode.MethodNotFound,
|
|
683
|
+
`Unknown prompt: ${request.params.name}`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const args2 = request.params.arguments;
|
|
687
|
+
for (const arg of prompt.arguments ?? []) {
|
|
688
|
+
if (arg.required && !(args2 && arg.name in args2)) {
|
|
689
|
+
throw new McpError(
|
|
690
|
+
ErrorCode.InvalidRequest,
|
|
691
|
+
`Prompt '${request.params.name}' requires argument '${arg.name}': ${arg.description || "No description provided"}`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
let result;
|
|
696
|
+
try {
|
|
697
|
+
result = await prompt.load(
|
|
698
|
+
args2,
|
|
699
|
+
this.#auth
|
|
700
|
+
);
|
|
701
|
+
} catch (error2) {
|
|
702
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
703
|
+
throw new McpError(
|
|
704
|
+
ErrorCode.InternalError,
|
|
705
|
+
`Failed to load prompt '${request.params.name}': ${errorMessage}`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (typeof result === "string") {
|
|
709
|
+
return {
|
|
710
|
+
description: prompt.description,
|
|
711
|
+
messages: [
|
|
712
|
+
{
|
|
713
|
+
content: { text: result, type: "text" },
|
|
714
|
+
role: "user"
|
|
715
|
+
}
|
|
716
|
+
]
|
|
717
|
+
};
|
|
718
|
+
} else {
|
|
719
|
+
return {
|
|
720
|
+
description: prompt.description,
|
|
721
|
+
messages: result.messages
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
setupResourceHandlers() {
|
|
727
|
+
let cachedResourcesList = null;
|
|
728
|
+
this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
729
|
+
if (cachedResourcesList) {
|
|
730
|
+
return {
|
|
731
|
+
resources: cachedResourcesList
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
cachedResourcesList = Array.from(this.#resources.values()).map(
|
|
735
|
+
(resource) => ({
|
|
736
|
+
description: resource.description,
|
|
737
|
+
mimeType: resource.mimeType,
|
|
738
|
+
name: resource.name,
|
|
739
|
+
uri: resource.uri
|
|
740
|
+
})
|
|
741
|
+
);
|
|
742
|
+
return {
|
|
743
|
+
resources: cachedResourcesList
|
|
744
|
+
};
|
|
745
|
+
});
|
|
746
|
+
this.#server.setRequestHandler(
|
|
747
|
+
ReadResourceRequestSchema,
|
|
748
|
+
async (request) => {
|
|
749
|
+
if ("uri" in request.params) {
|
|
750
|
+
const resource = this.#resources.get(request.params.uri);
|
|
751
|
+
if (!resource) {
|
|
752
|
+
for (const resourceTemplate of this.#resourceTemplates.values()) {
|
|
753
|
+
const uriTemplate = parseURITemplate(
|
|
754
|
+
resourceTemplate.uriTemplate
|
|
755
|
+
);
|
|
756
|
+
const match = uriTemplate.fromUri(request.params.uri);
|
|
757
|
+
if (!match) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const uri = uriTemplate.fill(match);
|
|
761
|
+
const result = await resourceTemplate.load(match, this.#auth);
|
|
762
|
+
const resources = Array.isArray(result) ? result : [result];
|
|
763
|
+
return {
|
|
764
|
+
contents: resources.map((resource2) => ({
|
|
765
|
+
...resource2,
|
|
766
|
+
description: resourceTemplate.description,
|
|
767
|
+
mimeType: resource2.mimeType ?? resourceTemplate.mimeType,
|
|
768
|
+
name: resourceTemplate.name,
|
|
769
|
+
uri: resource2.uri ?? uri
|
|
770
|
+
}))
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
throw new McpError(
|
|
774
|
+
ErrorCode.MethodNotFound,
|
|
775
|
+
`Resource not found: '${request.params.uri}'. Available resources: ${Array.from(this.#resources.values()).map((r) => r.uri).join(", ") || "none"}`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (!("uri" in resource)) {
|
|
779
|
+
throw new UnexpectedStateError("Resource does not support reading");
|
|
780
|
+
}
|
|
781
|
+
let maybeArrayResult;
|
|
782
|
+
try {
|
|
783
|
+
maybeArrayResult = await resource.load(this.#auth);
|
|
784
|
+
} catch (error2) {
|
|
785
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
786
|
+
throw new McpError(
|
|
787
|
+
ErrorCode.InternalError,
|
|
788
|
+
`Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,
|
|
789
|
+
{
|
|
790
|
+
uri: resource.uri
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult];
|
|
795
|
+
return {
|
|
796
|
+
contents: resourceResults.map((result) => ({
|
|
797
|
+
...result,
|
|
798
|
+
mimeType: result.mimeType ?? resource.mimeType,
|
|
799
|
+
name: resource.name,
|
|
800
|
+
uri: result.uri ?? resource.uri
|
|
801
|
+
}))
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
throw new UnexpectedStateError("Unknown resource request", {
|
|
805
|
+
request
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
setupResourceTemplateHandlers() {
|
|
811
|
+
let cachedResourceTemplatesList = null;
|
|
812
|
+
this.#server.setRequestHandler(
|
|
813
|
+
ListResourceTemplatesRequestSchema,
|
|
814
|
+
async () => {
|
|
815
|
+
if (cachedResourceTemplatesList) {
|
|
816
|
+
return {
|
|
817
|
+
resourceTemplates: cachedResourceTemplatesList
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
cachedResourceTemplatesList = Array.from(
|
|
821
|
+
this.#resourceTemplates.values()
|
|
822
|
+
).map((resourceTemplate) => ({
|
|
823
|
+
description: resourceTemplate.description,
|
|
824
|
+
mimeType: resourceTemplate.mimeType,
|
|
825
|
+
name: resourceTemplate.name,
|
|
826
|
+
uriTemplate: resourceTemplate.uriTemplate
|
|
827
|
+
}));
|
|
828
|
+
return {
|
|
829
|
+
resourceTemplates: cachedResourceTemplatesList
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
setupRootsHandlers() {
|
|
835
|
+
if (this.#rootsConfig?.enabled === false) {
|
|
836
|
+
this.#logger.debug(
|
|
837
|
+
"[FastMCP debug] roots capability explicitly disabled via config"
|
|
838
|
+
);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (typeof this.#server.listRoots === "function") {
|
|
842
|
+
this.#server.setNotificationHandler(
|
|
843
|
+
RootsListChangedNotificationSchema,
|
|
844
|
+
() => {
|
|
845
|
+
this.#server.listRoots().then((roots) => {
|
|
846
|
+
this.#roots = roots.roots;
|
|
847
|
+
this.emit("rootsChanged", {
|
|
848
|
+
roots: roots.roots
|
|
849
|
+
});
|
|
850
|
+
}).catch((error2) => {
|
|
851
|
+
if (error2 instanceof McpError && error2.code === ErrorCode.MethodNotFound) {
|
|
852
|
+
this.#logger.debug(
|
|
853
|
+
"[FastMCP debug] listRoots method not supported by client"
|
|
854
|
+
);
|
|
855
|
+
} else {
|
|
856
|
+
this.#logger.error(
|
|
857
|
+
`[FastMCP error] received error listing roots.
|
|
858
|
+
|
|
859
|
+
${error2 instanceof Error ? error2.stack : JSON.stringify(error2)}`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
} else {
|
|
866
|
+
this.#logger.debug(
|
|
867
|
+
"[FastMCP debug] roots capability not available, not setting up notification handler"
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
setupToolHandlers(tools) {
|
|
872
|
+
const toolsMap = new Map(tools.map((tool) => [tool.name, tool]));
|
|
873
|
+
const listedTools = tools.filter(
|
|
874
|
+
(tool) => tool.canList ? tool.canList(this.#auth) : true
|
|
875
|
+
);
|
|
876
|
+
let cachedToolsList = null;
|
|
877
|
+
this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
878
|
+
if (cachedToolsList) {
|
|
879
|
+
return {
|
|
880
|
+
tools: cachedToolsList
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
cachedToolsList = await Promise.all(
|
|
884
|
+
listedTools.map(async (tool) => {
|
|
885
|
+
return {
|
|
886
|
+
annotations: tool.annotations,
|
|
887
|
+
// Top-level Tool.title (MCP 2025-06-18). Clients such as Codex index this
|
|
888
|
+
// for tool search and do not read annotations.title.
|
|
889
|
+
...tool.annotations?.title && { title: tool.annotations.title },
|
|
890
|
+
description: tool.description,
|
|
891
|
+
inputSchema: tool.parameters ? strictJsonSchema(await toJsonSchema(tool.parameters)) : {
|
|
892
|
+
additionalProperties: false,
|
|
893
|
+
properties: {},
|
|
894
|
+
type: "object"
|
|
895
|
+
},
|
|
896
|
+
name: tool.name,
|
|
897
|
+
...tool.outputSchema && {
|
|
898
|
+
outputSchema: strictJsonSchema(
|
|
899
|
+
await toJsonSchema(tool.outputSchema)
|
|
900
|
+
)
|
|
901
|
+
},
|
|
902
|
+
// Pass through _meta for MCP ext-apps UI support (issue #229)
|
|
903
|
+
...tool._meta && { _meta: tool._meta }
|
|
904
|
+
};
|
|
905
|
+
})
|
|
906
|
+
);
|
|
907
|
+
return {
|
|
908
|
+
tools: cachedToolsList
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
912
|
+
const tool = toolsMap.get(request.params.name);
|
|
913
|
+
if (!tool) {
|
|
914
|
+
throw new McpError(
|
|
915
|
+
ErrorCode.MethodNotFound,
|
|
916
|
+
`Unknown tool: ${request.params.name}`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (tool.beforeValidate) {
|
|
920
|
+
const earlyResult = await tool.beforeValidate(
|
|
921
|
+
request.params.arguments,
|
|
922
|
+
this.#auth
|
|
923
|
+
);
|
|
924
|
+
if (earlyResult !== void 0) {
|
|
925
|
+
return ContentResultZodSchema.parse(earlyResult);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
let args2 = void 0;
|
|
929
|
+
if (tool.parameters) {
|
|
930
|
+
const parsed = await tool.parameters["~standard"].validate(
|
|
931
|
+
request.params.arguments
|
|
932
|
+
);
|
|
933
|
+
if (parsed.issues) {
|
|
934
|
+
const friendlyErrors = this.#formatSchemaIssues(parsed.issues);
|
|
935
|
+
throw new McpError(
|
|
936
|
+
ErrorCode.InvalidParams,
|
|
937
|
+
`Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
args2 = parsed.value;
|
|
941
|
+
}
|
|
942
|
+
const progressToken = request.params?._meta?.progressToken;
|
|
943
|
+
let result;
|
|
944
|
+
try {
|
|
945
|
+
const reportProgress = async (progress) => {
|
|
946
|
+
if (progressToken === void 0) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
await this.#server.notification({
|
|
951
|
+
method: "notifications/progress",
|
|
952
|
+
params: {
|
|
953
|
+
...progress,
|
|
954
|
+
progressToken
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
if (this.#needsEventLoopFlush) {
|
|
958
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
959
|
+
}
|
|
960
|
+
} catch (progressError) {
|
|
961
|
+
this.#logger.warn(
|
|
962
|
+
`[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
|
|
963
|
+
progressError instanceof Error ? progressError.message : String(progressError)
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
const log = {
|
|
968
|
+
debug: (message, context) => {
|
|
969
|
+
this.#server.sendLoggingMessage({
|
|
970
|
+
data: {
|
|
971
|
+
context,
|
|
972
|
+
message
|
|
973
|
+
},
|
|
974
|
+
level: "debug"
|
|
975
|
+
});
|
|
976
|
+
},
|
|
977
|
+
error: (message, context) => {
|
|
978
|
+
this.#server.sendLoggingMessage({
|
|
979
|
+
data: {
|
|
980
|
+
context,
|
|
981
|
+
message
|
|
982
|
+
},
|
|
983
|
+
level: "error"
|
|
984
|
+
});
|
|
985
|
+
},
|
|
986
|
+
info: (message, context) => {
|
|
987
|
+
this.#server.sendLoggingMessage({
|
|
988
|
+
data: {
|
|
989
|
+
context,
|
|
990
|
+
message
|
|
991
|
+
},
|
|
992
|
+
level: "info"
|
|
993
|
+
});
|
|
994
|
+
},
|
|
995
|
+
warn: (message, context) => {
|
|
996
|
+
this.#server.sendLoggingMessage({
|
|
997
|
+
data: {
|
|
998
|
+
context,
|
|
999
|
+
message
|
|
1000
|
+
},
|
|
1001
|
+
level: "warning"
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
const streamContent = async (content) => {
|
|
1006
|
+
const contentArray = Array.isArray(content) ? content : [content];
|
|
1007
|
+
try {
|
|
1008
|
+
await this.#server.notification({
|
|
1009
|
+
method: "notifications/tool/streamContent",
|
|
1010
|
+
params: {
|
|
1011
|
+
content: contentArray,
|
|
1012
|
+
toolName: request.params.name
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
if (this.#needsEventLoopFlush) {
|
|
1016
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1017
|
+
}
|
|
1018
|
+
} catch (streamError) {
|
|
1019
|
+
this.#logger.warn(
|
|
1020
|
+
`[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
|
|
1021
|
+
streamError instanceof Error ? streamError.message : String(streamError)
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
if (this.#onToolCall) {
|
|
1026
|
+
await this.#onToolCall({
|
|
1027
|
+
arguments: args2 ?? {},
|
|
1028
|
+
toolName: request.params.name
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
const executeToolPromise = tool.execute(args2, {
|
|
1032
|
+
client: {
|
|
1033
|
+
version: this.#server.getClientVersion()
|
|
1034
|
+
},
|
|
1035
|
+
log,
|
|
1036
|
+
reportProgress,
|
|
1037
|
+
requestId: typeof request.params?._meta?.requestId === "string" ? request.params._meta.requestId : void 0,
|
|
1038
|
+
session: this.#auth,
|
|
1039
|
+
sessionId: this.#sessionId,
|
|
1040
|
+
streamContent
|
|
1041
|
+
});
|
|
1042
|
+
const maybeStringResult = await (tool.timeoutMs ? Promise.race([
|
|
1043
|
+
executeToolPromise,
|
|
1044
|
+
new Promise((_, reject) => {
|
|
1045
|
+
const timeoutId = setTimeout(() => {
|
|
1046
|
+
reject(
|
|
1047
|
+
new UserError(
|
|
1048
|
+
`Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
}, tool.timeoutMs);
|
|
1052
|
+
executeToolPromise.then(
|
|
1053
|
+
() => clearTimeout(timeoutId),
|
|
1054
|
+
() => clearTimeout(timeoutId)
|
|
1055
|
+
);
|
|
1056
|
+
})
|
|
1057
|
+
]) : executeToolPromise);
|
|
1058
|
+
await delay(1);
|
|
1059
|
+
if (maybeStringResult === void 0 || maybeStringResult === null) {
|
|
1060
|
+
result = ContentResultZodSchema.parse({
|
|
1061
|
+
content: []
|
|
1062
|
+
});
|
|
1063
|
+
} else if (typeof maybeStringResult === "string") {
|
|
1064
|
+
result = ContentResultZodSchema.parse({
|
|
1065
|
+
content: [{ text: maybeStringResult, type: "text" }]
|
|
1066
|
+
});
|
|
1067
|
+
} else if ("type" in maybeStringResult) {
|
|
1068
|
+
result = ContentResultZodSchema.parse({
|
|
1069
|
+
content: [maybeStringResult]
|
|
1070
|
+
});
|
|
1071
|
+
} else if ("content" in maybeStringResult) {
|
|
1072
|
+
result = ContentResultZodSchema.parse(maybeStringResult);
|
|
1073
|
+
if (result.structuredContent !== void 0 && tool.outputSchema) {
|
|
1074
|
+
result.structuredContent = await this.#validateStructuredContent(
|
|
1075
|
+
tool,
|
|
1076
|
+
result.structuredContent,
|
|
1077
|
+
request.params.name
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
} else if (tool.outputSchema) {
|
|
1081
|
+
const structuredContent = await this.#validateStructuredContent(
|
|
1082
|
+
tool,
|
|
1083
|
+
maybeStringResult,
|
|
1084
|
+
request.params.name
|
|
1085
|
+
);
|
|
1086
|
+
result = ContentResultZodSchema.parse({
|
|
1087
|
+
content: [
|
|
1088
|
+
{
|
|
1089
|
+
text: JSON.stringify(structuredContent),
|
|
1090
|
+
type: "text"
|
|
1091
|
+
}
|
|
1092
|
+
],
|
|
1093
|
+
structuredContent
|
|
1094
|
+
});
|
|
1095
|
+
} else {
|
|
1096
|
+
result = ContentResultZodSchema.parse(maybeStringResult);
|
|
1097
|
+
}
|
|
1098
|
+
} catch (error2) {
|
|
1099
|
+
if (error2 instanceof UserError) {
|
|
1100
|
+
return {
|
|
1101
|
+
content: [{ text: error2.message, type: "text" }],
|
|
1102
|
+
isError: true,
|
|
1103
|
+
...error2.extras ? { structuredContent: error2.extras } : {}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
1107
|
+
return {
|
|
1108
|
+
content: [
|
|
1109
|
+
{
|
|
1110
|
+
text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,
|
|
1111
|
+
type: "text"
|
|
1112
|
+
}
|
|
1113
|
+
],
|
|
1114
|
+
isError: true
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
return result;
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
function camelToSnakeCase(str2) {
|
|
1122
|
+
return str2.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1123
|
+
}
|
|
1124
|
+
function convertObjectToSnakeCase(obj) {
|
|
1125
|
+
const result = {};
|
|
1126
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1127
|
+
const snakeKey = camelToSnakeCase(key);
|
|
1128
|
+
result[snakeKey] = value;
|
|
1129
|
+
}
|
|
1130
|
+
return result;
|
|
1131
|
+
}
|
|
1132
|
+
function parseBasicAuthHeader(authHeader) {
|
|
1133
|
+
const basicMatch = authHeader?.match(/^Basic\s+(.+)$/);
|
|
1134
|
+
if (!basicMatch) return null;
|
|
1135
|
+
try {
|
|
1136
|
+
const credentials = Buffer.from(basicMatch[1], "base64").toString("utf-8");
|
|
1137
|
+
const credMatch = credentials.match(/^([^:]+):(.*)$/);
|
|
1138
|
+
if (!credMatch) return null;
|
|
1139
|
+
return { clientId: credMatch[1], clientSecret: credMatch[2] };
|
|
1140
|
+
} catch {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
var FastMCPEventEmitterBase = EventEmitter;
|
|
1145
|
+
var FastMCPEventEmitter = class extends FastMCPEventEmitterBase {
|
|
1146
|
+
};
|
|
1147
|
+
var FastMCP = class extends FastMCPEventEmitter {
|
|
1148
|
+
constructor(options) {
|
|
1149
|
+
super();
|
|
1150
|
+
this.options = options;
|
|
1151
|
+
this.#options = options;
|
|
1152
|
+
this.#logger = options.logger || console;
|
|
1153
|
+
if (options.auth) {
|
|
1154
|
+
if (!options.authenticate) {
|
|
1155
|
+
this.#authenticate = ((request) => options.auth.authenticate(request));
|
|
1156
|
+
} else {
|
|
1157
|
+
this.#authenticate = options.authenticate;
|
|
1158
|
+
}
|
|
1159
|
+
if (!options.oauth) {
|
|
1160
|
+
this.#options = {
|
|
1161
|
+
...options,
|
|
1162
|
+
oauth: options.auth.getOAuthConfig()
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
this.#authenticate = options.authenticate;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
get serverState() {
|
|
1170
|
+
return this.#serverState;
|
|
1171
|
+
}
|
|
1172
|
+
get sessions() {
|
|
1173
|
+
return this.#sessions;
|
|
1174
|
+
}
|
|
1175
|
+
#authenticate;
|
|
1176
|
+
#honoApp = new Hono();
|
|
1177
|
+
#httpStreamServer = null;
|
|
1178
|
+
#logger;
|
|
1179
|
+
#options;
|
|
1180
|
+
#prompts = [];
|
|
1181
|
+
#resources = [];
|
|
1182
|
+
#resourcesTemplates = [];
|
|
1183
|
+
#serverState = "stopped";
|
|
1184
|
+
#sessions = [];
|
|
1185
|
+
#tools = [];
|
|
1186
|
+
/**
|
|
1187
|
+
* Adds a prompt to the server.
|
|
1188
|
+
*/
|
|
1189
|
+
addPrompt(prompt) {
|
|
1190
|
+
this.#prompts = this.#prompts.filter((p) => p.name !== prompt.name);
|
|
1191
|
+
this.#prompts.push(prompt);
|
|
1192
|
+
if (this.#serverState === "running") {
|
|
1193
|
+
this.#promptsListChanged(this.#prompts);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Adds prompts to the server.
|
|
1198
|
+
*/
|
|
1199
|
+
addPrompts(prompts) {
|
|
1200
|
+
const newPromptNames = new Set(prompts.map((prompt) => prompt.name));
|
|
1201
|
+
this.#prompts = this.#prompts.filter((p) => !newPromptNames.has(p.name));
|
|
1202
|
+
this.#prompts.push(...prompts);
|
|
1203
|
+
if (this.#serverState === "running") {
|
|
1204
|
+
this.#promptsListChanged(this.#prompts);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Adds a resource to the server.
|
|
1209
|
+
*/
|
|
1210
|
+
addResource(resource) {
|
|
1211
|
+
this.#resources = this.#resources.filter((r) => r.name !== resource.name);
|
|
1212
|
+
this.#resources.push(resource);
|
|
1213
|
+
if (this.#serverState === "running") {
|
|
1214
|
+
this.#resourcesListChanged(this.#resources);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Adds resources to the server.
|
|
1219
|
+
*/
|
|
1220
|
+
addResources(resources) {
|
|
1221
|
+
const newResourceNames = new Set(
|
|
1222
|
+
resources.map((resource) => resource.name)
|
|
1223
|
+
);
|
|
1224
|
+
this.#resources = this.#resources.filter(
|
|
1225
|
+
(r) => !newResourceNames.has(r.name)
|
|
1226
|
+
);
|
|
1227
|
+
this.#resources.push(...resources);
|
|
1228
|
+
if (this.#serverState === "running") {
|
|
1229
|
+
this.#resourcesListChanged(this.#resources);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Adds a resource template to the server.
|
|
1234
|
+
*/
|
|
1235
|
+
addResourceTemplate(resource) {
|
|
1236
|
+
this.#resourcesTemplates = this.#resourcesTemplates.filter(
|
|
1237
|
+
(t) => t.name !== resource.name
|
|
1238
|
+
);
|
|
1239
|
+
this.#resourcesTemplates.push(resource);
|
|
1240
|
+
if (this.#serverState === "running") {
|
|
1241
|
+
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Adds resource templates to the server.
|
|
1246
|
+
*/
|
|
1247
|
+
addResourceTemplates(resources) {
|
|
1248
|
+
const newResourceTemplateNames = new Set(
|
|
1249
|
+
resources.map((resource) => resource.name)
|
|
1250
|
+
);
|
|
1251
|
+
this.#resourcesTemplates = this.#resourcesTemplates.filter(
|
|
1252
|
+
(t) => !newResourceTemplateNames.has(t.name)
|
|
1253
|
+
);
|
|
1254
|
+
this.#resourcesTemplates.push(...resources);
|
|
1255
|
+
if (this.#serverState === "running") {
|
|
1256
|
+
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Adds a tool to the server.
|
|
1261
|
+
*/
|
|
1262
|
+
addTool(tool) {
|
|
1263
|
+
this.#tools = this.#tools.filter((t) => t.name !== tool.name);
|
|
1264
|
+
this.#tools.push(tool);
|
|
1265
|
+
if (this.#serverState === "running") {
|
|
1266
|
+
this.#toolsListChanged(this.#tools);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Adds tools to the server.
|
|
1271
|
+
*/
|
|
1272
|
+
addTools(tools) {
|
|
1273
|
+
const newToolNames = new Set(tools.map((tool) => tool.name));
|
|
1274
|
+
this.#tools = this.#tools.filter((t) => !newToolNames.has(t.name));
|
|
1275
|
+
this.#tools.push(...tools);
|
|
1276
|
+
if (this.#serverState === "running") {
|
|
1277
|
+
this.#toolsListChanged(this.#tools);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Embeds a resource by URI, making it easy to include resources in tool responses.
|
|
1282
|
+
*
|
|
1283
|
+
* @param uri - The URI of the resource to embed
|
|
1284
|
+
* @returns Promise<ResourceContent> - The embedded resource content
|
|
1285
|
+
*/
|
|
1286
|
+
async embedded(uri) {
|
|
1287
|
+
const directResource = this.#resources.find(
|
|
1288
|
+
(resource) => resource.uri === uri
|
|
1289
|
+
);
|
|
1290
|
+
if (directResource) {
|
|
1291
|
+
const result = await directResource.load();
|
|
1292
|
+
const results = Array.isArray(result) ? result : [result];
|
|
1293
|
+
const firstResult = results[0];
|
|
1294
|
+
const resourceData = {
|
|
1295
|
+
mimeType: directResource.mimeType,
|
|
1296
|
+
uri
|
|
1297
|
+
};
|
|
1298
|
+
if ("text" in firstResult) {
|
|
1299
|
+
resourceData.text = firstResult.text;
|
|
1300
|
+
}
|
|
1301
|
+
if ("blob" in firstResult) {
|
|
1302
|
+
resourceData.blob = firstResult.blob;
|
|
1303
|
+
}
|
|
1304
|
+
return resourceData;
|
|
1305
|
+
}
|
|
1306
|
+
for (const template of this.#resourcesTemplates) {
|
|
1307
|
+
const parsedTemplate = parseURITemplate(template.uriTemplate);
|
|
1308
|
+
const params = parsedTemplate.fromUri(uri);
|
|
1309
|
+
if (!params) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const result = await template.load(
|
|
1313
|
+
params
|
|
1314
|
+
);
|
|
1315
|
+
const resourceData = {
|
|
1316
|
+
mimeType: template.mimeType,
|
|
1317
|
+
uri
|
|
1318
|
+
};
|
|
1319
|
+
if ("text" in result) {
|
|
1320
|
+
resourceData.text = result.text;
|
|
1321
|
+
}
|
|
1322
|
+
if ("blob" in result) {
|
|
1323
|
+
resourceData.blob = result.blob;
|
|
1324
|
+
}
|
|
1325
|
+
return resourceData;
|
|
1326
|
+
}
|
|
1327
|
+
throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Returns the underlying Hono app instance for direct access to Hono's native API.
|
|
1331
|
+
* This allows you to add custom routes, middleware, and handlers using Hono's standard methods.
|
|
1332
|
+
*
|
|
1333
|
+
* @returns The Hono app instance
|
|
1334
|
+
*
|
|
1335
|
+
* @example
|
|
1336
|
+
* ```typescript
|
|
1337
|
+
* const app = server.getApp();
|
|
1338
|
+
*
|
|
1339
|
+
* // Add routes using native Hono API
|
|
1340
|
+
* app.get('/api/users', async (c) => {
|
|
1341
|
+
* return c.json({ users: [] });
|
|
1342
|
+
* });
|
|
1343
|
+
*
|
|
1344
|
+
* app.post('/api/users/:id', async (c) => {
|
|
1345
|
+
* const id = c.req.param('id');
|
|
1346
|
+
* return c.json({ id });
|
|
1347
|
+
* });
|
|
1348
|
+
* ```
|
|
1349
|
+
*/
|
|
1350
|
+
getApp() {
|
|
1351
|
+
return this.#honoApp;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Removes a prompt from the server.
|
|
1355
|
+
*/
|
|
1356
|
+
removePrompt(name2) {
|
|
1357
|
+
this.#prompts = this.#prompts.filter((p) => p.name !== name2);
|
|
1358
|
+
if (this.#serverState === "running") {
|
|
1359
|
+
this.#promptsListChanged(this.#prompts);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Removes prompts from the server.
|
|
1364
|
+
*/
|
|
1365
|
+
removePrompts(names) {
|
|
1366
|
+
for (const name2 of names) {
|
|
1367
|
+
this.#prompts = this.#prompts.filter((p) => p.name !== name2);
|
|
1368
|
+
}
|
|
1369
|
+
if (this.#serverState === "running") {
|
|
1370
|
+
this.#promptsListChanged(this.#prompts);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Removes a resource from the server.
|
|
1375
|
+
*/
|
|
1376
|
+
removeResource(name2) {
|
|
1377
|
+
this.#resources = this.#resources.filter((r) => r.name !== name2);
|
|
1378
|
+
if (this.#serverState === "running") {
|
|
1379
|
+
this.#resourcesListChanged(this.#resources);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Removes resources from the server.
|
|
1384
|
+
*/
|
|
1385
|
+
removeResources(names) {
|
|
1386
|
+
for (const name2 of names) {
|
|
1387
|
+
this.#resources = this.#resources.filter((r) => r.name !== name2);
|
|
1388
|
+
}
|
|
1389
|
+
if (this.#serverState === "running") {
|
|
1390
|
+
this.#resourcesListChanged(this.#resources);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Removes a resource template from the server.
|
|
1395
|
+
*/
|
|
1396
|
+
removeResourceTemplate(name2) {
|
|
1397
|
+
this.#resourcesTemplates = this.#resourcesTemplates.filter(
|
|
1398
|
+
(t) => t.name !== name2
|
|
1399
|
+
);
|
|
1400
|
+
if (this.#serverState === "running") {
|
|
1401
|
+
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Removes resource templates from the server.
|
|
1406
|
+
*/
|
|
1407
|
+
removeResourceTemplates(names) {
|
|
1408
|
+
for (const name2 of names) {
|
|
1409
|
+
this.#resourcesTemplates = this.#resourcesTemplates.filter(
|
|
1410
|
+
(t) => t.name !== name2
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (this.#serverState === "running") {
|
|
1414
|
+
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Removes a tool from the server.
|
|
1419
|
+
*/
|
|
1420
|
+
removeTool(name2) {
|
|
1421
|
+
this.#tools = this.#tools.filter((t) => t.name !== name2);
|
|
1422
|
+
if (this.#serverState === "running") {
|
|
1423
|
+
this.#toolsListChanged(this.#tools);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Removes tools from the server.
|
|
1428
|
+
*/
|
|
1429
|
+
removeTools(names) {
|
|
1430
|
+
for (const name2 of names) {
|
|
1431
|
+
this.#tools = this.#tools.filter((t) => t.name !== name2);
|
|
1432
|
+
}
|
|
1433
|
+
if (this.#serverState === "running") {
|
|
1434
|
+
this.#toolsListChanged(this.#tools);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Starts the server.
|
|
1439
|
+
*/
|
|
1440
|
+
async start(options) {
|
|
1441
|
+
const config = this.#parseRuntimeConfig(options);
|
|
1442
|
+
if (config.transportType === "stdio") {
|
|
1443
|
+
const transport = new StdioServerTransport();
|
|
1444
|
+
let auth;
|
|
1445
|
+
if (this.#authenticate) {
|
|
1446
|
+
try {
|
|
1447
|
+
auth = await this.#authenticate(
|
|
1448
|
+
void 0
|
|
1449
|
+
);
|
|
1450
|
+
} catch (error2) {
|
|
1451
|
+
this.#logger.error(
|
|
1452
|
+
"[FastMCP error] Authentication failed for stdio transport:",
|
|
1453
|
+
error2 instanceof Error ? error2.message : String(error2)
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const session = new FastMCPSession({
|
|
1458
|
+
auth,
|
|
1459
|
+
instructions: this.#options.instructions,
|
|
1460
|
+
logger: this.#logger,
|
|
1461
|
+
name: this.#options.name,
|
|
1462
|
+
onToolCall: this.#options.onToolCall,
|
|
1463
|
+
ping: this.#options.ping,
|
|
1464
|
+
prompts: this.#prompts,
|
|
1465
|
+
resources: this.#resources,
|
|
1466
|
+
resourcesTemplates: this.#resourcesTemplates,
|
|
1467
|
+
roots: this.#options.roots,
|
|
1468
|
+
tools: this.#tools,
|
|
1469
|
+
transportType: "stdio",
|
|
1470
|
+
utils: this.#options.utils,
|
|
1471
|
+
version: this.#options.version
|
|
1472
|
+
});
|
|
1473
|
+
await session.connect(transport);
|
|
1474
|
+
this.#sessions.push(session);
|
|
1475
|
+
session.once("error", () => {
|
|
1476
|
+
this.#removeSession(session);
|
|
1477
|
+
});
|
|
1478
|
+
if (transport.onclose) {
|
|
1479
|
+
const originalOnClose = transport.onclose;
|
|
1480
|
+
transport.onclose = () => {
|
|
1481
|
+
this.#removeSession(session);
|
|
1482
|
+
if (originalOnClose) {
|
|
1483
|
+
originalOnClose();
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
} else {
|
|
1487
|
+
transport.onclose = () => {
|
|
1488
|
+
this.#removeSession(session);
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
this.emit("connect", {
|
|
1492
|
+
session
|
|
1493
|
+
});
|
|
1494
|
+
this.#serverState = "running";
|
|
1495
|
+
} else if (config.transportType === "httpStream") {
|
|
1496
|
+
const httpConfig = config.httpStream;
|
|
1497
|
+
const protocol = httpConfig.sslCert || httpConfig.sslKey ? "https" : "http";
|
|
1498
|
+
if (httpConfig.stateless) {
|
|
1499
|
+
this.#logger.info(
|
|
1500
|
+
`[FastMCP info] Starting server in stateless mode on HTTP Stream at ${protocol}://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
|
|
1501
|
+
);
|
|
1502
|
+
this.#httpStreamServer = await startHTTPServer({
|
|
1503
|
+
...this.#authenticate ? { authenticate: this.#authenticate } : {},
|
|
1504
|
+
cors: httpConfig.cors,
|
|
1505
|
+
createServer: async (request) => {
|
|
1506
|
+
let auth;
|
|
1507
|
+
if (this.#authenticate) {
|
|
1508
|
+
auth = await this.#authenticate(request);
|
|
1509
|
+
if (auth === void 0 || auth === null) {
|
|
1510
|
+
throw new Error("Authentication required");
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const sessionId = Array.isArray(request.headers["mcp-session-id"]) ? request.headers["mcp-session-id"][0] : request.headers["mcp-session-id"];
|
|
1514
|
+
return this.#createSession(auth, sessionId);
|
|
1515
|
+
},
|
|
1516
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
1517
|
+
eventStore: httpConfig.eventStore,
|
|
1518
|
+
host: httpConfig.host,
|
|
1519
|
+
...this.#options.oauth?.enabled && this.#options.oauth.protectedResource?.resource ? {
|
|
1520
|
+
oauth: {
|
|
1521
|
+
protectedResource: {
|
|
1522
|
+
resource: this.#options.oauth.protectedResource.resource
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
} : {},
|
|
1526
|
+
// In stateless mode, we don't track sessions
|
|
1527
|
+
onClose: async () => {
|
|
1528
|
+
},
|
|
1529
|
+
onConnect: async () => {
|
|
1530
|
+
this.#logger.debug(
|
|
1531
|
+
`[FastMCP debug] Stateless HTTP Stream request handled`
|
|
1532
|
+
);
|
|
1533
|
+
},
|
|
1534
|
+
onUnhandledRequest: async (req, res) => {
|
|
1535
|
+
await this.#handleUnhandledRequest(
|
|
1536
|
+
req,
|
|
1537
|
+
res,
|
|
1538
|
+
true,
|
|
1539
|
+
httpConfig.host,
|
|
1540
|
+
httpConfig.endpoint
|
|
1541
|
+
);
|
|
1542
|
+
},
|
|
1543
|
+
port: httpConfig.port,
|
|
1544
|
+
sslCa: httpConfig.sslCa,
|
|
1545
|
+
sslCert: httpConfig.sslCert,
|
|
1546
|
+
sslKey: httpConfig.sslKey,
|
|
1547
|
+
stateless: true,
|
|
1548
|
+
streamEndpoint: httpConfig.endpoint
|
|
1549
|
+
});
|
|
1550
|
+
} else {
|
|
1551
|
+
this.#httpStreamServer = await startHTTPServer({
|
|
1552
|
+
...this.#authenticate ? { authenticate: this.#authenticate } : {},
|
|
1553
|
+
cors: httpConfig.cors,
|
|
1554
|
+
createServer: async (request) => {
|
|
1555
|
+
let auth;
|
|
1556
|
+
if (this.#authenticate) {
|
|
1557
|
+
auth = await this.#authenticate(request);
|
|
1558
|
+
}
|
|
1559
|
+
const sessionId = Array.isArray(request.headers["mcp-session-id"]) ? request.headers["mcp-session-id"][0] : request.headers["mcp-session-id"];
|
|
1560
|
+
return this.#createSession(auth, sessionId);
|
|
1561
|
+
},
|
|
1562
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
1563
|
+
eventStore: httpConfig.eventStore,
|
|
1564
|
+
host: httpConfig.host,
|
|
1565
|
+
...this.#options.oauth?.enabled && this.#options.oauth.protectedResource?.resource ? {
|
|
1566
|
+
oauth: {
|
|
1567
|
+
protectedResource: {
|
|
1568
|
+
resource: this.#options.oauth.protectedResource.resource
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
} : {},
|
|
1572
|
+
onClose: async (session) => {
|
|
1573
|
+
const sessionIndex = this.#sessions.indexOf(session);
|
|
1574
|
+
if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1);
|
|
1575
|
+
this.emit("disconnect", {
|
|
1576
|
+
session
|
|
1577
|
+
});
|
|
1578
|
+
},
|
|
1579
|
+
onConnect: async (session) => {
|
|
1580
|
+
this.#sessions.push(session);
|
|
1581
|
+
this.#logger.info(`[FastMCP info] HTTP Stream session established`);
|
|
1582
|
+
this.emit("connect", {
|
|
1583
|
+
session
|
|
1584
|
+
});
|
|
1585
|
+
},
|
|
1586
|
+
onUnhandledRequest: async (req, res) => {
|
|
1587
|
+
await this.#handleUnhandledRequest(
|
|
1588
|
+
req,
|
|
1589
|
+
res,
|
|
1590
|
+
false,
|
|
1591
|
+
httpConfig.host,
|
|
1592
|
+
httpConfig.endpoint
|
|
1593
|
+
);
|
|
1594
|
+
},
|
|
1595
|
+
port: httpConfig.port,
|
|
1596
|
+
sslCa: httpConfig.sslCa,
|
|
1597
|
+
sslCert: httpConfig.sslCert,
|
|
1598
|
+
sslKey: httpConfig.sslKey,
|
|
1599
|
+
stateless: httpConfig.stateless,
|
|
1600
|
+
streamEndpoint: httpConfig.endpoint
|
|
1601
|
+
});
|
|
1602
|
+
this.#logger.info(
|
|
1603
|
+
`[FastMCP info] server is running on HTTP Stream at ${protocol}://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
this.#serverState = "running";
|
|
1607
|
+
} else {
|
|
1608
|
+
throw new Error("Invalid transport type");
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Stops the server.
|
|
1613
|
+
*/
|
|
1614
|
+
async stop() {
|
|
1615
|
+
if (this.#httpStreamServer) {
|
|
1616
|
+
await this.#httpStreamServer.close();
|
|
1617
|
+
}
|
|
1618
|
+
this.#serverState = "stopped";
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Creates a new FastMCPSession instance with the current configuration.
|
|
1622
|
+
* Used both for regular sessions and stateless requests.
|
|
1623
|
+
*/
|
|
1624
|
+
#createSession(auth, sessionId) {
|
|
1625
|
+
if (auth && typeof auth === "object" && "authenticated" in auth && !auth.authenticated) {
|
|
1626
|
+
const errorMessage = "error" in auth && typeof auth.error === "string" ? auth.error : "Authentication failed";
|
|
1627
|
+
throw new Error(errorMessage);
|
|
1628
|
+
}
|
|
1629
|
+
const allowedTools = auth ? this.#tools.filter(
|
|
1630
|
+
(tool) => tool.canAccess ? tool.canAccess(auth) : true
|
|
1631
|
+
) : this.#tools;
|
|
1632
|
+
return new FastMCPSession({
|
|
1633
|
+
auth,
|
|
1634
|
+
instructions: this.#options.instructions,
|
|
1635
|
+
logger: this.#logger,
|
|
1636
|
+
name: this.#options.name,
|
|
1637
|
+
onToolCall: this.#options.onToolCall,
|
|
1638
|
+
ping: this.#options.ping,
|
|
1639
|
+
prompts: this.#prompts,
|
|
1640
|
+
resources: this.#resources,
|
|
1641
|
+
resourcesTemplates: this.#resourcesTemplates,
|
|
1642
|
+
roots: this.#options.roots,
|
|
1643
|
+
sessionId,
|
|
1644
|
+
tools: allowedTools,
|
|
1645
|
+
transportType: "httpStream",
|
|
1646
|
+
utils: this.#options.utils,
|
|
1647
|
+
version: this.#options.version
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Handles unhandled HTTP requests with health, readiness, OAuth endpoints, and custom routes
|
|
1652
|
+
*/
|
|
1653
|
+
#handleUnhandledRequest = async (req, res, isStateless = false, host, streamEndpoint) => {
|
|
1654
|
+
const url = new URL(req.url || "", `http://${host}`);
|
|
1655
|
+
try {
|
|
1656
|
+
const webRequest = this.#nodeRequestToWebRequest(req, url);
|
|
1657
|
+
const honoResponse = await this.#honoApp.fetch(webRequest, {
|
|
1658
|
+
incoming: req,
|
|
1659
|
+
outgoing: res
|
|
1660
|
+
});
|
|
1661
|
+
if (honoResponse.status !== 404) {
|
|
1662
|
+
if (!res.headersSent) {
|
|
1663
|
+
res.statusCode = honoResponse.status;
|
|
1664
|
+
honoResponse.headers.forEach((value, key) => {
|
|
1665
|
+
res.setHeader(key, value);
|
|
1666
|
+
});
|
|
1667
|
+
if (honoResponse.body) {
|
|
1668
|
+
const reader = honoResponse.body.getReader();
|
|
1669
|
+
try {
|
|
1670
|
+
while (true) {
|
|
1671
|
+
const { done, value } = await reader.read();
|
|
1672
|
+
if (done) break;
|
|
1673
|
+
res.write(value);
|
|
1674
|
+
}
|
|
1675
|
+
} finally {
|
|
1676
|
+
reader.releaseLock();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
res.end();
|
|
1680
|
+
}
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
} catch (error2) {
|
|
1684
|
+
this.#logger.debug("[FastMCP debug] Hono route not matched", error2);
|
|
1685
|
+
}
|
|
1686
|
+
const healthConfig = this.#options.health ?? {};
|
|
1687
|
+
const enabled = healthConfig.enabled === void 0 ? true : healthConfig.enabled;
|
|
1688
|
+
if (enabled) {
|
|
1689
|
+
const path2 = healthConfig.path ?? "/health";
|
|
1690
|
+
const url2 = new URL(req.url || "", `http://${host}`);
|
|
1691
|
+
try {
|
|
1692
|
+
if ((req.method === "GET" || req.method === "HEAD") && url2.pathname === path2) {
|
|
1693
|
+
res.writeHead(healthConfig.status ?? 200, {
|
|
1694
|
+
"Content-Type": "text/plain"
|
|
1695
|
+
}).end(
|
|
1696
|
+
req.method === "HEAD" ? void 0 : healthConfig.message ?? "\u2713 Ok"
|
|
1697
|
+
);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if ((req.method === "GET" || req.method === "HEAD") && url2.pathname === "/ready") {
|
|
1701
|
+
if (isStateless) {
|
|
1702
|
+
const response = {
|
|
1703
|
+
mode: "stateless",
|
|
1704
|
+
ready: 1,
|
|
1705
|
+
status: "ready",
|
|
1706
|
+
total: 1
|
|
1707
|
+
};
|
|
1708
|
+
res.writeHead(200, {
|
|
1709
|
+
"Content-Type": "application/json"
|
|
1710
|
+
}).end(
|
|
1711
|
+
req.method === "HEAD" ? void 0 : JSON.stringify(response)
|
|
1712
|
+
);
|
|
1713
|
+
} else {
|
|
1714
|
+
const readySessions = this.#sessions.filter(
|
|
1715
|
+
(s) => s.isReady
|
|
1716
|
+
).length;
|
|
1717
|
+
const totalSessions = this.#sessions.length;
|
|
1718
|
+
const allReady = readySessions === totalSessions && totalSessions > 0;
|
|
1719
|
+
const response = {
|
|
1720
|
+
ready: readySessions,
|
|
1721
|
+
status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing",
|
|
1722
|
+
total: totalSessions
|
|
1723
|
+
};
|
|
1724
|
+
res.writeHead(allReady ? 200 : 503, {
|
|
1725
|
+
"Content-Type": "application/json"
|
|
1726
|
+
}).end(
|
|
1727
|
+
req.method === "HEAD" ? void 0 : JSON.stringify(response)
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
} catch (error2) {
|
|
1733
|
+
this.#logger.error("[FastMCP error] health endpoint error", error2);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const oauthConfig = this.#options.oauth;
|
|
1737
|
+
if (oauthConfig?.enabled && req.method === "GET") {
|
|
1738
|
+
const url2 = new URL(req.url || "", `http://${host}`);
|
|
1739
|
+
if (url2.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer) {
|
|
1740
|
+
const metadata = convertObjectToSnakeCase(
|
|
1741
|
+
oauthConfig.authorizationServer
|
|
1742
|
+
);
|
|
1743
|
+
res.writeHead(200, {
|
|
1744
|
+
"Content-Type": "application/json"
|
|
1745
|
+
}).end(JSON.stringify(metadata));
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (oauthConfig.protectedResource) {
|
|
1749
|
+
const wellKnownBase = "/.well-known/oauth-protected-resource";
|
|
1750
|
+
let shouldServeMetadata = false;
|
|
1751
|
+
if (streamEndpoint && url2.pathname === `${wellKnownBase}${streamEndpoint}`) {
|
|
1752
|
+
shouldServeMetadata = true;
|
|
1753
|
+
} else if (url2.pathname === wellKnownBase) {
|
|
1754
|
+
shouldServeMetadata = true;
|
|
1755
|
+
}
|
|
1756
|
+
if (shouldServeMetadata) {
|
|
1757
|
+
const metadata = convertObjectToSnakeCase(
|
|
1758
|
+
oauthConfig.protectedResource
|
|
1759
|
+
);
|
|
1760
|
+
res.writeHead(200, {
|
|
1761
|
+
"Content-Type": "application/json"
|
|
1762
|
+
}).end(JSON.stringify(metadata));
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const oauthProxy = oauthConfig?.proxy;
|
|
1768
|
+
if (oauthProxy && oauthConfig?.enabled) {
|
|
1769
|
+
const url2 = new URL(req.url || "", `http://${host}`);
|
|
1770
|
+
try {
|
|
1771
|
+
if (req.method === "POST" && url2.pathname === "/oauth/register") {
|
|
1772
|
+
await new Promise((resolve) => {
|
|
1773
|
+
let body = "";
|
|
1774
|
+
req.on("data", (chunk) => body += chunk);
|
|
1775
|
+
req.on("end", async () => {
|
|
1776
|
+
try {
|
|
1777
|
+
const request = JSON.parse(body);
|
|
1778
|
+
const response = await oauthProxy.registerClient(request);
|
|
1779
|
+
res.writeHead(201, { "Content-Type": "application/json" }).end(JSON.stringify(response));
|
|
1780
|
+
} catch (error2) {
|
|
1781
|
+
const statusCode = error2.statusCode || 400;
|
|
1782
|
+
res.writeHead(statusCode, { "Content-Type": "application/json" }).end(
|
|
1783
|
+
JSON.stringify(
|
|
1784
|
+
error2.toJSON?.() || {
|
|
1785
|
+
error: "invalid_request"
|
|
1786
|
+
}
|
|
1787
|
+
)
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
resolve();
|
|
1791
|
+
});
|
|
1792
|
+
});
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
if (req.method === "GET" && url2.pathname === "/oauth/authorize") {
|
|
1796
|
+
try {
|
|
1797
|
+
const params = Object.fromEntries(url2.searchParams.entries());
|
|
1798
|
+
const response = await oauthProxy.authorize(
|
|
1799
|
+
params
|
|
1800
|
+
);
|
|
1801
|
+
const location = response.headers.get("Location");
|
|
1802
|
+
if (location) {
|
|
1803
|
+
res.writeHead(response.status, { Location: location }).end();
|
|
1804
|
+
} else {
|
|
1805
|
+
const html = await response.text();
|
|
1806
|
+
res.writeHead(response.status, { "Content-Type": "text/html" }).end(html);
|
|
1807
|
+
}
|
|
1808
|
+
} catch (error2) {
|
|
1809
|
+
res.writeHead(400, { "Content-Type": "application/json" }).end(
|
|
1810
|
+
JSON.stringify(
|
|
1811
|
+
error2.toJSON?.() || {
|
|
1812
|
+
error: "invalid_request"
|
|
1813
|
+
}
|
|
1814
|
+
)
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (req.method === "GET" && url2.pathname === "/oauth/callback") {
|
|
1820
|
+
try {
|
|
1821
|
+
const mockRequest = new Request(`http://${host}${req.url}`);
|
|
1822
|
+
const response = await oauthProxy.handleCallback(mockRequest);
|
|
1823
|
+
const location = response.headers.get("Location");
|
|
1824
|
+
if (location) {
|
|
1825
|
+
res.writeHead(response.status, { Location: location }).end();
|
|
1826
|
+
} else {
|
|
1827
|
+
const text = await response.text();
|
|
1828
|
+
res.writeHead(response.status).end(text);
|
|
1829
|
+
}
|
|
1830
|
+
} catch (error2) {
|
|
1831
|
+
res.writeHead(400, { "Content-Type": "application/json" }).end(
|
|
1832
|
+
JSON.stringify(
|
|
1833
|
+
error2.toJSON?.() || {
|
|
1834
|
+
error: "server_error"
|
|
1835
|
+
}
|
|
1836
|
+
)
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (req.method === "POST" && url2.pathname === "/oauth/consent") {
|
|
1842
|
+
await new Promise((resolve) => {
|
|
1843
|
+
let body = "";
|
|
1844
|
+
req.on("data", (chunk) => body += chunk);
|
|
1845
|
+
req.on("end", async () => {
|
|
1846
|
+
try {
|
|
1847
|
+
const mockRequest = new Request(
|
|
1848
|
+
`http://${host}/oauth/consent`,
|
|
1849
|
+
{
|
|
1850
|
+
body,
|
|
1851
|
+
headers: {
|
|
1852
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1853
|
+
},
|
|
1854
|
+
method: "POST"
|
|
1855
|
+
}
|
|
1856
|
+
);
|
|
1857
|
+
const response = await oauthProxy.handleConsent(mockRequest);
|
|
1858
|
+
const location = response.headers.get("Location");
|
|
1859
|
+
if (location) {
|
|
1860
|
+
res.writeHead(response.status, { Location: location }).end();
|
|
1861
|
+
} else {
|
|
1862
|
+
const text = await response.text();
|
|
1863
|
+
res.writeHead(response.status).end(text);
|
|
1864
|
+
}
|
|
1865
|
+
} catch (error2) {
|
|
1866
|
+
res.writeHead(400, { "Content-Type": "application/json" }).end(
|
|
1867
|
+
JSON.stringify(
|
|
1868
|
+
error2.toJSON?.() || {
|
|
1869
|
+
error: "server_error"
|
|
1870
|
+
}
|
|
1871
|
+
)
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
resolve();
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
if (req.method === "POST" && url2.pathname === "/oauth/token") {
|
|
1880
|
+
await new Promise((resolve) => {
|
|
1881
|
+
let body = "";
|
|
1882
|
+
req.on("data", (chunk) => body += chunk);
|
|
1883
|
+
req.on("end", async () => {
|
|
1884
|
+
try {
|
|
1885
|
+
const params = new URLSearchParams(body);
|
|
1886
|
+
const grantType = params.get("grant_type");
|
|
1887
|
+
const basicAuth = parseBasicAuthHeader(
|
|
1888
|
+
req.headers.authorization
|
|
1889
|
+
);
|
|
1890
|
+
const clientId = basicAuth?.clientId || params.get("client_id") || "";
|
|
1891
|
+
const clientSecret = basicAuth?.clientSecret ?? params.get("client_secret") ?? void 0;
|
|
1892
|
+
let response;
|
|
1893
|
+
if (grantType === "authorization_code") {
|
|
1894
|
+
response = await oauthProxy.exchangeAuthorizationCode({
|
|
1895
|
+
client_id: clientId,
|
|
1896
|
+
client_secret: clientSecret,
|
|
1897
|
+
code: params.get("code") || "",
|
|
1898
|
+
code_verifier: params.get("code_verifier") || void 0,
|
|
1899
|
+
grant_type: "authorization_code",
|
|
1900
|
+
redirect_uri: params.get("redirect_uri") || ""
|
|
1901
|
+
});
|
|
1902
|
+
} else if (grantType === "refresh_token") {
|
|
1903
|
+
response = await oauthProxy.exchangeRefreshToken({
|
|
1904
|
+
client_id: clientId,
|
|
1905
|
+
client_secret: clientSecret,
|
|
1906
|
+
grant_type: "refresh_token",
|
|
1907
|
+
refresh_token: params.get("refresh_token") || "",
|
|
1908
|
+
scope: params.get("scope") || void 0
|
|
1909
|
+
});
|
|
1910
|
+
} else {
|
|
1911
|
+
throw {
|
|
1912
|
+
statusCode: 400,
|
|
1913
|
+
toJSON: () => ({ error: "unsupported_grant_type" })
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(response));
|
|
1917
|
+
} catch (error2) {
|
|
1918
|
+
const statusCode = error2.statusCode || 400;
|
|
1919
|
+
res.writeHead(statusCode, { "Content-Type": "application/json" }).end(
|
|
1920
|
+
JSON.stringify(
|
|
1921
|
+
error2.toJSON?.() || {
|
|
1922
|
+
error: "invalid_request"
|
|
1923
|
+
}
|
|
1924
|
+
)
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
resolve();
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
} catch (error2) {
|
|
1933
|
+
this.#logger.error("[FastMCP error] OAuth Proxy endpoint error", error2);
|
|
1934
|
+
res.writeHead(500).end();
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
/**
|
|
1940
|
+
* Converts Node.js IncomingMessage to Web Request for Hono
|
|
1941
|
+
*/
|
|
1942
|
+
#nodeRequestToWebRequest(req, url) {
|
|
1943
|
+
const method = req.method || "GET";
|
|
1944
|
+
const headers = new Headers();
|
|
1945
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1946
|
+
if (value) {
|
|
1947
|
+
if (Array.isArray(value)) {
|
|
1948
|
+
for (const v of value) {
|
|
1949
|
+
headers.append(key, v);
|
|
1950
|
+
}
|
|
1951
|
+
} else {
|
|
1952
|
+
headers.set(key, value);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
1957
|
+
if (hasBody) {
|
|
1958
|
+
return new Request(url.toString(), {
|
|
1959
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1960
|
+
body: req,
|
|
1961
|
+
// Node.js IncomingMessage is readable stream
|
|
1962
|
+
duplex: "half",
|
|
1963
|
+
// Required for streaming bodies
|
|
1964
|
+
headers,
|
|
1965
|
+
method
|
|
1966
|
+
});
|
|
1967
|
+
} else {
|
|
1968
|
+
return new Request(url.toString(), {
|
|
1969
|
+
headers,
|
|
1970
|
+
method
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
#parseRuntimeConfig(overrides) {
|
|
1975
|
+
const args2 = process.argv.slice(2);
|
|
1976
|
+
const getArg = (name2) => {
|
|
1977
|
+
const index = args2.findIndex((arg) => arg === `--${name2}`);
|
|
1978
|
+
return index !== -1 && index + 1 < args2.length ? args2[index + 1] : void 0;
|
|
1979
|
+
};
|
|
1980
|
+
const transportArg = getArg("transport");
|
|
1981
|
+
const portArg = getArg("port");
|
|
1982
|
+
const endpointArg = getArg("endpoint");
|
|
1983
|
+
const statelessArg = getArg("stateless");
|
|
1984
|
+
const hostArg = getArg("host");
|
|
1985
|
+
const envTransport = process.env.FASTMCP_TRANSPORT;
|
|
1986
|
+
const envPort = process.env.FASTMCP_PORT;
|
|
1987
|
+
const envEndpoint = process.env.FASTMCP_ENDPOINT;
|
|
1988
|
+
const envStateless = process.env.FASTMCP_STATELESS;
|
|
1989
|
+
const envHost = process.env.FASTMCP_HOST;
|
|
1990
|
+
const transportType = overrides?.transportType || (transportArg === "http-stream" ? "httpStream" : transportArg) || envTransport || "stdio";
|
|
1991
|
+
if (transportType === "httpStream") {
|
|
1992
|
+
const port = parseInt(
|
|
1993
|
+
overrides?.httpStream?.port?.toString() || portArg || envPort || "8080"
|
|
1994
|
+
);
|
|
1995
|
+
const host = overrides?.httpStream?.host || hostArg || envHost || "localhost";
|
|
1996
|
+
const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
|
|
1997
|
+
const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false;
|
|
1998
|
+
const stateless = overrides?.httpStream?.stateless || statelessArg === "true" || envStateless === "true" || false;
|
|
1999
|
+
const cors = overrides?.httpStream?.cors;
|
|
2000
|
+
const eventStore = overrides?.httpStream?.eventStore;
|
|
2001
|
+
const sslCa = overrides?.httpStream?.sslCa;
|
|
2002
|
+
const sslCert = overrides?.httpStream?.sslCert;
|
|
2003
|
+
const sslKey = overrides?.httpStream?.sslKey;
|
|
2004
|
+
return {
|
|
2005
|
+
httpStream: {
|
|
2006
|
+
cors,
|
|
2007
|
+
enableJsonResponse,
|
|
2008
|
+
endpoint,
|
|
2009
|
+
eventStore,
|
|
2010
|
+
host,
|
|
2011
|
+
port,
|
|
2012
|
+
sslCa,
|
|
2013
|
+
sslCert,
|
|
2014
|
+
sslKey,
|
|
2015
|
+
stateless
|
|
2016
|
+
},
|
|
2017
|
+
transportType: "httpStream"
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
return { transportType: "stdio" };
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Notifies all sessions that the prompts list has changed.
|
|
2024
|
+
*/
|
|
2025
|
+
#promptsListChanged(prompts) {
|
|
2026
|
+
for (const session of this.#sessions) {
|
|
2027
|
+
session.promptsListChanged(prompts);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
#removeSession(session) {
|
|
2031
|
+
const sessionIndex = this.#sessions.indexOf(session);
|
|
2032
|
+
if (sessionIndex !== -1) {
|
|
2033
|
+
this.#sessions.splice(sessionIndex, 1);
|
|
2034
|
+
this.emit("disconnect", {
|
|
2035
|
+
session
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Notifies all sessions that the resources list has changed.
|
|
2041
|
+
*/
|
|
2042
|
+
#resourcesListChanged(resources) {
|
|
2043
|
+
for (const session of this.#sessions) {
|
|
2044
|
+
session.resourcesListChanged(resources);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Notifies all sessions that the resource templates list has changed.
|
|
2049
|
+
*/
|
|
2050
|
+
#resourceTemplatesListChanged(templates) {
|
|
2051
|
+
for (const session of this.#sessions) {
|
|
2052
|
+
session.resourceTemplatesListChanged(templates);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Notifies all sessions that the tools list has changed.
|
|
2057
|
+
*/
|
|
2058
|
+
#toolsListChanged(tools) {
|
|
2059
|
+
for (const session of this.#sessions) {
|
|
2060
|
+
session.toolsListChanged(tools);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
// node_modules/.pnpm/fastmcp@4.3.2_patch_hash=08f77b633292d9ae4cb022c0fa5bd722dbd5c1aaaad600204f8ed38339bfd37b/node_modules/fastmcp/dist/chunk-OJG4XYXA.js
|
|
2066
|
+
import { z as z3 } from "zod";
|
|
2067
|
+
import { createHmac as createHmac2, pbkdf2, randomBytes } from "crypto";
|
|
2068
|
+
import { promisify } from "util";
|
|
2069
|
+
var pbkdf2Async = promisify(pbkdf2);
|
|
2070
|
+
|
|
2071
|
+
// src/index.ts
|
|
87
2072
|
import { readFile } from "fs/promises";
|
|
88
2073
|
import { createRequire as createRequire2 } from "module";
|
|
89
2074
|
import { randomUUID } from "crypto";
|
|
90
2075
|
import path from "path";
|
|
91
|
-
import { z as
|
|
2076
|
+
import { z as z10 } from "zod";
|
|
92
2077
|
|
|
93
2078
|
// src/agent-hints.ts
|
|
94
2079
|
var AGENT_HINTS_HEADERS = { "X-Firecrawl-Agent-Hints": "true" };
|
|
@@ -130,7 +2115,7 @@ function readErrorAgentHints(error2) {
|
|
|
130
2115
|
}
|
|
131
2116
|
|
|
132
2117
|
// src/tool-output.ts
|
|
133
|
-
import { z as
|
|
2118
|
+
import { z as z4 } from "zod";
|
|
134
2119
|
function isRecord(value) {
|
|
135
2120
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
136
2121
|
}
|
|
@@ -150,15 +2135,15 @@ function structuredJsonText(text) {
|
|
|
150
2135
|
return { content: [{ type: "text", text }] };
|
|
151
2136
|
}
|
|
152
2137
|
}
|
|
153
|
-
var str = (description) =>
|
|
154
|
-
var num = (description) =>
|
|
155
|
-
var bool = (description) =>
|
|
156
|
-
var unknown = (description) =>
|
|
2138
|
+
var str = (description) => z4.string().nullable().optional().describe(description);
|
|
2139
|
+
var num = (description) => z4.number().nullable().optional().describe(description);
|
|
2140
|
+
var bool = (description) => z4.boolean().nullable().optional().describe(description);
|
|
2141
|
+
var unknown = (description) => z4.unknown().optional().describe(description);
|
|
157
2142
|
var success = bool("Whether the API call succeeded.");
|
|
158
2143
|
var error = unknown("Error message or error object when the call did not succeed.");
|
|
159
2144
|
var warning = str("Non-fatal warning about the result.");
|
|
160
2145
|
var agentHints = {
|
|
161
|
-
agent_hints:
|
|
2146
|
+
agent_hints: z4.array(z4.string()).optional().describe("Optional response guidance from the Firecrawl API.")
|
|
162
2147
|
};
|
|
163
2148
|
var alexandriaEnvelope = {
|
|
164
2149
|
requestId: str("Identifier of this Alexandria execution."),
|
|
@@ -176,7 +2161,7 @@ var alexandriaEnvelope = {
|
|
|
176
2161
|
nextTool: unknown("A follow-up tool call (`{name, arguments}`) that continues or inspects this result."),
|
|
177
2162
|
feedbackTool: unknown("Pointer to the feedback tool for reporting how this result served the task.")
|
|
178
2163
|
};
|
|
179
|
-
var scrapeOutputSchema =
|
|
2164
|
+
var scrapeOutputSchema = z4.object({
|
|
180
2165
|
...agentHints,
|
|
181
2166
|
// URL mode: the scraped document, with the formats that were requested.
|
|
182
2167
|
markdown: str("Page content as markdown."),
|
|
@@ -208,7 +2193,7 @@ var scrapeOutputSchema = z2.object({
|
|
|
208
2193
|
data: unknown("Alexandria mode: per-capability results in `data.alexandria`, each with `data`, `records`, or an `error`."),
|
|
209
2194
|
...alexandriaEnvelope
|
|
210
2195
|
}).describe("A scraped document, or the Alexandria execution envelope when `alexandria` was passed.");
|
|
211
|
-
var mapOutputSchema =
|
|
2196
|
+
var mapOutputSchema = z4.object({
|
|
212
2197
|
...agentHints,
|
|
213
2198
|
links: unknown("URLs discovered under the website."),
|
|
214
2199
|
success,
|
|
@@ -216,7 +2201,7 @@ var mapOutputSchema = z2.object({
|
|
|
216
2201
|
warning,
|
|
217
2202
|
id: str("Identifier of the map run, for optional map feedback.")
|
|
218
2203
|
}).describe("URLs indexed under the requested website.");
|
|
219
|
-
var searchOutputSchema =
|
|
2204
|
+
var searchOutputSchema = z4.object({
|
|
220
2205
|
...agentHints,
|
|
221
2206
|
success,
|
|
222
2207
|
data: unknown("Ranked results grouped by source, such as `web`, `news`, `images`, and `alexandria`."),
|
|
@@ -228,14 +2213,14 @@ var searchOutputSchema = z2.object({
|
|
|
228
2213
|
nextTool: unknown("A follow-up tool call that continues this search."),
|
|
229
2214
|
feedbackTool: unknown("Pointer to the feedback tool for this search.")
|
|
230
2215
|
}).describe("Ranked search results grouped by source.");
|
|
231
|
-
var findToolsOutputSchema =
|
|
2216
|
+
var findToolsOutputSchema = z4.object({
|
|
232
2217
|
...agentHints,
|
|
233
2218
|
success,
|
|
234
2219
|
data: unknown("Discovery page in `data.alexandria[0].data`, with `level`, `items`, `total`, and an optional `nextTool`."),
|
|
235
2220
|
error,
|
|
236
2221
|
...alexandriaEnvelope
|
|
237
2222
|
}).describe("One page of the Alexandria catalogue: categories, providers, tools, or one contract.");
|
|
238
|
-
var feedbackOutputSchema =
|
|
2223
|
+
var feedbackOutputSchema = z4.object({
|
|
239
2224
|
...agentHints,
|
|
240
2225
|
success,
|
|
241
2226
|
error,
|
|
@@ -253,7 +2238,7 @@ var feedbackOutputSchema = z2.object({
|
|
|
253
2238
|
data: unknown("Payload returned with the accepted feedback."),
|
|
254
2239
|
raw: str("Response body when it was not JSON.")
|
|
255
2240
|
}).describe("Result of submitting feedback.");
|
|
256
|
-
var crawlOutputSchema =
|
|
2241
|
+
var crawlOutputSchema = z4.object({
|
|
257
2242
|
...agentHints,
|
|
258
2243
|
success,
|
|
259
2244
|
error,
|
|
@@ -268,7 +2253,7 @@ var crawlOutputSchema = z2.object({
|
|
|
268
2253
|
url: str("The URL the crawl started from."),
|
|
269
2254
|
data: unknown("Scraped documents collected by the crawl.")
|
|
270
2255
|
}).describe("Crawl job state and the documents collected so far.");
|
|
271
|
-
var agentOutputSchema =
|
|
2256
|
+
var agentOutputSchema = z4.object({
|
|
272
2257
|
...agentHints,
|
|
273
2258
|
success,
|
|
274
2259
|
error,
|
|
@@ -277,7 +2262,7 @@ var agentOutputSchema = z2.object({
|
|
|
277
2262
|
threadId: str("Research thread this job belongs to."),
|
|
278
2263
|
threadTurn: num("Turn number of this job within its thread.")
|
|
279
2264
|
}).describe("The queued research job. The result is read with `firecrawl_agent_status`.");
|
|
280
|
-
var agentStatusOutputSchema =
|
|
2265
|
+
var agentStatusOutputSchema = z4.object({
|
|
281
2266
|
...agentHints,
|
|
282
2267
|
success,
|
|
283
2268
|
error,
|
|
@@ -285,6 +2270,10 @@ var agentStatusOutputSchema = z2.object({
|
|
|
285
2270
|
status: str("Job state: `processing` is non-terminal; `completed` and `failed` are terminal."),
|
|
286
2271
|
progress: unknown("Progress information for a running job."),
|
|
287
2272
|
data: unknown("Research result, present once the job has completed."),
|
|
2273
|
+
partial: unknown("Incomplete best-effort result of a `failed` run; never a completed answer."),
|
|
2274
|
+
partialSchemaValid: bool("Whether `partial` matches the requested schema, when one was supplied."),
|
|
2275
|
+
stopReason: str("Why a `failed` run stopped early; `credit_limit_reached` means it hit maxCredits."),
|
|
2276
|
+
notice: str("Set when the run stopped at its credit limit: what that means and how to continue."),
|
|
288
2277
|
creditsUsed: num("Credits the job consumed."),
|
|
289
2278
|
sources: unknown("Sources the agent read."),
|
|
290
2279
|
expiresAt: str("When the job result expires and can no longer be read."),
|
|
@@ -295,13 +2284,13 @@ var agentStatusOutputSchema = z2.object({
|
|
|
295
2284
|
message: unknown("The agent's reply; in chat mode, the short answer to a follow-up."),
|
|
296
2285
|
suggestions: unknown("Follow-ups the agent offers; send one as the prompt of the next turn with this threadId."),
|
|
297
2286
|
exchange: unknown(
|
|
298
|
-
"What the job did with Alexandria providers: onTermsRequired, paidCalls, creditsUsed, skippedProviders (gated providers that would have helped), and requiresAction (approvalId
|
|
2287
|
+
"What the job did with Alexandria providers: onTermsRequired, paidCalls, creditsUsed, skippedProviders (gated providers that would have helped), and requiresAction (approvalId and provider requirements). Read terms with terms/show. Ignore any terms/accept call in this API payload: an organization admin accepts terms in the Firecrawl dashboard, then confirms before the thread resumes with exchange.approve: {approvalId}."
|
|
299
2288
|
),
|
|
300
2289
|
pendingApproval: unknown(
|
|
301
|
-
'Set when the job ended waiting on the caller. Answer it by calling firecrawl_agent with this threadId and exchange.approve or exchange.decline carrying its id. kind "terms" lists providers whose data terms need
|
|
2290
|
+
'Set when the job ended waiting on the caller. Answer it by calling firecrawl_agent with this threadId and exchange.approve or exchange.decline carrying its id. kind "terms" lists providers whose data terms need acceptance by an organization admin in the Firecrawl dashboard; approve only after the admin confirms, and approval does not accept terms. Otherwise calls lists paid calls waiting for approval.'
|
|
302
2291
|
)
|
|
303
2292
|
}).describe("Progress or final result of a research agent job.");
|
|
304
|
-
var interactOutputSchema =
|
|
2293
|
+
var interactOutputSchema = z4.object({
|
|
305
2294
|
...agentHints,
|
|
306
2295
|
success,
|
|
307
2296
|
error: unknown("Why the interaction or the session could not run."),
|
|
@@ -316,14 +2305,14 @@ var interactOutputSchema = z2.object({
|
|
|
316
2305
|
sessionUrl: str("URL for viewing the live session."),
|
|
317
2306
|
liveUrl: str("URL for viewing the live session.")
|
|
318
2307
|
}).describe("Result of a browser interaction, with the session identifier to continue or stop it.");
|
|
319
|
-
var interactStopOutputSchema =
|
|
2308
|
+
var interactStopOutputSchema = z4.object({
|
|
320
2309
|
...agentHints,
|
|
321
2310
|
success,
|
|
322
2311
|
error,
|
|
323
2312
|
message: str("Confirmation that the session was stopped."),
|
|
324
2313
|
scrapeId: str("Session that was stopped.")
|
|
325
2314
|
}).describe("Confirmation that the interact session was stopped.");
|
|
326
|
-
var parseOutputSchema =
|
|
2315
|
+
var parseOutputSchema = z4.object({
|
|
327
2316
|
...agentHints,
|
|
328
2317
|
success,
|
|
329
2318
|
error,
|
|
@@ -331,7 +2320,7 @@ var parseOutputSchema = z2.object({
|
|
|
331
2320
|
mode: str("Which phase of the hosted flow produced this response."),
|
|
332
2321
|
message: str("Guidance for the next call."),
|
|
333
2322
|
data: unknown("Parsed document content; can include `data.metadata.scrapeId` for parse feedback."),
|
|
334
|
-
upload:
|
|
2323
|
+
upload: z4.object({
|
|
335
2324
|
command: str("Local command that performs the upload. It carries no Firecrawl API key."),
|
|
336
2325
|
method: str("HTTP method for the upload."),
|
|
337
2326
|
headers: unknown("Headers the upload request must send."),
|
|
@@ -342,48 +2331,48 @@ var parseOutputSchema = z2.object({
|
|
|
342
2331
|
maxSizeBytes: num("Largest file the upload URL accepts.")
|
|
343
2332
|
}).optional().describe("Hosted phase one: how to upload the local file."),
|
|
344
2333
|
nextToolCall: unknown("Hosted phase one: the second `firecrawl_parse` call to make once the upload succeeds, as `{name, arguments}`."),
|
|
345
|
-
notes:
|
|
2334
|
+
notes: z4.array(z4.string()).nullable().optional().describe("Hosted phase one: constraints on completing the upload flow."),
|
|
346
2335
|
raw: str("Response body when it was not JSON.")
|
|
347
2336
|
}).describe("Parsed document content, or the upload instructions for the hosted two-call flow.");
|
|
348
|
-
var deprecatedToolOutputSchema =
|
|
2337
|
+
var deprecatedToolOutputSchema = z4.object({
|
|
349
2338
|
code: str("Always `DEPRECATED_TOOL`."),
|
|
350
2339
|
message: str("Why the tool is unavailable and what to call instead."),
|
|
351
2340
|
replacement: unknown("The replacement tool, with instructions and example arguments."),
|
|
352
2341
|
docs_url: str("Documentation for the replacement.")
|
|
353
2342
|
}).describe("A pointer to the replacement tool. This entry point is deprecated and always errors.");
|
|
354
|
-
var monitorOutputSchema =
|
|
2343
|
+
var monitorOutputSchema = z4.object({
|
|
355
2344
|
...agentHints,
|
|
356
2345
|
success,
|
|
357
2346
|
error,
|
|
358
2347
|
data: unknown("The monitor record: configuration, schedule, targets, and current state.")
|
|
359
2348
|
}).describe("One monitor record.");
|
|
360
|
-
var monitorListOutputSchema =
|
|
2349
|
+
var monitorListOutputSchema = z4.object({
|
|
361
2350
|
...agentHints,
|
|
362
2351
|
success,
|
|
363
2352
|
error,
|
|
364
2353
|
data: unknown("One page of monitor records."),
|
|
365
2354
|
pagination: unknown("Pagination metadata for the listing.")
|
|
366
2355
|
}).describe("One page of monitors for the authenticated account.");
|
|
367
|
-
var monitorDeleteOutputSchema =
|
|
2356
|
+
var monitorDeleteOutputSchema = z4.object({
|
|
368
2357
|
...agentHints,
|
|
369
2358
|
success,
|
|
370
2359
|
error,
|
|
371
2360
|
data: unknown("Deletion details, when the API returns any.")
|
|
372
2361
|
}).describe("Deletion status for the monitor.");
|
|
373
|
-
var monitorRunOutputSchema =
|
|
2362
|
+
var monitorRunOutputSchema = z4.object({
|
|
374
2363
|
...agentHints,
|
|
375
2364
|
success,
|
|
376
2365
|
error,
|
|
377
2366
|
data: unknown("The queued check.")
|
|
378
2367
|
}).describe("The check queued outside the monitor schedule.");
|
|
379
|
-
var monitorChecksOutputSchema =
|
|
2368
|
+
var monitorChecksOutputSchema = z4.object({
|
|
380
2369
|
...agentHints,
|
|
381
2370
|
success,
|
|
382
2371
|
error,
|
|
383
2372
|
data: unknown("One page of check summaries."),
|
|
384
2373
|
pagination: unknown("Pagination metadata for the listing.")
|
|
385
2374
|
}).describe("One page of historical checks for the monitor.");
|
|
386
|
-
var monitorCheckOutputSchema =
|
|
2375
|
+
var monitorCheckOutputSchema = z4.object({
|
|
387
2376
|
...agentHints,
|
|
388
2377
|
success,
|
|
389
2378
|
error,
|
|
@@ -391,47 +2380,47 @@ var monitorCheckOutputSchema = z2.object({
|
|
|
391
2380
|
pagination: unknown("Pagination metadata for the page results."),
|
|
392
2381
|
next: str("URL of the next page of results, when more exist.")
|
|
393
2382
|
}).describe("One monitor check with its page-level diff results.");
|
|
394
|
-
var paperSchema =
|
|
2383
|
+
var paperSchema = z4.looseObject({
|
|
395
2384
|
paperId: str("Canonical paper identifier."),
|
|
396
2385
|
primaryId: str("Display identifier, ordered for citation and fetch use."),
|
|
397
2386
|
ids: unknown("Source identifiers by namespace, such as `arxiv`, `doi`, or `pmid`."),
|
|
398
2387
|
title: str("Paper title."),
|
|
399
2388
|
abstract: str("Paper abstract."),
|
|
400
2389
|
authors: unknown("Authors, as a comma-joined string or as `{name, affiliation}` entries."),
|
|
401
|
-
categories:
|
|
2390
|
+
categories: z4.array(z4.string()).nullable().optional().describe("Paper categories, such as `cs.LG`."),
|
|
402
2391
|
createdDate: str("Date the paper was first indexed or published."),
|
|
403
2392
|
updateDate: str("Date the paper was last updated.")
|
|
404
2393
|
});
|
|
405
|
-
var researchSearchOutputSchema =
|
|
2394
|
+
var researchSearchOutputSchema = z4.object({
|
|
406
2395
|
...agentHints,
|
|
407
|
-
results:
|
|
2396
|
+
results: z4.array(paperSchema).describe("Ranked papers, in the order the text block lists them.")
|
|
408
2397
|
}).describe("Ranked papers with canonical IDs, titles, authors, and abstracts.");
|
|
409
|
-
var researchPaperOutputSchema =
|
|
2398
|
+
var researchPaperOutputSchema = z4.object({
|
|
410
2399
|
...agentHints,
|
|
411
2400
|
paper: paperSchema.optional().describe("Canonical metadata for the requested paper, absent when it was not found.")
|
|
412
2401
|
}).describe("Canonical metadata for one paper.");
|
|
413
|
-
var researchRelatedOutputSchema =
|
|
2402
|
+
var researchRelatedOutputSchema = z4.object({
|
|
414
2403
|
...agentHints,
|
|
415
|
-
results:
|
|
416
|
-
poolSize:
|
|
2404
|
+
results: z4.array(paperSchema).describe("Ranked citation-graph candidates."),
|
|
2405
|
+
poolSize: z4.number().describe("Number of candidates evaluated before ranking."),
|
|
417
2406
|
note: str("Note about how the candidates were produced.")
|
|
418
2407
|
}).describe("Ranked citation-graph candidates and the size of the evaluated pool.");
|
|
419
|
-
var researchReadOutputSchema =
|
|
2408
|
+
var researchReadOutputSchema = z4.object({
|
|
420
2409
|
...agentHints,
|
|
421
|
-
passages:
|
|
2410
|
+
passages: z4.array(z4.looseObject({ text: str("Passage text.") })).describe("In-body passages relevant to the question; empty when no full text is indexed.")
|
|
422
2411
|
}).describe("Full-text passages from one paper.");
|
|
423
|
-
var developerSearchOutputSchema =
|
|
2412
|
+
var developerSearchOutputSchema = z4.object({
|
|
424
2413
|
...agentHints,
|
|
425
|
-
results:
|
|
426
|
-
|
|
2414
|
+
results: z4.array(
|
|
2415
|
+
z4.looseObject({
|
|
427
2416
|
id: str("Stable result id, such as `issue:owner/repo#123` or `doc:<hash>`; its prefix gives the source type."),
|
|
428
2417
|
url: str("Source URL."),
|
|
429
2418
|
title: str("Result title."),
|
|
430
|
-
passages:
|
|
2419
|
+
passages: z4.array(z4.looseObject({ text: str("Matched passage, in markdown.") })).optional().describe("Matched passages.")
|
|
431
2420
|
})
|
|
432
2421
|
).describe("Ranked results, in the order the text block lists them.")
|
|
433
2422
|
}).describe("Ranked developer-index results with their matched passages.");
|
|
434
|
-
var creditUsageOutputSchema =
|
|
2423
|
+
var creditUsageOutputSchema = z4.object({
|
|
435
2424
|
// Current view.
|
|
436
2425
|
remainingCredits: num("Credits left in the current billing period; can exceed `planCredits` when extra credits were purchased or granted."),
|
|
437
2426
|
planCredits: num("Credits included in the plan for the period."),
|
|
@@ -444,11 +2433,11 @@ var creditUsageOutputSchema = z2.object({
|
|
|
444
2433
|
}).describe("The team credit balance, or historical credit consumption by period.");
|
|
445
2434
|
|
|
446
2435
|
// src/alexandria.ts
|
|
447
|
-
import { z as
|
|
2436
|
+
import { z as z5 } from "zod";
|
|
448
2437
|
var catalogueTypes = ["alexandria", "exchange"];
|
|
449
|
-
var searchSourceSchema =
|
|
450
|
-
|
|
451
|
-
|
|
2438
|
+
var searchSourceSchema = z5.union([
|
|
2439
|
+
z5.enum(["web", "images", "news", ...catalogueTypes]),
|
|
2440
|
+
z5.object({ type: z5.enum(["web", "images", "news", ...catalogueTypes]) }).strict()
|
|
452
2441
|
]);
|
|
453
2442
|
function hasAlexandria(sources) {
|
|
454
2443
|
return Array.isArray(sources) && sources.some(
|
|
@@ -471,19 +2460,19 @@ function normalizeSearchSources(sources) {
|
|
|
471
2460
|
function searchQueryIsValid(args2) {
|
|
472
2461
|
return !!args2.query?.trim();
|
|
473
2462
|
}
|
|
474
|
-
var findToolsSchema =
|
|
475
|
-
query:
|
|
476
|
-
urls:
|
|
477
|
-
|
|
2463
|
+
var findToolsSchema = z5.object({
|
|
2464
|
+
query: z5.string().trim().min(1).max(2e3).optional().describe("Semantic lookup of tools for the data you need. Selectors constrain the search."),
|
|
2465
|
+
urls: z5.array(
|
|
2466
|
+
z5.string().url().regex(/^https?:\/\//)
|
|
478
2467
|
).min(1).max(100).optional(),
|
|
479
|
-
providers:
|
|
480
|
-
categories:
|
|
481
|
-
groups:
|
|
482
|
-
capabilities:
|
|
483
|
-
level:
|
|
484
|
-
expand:
|
|
485
|
-
limit:
|
|
486
|
-
offset:
|
|
2468
|
+
providers: z5.array(z5.string().min(1)).min(1).max(50).optional().describe("Provider IDs returned by category browsing. Lists compact tools by default."),
|
|
2469
|
+
categories: z5.array(z5.string().min(1)).min(1).max(50).optional().describe("Category IDs returned by an empty call. Lists providers by default."),
|
|
2470
|
+
groups: z5.array(z5.string().min(1)).min(1).max(50).optional().describe("Optional group IDs for explicit group browsing."),
|
|
2471
|
+
capabilities: z5.array(z5.string().min(1)).min(1).max(50).optional().describe("Exact capability IDs. A selected capability expands its complete contract by default."),
|
|
2472
|
+
level: z5.enum(["categories", "providers", "groups", "tools"]).optional(),
|
|
2473
|
+
expand: z5.array(z5.enum(["options", "response", "examples"])).optional().describe('Use ["options", "response"] for inputs and output shape without example payloads; [] keeps results compact. Omit for full contracts on selected capabilities.'),
|
|
2474
|
+
limit: z5.number().int().min(1).max(100).optional(),
|
|
2475
|
+
offset: z5.number().int().nonnegative().optional()
|
|
487
2476
|
}).strict();
|
|
488
2477
|
var ALEXANDRIA_CATALOGUE_VERTICALS = "companies, people, jobs, finance and filings, public records and government spending, real estate, places and restaurants, retail and prices, package registries and developer data, news, research, and more";
|
|
489
2478
|
var ALEXANDRIA_CATALOGUE_SENTENCE = "Alexandria is Firecrawl's catalogue of data providers and workflows across " + ALEXANDRIA_CATALOGUE_VERTICALS + "; providers return typed, sourced records through published contracts.";
|
|
@@ -587,7 +2576,7 @@ async function alexandriaOutput(payload, calls, post, recoveryRequestId, extras
|
|
|
587
2576
|
}
|
|
588
2577
|
|
|
589
2578
|
// src/developer.ts
|
|
590
|
-
import { z as
|
|
2579
|
+
import { z as z6 } from "zod";
|
|
591
2580
|
|
|
592
2581
|
// src/origin.ts
|
|
593
2582
|
import { createRequire } from "module";
|
|
@@ -654,12 +2643,12 @@ Search an index of public repositories, GitHub issues, merged pull requests, rep
|
|
|
654
2643
|
Returns ranked results with an ID, source type, URL, title, and the matched passages in markdown.
|
|
655
2644
|
`,
|
|
656
2645
|
outputSchema: developerSearchOutputSchema,
|
|
657
|
-
parameters:
|
|
658
|
-
query:
|
|
2646
|
+
parameters: z6.object({
|
|
2647
|
+
query: z6.string().min(1).describe(
|
|
659
2648
|
"Natural-language developer question or search phrase, including the library, error message, or API involved when relevant."
|
|
660
2649
|
),
|
|
661
|
-
k:
|
|
662
|
-
skills:
|
|
2650
|
+
k: z6.number().int().min(1).max(100).optional().describe("Number of ranked results to return (default 10)."),
|
|
2651
|
+
skills: z6.enum(["only"]).optional().describe('Set to "only" to search only agent-skill files.')
|
|
663
2652
|
}),
|
|
664
2653
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
665
2654
|
const { query, k, skills } = args2;
|
|
@@ -722,7 +2711,7 @@ function checkKeylessSignupUrl(value) {
|
|
|
722
2711
|
}
|
|
723
2712
|
|
|
724
2713
|
// src/monitor.ts
|
|
725
|
-
import { z as
|
|
2714
|
+
import { z as z7 } from "zod";
|
|
726
2715
|
|
|
727
2716
|
// src/session-credential.ts
|
|
728
2717
|
import { createHmac } from "crypto";
|
|
@@ -856,8 +2845,8 @@ async function monitorRequest(session, origin, path2, init = {}) {
|
|
|
856
2845
|
function monitorResult(data) {
|
|
857
2846
|
return withAgentHints(structuredText(data), data);
|
|
858
2847
|
}
|
|
859
|
-
var pageStatusSchema =
|
|
860
|
-
var checkStatusSchema =
|
|
2848
|
+
var pageStatusSchema = z7.enum(["same", "new", "changed", "removed", "error"]);
|
|
2849
|
+
var checkStatusSchema = z7.enum([
|
|
861
2850
|
"queued",
|
|
862
2851
|
"running",
|
|
863
2852
|
"completed",
|
|
@@ -951,22 +2940,22 @@ Create a recurring scrape, crawl, or search monitor that compares each check wit
|
|
|
951
2940
|
In the simple form, a \`goal\` is required. If \`queries\` contains one or more non-empty values and is supplied with \`page\`/\`pages\`, \`queries\` create the search target and page targets are ignored. A monitor schedules future network checks and can send configured email or webhook notifications. Returns the created monitor.
|
|
952
2941
|
`,
|
|
953
2942
|
outputSchema: monitorOutputSchema,
|
|
954
|
-
parameters:
|
|
955
|
-
body:
|
|
956
|
-
page:
|
|
957
|
-
pages:
|
|
958
|
-
queries:
|
|
959
|
-
searchWindow:
|
|
960
|
-
maxResults:
|
|
961
|
-
includeDomains:
|
|
962
|
-
excludeDomains:
|
|
963
|
-
goal:
|
|
964
|
-
name:
|
|
965
|
-
scheduleText:
|
|
966
|
-
timezone:
|
|
967
|
-
email:
|
|
968
|
-
includeDiffs:
|
|
969
|
-
webhookUrl:
|
|
2943
|
+
parameters: z7.object({
|
|
2944
|
+
body: z7.record(z7.string(), z7.any()).optional(),
|
|
2945
|
+
page: z7.string().optional(),
|
|
2946
|
+
pages: z7.array(z7.string()).optional(),
|
|
2947
|
+
queries: z7.array(z7.string()).optional(),
|
|
2948
|
+
searchWindow: z7.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
|
|
2949
|
+
maxResults: z7.number().int().min(1).max(50).optional(),
|
|
2950
|
+
includeDomains: z7.array(z7.string()).optional(),
|
|
2951
|
+
excludeDomains: z7.array(z7.string()).optional(),
|
|
2952
|
+
goal: z7.string().optional(),
|
|
2953
|
+
name: z7.string().optional(),
|
|
2954
|
+
scheduleText: z7.string().optional(),
|
|
2955
|
+
timezone: z7.string().optional(),
|
|
2956
|
+
email: z7.string().optional(),
|
|
2957
|
+
includeDiffs: z7.boolean().optional(),
|
|
2958
|
+
webhookUrl: z7.string().optional()
|
|
970
2959
|
}),
|
|
971
2960
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
972
2961
|
const body = buildMonitorCreateBody(args2);
|
|
@@ -998,9 +2987,9 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
|
|
|
998
2987
|
List monitors for the authenticated account with optional pagination controls. Returns one page of monitor records and pagination metadata.
|
|
999
2988
|
`,
|
|
1000
2989
|
outputSchema: monitorListOutputSchema,
|
|
1001
|
-
parameters:
|
|
1002
|
-
limit:
|
|
1003
|
-
offset:
|
|
2990
|
+
parameters: z7.object({
|
|
2991
|
+
limit: z7.number().int().positive().optional(),
|
|
2992
|
+
offset: z7.number().int().nonnegative().optional()
|
|
1004
2993
|
}),
|
|
1005
2994
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
1006
2995
|
const { limit, offset } = args2;
|
|
@@ -1030,7 +3019,7 @@ List monitors for the authenticated account with optional pagination controls. R
|
|
|
1030
3019
|
Retrieve one monitor by ID, including its configuration and current state. This does not run or modify the monitor.
|
|
1031
3020
|
`,
|
|
1032
3021
|
outputSchema: monitorOutputSchema,
|
|
1033
|
-
parameters:
|
|
3022
|
+
parameters: z7.object({ id: z7.string() }),
|
|
1034
3023
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
1035
3024
|
const { id } = args2;
|
|
1036
3025
|
const res = await monitorRequest(
|
|
@@ -1058,9 +3047,9 @@ Patch an existing monitor by ID. The body can change its name, active/paused sta
|
|
|
1058
3047
|
Returns the updated monitor.
|
|
1059
3048
|
`,
|
|
1060
3049
|
outputSchema: monitorOutputSchema,
|
|
1061
|
-
parameters:
|
|
1062
|
-
id:
|
|
1063
|
-
body:
|
|
3050
|
+
parameters: z7.object({
|
|
3051
|
+
id: z7.string(),
|
|
3052
|
+
body: z7.record(z7.string(), z7.any())
|
|
1064
3053
|
}),
|
|
1065
3054
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
1066
3055
|
const { id, body } = args2;
|
|
@@ -1088,7 +3077,7 @@ Returns the updated monitor.
|
|
|
1088
3077
|
Permanently delete a monitor by ID and stop its future schedule. This operation cannot be undone and returns deletion status.
|
|
1089
3078
|
`,
|
|
1090
3079
|
outputSchema: monitorDeleteOutputSchema,
|
|
1091
|
-
parameters:
|
|
3080
|
+
parameters: z7.object({ id: z7.string() }),
|
|
1092
3081
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
1093
3082
|
const { id } = args2;
|
|
1094
3083
|
log.info("Deleting monitor", { id });
|
|
@@ -1116,7 +3105,7 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
|
|
|
1116
3105
|
Queue an immediate check for a monitor outside its normal schedule. This starts network work for the monitor's configured targets and returns the queued check.
|
|
1117
3106
|
`,
|
|
1118
3107
|
outputSchema: monitorRunOutputSchema,
|
|
1119
|
-
parameters:
|
|
3108
|
+
parameters: z7.object({ id: z7.string() }),
|
|
1120
3109
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
1121
3110
|
const { id } = args2;
|
|
1122
3111
|
const res = await monitorRequest(
|
|
@@ -1143,10 +3132,10 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
|
|
|
1143
3132
|
List historical checks for a monitor, optionally filtered by status and bounded by a result limit. Returns one page of check summaries and pagination metadata.
|
|
1144
3133
|
`,
|
|
1145
3134
|
outputSchema: monitorChecksOutputSchema,
|
|
1146
|
-
parameters:
|
|
1147
|
-
id:
|
|
1148
|
-
limit:
|
|
1149
|
-
offset:
|
|
3135
|
+
parameters: z7.object({
|
|
3136
|
+
id: z7.string(),
|
|
3137
|
+
limit: z7.number().int().positive().optional(),
|
|
3138
|
+
offset: z7.number().int().nonnegative().optional(),
|
|
1150
3139
|
status: checkStatusSchema.optional()
|
|
1151
3140
|
}),
|
|
1152
3141
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
@@ -1177,11 +3166,11 @@ Retrieve one monitor check and its page-level results, optionally filtered by pa
|
|
|
1177
3166
|
Markdown tracking returns a unified text diff, JSON tracking returns field paths with previous/current values and a current snapshot, and mixed tracking returns both. Returns one page of results plus a \`next\` URL when more pages exist.
|
|
1178
3167
|
`,
|
|
1179
3168
|
outputSchema: monitorCheckOutputSchema,
|
|
1180
|
-
parameters:
|
|
1181
|
-
id:
|
|
1182
|
-
checkId:
|
|
1183
|
-
limit:
|
|
1184
|
-
skip:
|
|
3169
|
+
parameters: z7.object({
|
|
3170
|
+
id: z7.string(),
|
|
3171
|
+
checkId: z7.string(),
|
|
3172
|
+
limit: z7.number().int().positive().optional(),
|
|
3173
|
+
skip: z7.number().int().nonnegative().optional(),
|
|
1185
3174
|
pageStatus: pageStatusSchema.optional()
|
|
1186
3175
|
}),
|
|
1187
3176
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
@@ -1198,8 +3187,7 @@ Markdown tracking returns a unified text diff, JSON tracking returns field paths
|
|
|
1198
3187
|
}
|
|
1199
3188
|
|
|
1200
3189
|
// src/research.ts
|
|
1201
|
-
import { z as
|
|
1202
|
-
import { UserError } from "fastmcp";
|
|
3190
|
+
import { z as z8 } from "zod";
|
|
1203
3191
|
var BASE2 = "/v2/search/research";
|
|
1204
3192
|
function researchHeaders(origin) {
|
|
1205
3193
|
return { ...originHeaders(origin), ...AGENT_HINTS_HEADERS };
|
|
@@ -1318,21 +3306,21 @@ Several distinct framings of the same question surface different papers than a s
|
|
|
1318
3306
|
Returns ranked papers with canonical IDs, titles, authors, and abstracts.
|
|
1319
3307
|
`,
|
|
1320
3308
|
outputSchema: researchSearchOutputSchema,
|
|
1321
|
-
parameters:
|
|
1322
|
-
query:
|
|
3309
|
+
parameters: z8.object({
|
|
3310
|
+
query: z8.string().min(1).describe(
|
|
1323
3311
|
"Natural-language research topic or question, including methods, systems, conditions, populations, interventions, or outcomes when relevant."
|
|
1324
3312
|
),
|
|
1325
|
-
k:
|
|
1326
|
-
authors:
|
|
3313
|
+
k: z8.number().int().min(1).max(500).optional().describe("Number of ranked papers to return (default 40)."),
|
|
3314
|
+
authors: z8.array(z8.string()).optional().describe(
|
|
1327
3315
|
"Author substring filter(s); ALL must match (case-insensitive)."
|
|
1328
3316
|
),
|
|
1329
|
-
categories:
|
|
3317
|
+
categories: z8.array(z8.string()).optional().describe(
|
|
1330
3318
|
"Paper category filter(s) (e.g. `cs.LG`); ALL provided values must match."
|
|
1331
3319
|
),
|
|
1332
|
-
from:
|
|
3320
|
+
from: z8.string().optional().describe(
|
|
1333
3321
|
"Inclusive lower bound on created/updated date (`YYYY-MM-DD`)."
|
|
1334
3322
|
),
|
|
1335
|
-
to:
|
|
3323
|
+
to: z8.string().optional().describe(
|
|
1336
3324
|
"Inclusive upper bound on created/updated date (`YYYY-MM-DD`)."
|
|
1337
3325
|
)
|
|
1338
3326
|
}),
|
|
@@ -1369,8 +3357,8 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
|
|
|
1369
3357
|
Retrieve canonical metadata for one paper ID, such as an arXiv, PMC, PMID, or DOI identifier. Returns the title, abstract, authors, categories, source IDs, and dates as markdown.
|
|
1370
3358
|
`,
|
|
1371
3359
|
outputSchema: researchPaperOutputSchema,
|
|
1372
|
-
parameters:
|
|
1373
|
-
paperId:
|
|
3360
|
+
parameters: z8.object({
|
|
3361
|
+
paperId: z8.string().min(1).describe(
|
|
1374
3362
|
"Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
|
|
1375
3363
|
)
|
|
1376
3364
|
}),
|
|
@@ -1405,12 +3393,12 @@ Find citation-graph candidates from one to ten \`seed_ids\`; the first ID is the
|
|
|
1405
3393
|
Returns ranked candidates and the evaluated pool size.
|
|
1406
3394
|
`,
|
|
1407
3395
|
outputSchema: researchRelatedOutputSchema,
|
|
1408
|
-
parameters:
|
|
1409
|
-
seed_ids:
|
|
1410
|
-
intent:
|
|
1411
|
-
mode:
|
|
1412
|
-
k:
|
|
1413
|
-
rerank:
|
|
3396
|
+
parameters: z8.object({
|
|
3397
|
+
seed_ids: z8.array(z8.string()).min(1).max(10),
|
|
3398
|
+
intent: z8.string().min(1),
|
|
3399
|
+
mode: z8.enum(["similar", "citers", "references"]).optional(),
|
|
3400
|
+
k: z8.number().int().min(1).max(500).optional(),
|
|
3401
|
+
rerank: z8.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
|
|
1414
3402
|
}),
|
|
1415
3403
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
1416
3404
|
const { seed_ids, intent, mode, k, rerank } = args2;
|
|
@@ -1457,12 +3445,12 @@ Retrieve in-body passages from one paper that are relevant to a specific questio
|
|
|
1457
3445
|
Returns matching passages or a notice when full text is unavailable.
|
|
1458
3446
|
`,
|
|
1459
3447
|
outputSchema: researchReadOutputSchema,
|
|
1460
|
-
parameters:
|
|
1461
|
-
paperId:
|
|
3448
|
+
parameters: z8.object({
|
|
3449
|
+
paperId: z8.string().min(1).describe(
|
|
1462
3450
|
"Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
|
|
1463
3451
|
),
|
|
1464
|
-
question:
|
|
1465
|
-
k:
|
|
3452
|
+
question: z8.string().min(1),
|
|
3453
|
+
k: z8.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
|
|
1466
3454
|
}),
|
|
1467
3455
|
execute: async (args2, { session, client: mcpClient }) => {
|
|
1468
3456
|
const { paperId, question, k } = args2;
|
|
@@ -1493,9 +3481,9 @@ Returns matching passages or a notice when full text is unavailable.
|
|
|
1493
3481
|
Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub issues, pull requests, and READMEs, plus code documentation, returned as matched passages.
|
|
1494
3482
|
`,
|
|
1495
3483
|
outputSchema: deprecatedToolOutputSchema,
|
|
1496
|
-
parameters:
|
|
1497
|
-
query:
|
|
1498
|
-
k:
|
|
3484
|
+
parameters: z8.object({
|
|
3485
|
+
query: z8.string().min(1),
|
|
3486
|
+
k: z8.number().int().min(1).max(100).optional()
|
|
1499
3487
|
}),
|
|
1500
3488
|
canList: () => false,
|
|
1501
3489
|
beforeValidate: () => {
|
|
@@ -1514,7 +3502,7 @@ Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub
|
|
|
1514
3502
|
}
|
|
1515
3503
|
|
|
1516
3504
|
// src/usage.ts
|
|
1517
|
-
import { z as
|
|
3505
|
+
import { z as z9 } from "zod";
|
|
1518
3506
|
function assertSuccessful(status, response, operation) {
|
|
1519
3507
|
if (status === 200 && response.success) return;
|
|
1520
3508
|
throw new Error(response.error || `Failed to ${operation}`);
|
|
@@ -1536,11 +3524,11 @@ The current view returns \`remainingCredits\`, \`planCredits\`, \`billingPeriodS
|
|
|
1536
3524
|
The historical view returns periods sorted by start date with \`startDate\`, \`endDate\`, and \`creditsUsed\`. Set \`byApiKey\` to include separate periods per API key, identified by the optional \`apiKey\` field. The newest period's \`endDate\` can be null.
|
|
1537
3525
|
`,
|
|
1538
3526
|
outputSchema: creditUsageOutputSchema,
|
|
1539
|
-
parameters:
|
|
1540
|
-
view:
|
|
3527
|
+
parameters: z9.object({
|
|
3528
|
+
view: z9.enum(["current", "historical"]).optional().describe(
|
|
1541
3529
|
"Select current balance or historical monthly usage. Defaults to current."
|
|
1542
3530
|
),
|
|
1543
|
-
byApiKey:
|
|
3531
|
+
byApiKey: z9.boolean().optional().describe(
|
|
1544
3532
|
'Break historical usage down by API key. When view is omitted, true selects the historical view; it cannot be combined with view "current".'
|
|
1545
3533
|
)
|
|
1546
3534
|
}),
|
|
@@ -2163,30 +4151,32 @@ function removeEmptyTopLevel(obj) {
|
|
|
2163
4151
|
}
|
|
2164
4152
|
return out;
|
|
2165
4153
|
}
|
|
2166
|
-
var searchDomainSchema =
|
|
4154
|
+
var searchDomainSchema = z10.string().trim().toLowerCase().min(1).max(253).regex(
|
|
2167
4155
|
/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/,
|
|
2168
4156
|
"Domain must be a valid hostname without protocol or path"
|
|
2169
4157
|
);
|
|
2170
4158
|
var searchToolBaseFields = {
|
|
2171
|
-
toolDetail:
|
|
2172
|
-
query:
|
|
2173
|
-
|
|
4159
|
+
toolDetail: z10.enum(["compact", "summary", "full"]).optional().describe("Compact by default. Compact returns only provider, capability and description; full includes contracts. Inspect selected compact tools with firecrawl_find_tools providers and capabilities."),
|
|
4160
|
+
query: z10.string().min(1).describe("Query for web and semantic tool discovery. Operators include quoted phrases, `-term`, `site:host`, `inurl:term`, `intitle:term`, and `related:host`; the set is non-exhaustive. Catalogue browsing is available through firecrawl_find_tools."),
|
|
4161
|
+
objective: z10.string().trim().min(1).max(5e3).optional().describe("Optional broader goal for this search, if known. Avoid sensitive information."),
|
|
4162
|
+
clientModel: z10.string().trim().min(1).max(128).optional().describe("Optional model identifier, if known."),
|
|
4163
|
+
domainTools: z10.boolean().optional().describe(
|
|
2174
4164
|
"Include domain-matched tools for result URLs. Defaults to true when Alexandria is combined with web, news or images; semantic-only search leaves domain matching off."
|
|
2175
4165
|
),
|
|
2176
|
-
highlights:
|
|
4166
|
+
highlights: z10.boolean().optional().describe(
|
|
2177
4167
|
"Return query-relevant page excerpts for web and news results when available (default). Highlights appear in web `description` and news `snippet`; otherwise, original snippets are returned. Set to false to keep the original search snippets."
|
|
2178
4168
|
),
|
|
2179
|
-
limit:
|
|
2180
|
-
tbs:
|
|
2181
|
-
filter:
|
|
2182
|
-
location:
|
|
2183
|
-
includeDomains:
|
|
2184
|
-
excludeDomains:
|
|
2185
|
-
sources:
|
|
2186
|
-
categories:
|
|
4169
|
+
limit: z10.number().int().min(1).max(100).optional(),
|
|
4170
|
+
tbs: z10.string().optional(),
|
|
4171
|
+
filter: z10.string().optional(),
|
|
4172
|
+
location: z10.string().optional(),
|
|
4173
|
+
includeDomains: z10.array(searchDomainSchema).optional().describe("Hostnames to restrict results to. Mutually exclusive with excludeDomains."),
|
|
4174
|
+
excludeDomains: z10.array(searchDomainSchema).optional().describe("Hostnames to leave out of results. Mutually exclusive with includeDomains."),
|
|
4175
|
+
sources: z10.array(searchSourceSchema).optional().describe("Search sources; authenticated sessions default to web + alexandria, keyless sessions to web only. " + ALEXANDRIA_SOURCES_OPT_OUT + ' Use ["alexandria"] alone for provider discovery without web results.'),
|
|
4176
|
+
categories: z10.array(z10.enum(["research", "pdf", "developer"])).optional().describe(
|
|
2187
4177
|
'Limit results to specific source types. `research` restricts ordinary web results to research-affiliated websites and returns page snippets, which is separate from the `firecrawl_research_*` tools that search paper abstracts and full text across biomedical (PubMed, bioRxiv, medRxiv) and arXiv literature; `pdf` searches PDF results; `developer` searches an index built for coding agents over public repositories, GitHub issues, merged pull requests, repository READMEs, and code documentation. `developer` returns hits in `data.web` with `category: "developer"`; the other categories also filter `data.web`.'
|
|
2188
4178
|
),
|
|
2189
|
-
enterprise:
|
|
4179
|
+
enterprise: z10.array(z10.enum(["default", "anon", "zdr"])).optional()
|
|
2190
4180
|
};
|
|
2191
4181
|
function searchDomainsAreExclusive(args2) {
|
|
2192
4182
|
return !(args2.includeDomains?.length && args2.excludeDomains?.length);
|
|
@@ -2194,15 +4184,15 @@ function searchDomainsAreExclusive(args2) {
|
|
|
2194
4184
|
var SEARCH_DOMAINS_CONFLICT_MESSAGE = "includeDomains and excludeDomains cannot both be specified";
|
|
2195
4185
|
var EXCHANGE_KEY_REQUIRED_MESSAGE = "Alexandria requires an API key on a team with Alexandria access";
|
|
2196
4186
|
var EXCHANGE_MAX_CALLS = 10;
|
|
2197
|
-
var exchangeCallSchema =
|
|
2198
|
-
provider:
|
|
2199
|
-
capability:
|
|
4187
|
+
var exchangeCallSchema = z10.object({
|
|
4188
|
+
provider: z10.string().min(1).describe('Provider slug, e.g. "fred".'),
|
|
4189
|
+
capability: z10.string().min(1).describe(
|
|
2200
4190
|
'Capability address as returned by search or discover, e.g. "series/observations".'
|
|
2201
4191
|
),
|
|
2202
|
-
version:
|
|
2203
|
-
options:
|
|
4192
|
+
version: z10.string().trim().min(1).max(128).optional().describe("Optional published workflow version. Omit to use the latest version."),
|
|
4193
|
+
options: z10.record(z10.string(), z10.any()).optional().describe("Capability options as declared by its contract.")
|
|
2204
4194
|
});
|
|
2205
|
-
var exchangeCallsSchema =
|
|
4195
|
+
var exchangeCallsSchema = z10.array(exchangeCallSchema).min(1).max(EXCHANGE_MAX_CALLS);
|
|
2206
4196
|
function assertExchangeCredential(session) {
|
|
2207
4197
|
if (hasCredential(session)) return;
|
|
2208
4198
|
const payload = {
|
|
@@ -2210,9 +4200,18 @@ function assertExchangeCredential(session) {
|
|
|
2210
4200
|
message: EXCHANGE_KEY_REQUIRED_MESSAGE,
|
|
2211
4201
|
docs_url: MCP_CONNECTION_GUIDE_URL
|
|
2212
4202
|
};
|
|
2213
|
-
throw new
|
|
4203
|
+
throw new UserError(payload.message, payload);
|
|
2214
4204
|
}
|
|
2215
4205
|
var TERMS_REQUIRED_CODE = "THIRD_PARTY_DATA_TERMS_REQUIRED";
|
|
4206
|
+
var DATA_SOURCES_SETTINGS_URL = "https://www.firecrawl.dev/app/settings?tab=data-sources";
|
|
4207
|
+
function isTermsWrite(call) {
|
|
4208
|
+
const capability = call.capability.trim().toLowerCase();
|
|
4209
|
+
return call.provider.trim().toLowerCase() === "firecrawl" && capability.startsWith("terms/") && capability !== "terms/show";
|
|
4210
|
+
}
|
|
4211
|
+
function termsWriteError() {
|
|
4212
|
+
const message = `Provider terms are accepted in the Firecrawl dashboard, not through this connection. Read them with terms/show, then ask an organization admin to accept them at ${DATA_SOURCES_SETTINGS_URL}.`;
|
|
4213
|
+
return new UserError(message, { code: "invalid_option", status: 400, message });
|
|
4214
|
+
}
|
|
2216
4215
|
function termsRequiredAction(body) {
|
|
2217
4216
|
const data = body;
|
|
2218
4217
|
if (data?.code !== TERMS_REQUIRED_CODE) return void 0;
|
|
@@ -2230,11 +4229,11 @@ function termsRequiredError(action, context, hints) {
|
|
|
2230
4229
|
const requestId = context.requestId;
|
|
2231
4230
|
const retryIdentity = requestId ? ` and requestId ${requestId}` : "";
|
|
2232
4231
|
const message = `Alexandria provider terms required. An organization admin must accept the ${action.terms} provider's terms (version ${action.version}) before this request can run.`;
|
|
2233
|
-
return new
|
|
4232
|
+
return new UserError(
|
|
2234
4233
|
`${message}
|
|
2235
4234
|
|
|
2236
|
-
1. Read the agreement with firecrawl_scrape using alexandria: {provider: "firecrawl", capability: "terms/show", options: {provider: "${action.terms}"}} and present it to the user.
|
|
2237
|
-
2. After
|
|
4235
|
+
1. Read the agreement with firecrawl_scrape using alexandria: {provider: "firecrawl", capability: "terms/show", options: {provider: "${action.terms}"}} and present it to the user. Send terms/show separately from provider execution. Terms are accepted in the Firecrawl dashboard, not through this connection: ask an organization admin to accept them at ${action.url}, or ${DATA_SOURCES_SETTINGS_URL} if that page is unavailable. Never infer acceptance from a data request.
|
|
4236
|
+
2. After the admin confirms, call ${context.tool} again with the identical payload${retryIdentity}. If the same terms error persists, stop and ask an organization admin to check access at ${DATA_SOURCES_SETTINGS_URL}.
|
|
2238
4237
|
|
|
2239
4238
|
Do not retry until acceptance is confirmed.`,
|
|
2240
4239
|
{
|
|
@@ -2295,8 +4294,8 @@ async function relayExchangeError(run, context) {
|
|
|
2295
4294
|
const message = typeof data?.error === "string" ? data.error : `Alexandria request failed (HTTP ${response.status})`;
|
|
2296
4295
|
const disabledProvider = response.status === 403 ? context.providers?.find((provider) => message === `Access to ${provider} is disabled for this organization.`) : void 0;
|
|
2297
4296
|
if (disabledProvider) {
|
|
2298
|
-
throw new
|
|
2299
|
-
`${message} Read the provider terms and status using nextTool and present them to the user. Never infer acceptance from a data request.
|
|
4297
|
+
throw new UserError(
|
|
4298
|
+
`${message} Read the provider terms and status using nextTool and present them to the user. Never infer acceptance from a data request. Terms are accepted in the Firecrawl dashboard, not through this connection; acceptance may not restore disabled access, and an organization admin can review access at ${DATA_SOURCES_SETTINGS_URL}. Retry the original request only after access is restored.`,
|
|
2300
4299
|
{
|
|
2301
4300
|
code: typeof data?.code === "string" ? data.code : "exchange_error",
|
|
2302
4301
|
status: 403,
|
|
@@ -2311,7 +4310,7 @@ async function relayExchangeError(run, context) {
|
|
|
2311
4310
|
}
|
|
2312
4311
|
);
|
|
2313
4312
|
}
|
|
2314
|
-
throw new
|
|
4313
|
+
throw new UserError(message, {
|
|
2315
4314
|
code: typeof data?.code === "string" ? data.code : "exchange_error",
|
|
2316
4315
|
status: response.status,
|
|
2317
4316
|
message,
|
|
@@ -2548,12 +4547,12 @@ async function runWithCredentialRecovery(run, requestId, session) {
|
|
|
2548
4547
|
const hints = readErrorAgentHints(error2);
|
|
2549
4548
|
if (hints) {
|
|
2550
4549
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2551
|
-
throw new
|
|
4550
|
+
throw new UserError(
|
|
2552
4551
|
hints.length ? `${message}
|
|
2553
4552
|
|
|
2554
4553
|
${agentHintsText(hints)}` : message,
|
|
2555
4554
|
{
|
|
2556
|
-
...error2 instanceof
|
|
4555
|
+
...error2 instanceof UserError ? error2.extras : {},
|
|
2557
4556
|
error: message,
|
|
2558
4557
|
agent_hints: hints
|
|
2559
4558
|
}
|
|
@@ -2562,7 +4561,7 @@ ${agentHintsText(hints)}` : message,
|
|
|
2562
4561
|
throw error2;
|
|
2563
4562
|
}
|
|
2564
4563
|
const payload = recoveryPayload("CREDENTIAL_INVALID", requestId);
|
|
2565
|
-
throw new
|
|
4564
|
+
throw new UserError(String(payload.message), payload);
|
|
2566
4565
|
}
|
|
2567
4566
|
}
|
|
2568
4567
|
function recoveryPayload(code, requestId = randomUUID(), options = {}) {
|
|
@@ -2691,7 +4690,7 @@ function guardHostedTool(tool, { logActions }) {
|
|
|
2691
4690
|
signupUrl: code === "KEYLESS_TOOL_NOT_AVAILABLE" ? await hostedKeylessSignupUrl(session) : void 0
|
|
2692
4691
|
});
|
|
2693
4692
|
if (logActions) {
|
|
2694
|
-
emitActionLog(tool.name, "error", session, new
|
|
4693
|
+
emitActionLog(tool.name, "error", session, new UserError(String(payload2.message), payload2), requestId, code);
|
|
2695
4694
|
}
|
|
2696
4695
|
return {
|
|
2697
4696
|
content: [{ type: "text", text: String(payload2.message) }],
|
|
@@ -2707,7 +4706,7 @@ function guardHostedTool(tool, { logActions }) {
|
|
|
2707
4706
|
tool.name,
|
|
2708
4707
|
"error",
|
|
2709
4708
|
session,
|
|
2710
|
-
new
|
|
4709
|
+
new UserError(`Tool validation failed: ${recoveryCode}`, payload),
|
|
2711
4710
|
randomUUID(),
|
|
2712
4711
|
recoveryCode
|
|
2713
4712
|
);
|
|
@@ -2728,16 +4727,16 @@ function guardHostedTool(tool, { logActions }) {
|
|
|
2728
4727
|
if (invocationSession.credentialError) {
|
|
2729
4728
|
const code = "CREDENTIAL_INVALID";
|
|
2730
4729
|
const payload = recoveryPayload(code, requestId);
|
|
2731
|
-
if (logActions) emitActionLog(tool.name, "error", invocationSession, new
|
|
2732
|
-
throw new
|
|
4730
|
+
if (logActions) emitActionLog(tool.name, "error", invocationSession, new UserError(String(payload.message), payload), requestId, code);
|
|
4731
|
+
throw new UserError(String(payload.message), payload);
|
|
2733
4732
|
}
|
|
2734
4733
|
if (isHostedKeylessSession(invocationSession) && !keylessTool) {
|
|
2735
4734
|
const code = "KEYLESS_TOOL_NOT_AVAILABLE";
|
|
2736
4735
|
const payload = recoveryPayload(code, requestId, {
|
|
2737
4736
|
signupUrl: await hostedKeylessSignupUrl(invocationSession)
|
|
2738
4737
|
});
|
|
2739
|
-
if (logActions) emitActionLog(tool.name, "error", invocationSession, new
|
|
2740
|
-
throw new
|
|
4738
|
+
if (logActions) emitActionLog(tool.name, "error", invocationSession, new UserError(String(payload.message), payload), requestId, code);
|
|
4739
|
+
throw new UserError(String(payload.message), payload);
|
|
2741
4740
|
}
|
|
2742
4741
|
const runTool = async () => {
|
|
2743
4742
|
try {
|
|
@@ -2969,10 +4968,10 @@ function transformScrapeParams(args2) {
|
|
|
2969
4968
|
delete out.pdfOptions;
|
|
2970
4969
|
return out;
|
|
2971
4970
|
}
|
|
2972
|
-
var scrapeParamsSchema =
|
|
2973
|
-
url:
|
|
2974
|
-
formats:
|
|
2975
|
-
|
|
4971
|
+
var scrapeParamsSchema = z10.object({
|
|
4972
|
+
url: z10.string().url(),
|
|
4973
|
+
formats: z10.array(
|
|
4974
|
+
z10.enum([
|
|
2976
4975
|
"markdown",
|
|
2977
4976
|
"html",
|
|
2978
4977
|
"rawHtml",
|
|
@@ -2986,71 +4985,71 @@ var scrapeParamsSchema = z8.object({
|
|
|
2986
4985
|
"audio"
|
|
2987
4986
|
])
|
|
2988
4987
|
).optional(),
|
|
2989
|
-
jsonOptions:
|
|
2990
|
-
prompt:
|
|
2991
|
-
schema:
|
|
4988
|
+
jsonOptions: z10.object({
|
|
4989
|
+
prompt: z10.string().optional(),
|
|
4990
|
+
schema: z10.record(z10.string(), z10.any()).optional()
|
|
2992
4991
|
}).optional(),
|
|
2993
|
-
queryOptions:
|
|
2994
|
-
prompt:
|
|
2995
|
-
mode:
|
|
4992
|
+
queryOptions: z10.object({
|
|
4993
|
+
prompt: z10.string().max(1e4),
|
|
4994
|
+
mode: z10.enum(["directQuote", "freeform"]).default("freeform")
|
|
2996
4995
|
}).optional(),
|
|
2997
|
-
screenshotOptions:
|
|
2998
|
-
fullPage:
|
|
2999
|
-
quality:
|
|
3000
|
-
viewport:
|
|
4996
|
+
screenshotOptions: z10.object({
|
|
4997
|
+
fullPage: z10.boolean().optional(),
|
|
4998
|
+
quality: z10.number().optional(),
|
|
4999
|
+
viewport: z10.object({ width: z10.number(), height: z10.number() }).optional()
|
|
3001
5000
|
}).optional(),
|
|
3002
|
-
parsers:
|
|
3003
|
-
pdfOptions:
|
|
3004
|
-
maxPages:
|
|
5001
|
+
parsers: z10.array(z10.enum(["pdf"])).optional(),
|
|
5002
|
+
pdfOptions: z10.object({
|
|
5003
|
+
maxPages: z10.number().int().min(1).max(1e4).optional()
|
|
3005
5004
|
}).optional(),
|
|
3006
|
-
onlyMainContent:
|
|
3007
|
-
redactPII:
|
|
3008
|
-
includeTags:
|
|
3009
|
-
excludeTags:
|
|
3010
|
-
waitFor:
|
|
5005
|
+
onlyMainContent: z10.boolean().optional(),
|
|
5006
|
+
redactPII: z10.boolean().optional(),
|
|
5007
|
+
includeTags: z10.array(z10.string()).optional(),
|
|
5008
|
+
excludeTags: z10.array(z10.string()).optional(),
|
|
5009
|
+
waitFor: z10.number().optional(),
|
|
3011
5010
|
...SAFE_MODE ? {} : {
|
|
3012
|
-
actions:
|
|
3013
|
-
|
|
3014
|
-
type:
|
|
3015
|
-
selector:
|
|
3016
|
-
milliseconds:
|
|
3017
|
-
text:
|
|
3018
|
-
key:
|
|
3019
|
-
direction:
|
|
3020
|
-
script:
|
|
3021
|
-
fullPage:
|
|
5011
|
+
actions: z10.array(
|
|
5012
|
+
z10.object({
|
|
5013
|
+
type: z10.enum(allowedActionTypes),
|
|
5014
|
+
selector: z10.string().optional(),
|
|
5015
|
+
milliseconds: z10.number().optional(),
|
|
5016
|
+
text: z10.string().optional(),
|
|
5017
|
+
key: z10.string().optional(),
|
|
5018
|
+
direction: z10.enum(["up", "down"]).optional(),
|
|
5019
|
+
script: z10.string().optional(),
|
|
5020
|
+
fullPage: z10.boolean().optional()
|
|
3022
5021
|
})
|
|
3023
5022
|
).optional()
|
|
3024
5023
|
},
|
|
3025
|
-
mobile:
|
|
3026
|
-
skipTlsVerification:
|
|
3027
|
-
removeBase64Images:
|
|
3028
|
-
location:
|
|
3029
|
-
country:
|
|
3030
|
-
languages:
|
|
5024
|
+
mobile: z10.boolean().optional(),
|
|
5025
|
+
skipTlsVerification: z10.boolean().optional(),
|
|
5026
|
+
removeBase64Images: z10.boolean().optional(),
|
|
5027
|
+
location: z10.object({
|
|
5028
|
+
country: z10.string().optional(),
|
|
5029
|
+
languages: z10.array(z10.string()).optional()
|
|
3031
5030
|
}).optional(),
|
|
3032
|
-
storeInCache:
|
|
3033
|
-
zeroDataRetention:
|
|
3034
|
-
maxAge:
|
|
3035
|
-
lockdown:
|
|
3036
|
-
proxy:
|
|
3037
|
-
profile:
|
|
3038
|
-
name:
|
|
3039
|
-
saveChanges:
|
|
5031
|
+
storeInCache: z10.boolean().optional(),
|
|
5032
|
+
zeroDataRetention: z10.boolean().optional(),
|
|
5033
|
+
maxAge: z10.number().optional(),
|
|
5034
|
+
lockdown: z10.boolean().optional(),
|
|
5035
|
+
proxy: z10.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
|
|
5036
|
+
profile: z10.object({
|
|
5037
|
+
name: z10.string(),
|
|
5038
|
+
saveChanges: z10.boolean().optional()
|
|
3040
5039
|
}).optional()
|
|
3041
5040
|
});
|
|
3042
5041
|
var ALEXANDRIA_IGNORED_SCRAPE_OPTIONS = /* @__PURE__ */ new Set(["toolDetail", "domainTools"]);
|
|
3043
5042
|
var scrapeToolParamsSchema = scrapeParamsSchema.extend({
|
|
3044
|
-
url:
|
|
3045
|
-
timeout:
|
|
3046
|
-
requestId:
|
|
5043
|
+
url: z10.string().url().optional(),
|
|
5044
|
+
timeout: z10.number().int().positive().optional().describe("Execution timeout in milliseconds."),
|
|
5045
|
+
requestId: z10.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe(
|
|
3047
5046
|
"Idempotency key bound to one Alexandria execution payload. Generated when omitted and returned with the result."
|
|
3048
5047
|
),
|
|
3049
|
-
alexandria:
|
|
5048
|
+
alexandria: z10.union([exchangeCallSchema, exchangeCallsSchema]).optional().describe(
|
|
3050
5049
|
"Catalogued Alexandria capability invocation, mutually exclusive with url. One {provider, capability, options} object or an array of 1-10, with contracts available through firecrawl_search or firecrawl_find_tools. Each call may include version to pin a published workflow; omitting it uses latest. Only requestId and timeout are supported alongside alexandria. " + ALEXANDRIA_CONTRACT_GUIDANCE + " Returns per-capability results in data.alexandria with data, records, or an error with a code; individual capabilities can fail even when the outer response succeeds. Requires an API key on a team with Alexandria enabled. Some providers require accepted terms; blocked requests return the applicable requirements."
|
|
3051
5050
|
),
|
|
3052
|
-
toolDetail:
|
|
3053
|
-
domainTools:
|
|
5051
|
+
toolDetail: z10.enum(["compact", "summary", "full"]).optional().describe("URL mode only: domain discovery detail, summary by default; compact returns provider/capability/description, full includes contracts. Ignored with alexandria."),
|
|
5052
|
+
domainTools: z10.boolean().optional().describe(
|
|
3054
5053
|
"URL mode only: include domain-matched Alexandria tools for the page in tools on the returned document. Ignored with alexandria."
|
|
3055
5054
|
)
|
|
3056
5055
|
}).refine(
|
|
@@ -3070,9 +5069,9 @@ var scrapeToolParamsSchema = scrapeParamsSchema.extend({
|
|
|
3070
5069
|
(args2) => !args2.requestId || !!args2.alexandria,
|
|
3071
5070
|
"requestId applies to Alexandria execution only"
|
|
3072
5071
|
);
|
|
3073
|
-
var parseOptionParamsSchema =
|
|
3074
|
-
formats:
|
|
3075
|
-
|
|
5072
|
+
var parseOptionParamsSchema = z10.object({
|
|
5073
|
+
formats: z10.array(
|
|
5074
|
+
z10.enum([
|
|
3076
5075
|
"markdown",
|
|
3077
5076
|
"html",
|
|
3078
5077
|
"rawHtml",
|
|
@@ -3082,48 +5081,48 @@ var parseOptionParamsSchema = z8.object({
|
|
|
3082
5081
|
"query"
|
|
3083
5082
|
])
|
|
3084
5083
|
).optional(),
|
|
3085
|
-
jsonOptions:
|
|
3086
|
-
prompt:
|
|
3087
|
-
schema:
|
|
5084
|
+
jsonOptions: z10.object({
|
|
5085
|
+
prompt: z10.string().optional(),
|
|
5086
|
+
schema: z10.record(z10.string(), z10.any()).optional()
|
|
3088
5087
|
}).optional(),
|
|
3089
|
-
queryOptions:
|
|
3090
|
-
prompt:
|
|
3091
|
-
mode:
|
|
5088
|
+
queryOptions: z10.object({
|
|
5089
|
+
prompt: z10.string().max(1e4),
|
|
5090
|
+
mode: z10.enum(["directQuote", "freeform"]).default("freeform")
|
|
3092
5091
|
}).optional(),
|
|
3093
|
-
parsers:
|
|
3094
|
-
pdfOptions:
|
|
3095
|
-
maxPages:
|
|
5092
|
+
parsers: z10.array(z10.enum(["pdf"])).optional(),
|
|
5093
|
+
pdfOptions: z10.object({
|
|
5094
|
+
maxPages: z10.number().int().min(1).max(1e4).optional()
|
|
3096
5095
|
}).optional(),
|
|
3097
|
-
onlyMainContent:
|
|
3098
|
-
redactPII:
|
|
3099
|
-
includeTags:
|
|
3100
|
-
excludeTags:
|
|
3101
|
-
removeBase64Images:
|
|
3102
|
-
skipTlsVerification:
|
|
3103
|
-
storeInCache:
|
|
3104
|
-
zeroDataRetention:
|
|
3105
|
-
maxAge:
|
|
3106
|
-
proxy:
|
|
5096
|
+
onlyMainContent: z10.boolean().optional(),
|
|
5097
|
+
redactPII: z10.boolean().optional(),
|
|
5098
|
+
includeTags: z10.array(z10.string()).optional(),
|
|
5099
|
+
excludeTags: z10.array(z10.string()).optional(),
|
|
5100
|
+
removeBase64Images: z10.boolean().optional(),
|
|
5101
|
+
skipTlsVerification: z10.boolean().optional(),
|
|
5102
|
+
storeInCache: z10.boolean().optional(),
|
|
5103
|
+
zeroDataRetention: z10.boolean().optional(),
|
|
5104
|
+
maxAge: z10.number().optional().describe("Ignored: parse never reuses or stores indexed content."),
|
|
5105
|
+
proxy: z10.enum(["basic", "auto"]).optional()
|
|
3107
5106
|
});
|
|
3108
5107
|
var localParseParamsSchema = parseOptionParamsSchema.extend({
|
|
3109
|
-
filePath:
|
|
5108
|
+
filePath: z10.string().min(1).describe(
|
|
3110
5109
|
"Absolute or relative path to a local file to parse. Supported: .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls"
|
|
3111
5110
|
),
|
|
3112
|
-
contentType:
|
|
5111
|
+
contentType: z10.string().optional().describe(
|
|
3113
5112
|
"Optional MIME type override. If omitted, the server infers the file kind from the extension."
|
|
3114
5113
|
)
|
|
3115
5114
|
});
|
|
3116
5115
|
var hostedParseParamsSchema = parseOptionParamsSchema.extend({
|
|
3117
|
-
filePath:
|
|
5116
|
+
filePath: z10.string().min(1).optional().describe(
|
|
3118
5117
|
"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."
|
|
3119
5118
|
),
|
|
3120
|
-
uploadRef:
|
|
5119
|
+
uploadRef: z10.string().min(1).optional().describe(
|
|
3121
5120
|
"Phase 2 only: short-lived upload reference returned by phase 1 after the local PUT upload completes."
|
|
3122
5121
|
),
|
|
3123
|
-
contentType:
|
|
5122
|
+
contentType: z10.string().optional().describe(
|
|
3124
5123
|
"Phase 1 MIME type override. If omitted, the server infers it from the file extension without reading the file."
|
|
3125
5124
|
),
|
|
3126
|
-
declaredSizeBytes:
|
|
5125
|
+
declaredSizeBytes: z10.number().int().positive().optional().describe(
|
|
3127
5126
|
"Optional phase 1 size declaration. Hosted MCP does not stat the file; provide this only if the caller already knows it."
|
|
3128
5127
|
)
|
|
3129
5128
|
}).superRefine((value, ctx) => {
|
|
@@ -3131,7 +5130,7 @@ var hostedParseParamsSchema = parseOptionParamsSchema.extend({
|
|
|
3131
5130
|
const hasUploadRef = typeof value.uploadRef === "string" && value.uploadRef.length > 0;
|
|
3132
5131
|
if (hasFilePath === hasUploadRef) {
|
|
3133
5132
|
ctx.addIssue({
|
|
3134
|
-
code:
|
|
5133
|
+
code: z10.ZodIssueCode.custom,
|
|
3135
5134
|
message: "Hosted firecrawl_parse requires exactly one of filePath (phase 1) or uploadRef (phase 2).",
|
|
3136
5135
|
path: hasFilePath && hasUploadRef ? ["uploadRef"] : ["filePath"]
|
|
3137
5136
|
});
|
|
@@ -3266,7 +5265,7 @@ async function executeHostedParse(args2, session, log, origin) {
|
|
|
3266
5265
|
option: "zeroDataRetention",
|
|
3267
5266
|
message: "Zero Data Retention is not available in anonymous keyless mode. Omit zeroDataRetention to parse with keyless access, or configure an API key for a team where Zero Data Retention is enabled, then retry."
|
|
3268
5267
|
};
|
|
3269
|
-
throw new
|
|
5268
|
+
throw new UserError(String(payload.message), payload);
|
|
3270
5269
|
}
|
|
3271
5270
|
const options = extractParseOptions(args2);
|
|
3272
5271
|
if (hasFilePath && args2.filePath) {
|
|
@@ -3336,8 +5335,9 @@ var scrapeTool = {
|
|
|
3336
5335
|
name: "firecrawl_scrape",
|
|
3337
5336
|
annotations: {
|
|
3338
5337
|
title: "Firecrawl scrape",
|
|
3339
|
-
|
|
3340
|
-
//
|
|
5338
|
+
// Hosted scrape omits browser actions and refuses provider terms writes
|
|
5339
|
+
// before execution.
|
|
5340
|
+
readOnlyHint: SAFE_MODE,
|
|
3341
5341
|
openWorldHint: true,
|
|
3342
5342
|
// Accepts any user-supplied URL on the public web.
|
|
3343
5343
|
destructiveHint: false
|
|
@@ -3364,6 +5364,9 @@ Alexandria mode, on an authenticated session with Alexandria access: \`alexandri
|
|
|
3364
5364
|
} = args2;
|
|
3365
5365
|
if (alexandria) {
|
|
3366
5366
|
assertExchangeCredential(session);
|
|
5367
|
+
if ((Array.isArray(alexandria) ? alexandria : [alexandria]).some(isTermsWrite)) {
|
|
5368
|
+
throw termsWriteError();
|
|
5369
|
+
}
|
|
3367
5370
|
log.info("Executing Alexandria capabilities", {
|
|
3368
5371
|
count: Array.isArray(alexandria) ? alexandria.length : 1
|
|
3369
5372
|
});
|
|
@@ -3427,13 +5430,13 @@ Enumerate URLs indexed under one website through Firecrawl without fetching each
|
|
|
3427
5430
|
Returns matching URLs rather than page bodies. Retrieve one page with \`firecrawl_scrape\`; collect content across multiple pages with \`firecrawl_crawl\`. Authenticated responses can include an \`id\` for optional map feedback.
|
|
3428
5431
|
`,
|
|
3429
5432
|
outputSchema: mapOutputSchema,
|
|
3430
|
-
parameters:
|
|
3431
|
-
url:
|
|
3432
|
-
search:
|
|
3433
|
-
sitemap:
|
|
3434
|
-
includeSubdomains:
|
|
3435
|
-
limit:
|
|
3436
|
-
ignoreQueryParameters:
|
|
5433
|
+
parameters: z10.object({
|
|
5434
|
+
url: z10.string().url(),
|
|
5435
|
+
search: z10.string().optional(),
|
|
5436
|
+
sitemap: z10.enum(["include", "skip", "only"]).optional(),
|
|
5437
|
+
includeSubdomains: z10.boolean().optional(),
|
|
5438
|
+
limit: z10.number().optional(),
|
|
5439
|
+
ignoreQueryParameters: z10.boolean().optional()
|
|
3437
5440
|
}),
|
|
3438
5441
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
3439
5442
|
const { url, ...options } = args2;
|
|
@@ -3472,7 +5475,7 @@ On an authenticated session, tool matches describe available capabilities; \`fir
|
|
|
3472
5475
|
For a programming question, add \`categories: ["developer"]\`; its hits return in \`data.web\` with \`category: "developer"\`. \`categories: ["research"]\` restricts web results to research-affiliated websites; the \`firecrawl_research_*\` tools are a separate surface over paper abstracts and full text (PubMed, bioRxiv, medRxiv, arXiv). Query operators, domain filters, \`categories\`, \`toolDetail\` and \`scrapeOptions\` are described on their parameters. Returns source-type result groups and usage metadata. Authenticated responses can include an \`id\` for optional search feedback.
|
|
3473
5476
|
`,
|
|
3474
5477
|
outputSchema: searchOutputSchema,
|
|
3475
|
-
parameters:
|
|
5478
|
+
parameters: z10.object({
|
|
3476
5479
|
...searchToolBaseFields,
|
|
3477
5480
|
scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional().describe("Attach page content for web results in the same call. These fetches ignore maxAge, so use firecrawl_scrape when you need a live fetch. scrapeOptions fetches web pages, never Alexandria provider tools.")
|
|
3478
5481
|
}).refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
|
|
@@ -3559,9 +5562,9 @@ async function executeExchangeCalls(session, alexandria, suppliedRequestId, time
|
|
|
3559
5562
|
);
|
|
3560
5563
|
} catch (error2) {
|
|
3561
5564
|
if (error2?.response?.status === 401) throw error2;
|
|
3562
|
-
throw new
|
|
5565
|
+
throw new UserError(
|
|
3563
5566
|
`${error2 instanceof Error ? error2.message : String(error2)} Request ID: ${requestId}. Reuse this ID only with the identical payload.`,
|
|
3564
|
-
{ ...error2 instanceof
|
|
5567
|
+
{ ...error2 instanceof UserError ? error2.extras : {}, requestId }
|
|
3565
5568
|
);
|
|
3566
5569
|
}
|
|
3567
5570
|
}
|
|
@@ -3583,7 +5586,7 @@ var findToolsTool = {
|
|
|
3583
5586
|
try {
|
|
3584
5587
|
options = findToolsOptions(args2);
|
|
3585
5588
|
} catch (error2) {
|
|
3586
|
-
throw new
|
|
5589
|
+
throw new UserError(error2 instanceof Error ? error2.message : String(error2));
|
|
3587
5590
|
}
|
|
3588
5591
|
if (options.level === "categories") {
|
|
3589
5592
|
assertExchangeCredential(session);
|
|
@@ -3593,7 +5596,7 @@ var findToolsTool = {
|
|
|
3593
5596
|
);
|
|
3594
5597
|
const rows = response?.data?.cohorts;
|
|
3595
5598
|
if (!Array.isArray(rows) || rows.some((row) => typeof row?.cohort !== "string" || typeof row?.about !== "string")) {
|
|
3596
|
-
throw new
|
|
5599
|
+
throw new UserError("Category discovery returned an invalid category index.");
|
|
3597
5600
|
}
|
|
3598
5601
|
const offset = options.offset ?? 0;
|
|
3599
5602
|
const items = rows.slice(offset, offset + options.limit).map((row) => ({
|
|
@@ -3611,7 +5614,7 @@ var findToolsTool = {
|
|
|
3611
5614
|
};
|
|
3612
5615
|
server.addTool(findToolsTool);
|
|
3613
5616
|
var SEARCH_SURFACE_SCRAPE_DESCRIPTION = `
|
|
3614
|
-
Scrape one URL and return its content, or execute catalogued Alexandria capabilities. URL mode returns markdown by default, or HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema, plus page metadata. Firecrawl may serve recently indexed content; set \`maxAge: 0\` for a live fetch. A successful response does not by itself confirm the page is still current.
|
|
5617
|
+
Scrape one URL and return its content, or execute catalogued Alexandria capabilities. URL mode returns markdown by default, or HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema, plus page metadata. Firecrawl may serve recently indexed content; set \`maxAge: 0\` for a live fetch. A successful response does not by itself confirm the page is still current. A named browser profile loads saved session data.
|
|
3615
5618
|
|
|
3616
5619
|
\`firecrawl_search\` with \`sources\` unset and \`firecrawl_find_tools\` can discover providers for the same fields across several pages; a matching Alexandria provider returns typed records in one call.
|
|
3617
5620
|
|
|
@@ -3695,7 +5698,7 @@ async function keylessPost(path2, body, session, originOverride) {
|
|
|
3695
5698
|
retryAfterSeconds: eligibility.retryAfterSeconds,
|
|
3696
5699
|
signupUrl: eligibility.signupUrl
|
|
3697
5700
|
});
|
|
3698
|
-
throw new
|
|
5701
|
+
throw new UserError(String(payload.message), payload);
|
|
3699
5702
|
}
|
|
3700
5703
|
}
|
|
3701
5704
|
const headers = {
|
|
@@ -3722,7 +5725,7 @@ async function keylessPost(path2, body, session, originOverride) {
|
|
|
3722
5725
|
});
|
|
3723
5726
|
const hints = readAgentHints(json);
|
|
3724
5727
|
if (hints) payload.agent_hints = hints;
|
|
3725
|
-
throw new
|
|
5728
|
+
throw new UserError(String(payload.message), payload);
|
|
3726
5729
|
}
|
|
3727
5730
|
throw new CoreHttpError(
|
|
3728
5731
|
json?.error || `Firecrawl request failed (HTTP ${response.status})`,
|
|
@@ -3792,17 +5795,17 @@ async function waitForCrawlCompletionWithOrigin(client, jobId, origin, pollInter
|
|
|
3792
5795
|
);
|
|
3793
5796
|
}
|
|
3794
5797
|
}
|
|
3795
|
-
var feedbackIssueSchema =
|
|
5798
|
+
var feedbackIssueSchema = z10.string().trim().min(1).max(80).regex(
|
|
3796
5799
|
/^[a-z0-9][a-z0-9_-]*$/,
|
|
3797
5800
|
"Issue codes must use lowercase letters, numbers, underscores, or hyphens"
|
|
3798
5801
|
);
|
|
3799
|
-
var valuableSourceSchema =
|
|
3800
|
-
url:
|
|
3801
|
-
reason:
|
|
5802
|
+
var valuableSourceSchema = z10.object({
|
|
5803
|
+
url: z10.string().url(),
|
|
5804
|
+
reason: z10.string().max(1e3).optional()
|
|
3802
5805
|
});
|
|
3803
|
-
var missingContentSchema =
|
|
3804
|
-
topic:
|
|
3805
|
-
description:
|
|
5806
|
+
var missingContentSchema = z10.object({
|
|
5807
|
+
topic: z10.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
|
|
5808
|
+
description: z10.string().max(2e3).optional()
|
|
3806
5809
|
});
|
|
3807
5810
|
var FEEDBACK_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
3808
5811
|
function feedbackEnvEnabled(...keys) {
|
|
@@ -3841,24 +5844,24 @@ Records schema-validated quality feedback for a prior \`firecrawl_search\` UUID
|
|
|
3841
5844
|
Eligibility is limited to successful searches within the feedback age window. The record is idempotent per search ID. Eligible first feedback for a search can refund 1 credit; refunds are subject to the team's daily cap. The response reports whether a refund was applied, along with submission and daily-cap status.
|
|
3842
5845
|
`,
|
|
3843
5846
|
outputSchema: feedbackOutputSchema,
|
|
3844
|
-
parameters:
|
|
3845
|
-
searchId:
|
|
3846
|
-
rating:
|
|
3847
|
-
valuableSources:
|
|
3848
|
-
|
|
3849
|
-
url:
|
|
3850
|
-
reason:
|
|
5847
|
+
parameters: z10.object({
|
|
5848
|
+
searchId: z10.string().uuid("searchId must be the UUID returned by firecrawl_search"),
|
|
5849
|
+
rating: z10.enum(["good", "bad", "partial"]),
|
|
5850
|
+
valuableSources: z10.array(
|
|
5851
|
+
z10.object({
|
|
5852
|
+
url: z10.string().url(),
|
|
5853
|
+
reason: z10.string().max(1e3).optional()
|
|
3851
5854
|
})
|
|
3852
5855
|
).max(50).optional(),
|
|
3853
|
-
missingContent:
|
|
3854
|
-
|
|
3855
|
-
topic:
|
|
3856
|
-
description:
|
|
5856
|
+
missingContent: z10.array(
|
|
5857
|
+
z10.object({
|
|
5858
|
+
topic: z10.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
|
|
5859
|
+
description: z10.string().max(2e3).optional()
|
|
3857
5860
|
})
|
|
3858
5861
|
).max(20).optional().describe(
|
|
3859
5862
|
"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`."
|
|
3860
5863
|
),
|
|
3861
|
-
querySuggestions:
|
|
5864
|
+
querySuggestions: z10.string().max(2e3).optional()
|
|
3862
5865
|
}),
|
|
3863
5866
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
3864
5867
|
const origin = requestOrigin(mcpClient, session);
|
|
@@ -3960,20 +5963,20 @@ For an Alexandria session, set endpoint to \`alexandria\`, omit jobId, and provi
|
|
|
3960
5963
|
Returns submission status, feedback ID, and accounting fields.
|
|
3961
5964
|
`,
|
|
3962
5965
|
outputSchema: feedbackOutputSchema,
|
|
3963
|
-
parameters:
|
|
3964
|
-
endpoint:
|
|
3965
|
-
jobId:
|
|
5966
|
+
parameters: z10.object({
|
|
5967
|
+
endpoint: z10.enum(["search", "scrape", "parse", "map", "alexandria"]),
|
|
5968
|
+
jobId: z10.string().uuid("jobId must be the UUID returned by Firecrawl").optional(),
|
|
3966
5969
|
...alexandriaFeedbackFields,
|
|
3967
|
-
rating:
|
|
3968
|
-
issues:
|
|
3969
|
-
tags:
|
|
3970
|
-
note:
|
|
3971
|
-
valuableSources:
|
|
3972
|
-
missingContent:
|
|
3973
|
-
querySuggestions:
|
|
3974
|
-
url:
|
|
3975
|
-
pageNumbers:
|
|
3976
|
-
metadata:
|
|
5970
|
+
rating: z10.enum(["good", "bad", "partial"]),
|
|
5971
|
+
issues: z10.array(feedbackIssueSchema).max(20).optional(),
|
|
5972
|
+
tags: z10.array(feedbackIssueSchema).max(20).optional(),
|
|
5973
|
+
note: z10.string().max(4e3).optional(),
|
|
5974
|
+
valuableSources: z10.array(valuableSourceSchema).max(50).optional(),
|
|
5975
|
+
missingContent: z10.array(missingContentSchema).max(50).optional(),
|
|
5976
|
+
querySuggestions: z10.string().max(2e3).optional(),
|
|
5977
|
+
url: z10.string().url().optional(),
|
|
5978
|
+
pageNumbers: z10.array(z10.number().int().positive()).max(100).optional(),
|
|
5979
|
+
metadata: z10.record(z10.string(), z10.unknown()).optional()
|
|
3977
5980
|
}).superRefine((value, ctx) => {
|
|
3978
5981
|
if (value.endpoint === "alexandria") {
|
|
3979
5982
|
const parsed = alexandriaSessionFeedbackSchema.safeParse(value);
|
|
@@ -4085,25 +6088,25 @@ Start a multi-page crawl at a website URL, poll it to a terminal state, and retu
|
|
|
4085
6088
|
|
|
4086
6089
|
Crawl results can be large; use conservative limits when full-site coverage is unnecessary. Webhooks and interactive scrape actions are unavailable in safe mode. Returns the crawl ID, status, and page data.
|
|
4087
6090
|
`,
|
|
4088
|
-
parameters:
|
|
4089
|
-
url:
|
|
4090
|
-
prompt:
|
|
4091
|
-
excludePaths:
|
|
4092
|
-
includePaths:
|
|
4093
|
-
maxDiscoveryDepth:
|
|
4094
|
-
sitemap:
|
|
4095
|
-
limit:
|
|
4096
|
-
allowExternalLinks:
|
|
4097
|
-
allowSubdomains:
|
|
4098
|
-
crawlEntireDomain:
|
|
4099
|
-
delay:
|
|
4100
|
-
maxConcurrency:
|
|
6091
|
+
parameters: z10.object({
|
|
6092
|
+
url: z10.string(),
|
|
6093
|
+
prompt: z10.string().optional(),
|
|
6094
|
+
excludePaths: z10.array(z10.string()).optional(),
|
|
6095
|
+
includePaths: z10.array(z10.string()).optional(),
|
|
6096
|
+
maxDiscoveryDepth: z10.number().optional(),
|
|
6097
|
+
sitemap: z10.enum(["skip", "include", "only"]).optional(),
|
|
6098
|
+
limit: z10.number().optional(),
|
|
6099
|
+
allowExternalLinks: z10.boolean().optional(),
|
|
6100
|
+
allowSubdomains: z10.boolean().optional(),
|
|
6101
|
+
crawlEntireDomain: z10.boolean().optional(),
|
|
6102
|
+
delay: z10.number().optional(),
|
|
6103
|
+
maxConcurrency: z10.number().optional(),
|
|
4101
6104
|
...SAFE_MODE ? {} : {
|
|
4102
|
-
webhook:
|
|
4103
|
-
webhookHeaders:
|
|
6105
|
+
webhook: z10.string().optional(),
|
|
6106
|
+
webhookHeaders: z10.record(z10.string(), z10.string()).optional()
|
|
4104
6107
|
},
|
|
4105
|
-
deduplicateSimilarURLs:
|
|
4106
|
-
ignoreQueryParameters:
|
|
6108
|
+
deduplicateSimilarURLs: z10.boolean().optional(),
|
|
6109
|
+
ignoreQueryParameters: z10.boolean().optional(),
|
|
4107
6110
|
scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
|
|
4108
6111
|
}),
|
|
4109
6112
|
outputSchema: crawlOutputSchema,
|
|
@@ -4160,7 +6163,7 @@ server.addTool({
|
|
|
4160
6163
|
Retrieve the current status, progress, and available results for an existing crawl ID. This only reads Firecrawl job state and does not start or modify the crawl.
|
|
4161
6164
|
`,
|
|
4162
6165
|
outputSchema: crawlOutputSchema,
|
|
4163
|
-
parameters:
|
|
6166
|
+
parameters: z10.object({ id: z10.string() }),
|
|
4164
6167
|
execute: async (args2, {
|
|
4165
6168
|
session,
|
|
4166
6169
|
client: mcpClient
|
|
@@ -4187,13 +6190,13 @@ server.addTool({
|
|
|
4187
6190
|
Deprecated compatibility entry point. Use firecrawl_scrape once per known URL with formats: ["json"] and jsonOptions containing the prompt and schema. Use firecrawl_agent for multi-source research when the URLs are not known or the data spans several sites.
|
|
4188
6191
|
`,
|
|
4189
6192
|
outputSchema: deprecatedToolOutputSchema,
|
|
4190
|
-
parameters:
|
|
4191
|
-
urls:
|
|
4192
|
-
prompt:
|
|
4193
|
-
schema:
|
|
4194
|
-
allowExternalLinks:
|
|
4195
|
-
enableWebSearch:
|
|
4196
|
-
includeSubdomains:
|
|
6193
|
+
parameters: z10.object({
|
|
6194
|
+
urls: z10.array(z10.string()),
|
|
6195
|
+
prompt: z10.string().optional(),
|
|
6196
|
+
schema: z10.record(z10.string(), z10.any()).optional(),
|
|
6197
|
+
allowExternalLinks: z10.boolean().optional(),
|
|
6198
|
+
enableWebSearch: z10.boolean().optional(),
|
|
6199
|
+
includeSubdomains: z10.boolean().optional()
|
|
4197
6200
|
}),
|
|
4198
6201
|
canList: () => false,
|
|
4199
6202
|
beforeValidate: () => {
|
|
@@ -4206,28 +6209,28 @@ Deprecated compatibility entry point. Use firecrawl_scrape once per known URL wi
|
|
|
4206
6209
|
},
|
|
4207
6210
|
execute: async () => {
|
|
4208
6211
|
const payload = deprecatedExtractPayload();
|
|
4209
|
-
throw new
|
|
6212
|
+
throw new UserError(payload.message, payload);
|
|
4210
6213
|
}
|
|
4211
6214
|
});
|
|
4212
|
-
var agentExchangeSchema =
|
|
4213
|
-
enabled:
|
|
4214
|
-
toolkits:
|
|
4215
|
-
maxCalls:
|
|
4216
|
-
requireApproval:
|
|
6215
|
+
var agentExchangeSchema = z10.strictObject({
|
|
6216
|
+
enabled: z10.boolean().optional().describe("Let the agent call Alexandria providers. On by default."),
|
|
6217
|
+
toolkits: z10.array(z10.string()).max(5).optional().describe("Pin up to 5 providers the agent may use, by slug. Omitted means the whole catalog."),
|
|
6218
|
+
maxCalls: z10.number().int().min(1).max(30).optional().describe("Most provider calls the agent may make in this turn."),
|
|
6219
|
+
requireApproval: z10.boolean().optional().describe(
|
|
4217
6220
|
'End the turn with a paid-call pendingApproval before any paid provider call. Requires mode "chat" on the same request, even on a follow-up.'
|
|
4218
6221
|
),
|
|
4219
|
-
approve:
|
|
4220
|
-
approvalId:
|
|
4221
|
-
callIds:
|
|
4222
|
-
always:
|
|
6222
|
+
approve: z10.strictObject({
|
|
6223
|
+
approvalId: z10.string().uuid(),
|
|
6224
|
+
callIds: z10.array(z10.string()).optional(),
|
|
6225
|
+
always: z10.boolean().optional()
|
|
4223
6226
|
}).optional().describe(
|
|
4224
|
-
"Answer yes to the pendingApproval the previous turn of this thread ended on (its id, also exchange.requiresAction.approvalId). Needs threadId. For a terms offer, send it only after the
|
|
6227
|
+
"Answer yes to the pendingApproval the previous turn of this thread ended on (its id, also exchange.requiresAction.approvalId). Needs threadId. For a terms offer, send it only after an organization admin confirms acceptance in the Firecrawl dashboard; this does not accept terms. callIds and always are ignored on terms offers. For paid calls, callIds picks a subset (default all) and always stops asking for the rest of the thread."
|
|
4225
6228
|
),
|
|
4226
|
-
decline:
|
|
6229
|
+
decline: z10.strictObject({ approvalId: z10.string().uuid() }).optional().describe(
|
|
4227
6230
|
"Answer no to that pendingApproval. Needs threadId. A declined terms offer keeps those providers out of the rest of the thread."
|
|
4228
6231
|
),
|
|
4229
|
-
onTermsRequired:
|
|
4230
|
-
`What to do when a provider the agent would use needs data terms the team has not accepted. Gated providers are never called. "skip" (default): answer with accepted providers and list the rest in exchange.skippedProviders. "ask": the same, plus a terms pendingApproval and exchange.requiresAction
|
|
6232
|
+
onTermsRequired: z10.enum(["skip", "ask"]).optional().describe(
|
|
6233
|
+
`What to do when a provider the agent would use needs data terms the team has not accepted. Gated providers are never called. "skip" (default): answer with accepted providers and list the rest in exchange.skippedProviders. "ask": the same, plus a terms pendingApproval and exchange.requiresAction. Read terms with terms/show; an organization admin accepts them in the Firecrawl dashboard. There is no auto-accept. Omitted on a follow-up keeps the previous turn's value.`
|
|
4231
6234
|
)
|
|
4232
6235
|
}).describe(
|
|
4233
6236
|
"Alexandria provider settings for this turn, forwarded as the request's exchange object."
|
|
@@ -4250,24 +6253,24 @@ This call returns only a job ID, not the research result. Read the job with \`fi
|
|
|
4250
6253
|
|
|
4251
6254
|
The job also returns a \`threadId\`. To continue that thread, pass it with a follow-up \`prompt\`; omitted \`mode\`, \`urls\`, \`schema\` and exchange settings carry over from the previous turn.
|
|
4252
6255
|
|
|
4253
|
-
The agent only calls Alexandria providers whose
|
|
6256
|
+
The agent only calls Alexandria providers whose terms the team has accepted. \`exchange.skippedProviders\` lists gated providers. With \`exchange.onTermsRequired\` "ask", a terms \`pendingApproval\` and \`exchange.requiresAction\` carry the \`approvalId\` and provider requirements. Read terms/show through \`firecrawl_scrape\`. An organization admin must accept terms in the Firecrawl dashboard at the provider URL or ${DATA_SOURCES_SETTINGS_URL}. Ignore any terms/accept call in the API response. Only after the admin confirms acceptance, resume with the same \`threadId\` and \`exchange.approve: {approvalId}\`; this does not accept terms. To decline, use \`exchange.decline: {approvalId}\`. Never infer acceptance from a data request.
|
|
4254
6257
|
`,
|
|
4255
6258
|
outputSchema: agentOutputSchema,
|
|
4256
|
-
parameters:
|
|
4257
|
-
prompt:
|
|
4258
|
-
urls:
|
|
4259
|
-
schema:
|
|
4260
|
-
effort:
|
|
4261
|
-
maxCredits:
|
|
6259
|
+
parameters: z10.object({
|
|
6260
|
+
prompt: z10.string().min(1).max(1e4),
|
|
6261
|
+
urls: z10.array(z10.string().url()).optional(),
|
|
6262
|
+
schema: z10.record(z10.string(), z10.any()).optional(),
|
|
6263
|
+
effort: z10.enum(["low", "medium", "high"]).optional().describe("Reasoning budget for the agent task."),
|
|
6264
|
+
maxCredits: z10.number().int().positive().optional().describe(
|
|
4262
6265
|
"Spending limit in credits for this run. Defaults to 2500 on the API."
|
|
4263
6266
|
),
|
|
4264
|
-
strictConstrainToURLs:
|
|
6267
|
+
strictConstrainToURLs: z10.boolean().optional().describe(
|
|
4265
6268
|
"If true, agent will only visit URLs provided in the urls array."
|
|
4266
6269
|
),
|
|
4267
|
-
threadId:
|
|
6270
|
+
threadId: z10.string().uuid().optional().describe(
|
|
4268
6271
|
"Continue this thread: the threadId from an earlier firecrawl_agent or firecrawl_agent_status result. Omit to start a new thread."
|
|
4269
6272
|
),
|
|
4270
|
-
mode:
|
|
6273
|
+
mode: z10.enum(["extract", "chat"]).optional().describe(
|
|
4271
6274
|
`"extract" (default) returns the complete structured result every turn. "chat" lets a follow-up that asks no new data get a short reply in message instead of a re-run; required for exchange.requireApproval. Omitted on a follow-up keeps the previous turn's mode.`
|
|
4272
6275
|
),
|
|
4273
6276
|
exchange: agentExchangeSchema.optional()
|
|
@@ -4337,7 +6340,7 @@ Retrieve progress or final results for a \`firecrawl_agent\` job ID. A \`process
|
|
|
4337
6340
|
Returns job status, progress information, and result data when completed.
|
|
4338
6341
|
`,
|
|
4339
6342
|
outputSchema: agentStatusOutputSchema,
|
|
4340
|
-
parameters:
|
|
6343
|
+
parameters: z10.object({ id: z10.string() }),
|
|
4341
6344
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
4342
6345
|
const client = getClient(session);
|
|
4343
6346
|
const { id } = args2;
|
|
@@ -4346,9 +6349,41 @@ Returns job status, progress information, and result data when completed.
|
|
|
4346
6349
|
`/v2/agent/${encodeURIComponent(id)}`,
|
|
4347
6350
|
originHeaders(requestOrigin(mcpClient, session))
|
|
4348
6351
|
);
|
|
4349
|
-
return
|
|
6352
|
+
return agentStatusResult(res?.data ?? {});
|
|
4350
6353
|
}
|
|
4351
6354
|
});
|
|
6355
|
+
function agentStatusResult(data) {
|
|
6356
|
+
const run = data;
|
|
6357
|
+
if (!run || typeof run !== "object" || run.status !== "failed" || run.stopReason !== "credit_limit_reached") {
|
|
6358
|
+
return structuredText(data);
|
|
6359
|
+
}
|
|
6360
|
+
const lines = [
|
|
6361
|
+
"This agent run stopped at its credit limit (maxCredits) before finishing."
|
|
6362
|
+
];
|
|
6363
|
+
const hasPartial = run.partial !== void 0 && run.partial !== null;
|
|
6364
|
+
if (hasPartial) {
|
|
6365
|
+
const validity = run.partialSchemaValid === true ? " It matches the schema this run was given." : run.partialSchemaValid === false ? " It does not match the schema this run was given." : "";
|
|
6366
|
+
lines.push(
|
|
6367
|
+
`\`partial\` is an INCOMPLETE best-effort result, not a finished answer; tell the user it is incomplete.${validity}`
|
|
6368
|
+
);
|
|
6369
|
+
} else {
|
|
6370
|
+
lines.push("No partial result was recovered.");
|
|
6371
|
+
}
|
|
6372
|
+
if (typeof run.message === "string" && run.message.trim()) {
|
|
6373
|
+
lines.push(`Agent message: ${run.message.trim()}`);
|
|
6374
|
+
}
|
|
6375
|
+
const threadId = typeof run.threadId === "string" ? run.threadId : void 0;
|
|
6376
|
+
lines.push(
|
|
6377
|
+
threadId ? `To continue, call firecrawl_agent with threadId "${threadId}" and a prompt to finish the task (the follow-up ${hasPartial ? "resumes from this partial" : "keeps this thread"}), or start a new run with a higher maxCredits.` : "To continue, start a new firecrawl_agent run with a higher maxCredits."
|
|
6378
|
+
);
|
|
6379
|
+
const notice = lines.join(" ");
|
|
6380
|
+
return withStructured(
|
|
6381
|
+
`${notice}
|
|
6382
|
+
|
|
6383
|
+
${JSON.stringify(data, null, 2)}`,
|
|
6384
|
+
{ ...run, notice }
|
|
6385
|
+
);
|
|
6386
|
+
}
|
|
4352
6387
|
server.addTool({
|
|
4353
6388
|
name: "firecrawl_interact",
|
|
4354
6389
|
annotations: {
|
|
@@ -4366,13 +6401,13 @@ Open or reuse a live browser session to navigate a page, click controls, fill fi
|
|
|
4366
6401
|
This acts on the live site, so actions such as form submission can create persistent external side effects. Returns execution output, stdout/stderr, exit status, and session viewing URLs.
|
|
4367
6402
|
`,
|
|
4368
6403
|
outputSchema: interactOutputSchema,
|
|
4369
|
-
parameters:
|
|
4370
|
-
scrapeId:
|
|
4371
|
-
url:
|
|
4372
|
-
prompt:
|
|
4373
|
-
code:
|
|
4374
|
-
language:
|
|
4375
|
-
timeout:
|
|
6404
|
+
parameters: z10.object({
|
|
6405
|
+
scrapeId: z10.string().trim().min(1).optional(),
|
|
6406
|
+
url: z10.string().trim().url().optional(),
|
|
6407
|
+
prompt: z10.string().trim().min(1).optional(),
|
|
6408
|
+
code: z10.string().trim().min(1).optional(),
|
|
6409
|
+
language: z10.enum(["bash", "python", "node"]).optional(),
|
|
6410
|
+
timeout: z10.number().min(1).max(300).optional(),
|
|
4376
6411
|
scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
|
|
4377
6412
|
}).refine((data) => Boolean(data.scrapeId) !== Boolean(data.url), {
|
|
4378
6413
|
message: "Provide either 'url' (interact directly) or 'scrapeId' (reuse a previous scrape), not both."
|
|
@@ -4451,8 +6486,8 @@ server.addTool({
|
|
|
4451
6486
|
Stop the live interact session associated with a \`scrapeId\` and release its resources. Returns a success confirmation.
|
|
4452
6487
|
`,
|
|
4453
6488
|
outputSchema: interactStopOutputSchema,
|
|
4454
|
-
parameters:
|
|
4455
|
-
scrapeId:
|
|
6489
|
+
parameters: z10.object({
|
|
6490
|
+
scrapeId: z10.string()
|
|
4456
6491
|
}),
|
|
4457
6492
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
4458
6493
|
const client = getClient(session);
|
|
@@ -4576,13 +6611,15 @@ ${ALEXANDRIA_SEARCH_INSTRUCTIONS}
|
|
|
4576
6611
|
Returns result groups in \`data\` and an operation \`id\`.
|
|
4577
6612
|
`,
|
|
4578
6613
|
outputSchema: searchOutputSchema,
|
|
4579
|
-
parameters:
|
|
6614
|
+
parameters: z10.object({ ...searchToolBaseFields }).strict().refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
|
|
4580
6615
|
searchQueryIsValid,
|
|
4581
6616
|
"A query is required. Use firecrawl_find_tools to browse the catalogue."
|
|
4582
6617
|
),
|
|
4583
6618
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
4584
6619
|
const {
|
|
4585
6620
|
query,
|
|
6621
|
+
objective,
|
|
6622
|
+
clientModel,
|
|
4586
6623
|
includeDomains,
|
|
4587
6624
|
excludeDomains,
|
|
4588
6625
|
limit,
|
|
@@ -4600,6 +6637,8 @@ Returns result groups in \`data\` and an operation \`id\`.
|
|
|
4600
6637
|
const searchBody = {
|
|
4601
6638
|
query: searchQuery,
|
|
4602
6639
|
...removeEmptyTopLevel({
|
|
6640
|
+
objective,
|
|
6641
|
+
clientModel,
|
|
4603
6642
|
limit,
|
|
4604
6643
|
includeDomains,
|
|
4605
6644
|
excludeDomains,
|