firecrawl-mcp 3.25.4 → 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
@@ -45,10 +45,9 @@ var alexandriaFeedbackFields = {
45
45
  )
46
46
  ).max(20).optional()
47
47
  };
48
- var ALEXANDRIA_FEEDBACK_GUIDANCE = 'After an Alexandria task, whether a capability ran or discovery found nothing for the website, call firecrawl_feedback once per website with endpoint "alexandria": requestedWebsite (the url the user needed data from and the requestedFunctionality they needed), a rating, a rationale from observed results, and any providerFeedback or capabilityFeedback gaps. It is free, needs no job ID, has no deadline, and follows the answer rather than replacing it.';
49
48
  var ALEXANDRIA_FEEDBACK_HINT = {
50
49
  name: "firecrawl_feedback",
51
- when: "Once per website after the task is complete, including when no provider covered the site. Free; no job ID or deadline.",
50
+ when: "Optional after task completion; at most once per website, including uncovered sites. Free; no job ID or deadline.",
52
51
  arguments: {
53
52
  endpoint: "alexandria",
54
53
  rating: "<good | partial | bad>",
@@ -86,14 +85,367 @@ import { readFile } from "fs/promises";
86
85
  import { createRequire as createRequire2 } from "module";
87
86
  import { randomUUID } from "crypto";
88
87
  import path from "path";
89
- import { z as z7 } from "zod";
88
+ import { z as z8 } from "zod";
90
89
 
91
- // src/alexandria.ts
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
+
129
+ // src/tool-output.ts
92
130
  import { z as z2 } from "zod";
131
+ function isRecord(value) {
132
+ return typeof value === "object" && value !== null && !Array.isArray(value);
133
+ }
134
+ function withStructured(text, data) {
135
+ return isRecord(data) ? { content: [{ type: "text", text }], structuredContent: data } : { content: [{ type: "text", text }] };
136
+ }
137
+ function structuredText(data) {
138
+ return withStructured(JSON.stringify(data, null, 2), data);
139
+ }
140
+ function structuredCompact(data) {
141
+ return withStructured(JSON.stringify(data), data);
142
+ }
143
+ function structuredJsonText(text) {
144
+ try {
145
+ return withStructured(text, JSON.parse(text));
146
+ } catch {
147
+ return { content: [{ type: "text", text }] };
148
+ }
149
+ }
150
+ var str = (description) => z2.string().nullable().optional().describe(description);
151
+ var num = (description) => z2.number().nullable().optional().describe(description);
152
+ var bool = (description) => z2.boolean().nullable().optional().describe(description);
153
+ var unknown = (description) => z2.unknown().optional().describe(description);
154
+ var success = bool("Whether the API call succeeded.");
155
+ var error = unknown("Error message or error object when the call did not succeed.");
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
+ };
160
+ var alexandriaEnvelope = {
161
+ requestId: str("Identifier of this Alexandria execution."),
162
+ scrape_id: str("Identifier of the underlying scrape."),
163
+ receipt: unknown("Billing receipt for the execution."),
164
+ creditsCost: num("Credits this call consumed."),
165
+ delivery: str("`retained` when the full result stayed server-side instead of being inlined."),
166
+ responseBytes: num("Size of the full result in bytes."),
167
+ estimatedTokens: num("Estimated token cost of the full result."),
168
+ tokenEstimateMethod: str("How `estimatedTokens` was derived."),
169
+ inlineTokenBudget: num("Token budget above which a result is retained rather than inlined."),
170
+ workspaceId: str("Workspace holding a retained result, for inspection through virtual Bash."),
171
+ idleTtlSeconds: num("Seconds a retained workspace stays available while idle."),
172
+ message: str("Guidance that accompanies the result."),
173
+ nextTool: unknown("A follow-up tool call (`{name, arguments}`) that continues or inspects this result."),
174
+ feedbackTool: unknown("Pointer to the feedback tool for reporting how this result served the task.")
175
+ };
176
+ var scrapeOutputSchema = z2.object({
177
+ ...agentHints,
178
+ // URL mode: the scraped document, with the formats that were requested.
179
+ markdown: str("Page content as markdown."),
180
+ html: str("Processed HTML of the page."),
181
+ rawHtml: str("Unprocessed HTML of the page."),
182
+ summary: str("Summary of the page content."),
183
+ links: unknown("Links found on the page."),
184
+ screenshot: unknown("Screenshot of the page."),
185
+ images: unknown("Images found on the page."),
186
+ audio: unknown("Audio extracted from the page."),
187
+ video: unknown("Video extracted from the page."),
188
+ answer: str("Targeted answer to the question that was asked of the page."),
189
+ highlights: unknown("Highlighted passages from the page."),
190
+ pages: unknown("Physical PDF pages, when `parsers[].pages` is set."),
191
+ blocks: unknown("Typed PDF layout blocks, when `parsers[].blocks` is set."),
192
+ product: unknown("Product data extracted from the page."),
193
+ menu: unknown("Menu data extracted from the page."),
194
+ json: unknown("Structured data matching the requested JSON schema or prompt."),
195
+ attributes: unknown("Values collected by the requested attribute selectors."),
196
+ actions: unknown("Results of the browser actions that ran during the scrape."),
197
+ changeTracking: unknown("Change-tracking comparison against the previous scrape."),
198
+ branding: unknown("Branding data extracted from the page."),
199
+ metadata: unknown("Page metadata; authenticated responses can include `metadata.scrapeId` for scrape feedback."),
200
+ tools: unknown("Domain-matched Alexandria tools for the page, when `domainTools` is set."),
201
+ warning,
202
+ // Alexandria mode and error envelopes.
203
+ success,
204
+ error,
205
+ data: unknown("Alexandria mode: per-capability results in `data.alexandria`, each with `data`, `records`, or an `error`."),
206
+ ...alexandriaEnvelope
207
+ }).describe("A scraped document, or the Alexandria execution envelope when `alexandria` was passed.");
208
+ var mapOutputSchema = z2.object({
209
+ ...agentHints,
210
+ links: unknown("URLs discovered under the website."),
211
+ success,
212
+ error,
213
+ warning,
214
+ id: str("Identifier of the map run, for optional map feedback.")
215
+ }).describe("URLs indexed under the requested website.");
216
+ var searchOutputSchema = z2.object({
217
+ ...agentHints,
218
+ success,
219
+ data: unknown("Ranked results grouped by source, such as `web`, `news`, `images`, and `alexandria`."),
220
+ error,
221
+ warning,
222
+ id: str("Search identifier, for optional `firecrawl_search_feedback`."),
223
+ creditsUsed: num("Credits this search consumed."),
224
+ tools: unknown("Domain-matched Alexandria tools for the results."),
225
+ nextTool: unknown("A follow-up tool call that continues this search."),
226
+ feedbackTool: unknown("Pointer to the feedback tool for this search.")
227
+ }).describe("Ranked search results grouped by source.");
228
+ var findToolsOutputSchema = z2.object({
229
+ ...agentHints,
230
+ success,
231
+ data: unknown("Discovery page in `data.alexandria[0].data`, with `level`, `items`, `total`, and an optional `nextTool`."),
232
+ error,
233
+ ...alexandriaEnvelope
234
+ }).describe("One page of the Alexandria catalogue: categories, providers, tools, or one contract.");
235
+ var feedbackOutputSchema = z2.object({
236
+ ...agentHints,
237
+ success,
238
+ error,
239
+ status: num("HTTP status when the submission was rejected."),
240
+ feedbackErrorCode: str("Machine-readable reason the submission was rejected."),
241
+ retryable: bool("Whether retrying the submission can succeed."),
242
+ message: str("Human-readable result of the submission."),
243
+ feedbackId: str("Identifier of the recorded feedback."),
244
+ creditsRefunded: num("Credits refunded for this submission, when eligible."),
245
+ creditsRefundedToday: num("Credits refunded to the team so far this UTC day."),
246
+ dailyRefundCap: num("The team's daily refund cap in credits."),
247
+ dailyCapReached: bool("Whether the daily refund cap is reached; further feedback today still records but refunds nothing."),
248
+ alreadySubmitted: bool("Whether feedback for this job was already recorded."),
249
+ warning,
250
+ data: unknown("Payload returned with the accepted feedback."),
251
+ raw: str("Response body when it was not JSON.")
252
+ }).describe("Result of submitting feedback.");
253
+ var crawlOutputSchema = z2.object({
254
+ ...agentHints,
255
+ success,
256
+ error,
257
+ warning,
258
+ id: str("Crawl identifier, for `firecrawl_check_crawl_status`."),
259
+ status: str("Crawl state, such as `scraping`, `completed`, or `failed`."),
260
+ completed: num("Pages crawled so far."),
261
+ total: num("Pages the crawl expects to visit."),
262
+ creditsUsed: num("Credits the crawl consumed."),
263
+ expiresAt: str("When the crawl results expire."),
264
+ next: str("URL of the next page of results, when the data is paginated."),
265
+ url: str("The URL the crawl started from."),
266
+ data: unknown("Scraped documents collected by the crawl.")
267
+ }).describe("Crawl job state and the documents collected so far.");
268
+ var agentOutputSchema = z2.object({
269
+ ...agentHints,
270
+ success,
271
+ error,
272
+ id: str("Agent job identifier, for `firecrawl_agent_status`."),
273
+ status: str("Job state at submission time."),
274
+ threadId: str("Research thread this job belongs to."),
275
+ threadTurn: num("Turn number of this job within its thread.")
276
+ }).describe("The queued research job. The result is read with `firecrawl_agent_status`.");
277
+ var agentStatusOutputSchema = z2.object({
278
+ ...agentHints,
279
+ success,
280
+ error,
281
+ id: str("Agent job identifier."),
282
+ status: str("Job state: `processing` is non-terminal; `completed` and `failed` are terminal."),
283
+ progress: unknown("Progress information for a running job."),
284
+ data: unknown("Research result, present once the job has completed."),
285
+ creditsUsed: num("Credits the job consumed."),
286
+ sources: unknown("Sources the agent read."),
287
+ expiresAt: str("When the job result expires and can no longer be read."),
288
+ model: str("Agent model that ran the job."),
289
+ mode: str("Agent mode the job ran in."),
290
+ threadId: str("Research thread this job belongs to."),
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
+ )
300
+ }).describe("Progress or final result of a research agent job.");
301
+ var interactOutputSchema = z2.object({
302
+ ...agentHints,
303
+ success,
304
+ error: unknown("Why the interaction or the session could not run."),
305
+ url: str("The URL the session was opened from, when opening failed."),
306
+ scrapeId: str("Session identifier; pass it to reuse the session or to `firecrawl_interact_stop`."),
307
+ result: unknown("Interaction result, when it is not an object of its own."),
308
+ output: unknown("Execution output."),
309
+ stdout: str("Standard output of the executed code."),
310
+ stderr: str("Standard error of the executed code."),
311
+ exitCode: num("Exit status of the executed code."),
312
+ data: unknown("Payload returned by the interaction."),
313
+ sessionUrl: str("URL for viewing the live session."),
314
+ liveUrl: str("URL for viewing the live session.")
315
+ }).describe("Result of a browser interaction, with the session identifier to continue or stop it.");
316
+ var interactStopOutputSchema = z2.object({
317
+ ...agentHints,
318
+ success,
319
+ error,
320
+ message: str("Confirmation that the session was stopped."),
321
+ scrapeId: str("Session that was stopped.")
322
+ }).describe("Confirmation that the interact session was stopped.");
323
+ var parseOutputSchema = z2.object({
324
+ ...agentHints,
325
+ success,
326
+ error,
327
+ warning,
328
+ mode: str("Which phase of the hosted flow produced this response."),
329
+ message: str("Guidance for the next call."),
330
+ data: unknown("Parsed document content; can include `data.metadata.scrapeId` for parse feedback."),
331
+ upload: z2.object({
332
+ command: str("Local command that performs the upload. It carries no Firecrawl API key."),
333
+ method: str("HTTP method for the upload."),
334
+ headers: unknown("Headers the upload request must send."),
335
+ fields: unknown("Form fields the upload request must send, for a POST upload."),
336
+ uploadUrl: str("URL to upload the local file to."),
337
+ uploadRef: str("Reference to pass back on the second call."),
338
+ expiresAt: str("When the upload URL expires."),
339
+ maxSizeBytes: num("Largest file the upload URL accepts.")
340
+ }).optional().describe("Hosted phase one: how to upload the local file."),
341
+ nextToolCall: unknown("Hosted phase one: the second `firecrawl_parse` call to make once the upload succeeds, as `{name, arguments}`."),
342
+ notes: z2.array(z2.string()).nullable().optional().describe("Hosted phase one: constraints on completing the upload flow."),
343
+ raw: str("Response body when it was not JSON.")
344
+ }).describe("Parsed document content, or the upload instructions for the hosted two-call flow.");
345
+ var deprecatedToolOutputSchema = z2.object({
346
+ code: str("Always `DEPRECATED_TOOL`."),
347
+ message: str("Why the tool is unavailable and what to call instead."),
348
+ replacement: unknown("The replacement tool, with instructions and example arguments."),
349
+ docs_url: str("Documentation for the replacement.")
350
+ }).describe("A pointer to the replacement tool. This entry point is deprecated and always errors.");
351
+ var monitorOutputSchema = z2.object({
352
+ ...agentHints,
353
+ success,
354
+ error,
355
+ data: unknown("The monitor record: configuration, schedule, targets, and current state.")
356
+ }).describe("One monitor record.");
357
+ var monitorListOutputSchema = z2.object({
358
+ ...agentHints,
359
+ success,
360
+ error,
361
+ data: unknown("One page of monitor records."),
362
+ pagination: unknown("Pagination metadata for the listing.")
363
+ }).describe("One page of monitors for the authenticated account.");
364
+ var monitorDeleteOutputSchema = z2.object({
365
+ ...agentHints,
366
+ success,
367
+ error,
368
+ data: unknown("Deletion details, when the API returns any.")
369
+ }).describe("Deletion status for the monitor.");
370
+ var monitorRunOutputSchema = z2.object({
371
+ ...agentHints,
372
+ success,
373
+ error,
374
+ data: unknown("The queued check.")
375
+ }).describe("The check queued outside the monitor schedule.");
376
+ var monitorChecksOutputSchema = z2.object({
377
+ ...agentHints,
378
+ success,
379
+ error,
380
+ data: unknown("One page of check summaries."),
381
+ pagination: unknown("Pagination metadata for the listing.")
382
+ }).describe("One page of historical checks for the monitor.");
383
+ var monitorCheckOutputSchema = z2.object({
384
+ ...agentHints,
385
+ success,
386
+ error,
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."),
388
+ pagination: unknown("Pagination metadata for the page results."),
389
+ next: str("URL of the next page of results, when more exist.")
390
+ }).describe("One monitor check with its page-level diff results.");
391
+ var paperSchema = z2.looseObject({
392
+ paperId: str("Canonical paper identifier."),
393
+ primaryId: str("Display identifier, ordered for citation and fetch use."),
394
+ ids: unknown("Source identifiers by namespace, such as `arxiv`, `doi`, or `pmid`."),
395
+ title: str("Paper title."),
396
+ abstract: str("Paper abstract."),
397
+ authors: unknown("Authors, as a comma-joined string or as `{name, affiliation}` entries."),
398
+ categories: z2.array(z2.string()).nullable().optional().describe("Paper categories, such as `cs.LG`."),
399
+ createdDate: str("Date the paper was first indexed or published."),
400
+ updateDate: str("Date the paper was last updated.")
401
+ });
402
+ var researchSearchOutputSchema = z2.object({
403
+ ...agentHints,
404
+ results: z2.array(paperSchema).describe("Ranked papers, in the order the text block lists them.")
405
+ }).describe("Ranked papers with canonical IDs, titles, authors, and abstracts.");
406
+ var researchPaperOutputSchema = z2.object({
407
+ ...agentHints,
408
+ paper: paperSchema.optional().describe("Canonical metadata for the requested paper, absent when it was not found.")
409
+ }).describe("Canonical metadata for one paper.");
410
+ var researchRelatedOutputSchema = z2.object({
411
+ ...agentHints,
412
+ results: z2.array(paperSchema).describe("Ranked citation-graph candidates."),
413
+ poolSize: z2.number().describe("Number of candidates evaluated before ranking."),
414
+ note: str("Note about how the candidates were produced.")
415
+ }).describe("Ranked citation-graph candidates and the size of the evaluated pool.");
416
+ var researchReadOutputSchema = z2.object({
417
+ ...agentHints,
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.")
419
+ }).describe("Full-text passages from one paper.");
420
+ var developerSearchOutputSchema = z2.object({
421
+ ...agentHints,
422
+ results: z2.array(
423
+ z2.looseObject({
424
+ id: str("Stable result id, such as `issue:owner/repo#123` or `doc:<hash>`; its prefix gives the source type."),
425
+ url: str("Source URL."),
426
+ title: str("Result title."),
427
+ passages: z2.array(z2.looseObject({ text: str("Matched passage, in markdown.") })).optional().describe("Matched passages.")
428
+ })
429
+ ).describe("Ranked results, in the order the text block lists them.")
430
+ }).describe("Ranked developer-index results with their matched passages.");
431
+ var creditUsageOutputSchema = z2.object({
432
+ // Current view.
433
+ remainingCredits: num("Credits left in the current billing period; can exceed `planCredits` when extra credits were purchased or granted."),
434
+ planCredits: num("Credits included in the plan for the period."),
435
+ billingPeriodStart: str("Start of the current billing period; null when the billing provider reports no period."),
436
+ billingPeriodEnd: str("End of the current billing period; null when the billing provider reports no period."),
437
+ // Historical view.
438
+ success,
439
+ error,
440
+ periods: unknown("Historical periods sorted by start date, each with `startDate`, `endDate`, `creditsUsed`, and `apiKey` when broken down by key.")
441
+ }).describe("The team credit balance, or historical credit consumption by period.");
442
+
443
+ // src/alexandria.ts
444
+ import { z as z3 } from "zod";
93
445
  var catalogueTypes = ["alexandria", "exchange"];
