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/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 logical execution; reuse it only for a retry of the identical payload."),
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 { expand, ...selectors } = item.next.options;
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
- constructor(message, status) {
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 = originHeaders(origin);
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 structuredText(res);
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 structuredText(res);
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 structuredText(res);
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 structuredText(res);
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 structuredText(res);
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 structuredText(res);
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 structuredText(res);
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 structuredText(res);
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
- originHeaders(requestOrigin(mcpClient, session))
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
- originHeaders(requestOrigin(mcpClient, session))
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
- originHeaders(requestOrigin(mcpClient, session))
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
- originHeaders(requestOrigin(mcpClient, session))
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
- async function introspectToken(token, expectedResource) {
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) throw termsRequiredError(action, context);
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. Alexandria requestId identifies one logical execution; repeated attempts of the identical payload require the same ID, including pending or uncertain executions. 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. A terms-gated Alexandria provider fails with code THIRD_PARTY_DATA_TERMS_REQUIRED and a requiresAction.url. Through firecrawl_scrape, terms/show displays the agreement and terms/accept records acceptance; acceptance requires explicit user authorization for the reviewed version and digest and confirmed:true. A data request does not authorize acceptance. The dashboard URL provides an alternative for an organization admin. A retained result provides nextTool access without repeating a successful provider call. Provide only the required inputs and account for stated network or external side effects.`;
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 = () => runWithCredentialRecovery(
2462
- () => execute(args2, invocationContext),
2463
- requestId,
2464
- invocationSession
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
- return new FirecrawlApp(config);
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
- ...config.headers ?? {},
2560
- Authorization: `Bearer ${credential}`
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
- "Identifies one logical Alexandria execution; generated when omitted and returned with the result. Repeated attempts of the identical payload require the same ID. A new ID cannot reconcile a pending or uncertain execution. A caller-supplied ID supports recovery if no response is received. Errors relay a code and may include chargeId. request_in_flight (409) means the execution is pending; request_unresolved (503) requires reconciliation under the same ID; duplicate_request (409) means the ID belongs to a different payload; unknown_provider (404), insufficient_credits (402) and billing_unavailable (503) mean nothing executed."
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. nextTool identifies access to retained results without repeating a successful provider call. Requires an API key on a team with Alexandria enabled. THIRD_PARTY_DATA_TERMS_REQUIRED (403) means execution is blocked by provider terms, with requiresAction.url for review by an organization admin. Through this tool, terms/show displays the agreement and terms/accept records acceptance; acceptance requires explicit user authorization for the reviewed version and digest and confirmed:true. A data request does not authorize acceptance. Provider execution remains blocked until acceptance is confirmed."
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\`; the \`alexandria\` and \`requestId\` parameters describe batching, retries, errors and provider terms.
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
- () => client.scrape(String(url), {
3056
- ...cleaned,
3057
- origin
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 client.map(String(url), {
3099
- ...cleaned,
3100
- origin: requestOrigin(mcpClient, session)
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\`; the \`alexandria\` and \`requestId\` parameters describe batching, retries, errors and provider terms. Alexandria execution is billed at the capability's listed price and needs a team with Alexandria enabled.
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 Error(
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 = { ...originHeaders(origin) };
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
- throw new Error(
4098
- `Parse request failed with status ${response.status}: ${responseText}`
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 {