firecrawl-mcp 3.25.5 → 3.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -1
- package/dist/agent-hints.js +46 -0
- package/dist/index.js +447 -69
- package/dist/introspection-cache.js +67 -0
- package/package.json +15 -14
package/dist/index.js
CHANGED
|
@@ -87,6 +87,45 @@ import { randomUUID } from "crypto";
|
|
|
87
87
|
import path from "path";
|
|
88
88
|
import { z as z8 } from "zod";
|
|
89
89
|
|
|
90
|
+
// src/agent-hints.ts
|
|
91
|
+
var AGENT_HINTS_HEADERS = { "X-Firecrawl-Agent-Hints": "true" };
|
|
92
|
+
function readAgentHints(value) {
|
|
93
|
+
if (!value || typeof value !== "object") return void 0;
|
|
94
|
+
const hints = value.agent_hints;
|
|
95
|
+
return Array.isArray(hints) && hints.every((hint) => typeof hint === "string") ? hints : void 0;
|
|
96
|
+
}
|
|
97
|
+
function preserveAgentHints(data, envelope) {
|
|
98
|
+
const hints = readAgentHints(envelope);
|
|
99
|
+
if (!hints) return data;
|
|
100
|
+
return data && typeof data === "object" && !Array.isArray(data) ? { ...data, agent_hints: hints } : { data, agent_hints: hints };
|
|
101
|
+
}
|
|
102
|
+
function agentHintsText(hints) {
|
|
103
|
+
return `Firecrawl API agent_hints (response guidance, separate from page content):
|
|
104
|
+
${JSON.stringify(hints, null, 2)}`;
|
|
105
|
+
}
|
|
106
|
+
function withAgentHints(result, response, separateText = false) {
|
|
107
|
+
const hints = readAgentHints(response);
|
|
108
|
+
if (!hints) return result;
|
|
109
|
+
return {
|
|
110
|
+
...result,
|
|
111
|
+
content: separateText && hints.length ? [...result.content, { type: "text", text: agentHintsText(hints) }] : result.content,
|
|
112
|
+
structuredContent: { ...result.structuredContent, agent_hints: hints }
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function readErrorAgentHints(error2) {
|
|
116
|
+
const hints = readAgentHints(error2);
|
|
117
|
+
if (hints) return hints;
|
|
118
|
+
if (!error2 || typeof error2 !== "object") return void 0;
|
|
119
|
+
const extras = error2.extras;
|
|
120
|
+
const extrasHints = readAgentHints(extras);
|
|
121
|
+
if (extrasHints) return extrasHints;
|
|
122
|
+
const details = error2.details;
|
|
123
|
+
const detailsHints = readAgentHints(details);
|
|
124
|
+
if (detailsHints) return detailsHints;
|
|
125
|
+
const response = error2.response;
|
|
126
|
+
return readAgentHints(response?.data);
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
// src/tool-output.ts
|
|
91
130
|
import { z as z2 } from "zod";
|
|
92
131
|
function isRecord(value) {
|
|
@@ -115,8 +154,11 @@ var unknown = (description) => z2.unknown().optional().describe(description);
|
|
|
115
154
|
var success = bool("Whether the API call succeeded.");
|
|
116
155
|
var error = unknown("Error message or error object when the call did not succeed.");
|
|
117
156
|
var warning = str("Non-fatal warning about the result.");
|
|
157
|
+
var agentHints = {
|
|
158
|
+
agent_hints: z2.array(z2.string()).optional().describe("Optional response guidance from the Firecrawl API.")
|
|
159
|
+
};
|
|
118
160
|
var alexandriaEnvelope = {
|
|
119
|
-
requestId: str("Identifier of this
|
|
161
|
+
requestId: str("Identifier of this Alexandria execution."),
|
|
120
162
|
scrape_id: str("Identifier of the underlying scrape."),
|
|
121
163
|
receipt: unknown("Billing receipt for the execution."),
|
|
122
164
|
creditsCost: num("Credits this call consumed."),
|
|
@@ -132,6 +174,7 @@ var alexandriaEnvelope = {
|
|
|
132
174
|
feedbackTool: unknown("Pointer to the feedback tool for reporting how this result served the task.")
|
|
133
175
|
};
|
|
134
176
|
var scrapeOutputSchema = z2.object({
|
|
177
|
+
...agentHints,
|
|
135
178
|
// URL mode: the scraped document, with the formats that were requested.
|
|
136
179
|
markdown: str("Page content as markdown."),
|
|
137
180
|
html: str("Processed HTML of the page."),
|
|
@@ -163,6 +206,7 @@ var scrapeOutputSchema = z2.object({
|
|
|
163
206
|
...alexandriaEnvelope
|
|
164
207
|
}).describe("A scraped document, or the Alexandria execution envelope when `alexandria` was passed.");
|
|
165
208
|
var mapOutputSchema = z2.object({
|
|
209
|
+
...agentHints,
|
|
166
210
|
links: unknown("URLs discovered under the website."),
|
|
167
211
|
success,
|
|
168
212
|
error,
|
|
@@ -170,6 +214,7 @@ var mapOutputSchema = z2.object({
|
|
|
170
214
|
id: str("Identifier of the map run, for optional map feedback.")
|
|
171
215
|
}).describe("URLs indexed under the requested website.");
|
|
172
216
|
var searchOutputSchema = z2.object({
|
|
217
|
+
...agentHints,
|
|
173
218
|
success,
|
|
174
219
|
data: unknown("Ranked results grouped by source, such as `web`, `news`, `images`, and `alexandria`."),
|
|
175
220
|
error,
|
|
@@ -181,12 +226,14 @@ var searchOutputSchema = z2.object({
|
|
|
181
226
|
feedbackTool: unknown("Pointer to the feedback tool for this search.")
|
|
182
227
|
}).describe("Ranked search results grouped by source.");
|
|
183
228
|
var findToolsOutputSchema = z2.object({
|
|
229
|
+
...agentHints,
|
|
184
230
|
success,
|
|
185
231
|
data: unknown("Discovery page in `data.alexandria[0].data`, with `level`, `items`, `total`, and an optional `nextTool`."),
|
|
186
232
|
error,
|
|
187
233
|
...alexandriaEnvelope
|
|
188
234
|
}).describe("One page of the Alexandria catalogue: categories, providers, tools, or one contract.");
|
|
189
235
|
var feedbackOutputSchema = z2.object({
|
|
236
|
+
...agentHints,
|
|
190
237
|
success,
|
|
191
238
|
error,
|
|
192
239
|
status: num("HTTP status when the submission was rejected."),
|
|
@@ -204,6 +251,7 @@ var feedbackOutputSchema = z2.object({
|
|
|
204
251
|
raw: str("Response body when it was not JSON.")
|
|
205
252
|
}).describe("Result of submitting feedback.");
|
|
206
253
|
var crawlOutputSchema = z2.object({
|
|
254
|
+
...agentHints,
|
|
207
255
|
success,
|
|
208
256
|
error,
|
|
209
257
|
warning,
|
|
@@ -218,6 +266,7 @@ var crawlOutputSchema = z2.object({
|
|
|
218
266
|
data: unknown("Scraped documents collected by the crawl.")
|
|
219
267
|
}).describe("Crawl job state and the documents collected so far.");
|
|
220
268
|
var agentOutputSchema = z2.object({
|
|
269
|
+
...agentHints,
|
|
221
270
|
success,
|
|
222
271
|
error,
|
|
223
272
|
id: str("Agent job identifier, for `firecrawl_agent_status`."),
|
|
@@ -226,6 +275,7 @@ var agentOutputSchema = z2.object({
|
|
|
226
275
|
threadTurn: num("Turn number of this job within its thread.")
|
|
227
276
|
}).describe("The queued research job. The result is read with `firecrawl_agent_status`.");
|
|
228
277
|
var agentStatusOutputSchema = z2.object({
|
|
278
|
+
...agentHints,
|
|
229
279
|
success,
|
|
230
280
|
error,
|
|
231
281
|
id: str("Agent job identifier."),
|
|
@@ -238,9 +288,18 @@ var agentStatusOutputSchema = z2.object({
|
|
|
238
288
|
model: str("Agent model that ran the job."),
|
|
239
289
|
mode: str("Agent mode the job ran in."),
|
|
240
290
|
threadId: str("Research thread this job belongs to."),
|
|
241
|
-
threadTurn: num("Turn number of this job within its thread.")
|
|
291
|
+
threadTurn: num("Turn number of this job within its thread."),
|
|
292
|
+
message: unknown("The agent's reply; in chat mode, the short answer to a follow-up."),
|
|
293
|
+
suggestions: unknown("Follow-ups the agent offers; send one as the prompt of the next turn with this threadId."),
|
|
294
|
+
exchange: unknown(
|
|
295
|
+
"What the job did with Alexandria providers: onTermsRequired, paidCalls, creditsUsed, skippedProviders (gated providers that would have helped), and requiresAction (approvalId plus the terms/show and terms/accept calls, each provider digest string | null and always present; call accept only with the user's explicit consent, then continue the thread with exchange.approve: {approvalId})."
|
|
296
|
+
),
|
|
297
|
+
pendingApproval: unknown(
|
|
298
|
+
'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 accepting; otherwise calls lists paid calls waiting for approval.'
|
|
299
|
+
)
|
|
242
300
|
}).describe("Progress or final result of a research agent job.");
|
|
243
301
|
var interactOutputSchema = z2.object({
|
|
302
|
+
...agentHints,
|
|
244
303
|
success,
|
|
245
304
|
error: unknown("Why the interaction or the session could not run."),
|
|
246
305
|
url: str("The URL the session was opened from, when opening failed."),
|
|
@@ -255,12 +314,14 @@ var interactOutputSchema = z2.object({
|
|
|
255
314
|
liveUrl: str("URL for viewing the live session.")
|
|
256
315
|
}).describe("Result of a browser interaction, with the session identifier to continue or stop it.");
|
|
257
316
|
var interactStopOutputSchema = z2.object({
|
|
317
|
+
...agentHints,
|
|
258
318
|
success,
|
|
259
319
|
error,
|
|
260
320
|
message: str("Confirmation that the session was stopped."),
|
|
261
321
|
scrapeId: str("Session that was stopped.")
|
|
262
322
|
}).describe("Confirmation that the interact session was stopped.");
|
|
263
323
|
var parseOutputSchema = z2.object({
|
|
324
|
+
...agentHints,
|
|
264
325
|
success,
|
|
265
326
|
error,
|
|
266
327
|
warning,
|
|
@@ -288,33 +349,39 @@ var deprecatedToolOutputSchema = z2.object({
|
|
|
288
349
|
docs_url: str("Documentation for the replacement.")
|
|
289
350
|
}).describe("A pointer to the replacement tool. This entry point is deprecated and always errors.");
|
|
290
351
|
var monitorOutputSchema = z2.object({
|
|
352
|
+
...agentHints,
|
|
291
353
|
success,
|
|
292
354
|
error,
|
|
293
355
|
data: unknown("The monitor record: configuration, schedule, targets, and current state.")
|
|
294
356
|
}).describe("One monitor record.");
|
|
295
357
|
var monitorListOutputSchema = z2.object({
|
|
358
|
+
...agentHints,
|
|
296
359
|
success,
|
|
297
360
|
error,
|
|
298
361
|
data: unknown("One page of monitor records."),
|
|
299
362
|
pagination: unknown("Pagination metadata for the listing.")
|
|
300
363
|
}).describe("One page of monitors for the authenticated account.");
|
|
301
364
|
var monitorDeleteOutputSchema = z2.object({
|
|
365
|
+
...agentHints,
|
|
302
366
|
success,
|
|
303
367
|
error,
|
|
304
368
|
data: unknown("Deletion details, when the API returns any.")
|
|
305
369
|
}).describe("Deletion status for the monitor.");
|
|
306
370
|
var monitorRunOutputSchema = z2.object({
|
|
371
|
+
...agentHints,
|
|
307
372
|
success,
|
|
308
373
|
error,
|
|
309
374
|
data: unknown("The queued check.")
|
|
310
375
|
}).describe("The check queued outside the monitor schedule.");
|
|
311
376
|
var monitorChecksOutputSchema = z2.object({
|
|
377
|
+
...agentHints,
|
|
312
378
|
success,
|
|
313
379
|
error,
|
|
314
380
|
data: unknown("One page of check summaries."),
|
|
315
381
|
pagination: unknown("Pagination metadata for the listing.")
|
|
316
382
|
}).describe("One page of historical checks for the monitor.");
|
|
317
383
|
var monitorCheckOutputSchema = z2.object({
|
|
384
|
+
...agentHints,
|
|
318
385
|
success,
|
|
319
386
|
error,
|
|
320
387
|
data: unknown("The check and its page-level results: each page reports `same`, `new`, `changed`, `removed`, or `error`, with diffs and any goal judgment."),
|
|
@@ -333,20 +400,25 @@ var paperSchema = z2.looseObject({
|
|
|
333
400
|
updateDate: str("Date the paper was last updated.")
|
|
334
401
|
});
|
|
335
402
|
var researchSearchOutputSchema = z2.object({
|
|
403
|
+
...agentHints,
|
|
336
404
|
results: z2.array(paperSchema).describe("Ranked papers, in the order the text block lists them.")
|
|
337
405
|
}).describe("Ranked papers with canonical IDs, titles, authors, and abstracts.");
|
|
338
406
|
var researchPaperOutputSchema = z2.object({
|
|
407
|
+
...agentHints,
|
|
339
408
|
paper: paperSchema.optional().describe("Canonical metadata for the requested paper, absent when it was not found.")
|
|
340
409
|
}).describe("Canonical metadata for one paper.");
|
|
341
410
|
var researchRelatedOutputSchema = z2.object({
|
|
411
|
+
...agentHints,
|
|
342
412
|
results: z2.array(paperSchema).describe("Ranked citation-graph candidates."),
|
|
343
413
|
poolSize: z2.number().describe("Number of candidates evaluated before ranking."),
|
|
344
414
|
note: str("Note about how the candidates were produced.")
|
|
345
415
|
}).describe("Ranked citation-graph candidates and the size of the evaluated pool.");
|
|
346
416
|
var researchReadOutputSchema = z2.object({
|
|
417
|
+
...agentHints,
|
|
347
418
|
passages: z2.array(z2.looseObject({ text: str("Passage text.") })).describe("In-body passages relevant to the question; empty when no full text is indexed.")
|
|
348
419
|
}).describe("Full-text passages from one paper.");
|
|
349
420
|
var developerSearchOutputSchema = z2.object({
|
|
421
|
+
...agentHints,
|
|
350
422
|
results: z2.array(
|
|
351
423
|
z2.looseObject({
|
|
352
424
|
id: str("Stable result id, such as `issue:owner/repo#123` or `doc:<hash>`; its prefix gives the source type."),
|
|
@@ -435,7 +507,8 @@ function withFindToolsNavigation(envelope) {
|
|
|
435
507
|
const link = (next) => next?.provider === "firecrawl" && next?.capability === "find-tools" && next.options ? { name: "firecrawl_find_tools", arguments: next.options } : void 0;
|
|
436
508
|
for (const item of page.items) {
|
|
437
509
|
if (page.level === "providers" && link(item.next)) {
|
|
438
|
-
const
|
|
510
|
+
const selectors = { ...item.next.options };
|
|
511
|
+
delete selectors.expand;
|
|
439
512
|
item.next = { ...item.next, options: { ...selectors, level: "tools" } };
|
|
440
513
|
}
|
|
441
514
|
const nextTool = link(item.next);
|
|
@@ -594,7 +667,7 @@ Returns ranked results with an ID, source type, URL, title, and the matched pass
|
|
|
594
667
|
const client = getClient2(session);
|
|
595
668
|
const res = await client.http.get(`${BASE}?${params.toString()}`, originHeaders(requestOrigin(mcpClient, session)));
|
|
596
669
|
const results = res.data?.results ?? [];
|
|
597
|
-
return withStructured(fmtDeveloper(results), { results });
|
|
670
|
+
return withAgentHints(withStructured(fmtDeveloper(results), { results }), res.data, true);
|
|
598
671
|
}
|
|
599
672
|
});
|
|
600
673
|
}
|
|
@@ -621,10 +694,12 @@ import { createHmac } from "crypto";
|
|
|
621
694
|
var managedOAuthApiKey = /* @__PURE__ */ Symbol("firecrawlManagedOAuthApiKey");
|
|
622
695
|
var CoreHttpError = class extends Error {
|
|
623
696
|
status;
|
|
624
|
-
|
|
697
|
+
agent_hints;
|
|
698
|
+
constructor(message, status, agentHints2) {
|
|
625
699
|
super(message);
|
|
626
700
|
this.name = "CoreHttpError";
|
|
627
701
|
this.status = status;
|
|
702
|
+
if (agentHints2 !== void 0) this.agent_hints = agentHints2;
|
|
628
703
|
}
|
|
629
704
|
};
|
|
630
705
|
var CredentialValidationUnavailableError = class extends Error {
|
|
@@ -636,11 +711,12 @@ var CredentialValidationUnavailableError = class extends Error {
|
|
|
636
711
|
}
|
|
637
712
|
};
|
|
638
713
|
function credentialValidationUnavailable(diagnostics) {
|
|
639
|
-
const { aborted, elapsedMs, reason, resource, status } = diagnostics;
|
|
714
|
+
const { aborted, edgeMitigation: edgeMitigation2, elapsedMs, reason, resource, status } = diagnostics;
|
|
640
715
|
console.error(
|
|
641
716
|
"[MCP_CREDENTIAL_VALIDATION]",
|
|
642
717
|
JSON.stringify({
|
|
643
718
|
aborted: aborted ?? null,
|
|
719
|
+
edge_mitigation: edgeMitigation2 ?? null,
|
|
644
720
|
elapsed_ms: elapsedMs ?? null,
|
|
645
721
|
introspect_status: status ?? null,
|
|
646
722
|
reason,
|
|
@@ -724,7 +800,10 @@ async function monitorRequest(session, origin, path2, init = {}) {
|
|
|
724
800
|
const s = qs.toString();
|
|
725
801
|
if (s) url += `?${s}`;
|
|
726
802
|
}
|
|
727
|
-
const headers =
|
|
803
|
+
const headers = {
|
|
804
|
+
...originHeaders(origin),
|
|
805
|
+
...AGENT_HINTS_HEADERS
|
|
806
|
+
};
|
|
728
807
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
729
808
|
if (init.body !== void 0) headers["Content-Type"] = "application/json";
|
|
730
809
|
const response = await fetch(url, {
|
|
@@ -735,10 +814,13 @@ async function monitorRequest(session, origin, path2, init = {}) {
|
|
|
735
814
|
const payload = await response.json().catch(() => ({}));
|
|
736
815
|
if (!response.ok || payload?.success === false) {
|
|
737
816
|
const message = payload?.error || `HTTP ${response.status}: ${response.statusText || "Request failed"}`;
|
|
738
|
-
throw new CoreHttpError(message, response.status);
|
|
817
|
+
throw new CoreHttpError(message, response.status, readAgentHints(payload));
|
|
739
818
|
}
|
|
740
819
|
return payload;
|
|
741
820
|
}
|
|
821
|
+
function monitorResult(data) {
|
|
822
|
+
return withAgentHints(structuredText(data), data);
|
|
823
|
+
}
|
|
742
824
|
var pageStatusSchema = z5.enum(["same", "new", "changed", "removed", "error"]);
|
|
743
825
|
var checkStatusSchema = z5.enum([
|
|
744
826
|
"queued",
|
|
@@ -863,7 +945,7 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
|
|
|
863
945
|
body
|
|
864
946
|
}
|
|
865
947
|
);
|
|
866
|
-
return
|
|
948
|
+
return monitorResult(res);
|
|
867
949
|
}
|
|
868
950
|
});
|
|
869
951
|
server2.addTool({
|
|
@@ -895,7 +977,7 @@ List monitors for the authenticated account with optional pagination controls. R
|
|
|
895
977
|
query: { limit, offset }
|
|
896
978
|
}
|
|
897
979
|
);
|
|
898
|
-
return
|
|
980
|
+
return monitorResult(res);
|
|
899
981
|
}
|
|
900
982
|
});
|
|
901
983
|
server2.addTool({
|
|
@@ -921,7 +1003,7 @@ Retrieve one monitor by ID, including its configuration and current state. This
|
|
|
921
1003
|
requestOrigin(mcpClient, session),
|
|
922
1004
|
`/monitor/${encodeURIComponent(id)}`
|
|
923
1005
|
);
|
|
924
|
-
return
|
|
1006
|
+
return monitorResult(res);
|
|
925
1007
|
}
|
|
926
1008
|
});
|
|
927
1009
|
server2.addTool({
|
|
@@ -953,7 +1035,7 @@ Returns the updated monitor.
|
|
|
953
1035
|
`/monitor/${encodeURIComponent(id)}`,
|
|
954
1036
|
{ method: "PATCH", body }
|
|
955
1037
|
);
|
|
956
|
-
return
|
|
1038
|
+
return monitorResult(res);
|
|
957
1039
|
}
|
|
958
1040
|
});
|
|
959
1041
|
server2.addTool({
|
|
@@ -981,7 +1063,7 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
|
|
|
981
1063
|
`/monitor/${encodeURIComponent(id)}`,
|
|
982
1064
|
{ method: "DELETE" }
|
|
983
1065
|
);
|
|
984
|
-
return
|
|
1066
|
+
return monitorResult(res);
|
|
985
1067
|
}
|
|
986
1068
|
});
|
|
987
1069
|
server2.addTool({
|
|
@@ -1008,7 +1090,7 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
|
|
|
1008
1090
|
`/monitor/${encodeURIComponent(id)}/run`,
|
|
1009
1091
|
{ method: "POST" }
|
|
1010
1092
|
);
|
|
1011
|
-
return
|
|
1093
|
+
return monitorResult(res);
|
|
1012
1094
|
}
|
|
1013
1095
|
});
|
|
1014
1096
|
server2.addTool({
|
|
@@ -1040,7 +1122,7 @@ List historical checks for a monitor, optionally filtered by status and bounded
|
|
|
1040
1122
|
`/monitor/${encodeURIComponent(id)}/checks`,
|
|
1041
1123
|
{ query: { limit, offset, status } }
|
|
1042
1124
|
);
|
|
1043
|
-
return
|
|
1125
|
+
return monitorResult(res);
|
|
1044
1126
|
}
|
|
1045
1127
|
});
|
|
1046
1128
|
server2.addTool({
|
|
@@ -1075,7 +1157,7 @@ Markdown tracking returns a unified text diff, JSON tracking returns field paths
|
|
|
1075
1157
|
`/monitor/${encodeURIComponent(id)}/checks/${encodeURIComponent(checkId)}`,
|
|
1076
1158
|
{ query: { limit, skip, status: pageStatus } }
|
|
1077
1159
|
);
|
|
1078
|
-
return
|
|
1160
|
+
return monitorResult(res);
|
|
1079
1161
|
}
|
|
1080
1162
|
});
|
|
1081
1163
|
}
|
|
@@ -1084,6 +1166,9 @@ Markdown tracking returns a unified text diff, JSON tracking returns field paths
|
|
|
1084
1166
|
import { z as z6 } from "zod";
|
|
1085
1167
|
import { UserError } from "fastmcp";
|
|
1086
1168
|
var BASE2 = "/v2/search/research";
|
|
1169
|
+
function researchHeaders(origin) {
|
|
1170
|
+
return { ...originHeaders(origin), ...AGENT_HINTS_HEADERS };
|
|
1171
|
+
}
|
|
1087
1172
|
function appendParam(params, key, value) {
|
|
1088
1173
|
if (value == null) return;
|
|
1089
1174
|
if (Array.isArray(value)) {
|
|
@@ -1228,10 +1313,10 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
|
|
|
1228
1313
|
const client = getClient2(session);
|
|
1229
1314
|
const res = await client.http.get(
|
|
1230
1315
|
withQuery(`${BASE2}/papers`, params),
|
|
1231
|
-
|
|
1316
|
+
researchHeaders(requestOrigin(mcpClient, session))
|
|
1232
1317
|
);
|
|
1233
1318
|
const results = res.data?.results ?? [];
|
|
1234
|
-
return withStructured(fmtHits(results), { results });
|
|
1319
|
+
return withAgentHints(withStructured(fmtHits(results), { results }), res.data, true);
|
|
1235
1320
|
}
|
|
1236
1321
|
});
|
|
1237
1322
|
server2.addTool({
|
|
@@ -1259,13 +1344,13 @@ Retrieve canonical metadata for one paper ID, such as an arXiv, PMC, PMID, or DO
|
|
|
1259
1344
|
const client = getClient2(session);
|
|
1260
1345
|
const res = await client.http.get(
|
|
1261
1346
|
`${BASE2}/papers/${encodeURIComponent(paperId)}`,
|
|
1262
|
-
|
|
1347
|
+
researchHeaders(requestOrigin(mcpClient, session))
|
|
1263
1348
|
);
|
|
1264
1349
|
const paper = res.data?.paper;
|
|
1265
|
-
return withStructured(
|
|
1350
|
+
return withAgentHints(withStructured(
|
|
1266
1351
|
fmtPaperMetadata(paper),
|
|
1267
1352
|
paper ? { paper } : {}
|
|
1268
|
-
);
|
|
1353
|
+
), res.data, true);
|
|
1269
1354
|
}
|
|
1270
1355
|
});
|
|
1271
1356
|
server2.addTool({
|
|
@@ -1307,17 +1392,17 @@ Returns ranked candidates and the evaluated pool size.
|
|
|
1307
1392
|
`${BASE2}/papers/${encodeURIComponent(primary)}/similar`,
|
|
1308
1393
|
params
|
|
1309
1394
|
),
|
|
1310
|
-
|
|
1395
|
+
researchHeaders(requestOrigin(mcpClient, session))
|
|
1311
1396
|
);
|
|
1312
1397
|
const results = res.data?.results ?? [];
|
|
1313
1398
|
const poolSize = res.data?.poolSize ?? 0;
|
|
1314
1399
|
const note = res.data?.note ? `
|
|
1315
1400
|
note: ${res.data.note}` : "";
|
|
1316
|
-
return withStructured(
|
|
1401
|
+
return withAgentHints(withStructured(
|
|
1317
1402
|
`${fmtHits(results)}
|
|
1318
1403
|
(poolSize=${poolSize})${note}`,
|
|
1319
1404
|
{ results, poolSize, ...res.data?.note ? { note: res.data.note } : {} }
|
|
1320
|
-
);
|
|
1405
|
+
), res.data, true);
|
|
1321
1406
|
}
|
|
1322
1407
|
});
|
|
1323
1408
|
server2.addTool({
|
|
@@ -1352,13 +1437,13 @@ Returns matching passages or a notice when full text is unavailable.
|
|
|
1352
1437
|
const client = getClient2(session);
|
|
1353
1438
|
const res = await client.http.get(
|
|
1354
1439
|
withQuery(`${BASE2}/papers/${encodeURIComponent(paperId)}`, params),
|
|
1355
|
-
|
|
1440
|
+
researchHeaders(requestOrigin(mcpClient, session))
|
|
1356
1441
|
);
|
|
1357
1442
|
const passages = res.data?.passages ?? [];
|
|
1358
|
-
return withStructured(
|
|
1443
|
+
return withAgentHints(withStructured(
|
|
1359
1444
|
passages.length ? passages.map((p) => p.text).join("\n---\n") : "(no full-text passages available for this paper)",
|
|
1360
1445
|
{ passages }
|
|
1361
|
-
);
|
|
1446
|
+
), res.data, true);
|
|
1362
1447
|
}
|
|
1363
1448
|
});
|
|
1364
1449
|
server2.addTool({
|
|
@@ -1468,6 +1553,68 @@ function escapeWWWAuthenticateValue(value) {
|
|
|
1468
1553
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1469
1554
|
}
|
|
1470
1555
|
|
|
1556
|
+
// src/introspection-cache.ts
|
|
1557
|
+
import { createHash } from "crypto";
|
|
1558
|
+
function createIntrospectionCache(options) {
|
|
1559
|
+
const now = options.now ?? Date.now;
|
|
1560
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1561
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1562
|
+
function store(key, value) {
|
|
1563
|
+
const at = now();
|
|
1564
|
+
const ttl = options.ttlMs(value, at);
|
|
1565
|
+
if (!(ttl > 0)) return;
|
|
1566
|
+
entries.delete(key);
|
|
1567
|
+
entries.set(key, { expiresAt: at + ttl, value });
|
|
1568
|
+
while (entries.size > options.maxEntries) {
|
|
1569
|
+
const oldest = entries.keys().next().value;
|
|
1570
|
+
if (oldest === void 0) break;
|
|
1571
|
+
entries.delete(oldest);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
async get(parts, load) {
|
|
1576
|
+
const key = createHash("sha256").update(parts.join("\0")).digest("hex");
|
|
1577
|
+
const hit = entries.get(key);
|
|
1578
|
+
if (hit) {
|
|
1579
|
+
entries.delete(key);
|
|
1580
|
+
if (hit.expiresAt > now()) {
|
|
1581
|
+
entries.set(key, hit);
|
|
1582
|
+
return hit.value;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const pending = inflight.get(key);
|
|
1586
|
+
if (pending) return pending;
|
|
1587
|
+
const request = load().then(
|
|
1588
|
+
(value) => {
|
|
1589
|
+
inflight.delete(key);
|
|
1590
|
+
store(key, value);
|
|
1591
|
+
return value;
|
|
1592
|
+
},
|
|
1593
|
+
(error2) => {
|
|
1594
|
+
inflight.delete(key);
|
|
1595
|
+
throw error2;
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
inflight.set(key, request);
|
|
1599
|
+
return request;
|
|
1600
|
+
},
|
|
1601
|
+
clear() {
|
|
1602
|
+
entries.clear();
|
|
1603
|
+
},
|
|
1604
|
+
get size() {
|
|
1605
|
+
return entries.size;
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
var INTROSPECTION_ACTIVE_TTL_MS = 6e4;
|
|
1610
|
+
var INTROSPECTION_INACTIVE_TTL_MS = 1e4;
|
|
1611
|
+
function introspectionTtlMs(value, now, activeTtlMs = INTROSPECTION_ACTIVE_TTL_MS, inactiveTtlMs = INTROSPECTION_INACTIVE_TTL_MS) {
|
|
1612
|
+
if (!value.active) return inactiveTtlMs;
|
|
1613
|
+
if (value.exp === void 0) return activeTtlMs;
|
|
1614
|
+
if (typeof value.exp !== "number" || !Number.isFinite(value.exp)) return 0;
|
|
1615
|
+
return Math.min(activeTtlMs, value.exp * 1e3 - now);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1471
1618
|
// src/index.ts
|
|
1472
1619
|
dotenv.config({ debug: false, quiet: true });
|
|
1473
1620
|
var require2 = createRequire2(import.meta.url);
|
|
@@ -1658,7 +1805,40 @@ function credentialMetadata(data) {
|
|
|
1658
1805
|
resource: typeof data.aud === "string" ? data.aud : void 0
|
|
1659
1806
|
};
|
|
1660
1807
|
}
|
|
1661
|
-
|
|
1808
|
+
function introspectionActiveTtlMs() {
|
|
1809
|
+
const raw = normalizeHeader(
|
|
1810
|
+
process.env.FIRECRAWL_OAUTH_INTROSPECT_CACHE_TTL_MS
|
|
1811
|
+
);
|
|
1812
|
+
if (raw === void 0) return INTROSPECTION_ACTIVE_TTL_MS;
|
|
1813
|
+
const parsed = Number(raw);
|
|
1814
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : INTROSPECTION_ACTIVE_TTL_MS;
|
|
1815
|
+
}
|
|
1816
|
+
var introspectionCache = createIntrospectionCache({
|
|
1817
|
+
maxEntries: 5e4,
|
|
1818
|
+
ttlMs: (value, now) => {
|
|
1819
|
+
const activeTtlMs = introspectionActiveTtlMs();
|
|
1820
|
+
if (activeTtlMs === 0) return 0;
|
|
1821
|
+
return introspectionTtlMs(
|
|
1822
|
+
value,
|
|
1823
|
+
now,
|
|
1824
|
+
activeTtlMs,
|
|
1825
|
+
Math.min(activeTtlMs, INTROSPECTION_INACTIVE_TTL_MS)
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
function introspectToken(token, expectedResource) {
|
|
1830
|
+
return introspectionCache.get(
|
|
1831
|
+
[token, expectedResource],
|
|
1832
|
+
() => fetchIntrospection(token, expectedResource)
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
var EDGE_MITIGATIONS = /* @__PURE__ */ new Set(["deny", "challenge", "rate_limit"]);
|
|
1836
|
+
function edgeMitigation(response) {
|
|
1837
|
+
const value = response.headers.get("x-vercel-mitigated")?.trim().toLowerCase();
|
|
1838
|
+
if (!value) return void 0;
|
|
1839
|
+
return EDGE_MITIGATIONS.has(value) ? value : "other";
|
|
1840
|
+
}
|
|
1841
|
+
async function fetchIntrospection(token, expectedResource) {
|
|
1662
1842
|
const introspectionSecret = getOAuthIntrospectionSecret();
|
|
1663
1843
|
if (!introspectionSecret) {
|
|
1664
1844
|
throw credentialValidationUnavailable({
|
|
@@ -1697,6 +1877,7 @@ async function introspectToken(token, expectedResource) {
|
|
|
1697
1877
|
}
|
|
1698
1878
|
if (!response.ok) {
|
|
1699
1879
|
throw credentialValidationUnavailable({
|
|
1880
|
+
edgeMitigation: edgeMitigation(response),
|
|
1700
1881
|
elapsedMs: elapsedMs(),
|
|
1701
1882
|
reason: "introspect_http_status",
|
|
1702
1883
|
resource: expectedResource,
|
|
@@ -2010,7 +2191,7 @@ function termsRequiredAction(body) {
|
|
|
2010
2191
|
url: action.url
|
|
2011
2192
|
};
|
|
2012
2193
|
}
|
|
2013
|
-
function termsRequiredError(action, context) {
|
|
2194
|
+
function termsRequiredError(action, context, hints) {
|
|
2014
2195
|
const requestId = context.requestId;
|
|
2015
2196
|
const retryIdentity = requestId ? ` and requestId ${requestId}` : "";
|
|
2016
2197
|
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.`;
|
|
@@ -2025,6 +2206,7 @@ Do not retry until acceptance is confirmed.`,
|
|
|
2025
2206
|
code: TERMS_REQUIRED_CODE,
|
|
2026
2207
|
status: 403,
|
|
2027
2208
|
message,
|
|
2209
|
+
...hints ? { agent_hints: hints } : {},
|
|
2028
2210
|
...requestId ? { requestId } : {},
|
|
2029
2211
|
requiresAction: action,
|
|
2030
2212
|
nextTool: {
|
|
@@ -2055,7 +2237,8 @@ Do not retry until acceptance is confirmed.`,
|
|
|
2055
2237
|
function throwIfTermsRequired(error2, context) {
|
|
2056
2238
|
const source = error2;
|
|
2057
2239
|
const action = termsRequiredAction(source?.response?.data ?? source?.details);
|
|
2058
|
-
if (action)
|
|
2240
|
+
if (action)
|
|
2241
|
+
throw termsRequiredError(action, context, readErrorAgentHints(error2));
|
|
2059
2242
|
}
|
|
2060
2243
|
async function relayTermsRequired(run, context) {
|
|
2061
2244
|
try {
|
|
@@ -2073,6 +2256,7 @@ async function relayExchangeError(run, context) {
|
|
|
2073
2256
|
const response = error2?.response;
|
|
2074
2257
|
if (!response || response.status === 401) throw error2;
|
|
2075
2258
|
const data = response.data;
|
|
2259
|
+
const hints = readErrorAgentHints(error2);
|
|
2076
2260
|
const message = typeof data?.error === "string" ? data.error : `Alexandria request failed (HTTP ${response.status})`;
|
|
2077
2261
|
const disabledProvider = response.status === 403 ? context.providers?.find((provider) => message === `Access to ${provider} is disabled for this organization.`) : void 0;
|
|
2078
2262
|
if (disabledProvider) {
|
|
@@ -2082,6 +2266,7 @@ async function relayExchangeError(run, context) {
|
|
|
2082
2266
|
code: typeof data?.code === "string" ? data.code : "exchange_error",
|
|
2083
2267
|
status: 403,
|
|
2084
2268
|
message,
|
|
2269
|
+
...hints ? { agent_hints: hints } : {},
|
|
2085
2270
|
nextTool: {
|
|
2086
2271
|
name: "firecrawl_scrape",
|
|
2087
2272
|
arguments: {
|
|
@@ -2095,6 +2280,7 @@ async function relayExchangeError(run, context) {
|
|
|
2095
2280
|
code: typeof data?.code === "string" ? data.code : "exchange_error",
|
|
2096
2281
|
status: response.status,
|
|
2097
2282
|
message,
|
|
2283
|
+
...hints ? { agent_hints: hints } : {},
|
|
2098
2284
|
...typeof data?.chargeId === "string" ? { chargeId: data.chargeId } : {}
|
|
2099
2285
|
});
|
|
2100
2286
|
}
|
|
@@ -2148,7 +2334,7 @@ var ConsoleLogger = class {
|
|
|
2148
2334
|
var openAiAppsChallengeToken = normalizeHeader(
|
|
2149
2335
|
process.env.OPENAI_APPS_CHALLENGE_TOKEN
|
|
2150
2336
|
);
|
|
2151
|
-
var FULL_PROFILE_INSTRUCTIONS = `Firecrawl provides web search, page retrieval, site URL discovery, multi-page collection, structured page data, monitoring, and multi-source research that returns structured data. Match the requested operation to the tool boundary: firecrawl_scrape retrieves one supplied page and can return JSON matching a supplied schema, firecrawl_map enumerates URLs under a site without retrieving their content, and firecrawl_agent runs multi-source research and returns structured data when the URLs are not known or the answer spans several sites (an entity plus its fields, a list, a dataset); its result is read with firecrawl_agent_status. Authenticated firecrawl_search returns web results together with matching Alexandria providers in data.tools. ${ALEXANDRIA_CATALOGUE_SENTENCE} A matching provider can return the same fields across several entities, provenance, exact figures or timestamps, or a large set of records through firecrawl_scrape with its published contract. If web results already answer the question, use them. For the same fields across multiple pages, firecrawl_find_tools offers free provider discovery. Use firecrawl_find_tools to read a contract that was not returned in full or to browse the catalogue by category. ${ALEXANDRIA_SOURCES_OPT_OUT} If no provider fits, continue with web search or firecrawl_agent. For biomedical, life-science, clinical, or arXiv literature, the firecrawl_research_* tools search a paper index of abstracts and full text; firecrawl_search with categories: ["research"] is a website filter over ordinary web results and reaches different sources. For a programming question (code behaviour, a library or framework, an API contract, an error message, or a known bug), firecrawl_developer_search (or firecrawl_search with categories: ["developer"]) searches an index of public repositories, GitHub issues, merged pull requests, READMEs, and code documentation.
|
|
2337
|
+
var FULL_PROFILE_INSTRUCTIONS = `Firecrawl provides web search, page retrieval, site URL discovery, multi-page collection, structured page data, monitoring, and multi-source research that returns structured data. Match the requested operation to the tool boundary: firecrawl_scrape retrieves one supplied page and can return JSON matching a supplied schema, firecrawl_map enumerates URLs under a site without retrieving their content, and firecrawl_agent runs multi-source research and returns structured data when the URLs are not known or the answer spans several sites (an entity plus its fields, a list, a dataset); its result is read with firecrawl_agent_status. Authenticated firecrawl_search returns web results together with matching Alexandria providers in data.tools. ${ALEXANDRIA_CATALOGUE_SENTENCE} A matching provider can return the same fields across several entities, provenance, exact figures or timestamps, or a large set of records through firecrawl_scrape with its published contract. If web results already answer the question, use them. For the same fields across multiple pages, firecrawl_find_tools offers free provider discovery. Use firecrawl_find_tools to read a contract that was not returned in full or to browse the catalogue by category. ${ALEXANDRIA_SOURCES_OPT_OUT} If no provider fits, continue with web search or firecrawl_agent. For biomedical, life-science, clinical, or arXiv literature, the firecrawl_research_* tools search a paper index of abstracts and full text; firecrawl_search with categories: ["research"] is a website filter over ordinary web results and reaches different sources. For a programming question (code behaviour, a library or framework, an API contract, an error message, or a known bug), firecrawl_developer_search (or firecrawl_search with categories: ["developer"]) searches an index of public repositories, GitHub issues, merged pull requests, READMEs, and code documentation. firecrawl_search with sources: [{type: "alexandria"}] returns compact tool summaries in data.tools; toolDetail: "full" includes contracts, firecrawl_find_tools starts with categories, lists providers, then compact tools, and expands the selected full contract, and firecrawl_scrape with alexandria: [{provider, capability, options}] executes up to ten capabilities and returns their results. Alexandria access needs an API key on a team with it enabled. Provide only the required inputs and account for stated network or external side effects.`;
|
|
2152
2338
|
var KEYLESS_PROFILE_INSTRUCTIONS = `Hosted keyless sessions expose firecrawl_search, firecrawl_scrape, and firecrawl_parse with usage limits. firecrawl_search searches the web. For programming questions, firecrawl_search with categories: ["developer"] searches indexed public repositories, GitHub issues, merged pull requests, repository READMEs, and code documentation. For biomedical, life-science, clinical, or arXiv literature, firecrawl_search with categories: ["research"] filters ordinary web results to research-affiliated websites. firecrawl_scrape retrieves one supplied page and can return JSON matching a supplied schema. firecrawl_parse processes supported local files through its two-phase upload flow. An Authorization bearer API key can provide higher usage limits and expose additional tools, subject to plan, deployment, and team policy, including firecrawl_map for site URL discovery, firecrawl_agent and firecrawl_agent_status for multi-source research that returns structured data when the URLs are not known, firecrawl_research_* for paper-index and repository research, and firecrawl_find_tools as the progressive Alexandria catalogue lookup alongside the Alexandria options of firecrawl_search and firecrawl_scrape for catalogued data providers.`;
|
|
2153
2339
|
var SEARCH_PROFILE_INSTRUCTIONS = ALEXANDRIA_SEARCH_INSTRUCTIONS + ` Firecrawl provides web, developer, and research search, and executes catalogued Alexandria data providers. Use firecrawl_search to find relevant results across the web and specialized indexes; authenticated searches also return matching Alexandria providers in data.tools. firecrawl_find_tools provides catalogue browsing and provider contracts; firecrawl_scrape with an alexandria body executes a selected capability; firecrawl_scrape with a url retrieves one supplied page. For a programming question, firecrawl_developer_search searches indexed public repositories, GitHub issues, merged pull requests, READMEs, and code documentation and returns the matched passages, and skills: "only" narrows it to agent-skill files; firecrawl_search with categories: ["developer"] reaches the same index beside ordinary web results, returning the hits in the web group rather than as passages and offering no skills filter. For a biomedical, life-science, clinical, or arXiv literature question, the firecrawl_research_* tools search the paper index, while categories: ["research"] on firecrawl_search filters ordinary web results to research-affiliated websites. Use the firecrawl_research_* tools to search academic and research literature, expand from anchor papers via the citation graph, and read full-text passages from a specific paper. Search and discovery tools are read-only and return ranked results. Billing: web, developer and research search are billed per request; Alexandria discovery (firecrawl_search with sources ["alexandria"] alone, and firecrawl_find_tools) is free; firecrawl_scrape is billed, as a page retrieval in url mode or at each executed capability's listed price in alexandria mode.`;
|
|
2154
2340
|
var SEARCH_PROFILE_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -2304,6 +2490,20 @@ async function runWithCredentialRecovery(run, requestId, session) {
|
|
|
2304
2490
|
return await run();
|
|
2305
2491
|
} catch (error2) {
|
|
2306
2492
|
if (session?.authType !== "api-key" || !isCoreCredentialRejection(error2)) {
|
|
2493
|
+
const hints = readErrorAgentHints(error2);
|
|
2494
|
+
if (hints) {
|
|
2495
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2496
|
+
throw new UserError2(
|
|
2497
|
+
hints.length ? `${message}
|
|
2498
|
+
|
|
2499
|
+
${agentHintsText(hints)}` : message,
|
|
2500
|
+
{
|
|
2501
|
+
...error2 instanceof UserError2 ? error2.extras : {},
|
|
2502
|
+
error: message,
|
|
2503
|
+
agent_hints: hints
|
|
2504
|
+
}
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2307
2507
|
throw error2;
|
|
2308
2508
|
}
|
|
2309
2509
|
const payload = recoveryPayload("CREDENTIAL_INVALID", requestId);
|
|
@@ -2389,6 +2589,26 @@ function emitActionLog(toolName, status, session, error2, requestId = randomUUID
|
|
|
2389
2589
|
signal: AbortSignal.timeout(1500)
|
|
2390
2590
|
}).catch(() => void 0);
|
|
2391
2591
|
}
|
|
2592
|
+
var AGENT_HINT_LOG_MAX_CHARS = 1e3;
|
|
2593
|
+
function emitAgentHintsLog(toolName, status, hints, session, requestId) {
|
|
2594
|
+
if (process.env.CLOUD_SERVICE !== "true" || !hints) return;
|
|
2595
|
+
console.error(
|
|
2596
|
+
"[MCP_AGENT_HINTS]",
|
|
2597
|
+
JSON.stringify({
|
|
2598
|
+
tool_name: toolName,
|
|
2599
|
+
status,
|
|
2600
|
+
request_id: requestId,
|
|
2601
|
+
auth_type: session.authType ?? "none",
|
|
2602
|
+
// The companion search server shares this process and wrapper.
|
|
2603
|
+
profile: session.profile ?? primaryProfile.id,
|
|
2604
|
+
hint_count: hints.length,
|
|
2605
|
+
hints: hints.map((hint) => hint.slice(0, AGENT_HINT_LOG_MAX_CHARS))
|
|
2606
|
+
})
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
function resultAgentHints(result) {
|
|
2610
|
+
return result && typeof result === "object" ? readAgentHints(result.structuredContent) : void 0;
|
|
2611
|
+
}
|
|
2392
2612
|
function guardHostedTool(tool, { logActions }) {
|
|
2393
2613
|
const keylessTool = KEYLESS_TOOL_NAMES.has(tool.name);
|
|
2394
2614
|
const execute = tool.execute;
|
|
@@ -2458,11 +2678,26 @@ function guardHostedTool(tool, { logActions }) {
|
|
|
2458
2678
|
if (logActions) emitActionLog(tool.name, "error", invocationSession, new UserError2(String(payload.message), payload), requestId, code);
|
|
2459
2679
|
throw new UserError2(String(payload.message), payload);
|
|
2460
2680
|
}
|
|
2461
|
-
const runTool = () =>
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2681
|
+
const runTool = async () => {
|
|
2682
|
+
try {
|
|
2683
|
+
const result = await runWithCredentialRecovery(
|
|
2684
|
+
() => execute(args2, invocationContext),
|
|
2685
|
+
requestId,
|
|
2686
|
+
invocationSession
|
|
2687
|
+
);
|
|
2688
|
+
emitAgentHintsLog(
|
|
2689
|
+
tool.name,
|
|
2690
|
+
result?.isError ? "error" : "success",
|
|
2691
|
+
resultAgentHints(result),
|
|
2692
|
+
invocationSession,
|
|
2693
|
+
requestId
|
|
2694
|
+
);
|
|
2695
|
+
return result;
|
|
2696
|
+
} catch (error2) {
|
|
2697
|
+
emitAgentHintsLog(tool.name, "error", readErrorAgentHints(error2), invocationSession, requestId);
|
|
2698
|
+
throw error2;
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2466
2701
|
if (!logActions) return runTool();
|
|
2467
2702
|
emitActionLog(tool.name, "started", invocationSession, void 0, requestId);
|
|
2468
2703
|
try {
|
|
@@ -2517,6 +2752,7 @@ server.getApp().get("/ready", (context) => {
|
|
|
2517
2752
|
}
|
|
2518
2753
|
return missing.length ? context.json({ ok: false, missing }, 503) : context.json({ ok: true }, 200);
|
|
2519
2754
|
});
|
|
2755
|
+
var warnedMissingAgentHintsInterceptor = false;
|
|
2520
2756
|
function createClient(apiKey) {
|
|
2521
2757
|
const config = {
|
|
2522
2758
|
...process.env.FIRECRAWL_API_URL && {
|
|
@@ -2526,7 +2762,44 @@ function createClient(apiKey) {
|
|
|
2526
2762
|
if (apiKey) {
|
|
2527
2763
|
config.apiKey = apiKey;
|
|
2528
2764
|
}
|
|
2529
|
-
|
|
2765
|
+
const client = new FirecrawlApp(config);
|
|
2766
|
+
const axiosInstance = client.http?.instance;
|
|
2767
|
+
if (axiosInstance?.interceptors?.request?.use) {
|
|
2768
|
+
axiosInstance.interceptors.request.use((request) => {
|
|
2769
|
+
if (typeof request.headers?.set === "function") {
|
|
2770
|
+
request.headers.set("X-Firecrawl-Agent-Hints", "true");
|
|
2771
|
+
} else {
|
|
2772
|
+
request.headers = { ...request.headers ?? {}, ...AGENT_HINTS_HEADERS };
|
|
2773
|
+
}
|
|
2774
|
+
return request;
|
|
2775
|
+
});
|
|
2776
|
+
} else if (!warnedMissingAgentHintsInterceptor) {
|
|
2777
|
+
warnedMissingAgentHintsInterceptor = true;
|
|
2778
|
+
console.warn(
|
|
2779
|
+
"[firecrawl-mcp] SDK request interceptor unavailable; API agent hints may be absent."
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
return client;
|
|
2783
|
+
}
|
|
2784
|
+
async function sdkResultWithAgentHints(client, execute) {
|
|
2785
|
+
const responseInterceptors = client.http?.instance?.interceptors?.response;
|
|
2786
|
+
if (!responseInterceptors?.use) return execute();
|
|
2787
|
+
let hints;
|
|
2788
|
+
const interceptor = responseInterceptors.use((response) => {
|
|
2789
|
+
hints = readAgentHints(response?.data) ?? hints;
|
|
2790
|
+
return response;
|
|
2791
|
+
});
|
|
2792
|
+
try {
|
|
2793
|
+
const result = await execute();
|
|
2794
|
+
return preserveAgentHints(result, { agent_hints: hints });
|
|
2795
|
+
} catch (error2) {
|
|
2796
|
+
if (hints && error2 && typeof error2 === "object" && !readErrorAgentHints(error2)) {
|
|
2797
|
+
error2.agent_hints = hints;
|
|
2798
|
+
}
|
|
2799
|
+
throw error2;
|
|
2800
|
+
} finally {
|
|
2801
|
+
responseInterceptors.eject?.(interceptor);
|
|
2802
|
+
}
|
|
2530
2803
|
}
|
|
2531
2804
|
var SAFE_MODE = process.env.CLOUD_SERVICE === "true";
|
|
2532
2805
|
function getClient(session) {
|
|
@@ -2555,10 +2828,14 @@ function getClient(session) {
|
|
|
2555
2828
|
reason: "delegated_credential_unavailable"
|
|
2556
2829
|
});
|
|
2557
2830
|
}
|
|
2558
|
-
config.headers
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2831
|
+
if (typeof config.headers?.set === "function") {
|
|
2832
|
+
config.headers.set("Authorization", `Bearer ${credential}`);
|
|
2833
|
+
} else {
|
|
2834
|
+
config.headers = {
|
|
2835
|
+
...config.headers ?? {},
|
|
2836
|
+
Authorization: `Bearer ${credential}`
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2562
2839
|
return config;
|
|
2563
2840
|
});
|
|
2564
2841
|
return client;
|
|
@@ -2706,10 +2983,10 @@ var scrapeToolParamsSchema = scrapeParamsSchema.extend({
|
|
|
2706
2983
|
url: z8.string().url().optional(),
|
|
2707
2984
|
timeout: z8.number().int().positive().optional().describe("Execution timeout in milliseconds."),
|
|
2708
2985
|
requestId: z8.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe(
|
|
2709
|
-
"
|
|
2986
|
+
"Idempotency key bound to one Alexandria execution payload. Generated when omitted and returned with the result."
|
|
2710
2987
|
),
|
|
2711
2988
|
alexandria: z8.union([exchangeCallSchema, exchangeCallsSchema]).optional().describe(
|
|
2712
|
-
"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.
|
|
2989
|
+
"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."
|
|
2713
2990
|
),
|
|
2714
2991
|
toolDetail: z8.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."),
|
|
2715
2992
|
domainTools: z8.boolean().optional().describe(
|
|
@@ -2846,6 +3123,7 @@ async function apiPostJson(pathName, body, apiKey, origin) {
|
|
|
2846
3123
|
const response = await fetch(`${resolveApiBaseUrl()}${pathName}`, {
|
|
2847
3124
|
method: "POST",
|
|
2848
3125
|
headers: {
|
|
3126
|
+
...AGENT_HINTS_HEADERS,
|
|
2849
3127
|
"Content-Type": "application/json",
|
|
2850
3128
|
Authorization: `Bearer ${apiKey}`,
|
|
2851
3129
|
...origin ? originHeaders(origin) : {}
|
|
@@ -2862,7 +3140,8 @@ async function apiPostJson(pathName, body, apiKey, origin) {
|
|
|
2862
3140
|
if (!response.ok) {
|
|
2863
3141
|
throw new CoreHttpError(
|
|
2864
3142
|
parsed?.error || parsed?.message || `Firecrawl request failed (HTTP ${response.status})`,
|
|
2865
|
-
response.status
|
|
3143
|
+
response.status,
|
|
3144
|
+
readAgentHints(parsed)
|
|
2866
3145
|
);
|
|
2867
3146
|
}
|
|
2868
3147
|
return parsed;
|
|
@@ -2944,6 +3223,7 @@ async function executeHostedParse(args2, session, log, origin) {
|
|
|
2944
3223
|
session,
|
|
2945
3224
|
origin
|
|
2946
3225
|
);
|
|
3226
|
+
const uploadHints = readAgentHints(uploadJson);
|
|
2947
3227
|
const upload = parseApiData(uploadJson);
|
|
2948
3228
|
if (!upload?.uploadUrl || !upload?.uploadRef) {
|
|
2949
3229
|
throw new Error(
|
|
@@ -2954,6 +3234,7 @@ async function executeHostedParse(args2, session, log, origin) {
|
|
|
2954
3234
|
const uploadForCommand = { ...upload, headers: uploadHeaders };
|
|
2955
3235
|
return asText({
|
|
2956
3236
|
success: true,
|
|
3237
|
+
...uploadHints ? { agent_hints: uploadHints } : {},
|
|
2957
3238
|
mode: "hosted-upload-ref-awaiting-upload",
|
|
2958
3239
|
message: "Hosted MCP cannot read local files. Run the local upload command, then call firecrawl_parse again with uploadRef. No Firecrawl API key is included in this command.",
|
|
2959
3240
|
upload: {
|
|
@@ -3008,7 +3289,7 @@ Firecrawl may serve recently indexed content; set \`maxAge: 0\` for a live fetch
|
|
|
3008
3289
|
|
|
3009
3290
|
On an authenticated session with Alexandria access, \`firecrawl_search\` with \`sources\` unset and \`firecrawl_find_tools\` can discover providers for the same fields across several pages; a matching provider returns typed records in one call. Keyless sessions have no provider matches.
|
|
3010
3291
|
|
|
3011
|
-
Alexandria mode, on an authenticated session with Alexandria access: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url
|
|
3292
|
+
Alexandria mode, on an authenticated session with Alexandria access: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`.
|
|
3012
3293
|
`,
|
|
3013
3294
|
outputSchema: scrapeOutputSchema,
|
|
3014
3295
|
parameters: scrapeToolParamsSchema,
|
|
@@ -3048,14 +3329,17 @@ Alexandria mode, on an authenticated session with Alexandria access: \`alexandri
|
|
|
3048
3329
|
},
|
|
3049
3330
|
session
|
|
3050
3331
|
);
|
|
3051
|
-
return structuredText(json?.data ?? json);
|
|
3332
|
+
return structuredText(preserveAgentHints(json?.data ?? json, json));
|
|
3052
3333
|
}
|
|
3053
3334
|
const client = getClient(session);
|
|
3054
3335
|
const res = await relayTermsRequired(
|
|
3055
|
-
() =>
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3336
|
+
() => sdkResultWithAgentHints(
|
|
3337
|
+
client,
|
|
3338
|
+
() => client.scrape(String(url), {
|
|
3339
|
+
...cleaned,
|
|
3340
|
+
origin
|
|
3341
|
+
})
|
|
3342
|
+
),
|
|
3059
3343
|
{ tool: "firecrawl_scrape" }
|
|
3060
3344
|
);
|
|
3061
3345
|
return structuredText(res);
|
|
@@ -3095,10 +3379,13 @@ Returns matching URLs rather than page bodies. Retrieve one page with \`firecraw
|
|
|
3095
3379
|
const client = getClient(session);
|
|
3096
3380
|
const cleaned = removeEmptyTopLevel(options);
|
|
3097
3381
|
log.info("Mapping URL", { url: String(url) });
|
|
3098
|
-
const res = await
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3382
|
+
const res = await sdkResultWithAgentHints(
|
|
3383
|
+
client,
|
|
3384
|
+
() => client.map(String(url), {
|
|
3385
|
+
...cleaned,
|
|
3386
|
+
origin: requestOrigin(mcpClient, session)
|
|
3387
|
+
})
|
|
3388
|
+
);
|
|
3102
3389
|
return structuredText(res);
|
|
3103
3390
|
}
|
|
3104
3391
|
});
|
|
@@ -3267,7 +3554,7 @@ Scrape one URL and return its content, or execute catalogued Alexandria capabili
|
|
|
3267
3554
|
|
|
3268
3555
|
\`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.
|
|
3269
3556
|
|
|
3270
|
-
Alexandria mode: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url
|
|
3557
|
+
Alexandria mode: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`. Alexandria execution is billed at the capability's listed price and needs a team with Alexandria enabled.
|
|
3271
3558
|
`;
|
|
3272
3559
|
var searchSurfaceScrapeTool = {
|
|
3273
3560
|
...scrapeTool,
|
|
@@ -3340,6 +3627,7 @@ async function keylessPost(path2, body, session, originOverride) {
|
|
|
3340
3627
|
}
|
|
3341
3628
|
}
|
|
3342
3629
|
const headers = {
|
|
3630
|
+
...AGENT_HINTS_HEADERS,
|
|
3343
3631
|
...originHeaders(origin),
|
|
3344
3632
|
"Content-Type": "application/json"
|
|
3345
3633
|
};
|
|
@@ -3359,10 +3647,14 @@ async function keylessPost(path2, body, session, originOverride) {
|
|
|
3359
3647
|
const payload = recoveryPayload(code, session?.requestId, {
|
|
3360
3648
|
retryAfterSeconds: Number.isFinite(json?.retry_after_seconds) && json.retry_after_seconds > 0 ? json.retry_after_seconds : void 0
|
|
3361
3649
|
});
|
|
3650
|
+
const hints = readAgentHints(json);
|
|
3651
|
+
if (hints) payload.agent_hints = hints;
|
|
3362
3652
|
throw new UserError2(String(payload.message), payload);
|
|
3363
3653
|
}
|
|
3364
|
-
throw new
|
|
3365
|
-
json?.error || `Firecrawl request failed (HTTP ${response.status})
|
|
3654
|
+
throw new CoreHttpError(
|
|
3655
|
+
json?.error || `Firecrawl request failed (HTTP ${response.status})`,
|
|
3656
|
+
response.status,
|
|
3657
|
+
readAgentHints(json)
|
|
3366
3658
|
);
|
|
3367
3659
|
}
|
|
3368
3660
|
return json;
|
|
@@ -3383,7 +3675,8 @@ async function getCrawlStatusWithOrigin(client, jobId, origin) {
|
|
|
3383
3675
|
creditsUsed: body.creditsUsed,
|
|
3384
3676
|
expiresAt: body.expiresAt,
|
|
3385
3677
|
next: body.next ?? null,
|
|
3386
|
-
data: initialDocs
|
|
3678
|
+
data: initialDocs,
|
|
3679
|
+
...readAgentHints(body) ? { agent_hints: readAgentHints(body) } : {}
|
|
3387
3680
|
};
|
|
3388
3681
|
}
|
|
3389
3682
|
const docs = initialDocs.slice();
|
|
@@ -3407,7 +3700,8 @@ async function getCrawlStatusWithOrigin(client, jobId, origin) {
|
|
|
3407
3700
|
creditsUsed: body.creditsUsed,
|
|
3408
3701
|
expiresAt: body.expiresAt,
|
|
3409
3702
|
next: null,
|
|
3410
|
-
data: docs
|
|
3703
|
+
data: docs,
|
|
3704
|
+
...readAgentHints(body) ? { agent_hints: readAgentHints(body) } : {}
|
|
3411
3705
|
};
|
|
3412
3706
|
}
|
|
3413
3707
|
async function waitForCrawlCompletionWithOrigin(client, jobId, origin, pollInterval = 2, timeout) {
|
|
@@ -3518,6 +3812,7 @@ Eligibility is limited to successful searches within the feedback age window. Th
|
|
|
3518
3812
|
}
|
|
3519
3813
|
if (querySuggestions) body.querySuggestions = querySuggestions;
|
|
3520
3814
|
const headers = {
|
|
3815
|
+
...AGENT_HINTS_HEADERS,
|
|
3521
3816
|
...originHeaders(origin),
|
|
3522
3817
|
"Content-Type": "application/json"
|
|
3523
3818
|
};
|
|
@@ -3556,7 +3851,8 @@ Eligibility is limited to successful searches within the feedback age window. Th
|
|
|
3556
3851
|
status: response.status,
|
|
3557
3852
|
feedbackErrorCode: parsed?.feedbackErrorCode,
|
|
3558
3853
|
error: parsed?.error ?? `HTTP ${response.status}`,
|
|
3559
|
-
retryable: response.status >= 500
|
|
3854
|
+
retryable: response.status >= 500,
|
|
3855
|
+
...readAgentHints(parsed) ? { agent_hints: readAgentHints(parsed) } : {}
|
|
3560
3856
|
});
|
|
3561
3857
|
}
|
|
3562
3858
|
return structuredText(parsed);
|
|
@@ -3638,6 +3934,7 @@ Returns submission status, feedback ID, and accounting fields.
|
|
|
3638
3934
|
} = args2;
|
|
3639
3935
|
const apiBase = resolveApiBaseUrl();
|
|
3640
3936
|
const headers = {
|
|
3937
|
+
...AGENT_HINTS_HEADERS,
|
|
3641
3938
|
...originHeaders(origin),
|
|
3642
3939
|
"Content-Type": "application/json"
|
|
3643
3940
|
};
|
|
@@ -3691,7 +3988,8 @@ Returns submission status, feedback ID, and accounting fields.
|
|
|
3691
3988
|
status: response.status,
|
|
3692
3989
|
feedbackErrorCode: parsed?.feedbackErrorCode,
|
|
3693
3990
|
error: parsed?.error ?? `HTTP ${response.status}`,
|
|
3694
|
-
retryable: response.status >= 500
|
|
3991
|
+
retryable: response.status >= 500,
|
|
3992
|
+
...readAgentHints(parsed) ? { agent_hints: readAgentHints(parsed) } : {}
|
|
3695
3993
|
});
|
|
3696
3994
|
}
|
|
3697
3995
|
return structuredText(parsed);
|
|
@@ -3838,6 +4136,29 @@ Deprecated compatibility entry point. Use firecrawl_scrape once per known URL wi
|
|
|
3838
4136
|
throw new UserError2(payload.message, payload);
|
|
3839
4137
|
}
|
|
3840
4138
|
});
|
|
4139
|
+
var agentExchangeSchema = z8.strictObject({
|
|
4140
|
+
enabled: z8.boolean().optional().describe("Let the agent call Alexandria providers. On by default."),
|
|
4141
|
+
toolkits: z8.array(z8.string()).max(5).optional().describe("Pin up to 5 providers the agent may use, by slug. Omitted means the whole catalog."),
|
|
4142
|
+
maxCalls: z8.number().int().min(1).max(30).optional().describe("Most provider calls the agent may make in this turn."),
|
|
4143
|
+
requireApproval: z8.boolean().optional().describe(
|
|
4144
|
+
'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.'
|
|
4145
|
+
),
|
|
4146
|
+
approve: z8.strictObject({
|
|
4147
|
+
approvalId: z8.string().uuid(),
|
|
4148
|
+
callIds: z8.array(z8.string()).optional(),
|
|
4149
|
+
always: z8.boolean().optional()
|
|
4150
|
+
}).optional().describe(
|
|
4151
|
+
"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 user explicitly agreed and terms/accept succeeded; 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."
|
|
4152
|
+
),
|
|
4153
|
+
decline: z8.strictObject({ approvalId: z8.string().uuid() }).optional().describe(
|
|
4154
|
+
"Answer no to that pendingApproval. Needs threadId. A declined terms offer keeps those providers out of the rest of the thread."
|
|
4155
|
+
),
|
|
4156
|
+
onTermsRequired: z8.enum(["skip", "ask"]).optional().describe(
|
|
4157
|
+
`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 with the terms/show and terms/accept calls. Each provider digest is string | null and always present; when null, terms/show returns it. There is no auto-accept. Omitted on a follow-up keeps the previous turn's value.`
|
|
4158
|
+
)
|
|
4159
|
+
}).describe(
|
|
4160
|
+
"Alexandria provider settings for this turn, forwarded as the request's exchange object."
|
|
4161
|
+
);
|
|
3841
4162
|
server.addTool({
|
|
3842
4163
|
name: "firecrawl_agent",
|
|
3843
4164
|
annotations: {
|
|
@@ -3850,27 +4171,74 @@ server.addTool({
|
|
|
3850
4171
|
// Gathers information only; does not delete external data or user resources.
|
|
3851
4172
|
},
|
|
3852
4173
|
description: `
|
|
3853
|
-
Run web research that returns structured data when the URLs are not known or the answer spans several sites. Describe the fields you need in \`prompt\`, optionally pass a JSON \`schema\` and seed \`urls\`, and the research agent searches, navigates, reads pages, and returns JSON assembled across sources. Use it to research an entity plus its fields (founders, pricing, contact details), to build lists and datasets (companies, people, products, jobs, papers), and for pages that need navigation or interaction to reach the data.
|
|
4174
|
+
Run web research that returns structured data when the URLs are not known or the answer spans several sites. Describe the fields you need in \`prompt\`, optionally pass a JSON \`schema\` and seed \`urls\`, and the research agent searches, navigates, reads pages, and returns JSON assembled across sources. Use it to research an entity plus its fields (founders, pricing, contact details), to build lists and datasets (companies, people, products, jobs, papers), and for pages that need navigation or interaction to reach the data. Optional \`effort\` sets the reasoning budget, \`maxCredits\` caps spend, and \`strictConstrainToURLs\` keeps the agent to the supplied \`urls\`.
|
|
3854
4175
|
|
|
3855
4176
|
This call returns only a job ID, not the research result. Read the job with \`firecrawl_agent_status\` until it reaches \`completed\` or \`failed\`; a typical research run takes one to three minutes. For one known URL use \`firecrawl_scrape\` (with formats: ["json"] for structured output); for a plain lookup that a results page answers, use \`firecrawl_search\`.
|
|
4177
|
+
|
|
4178
|
+
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.
|
|
4179
|
+
|
|
4180
|
+
The agent only calls Alexandria providers whose data terms the team has accepted. The status result's \`exchange.skippedProviders\` lists gated providers that would have helped. With \`exchange.onTermsRequired\` "ask", a terms offer ends the turn: \`pendingApproval\` (kind "terms") and \`exchange.requiresAction\` carry the \`approvalId\` and the exact terms/show and terms/accept calls. Show the user the terms, get their EXPLICIT consent, run terms/accept through \`firecrawl_scrape\`, then call \`firecrawl_agent\` with the same \`threadId\` and \`exchange.approve: {approvalId}\`. If they decline, send \`exchange.decline: {approvalId}\` instead. Never call terms/accept without that consent; a data request is not consent.
|
|
3856
4181
|
`,
|
|
3857
4182
|
outputSchema: agentOutputSchema,
|
|
3858
4183
|
parameters: z8.object({
|
|
3859
4184
|
prompt: z8.string().min(1).max(1e4),
|
|
3860
4185
|
urls: z8.array(z8.string().url()).optional(),
|
|
3861
|
-
schema: z8.record(z8.string(), z8.any()).optional()
|
|
3862
|
-
|
|
4186
|
+
schema: z8.record(z8.string(), z8.any()).optional(),
|
|
4187
|
+
effort: z8.enum(["low", "medium", "high"]).optional().describe("Reasoning budget for the agent task."),
|
|
4188
|
+
maxCredits: z8.number().int().positive().optional().describe(
|
|
4189
|
+
"Spending limit in credits for this run. Defaults to 2500 on the API."
|
|
4190
|
+
),
|
|
4191
|
+
strictConstrainToURLs: z8.boolean().optional().describe(
|
|
4192
|
+
"If true, agent will only visit URLs provided in the urls array."
|
|
4193
|
+
),
|
|
4194
|
+
threadId: z8.string().uuid().optional().describe(
|
|
4195
|
+
"Continue this thread: the threadId from an earlier firecrawl_agent or firecrawl_agent_status result. Omit to start a new thread."
|
|
4196
|
+
),
|
|
4197
|
+
mode: z8.enum(["extract", "chat"]).optional().describe(
|
|
4198
|
+
`"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.`
|
|
4199
|
+
),
|
|
4200
|
+
exchange: agentExchangeSchema.optional()
|
|
4201
|
+
}).refine(
|
|
4202
|
+
(data) => !(data.exchange?.approve && data.exchange?.decline),
|
|
4203
|
+
{
|
|
4204
|
+
message: "Send exchange.approve or exchange.decline, not both: each answers the pending approval one way.",
|
|
4205
|
+
path: ["exchange"]
|
|
4206
|
+
}
|
|
4207
|
+
).refine(
|
|
4208
|
+
(data) => !(data.exchange?.approve || data.exchange?.decline) || Boolean(data.threadId),
|
|
4209
|
+
{
|
|
4210
|
+
message: "exchange.approve and exchange.decline answer a pending approval on an existing thread: pass that thread's threadId.",
|
|
4211
|
+
path: ["threadId"]
|
|
4212
|
+
}
|
|
4213
|
+
).refine(
|
|
4214
|
+
(data) => !data.exchange?.requireApproval || data.mode === "chat",
|
|
4215
|
+
{
|
|
4216
|
+
// The agent service checks the mode sent on this request, not the
|
|
4217
|
+
// thread's inherited one, and the gateway turns its 400 into a 500.
|
|
4218
|
+
message: 'exchange.requireApproval needs mode: "chat" on the same request, including on a follow-up.',
|
|
4219
|
+
path: ["mode"]
|
|
4220
|
+
}
|
|
4221
|
+
),
|
|
3863
4222
|
execute: async (args2, { session, log, client: mcpClient }) => {
|
|
3864
4223
|
const client = getClient(session);
|
|
3865
4224
|
const a = args2;
|
|
3866
4225
|
log.info("Starting agent", {
|
|
3867
4226
|
prompt: a.prompt.substring(0, 100),
|
|
3868
|
-
urlCount: Array.isArray(a.urls) ? a.urls.length : 0
|
|
4227
|
+
urlCount: Array.isArray(a.urls) ? a.urls.length : 0,
|
|
4228
|
+
threadId: a.threadId ?? null
|
|
3869
4229
|
});
|
|
3870
4230
|
const agentBody = removeEmptyTopLevel({
|
|
3871
4231
|
prompt: a.prompt,
|
|
3872
4232
|
urls: a.urls,
|
|
3873
|
-
schema: a.schema || void 0
|
|
4233
|
+
schema: a.schema || void 0,
|
|
4234
|
+
effort: a.effort,
|
|
4235
|
+
maxCredits: a.maxCredits,
|
|
4236
|
+
strictConstrainToURLs: a.strictConstrainToURLs,
|
|
4237
|
+
threadId: a.threadId,
|
|
4238
|
+
mode: a.mode,
|
|
4239
|
+
// Forwarded verbatim: the agent service owns the defaults and the
|
|
4240
|
+
// per-thread inheritance.
|
|
4241
|
+
exchange: a.exchange
|
|
3874
4242
|
});
|
|
3875
4243
|
const res = await client.startAgent({
|
|
3876
4244
|
...agentBody,
|
|
@@ -4076,7 +4444,10 @@ Set \`redactPII\` to request redaction of personally identifiable information in
|
|
|
4076
4444
|
});
|
|
4077
4445
|
form.append("file", blob, filename);
|
|
4078
4446
|
form.append("options", JSON.stringify(optionsPayload));
|
|
4079
|
-
const headers = {
|
|
4447
|
+
const headers = {
|
|
4448
|
+
...AGENT_HINTS_HEADERS,
|
|
4449
|
+
...originHeaders(origin)
|
|
4450
|
+
};
|
|
4080
4451
|
const credential = credentialForOutboundRequest(session);
|
|
4081
4452
|
if (credential) {
|
|
4082
4453
|
headers["Authorization"] = `Bearer ${credential}`;
|
|
@@ -4094,8 +4465,15 @@ Set \`redactPII\` to request redaction of personally identifiable information in
|
|
|
4094
4465
|
});
|
|
4095
4466
|
const responseText = await response.text();
|
|
4096
4467
|
if (!response.ok) {
|
|
4097
|
-
|
|
4098
|
-
|
|
4468
|
+
let parsed;
|
|
4469
|
+
try {
|
|
4470
|
+
parsed = JSON.parse(responseText);
|
|
4471
|
+
} catch {
|
|
4472
|
+
}
|
|
4473
|
+
throw new CoreHttpError(
|
|
4474
|
+
`Parse request failed with status ${response.status}: ${responseText}`,
|
|
4475
|
+
response.status,
|
|
4476
|
+
readAgentHints(parsed)
|
|
4099
4477
|
);
|
|
4100
4478
|
}
|
|
4101
4479
|
try {
|