94
- var searchSourceSchema = z2.union([
95
- z2.enum(["web", "images", "news", ...catalogueTypes]),
96
- z2.object({ type: z2.enum(["web", "images", "news", ...catalogueTypes]) }).strict()
446
+ var searchSourceSchema = z3.union([
447
+ z3.enum(["web", "images", "news", ...catalogueTypes]),
448
+ z3.object({ type: z3.enum(["web", "images", "news", ...catalogueTypes]) }).strict()
97
449
  ]);
98
450
  function hasAlexandria(sources) {
99
451
  return Array.isArray(sources) && sources.some(
@@ -116,25 +468,25 @@ function normalizeSearchSources(sources) {
116
468
  function searchQueryIsValid(args2) {
117
469
  return !!args2.query?.trim();
118
470
  }
119
- var findToolsSchema = z2.object({
120
- query: z2.string().trim().min(1).max(2e3).optional().describe("Semantic lookup of tools for the data you need. Selectors constrain the search."),
121
- urls: z2.array(
122
- z2.string().url().regex(/^https?:\/\//)
471
+ var findToolsSchema = z3.object({
472
+ query: z3.string().trim().min(1).max(2e3).optional().describe("Semantic lookup of tools for the data you need. Selectors constrain the search."),
473
+ urls: z3.array(
474
+ z3.string().url().regex(/^https?:\/\//)
123
475
  ).min(1).max(100).optional(),
124
- providers: z2.array(z2.string().min(1)).min(1).max(50).optional().describe("Provider IDs returned by category browsing. Lists compact tools by default."),
125
- categories: z2.array(z2.string().min(1)).min(1).max(50).optional().describe("Category IDs returned by an empty call. Lists providers by default."),
126
- groups: z2.array(z2.string().min(1)).min(1).max(50).optional().describe("Optional group IDs for explicit group browsing."),
127
- capabilities: z2.array(z2.string().min(1)).min(1).max(50).optional().describe("Exact capability IDs. A selected capability expands its complete contract by default."),
128
- level: z2.enum(["categories", "providers", "groups", "tools"]).optional(),
129
- expand: z2.array(z2.enum(["options", "response", "examples"])).optional().describe('Use ["options", "response"] for inputs and output shape without example payloads; [] keeps results compact. Omit for full contracts on selected capabilities.'),
130
- limit: z2.number().int().min(1).max(100).optional(),
131
- offset: z2.number().int().nonnegative().optional()
476
+ providers: z3.array(z3.string().min(1)).min(1).max(50).optional().describe("Provider IDs returned by category browsing. Lists compact tools by default."),
477
+ categories: z3.array(z3.string().min(1)).min(1).max(50).optional().describe("Category IDs returned by an empty call. Lists providers by default."),
478
+ groups: z3.array(z3.string().min(1)).min(1).max(50).optional().describe("Optional group IDs for explicit group browsing."),
479
+ capabilities: z3.array(z3.string().min(1)).min(1).max(50).optional().describe("Exact capability IDs. A selected capability expands its complete contract by default."),
480
+ level: z3.enum(["categories", "providers", "groups", "tools"]).optional(),
481
+ expand: z3.array(z3.enum(["options", "response", "examples"])).optional().describe('Use ["options", "response"] for inputs and output shape without example payloads; [] keeps results compact. Omit for full contracts on selected capabilities.'),
482
+ limit: z3.number().int().min(1).max(100).optional(),
483
+ offset: z3.number().int().nonnegative().optional()
132
484
  }).strict();
133
485
  var ALEXANDRIA_CATALOGUE_VERTICALS = "companies, people, jobs, finance and filings, public records and government spending, real estate, places and restaurants, retail and prices, package registries and developer data, news, research, and more";
134
486
  var ALEXANDRIA_CATALOGUE_SENTENCE = "Alexandria is Firecrawl's catalogue of data providers and workflows across " + ALEXANDRIA_CATALOGUE_VERTICALS + "; providers return typed, sourced records through published contracts.";
135
- var ALEXANDRIA_SOURCES_OPT_OUT = 'Passing sources without alexandria in it (for example ["web"] or ["news"]) excludes Alexandria provider matches; omit sources unless you specifically need web-only or news-only results, or include "alexandria" alongside them.';
487
+ var ALEXANDRIA_SOURCES_OPT_OUT = 'A search with sources: ["web"] omits semantic provider discovery; domainTools: true can still return website-matched tools. Web-only results use domainTools: false.';
136
488
  var ALEXANDRIA_SEARCH_LEAD = "Authenticated search also returns matching Alexandria data providers in data.tools (" + ALEXANDRIA_CATALOGUE_VERTICALS + "). Prefer a provider over scraping pages when the task needs the same fields across several entities, exact figures or timestamps, provenance, or many records; use web results when they already answer the question. " + ALEXANDRIA_SOURCES_OPT_OUT;
137
- var ALEXANDRIA_CONTRACT_GUIDANCE = "Read the selected contract before executing: required inputs and requiresOneOf groups (at least one member per group), example.request/example.response when present, and response.key (do not assume records is the result key). Follow the declared pagination input and response cursor, preserving filters; catalogue next is separate from provider pagination.";
489
+ var ALEXANDRIA_CONTRACT_GUIDANCE = "The selected contract marks required inputs and any requiresOneOf groups (at least one member per group); it may include example.request/example.response and response.key (which may differ from records). Where pagination is declared, its fields govern paging with the same filters; catalogue next is separate from provider pagination.";
138
490
  function findToolsOptions(args2) {
139
491
  const level = args2.level ?? (args2.query || args2.capabilities?.length || args2.providers?.length || args2.groups?.length || args2.urls?.length ? "tools" : args2.categories?.length ? "providers" : "categories");
140
492
  if (level === "categories" && (args2.query || args2.providers?.length || args2.categories?.length || args2.groups?.length || args2.capabilities?.length || args2.urls?.length || args2.expand !== void 0)) {
@@ -155,7 +507,8 @@ function withFindToolsNavigation(envelope) {
155
507
  const link = (next) => next?.provider === "firecrawl" && next?.capability === "find-tools" && next.options ? { name: "firecrawl_find_tools", arguments: next.options } : void 0;
156
508
  for (const item of page.items) {
157
509
  if (page.level === "providers" && link(item.next)) {
158
- const { expand, ...selectors } = item.next.options;
510
+ const selectors = { ...item.next.options };
511
+ delete selectors.expand;
159
512
  item.next = { ...item.next, options: { ...selectors, level: "tools" } };
160
513
  }
161
514
  const nextTool = link(item.next);
@@ -164,7 +517,7 @@ function withFindToolsNavigation(envelope) {
164
517
  if (link(page.next)) page.nextTool = link(page.next);
165
518
  return envelope;
166
519
  }
167
- var ALEXANDRIA_SEARCH_INSTRUCTIONS = 'Authenticated search combines web results, semantic tool summaries and domain matches. Use sources: ["alexandria"] for semantic tools only, or sources: ["web"] for web only. domainTools: false disables domain matching. Tool matches describe available structured-data capabilities, not executed data. toolDetail: "compact" (default) returns only provider, capability and description; "summary" adds metadata; "full" includes their input and output contracts. Execute a matched tool through firecrawl_scrape with an alexandria body; use firecrawl_find_tools to browse the catalogue or read a full contract.';
520
+ var ALEXANDRIA_SEARCH_INSTRUCTIONS = 'Authenticated search combines web results, semantic tool summaries and domain matches. Use sources: ["alexandria"] for semantic tools only. ' + ALEXANDRIA_SOURCES_OPT_OUT + ' Tool matches describe available structured-data capabilities, not executed data. toolDetail: "compact" (default) returns only provider, capability and description; "summary" adds metadata; "full" includes their input and output contracts. firecrawl_scrape with an alexandria body executes a selected capability; firecrawl_find_tools provides catalogue browsing and full contracts.';
168
521
 
169
522
  // src/alexandria-output.ts
170
523
  var INLINE_TOKEN_BUDGET = 2e4;
@@ -231,7 +584,7 @@ async function alexandriaOutput(payload, calls, post, recoveryRequestId, extras
231
584
  }
232
585
 
233
586
  // src/developer.ts
234
- import { z as z3 } from "zod";
587
+ import { z as z4 } from "zod";
235
588
 
236
589
  // src/origin.ts
237
590
  import { createRequire } from "module";
@@ -297,12 +650,13 @@ Search an index of public repositories, GitHub issues, merged pull requests, rep
297
650
 
298
651
  Returns ranked results with an ID, source type, URL, title, and the matched passages in markdown.
299
652
  `,
300
- parameters: z3.object({
301
- query: z3.string().min(1).describe(
653
+ outputSchema: developerSearchOutputSchema,
654
+ parameters: z4.object({
655
+ query: z4.string().min(1).describe(
302
656
  "Natural-language developer question or search phrase, including the library, error message, or API involved when relevant."
303
657
  ),
304
- k: z3.number().int().min(1).max(100).optional().describe("Number of ranked results to return (default 10)."),
305
- skills: z3.enum(["only"]).optional().describe('Set to "only" to search only agent-skill files.')
658
+ k: z4.number().int().min(1).max(100).optional().describe("Number of ranked results to return (default 10)."),
659
+ skills: z4.enum(["only"]).optional().describe('Set to "only" to search only agent-skill files.')
306
660
  }),
307
661
  execute: async (args2, { session, client: mcpClient }) => {
308
662
  const { query, k, skills } = args2;
@@ -312,7 +666,8 @@ Returns ranked results with an ID, source type, URL, title, and the matched pass
312
666
  if (skills != null) params.append("skills", skills);
313
667
  const client = getClient2(session);
314
668
  const res = await client.http.get(`${BASE}?${params.toString()}`, originHeaders(requestOrigin(mcpClient, session)));
315
- return fmtDeveloper(res.data?.results);
669
+ const results = res.data?.results ?? [];
670
+ return withAgentHints(withStructured(fmtDeveloper(results), { results }), res.data, true);
316
671
  }
317
672
  });
318
673
  }
@@ -332,17 +687,19 @@ function extractSingleTrustedClientIp(rawForwardedFor) {
332
687
  }
333
688
 
334
689
  // src/monitor.ts
335
- import { z as z4 } from "zod";
690
+ import { z as z5 } from "zod";
336
691
 
337
692
  // src/session-credential.ts
338
693
  import { createHmac } from "crypto";
339
694
  var managedOAuthApiKey = /* @__PURE__ */ Symbol("firecrawlManagedOAuthApiKey");
340
695
  var CoreHttpError = class extends Error {
341
696
  status;
342
- constructor(message, status) {
697
+ agent_hints;
698
+ constructor(message, status, agentHints2) {
343
699
  super(message);
344
700
  this.name = "CoreHttpError";
345
701
  this.status = status;
702
+ if (agentHints2 !== void 0) this.agent_hints = agentHints2;
346
703
  }
347
704
  };
348
705
  var CredentialValidationUnavailableError = class extends Error {
@@ -354,11 +711,12 @@ var CredentialValidationUnavailableError = class extends Error {
354
711
  }
355
712
  };
356
713
  function credentialValidationUnavailable(diagnostics) {
357
- const { aborted, elapsedMs, reason, resource, status } = diagnostics;
714
+ const { aborted, edgeMitigation: edgeMitigation2, elapsedMs, reason, resource, status } = diagnostics;
358
715
  console.error(
359
716
  "[MCP_CREDENTIAL_VALIDATION]",
360
717
  JSON.stringify({
361
718
  aborted: aborted ?? null,
719
+ edge_mitigation: edgeMitigation2 ?? null,
362
720
  elapsed_ms: elapsedMs ?? null,
363
721
  introspect_status: status ?? null,
364
722
  reason,
@@ -442,7 +800,10 @@ async function monitorRequest(session, origin, path2, init = {}) {
442
800
  const s = qs.toString();
443
801
  if (s) url += `?${s}`;
444
802
  }
445
- const headers = originHeaders(origin);
803
+ const headers = {
804
+ ...originHeaders(origin),
805
+ ...AGENT_HINTS_HEADERS
806
+ };
446
807
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
447
808
  if (init.body !== void 0) headers["Content-Type"] = "application/json";
448
809
  const response = await fetch(url, {
@@ -453,15 +814,15 @@ async function monitorRequest(session, origin, path2, init = {}) {
453
814
  const payload = await response.json().catch(() => ({}));
454
815
  if (!response.ok || payload?.success === false) {
455
816
  const message = payload?.error || `HTTP ${response.status}: ${response.statusText || "Request failed"}`;
456
- throw new CoreHttpError(message, response.status);
817
+ throw new CoreHttpError(message, response.status, readAgentHints(payload));
457
818
  }
458
819
  return payload;
459
820
  }
460
- function asText(data) {
461
- return JSON.stringify(data, null, 2);
821
+ function monitorResult(data) {
822
+ return withAgentHints(structuredText(data), data);
462
823
  }
463
- var pageStatusSchema = z4.enum(["same", "new", "changed", "removed", "error"]);
464
- var checkStatusSchema = z4.enum([
824
+ var pageStatusSchema = z5.enum(["same", "new", "changed", "removed", "error"]);
825
+ var checkStatusSchema = z5.enum([
465
826
  "queued",
466
827
  "running",
467
828
  "completed",
@@ -554,22 +915,23 @@ Create a recurring scrape, crawl, or search monitor that compares each check wit
554
915
 
555
916
  In the simple form, a \`goal\` is required. If \`queries\` contains one or more non-empty values and is supplied with \`page\`/\`pages\`, \`queries\` create the search target and page targets are ignored. A monitor schedules future network checks and can send configured email or webhook notifications. Returns the created monitor.
556
917
  `,
557
- parameters: z4.object({
558
- body: z4.record(z4.string(), z4.any()).optional(),
559
- page: z4.string().optional(),
560
- pages: z4.array(z4.string()).optional(),
561
- queries: z4.array(z4.string()).optional(),
562
- searchWindow: z4.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
563
- maxResults: z4.number().int().min(1).max(50).optional(),
564
- includeDomains: z4.array(z4.string()).optional(),
565
- excludeDomains: z4.array(z4.string()).optional(),
566
- goal: z4.string().optional(),
567
- name: z4.string().optional(),
568
- scheduleText: z4.string().optional(),
569
- timezone: z4.string().optional(),
570
- email: z4.string().optional(),
571
- includeDiffs: z4.boolean().optional(),
572
- webhookUrl: z4.string().optional()
918
+ outputSchema: monitorOutputSchema,
919
+ parameters: z5.object({
920
+ body: z5.record(z5.string(), z5.any()).optional(),
921
+ page: z5.string().optional(),
922
+ pages: z5.array(z5.string()).optional(),
923
+ queries: z5.array(z5.string()).optional(),
924
+ searchWindow: z5.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
925
+ maxResults: z5.number().int().min(1).max(50).optional(),
926
+ includeDomains: z5.array(z5.string()).optional(),
927
+ excludeDomains: z5.array(z5.string()).optional(),
928
+ goal: z5.string().optional(),
929
+ name: z5.string().optional(),
930
+ scheduleText: z5.string().optional(),
931
+ timezone: z5.string().optional(),
932
+ email: z5.string().optional(),
933
+ includeDiffs: z5.boolean().optional(),
934
+ webhookUrl: z5.string().optional()
573
935
  }),
574
936
  execute: async (args2, { session, log, client: mcpClient }) => {
575
937
  const body = buildMonitorCreateBody(args2);
@@ -583,7 +945,7 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
583
945
  body
584
946
  }
585
947
  );
586
- return asText(res);
948
+ return monitorResult(res);
587
949
  }
588
950
  });
589
951
  server2.addTool({
@@ -600,9 +962,10 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
600
962
  description: `
601
963
  List monitors for the authenticated account with optional pagination controls. Returns one page of monitor records and pagination metadata.
602
964
  `,
603
- parameters: z4.object({
604
- limit: z4.number().int().positive().optional(),
605
- offset: z4.number().int().nonnegative().optional()
965
+ outputSchema: monitorListOutputSchema,
966
+ parameters: z5.object({
967
+ limit: z5.number().int().positive().optional(),
968
+ offset: z5.number().int().nonnegative().optional()
606
969
  }),
607
970
  execute: async (args2, { session, client: mcpClient }) => {
608
971
  const { limit, offset } = args2;
@@ -614,7 +977,7 @@ List monitors for the authenticated account with optional pagination controls. R
614
977
  query: { limit, offset }
615
978
  }
616
979
  );
617
- return asText(res);
980
+ return monitorResult(res);
618
981
  }
619
982
  });
620
983
  server2.addTool({
@@ -631,7 +994,8 @@ List monitors for the authenticated account with optional pagination controls. R
631
994
  description: `
632
995
  Retrieve one monitor by ID, including its configuration and current state. This does not run or modify the monitor.
633
996
  `,
634
- parameters: z4.object({ id: z4.string() }),
997
+ outputSchema: monitorOutputSchema,
998
+ parameters: z5.object({ id: z5.string() }),
635
999
  execute: async (args2, { session, client: mcpClient }) => {
636
1000
  const { id } = args2;
637
1001
  const res = await monitorRequest(
@@ -639,7 +1003,7 @@ Retrieve one monitor by ID, including its configuration and current state. This
639
1003
  requestOrigin(mcpClient, session),
640
1004
  `/monitor/${encodeURIComponent(id)}`
641
1005
  );
642
- return asText(res);
1006
+ return monitorResult(res);
643
1007
  }
644
1008
  });
645
1009
  server2.addTool({
@@ -658,9 +1022,10 @@ Patch an existing monitor by ID. The body can change its name, active/paused sta
658
1022
 
659
1023
  Returns the updated monitor.
660
1024
  `,
661
- parameters: z4.object({
662
- id: z4.string(),
663
- body: z4.record(z4.string(), z4.any())
1025
+ outputSchema: monitorOutputSchema,
1026
+ parameters: z5.object({
1027
+ id: z5.string(),
1028
+ body: z5.record(z5.string(), z5.any())
664
1029
  }),
665
1030
  execute: async (args2, { session, client: mcpClient }) => {
666
1031
  const { id, body } = args2;
@@ -670,7 +1035,7 @@ Returns the updated monitor.
670
1035
  `/monitor/${encodeURIComponent(id)}`,
671
1036
  { method: "PATCH", body }
672
1037
  );
673
- return asText(res);
1038
+ return monitorResult(res);
674
1039
  }
675
1040
  });
676
1041
  server2.addTool({
@@ -687,7 +1052,8 @@ Returns the updated monitor.
687
1052
  description: `
688
1053
  Permanently delete a monitor by ID and stop its future schedule. This operation cannot be undone and returns deletion status.
689
1054
  `,
690
- parameters: z4.object({ id: z4.string() }),
1055
+ outputSchema: monitorDeleteOutputSchema,
1056
+ parameters: z5.object({ id: z5.string() }),
691
1057
  execute: async (args2, { session, log, client: mcpClient }) => {
692
1058
  const { id } = args2;
693
1059
  log.info("Deleting monitor", { id });
@@ -697,7 +1063,7 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
697
1063
  `/monitor/${encodeURIComponent(id)}`,
698
1064
  { method: "DELETE" }
699
1065
  );
700
- return asText(res);
1066
+ return monitorResult(res);
701
1067
  }
702
1068
  });
703
1069
  server2.addTool({
@@ -714,7 +1080,8 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
714
1080
  description: `
715
1081
  Queue an immediate check for a monitor outside its normal schedule. This starts network work for the monitor's configured targets and returns the queued check.
716
1082
  `,
717
- parameters: z4.object({ id: z4.string() }),
1083
+ outputSchema: monitorRunOutputSchema,
1084
+ parameters: z5.object({ id: z5.string() }),
718
1085
  execute: async (args2, { session, client: mcpClient }) => {
719
1086
  const { id } = args2;
720
1087
  const res = await monitorRequest(
@@ -723,7 +1090,7 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
723
1090
  `/monitor/${encodeURIComponent(id)}/run`,
724
1091
  { method: "POST" }
725
1092
  );
726
- return asText(res);
1093
+ return monitorResult(res);
727
1094
  }
728
1095
  });
729
1096
  server2.addTool({
@@ -740,10 +1107,11 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
740
1107
  description: `
741
1108
  List historical checks for a monitor, optionally filtered by status and bounded by a result limit. Returns one page of check summaries and pagination metadata.
742
1109
  `,
743
- parameters: z4.object({
744
- id: z4.string(),
745
- limit: z4.number().int().positive().optional(),
746
- offset: z4.number().int().nonnegative().optional(),
1110
+ outputSchema: monitorChecksOutputSchema,
1111
+ parameters: z5.object({
1112
+ id: z5.string(),
1113
+ limit: z5.number().int().positive().optional(),
1114
+ offset: z5.number().int().nonnegative().optional(),
747
1115
  status: checkStatusSchema.optional()
748
1116
  }),
749
1117
  execute: async (args2, { session, client: mcpClient }) => {
@@ -754,7 +1122,7 @@ List historical checks for a monitor, optionally filtered by status and bounded
754
1122
  `/monitor/${encodeURIComponent(id)}/checks`,
755
1123
  { query: { limit, offset, status } }
756
1124
  );
757
- return asText(res);
1125
+ return monitorResult(res);
758
1126
  }
759
1127
  });
760
1128
  server2.addTool({
@@ -773,11 +1141,12 @@ Retrieve one monitor check and its page-level results, optionally filtered by pa
773
1141
 
774
1142
  Markdown tracking returns a unified text diff, JSON tracking returns field paths with previous/current values and a current snapshot, and mixed tracking returns both. Returns one page of results plus a \`next\` URL when more pages exist.
775
1143
  `,
776
- parameters: z4.object({
777
- id: z4.string(),
778
- checkId: z4.string(),
779
- limit: z4.number().int().positive().optional(),
780
- skip: z4.number().int().nonnegative().optional(),
1144
+ outputSchema: monitorCheckOutputSchema,
1145
+ parameters: z5.object({
1146
+ id: z5.string(),
1147
+ checkId: z5.string(),
1148
+ limit: z5.number().int().positive().optional(),
1149
+ skip: z5.number().int().nonnegative().optional(),
781
1150
  pageStatus: pageStatusSchema.optional()
782
1151
  }),
783
1152
  execute: async (args2, { session, client: mcpClient }) => {
@@ -788,15 +1157,18 @@ Markdown tracking returns a unified text diff, JSON tracking returns field paths
788
1157
  `/monitor/${encodeURIComponent(id)}/checks/${encodeURIComponent(checkId)}`,
789
1158
  { query: { limit, skip, status: pageStatus } }
790
1159
  );
791
- return asText(res);
1160
+ return monitorResult(res);
792
1161
  }
793
1162
  });
794
1163
  }
795
1164
 
796
1165
  // src/research.ts
797
- import { z as z5 } from "zod";
1166
+ import { z as z6 } from "zod";
798
1167
  import { UserError } from "fastmcp";
799
1168
  var BASE2 = "/v2/search/research";
1169
+ function researchHeaders(origin) {
1170
+ return { ...originHeaders(origin), ...AGENT_HINTS_HEADERS };
1171
+ }
800
1172
  function appendParam(params, key, value) {
801
1173
  if (value == null) return;
802
1174
  if (Array.isArray(value)) {
@@ -910,21 +1282,22 @@ Several distinct framings of the same question surface different papers than a s
910
1282
 
911
1283
  Returns ranked papers with canonical IDs, titles, authors, and abstracts.
912
1284
  `,
913
- parameters: z5.object({
914
- query: z5.string().min(1).describe(
1285
+ outputSchema: researchSearchOutputSchema,
1286
+ parameters: z6.object({
1287
+ query: z6.string().min(1).describe(
915
1288
  "Natural-language research topic or question, including methods, systems, conditions, populations, interventions, or outcomes when relevant."
916
1289
  ),
917
- k: z5.number().int().min(1).max(500).optional().describe("Number of ranked papers to return (default 40)."),
918
- authors: z5.array(z5.string()).optional().describe(
1290
+ k: z6.number().int().min(1).max(500).optional().describe("Number of ranked papers to return (default 40)."),
1291
+ authors: z6.array(z6.string()).optional().describe(
919
1292
  "Author substring filter(s); ALL must match (case-insensitive)."
920
1293
  ),
921
- categories: z5.array(z5.string()).optional().describe(
1294
+ categories: z6.array(z6.string()).optional().describe(
922
1295
  "Paper category filter(s) (e.g. `cs.LG`); ALL provided values must match."
923
1296
  ),
924
- from: z5.string().optional().describe(
1297
+ from: z6.string().optional().describe(
925
1298
  "Inclusive lower bound on created/updated date (`YYYY-MM-DD`)."
926
1299
  ),
927
- to: z5.string().optional().describe(
1300
+ to: z6.string().optional().describe(
928
1301
  "Inclusive upper bound on created/updated date (`YYYY-MM-DD`)."
929
1302
  )
930
1303
  }),
@@ -940,9 +1313,10 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
940
1313
  const client = getClient2(session);
941
1314
  const res = await client.http.get(
942
1315
  withQuery(`${BASE2}/papers`, params),
943
- originHeaders(requestOrigin(mcpClient, session))
1316
+ researchHeaders(requestOrigin(mcpClient, session))
944
1317
  );
945
- return fmtHits(res.data?.results);
1318
+ const results = res.data?.results ?? [];
1319
+ return withAgentHints(withStructured(fmtHits(results), { results }), res.data, true);
946
1320
  }
947
1321
  });
948
1322
  server2.addTool({
@@ -959,8 +1333,9 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
959
1333
  description: `
960
1334
  Retrieve canonical metadata for one paper ID, such as an arXiv, PMC, PMID, or DOI identifier. Returns the title, abstract, authors, categories, source IDs, and dates as markdown.
961
1335
  `,
962
- parameters: z5.object({
963
- paperId: z5.string().min(1).describe(
1336
+ outputSchema: researchPaperOutputSchema,
1337
+ parameters: z6.object({
1338
+ paperId: z6.string().min(1).describe(
964
1339
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
965
1340
  )
966
1341
  }),
@@ -969,9 +1344,13 @@ Retrieve canonical metadata for one paper ID, such as an arXiv, PMC, PMID, or DO
969
1344
  const client = getClient2(session);
970
1345
  const res = await client.http.get(
971
1346
  `${BASE2}/papers/${encodeURIComponent(paperId)}`,
972
- originHeaders(requestOrigin(mcpClient, session))
1347
+ researchHeaders(requestOrigin(mcpClient, session))
973
1348
  );
974
- return fmtPaperMetadata(res.data?.paper);
1349
+ const paper = res.data?.paper;
1350
+ return withAgentHints(withStructured(
1351
+ fmtPaperMetadata(paper),
1352
+ paper ? { paper } : {}
1353
+ ), res.data, true);
975
1354
  }
976
1355
  });
977
1356
  server2.addTool({
@@ -990,12 +1369,13 @@ Find citation-graph candidates from one to ten \`seed_ids\`; the first ID is the
990
1369
 
991
1370
  Returns ranked candidates and the evaluated pool size.
992
1371
  `,
993
- parameters: z5.object({
994
- seed_ids: z5.array(z5.string()).min(1).max(10),
995
- intent: z5.string().min(1),
996
- mode: z5.enum(["similar", "citers", "references"]).optional(),
997
- k: z5.number().int().min(1).max(500).optional(),
998
- rerank: z5.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
1372
+ outputSchema: researchRelatedOutputSchema,
1373
+ parameters: z6.object({
1374
+ seed_ids: z6.array(z6.string()).min(1).max(10),
1375
+ intent: z6.string().min(1),
1376
+ mode: z6.enum(["similar", "citers", "references"]).optional(),
1377
+ k: z6.number().int().min(1).max(500).optional(),
1378
+ rerank: z6.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
999
1379
  }),
1000
1380
  execute: async (args2, { session, client: mcpClient }) => {
1001
1381
  const { seed_ids, intent, mode, k, rerank } = args2;
@@ -1012,12 +1392,17 @@ Returns ranked candidates and the evaluated pool size.
1012
1392
  `${BASE2}/papers/${encodeURIComponent(primary)}/similar`,
1013
1393
  params
1014
1394
  ),
1015
- originHeaders(requestOrigin(mcpClient, session))
1395
+ researchHeaders(requestOrigin(mcpClient, session))
1016
1396
  );
1397
+ const results = res.data?.results ?? [];
1398
+ const poolSize = res.data?.poolSize ?? 0;
1017
1399
  const note = res.data?.note ? `
1018
1400
  note: ${res.data.note}` : "";
1019
- return `${fmtHits(res.data?.results)}
1020
- (poolSize=${res.data?.poolSize ?? 0})${note}`;
1401
+ return withAgentHints(withStructured(
1402
+ `${fmtHits(results)}
1403
+ (poolSize=${poolSize})${note}`,
1404
+ { results, poolSize, ...res.data?.note ? { note: res.data.note } : {} }
1405
+ ), res.data, true);
1021
1406
  }
1022
1407
  });
1023
1408
  server2.addTool({
@@ -1036,12 +1421,13 @@ Retrieve in-body passages from one paper that are relevant to a specific questio
1036
1421
 
1037
1422
  Returns matching passages or a notice when full text is unavailable.
1038
1423
  `,
1039
- parameters: z5.object({
1040
- paperId: z5.string().min(1).describe(
1424
+ outputSchema: researchReadOutputSchema,
1425
+ parameters: z6.object({
1426
+ paperId: z6.string().min(1).describe(
1041
1427
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
1042
1428
  ),
1043
- question: z5.string().min(1),
1044
- k: z5.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
1429
+ question: z6.string().min(1),
1430
+ k: z6.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
1045
1431
  }),
1046
1432
  execute: async (args2, { session, client: mcpClient }) => {
1047
1433
  const { paperId, question, k } = args2;
@@ -1051,10 +1437,13 @@ Returns matching passages or a notice when full text is unavailable.
1051
1437
  const client = getClient2(session);
1052
1438
  const res = await client.http.get(
1053
1439
  withQuery(`${BASE2}/papers/${encodeURIComponent(paperId)}`, params),
1054
- originHeaders(requestOrigin(mcpClient, session))
1440
+ researchHeaders(requestOrigin(mcpClient, session))
1055
1441
  );
1056
1442
  const passages = res.data?.passages ?? [];
1057
- return passages.length ? passages.map((p) => p.text).join("\n---\n") : "(no full-text passages available for this paper)";
1443
+ return withAgentHints(withStructured(
1444
+ passages.length ? passages.map((p) => p.text).join("\n---\n") : "(no full-text passages available for this paper)",
1445
+ { passages }
1446
+ ), res.data, true);
1058
1447
  }
1059
1448
  });
1060
1449
  server2.addTool({
@@ -1068,9 +1457,10 @@ Returns matching passages or a notice when full text is unavailable.
1068
1457
  description: `
1069
1458
  Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub issues, pull requests, and READMEs, plus code documentation, returned as matched passages.
1070
1459
  `,
1071
- parameters: z5.object({
1072
- query: z5.string().min(1),
1073
- k: z5.number().int().min(1).max(100).optional()
1460
+ outputSchema: deprecatedToolOutputSchema,
1461
+ parameters: z6.object({
1462
+ query: z6.string().min(1),
1463
+ k: z6.number().int().min(1).max(100).optional()
1074
1464
  }),
1075
1465
  canList: () => false,
1076
1466
  beforeValidate: () => {
@@ -1089,10 +1479,7 @@ Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub
1089
1479
  }
1090
1480
 
1091
1481
  // src/usage.ts
1092
- import { z as z6 } from "zod";
1093
- function asText2(data) {
1094
- return JSON.stringify(data, null, 2);
1095
- }
1482
+ import { z as z7 } from "zod";
1096
1483
  function assertSuccessful(status, response, operation) {
1097
1484
  if (status === 200 && response.success) return;
1098
1485
  throw new Error(response.error || `Failed to ${operation}`);
@@ -1113,11 +1500,12 @@ The current view returns \`remainingCredits\`, \`planCredits\`, \`billingPeriodS
1113
1500
 
1114
1501
  The historical view returns periods sorted by start date with \`startDate\`, \`endDate\`, and \`creditsUsed\`. Set \`byApiKey\` to include separate periods per API key, identified by the optional \`apiKey\` field. The newest period's \`endDate\` can be null.
1115
1502
  `,
1116
- parameters: z6.object({
1117
- view: z6.enum(["current", "historical"]).optional().describe(
1503
+ outputSchema: creditUsageOutputSchema,
1504
+ parameters: z7.object({
1505
+ view: z7.enum(["current", "historical"]).optional().describe(
1118
1506
  "Select current balance or historical monthly usage. Defaults to current."
1119
1507
  ),
1120
- byApiKey: z6.boolean().optional().describe(
1508
+ byApiKey: z7.boolean().optional().describe(
1121
1509
  'Break historical usage down by API key. When view is omitted, true selects the historical view; it cannot be combined with view "current".'
1122
1510
  )
1123
1511
  }),
@@ -1142,7 +1530,7 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e
1142
1530
  response2.data,
1143
1531
  "get historical credit usage"
1144
1532
  );
1145
- return asText2(response2.data);
1533
+ return structuredText(response2.data);
1146
1534
  }
1147
1535
  const response = await client.http.get(
1148
1536
  "/v2/team/credit-usage",
@@ -1150,7 +1538,7 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e
1150
1538
  );
1151
1539
  assertSuccessful(response.status, response.data, "get credit usage");
1152
1540
  const data = response.data.data ?? response.data;
1153
- return asText2({
1541
+ return structuredText({
1154
1542
  remainingCredits: data.remainingCredits ?? data.remaining_credits ?? 0,
1155
1543
  planCredits: data.planCredits ?? data.plan_credits,
1156
1544
  billingPeriodStart: data.billingPeriodStart ?? data.billing_period_start ?? null,
@@ -1165,6 +1553,68 @@ function escapeWWWAuthenticateValue(value) {
1165
1553
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1166
1554
  }
1167
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
+
1168
1618
  // src/index.ts
1169
1619
  dotenv.config({ debug: false, quiet: true });
1170
1620
  var require2 = createRequire2(import.meta.url);
@@ -1248,11 +1698,11 @@ function getOAuthProtectedResourceMetadataUrl(profile) {
1248
1698
  const base = `${resource.origin}/.well-known/oauth-protected-resource`;
1249
1699
  return profile.id === "full" ? base : `${base}${resource.pathname}`;
1250
1700
  }
1251
- function createOAuthChallengeResponse(error, profile, details = {}) {
1701
+ function createOAuthChallengeResponse(error2, profile, details = {}) {
1252
1702
  if (!isMcpOAuthEnabled()) {
1253
1703
  return void 0;
1254
1704
  }
1255
- const errorMessage = error instanceof Error ? error.message : String(error || "Unauthorized");
1705
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2 || "Unauthorized");
1256
1706
  const wwwAuthenticateDescription = errorMessage.replace(/[\r\n]+/g, " ").trim();
1257
1707
  const wwwAuthenticate = [
1258
1708
  ...profile.advertiseOAuth ? [
@@ -1304,8 +1754,8 @@ function createInvalidOAuthRecoveryResponse(recovery) {
1304
1754
  );
1305
1755
  }
1306
1756
  var CREDENTIAL_VALIDATION_RETRY_AFTER_SECONDS = 5;
1307
- function createCredentialValidationUnavailableResponse(error) {
1308
- return new Response(error.message, {
1757
+ function createCredentialValidationUnavailableResponse(error2) {
1758
+ return new Response(error2.message, {
1309
1759
  headers: {
1310
1760
  "Content-Type": "text/plain; charset=utf-8",
1311
1761
  "Retry-After": String(CREDENTIAL_VALIDATION_RETRY_AFTER_SECONDS)
@@ -1355,7 +1805,40 @@ function credentialMetadata(data) {
1355
1805
  resource: typeof data.aud === "string" ? data.aud : void 0
1356
1806
  };
1357
1807
  }
1358
- 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) {
1359
1842
  const introspectionSecret = getOAuthIntrospectionSecret();
1360
1843
  if (!introspectionSecret) {
1361
1844
  throw credentialValidationUnavailable({
@@ -1394,6 +1877,7 @@ async function introspectToken(token, expectedResource) {
1394
1877
  }
1395
1878
  if (!response.ok) {
1396
1879
  throw credentialValidationUnavailable({
1880
+ edgeMitigation: edgeMitigation(response),
1397
1881
  elapsedMs: elapsedMs(),
1398
1882
  reason: "introspect_http_status",
1399
1883
  resource: expectedResource,
@@ -1601,13 +2085,13 @@ function makeAuthenticate(profile) {
1601
2085
  session
1602
2086
  );
1603
2087
  return session;
1604
- }).catch((error) => {
2088
+ }).catch((error2) => {
1605
2089
  emitSearchCompanionAuthTelemetry(profile, request, "rejected");
1606
2090
  emitLegacyKeyPathTelemetry(profile, request, "rejected");
1607
- if (error instanceof InvalidFirecrawlCredentialError) {
1608
- throw createInvalidCredentialResponse(error);
2091
+ if (error2 instanceof InvalidFirecrawlCredentialError) {
2092
+ throw createInvalidCredentialResponse(error2);
1609
2093
  }
1610
- if (error instanceof InvalidOAuthCredentialError) {
2094
+ if (error2 instanceof InvalidOAuthCredentialError) {
1611
2095
  const recovery = invalidOAuthRecoveryPayload();
1612
2096
  const oauthChallenge2 = createOAuthChallengeResponse(
1613
2097
  new Error(recovery.message),
@@ -1616,15 +2100,15 @@ function makeAuthenticate(profile) {
1616
2100
  );
1617
2101
  throw oauthChallenge2 ?? createInvalidOAuthRecoveryResponse(recovery);
1618
2102
  }
1619
- if (error instanceof CredentialValidationUnavailableError) {
1620
- throw createCredentialValidationUnavailableResponse(error);
2103
+ if (error2 instanceof CredentialValidationUnavailableError) {
2104
+ throw createCredentialValidationUnavailableResponse(error2);
1621
2105
  }
1622
2106
  const shouldChallenge = requestShouldReceiveOAuthChallenge(request, profile);
1623
- const oauthChallenge = shouldChallenge ? createOAuthChallengeResponse(error, profile) : void 0;
2107
+ const oauthChallenge = shouldChallenge ? createOAuthChallengeResponse(error2, profile) : void 0;
1624
2108
  if (oauthChallenge) {
1625
2109
  throw oauthChallenge;
1626
2110
  }
1627
- throw error;
2111
+ throw error2;
1628
2112
  });
1629
2113
  if (request) {
1630
2114
  request[authResultByRequest] = authResult;
@@ -1644,30 +2128,30 @@ function removeEmptyTopLevel(obj) {
1644
2128
  }
1645
2129
  return out;
1646
2130
  }
1647
- var searchDomainSchema = z7.string().trim().toLowerCase().min(1).max(253).regex(
2131
+ var searchDomainSchema = z8.string().trim().toLowerCase().min(1).max(253).regex(
1648
2132
  /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/,
1649
2133
  "Domain must be a valid hostname without protocol or path"
1650
2134
  );
1651
2135
  var searchToolBaseFields = {
1652
- toolDetail: z7.enum(["compact", "summary", "full"]).optional().describe("Compact by default. Compact returns only provider, capability and description; full includes contracts. Inspect selected compact tools with firecrawl_find_tools providers and capabilities."),
1653
- query: z7.string().min(1).describe("Query for web and semantic tool discovery. Operators include quoted phrases, `-term`, `site:host`, `inurl:term`, `intitle:term`, and `related:host`; the set is non-exhaustive. Catalogue browsing is available through firecrawl_find_tools."),
1654
- domainTools: z7.boolean().optional().describe(
2136
+ toolDetail: z8.enum(["compact", "summary", "full"]).optional().describe("Compact by default. Compact returns only provider, capability and description; full includes contracts. Inspect selected compact tools with firecrawl_find_tools providers and capabilities."),
2137
+ query: z8.string().min(1).describe("Query for web and semantic tool discovery. Operators include quoted phrases, `-term`, `site:host`, `inurl:term`, `intitle:term`, and `related:host`; the set is non-exhaustive. Catalogue browsing is available through firecrawl_find_tools."),
2138
+ domainTools: z8.boolean().optional().describe(
1655
2139
  "Include domain-matched tools for result URLs. Defaults to true when Alexandria is combined with web, news or images; semantic-only search leaves domain matching off."
1656
2140
  ),
1657
- highlights: z7.boolean().optional().describe(
2141
+ highlights: z8.boolean().optional().describe(
1658
2142
  "Return query-relevant page excerpts for web and news results when available (default). Highlights appear in web `description` and news `snippet`; otherwise, original snippets are returned. Set to false to keep the original search snippets."
1659
2143
  ),
1660
- limit: z7.number().int().min(1).max(100).optional(),
1661
- tbs: z7.string().optional(),
1662
- filter: z7.string().optional(),
1663
- location: z7.string().optional(),
1664
- includeDomains: z7.array(searchDomainSchema).optional().describe("Hostnames to restrict results to. Mutually exclusive with excludeDomains."),
1665
- excludeDomains: z7.array(searchDomainSchema).optional().describe("Hostnames to leave out of results. Mutually exclusive with includeDomains."),
1666
- sources: z7.array(searchSourceSchema).optional().describe("Search sources; authenticated sessions default to web + alexandria, keyless sessions to web only. " + ALEXANDRIA_SOURCES_OPT_OUT + ' Use ["alexandria"] alone for provider discovery without web results.'),
1667
- categories: z7.array(z7.enum(["research", "pdf", "developer"])).optional().describe(
2144
+ limit: z8.number().int().min(1).max(100).optional(),
2145
+ tbs: z8.string().optional(),
2146
+ filter: z8.string().optional(),
2147
+ location: z8.string().optional(),
2148
+ includeDomains: z8.array(searchDomainSchema).optional().describe("Hostnames to restrict results to. Mutually exclusive with excludeDomains."),
2149
+ excludeDomains: z8.array(searchDomainSchema).optional().describe("Hostnames to leave out of results. Mutually exclusive with includeDomains."),
2150
+ sources: z8.array(searchSourceSchema).optional().describe("Search sources; authenticated sessions default to web + alexandria, keyless sessions to web only. " + ALEXANDRIA_SOURCES_OPT_OUT + ' Use ["alexandria"] alone for provider discovery without web results.'),
2151
+ categories: z8.array(z8.enum(["research", "pdf", "developer"])).optional().describe(
1668
2152
  'Limit results to specific source types. `research` restricts ordinary web results to research-affiliated websites and returns page snippets, which is separate from the `firecrawl_research_*` tools that search paper abstracts and full text across biomedical (PubMed, bioRxiv, medRxiv) and arXiv literature; `pdf` searches PDF results; `developer` searches an index built for coding agents over public repositories, GitHub issues, merged pull requests, repository READMEs, and code documentation. `developer` returns hits in `data.web` with `category: "developer"`; the other categories also filter `data.web`.'
1669
2153
  ),
1670
- enterprise: z7.array(z7.enum(["default", "anon", "zdr"])).optional()
2154
+ enterprise: z8.array(z8.enum(["default", "anon", "zdr"])).optional()
1671
2155
  };
1672
2156
  function searchDomainsAreExclusive(args2) {
1673
2157
  return !(args2.includeDomains?.length && args2.excludeDomains?.length);
@@ -1675,15 +2159,15 @@ function searchDomainsAreExclusive(args2) {
1675
2159
  var SEARCH_DOMAINS_CONFLICT_MESSAGE = "includeDomains and excludeDomains cannot both be specified";
1676
2160
  var EXCHANGE_KEY_REQUIRED_MESSAGE = "Alexandria requires an API key on a team with Alexandria access";
1677
2161
  var EXCHANGE_MAX_CALLS = 10;
1678
- var exchangeCallSchema = z7.object({
1679
- provider: z7.string().min(1).describe('Provider slug, e.g. "fred".'),
1680
- capability: z7.string().min(1).describe(
2162
+ var exchangeCallSchema = z8.object({
2163
+ provider: z8.string().min(1).describe('Provider slug, e.g. "fred".'),
2164
+ capability: z8.string().min(1).describe(
1681
2165
  'Capability address as returned by search or discover, e.g. "series/observations".'
1682
2166
  ),
1683
- version: z7.string().trim().min(1).max(128).optional().describe("Optional published workflow version. Omit to use the latest version."),
1684
- options: z7.record(z7.string(), z7.any()).optional().describe("Capability options as declared by its contract.")
2167
+ version: z8.string().trim().min(1).max(128).optional().describe("Optional published workflow version. Omit to use the latest version."),
2168
+ options: z8.record(z8.string(), z8.any()).optional().describe("Capability options as declared by its contract.")
1685
2169
  });
1686
- var exchangeCallsSchema = z7.array(exchangeCallSchema).min(1).max(EXCHANGE_MAX_CALLS);
2170
+ var exchangeCallsSchema = z8.array(exchangeCallSchema).min(1).max(EXCHANGE_MAX_CALLS);
1687
2171
  function assertExchangeCredential(session) {
1688
2172
  if (hasCredential(session)) return;
1689
2173
  const payload = {
@@ -1707,7 +2191,7 @@ function termsRequiredAction(body) {
1707
2191
  url: action.url
1708
2192
  };
1709
2193
  }
1710
- function termsRequiredError(action, context) {
2194
+ function termsRequiredError(action, context, hints) {
1711
2195
  const requestId = context.requestId;
1712
2196
  const retryIdentity = requestId ? ` and requestId ${requestId}` : "";
1713
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.`;
@@ -1722,6 +2206,7 @@ Do not retry until acceptance is confirmed.`,
1722
2206
  code: TERMS_REQUIRED_CODE,
1723
2207
  status: 403,
1724
2208
  message,
2209
+ ...hints ? { agent_hints: hints } : {},
1725
2210
  ...requestId ? { requestId } : {},
1726
2211
  requiresAction: action,
1727
2212
  nextTool: {
@@ -1749,27 +2234,29 @@ Do not retry until acceptance is confirmed.`,
1749
2234
  }
1750
2235
  );
1751
2236
  }
1752
- function throwIfTermsRequired(error, context) {
1753
- const source = error;
2237
+ function throwIfTermsRequired(error2, context) {
2238
+ const source = error2;
1754
2239
  const action = termsRequiredAction(source?.response?.data ?? source?.details);
1755
- if (action) throw termsRequiredError(action, context);
2240
+ if (action)
2241
+ throw termsRequiredError(action, context, readErrorAgentHints(error2));
1756
2242
  }
1757
2243
  async function relayTermsRequired(run, context) {
1758
2244
  try {
1759
2245
  return await run();
1760
- } catch (error) {
1761
- throwIfTermsRequired(error, context);
1762
- throw error;
2246
+ } catch (error2) {
2247
+ throwIfTermsRequired(error2, context);
2248
+ throw error2;
1763
2249
  }
1764
2250
  }
1765
2251
  async function relayExchangeError(run, context) {
1766
2252
  try {
1767
2253
  return await run();
1768
- } catch (error) {
1769
- throwIfTermsRequired(error, context);
1770
- const response = error?.response;
1771
- if (!response || response.status === 401) throw error;
2254
+ } catch (error2) {
2255
+ throwIfTermsRequired(error2, context);
2256
+ const response = error2?.response;
2257
+ if (!response || response.status === 401) throw error2;
1772
2258
  const data = response.data;
2259
+ const hints = readErrorAgentHints(error2);
1773
2260
  const message = typeof data?.error === "string" ? data.error : `Alexandria request failed (HTTP ${response.status})`;
1774
2261
  const disabledProvider = response.status === 403 ? context.providers?.find((provider) => message === `Access to ${provider} is disabled for this organization.`) : void 0;
1775
2262
  if (disabledProvider) {
@@ -1779,6 +2266,7 @@ async function relayExchangeError(run, context) {
1779
2266
  code: typeof data?.code === "string" ? data.code : "exchange_error",
1780
2267
  status: 403,
1781
2268
  message,
2269
+ ...hints ? { agent_hints: hints } : {},
1782
2270
  nextTool: {
1783
2271
  name: "firecrawl_scrape",
1784
2272
  arguments: {
@@ -1792,6 +2280,7 @@ async function relayExchangeError(run, context) {
1792
2280
  code: typeof data?.code === "string" ? data.code : "exchange_error",
1793
2281
  status: response.status,
1794
2282
  message,
2283
+ ...hints ? { agent_hints: hints } : {},
1795
2284
  ...typeof data?.chargeId === "string" ? { chargeId: data.chargeId } : {}
1796
2285
  });
1797
2286
  }
@@ -1799,14 +2288,14 @@ async function relayExchangeError(run, context) {
1799
2288
  async function postSearchWithFallback(client, body, implicitTools) {
1800
2289
  try {
1801
2290
  return await client.http.post("/v2/search", body);
1802
- } catch (error) {
1803
- const response = error?.response;
2291
+ } catch (error2) {
2292
+ const response = error2?.response;
1804
2293
  const unavailable = [
1805
2294
  "Provider discovery requires access and does not support zero data retention.",
1806
2295
  "This endpoint is not enabled for this team.",
1807
2296
  "Exchange is not enabled for this team."
1808
2297
  ].includes(response?.data?.error);
1809
- if (!implicitTools || response?.status !== 403 || !unavailable) throw error;
2298
+ if (!implicitTools || response?.status !== 403 || !unavailable) throw error2;
1810
2299
  return client.http.post("/v2/search", {
1811
2300
  ...body,
1812
2301
  sources: ["web"],
@@ -1845,10 +2334,9 @@ var ConsoleLogger = class {
1845
2334
  var openAiAppsChallengeToken = normalizeHeader(
1846
2335
  process.env.OPENAI_APPS_CHALLENGE_TOKEN
1847
2336
  );
1848
- 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} When a provider matches, prefer it over scraping pages if the task needs the same fields across several entities, provenance, exact figures or timestamps, or a large set of records; execute it through firecrawl_scrape with the returned contract. If web results already answer the question, use them. Before scraping more than one page for the same fields, spend one free firecrawl_find_tools call to check for a provider. 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 \u2014 code behaviour, a library or framework, an API contract, an error message, or a known bug \u2014 firecrawl_developer_search (or firecrawl_search with categories: ["developer"]) searches an index of public repositories, GitHub issues, merged pull requests, READMEs, and code documentation. Execution with firecrawl_scrape alexandria uses requestId; reuse the returned ID for retries of the same payload, never a new ID to bypass a pending or uncertain 409. 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. Follow the returned terms/show and terms/accept calls through firecrawl_scrape; acceptance requires explicit user authorization for the reviewed version and digest and confirmed:true. Otherwise direct an organization admin to the dashboard URL. Do not repeat successful provider calls just because the client could not display their output. Provide only the required inputs and account for stated network or external side effects.
1849
- ${ALEXANDRIA_FEEDBACK_GUIDANCE}`;
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.`;
1850
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.`;
1851
- 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. Use firecrawl_find_tools to browse the Alexandria catalogue or read a provider's full contract, and firecrawl_scrape with an alexandria body to execute a capability discovered that way; 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.`;
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.`;
1852
2340
  var SEARCH_PROFILE_TOOLS = /* @__PURE__ */ new Set([
1853
2341
  "firecrawl_search",
1854
2342
  "firecrawl_developer_search",
@@ -1992,17 +2480,31 @@ function invalidOAuthRecoveryPayload() {
1992
2480
  message: INVALID_OAUTH_MESSAGE
1993
2481
  });
1994
2482
  }
1995
- function isCoreCredentialRejection(error) {
1996
- if (!(error instanceof Error)) return false;
1997
- const candidate = error;
2483
+ function isCoreCredentialRejection(error2) {
2484
+ if (!(error2 instanceof Error)) return false;
2485
+ const candidate = error2;
1998
2486
  return candidate.status === 401 || candidate.response?.status === 401;
1999
2487
  }
2000
2488
  async function runWithCredentialRecovery(run, requestId, session) {
2001
2489
  try {
2002
2490
  return await run();
2003
- } catch (error) {
2004
- if (session?.authType !== "api-key" || !isCoreCredentialRejection(error)) {
2005
- throw error;
2491
+ } catch (error2) {
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
+ }
2507
+ throw error2;
2006
2508
  }
2007
2509
  const payload = recoveryPayload("CREDENTIAL_INVALID", requestId);
2008
2510
  throw new UserError2(String(payload.message), payload);
@@ -2055,7 +2557,7 @@ function deprecatedExtractPayload() {
2055
2557
  docs_url: "https://docs.firecrawl.dev/developer-guides/usage-guides/choosing-the-data-extractor"
2056
2558
  };
2057
2559
  }
2058
- function emitActionLog(toolName, status, session, error, requestId = randomUUID(), code) {
2560
+ function emitActionLog(toolName, status, session, error2, requestId = randomUUID(), code) {
2059
2561
  if (process.env.CLOUD_SERVICE !== "true") return;
2060
2562
  const payload = {
2061
2563
  team_id: session?.teamId,
@@ -2067,7 +2569,7 @@ function emitActionLog(toolName, status, session, error, requestId = randomUUID(
2067
2569
  status,
2068
2570
  request_id: requestId,
2069
2571
  resource: primaryProfile.resourceUrl,
2070
- ...error ? { error_class: error instanceof Error ? error.name : typeof error } : {},
2572
+ ...error2 ? { error_class: error2 instanceof Error ? error2.name : typeof error2 } : {},
2071
2573
  ...code ? { code } : {}
2072
2574
  };
2073
2575
  console.error("[MCP_ACTION]", JSON.stringify(payload));
@@ -2087,6 +2589,26 @@ function emitActionLog(toolName, status, session, error, requestId = randomUUID(
2087
2589
  signal: AbortSignal.timeout(1500)
2088
2590
  }).catch(() => void 0);
2089
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
+ }
2090
2612
  function guardHostedTool(tool, { logActions }) {
2091
2613
  const keylessTool = KEYLESS_TOOL_NAMES.has(tool.name);
2092
2614
  const execute = tool.execute;
@@ -2156,20 +2678,35 @@ function guardHostedTool(tool, { logActions }) {
2156
2678
  if (logActions) emitActionLog(tool.name, "error", invocationSession, new UserError2(String(payload.message), payload), requestId, code);
2157
2679
  throw new UserError2(String(payload.message), payload);
2158
2680
  }
2159
- const runTool = () => runWithCredentialRecovery(
2160
- () => execute(args2, invocationContext),
2161
- requestId,
2162
- invocationSession
2163
- );
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
+ };
2164
2701
  if (!logActions) return runTool();
2165
2702
  emitActionLog(tool.name, "started", invocationSession, void 0, requestId);
2166
2703
  try {
2167
2704
  const result = await runTool();
2168
2705
  emitActionLog(tool.name, "success", invocationSession, void 0, requestId);
2169
2706
  return result;
2170
- } catch (error) {
2171
- emitActionLog(tool.name, "error", invocationSession, error, requestId);
2172
- throw error;
2707
+ } catch (error2) {
2708
+ emitActionLog(tool.name, "error", invocationSession, error2, requestId);
2709
+ throw error2;
2173
2710
  }
2174
2711
  }
2175
2712
  };
@@ -2215,6 +2752,7 @@ server.getApp().get("/ready", (context) => {
2215
2752
  }
2216
2753
  return missing.length ? context.json({ ok: false, missing }, 503) : context.json({ ok: true }, 200);
2217
2754
  });
2755
+ var warnedMissingAgentHintsInterceptor = false;
2218
2756
  function createClient(apiKey) {
2219
2757
  const config = {
2220
2758
  ...process.env.FIRECRAWL_API_URL && {
@@ -2224,7 +2762,44 @@ function createClient(apiKey) {
2224
2762
  if (apiKey) {
2225
2763
  config.apiKey = apiKey;
2226
2764
  }
2227
- 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
+ }
2228
2803
  }
2229
2804
  var SAFE_MODE = process.env.CLOUD_SERVICE === "true";
2230
2805
  function getClient(session) {
@@ -2253,20 +2828,21 @@ function getClient(session) {
2253
2828
  reason: "delegated_credential_unavailable"
2254
2829
  });
2255
2830
  }
2256
- config.headers = {
2257
- ...config.headers ?? {},
2258
- Authorization: `Bearer ${credential}`
2259
- };
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
+ }
2260
2839
  return config;
2261
2840
  });
2262
2841
  return client;
2263
2842
  }
2264
- function asText3(data) {
2843
+ function asText(data) {
2265
2844
  return JSON.stringify(data, null, 2);
2266
2845
  }
2267
- function compactText(data) {
2268
- return JSON.stringify(data);
2269
- }
2270
2846
  var safeActionTypes = ["wait", "screenshot", "scroll", "scrape"];
2271
2847
  var otherActions = [
2272
2848
  "click",
@@ -2332,10 +2908,10 @@ function transformScrapeParams(args2) {
2332
2908
  delete out.pdfOptions;
2333
2909
  return out;
2334
2910
  }
2335
- var scrapeParamsSchema = z7.object({
2336
- url: z7.string().url(),
2337
- formats: z7.array(
2338
- z7.enum([
2911
+ var scrapeParamsSchema = z8.object({
2912
+ url: z8.string().url(),
2913
+ formats: z8.array(
2914
+ z8.enum([
2339
2915
  "markdown",
2340
2916
  "html",
2341
2917
  "rawHtml",
@@ -2349,87 +2925,93 @@ var scrapeParamsSchema = z7.object({
2349
2925
  "audio"
2350
2926
  ])
2351
2927
  ).optional(),
2352
- jsonOptions: z7.object({
2353
- prompt: z7.string().optional(),
2354
- schema: z7.record(z7.string(), z7.any()).optional()
2928
+ jsonOptions: z8.object({
2929
+ prompt: z8.string().optional(),
2930
+ schema: z8.record(z8.string(), z8.any()).optional()
2355
2931
  }).optional(),
2356
- queryOptions: z7.object({
2357
- prompt: z7.string().max(1e4),
2358
- mode: z7.enum(["directQuote", "freeform"]).default("freeform")
2932
+ queryOptions: z8.object({
2933
+ prompt: z8.string().max(1e4),
2934
+ mode: z8.enum(["directQuote", "freeform"]).default("freeform")
2359
2935
  }).optional(),
2360
- screenshotOptions: z7.object({
2361
- fullPage: z7.boolean().optional(),
2362
- quality: z7.number().optional(),
2363
- viewport: z7.object({ width: z7.number(), height: z7.number() }).optional()
2936
+ screenshotOptions: z8.object({
2937
+ fullPage: z8.boolean().optional(),
2938
+ quality: z8.number().optional(),
2939
+ viewport: z8.object({ width: z8.number(), height: z8.number() }).optional()
2364
2940
  }).optional(),
2365
- parsers: z7.array(z7.enum(["pdf"])).optional(),
2366
- pdfOptions: z7.object({
2367
- maxPages: z7.number().int().min(1).max(1e4).optional()
2941
+ parsers: z8.array(z8.enum(["pdf"])).optional(),
2942
+ pdfOptions: z8.object({
2943
+ maxPages: z8.number().int().min(1).max(1e4).optional()
2368
2944
  }).optional(),
2369
- onlyMainContent: z7.boolean().optional(),
2370
- redactPII: z7.boolean().optional(),
2371
- includeTags: z7.array(z7.string()).optional(),
2372
- excludeTags: z7.array(z7.string()).optional(),
2373
- waitFor: z7.number().optional(),
2945
+ onlyMainContent: z8.boolean().optional(),
2946
+ redactPII: z8.boolean().optional(),
2947
+ includeTags: z8.array(z8.string()).optional(),
2948
+ excludeTags: z8.array(z8.string()).optional(),
2949
+ waitFor: z8.number().optional(),
2374
2950
  ...SAFE_MODE ? {} : {
2375
- actions: z7.array(
2376
- z7.object({
2377
- type: z7.enum(allowedActionTypes),
2378
- selector: z7.string().optional(),
2379
- milliseconds: z7.number().optional(),
2380
- text: z7.string().optional(),
2381
- key: z7.string().optional(),
2382
- direction: z7.enum(["up", "down"]).optional(),
2383
- script: z7.string().optional(),
2384
- fullPage: z7.boolean().optional()
2951
+ actions: z8.array(
2952
+ z8.object({
2953
+ type: z8.enum(allowedActionTypes),
2954
+ selector: z8.string().optional(),
2955
+ milliseconds: z8.number().optional(),
2956
+ text: z8.string().optional(),
2957
+ key: z8.string().optional(),
2958
+ direction: z8.enum(["up", "down"]).optional(),
2959
+ script: z8.string().optional(),
2960
+ fullPage: z8.boolean().optional()
2385
2961
  })
2386
2962
  ).optional()
2387
2963
  },
2388
- mobile: z7.boolean().optional(),
2389
- skipTlsVerification: z7.boolean().optional(),
2390
- removeBase64Images: z7.boolean().optional(),
2391
- location: z7.object({
2392
- country: z7.string().optional(),
2393
- languages: z7.array(z7.string()).optional()
2964
+ mobile: z8.boolean().optional(),
2965
+ skipTlsVerification: z8.boolean().optional(),
2966
+ removeBase64Images: z8.boolean().optional(),
2967
+ location: z8.object({
2968
+ country: z8.string().optional(),
2969
+ languages: z8.array(z8.string()).optional()
2394
2970
  }).optional(),
2395
- storeInCache: z7.boolean().optional(),
2396
- zeroDataRetention: z7.boolean().optional(),
2397
- maxAge: z7.number().optional(),
2398
- lockdown: z7.boolean().optional(),
2399
- proxy: z7.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
2400
- profile: z7.object({
2401
- name: z7.string(),
2402
- saveChanges: z7.boolean().optional()
2971
+ storeInCache: z8.boolean().optional(),
2972
+ zeroDataRetention: z8.boolean().optional(),
2973
+ maxAge: z8.number().optional(),
2974
+ lockdown: z8.boolean().optional(),
2975
+ proxy: z8.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
2976
+ profile: z8.object({
2977
+ name: z8.string(),
2978
+ saveChanges: z8.boolean().optional()
2403
2979
  }).optional()
2404
2980
  });
2981
+ var ALEXANDRIA_IGNORED_SCRAPE_OPTIONS = /* @__PURE__ */ new Set(["toolDetail", "domainTools"]);
2405
2982
  var scrapeToolParamsSchema = scrapeParamsSchema.extend({
2406
- url: z7.string().url().optional(),
2407
- timeout: z7.number().int().positive().optional().describe("Execution timeout in milliseconds."),
2408
- requestId: z7.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe(
2409
- "Alexandria execution ID. Reuse the returned ID for retries of the identical payload, never a new ID to bypass pending or uncertain execution; generated when omitted. For potentially large workflow results, supply and preserve one before execution. Errors relay a code and chargeId: request_in_flight (409) retry the same requestId later; request_unresolved (503) keep the requestId for reconciliation, never mint a new one; duplicate_request (409) the requestId belongs to a different payload; unknown_provider (404), insufficient_credits (402) and billing_unavailable (503) mean nothing executed."
2983
+ url: z8.string().url().optional(),
2984
+ timeout: z8.number().int().positive().optional().describe("Execution timeout in milliseconds."),
2985
+ requestId: z8.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe(
2986
+ "Idempotency key bound to one Alexandria execution payload. Generated when omitted and returned with the result."
2410
2987
  ),
2411
- alexandria: z7.union([exchangeCallSchema, exchangeCallsSchema]).optional().describe(
2412
- "Execute catalogued Alexandria capabilities instead of scraping a URL. Exactly one of url or alexandria. One {provider, capability, options} object or an array of 1-10, found through firecrawl_search or firecrawl_find_tools. Each call may include version to pin a published workflow; omitting it uses latest. Only timeout also applies at the top level. " + ALEXANDRIA_CONTRACT_GUIDANCE + " Returns per-capability results in data.alexandria with data, records, or an error with a code; check each item even when the outer response succeeds. If a response provides nextTool, follow it to read a large result instead of repeating a successful provider call. Needs an API key on a team with Alexandria enabled. A terms-gated provider returns THIRD_PARTY_DATA_TERMS_REQUIRED (403) with requiresAction.url: follow the returned terms/show and terms/accept calls through this tool, accepting only after explicit user authorization for the reviewed version and digest; an organization admin can instead accept at the dashboard URL. Retry only after confirmed acceptance."
2988
+ alexandria: z8.union([exchangeCallSchema, exchangeCallsSchema]).optional().describe(
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."
2413
2990
  ),
2414
- toolDetail: z7.enum(["compact", "summary", "full"]).optional().describe("URL domain discovery detail: summary by default, compact returns provider/capability/description, full includes contracts."),
2415
- domainTools: z7.boolean().optional().describe(
2416
- "URL mode only: include domain-matched Alexandria tools for the page in tools on the returned document."
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."),
2992
+ domainTools: z8.boolean().optional().describe(
2993
+ "URL mode only: include domain-matched Alexandria tools for the page in tools on the returned document. Ignored with alexandria."
2417
2994
  )
2418
2995
  }).refine(
2419
2996
  (args2) => Boolean(args2.url) !== Boolean(args2.alexandria),
2420
2997
  "Provide exactly one of url or alexandria"
2421
2998
  ).refine(
2422
2999
  (args2) => !args2.alexandria || Object.entries(args2).every(
2423
- ([key, value]) => key === "alexandria" || key === "requestId" || key === "timeout" || value === void 0
3000
+ ([key, value]) => key === "alexandria" || key === "requestId" || key === "timeout" || // URL-mode discovery options that mean nothing when executing a
3001
+ // provider. Agents carry toolDetail over from firecrawl_search (where
3002
+ // it selects contract detail), so accept and ignore them rather than
3003
+ // failing the call: Codex sent toolDetail on 18 of 315 Alexandria
3004
+ // executions across the AX EXP-058 runs, each one a wasted round trip.
3005
+ ALEXANDRIA_IGNORED_SCRAPE_OPTIONS.has(key) || value === void 0
2424
3006
  ),
2425
3007
  "alexandria cannot be combined with url or other scrape options"
2426
3008
  ).refine(
2427
3009
  (args2) => !args2.requestId || !!args2.alexandria,
2428
3010
  "requestId applies to Alexandria execution only"
2429
3011
  );
2430
- var parseOptionParamsSchema = z7.object({
2431
- formats: z7.array(
2432
- z7.enum([
3012
+ var parseOptionParamsSchema = z8.object({
3013
+ formats: z8.array(
3014
+ z8.enum([
2433
3015
  "markdown",
2434
3016
  "html",
2435
3017
  "rawHtml",
@@ -2439,48 +3021,48 @@ var parseOptionParamsSchema = z7.object({
2439
3021
  "query"
2440
3022
  ])
2441
3023
  ).optional(),
2442
- jsonOptions: z7.object({
2443
- prompt: z7.string().optional(),
2444
- schema: z7.record(z7.string(), z7.any()).optional()
3024
+ jsonOptions: z8.object({
3025
+ prompt: z8.string().optional(),
3026
+ schema: z8.record(z8.string(), z8.any()).optional()
2445
3027
  }).optional(),
2446
- queryOptions: z7.object({
2447
- prompt: z7.string().max(1e4),
2448
- mode: z7.enum(["directQuote", "freeform"]).default("freeform")
3028
+ queryOptions: z8.object({
3029
+ prompt: z8.string().max(1e4),
3030
+ mode: z8.enum(["directQuote", "freeform"]).default("freeform")
2449
3031
  }).optional(),
2450
- parsers: z7.array(z7.enum(["pdf"])).optional(),
2451
- pdfOptions: z7.object({
2452
- maxPages: z7.number().int().min(1).max(1e4).optional()
3032
+ parsers: z8.array(z8.enum(["pdf"])).optional(),
3033
+ pdfOptions: z8.object({
3034
+ maxPages: z8.number().int().min(1).max(1e4).optional()
2453
3035
  }).optional(),
2454
- onlyMainContent: z7.boolean().optional(),
2455
- redactPII: z7.boolean().optional(),
2456
- includeTags: z7.array(z7.string()).optional(),
2457
- excludeTags: z7.array(z7.string()).optional(),
2458
- removeBase64Images: z7.boolean().optional(),
2459
- skipTlsVerification: z7.boolean().optional(),
2460
- storeInCache: z7.boolean().optional(),
2461
- zeroDataRetention: z7.boolean().optional(),
2462
- maxAge: z7.number().optional().describe("Ignored: parse never reuses or stores indexed content."),
2463
- proxy: z7.enum(["basic", "auto"]).optional()
3036
+ onlyMainContent: z8.boolean().optional(),
3037
+ redactPII: z8.boolean().optional(),
3038
+ includeTags: z8.array(z8.string()).optional(),
3039
+ excludeTags: z8.array(z8.string()).optional(),
3040
+ removeBase64Images: z8.boolean().optional(),
3041
+ skipTlsVerification: z8.boolean().optional(),
3042
+ storeInCache: z8.boolean().optional(),
3043
+ zeroDataRetention: z8.boolean().optional(),
3044
+ maxAge: z8.number().optional().describe("Ignored: parse never reuses or stores indexed content."),
3045
+ proxy: z8.enum(["basic", "auto"]).optional()
2464
3046
  });
2465
3047
  var localParseParamsSchema = parseOptionParamsSchema.extend({
2466
- filePath: z7.string().min(1).describe(
3048
+ filePath: z8.string().min(1).describe(
2467
3049
  "Absolute or relative path to a local file to parse. Supported: .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls"
2468
3050
  ),
2469
- contentType: z7.string().optional().describe(
3051
+ contentType: z8.string().optional().describe(
2470
3052
  "Optional MIME type override. If omitted, the server infers the file kind from the extension."
2471
3053
  )
2472
3054
  });
2473
3055
  var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2474
- filePath: z7.string().min(1).optional().describe(
3056
+ filePath: z8.string().min(1).optional().describe(
2475
3057
  "Phase 1 only: path to the local file on the caller/harness machine. Hosted MCP will not read or stat this path; it is used only to produce upload instructions."
2476
3058
  ),
2477
- uploadRef: z7.string().min(1).optional().describe(
3059
+ uploadRef: z8.string().min(1).optional().describe(
2478
3060
  "Phase 2 only: short-lived upload reference returned by phase 1 after the local PUT upload completes."
2479
3061
  ),
2480
- contentType: z7.string().optional().describe(
3062
+ contentType: z8.string().optional().describe(
2481
3063
  "Phase 1 MIME type override. If omitted, the server infers it from the file extension without reading the file."
2482
3064
  ),
2483
- declaredSizeBytes: z7.number().int().positive().optional().describe(
3065
+ declaredSizeBytes: z8.number().int().positive().optional().describe(
2484
3066
  "Optional phase 1 size declaration. Hosted MCP does not stat the file; provide this only if the caller already knows it."
2485
3067
  )
2486
3068
  }).superRefine((value, ctx) => {
@@ -2488,7 +3070,7 @@ var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2488
3070
  const hasUploadRef = typeof value.uploadRef === "string" && value.uploadRef.length > 0;
2489
3071
  if (hasFilePath === hasUploadRef) {
2490
3072
  ctx.addIssue({
2491
- code: z7.ZodIssueCode.custom,
3073
+ code: z8.ZodIssueCode.custom,
2492
3074
  message: "Hosted firecrawl_parse requires exactly one of filePath (phase 1) or uploadRef (phase 2).",
2493
3075
  path: hasFilePath && hasUploadRef ? ["uploadRef"] : ["filePath"]
2494
3076
  });
@@ -2541,6 +3123,7 @@ async function apiPostJson(pathName, body, apiKey, origin) {
2541
3123
  const response = await fetch(`${resolveApiBaseUrl()}${pathName}`, {
2542
3124
  method: "POST",
2543
3125
  headers: {
3126
+ ...AGENT_HINTS_HEADERS,
2544
3127
  "Content-Type": "application/json",
2545
3128
  Authorization: `Bearer ${apiKey}`,
2546
3129
  ...origin ? originHeaders(origin) : {}
@@ -2557,7 +3140,8 @@ async function apiPostJson(pathName, body, apiKey, origin) {
2557
3140
  if (!response.ok) {
2558
3141
  throw new CoreHttpError(
2559
3142
  parsed?.error || parsed?.message || `Firecrawl request failed (HTTP ${response.status})`,
2560
- response.status
3143
+ response.status,
3144
+ readAgentHints(parsed)
2561
3145
  );
2562
3146
  }
2563
3147
  return parsed;
@@ -2609,7 +3193,7 @@ async function executeHostedParse(args2, session, log, origin) {
2609
3193
  );
2610
3194
  }
2611
3195
  if (!hasCredential(session) && !isKeylessMode(session)) {
2612
- return asText3({
3196
+ return asText({
2613
3197
  success: false,
2614
3198
  mode: "hosted-upload-ref-auth-required",
2615
3199
  message: "Hosted firecrawl_parse requires an authenticated Firecrawl session or keyless eligibility before a local file upload URL can be minted. Connect a Firecrawl account, provide an API key, or use keyless hosted MCP while eligible, then call firecrawl_parse again."
@@ -2639,6 +3223,7 @@ async function executeHostedParse(args2, session, log, origin) {
2639
3223
  session,
2640
3224
  origin
2641
3225
  );
3226
+ const uploadHints = readAgentHints(uploadJson);
2642
3227
  const upload = parseApiData(uploadJson);
2643
3228
  if (!upload?.uploadUrl || !upload?.uploadRef) {
2644
3229
  throw new Error(
@@ -2647,8 +3232,9 @@ async function executeHostedParse(args2, session, log, origin) {
2647
3232
  }
2648
3233
  const uploadHeaders = upload.headers && Object.keys(upload.headers).length > 0 ? upload.headers : (upload.method ?? "PUT").toUpperCase() === "POST" ? {} : { "Content-Type": contentType };
2649
3234
  const uploadForCommand = { ...upload, headers: uploadHeaders };
2650
- return asText3({
3235
+ return asText({
2651
3236
  success: true,
3237
+ ...uploadHints ? { agent_hints: uploadHints } : {},
2652
3238
  mode: "hosted-upload-ref-awaiting-upload",
2653
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.",
2654
3240
  upload: {
@@ -2683,7 +3269,7 @@ async function executeHostedParse(args2, session, log, origin) {
2683
3269
  session,
2684
3270
  origin
2685
3271
  );
2686
- return asText3(parseJson);
3272
+ return asText(parseJson);
2687
3273
  }
2688
3274
  var scrapeTool = {
2689
3275
  name: "firecrawl_scrape",
@@ -2697,14 +3283,15 @@ var scrapeTool = {
2697
3283
  // Does not modify, delete, or write to external websites.
2698
3284
  },
2699
3285
  description: `
2700
- Scrape one URL and return its content: markdown by default, or HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema. Use it when the request identifies a page and needs its content or defined fields. Use \`firecrawl_search\` when additional web sources are needed, \`firecrawl_map\` to list a site's URLs, and \`firecrawl_crawl\` for a set of pages.
3286
+ Scrape one URL and return its content: markdown by default, or HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema. Use it when the request identifies a page and needs its content or defined fields. Use \`firecrawl_search\` when additional web sources are needed; on an authenticated session, \`firecrawl_map\` lists a site's URLs and \`firecrawl_crawl\` collects a set of pages.
2701
3287
 
2702
3288
  Firecrawl may serve recently indexed content; set \`maxAge: 0\` for a live fetch or a smaller \`maxAge\` to bound staleness. A successful response does not by itself confirm the page is still current. Browser actions can change the live page when interactive actions are enabled. Authenticated responses can include a \`metadata.scrapeId\` for optional scrape feedback.
2703
3289
 
2704
- On an authenticated session with Alexandria access, if you are about to scrape the same fields from several pages, first run \`firecrawl_search\` with \`sources\` unset (or \`firecrawl_find_tools\`): a matching Alexandria provider returns those fields as typed records in one call. Keyless sessions have no provider matches; scrape directly.
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.
2705
3291
 
2706
- Alexandria mode: pass \`alexandria\` instead of \`url\` to execute catalogued capabilities; the \`alexandria\` and \`requestId\` parameters describe batching, retries, errors and provider terms. Alexandria results include a \`feedbackTool\` pointer: after the task, report how the catalogue served the website through \`firecrawl_feedback\` with endpoint \`alexandria\` (free, no job ID).
3292
+ Alexandria mode, on an authenticated session with Alexandria access: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`.
2707
3293
  `,
3294
+ outputSchema: scrapeOutputSchema,
2708
3295
  parameters: scrapeToolParamsSchema,
2709
3296
  execute: async (args2, { session, log, client: mcpClient }) => {
2710
3297
  const origin = requestOrigin(mcpClient, session);
@@ -2719,7 +3306,9 @@ Alexandria mode: pass \`alexandria\` instead of \`url\` to execute catalogued ca
2719
3306
  log.info("Executing Alexandria capabilities", {
2720
3307
  count: Array.isArray(alexandria) ? alexandria.length : 1
2721
3308
  });
2722
- return executeExchangeCalls(session, alexandria, suppliedRequestId, options.timeout, origin);
3309
+ return structuredJsonText(
3310
+ await executeExchangeCalls(session, alexandria, suppliedRequestId, options.timeout, origin)
3311
+ );
2723
3312
  }
2724
3313
  const transformed = transformScrapeParams(
2725
3314
  options
@@ -2740,20 +3329,26 @@ Alexandria mode: pass \`alexandria\` instead of \`url\` to execute catalogued ca
2740
3329
  },
2741
3330
  session
2742
3331
  );
2743
- return asText3(json?.data ?? json);
3332
+ return structuredText(preserveAgentHints(json?.data ?? json, json));
2744
3333
  }
2745
3334
  const client = getClient(session);
2746
3335
  const res = await relayTermsRequired(
2747
- () => client.scrape(String(url), {
2748
- ...cleaned,
2749
- origin
2750
- }),
3336
+ () => sdkResultWithAgentHints(
3337
+ client,
3338
+ () => client.scrape(String(url), {
3339
+ ...cleaned,
3340
+ origin
3341
+ })
3342
+ ),
2751
3343
  { tool: "firecrawl_scrape" }
2752
3344
  );
2753
- return asText3(res);
3345
+ return structuredText(res);
2754
3346
  }
2755
3347
  };
2756
- server.addTool(scrapeTool);
3348
+ server.addTool({
3349
+ ...scrapeTool,
3350
+ _meta: { "anthropic/alwaysLoad": true }
3351
+ });
2757
3352
  server.addTool({
2758
3353
  name: "firecrawl_map",
2759
3354
  annotations: {
@@ -2770,28 +3365,33 @@ Enumerate URLs indexed under one website through Firecrawl without fetching each
2770
3365
 
2771
3366
  Returns matching URLs rather than page bodies. Retrieve one page with \`firecrawl_scrape\`; collect content across multiple pages with \`firecrawl_crawl\`. Authenticated responses can include an \`id\` for optional map feedback.
2772
3367
  `,
2773
- parameters: z7.object({
2774
- url: z7.string().url(),
2775
- search: z7.string().optional(),
2776
- sitemap: z7.enum(["include", "skip", "only"]).optional(),
2777
- includeSubdomains: z7.boolean().optional(),
2778
- limit: z7.number().optional(),
2779
- ignoreQueryParameters: z7.boolean().optional()
3368
+ outputSchema: mapOutputSchema,
3369
+ parameters: z8.object({
3370
+ url: z8.string().url(),
3371
+ search: z8.string().optional(),
3372
+ sitemap: z8.enum(["include", "skip", "only"]).optional(),
3373
+ includeSubdomains: z8.boolean().optional(),
3374
+ limit: z8.number().optional(),
3375
+ ignoreQueryParameters: z8.boolean().optional()
2780
3376
  }),
2781
3377
  execute: async (args2, { session, log, client: mcpClient }) => {
2782
3378
  const { url, ...options } = args2;
2783
3379
  const client = getClient(session);
2784
3380
  const cleaned = removeEmptyTopLevel(options);
2785
3381
  log.info("Mapping URL", { url: String(url) });
2786
- const res = await client.map(String(url), {
2787
- ...cleaned,
2788
- origin: requestOrigin(mcpClient, session)
2789
- });
2790
- return asText3(res);
3382
+ const res = await sdkResultWithAgentHints(
3383
+ client,
3384
+ () => client.map(String(url), {
3385
+ ...cleaned,
3386
+ origin: requestOrigin(mcpClient, session)
3387
+ })
3388
+ );
3389
+ return structuredText(res);
2791
3390
  }
2792
3391
  });
2793
3392
  server.addTool({
2794
3393
  name: "firecrawl_search",
3394
+ _meta: { "anthropic/alwaysLoad": true },
2795
3395
  annotations: {
2796
3396
  title: "Firecrawl web search",
2797
3397
  readOnlyHint: true,
@@ -2806,11 +3406,12 @@ Search web, news, or image sources and return ranked results with query-relevant
2806
3406
 
2807
3407
  ${ALEXANDRIA_SEARCH_LEAD}
2808
3408
 
2809
- Tool matches are discovery, not executed data: execute one through \`firecrawl_scrape\` with an \`alexandria\` body, or read its full contract with \`firecrawl_find_tools\`. After an Alexandria task, call firecrawl_feedback once per website with endpoint "alexandria" (free, no job ID).
3409
+ On an authenticated session, tool matches describe available capabilities; \`firecrawl_find_tools\` returns their contracts and \`firecrawl_scrape\` with an \`alexandria\` body executes a selected capability. Keyless sessions get no Alexandria matches in data.tools.
2810
3410
 
2811
3411
  For a programming question, add \`categories: ["developer"]\`; its hits return in \`data.web\` with \`category: "developer"\`. \`categories: ["research"]\` restricts web results to research-affiliated websites; the \`firecrawl_research_*\` tools are a separate surface over paper abstracts and full text (PubMed, bioRxiv, medRxiv, arXiv). Query operators, domain filters, \`categories\`, \`toolDetail\` and \`scrapeOptions\` are described on their parameters. Returns source-type result groups and usage metadata. Authenticated responses can include an \`id\` for optional search feedback.
2812
3412
  `,
2813
- parameters: z7.object({
3413
+ outputSchema: searchOutputSchema,
3414
+ parameters: z8.object({
2814
3415
  ...searchToolBaseFields,
2815
3416
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional().describe("Attach page content for web results in the same call. These fetches ignore maxAge, so use firecrawl_scrape when you need a live fetch. scrapeOptions fetches web pages, never Alexandria provider tools.")
2816
3417
  }).refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
@@ -2847,7 +3448,7 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
2847
3448
  const json = await keylessPost("/v2/search", searchBody, session);
2848
3449
  const keylessResponse = { ...json ?? {} };
2849
3450
  delete keylessResponse.id;
2850
- return compactText(keylessResponse);
3451
+ return structuredCompact(keylessResponse);
2851
3452
  }
2852
3453
  const client = getClient(session);
2853
3454
  const postSearch = () => postSearchWithFallback(
@@ -2857,7 +3458,7 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
2857
3458
  );
2858
3459
  const context = { tool: "firecrawl_search" };
2859
3460
  const httpRes = exchangeSource ? await relayExchangeError(postSearch, context) : await relayTermsRequired(postSearch, context);
2860
- return compactText(httpRes?.data ?? {});
3461
+ return structuredCompact(httpRes?.data ?? {});
2861
3462
  }
2862
3463
  });
2863
3464
  async function executeExchangeCalls(session, alexandria, suppliedRequestId, timeout, origin = requestOrigin(void 0, session)) {
@@ -2895,11 +3496,11 @@ async function executeExchangeCalls(session, alexandria, suppliedRequestId, time
2895
3496
  randomUUID(),
2896
3497
  alexandriaFeedbackAvailable(session) && alexandriaCallsWarrantFeedback(calls) ? { feedbackTool: ALEXANDRIA_FEEDBACK_HINT } : {}
2897
3498
  );
2898
- } catch (error) {
2899
- if (error?.response?.status === 401) throw error;
3499
+ } catch (error2) {
3500
+ if (error2?.response?.status === 401) throw error2;
2900
3501
  throw new UserError2(
2901
- `${error instanceof Error ? error.message : String(error)} Request ID: ${requestId}. Reuse this ID only with the identical payload.`,
2902
- { ...error instanceof UserError2 ? error.extras : {}, requestId }
3502
+ `${error2 instanceof Error ? error2.message : String(error2)} Request ID: ${requestId}. Reuse this ID only with the identical payload.`,
3503
+ { ...error2 instanceof UserError2 ? error2.extras : {}, requestId }
2903
3504
  );
2904
3505
  }
2905
3506
  }
@@ -2912,15 +3513,16 @@ var findToolsTool = {
2912
3513
  destructiveHint: false,
2913
3514
  idempotentHint: true
2914
3515
  },
2915
- description: "Browse Alexandria data providers and workflows or read a selected contract. Alexandria covers " + ALEXANDRIA_CATALOGUE_VERTICALS + ": typed, sourced records through published contracts. Prefer normal firecrawl_search for a data task; it already returns matching providers. Use this tool when the contract you need was not returned in full, to browse a category when search found nothing, or before scraping the same fields from several pages. Discovery is free. Use query for semantic discovery or urls to find providers for a website. With no arguments, browse categories, then providers and tools. Inspect a selected contract before executing through firecrawl_scrape; reuse contracts already returned. Follow nextTool for further discovery or pagination. Discovery does not execute providers. Use firecrawl_search when you also need web results. Results include a feedbackTool pointer: after the task, report how the catalogue served the website through firecrawl_feedback with endpoint alexandria, including when nothing covered it (free, no job ID).",
3516
+ description: "Browse Alexandria data providers and workflows or read a selected contract. Alexandria covers " + ALEXANDRIA_CATALOGUE_VERTICALS + ": typed, sourced records through published contracts. Prefer normal firecrawl_search for a data task; it already returns matching providers. Use this tool when the contract you need was not returned in full, to browse a category when search found nothing, or before scraping the same fields from several pages. Discovery is free. Use query for semantic discovery or urls to find providers for a website. With no arguments, browse categories, then providers and tools. Contracts describe the inputs and outputs for execution through firecrawl_scrape. nextTool identifies further discovery or pagination. Discovery does not execute providers. Use firecrawl_search when you also need web results.",
3517
+ outputSchema: findToolsOutputSchema,
2916
3518
  parameters: findToolsSchema,
2917
3519
  execute: async (args2, { session, client: mcpClient }) => {
2918
3520
  const origin = requestOrigin(mcpClient, session);
2919
3521
  let options;
2920
3522
  try {
2921
3523
  options = findToolsOptions(args2);
2922
- } catch (error) {
2923
- throw new UserError2(error instanceof Error ? error.message : String(error));
3524
+ } catch (error2) {
3525
+ throw new UserError2(error2 instanceof Error ? error2.message : String(error2));
2924
3526
  }
2925
3527
  if (options.level === "categories") {
2926
3528
  assertExchangeCredential(session);
@@ -2940,25 +3542,26 @@ var findToolsTool = {
2940
3542
  }));
2941
3543
  const page = { level: "categories", items, total: rows.length };
2942
3544
  if (offset + options.limit < rows.length) page.nextTool = { name: "firecrawl_find_tools", arguments: { level: "categories", limit: options.limit, offset: offset + options.limit } };
2943
- return compactText(withAlexandriaFeedbackHint(withFindToolsNavigation({ success: true, data: { creditsCost: 0, alexandria: [{ provider: "firecrawl", capability: "find-tools", creditsCost: 0, data: page }] } }), alexandriaFeedbackAvailable(session)));
3545
+ return structuredCompact(withAlexandriaFeedbackHint(withFindToolsNavigation({ success: true, data: { creditsCost: 0, alexandria: [{ provider: "firecrawl", capability: "find-tools", creditsCost: 0, data: page }] } }), alexandriaFeedbackAvailable(session)));
2944
3546
  }
2945
3547
  const result = await executeExchangeCalls(session, { provider: "firecrawl", capability: "find-tools", options }, void 0, void 0, origin);
2946
- return compactText(withFindToolsNavigation(JSON.parse(result)));
3548
+ return structuredCompact(withFindToolsNavigation(JSON.parse(result)));
2947
3549
  }
2948
3550
  };
2949
3551
  server.addTool(findToolsTool);
2950
3552
  var SEARCH_SURFACE_SCRAPE_DESCRIPTION = `
2951
3553
  Scrape one URL and return its content, or execute catalogued Alexandria capabilities. URL mode returns markdown by default, or HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema, plus page metadata. Firecrawl may serve recently indexed content; set \`maxAge: 0\` for a live fetch. A successful response does not by itself confirm the page is still current. Browser actions can change the live page when interactive actions are enabled, and a named browser profile can load saved session data and overwrite its stored state.
2952
3554
 
2953
- Before scraping the same fields from several pages, first run \`firecrawl_search\` with \`sources\` unset (or \`firecrawl_find_tools\`): a matching Alexandria provider returns those fields as typed records in one call.
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.
2954
3556
 
2955
- Alexandria mode: pass \`alexandria\` instead of \`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.
2956
3558
  `;
2957
3559
  var searchSurfaceScrapeTool = {
2958
3560
  ...scrapeTool,
3561
+ _meta: { "anthropic/alwaysLoad": true },
2959
3562
  description: SEARCH_SURFACE_SCRAPE_DESCRIPTION
2960
3563
  };
2961
- var SEARCH_SURFACE_FIND_TOOLS_DESCRIPTION = "Browse Alexandria data providers and workflows or read a selected contract. Alexandria covers " + ALEXANDRIA_CATALOGUE_VERTICALS + ": typed, sourced records through published contracts. Prefer normal firecrawl_search for a data task; it already returns matching providers. Use this tool when the contract you need was not returned in full, to browse a category when search found nothing, or before scraping the same fields from several pages. Discovery is free. Use query for semantic discovery or urls to find providers for a website. With no arguments, browse categories, then providers and tools. Inspect a selected contract before executing through firecrawl_scrape; reuse contracts already returned. Follow nextTool for further discovery or pagination. Discovery does not execute providers. Use firecrawl_search when you also need web results.";
3564
+ var SEARCH_SURFACE_FIND_TOOLS_DESCRIPTION = "Browse Alexandria data providers and workflows or read a selected contract. Alexandria covers " + ALEXANDRIA_CATALOGUE_VERTICALS + ": typed, sourced records through published contracts. Prefer normal firecrawl_search for a data task; it already returns matching providers. Use this tool when the contract you need was not returned in full, to browse a category when search found nothing, or before scraping the same fields from several pages. Discovery is free. Use query for semantic discovery or urls to find providers for a website. With no arguments, browse categories, then providers and tools. Contracts describe the inputs and outputs for execution through firecrawl_scrape. nextTool identifies further discovery or pagination. Discovery does not execute providers. Use firecrawl_search when you also need web results.";
2962
3565
  var searchSurfaceFindToolsTool = {
2963
3566
  ...findToolsTool,
2964
3567
  description: SEARCH_SURFACE_FIND_TOOLS_DESCRIPTION
@@ -3024,6 +3627,7 @@ async function keylessPost(path2, body, session, originOverride) {
3024
3627
  }
3025
3628
  }
3026
3629
  const headers = {
3630
+ ...AGENT_HINTS_HEADERS,
3027
3631
  ...originHeaders(origin),
3028
3632
  "Content-Type": "application/json"
3029
3633
  };
@@ -3043,10 +3647,14 @@ async function keylessPost(path2, body, session, originOverride) {
3043
3647
  const payload = recoveryPayload(code, session?.requestId, {
3044
3648
  retryAfterSeconds: Number.isFinite(json?.retry_after_seconds) && json.retry_after_seconds > 0 ? json.retry_after_seconds : void 0
3045
3649
  });
3650
+ const hints = readAgentHints(json);
3651
+ if (hints) payload.agent_hints = hints;
3046
3652
  throw new UserError2(String(payload.message), payload);
3047
3653
  }
3048
- throw new Error(
3049
- 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)
3050
3658
  );
3051
3659
  }
3052
3660
  return json;
@@ -3067,7 +3675,8 @@ async function getCrawlStatusWithOrigin(client, jobId, origin) {
3067
3675
  creditsUsed: body.creditsUsed,
3068
3676
  expiresAt: body.expiresAt,
3069
3677
  next: body.next ?? null,
3070
- data: initialDocs
3678
+ data: initialDocs,
3679
+ ...readAgentHints(body) ? { agent_hints: readAgentHints(body) } : {}
3071
3680
  };
3072
3681
  }
3073
3682
  const docs = initialDocs.slice();
@@ -3091,7 +3700,8 @@ async function getCrawlStatusWithOrigin(client, jobId, origin) {
3091
3700
  creditsUsed: body.creditsUsed,
3092
3701
  expiresAt: body.expiresAt,
3093
3702
  next: null,
3094
- data: docs
3703
+ data: docs,
3704
+ ...readAgentHints(body) ? { agent_hints: readAgentHints(body) } : {}
3095
3705
  };
3096
3706
  }
3097
3707
  async function waitForCrawlCompletionWithOrigin(client, jobId, origin, pollInterval = 2, timeout) {
@@ -3109,17 +3719,17 @@ async function waitForCrawlCompletionWithOrigin(client, jobId, origin, pollInter
3109
3719
  );
3110
3720
  }
3111
3721
  }
3112
- var feedbackIssueSchema = z7.string().trim().min(1).max(80).regex(
3722
+ var feedbackIssueSchema = z8.string().trim().min(1).max(80).regex(
3113
3723
  /^[a-z0-9][a-z0-9_-]*$/,
3114
3724
  "Issue codes must use lowercase letters, numbers, underscores, or hyphens"
3115
3725
  );
3116
- var valuableSourceSchema = z7.object({
3117
- url: z7.string().url(),
3118
- reason: z7.string().max(1e3).optional()
3726
+ var valuableSourceSchema = z8.object({
3727
+ url: z8.string().url(),
3728
+ reason: z8.string().max(1e3).optional()
3119
3729
  });
3120
- var missingContentSchema = z7.object({
3121
- topic: z7.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3122
- description: z7.string().max(2e3).optional()
3730
+ var missingContentSchema = z8.object({
3731
+ topic: z8.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3732
+ description: z8.string().max(2e3).optional()
3123
3733
  });
3124
3734
  var FEEDBACK_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
3125
3735
  function feedbackEnvEnabled(...keys) {
@@ -3157,24 +3767,25 @@ Records schema-validated quality feedback for a prior \`firecrawl_search\` UUID
3157
3767
 
3158
3768
  Eligibility is limited to successful searches within the feedback age window. The record is idempotent per search ID. Eligible first feedback for a search can refund 1 credit; refunds are subject to the team's daily cap. The response reports whether a refund was applied, along with submission and daily-cap status.
3159
3769
  `,
3160
- parameters: z7.object({
3161
- searchId: z7.string().uuid("searchId must be the UUID returned by firecrawl_search"),
3162
- rating: z7.enum(["good", "bad", "partial"]),
3163
- valuableSources: z7.array(
3164
- z7.object({
3165
- url: z7.string().url(),
3166
- reason: z7.string().max(1e3).optional()
3770
+ outputSchema: feedbackOutputSchema,
3771
+ parameters: z8.object({
3772
+ searchId: z8.string().uuid("searchId must be the UUID returned by firecrawl_search"),
3773
+ rating: z8.enum(["good", "bad", "partial"]),
3774
+ valuableSources: z8.array(
3775
+ z8.object({
3776
+ url: z8.string().url(),
3777
+ reason: z8.string().max(1e3).optional()
3167
3778
  })
3168
3779
  ).max(50).optional(),
3169
- missingContent: z7.array(
3170
- z7.object({
3171
- topic: z7.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3172
- description: z7.string().max(2e3).optional()
3780
+ missingContent: z8.array(
3781
+ z8.object({
3782
+ topic: z8.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3783
+ description: z8.string().max(2e3).optional()
3173
3784
  })
3174
3785
  ).max(20).optional().describe(
3175
3786
  "Array of specific pieces of content the agent expected to find but did not. One entry per distinct topic. Each entry has a short `topic` and optional longer `description`."
3176
3787
  ),
3177
- querySuggestions: z7.string().max(2e3).optional()
3788
+ querySuggestions: z8.string().max(2e3).optional()
3178
3789
  }),
3179
3790
  execute: async (args2, { session, log, client: mcpClient }) => {
3180
3791
  const origin = requestOrigin(mcpClient, session);
@@ -3201,6 +3812,7 @@ Eligibility is limited to successful searches within the feedback age window. Th
3201
3812
  }
3202
3813
  if (querySuggestions) body.querySuggestions = querySuggestions;
3203
3814
  const headers = {
3815
+ ...AGENT_HINTS_HEADERS,
3204
3816
  ...originHeaders(origin),
3205
3817
  "Content-Type": "application/json"
3206
3818
  };
@@ -3234,15 +3846,16 @@ Eligibility is limited to successful searches within the feedback age window. Th
3234
3846
  status: response.status,
3235
3847
  feedbackErrorCode: parsed?.feedbackErrorCode
3236
3848
  });
3237
- return asText3({
3849
+ return structuredText({
3238
3850
  success: false,
3239
3851
  status: response.status,
3240
3852
  feedbackErrorCode: parsed?.feedbackErrorCode,
3241
3853
  error: parsed?.error ?? `HTTP ${response.status}`,
3242
- retryable: response.status >= 500
3854
+ retryable: response.status >= 500,
3855
+ ...readAgentHints(parsed) ? { agent_hints: readAgentHints(parsed) } : {}
3243
3856
  });
3244
3857
  }
3245
- return asText3(parsed);
3858
+ return structuredText(parsed);
3246
3859
  }
3247
3860
  });
3248
3861
  }
@@ -3273,20 +3886,21 @@ For an Alexandria session, set endpoint to \`alexandria\`, omit jobId, and provi
3273
3886
 
3274
3887
  Returns submission status, feedback ID, and accounting fields.
3275
3888
  `,
3276
- parameters: z7.object({
3277
- endpoint: z7.enum(["search", "scrape", "parse", "map", "alexandria"]),
3278
- jobId: z7.string().uuid("jobId must be the UUID returned by Firecrawl").optional(),
3889
+ outputSchema: feedbackOutputSchema,
3890
+ parameters: z8.object({
3891
+ endpoint: z8.enum(["search", "scrape", "parse", "map", "alexandria"]),
3892
+ jobId: z8.string().uuid("jobId must be the UUID returned by Firecrawl").optional(),
3279
3893
  ...alexandriaFeedbackFields,
3280
- rating: z7.enum(["good", "bad", "partial"]),
3281
- issues: z7.array(feedbackIssueSchema).max(20).optional(),
3282
- tags: z7.array(feedbackIssueSchema).max(20).optional(),
3283
- note: z7.string().max(4e3).optional(),
3284
- valuableSources: z7.array(valuableSourceSchema).max(50).optional(),
3285
- missingContent: z7.array(missingContentSchema).max(50).optional(),
3286
- querySuggestions: z7.string().max(2e3).optional(),
3287
- url: z7.string().url().optional(),
3288
- pageNumbers: z7.array(z7.number().int().positive()).max(100).optional(),
3289
- metadata: z7.record(z7.string(), z7.unknown()).optional()
3894
+ rating: z8.enum(["good", "bad", "partial"]),
3895
+ issues: z8.array(feedbackIssueSchema).max(20).optional(),
3896
+ tags: z8.array(feedbackIssueSchema).max(20).optional(),
3897
+ note: z8.string().max(4e3).optional(),
3898
+ valuableSources: z8.array(valuableSourceSchema).max(50).optional(),
3899
+ missingContent: z8.array(missingContentSchema).max(50).optional(),
3900
+ querySuggestions: z8.string().max(2e3).optional(),
3901
+ url: z8.string().url().optional(),
3902
+ pageNumbers: z8.array(z8.number().int().positive()).max(100).optional(),
3903
+ metadata: z8.record(z8.string(), z8.unknown()).optional()
3290
3904
  }).superRefine((value, ctx) => {
3291
3905
  if (value.endpoint === "alexandria") {
3292
3906
  const parsed = alexandriaSessionFeedbackSchema.safeParse(value);
@@ -3320,6 +3934,7 @@ Returns submission status, feedback ID, and accounting fields.
3320
3934
  } = args2;
3321
3935
  const apiBase = resolveApiBaseUrl();
3322
3936
  const headers = {
3937
+ ...AGENT_HINTS_HEADERS,
3323
3938
  ...originHeaders(origin),
3324
3939
  "Content-Type": "application/json"
3325
3940
  };
@@ -3368,15 +3983,16 @@ Returns submission status, feedback ID, and accounting fields.
3368
3983
  status: response.status,
3369
3984
  feedbackErrorCode: parsed?.feedbackErrorCode
3370
3985
  });
3371
- return asText3({
3986
+ return structuredText({
3372
3987
  success: false,
3373
3988
  status: response.status,
3374
3989
  feedbackErrorCode: parsed?.feedbackErrorCode,
3375
3990
  error: parsed?.error ?? `HTTP ${response.status}`,
3376
- retryable: response.status >= 500
3991
+ retryable: response.status >= 500,
3992
+ ...readAgentHints(parsed) ? { agent_hints: readAgentHints(parsed) } : {}
3377
3993
  });
3378
3994
  }
3379
- return asText3(parsed);
3995
+ return structuredText(parsed);
3380
3996
  }
3381
3997
  });
3382
3998
  }
@@ -3396,27 +4012,28 @@ Start a multi-page crawl at a website URL, poll it to a terminal state, and retu
3396
4012
 
3397
4013
  Crawl results can be large; use conservative limits when full-site coverage is unnecessary. Webhooks and interactive scrape actions are unavailable in safe mode. Returns the crawl ID, status, and page data.
3398
4014
  `,
3399
- parameters: z7.object({
3400
- url: z7.string(),
3401
- prompt: z7.string().optional(),
3402
- excludePaths: z7.array(z7.string()).optional(),
3403
- includePaths: z7.array(z7.string()).optional(),
3404
- maxDiscoveryDepth: z7.number().optional(),
3405
- sitemap: z7.enum(["skip", "include", "only"]).optional(),
3406
- limit: z7.number().optional(),
3407
- allowExternalLinks: z7.boolean().optional(),
3408
- allowSubdomains: z7.boolean().optional(),
3409
- crawlEntireDomain: z7.boolean().optional(),
3410
- delay: z7.number().optional(),
3411
- maxConcurrency: z7.number().optional(),
4015
+ parameters: z8.object({
4016
+ url: z8.string(),
4017
+ prompt: z8.string().optional(),
4018
+ excludePaths: z8.array(z8.string()).optional(),
4019
+ includePaths: z8.array(z8.string()).optional(),
4020
+ maxDiscoveryDepth: z8.number().optional(),
4021
+ sitemap: z8.enum(["skip", "include", "only"]).optional(),
4022
+ limit: z8.number().optional(),
4023
+ allowExternalLinks: z8.boolean().optional(),
4024
+ allowSubdomains: z8.boolean().optional(),
4025
+ crawlEntireDomain: z8.boolean().optional(),
4026
+ delay: z8.number().optional(),
4027
+ maxConcurrency: z8.number().optional(),
3412
4028
  ...SAFE_MODE ? {} : {
3413
- webhook: z7.string().optional(),
3414
- webhookHeaders: z7.record(z7.string(), z7.string()).optional()
4029
+ webhook: z8.string().optional(),
4030
+ webhookHeaders: z8.record(z8.string(), z8.string()).optional()
3415
4031
  },
3416
- deduplicateSimilarURLs: z7.boolean().optional(),
3417
- ignoreQueryParameters: z7.boolean().optional(),
4032
+ deduplicateSimilarURLs: z8.boolean().optional(),
4033
+ ignoreQueryParameters: z8.boolean().optional(),
3418
4034
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3419
4035
  }),
4036
+ outputSchema: crawlOutputSchema,
3420
4037
  execute: async (args2, { session, log, client: mcpClient }) => {
3421
4038
  const origin = requestOrigin(mcpClient, session);
3422
4039
  const { url, ...options } = args2;
@@ -3443,7 +4060,7 @@ Crawl results can be large; use conservative limits when full-site coverage is u
3443
4060
  });
3444
4061
  const crawlId = started?.data?.id;
3445
4062
  if (!crawlId) {
3446
- return asText3(started?.data ?? {});
4063
+ return structuredText(started?.data ?? {});
3447
4064
  }
3448
4065
  const res = await waitForCrawlCompletionWithOrigin(
3449
4066
  client,
@@ -3452,7 +4069,7 @@ Crawl results can be large; use conservative limits when full-site coverage is u
3452
4069
  pollInterval,
3453
4070
  timeout
3454
4071
  );
3455
- return asText3(res);
4072
+ return structuredText(res);
3456
4073
  }
3457
4074
  });
3458
4075
  server.addTool({
@@ -3469,7 +4086,8 @@ server.addTool({
3469
4086
  description: `
3470
4087
  Retrieve the current status, progress, and available results for an existing crawl ID. This only reads Firecrawl job state and does not start or modify the crawl.
3471
4088
  `,
3472
- parameters: z7.object({ id: z7.string() }),
4089
+ outputSchema: crawlOutputSchema,
4090
+ parameters: z8.object({ id: z8.string() }),
3473
4091
  execute: async (args2, {
3474
4092
  session,
3475
4093
  client: mcpClient
@@ -3481,7 +4099,7 @@ Retrieve the current status, progress, and available results for an existing cra
3481
4099
  id,
3482
4100
  requestOrigin(mcpClient, session)
3483
4101
  );
3484
- return asText3(res);
4102
+ return structuredText(res);
3485
4103
  }
3486
4104
  });
3487
4105
  server.addTool({
@@ -3495,13 +4113,14 @@ server.addTool({
3495
4113
  description: `
3496
4114
  Deprecated compatibility entry point. Use firecrawl_scrape once per known URL with formats: ["json"] and jsonOptions containing the prompt and schema. Use firecrawl_agent for multi-source research when the URLs are not known or the data spans several sites.
3497
4115
  `,
3498
- parameters: z7.object({
3499
- urls: z7.array(z7.string()),
3500
- prompt: z7.string().optional(),
3501
- schema: z7.record(z7.string(), z7.any()).optional(),
3502
- allowExternalLinks: z7.boolean().optional(),
3503
- enableWebSearch: z7.boolean().optional(),
3504
- includeSubdomains: z7.boolean().optional()
4116
+ outputSchema: deprecatedToolOutputSchema,
4117
+ parameters: z8.object({
4118
+ urls: z8.array(z8.string()),
4119
+ prompt: z8.string().optional(),
4120
+ schema: z8.record(z8.string(), z8.any()).optional(),
4121
+ allowExternalLinks: z8.boolean().optional(),
4122
+ enableWebSearch: z8.boolean().optional(),
4123
+ includeSubdomains: z8.boolean().optional()
3505
4124
  }),
3506
4125
  canList: () => false,
3507
4126
  beforeValidate: () => {
@@ -3517,6 +4136,29 @@ Deprecated compatibility entry point. Use firecrawl_scrape once per known URL wi
3517
4136
  throw new UserError2(payload.message, payload);
3518
4137
  }
3519
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
+ );
3520
4162
  server.addTool({
3521
4163
  name: "firecrawl_agent",
3522
4164
  annotations: {
@@ -3529,32 +4171,80 @@ server.addTool({
3529
4171
  // Gathers information only; does not delete external data or user resources.
3530
4172
  },
3531
4173
  description: `
3532
- 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\`.
3533
4175
 
3534
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.
3535
4181
  `,
3536
- parameters: z7.object({
3537
- prompt: z7.string().min(1).max(1e4),
3538
- urls: z7.array(z7.string().url()).optional(),
3539
- schema: z7.record(z7.string(), z7.any()).optional()
3540
- }),
4182
+ outputSchema: agentOutputSchema,
4183
+ parameters: z8.object({
4184
+ prompt: z8.string().min(1).max(1e4),
4185
+ urls: z8.array(z8.string().url()).optional(),
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
+ ),
3541
4222
  execute: async (args2, { session, log, client: mcpClient }) => {
3542
4223
  const client = getClient(session);
3543
4224
  const a = args2;
3544
4225
  log.info("Starting agent", {
3545
4226
  prompt: a.prompt.substring(0, 100),
3546
- urlCount: Array.isArray(a.urls) ? a.urls.length : 0
4227
+ urlCount: Array.isArray(a.urls) ? a.urls.length : 0,
4228
+ threadId: a.threadId ?? null
3547
4229
  });
3548
4230
  const agentBody = removeEmptyTopLevel({
3549
4231
  prompt: a.prompt,
3550
4232
  urls: a.urls,
3551
- 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
3552
4242
  });
3553
4243
  const res = await client.startAgent({
3554
4244
  ...agentBody,
3555
4245
  origin: requestOrigin(mcpClient, session)
3556
4246
  });
3557
- return asText3(res);
4247
+ return structuredText(res);
3558
4248
  }
3559
4249
  });
3560
4250
  server.addTool({
@@ -3573,7 +4263,8 @@ Retrieve progress or final results for a \`firecrawl_agent\` job ID. A \`process
3573
4263
 
3574
4264
  Returns job status, progress information, and result data when completed.
3575
4265
  `,
3576
- parameters: z7.object({ id: z7.string() }),
4266
+ outputSchema: agentStatusOutputSchema,
4267
+ parameters: z8.object({ id: z8.string() }),
3577
4268
  execute: async (args2, { session, log, client: mcpClient }) => {
3578
4269
  const client = getClient(session);
3579
4270
  const { id } = args2;
@@ -3582,7 +4273,7 @@ Returns job status, progress information, and result data when completed.
3582
4273
  `/v2/agent/${encodeURIComponent(id)}`,
3583
4274
  originHeaders(requestOrigin(mcpClient, session))
3584
4275
  );
3585
- return asText3(res?.data ?? {});
4276
+ return structuredText(res?.data ?? {});
3586
4277
  }
3587
4278
  });
3588
4279
  server.addTool({
@@ -3601,13 +4292,14 @@ Open or reuse a live browser session to navigate a page, click controls, fill fi
3601
4292
 
3602
4293
  This acts on the live site, so actions such as form submission can create persistent external side effects. Returns execution output, stdout/stderr, exit status, and session viewing URLs.
3603
4294
  `,
3604
- parameters: z7.object({
3605
- scrapeId: z7.string().trim().min(1).optional(),
3606
- url: z7.string().trim().url().optional(),
3607
- prompt: z7.string().trim().min(1).optional(),
3608
- code: z7.string().trim().min(1).optional(),
3609
- language: z7.enum(["bash", "python", "node"]).optional(),
3610
- timeout: z7.number().min(1).max(300).optional(),
4295
+ outputSchema: interactOutputSchema,
4296
+ parameters: z8.object({
4297
+ scrapeId: z8.string().trim().min(1).optional(),
4298
+ url: z8.string().trim().url().optional(),
4299
+ prompt: z8.string().trim().min(1).optional(),
4300
+ code: z8.string().trim().min(1).optional(),
4301
+ language: z8.enum(["bash", "python", "node"]).optional(),
4302
+ timeout: z8.number().min(1).max(300).optional(),
3611
4303
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3612
4304
  }).refine((data) => Boolean(data.scrapeId) !== Boolean(data.url), {
3613
4305
  message: "Provide either 'url' (interact directly) or 'scrapeId' (reuse a previous scrape), not both."
@@ -3639,14 +4331,14 @@ This acts on the live site, so actions such as form submission can create persis
3639
4331
  });
3640
4332
  scrapeId = scraped?.metadata?.scrapeId;
3641
4333
  if (!scrapeId) {
3642
- return asText3({
4334
+ return structuredText({
3643
4335
  error: "Could not open an interact session: the scrape did not return a scrapeId. Try firecrawl_scrape first, then pass its scrapeId.",
3644
4336
  url
3645
4337
  });
3646
4338
  }
3647
4339
  }
3648
4340
  if (!scrapeId) {
3649
- return asText3({
4341
+ return structuredText({
3650
4342
  error: "Could not open an interact session: missing scrapeId.",
3651
4343
  url
3652
4344
  });
@@ -3660,15 +4352,15 @@ This acts on the live site, so actions such as form submission can create persis
3660
4352
  if (timeout != null) interactArgs.timeout = timeout;
3661
4353
  const res = await client.interact(activeScrapeId, interactArgs);
3662
4354
  if (openedFromUrl && res && typeof res === "object" && !Array.isArray(res)) {
3663
- return asText3({
4355
+ return structuredText({
3664
4356
  ...res,
3665
4357
  scrapeId: activeScrapeId
3666
4358
  });
3667
4359
  }
3668
4360
  if (openedFromUrl) {
3669
- return asText3({ scrapeId: activeScrapeId, result: res });
4361
+ return structuredText({ scrapeId: activeScrapeId, result: res });
3670
4362
  }
3671
- return asText3(res);
4363
+ return structuredText(res);
3672
4364
  }
3673
4365
  });
3674
4366
  server.addTool({
@@ -3685,8 +4377,9 @@ server.addTool({
3685
4377
  description: `
3686
4378
  Stop the live interact session associated with a \`scrapeId\` and release its resources. Returns a success confirmation.
3687
4379
  `,
3688
- parameters: z7.object({
3689
- scrapeId: z7.string()
4380
+ outputSchema: interactStopOutputSchema,
4381
+ parameters: z8.object({
4382
+ scrapeId: z8.string()
3690
4383
  }),
3691
4384
  execute: async (args2, { session, log, client: mcpClient }) => {
3692
4385
  const client = getClient(session);
@@ -3696,7 +4389,7 @@ Stop the live interact session associated with a \`scrapeId\` and release its re
3696
4389
  `/v2/scrape/${encodeURIComponent(scrapeId)}/interact`,
3697
4390
  originHeaders(requestOrigin(mcpClient, session))
3698
4391
  );
3699
- return asText3(res?.data ?? {});
4392
+ return structuredText(res?.data ?? {});
3700
4393
  }
3701
4394
  });
3702
4395
  server.addTool({
@@ -3717,11 +4410,14 @@ Local MCP reads \`filePath\` from the server filesystem. Hosted MCP uses two cal
3717
4410
 
3718
4411
  Set \`redactPII\` to request redaction of personally identifiable information in the returned content. \`zeroDataRetention\` requires an eligible authenticated account; omit it for anonymous keyless use. Returns upload instructions for hosted phase one or parsed document content for the final call. Authenticated final responses can include a \`data.metadata.scrapeId\` for optional parse feedback.
3719
4412
  `,
4413
+ outputSchema: parseOutputSchema,
3720
4414
  parameters: parseParamsSchema,
3721
4415
  execute: async (args2, { session, log, client: mcpClient }) => {
3722
4416
  const origin = requestOrigin(mcpClient, session);
3723
4417
  if (process.env.CLOUD_SERVICE === "true") {
3724
- return executeHostedParse(args2, session, log, origin);
4418
+ return structuredJsonText(
4419
+ await executeHostedParse(args2, session, log, origin)
4420
+ );
3725
4421
  }
3726
4422
  const apiUrl = process.env.FIRECRAWL_API_URL;
3727
4423
  if (!apiUrl) {
@@ -3748,7 +4444,10 @@ Set \`redactPII\` to request redaction of personally identifiable information in
3748
4444
  });
3749
4445
  form.append("file", blob, filename);
3750
4446
  form.append("options", JSON.stringify(optionsPayload));
3751
- const headers = { ...originHeaders(origin) };
4447
+ const headers = {
4448
+ ...AGENT_HINTS_HEADERS,
4449
+ ...originHeaders(origin)
4450
+ };
3752
4451
  const credential = credentialForOutboundRequest(session);
3753
4452
  if (credential) {
3754
4453
  headers["Authorization"] = `Bearer ${credential}`;
@@ -3766,20 +4465,28 @@ Set \`redactPII\` to request redaction of personally identifiable information in
3766
4465
  });
3767
4466
  const responseText = await response.text();
3768
4467
  if (!response.ok) {
3769
- throw new Error(
3770
- `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)
3771
4477
  );
3772
4478
  }
3773
4479
  try {
3774
- return asText3(JSON.parse(responseText));
4480
+ return structuredText(JSON.parse(responseText));
3775
4481
  } catch {
3776
- return responseText;
4482
+ return withStructured(responseText, { raw: responseText });
3777
4483
  }
3778
4484
  }
3779
4485
  });
3780
4486
  function registerMarketplaceSearchTool(registrar, getClientFn) {
3781
4487
  registrar.addTool({
3782
4488
  name: "firecrawl_search",
4489
+ _meta: { "anthropic/alwaysLoad": true },
3783
4490
  annotations: {
3784
4491
  title: "Firecrawl web search",
3785
4492
  readOnlyHint: true,
@@ -3795,7 +4502,8 @@ ${ALEXANDRIA_SEARCH_INSTRUCTIONS}
3795
4502
 
3796
4503
  Returns result groups in \`data\` and an operation \`id\`.
3797
4504
  `,
3798
- parameters: z7.object({ ...searchToolBaseFields }).strict().refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
4505
+ outputSchema: searchOutputSchema,
4506
+ parameters: z8.object({ ...searchToolBaseFields }).strict().refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
3799
4507
  searchQueryIsValid,
3800
4508
  "A query is required. Use firecrawl_find_tools to browse the catalogue."
3801
4509
  ),
@@ -3846,7 +4554,7 @@ Returns result groups in \`data\` and an operation \`id\`.
3846
4554
  );
3847
4555
  const context = { tool: "firecrawl_search" };
3848
4556
  const httpRes = exchangeSource ? await relayExchangeError(postSearch, context) : await relayTermsRequired(postSearch, context);
3849
- return compactText(httpRes?.data ?? {});
4557
+ return structuredCompact(httpRes?.data ?? {});
3850
4558
  }
3851
4559
  });
3852
4560
  }
@@ -3918,10 +4626,10 @@ if (searchProfileEnabled) {
3918
4626
  stateless: true
3919
4627
  }
3920
4628
  });
3921
- } catch (error) {
4629
+ } catch (error2) {
3922
4630
  console.error(
3923
4631
  `[search-profile] failed to start on port ${searchProfile.port}; the full surface is unaffected`,
3924
- error
4632
+ error2
3925
4633
  );
3926
4634
  }
3927
4635
  }