firecrawl-mcp 3.25.3 → 3.25.5

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +724 -412
  3. package/package.json +1 -1
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,295 @@ 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/tool-output.ts
92
91
  import { z as z2 } from "zod";
92
+ function isRecord(value) {
93
+ return typeof value === "object" && value !== null && !Array.isArray(value);
94
+ }
95
+ function withStructured(text, data) {
96
+ return isRecord(data) ? { content: [{ type: "text", text }], structuredContent: data } : { content: [{ type: "text", text }] };
97
+ }
98
+ function structuredText(data) {
99
+ return withStructured(JSON.stringify(data, null, 2), data);
100
+ }
101
+ function structuredCompact(data) {
102
+ return withStructured(JSON.stringify(data), data);
103
+ }
104
+ function structuredJsonText(text) {
105
+ try {
106
+ return withStructured(text, JSON.parse(text));
107
+ } catch {
108
+ return { content: [{ type: "text", text }] };
109
+ }
110
+ }
111
+ var str = (description) => z2.string().nullable().optional().describe(description);
112
+ var num = (description) => z2.number().nullable().optional().describe(description);
113
+ var bool = (description) => z2.boolean().nullable().optional().describe(description);
114
+ var unknown = (description) => z2.unknown().optional().describe(description);
115
+ var success = bool("Whether the API call succeeded.");
116
+ var error = unknown("Error message or error object when the call did not succeed.");
117
+ var warning = str("Non-fatal warning about the result.");
118
+ var alexandriaEnvelope = {
119
+ requestId: str("Identifier of this logical execution; reuse it only for a retry of the identical payload."),
120
+ scrape_id: str("Identifier of the underlying scrape."),
121
+ receipt: unknown("Billing receipt for the execution."),
122
+ creditsCost: num("Credits this call consumed."),
123
+ delivery: str("`retained` when the full result stayed server-side instead of being inlined."),
124
+ responseBytes: num("Size of the full result in bytes."),
125
+ estimatedTokens: num("Estimated token cost of the full result."),
126
+ tokenEstimateMethod: str("How `estimatedTokens` was derived."),
127
+ inlineTokenBudget: num("Token budget above which a result is retained rather than inlined."),
128
+ workspaceId: str("Workspace holding a retained result, for inspection through virtual Bash."),
129
+ idleTtlSeconds: num("Seconds a retained workspace stays available while idle."),
130
+ message: str("Guidance that accompanies the result."),
131
+ nextTool: unknown("A follow-up tool call (`{name, arguments}`) that continues or inspects this result."),
132
+ feedbackTool: unknown("Pointer to the feedback tool for reporting how this result served the task.")
133
+ };
134
+ var scrapeOutputSchema = z2.object({
135
+ // URL mode: the scraped document, with the formats that were requested.
136
+ markdown: str("Page content as markdown."),
137
+ html: str("Processed HTML of the page."),
138
+ rawHtml: str("Unprocessed HTML of the page."),
139
+ summary: str("Summary of the page content."),
140
+ links: unknown("Links found on the page."),
141
+ screenshot: unknown("Screenshot of the page."),
142
+ images: unknown("Images found on the page."),
143
+ audio: unknown("Audio extracted from the page."),
144
+ video: unknown("Video extracted from the page."),
145
+ answer: str("Targeted answer to the question that was asked of the page."),
146
+ highlights: unknown("Highlighted passages from the page."),
147
+ pages: unknown("Physical PDF pages, when `parsers[].pages` is set."),
148
+ blocks: unknown("Typed PDF layout blocks, when `parsers[].blocks` is set."),
149
+ product: unknown("Product data extracted from the page."),
150
+ menu: unknown("Menu data extracted from the page."),
151
+ json: unknown("Structured data matching the requested JSON schema or prompt."),
152
+ attributes: unknown("Values collected by the requested attribute selectors."),
153
+ actions: unknown("Results of the browser actions that ran during the scrape."),
154
+ changeTracking: unknown("Change-tracking comparison against the previous scrape."),
155
+ branding: unknown("Branding data extracted from the page."),
156
+ metadata: unknown("Page metadata; authenticated responses can include `metadata.scrapeId` for scrape feedback."),
157
+ tools: unknown("Domain-matched Alexandria tools for the page, when `domainTools` is set."),
158
+ warning,
159
+ // Alexandria mode and error envelopes.
160
+ success,
161
+ error,
162
+ data: unknown("Alexandria mode: per-capability results in `data.alexandria`, each with `data`, `records`, or an `error`."),
163
+ ...alexandriaEnvelope
164
+ }).describe("A scraped document, or the Alexandria execution envelope when `alexandria` was passed.");
165
+ var mapOutputSchema = z2.object({
166
+ links: unknown("URLs discovered under the website."),
167
+ success,
168
+ error,
169
+ warning,
170
+ id: str("Identifier of the map run, for optional map feedback.")
171
+ }).describe("URLs indexed under the requested website.");
172
+ var searchOutputSchema = z2.object({
173
+ success,
174
+ data: unknown("Ranked results grouped by source, such as `web`, `news`, `images`, and `alexandria`."),
175
+ error,
176
+ warning,
177
+ id: str("Search identifier, for optional `firecrawl_search_feedback`."),
178
+ creditsUsed: num("Credits this search consumed."),
179
+ tools: unknown("Domain-matched Alexandria tools for the results."),
180
+ nextTool: unknown("A follow-up tool call that continues this search."),
181
+ feedbackTool: unknown("Pointer to the feedback tool for this search.")
182
+ }).describe("Ranked search results grouped by source.");
183
+ var findToolsOutputSchema = z2.object({
184
+ success,
185
+ data: unknown("Discovery page in `data.alexandria[0].data`, with `level`, `items`, `total`, and an optional `nextTool`."),
186
+ error,
187
+ ...alexandriaEnvelope
188
+ }).describe("One page of the Alexandria catalogue: categories, providers, tools, or one contract.");
189
+ var feedbackOutputSchema = z2.object({
190
+ success,
191
+ error,
192
+ status: num("HTTP status when the submission was rejected."),
193
+ feedbackErrorCode: str("Machine-readable reason the submission was rejected."),
194
+ retryable: bool("Whether retrying the submission can succeed."),
195
+ message: str("Human-readable result of the submission."),
196
+ feedbackId: str("Identifier of the recorded feedback."),
197
+ creditsRefunded: num("Credits refunded for this submission, when eligible."),
198
+ creditsRefundedToday: num("Credits refunded to the team so far this UTC day."),
199
+ dailyRefundCap: num("The team's daily refund cap in credits."),
200
+ dailyCapReached: bool("Whether the daily refund cap is reached; further feedback today still records but refunds nothing."),
201
+ alreadySubmitted: bool("Whether feedback for this job was already recorded."),
202
+ warning,
203
+ data: unknown("Payload returned with the accepted feedback."),
204
+ raw: str("Response body when it was not JSON.")
205
+ }).describe("Result of submitting feedback.");
206
+ var crawlOutputSchema = z2.object({
207
+ success,
208
+ error,
209
+ warning,
210
+ id: str("Crawl identifier, for `firecrawl_check_crawl_status`."),
211
+ status: str("Crawl state, such as `scraping`, `completed`, or `failed`."),
212
+ completed: num("Pages crawled so far."),
213
+ total: num("Pages the crawl expects to visit."),
214
+ creditsUsed: num("Credits the crawl consumed."),
215
+ expiresAt: str("When the crawl results expire."),
216
+ next: str("URL of the next page of results, when the data is paginated."),
217
+ url: str("The URL the crawl started from."),
218
+ data: unknown("Scraped documents collected by the crawl.")
219
+ }).describe("Crawl job state and the documents collected so far.");
220
+ var agentOutputSchema = z2.object({
221
+ success,
222
+ error,
223
+ id: str("Agent job identifier, for `firecrawl_agent_status`."),
224
+ status: str("Job state at submission time."),
225
+ threadId: str("Research thread this job belongs to."),
226
+ threadTurn: num("Turn number of this job within its thread.")
227
+ }).describe("The queued research job. The result is read with `firecrawl_agent_status`.");
228
+ var agentStatusOutputSchema = z2.object({
229
+ success,
230
+ error,
231
+ id: str("Agent job identifier."),
232
+ status: str("Job state: `processing` is non-terminal; `completed` and `failed` are terminal."),
233
+ progress: unknown("Progress information for a running job."),
234
+ data: unknown("Research result, present once the job has completed."),
235
+ creditsUsed: num("Credits the job consumed."),
236
+ sources: unknown("Sources the agent read."),
237
+ expiresAt: str("When the job result expires and can no longer be read."),
238
+ model: str("Agent model that ran the job."),
239
+ mode: str("Agent mode the job ran in."),
240
+ threadId: str("Research thread this job belongs to."),
241
+ threadTurn: num("Turn number of this job within its thread.")
242
+ }).describe("Progress or final result of a research agent job.");
243
+ var interactOutputSchema = z2.object({
244
+ success,
245
+ error: unknown("Why the interaction or the session could not run."),
246
+ url: str("The URL the session was opened from, when opening failed."),
247
+ scrapeId: str("Session identifier; pass it to reuse the session or to `firecrawl_interact_stop`."),
248
+ result: unknown("Interaction result, when it is not an object of its own."),
249
+ output: unknown("Execution output."),
250
+ stdout: str("Standard output of the executed code."),
251
+ stderr: str("Standard error of the executed code."),
252
+ exitCode: num("Exit status of the executed code."),
253
+ data: unknown("Payload returned by the interaction."),
254
+ sessionUrl: str("URL for viewing the live session."),
255
+ liveUrl: str("URL for viewing the live session.")
256
+ }).describe("Result of a browser interaction, with the session identifier to continue or stop it.");
257
+ var interactStopOutputSchema = z2.object({
258
+ success,
259
+ error,
260
+ message: str("Confirmation that the session was stopped."),
261
+ scrapeId: str("Session that was stopped.")
262
+ }).describe("Confirmation that the interact session was stopped.");
263
+ var parseOutputSchema = z2.object({
264
+ success,
265
+ error,
266
+ warning,
267
+ mode: str("Which phase of the hosted flow produced this response."),
268
+ message: str("Guidance for the next call."),
269
+ data: unknown("Parsed document content; can include `data.metadata.scrapeId` for parse feedback."),
270
+ upload: z2.object({
271
+ command: str("Local command that performs the upload. It carries no Firecrawl API key."),
272
+ method: str("HTTP method for the upload."),
273
+ headers: unknown("Headers the upload request must send."),
274
+ fields: unknown("Form fields the upload request must send, for a POST upload."),
275
+ uploadUrl: str("URL to upload the local file to."),
276
+ uploadRef: str("Reference to pass back on the second call."),
277
+ expiresAt: str("When the upload URL expires."),
278
+ maxSizeBytes: num("Largest file the upload URL accepts.")
279
+ }).optional().describe("Hosted phase one: how to upload the local file."),
280
+ nextToolCall: unknown("Hosted phase one: the second `firecrawl_parse` call to make once the upload succeeds, as `{name, arguments}`."),
281
+ notes: z2.array(z2.string()).nullable().optional().describe("Hosted phase one: constraints on completing the upload flow."),
282
+ raw: str("Response body when it was not JSON.")
283
+ }).describe("Parsed document content, or the upload instructions for the hosted two-call flow.");
284
+ var deprecatedToolOutputSchema = z2.object({
285
+ code: str("Always `DEPRECATED_TOOL`."),
286
+ message: str("Why the tool is unavailable and what to call instead."),
287
+ replacement: unknown("The replacement tool, with instructions and example arguments."),
288
+ docs_url: str("Documentation for the replacement.")
289
+ }).describe("A pointer to the replacement tool. This entry point is deprecated and always errors.");
290
+ var monitorOutputSchema = z2.object({
291
+ success,
292
+ error,
293
+ data: unknown("The monitor record: configuration, schedule, targets, and current state.")
294
+ }).describe("One monitor record.");
295
+ var monitorListOutputSchema = z2.object({
296
+ success,
297
+ error,
298
+ data: unknown("One page of monitor records."),
299
+ pagination: unknown("Pagination metadata for the listing.")
300
+ }).describe("One page of monitors for the authenticated account.");
301
+ var monitorDeleteOutputSchema = z2.object({
302
+ success,
303
+ error,
304
+ data: unknown("Deletion details, when the API returns any.")
305
+ }).describe("Deletion status for the monitor.");
306
+ var monitorRunOutputSchema = z2.object({
307
+ success,
308
+ error,
309
+ data: unknown("The queued check.")
310
+ }).describe("The check queued outside the monitor schedule.");
311
+ var monitorChecksOutputSchema = z2.object({
312
+ success,
313
+ error,
314
+ data: unknown("One page of check summaries."),
315
+ pagination: unknown("Pagination metadata for the listing.")
316
+ }).describe("One page of historical checks for the monitor.");
317
+ var monitorCheckOutputSchema = z2.object({
318
+ success,
319
+ error,
320
+ data: unknown("The check and its page-level results: each page reports `same`, `new`, `changed`, `removed`, or `error`, with diffs and any goal judgment."),
321
+ pagination: unknown("Pagination metadata for the page results."),
322
+ next: str("URL of the next page of results, when more exist.")
323
+ }).describe("One monitor check with its page-level diff results.");
324
+ var paperSchema = z2.looseObject({
325
+ paperId: str("Canonical paper identifier."),
326
+ primaryId: str("Display identifier, ordered for citation and fetch use."),
327
+ ids: unknown("Source identifiers by namespace, such as `arxiv`, `doi`, or `pmid`."),
328
+ title: str("Paper title."),
329
+ abstract: str("Paper abstract."),
330
+ authors: unknown("Authors, as a comma-joined string or as `{name, affiliation}` entries."),
331
+ categories: z2.array(z2.string()).nullable().optional().describe("Paper categories, such as `cs.LG`."),
332
+ createdDate: str("Date the paper was first indexed or published."),
333
+ updateDate: str("Date the paper was last updated.")
334
+ });
335
+ var researchSearchOutputSchema = z2.object({
336
+ results: z2.array(paperSchema).describe("Ranked papers, in the order the text block lists them.")
337
+ }).describe("Ranked papers with canonical IDs, titles, authors, and abstracts.");
338
+ var researchPaperOutputSchema = z2.object({
339
+ paper: paperSchema.optional().describe("Canonical metadata for the requested paper, absent when it was not found.")
340
+ }).describe("Canonical metadata for one paper.");
341
+ var researchRelatedOutputSchema = z2.object({
342
+ results: z2.array(paperSchema).describe("Ranked citation-graph candidates."),
343
+ poolSize: z2.number().describe("Number of candidates evaluated before ranking."),
344
+ note: str("Note about how the candidates were produced.")
345
+ }).describe("Ranked citation-graph candidates and the size of the evaluated pool.");
346
+ var researchReadOutputSchema = z2.object({
347
+ passages: z2.array(z2.looseObject({ text: str("Passage text.") })).describe("In-body passages relevant to the question; empty when no full text is indexed.")
348
+ }).describe("Full-text passages from one paper.");
349
+ var developerSearchOutputSchema = z2.object({
350
+ results: z2.array(
351
+ z2.looseObject({
352
+ id: str("Stable result id, such as `issue:owner/repo#123` or `doc:<hash>`; its prefix gives the source type."),
353
+ url: str("Source URL."),
354
+ title: str("Result title."),
355
+ passages: z2.array(z2.looseObject({ text: str("Matched passage, in markdown.") })).optional().describe("Matched passages.")
356
+ })
357
+ ).describe("Ranked results, in the order the text block lists them.")
358
+ }).describe("Ranked developer-index results with their matched passages.");
359
+ var creditUsageOutputSchema = z2.object({
360
+ // Current view.
361
+ remainingCredits: num("Credits left in the current billing period; can exceed `planCredits` when extra credits were purchased or granted."),
362
+ planCredits: num("Credits included in the plan for the period."),
363
+ billingPeriodStart: str("Start of the current billing period; null when the billing provider reports no period."),
364
+ billingPeriodEnd: str("End of the current billing period; null when the billing provider reports no period."),
365
+ // Historical view.
366
+ success,
367
+ error,
368
+ periods: unknown("Historical periods sorted by start date, each with `startDate`, `endDate`, `creditsUsed`, and `apiKey` when broken down by key.")
369
+ }).describe("The team credit balance, or historical credit consumption by period.");
370
+
371
+ // src/alexandria.ts
372
+ import { z as z3 } from "zod";
93
373
  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()
374
+ var searchSourceSchema = z3.union([
375
+ z3.enum(["web", "images", "news", ...catalogueTypes]),
376
+ z3.object({ type: z3.enum(["web", "images", "news", ...catalogueTypes]) }).strict()
97
377
  ]);
98
378
  function hasAlexandria(sources) {
99
379
  return Array.isArray(sources) && sources.some(
@@ -116,24 +396,25 @@ function normalizeSearchSources(sources) {
116
396
  function searchQueryIsValid(args2) {
117
397
  return !!args2.query?.trim();
118
398
  }
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?:\/\//)
399
+ var findToolsSchema = z3.object({
400
+ query: z3.string().trim().min(1).max(2e3).optional().describe("Semantic lookup of tools for the data you need. Selectors constrain the search."),
401
+ urls: z3.array(
402
+ z3.string().url().regex(/^https?:\/\//)
123
403
  ).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()
404
+ providers: z3.array(z3.string().min(1)).min(1).max(50).optional().describe("Provider IDs returned by category browsing. Lists compact tools by default."),
405
+ categories: z3.array(z3.string().min(1)).min(1).max(50).optional().describe("Category IDs returned by an empty call. Lists providers by default."),
406
+ groups: z3.array(z3.string().min(1)).min(1).max(50).optional().describe("Optional group IDs for explicit group browsing."),
407
+ 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."),
408
+ level: z3.enum(["categories", "providers", "groups", "tools"]).optional(),
409
+ 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.'),
410
+ limit: z3.number().int().min(1).max(100).optional(),
411
+ offset: z3.number().int().nonnegative().optional()
132
412
  }).strict();
133
413
  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
414
  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.';
136
- var ALEXANDRIA_INSTRUCTIONS = "Start with the user\u2019s actual question and constraints. Authenticated search returns matching Alexandria providers beside web results; prefer a provider over page scraping when the task needs the same fields across several entities, provenance, exact figures, or many records, and use web results when they already answer the question. Authenticated search defaults to web + semantic Alexandria tools + domain-matched tools. Keyless search defaults to web only. " + ALEXANDRIA_CATALOGUE_SENTENCE + " Coverage varies; discover current tools rather than assuming one exists. Semantic discovery matches the data you need to capabilities even without a provider website in the web results. Domain matching connects result websites to tools that may fetch richer details, related records or collections beyond the linked page. Inspect coverage and required inputs; a matching domain alone does not guarantee a fit. " + ALEXANDRIA_SOURCES_OPT_OUT + ' Use sources: ["alexandria"] for semantic tools only, sources: ["web"] for web only, or sources: ["web"], domainTools: true for web plus domain tools. domainTools: false disables domain matching. Results in data.tools describe available tools, not executed data. Search defaults to toolDetail: "compact", returning only provider, capability and description; "summary" adds metadata and navigation. Inspect selected tools with firecrawl_find_tools using their providers and capabilities plus expand:["options","response"]; request examples only when the input shape is unclear. Batch related contract inspections and reuse complete contracts. Alternatively set toolDetail: "full" for contracts upfront. On the full MCP surface, firecrawl_find_tools supports semantic query lookup and is the list equivalent: {} lists categories; {categories:["<category-id>"]} lists providers; {providers:["<provider-id>"]} lists compact tools; adding capabilities:["<capability-id>"] expands the selected contract. Avoid expanding the entire catalogue. Read required inputs and requiresOneOf groups (at least one member per group), example.request/example.response when present, and response.key in the selected contract. 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. Execute through firecrawl_scrape with alexandria:{provider,capability,options}. Search scrapeOptions fetches web pages, never provider tools. Use web results when sufficient, tools when they offer a direct route to deeper data. ' + ALEXANDRIA_FEEDBACK_GUIDANCE;
415
+ 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.';
416
+ 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;
417
+ 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.";
137
418
  function findToolsOptions(args2) {
138
419
  const level = args2.level ?? (args2.query || args2.capabilities?.length || args2.providers?.length || args2.groups?.length || args2.urls?.length ? "tools" : args2.categories?.length ? "providers" : "categories");
139
420
  if (level === "categories" && (args2.query || args2.providers?.length || args2.categories?.length || args2.groups?.length || args2.capabilities?.length || args2.urls?.length || args2.expand !== void 0)) {
@@ -163,7 +444,7 @@ function withFindToolsNavigation(envelope) {
163
444
  if (link(page.next)) page.nextTool = link(page.next);
164
445
  return envelope;
165
446
  }
166
- 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.';
447
+ 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.';
167
448
 
168
449
  // src/alexandria-output.ts
169
450
  var INLINE_TOKEN_BUDGET = 2e4;
@@ -230,7 +511,7 @@ async function alexandriaOutput(payload, calls, post, recoveryRequestId, extras
230
511
  }
231
512
 
232
513
  // src/developer.ts
233
- import { z as z3 } from "zod";
514
+ import { z as z4 } from "zod";
234
515
 
235
516
  // src/origin.ts
236
517
  import { createRequire } from "module";
@@ -296,12 +577,13 @@ Search an index of public repositories, GitHub issues, merged pull requests, rep
296
577
 
297
578
  Returns ranked results with an ID, source type, URL, title, and the matched passages in markdown.
298
579
  `,
299
- parameters: z3.object({
300
- query: z3.string().min(1).describe(
580
+ outputSchema: developerSearchOutputSchema,
581
+ parameters: z4.object({
582
+ query: z4.string().min(1).describe(
301
583
  "Natural-language developer question or search phrase, including the library, error message, or API involved when relevant."
302
584
  ),
303
- k: z3.number().int().min(1).max(100).optional().describe("Number of ranked results to return (default 10)."),
304
- skills: z3.enum(["only"]).optional().describe('Set to "only" to search only agent-skill files.')
585
+ k: z4.number().int().min(1).max(100).optional().describe("Number of ranked results to return (default 10)."),
586
+ skills: z4.enum(["only"]).optional().describe('Set to "only" to search only agent-skill files.')
305
587
  }),
306
588
  execute: async (args2, { session, client: mcpClient }) => {
307
589
  const { query, k, skills } = args2;
@@ -311,7 +593,8 @@ Returns ranked results with an ID, source type, URL, title, and the matched pass
311
593
  if (skills != null) params.append("skills", skills);
312
594
  const client = getClient2(session);
313
595
  const res = await client.http.get(`${BASE}?${params.toString()}`, originHeaders(requestOrigin(mcpClient, session)));
314
- return fmtDeveloper(res.data?.results);
596
+ const results = res.data?.results ?? [];
597
+ return withStructured(fmtDeveloper(results), { results });
315
598
  }
316
599
  });
317
600
  }
@@ -331,7 +614,7 @@ function extractSingleTrustedClientIp(rawForwardedFor) {
331
614
  }
332
615
 
333
616
  // src/monitor.ts
334
- import { z as z4 } from "zod";
617
+ import { z as z5 } from "zod";
335
618
 
336
619
  // src/session-credential.ts
337
620
  import { createHmac } from "crypto";
@@ -456,11 +739,8 @@ async function monitorRequest(session, origin, path2, init = {}) {
456
739
  }
457
740
  return payload;
458
741
  }
459
- function asText(data) {
460
- return JSON.stringify(data, null, 2);
461
- }
462
- var pageStatusSchema = z4.enum(["same", "new", "changed", "removed", "error"]);
463
- var checkStatusSchema = z4.enum([
742
+ var pageStatusSchema = z5.enum(["same", "new", "changed", "removed", "error"]);
743
+ var checkStatusSchema = z5.enum([
464
744
  "queued",
465
745
  "running",
466
746
  "completed",
@@ -553,22 +833,23 @@ Create a recurring scrape, crawl, or search monitor that compares each check wit
553
833
 
554
834
  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.
555
835
  `,
556
- parameters: z4.object({
557
- body: z4.record(z4.string(), z4.any()).optional(),
558
- page: z4.string().optional(),
559
- pages: z4.array(z4.string()).optional(),
560
- queries: z4.array(z4.string()).optional(),
561
- searchWindow: z4.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
562
- maxResults: z4.number().int().min(1).max(50).optional(),
563
- includeDomains: z4.array(z4.string()).optional(),
564
- excludeDomains: z4.array(z4.string()).optional(),
565
- goal: z4.string().optional(),
566
- name: z4.string().optional(),
567
- scheduleText: z4.string().optional(),
568
- timezone: z4.string().optional(),
569
- email: z4.string().optional(),
570
- includeDiffs: z4.boolean().optional(),
571
- webhookUrl: z4.string().optional()
836
+ outputSchema: monitorOutputSchema,
837
+ parameters: z5.object({
838
+ body: z5.record(z5.string(), z5.any()).optional(),
839
+ page: z5.string().optional(),
840
+ pages: z5.array(z5.string()).optional(),
841
+ queries: z5.array(z5.string()).optional(),
842
+ searchWindow: z5.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
843
+ maxResults: z5.number().int().min(1).max(50).optional(),
844
+ includeDomains: z5.array(z5.string()).optional(),
845
+ excludeDomains: z5.array(z5.string()).optional(),
846
+ goal: z5.string().optional(),
847
+ name: z5.string().optional(),
848
+ scheduleText: z5.string().optional(),
849
+ timezone: z5.string().optional(),
850
+ email: z5.string().optional(),
851
+ includeDiffs: z5.boolean().optional(),
852
+ webhookUrl: z5.string().optional()
572
853
  }),
573
854
  execute: async (args2, { session, log, client: mcpClient }) => {
574
855
  const body = buildMonitorCreateBody(args2);
@@ -582,7 +863,7 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
582
863
  body
583
864
  }
584
865
  );
585
- return asText(res);
866
+ return structuredText(res);
586
867
  }
587
868
  });
588
869
  server2.addTool({
@@ -599,9 +880,10 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
599
880
  description: `
600
881
  List monitors for the authenticated account with optional pagination controls. Returns one page of monitor records and pagination metadata.
601
882
  `,
602
- parameters: z4.object({
603
- limit: z4.number().int().positive().optional(),
604
- offset: z4.number().int().nonnegative().optional()
883
+ outputSchema: monitorListOutputSchema,
884
+ parameters: z5.object({
885
+ limit: z5.number().int().positive().optional(),
886
+ offset: z5.number().int().nonnegative().optional()
605
887
  }),
606
888
  execute: async (args2, { session, client: mcpClient }) => {
607
889
  const { limit, offset } = args2;
@@ -613,7 +895,7 @@ List monitors for the authenticated account with optional pagination controls. R
613
895
  query: { limit, offset }
614
896
  }
615
897
  );
616
- return asText(res);
898
+ return structuredText(res);
617
899
  }
618
900
  });
619
901
  server2.addTool({
@@ -630,7 +912,8 @@ List monitors for the authenticated account with optional pagination controls. R
630
912
  description: `
631
913
  Retrieve one monitor by ID, including its configuration and current state. This does not run or modify the monitor.
632
914
  `,
633
- parameters: z4.object({ id: z4.string() }),
915
+ outputSchema: monitorOutputSchema,
916
+ parameters: z5.object({ id: z5.string() }),
634
917
  execute: async (args2, { session, client: mcpClient }) => {
635
918
  const { id } = args2;
636
919
  const res = await monitorRequest(
@@ -638,7 +921,7 @@ Retrieve one monitor by ID, including its configuration and current state. This
638
921
  requestOrigin(mcpClient, session),
639
922
  `/monitor/${encodeURIComponent(id)}`
640
923
  );
641
- return asText(res);
924
+ return structuredText(res);
642
925
  }
643
926
  });
644
927
  server2.addTool({
@@ -657,9 +940,10 @@ Patch an existing monitor by ID. The body can change its name, active/paused sta
657
940
 
658
941
  Returns the updated monitor.
659
942
  `,
660
- parameters: z4.object({
661
- id: z4.string(),
662
- body: z4.record(z4.string(), z4.any())
943
+ outputSchema: monitorOutputSchema,
944
+ parameters: z5.object({
945
+ id: z5.string(),
946
+ body: z5.record(z5.string(), z5.any())
663
947
  }),
664
948
  execute: async (args2, { session, client: mcpClient }) => {
665
949
  const { id, body } = args2;
@@ -669,7 +953,7 @@ Returns the updated monitor.
669
953
  `/monitor/${encodeURIComponent(id)}`,
670
954
  { method: "PATCH", body }
671
955
  );
672
- return asText(res);
956
+ return structuredText(res);
673
957
  }
674
958
  });
675
959
  server2.addTool({
@@ -686,7 +970,8 @@ Returns the updated monitor.
686
970
  description: `
687
971
  Permanently delete a monitor by ID and stop its future schedule. This operation cannot be undone and returns deletion status.
688
972
  `,
689
- parameters: z4.object({ id: z4.string() }),
973
+ outputSchema: monitorDeleteOutputSchema,
974
+ parameters: z5.object({ id: z5.string() }),
690
975
  execute: async (args2, { session, log, client: mcpClient }) => {
691
976
  const { id } = args2;
692
977
  log.info("Deleting monitor", { id });
@@ -696,7 +981,7 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
696
981
  `/monitor/${encodeURIComponent(id)}`,
697
982
  { method: "DELETE" }
698
983
  );
699
- return asText(res);
984
+ return structuredText(res);
700
985
  }
701
986
  });
702
987
  server2.addTool({
@@ -713,7 +998,8 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
713
998
  description: `
714
999
  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.
715
1000
  `,
716
- parameters: z4.object({ id: z4.string() }),
1001
+ outputSchema: monitorRunOutputSchema,
1002
+ parameters: z5.object({ id: z5.string() }),
717
1003
  execute: async (args2, { session, client: mcpClient }) => {
718
1004
  const { id } = args2;
719
1005
  const res = await monitorRequest(
@@ -722,7 +1008,7 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
722
1008
  `/monitor/${encodeURIComponent(id)}/run`,
723
1009
  { method: "POST" }
724
1010
  );
725
- return asText(res);
1011
+ return structuredText(res);
726
1012
  }
727
1013
  });
728
1014
  server2.addTool({
@@ -739,10 +1025,11 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
739
1025
  description: `
740
1026
  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.
741
1027
  `,
742
- parameters: z4.object({
743
- id: z4.string(),
744
- limit: z4.number().int().positive().optional(),
745
- offset: z4.number().int().nonnegative().optional(),
1028
+ outputSchema: monitorChecksOutputSchema,
1029
+ parameters: z5.object({
1030
+ id: z5.string(),
1031
+ limit: z5.number().int().positive().optional(),
1032
+ offset: z5.number().int().nonnegative().optional(),
746
1033
  status: checkStatusSchema.optional()
747
1034
  }),
748
1035
  execute: async (args2, { session, client: mcpClient }) => {
@@ -753,7 +1040,7 @@ List historical checks for a monitor, optionally filtered by status and bounded
753
1040
  `/monitor/${encodeURIComponent(id)}/checks`,
754
1041
  { query: { limit, offset, status } }
755
1042
  );
756
- return asText(res);
1043
+ return structuredText(res);
757
1044
  }
758
1045
  });
759
1046
  server2.addTool({
@@ -772,11 +1059,12 @@ Retrieve one monitor check and its page-level results, optionally filtered by pa
772
1059
 
773
1060
  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.
774
1061
  `,
775
- parameters: z4.object({
776
- id: z4.string(),
777
- checkId: z4.string(),
778
- limit: z4.number().int().positive().optional(),
779
- skip: z4.number().int().nonnegative().optional(),
1062
+ outputSchema: monitorCheckOutputSchema,
1063
+ parameters: z5.object({
1064
+ id: z5.string(),
1065
+ checkId: z5.string(),
1066
+ limit: z5.number().int().positive().optional(),
1067
+ skip: z5.number().int().nonnegative().optional(),
780
1068
  pageStatus: pageStatusSchema.optional()
781
1069
  }),
782
1070
  execute: async (args2, { session, client: mcpClient }) => {
@@ -787,13 +1075,13 @@ Markdown tracking returns a unified text diff, JSON tracking returns field paths
787
1075
  `/monitor/${encodeURIComponent(id)}/checks/${encodeURIComponent(checkId)}`,
788
1076
  { query: { limit, skip, status: pageStatus } }
789
1077
  );
790
- return asText(res);
1078
+ return structuredText(res);
791
1079
  }
792
1080
  });
793
1081
  }
794
1082
 
795
1083
  // src/research.ts
796
- import { z as z5 } from "zod";
1084
+ import { z as z6 } from "zod";
797
1085
  import { UserError } from "fastmcp";
798
1086
  var BASE2 = "/v2/search/research";
799
1087
  function appendParam(params, key, value) {
@@ -909,21 +1197,22 @@ Several distinct framings of the same question surface different papers than a s
909
1197
 
910
1198
  Returns ranked papers with canonical IDs, titles, authors, and abstracts.
911
1199
  `,
912
- parameters: z5.object({
913
- query: z5.string().min(1).describe(
1200
+ outputSchema: researchSearchOutputSchema,
1201
+ parameters: z6.object({
1202
+ query: z6.string().min(1).describe(
914
1203
  "Natural-language research topic or question, including methods, systems, conditions, populations, interventions, or outcomes when relevant."
915
1204
  ),
916
- k: z5.number().int().min(1).max(500).optional().describe("Number of ranked papers to return (default 40)."),
917
- authors: z5.array(z5.string()).optional().describe(
1205
+ k: z6.number().int().min(1).max(500).optional().describe("Number of ranked papers to return (default 40)."),
1206
+ authors: z6.array(z6.string()).optional().describe(
918
1207
  "Author substring filter(s); ALL must match (case-insensitive)."
919
1208
  ),
920
- categories: z5.array(z5.string()).optional().describe(
1209
+ categories: z6.array(z6.string()).optional().describe(
921
1210
  "Paper category filter(s) (e.g. `cs.LG`); ALL provided values must match."
922
1211
  ),
923
- from: z5.string().optional().describe(
1212
+ from: z6.string().optional().describe(
924
1213
  "Inclusive lower bound on created/updated date (`YYYY-MM-DD`)."
925
1214
  ),
926
- to: z5.string().optional().describe(
1215
+ to: z6.string().optional().describe(
927
1216
  "Inclusive upper bound on created/updated date (`YYYY-MM-DD`)."
928
1217
  )
929
1218
  }),
@@ -941,7 +1230,8 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
941
1230
  withQuery(`${BASE2}/papers`, params),
942
1231
  originHeaders(requestOrigin(mcpClient, session))
943
1232
  );
944
- return fmtHits(res.data?.results);
1233
+ const results = res.data?.results ?? [];
1234
+ return withStructured(fmtHits(results), { results });
945
1235
  }
946
1236
  });
947
1237
  server2.addTool({
@@ -958,8 +1248,9 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
958
1248
  description: `
959
1249
  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.
960
1250
  `,
961
- parameters: z5.object({
962
- paperId: z5.string().min(1).describe(
1251
+ outputSchema: researchPaperOutputSchema,
1252
+ parameters: z6.object({
1253
+ paperId: z6.string().min(1).describe(
963
1254
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
964
1255
  )
965
1256
  }),
@@ -970,7 +1261,11 @@ Retrieve canonical metadata for one paper ID, such as an arXiv, PMC, PMID, or DO
970
1261
  `${BASE2}/papers/${encodeURIComponent(paperId)}`,
971
1262
  originHeaders(requestOrigin(mcpClient, session))
972
1263
  );
973
- return fmtPaperMetadata(res.data?.paper);
1264
+ const paper = res.data?.paper;
1265
+ return withStructured(
1266
+ fmtPaperMetadata(paper),
1267
+ paper ? { paper } : {}
1268
+ );
974
1269
  }
975
1270
  });
976
1271
  server2.addTool({
@@ -989,12 +1284,13 @@ Find citation-graph candidates from one to ten \`seed_ids\`; the first ID is the
989
1284
 
990
1285
  Returns ranked candidates and the evaluated pool size.
991
1286
  `,
992
- parameters: z5.object({
993
- seed_ids: z5.array(z5.string()).min(1).max(10),
994
- intent: z5.string().min(1),
995
- mode: z5.enum(["similar", "citers", "references"]).optional(),
996
- k: z5.number().int().min(1).max(500).optional(),
997
- rerank: z5.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
1287
+ outputSchema: researchRelatedOutputSchema,
1288
+ parameters: z6.object({
1289
+ seed_ids: z6.array(z6.string()).min(1).max(10),
1290
+ intent: z6.string().min(1),
1291
+ mode: z6.enum(["similar", "citers", "references"]).optional(),
1292
+ k: z6.number().int().min(1).max(500).optional(),
1293
+ rerank: z6.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
998
1294
  }),
999
1295
  execute: async (args2, { session, client: mcpClient }) => {
1000
1296
  const { seed_ids, intent, mode, k, rerank } = args2;
@@ -1013,10 +1309,15 @@ Returns ranked candidates and the evaluated pool size.
1013
1309
  ),
1014
1310
  originHeaders(requestOrigin(mcpClient, session))
1015
1311
  );
1312
+ const results = res.data?.results ?? [];
1313
+ const poolSize = res.data?.poolSize ?? 0;
1016
1314
  const note = res.data?.note ? `
1017
1315
  note: ${res.data.note}` : "";
1018
- return `${fmtHits(res.data?.results)}
1019
- (poolSize=${res.data?.poolSize ?? 0})${note}`;
1316
+ return withStructured(
1317
+ `${fmtHits(results)}
1318
+ (poolSize=${poolSize})${note}`,
1319
+ { results, poolSize, ...res.data?.note ? { note: res.data.note } : {} }
1320
+ );
1020
1321
  }
1021
1322
  });
1022
1323
  server2.addTool({
@@ -1035,12 +1336,13 @@ Retrieve in-body passages from one paper that are relevant to a specific questio
1035
1336
 
1036
1337
  Returns matching passages or a notice when full text is unavailable.
1037
1338
  `,
1038
- parameters: z5.object({
1039
- paperId: z5.string().min(1).describe(
1339
+ outputSchema: researchReadOutputSchema,
1340
+ parameters: z6.object({
1341
+ paperId: z6.string().min(1).describe(
1040
1342
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
1041
1343
  ),
1042
- question: z5.string().min(1),
1043
- k: z5.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
1344
+ question: z6.string().min(1),
1345
+ k: z6.number().int().min(1).max(50).optional().describe("Number of passages to return (default 4).")
1044
1346
  }),
1045
1347
  execute: async (args2, { session, client: mcpClient }) => {
1046
1348
  const { paperId, question, k } = args2;
@@ -1053,7 +1355,10 @@ Returns matching passages or a notice when full text is unavailable.
1053
1355
  originHeaders(requestOrigin(mcpClient, session))
1054
1356
  );
1055
1357
  const passages = res.data?.passages ?? [];
1056
- return passages.length ? passages.map((p) => p.text).join("\n---\n") : "(no full-text passages available for this paper)";
1358
+ return withStructured(
1359
+ passages.length ? passages.map((p) => p.text).join("\n---\n") : "(no full-text passages available for this paper)",
1360
+ { passages }
1361
+ );
1057
1362
  }
1058
1363
  });
1059
1364
  server2.addTool({
@@ -1067,9 +1372,10 @@ Returns matching passages or a notice when full text is unavailable.
1067
1372
  description: `
1068
1373
  Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub issues, pull requests, and READMEs, plus code documentation, returned as matched passages.
1069
1374
  `,
1070
- parameters: z5.object({
1071
- query: z5.string().min(1),
1072
- k: z5.number().int().min(1).max(100).optional()
1375
+ outputSchema: deprecatedToolOutputSchema,
1376
+ parameters: z6.object({
1377
+ query: z6.string().min(1),
1378
+ k: z6.number().int().min(1).max(100).optional()
1073
1379
  }),
1074
1380
  canList: () => false,
1075
1381
  beforeValidate: () => {
@@ -1088,10 +1394,7 @@ Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub
1088
1394
  }
1089
1395
 
1090
1396
  // src/usage.ts
1091
- import { z as z6 } from "zod";
1092
- function asText2(data) {
1093
- return JSON.stringify(data, null, 2);
1094
- }
1397
+ import { z as z7 } from "zod";
1095
1398
  function assertSuccessful(status, response, operation) {
1096
1399
  if (status === 200 && response.success) return;
1097
1400
  throw new Error(response.error || `Failed to ${operation}`);
@@ -1112,11 +1415,12 @@ The current view returns \`remainingCredits\`, \`planCredits\`, \`billingPeriodS
1112
1415
 
1113
1416
  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.
1114
1417
  `,
1115
- parameters: z6.object({
1116
- view: z6.enum(["current", "historical"]).optional().describe(
1418
+ outputSchema: creditUsageOutputSchema,
1419
+ parameters: z7.object({
1420
+ view: z7.enum(["current", "historical"]).optional().describe(
1117
1421
  "Select current balance or historical monthly usage. Defaults to current."
1118
1422
  ),
1119
- byApiKey: z6.boolean().optional().describe(
1423
+ byApiKey: z7.boolean().optional().describe(
1120
1424
  'Break historical usage down by API key. When view is omitted, true selects the historical view; it cannot be combined with view "current".'
1121
1425
  )
1122
1426
  }),
@@ -1141,7 +1445,7 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e
1141
1445
  response2.data,
1142
1446
  "get historical credit usage"
1143
1447
  );
1144
- return asText2(response2.data);
1448
+ return structuredText(response2.data);
1145
1449
  }
1146
1450
  const response = await client.http.get(
1147
1451
  "/v2/team/credit-usage",
@@ -1149,7 +1453,7 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e
1149
1453
  );
1150
1454
  assertSuccessful(response.status, response.data, "get credit usage");
1151
1455
  const data = response.data.data ?? response.data;
1152
- return asText2({
1456
+ return structuredText({
1153
1457
  remainingCredits: data.remainingCredits ?? data.remaining_credits ?? 0,
1154
1458
  planCredits: data.planCredits ?? data.plan_credits,
1155
1459
  billingPeriodStart: data.billingPeriodStart ?? data.billing_period_start ?? null,
@@ -1247,11 +1551,11 @@ function getOAuthProtectedResourceMetadataUrl(profile) {
1247
1551
  const base = `${resource.origin}/.well-known/oauth-protected-resource`;
1248
1552
  return profile.id === "full" ? base : `${base}${resource.pathname}`;
1249
1553
  }
1250
- function createOAuthChallengeResponse(error, profile, details = {}) {
1554
+ function createOAuthChallengeResponse(error2, profile, details = {}) {
1251
1555
  if (!isMcpOAuthEnabled()) {
1252
1556
  return void 0;
1253
1557
  }
1254
- const errorMessage = error instanceof Error ? error.message : String(error || "Unauthorized");
1558
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2 || "Unauthorized");
1255
1559
  const wwwAuthenticateDescription = errorMessage.replace(/[\r\n]+/g, " ").trim();
1256
1560
  const wwwAuthenticate = [
1257
1561
  ...profile.advertiseOAuth ? [
@@ -1303,8 +1607,8 @@ function createInvalidOAuthRecoveryResponse(recovery) {
1303
1607
  );
1304
1608
  }
1305
1609
  var CREDENTIAL_VALIDATION_RETRY_AFTER_SECONDS = 5;
1306
- function createCredentialValidationUnavailableResponse(error) {
1307
- return new Response(error.message, {
1610
+ function createCredentialValidationUnavailableResponse(error2) {
1611
+ return new Response(error2.message, {
1308
1612
  headers: {
1309
1613
  "Content-Type": "text/plain; charset=utf-8",
1310
1614
  "Retry-After": String(CREDENTIAL_VALIDATION_RETRY_AFTER_SECONDS)
@@ -1600,13 +1904,13 @@ function makeAuthenticate(profile) {
1600
1904
  session
1601
1905
  );
1602
1906
  return session;
1603
- }).catch((error) => {
1907
+ }).catch((error2) => {
1604
1908
  emitSearchCompanionAuthTelemetry(profile, request, "rejected");
1605
1909
  emitLegacyKeyPathTelemetry(profile, request, "rejected");
1606
- if (error instanceof InvalidFirecrawlCredentialError) {
1607
- throw createInvalidCredentialResponse(error);
1910
+ if (error2 instanceof InvalidFirecrawlCredentialError) {
1911
+ throw createInvalidCredentialResponse(error2);
1608
1912
  }
1609
- if (error instanceof InvalidOAuthCredentialError) {
1913
+ if (error2 instanceof InvalidOAuthCredentialError) {
1610
1914
  const recovery = invalidOAuthRecoveryPayload();
1611
1915
  const oauthChallenge2 = createOAuthChallengeResponse(
1612
1916
  new Error(recovery.message),
@@ -1615,15 +1919,15 @@ function makeAuthenticate(profile) {
1615
1919
  );
1616
1920
  throw oauthChallenge2 ?? createInvalidOAuthRecoveryResponse(recovery);
1617
1921
  }
1618
- if (error instanceof CredentialValidationUnavailableError) {
1619
- throw createCredentialValidationUnavailableResponse(error);
1922
+ if (error2 instanceof CredentialValidationUnavailableError) {
1923
+ throw createCredentialValidationUnavailableResponse(error2);
1620
1924
  }
1621
1925
  const shouldChallenge = requestShouldReceiveOAuthChallenge(request, profile);
1622
- const oauthChallenge = shouldChallenge ? createOAuthChallengeResponse(error, profile) : void 0;
1926
+ const oauthChallenge = shouldChallenge ? createOAuthChallengeResponse(error2, profile) : void 0;
1623
1927
  if (oauthChallenge) {
1624
1928
  throw oauthChallenge;
1625
1929
  }
1626
- throw error;
1930
+ throw error2;
1627
1931
  });
1628
1932
  if (request) {
1629
1933
  request[authResultByRequest] = authResult;
@@ -1643,30 +1947,30 @@ function removeEmptyTopLevel(obj) {
1643
1947
  }
1644
1948
  return out;
1645
1949
  }
1646
- var searchDomainSchema = z7.string().trim().toLowerCase().min(1).max(253).regex(
1950
+ var searchDomainSchema = z8.string().trim().toLowerCase().min(1).max(253).regex(
1647
1951
  /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/,
1648
1952
  "Domain must be a valid hostname without protocol or path"
1649
1953
  );
1650
1954
  var searchToolBaseFields = {
1651
- 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."),
1652
- query: z7.string().min(1).describe("Query for web and semantic tool discovery. Catalogue browsing is available through firecrawl_find_tools."),
1653
- domainTools: z7.boolean().optional().describe(
1955
+ 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."),
1956
+ 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."),
1957
+ domainTools: z8.boolean().optional().describe(
1654
1958
  "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."
1655
1959
  ),
1656
- highlights: z7.boolean().optional().describe(
1960
+ highlights: z8.boolean().optional().describe(
1657
1961
  "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."
1658
1962
  ),
1659
- limit: z7.number().int().min(1).max(100).optional(),
1660
- tbs: z7.string().optional(),
1661
- filter: z7.string().optional(),
1662
- location: z7.string().optional(),
1663
- includeDomains: z7.array(searchDomainSchema).optional(),
1664
- excludeDomains: z7.array(searchDomainSchema).optional(),
1665
- 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.'),
1666
- categories: z7.array(z7.enum(["research", "pdf", "developer"])).optional().describe(
1963
+ limit: z8.number().int().min(1).max(100).optional(),
1964
+ tbs: z8.string().optional(),
1965
+ filter: z8.string().optional(),
1966
+ location: z8.string().optional(),
1967
+ includeDomains: z8.array(searchDomainSchema).optional().describe("Hostnames to restrict results to. Mutually exclusive with excludeDomains."),
1968
+ excludeDomains: z8.array(searchDomainSchema).optional().describe("Hostnames to leave out of results. Mutually exclusive with includeDomains."),
1969
+ 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.'),
1970
+ categories: z8.array(z8.enum(["research", "pdf", "developer"])).optional().describe(
1667
1971
  '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`.'
1668
1972
  ),
1669
- enterprise: z7.array(z7.enum(["default", "anon", "zdr"])).optional()
1973
+ enterprise: z8.array(z8.enum(["default", "anon", "zdr"])).optional()
1670
1974
  };
1671
1975
  function searchDomainsAreExclusive(args2) {
1672
1976
  return !(args2.includeDomains?.length && args2.excludeDomains?.length);
@@ -1674,15 +1978,15 @@ function searchDomainsAreExclusive(args2) {
1674
1978
  var SEARCH_DOMAINS_CONFLICT_MESSAGE = "includeDomains and excludeDomains cannot both be specified";
1675
1979
  var EXCHANGE_KEY_REQUIRED_MESSAGE = "Alexandria requires an API key on a team with Alexandria access";
1676
1980
  var EXCHANGE_MAX_CALLS = 10;
1677
- var exchangeCallSchema = z7.object({
1678
- provider: z7.string().min(1).describe('Provider slug, e.g. "fred".'),
1679
- capability: z7.string().min(1).describe(
1981
+ var exchangeCallSchema = z8.object({
1982
+ provider: z8.string().min(1).describe('Provider slug, e.g. "fred".'),
1983
+ capability: z8.string().min(1).describe(
1680
1984
  'Capability address as returned by search or discover, e.g. "series/observations".'
1681
1985
  ),
1682
- version: z7.string().trim().min(1).max(128).optional().describe("Optional published workflow version. Omit to use the latest version."),
1683
- options: z7.record(z7.string(), z7.any()).optional().describe("Capability options as declared by its contract.")
1986
+ version: z8.string().trim().min(1).max(128).optional().describe("Optional published workflow version. Omit to use the latest version."),
1987
+ options: z8.record(z8.string(), z8.any()).optional().describe("Capability options as declared by its contract.")
1684
1988
  });
1685
- var exchangeCallsSchema = z7.array(exchangeCallSchema).min(1).max(EXCHANGE_MAX_CALLS);
1989
+ var exchangeCallsSchema = z8.array(exchangeCallSchema).min(1).max(EXCHANGE_MAX_CALLS);
1686
1990
  function assertExchangeCredential(session) {
1687
1991
  if (hasCredential(session)) return;
1688
1992
  const payload = {
@@ -1748,26 +2052,26 @@ Do not retry until acceptance is confirmed.`,
1748
2052
  }
1749
2053
  );
1750
2054
  }
1751
- function throwIfTermsRequired(error, context) {
1752
- const source = error;
2055
+ function throwIfTermsRequired(error2, context) {
2056
+ const source = error2;
1753
2057
  const action = termsRequiredAction(source?.response?.data ?? source?.details);
1754
2058
  if (action) throw termsRequiredError(action, context);
1755
2059
  }
1756
2060
  async function relayTermsRequired(run, context) {
1757
2061
  try {
1758
2062
  return await run();
1759
- } catch (error) {
1760
- throwIfTermsRequired(error, context);
1761
- throw error;
2063
+ } catch (error2) {
2064
+ throwIfTermsRequired(error2, context);
2065
+ throw error2;
1762
2066
  }
1763
2067
  }
1764
2068
  async function relayExchangeError(run, context) {
1765
2069
  try {
1766
2070
  return await run();
1767
- } catch (error) {
1768
- throwIfTermsRequired(error, context);
1769
- const response = error?.response;
1770
- if (!response || response.status === 401) throw error;
2071
+ } catch (error2) {
2072
+ throwIfTermsRequired(error2, context);
2073
+ const response = error2?.response;
2074
+ if (!response || response.status === 401) throw error2;
1771
2075
  const data = response.data;
1772
2076
  const message = typeof data?.error === "string" ? data.error : `Alexandria request failed (HTTP ${response.status})`;
1773
2077
  const disabledProvider = response.status === 403 ? context.providers?.find((provider) => message === `Access to ${provider} is disabled for this organization.`) : void 0;
@@ -1798,14 +2102,14 @@ async function relayExchangeError(run, context) {
1798
2102
  async function postSearchWithFallback(client, body, implicitTools) {
1799
2103
  try {
1800
2104
  return await client.http.post("/v2/search", body);
1801
- } catch (error) {
1802
- const response = error?.response;
2105
+ } catch (error2) {
2106
+ const response = error2?.response;
1803
2107
  const unavailable = [
1804
2108
  "Provider discovery requires access and does not support zero data retention.",
1805
2109
  "This endpoint is not enabled for this team.",
1806
2110
  "Exchange is not enabled for this team."
1807
2111
  ].includes(response?.data?.error);
1808
- if (!implicitTools || response?.status !== 403 || !unavailable) throw error;
2112
+ if (!implicitTools || response?.status !== 403 || !unavailable) throw error2;
1809
2113
  return client.http.post("/v2/search", {
1810
2114
  ...body,
1811
2115
  sources: ["web"],
@@ -1844,10 +2148,9 @@ var ConsoleLogger = class {
1844
2148
  var openAiAppsChallengeToken = normalizeHeader(
1845
2149
  process.env.OPENAI_APPS_CHALLENGE_TOKEN
1846
2150
  );
1847
- var FULL_PROFILE_INSTRUCTIONS = `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 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. 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. 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. 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.
1848
- ${ALEXANDRIA_FEEDBACK_GUIDANCE}`;
2151
+ var FULL_PROFILE_INSTRUCTIONS = `Firecrawl provides web search, page retrieval, site URL discovery, multi-page collection, structured page data, monitoring, and multi-source research that returns structured data. Match the requested operation to the tool boundary: firecrawl_scrape retrieves one supplied page and can return JSON matching a supplied schema, firecrawl_map enumerates URLs under a site without retrieving their content, and firecrawl_agent runs multi-source research and returns structured data when the URLs are not known or the answer spans several sites (an entity plus its fields, a list, a dataset); its result is read with firecrawl_agent_status. Authenticated firecrawl_search returns web results together with matching Alexandria providers in data.tools. ${ALEXANDRIA_CATALOGUE_SENTENCE} A matching provider can return the same fields across several entities, provenance, exact figures or timestamps, or a large set of records through firecrawl_scrape with its published contract. If web results already answer the question, use them. For the same fields across multiple pages, firecrawl_find_tools offers free provider discovery. Use firecrawl_find_tools to read a contract that was not returned in full or to browse the catalogue by category. ${ALEXANDRIA_SOURCES_OPT_OUT} If no provider fits, continue with web search or firecrawl_agent. For biomedical, life-science, clinical, or arXiv literature, the firecrawl_research_* tools search a paper index of abstracts and full text; firecrawl_search with categories: ["research"] is a website filter over ordinary web results and reaches different sources. For a programming question (code behaviour, a library or framework, an API contract, an error message, or a known bug), firecrawl_developer_search (or firecrawl_search with categories: ["developer"]) searches an index of public repositories, GitHub issues, merged pull requests, READMEs, and code documentation. Alexandria requestId identifies one logical execution; repeated attempts of the identical payload require the same ID, including pending or uncertain executions. firecrawl_search with sources: [{type: "alexandria"}] returns compact tool summaries in data.tools; toolDetail: "full" includes contracts, firecrawl_find_tools starts with categories, lists providers, then compact tools, and expands the selected full contract, and firecrawl_scrape with alexandria: [{provider, capability, options}] executes up to ten capabilities and returns their results. Alexandria access needs an API key on a team with it enabled. A terms-gated Alexandria provider fails with code THIRD_PARTY_DATA_TERMS_REQUIRED and a requiresAction.url. Through firecrawl_scrape, terms/show displays the agreement and terms/accept records acceptance; acceptance requires explicit user authorization for the reviewed version and digest and confirmed:true. A data request does not authorize acceptance. The dashboard URL provides an alternative for an organization admin. A retained result provides nextTool access without repeating a successful provider call. Provide only the required inputs and account for stated network or external side effects.`;
1849
2152
  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.`;
1850
- 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.`;
2153
+ 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.`;
1851
2154
  var SEARCH_PROFILE_TOOLS = /* @__PURE__ */ new Set([
1852
2155
  "firecrawl_search",
1853
2156
  "firecrawl_developer_search",
@@ -1991,17 +2294,17 @@ function invalidOAuthRecoveryPayload() {
1991
2294
  message: INVALID_OAUTH_MESSAGE
1992
2295
  });
1993
2296
  }
1994
- function isCoreCredentialRejection(error) {
1995
- if (!(error instanceof Error)) return false;
1996
- const candidate = error;
2297
+ function isCoreCredentialRejection(error2) {
2298
+ if (!(error2 instanceof Error)) return false;
2299
+ const candidate = error2;
1997
2300
  return candidate.status === 401 || candidate.response?.status === 401;
1998
2301
  }
1999
2302
  async function runWithCredentialRecovery(run, requestId, session) {
2000
2303
  try {
2001
2304
  return await run();
2002
- } catch (error) {
2003
- if (session?.authType !== "api-key" || !isCoreCredentialRejection(error)) {
2004
- throw error;
2305
+ } catch (error2) {
2306
+ if (session?.authType !== "api-key" || !isCoreCredentialRejection(error2)) {
2307
+ throw error2;
2005
2308
  }
2006
2309
  const payload = recoveryPayload("CREDENTIAL_INVALID", requestId);
2007
2310
  throw new UserError2(String(payload.message), payload);
@@ -2054,7 +2357,7 @@ function deprecatedExtractPayload() {
2054
2357
  docs_url: "https://docs.firecrawl.dev/developer-guides/usage-guides/choosing-the-data-extractor"
2055
2358
  };
2056
2359
  }
2057
- function emitActionLog(toolName, status, session, error, requestId = randomUUID(), code) {
2360
+ function emitActionLog(toolName, status, session, error2, requestId = randomUUID(), code) {
2058
2361
  if (process.env.CLOUD_SERVICE !== "true") return;
2059
2362
  const payload = {
2060
2363
  team_id: session?.teamId,
@@ -2066,7 +2369,7 @@ function emitActionLog(toolName, status, session, error, requestId = randomUUID(
2066
2369
  status,
2067
2370
  request_id: requestId,
2068
2371
  resource: primaryProfile.resourceUrl,
2069
- ...error ? { error_class: error instanceof Error ? error.name : typeof error } : {},
2372
+ ...error2 ? { error_class: error2 instanceof Error ? error2.name : typeof error2 } : {},
2070
2373
  ...code ? { code } : {}
2071
2374
  };
2072
2375
  console.error("[MCP_ACTION]", JSON.stringify(payload));
@@ -2166,9 +2469,9 @@ function guardHostedTool(tool, { logActions }) {
2166
2469
  const result = await runTool();
2167
2470
  emitActionLog(tool.name, "success", invocationSession, void 0, requestId);
2168
2471
  return result;
2169
- } catch (error) {
2170
- emitActionLog(tool.name, "error", invocationSession, error, requestId);
2171
- throw error;
2472
+ } catch (error2) {
2473
+ emitActionLog(tool.name, "error", invocationSession, error2, requestId);
2474
+ throw error2;
2172
2475
  }
2173
2476
  }
2174
2477
  };
@@ -2260,12 +2563,9 @@ function getClient(session) {
2260
2563
  });
2261
2564
  return client;
2262
2565
  }
2263
- function asText3(data) {
2566
+ function asText(data) {
2264
2567
  return JSON.stringify(data, null, 2);
2265
2568
  }
2266
- function compactText(data) {
2267
- return JSON.stringify(data);
2268
- }
2269
2569
  var safeActionTypes = ["wait", "screenshot", "scroll", "scrape"];
2270
2570
  var otherActions = [
2271
2571
  "click",
@@ -2331,10 +2631,10 @@ function transformScrapeParams(args2) {
2331
2631
  delete out.pdfOptions;
2332
2632
  return out;
2333
2633
  }
2334
- var scrapeParamsSchema = z7.object({
2335
- url: z7.string().url(),
2336
- formats: z7.array(
2337
- z7.enum([
2634
+ var scrapeParamsSchema = z8.object({
2635
+ url: z8.string().url(),
2636
+ formats: z8.array(
2637
+ z8.enum([
2338
2638
  "markdown",
2339
2639
  "html",
2340
2640
  "rawHtml",
@@ -2348,87 +2648,93 @@ var scrapeParamsSchema = z7.object({
2348
2648
  "audio"
2349
2649
  ])
2350
2650
  ).optional(),
2351
- jsonOptions: z7.object({
2352
- prompt: z7.string().optional(),
2353
- schema: z7.record(z7.string(), z7.any()).optional()
2651
+ jsonOptions: z8.object({
2652
+ prompt: z8.string().optional(),
2653
+ schema: z8.record(z8.string(), z8.any()).optional()
2354
2654
  }).optional(),
2355
- queryOptions: z7.object({
2356
- prompt: z7.string().max(1e4),
2357
- mode: z7.enum(["directQuote", "freeform"]).default("freeform")
2655
+ queryOptions: z8.object({
2656
+ prompt: z8.string().max(1e4),
2657
+ mode: z8.enum(["directQuote", "freeform"]).default("freeform")
2358
2658
  }).optional(),
2359
- screenshotOptions: z7.object({
2360
- fullPage: z7.boolean().optional(),
2361
- quality: z7.number().optional(),
2362
- viewport: z7.object({ width: z7.number(), height: z7.number() }).optional()
2659
+ screenshotOptions: z8.object({
2660
+ fullPage: z8.boolean().optional(),
2661
+ quality: z8.number().optional(),
2662
+ viewport: z8.object({ width: z8.number(), height: z8.number() }).optional()
2363
2663
  }).optional(),
2364
- parsers: z7.array(z7.enum(["pdf"])).optional(),
2365
- pdfOptions: z7.object({
2366
- maxPages: z7.number().int().min(1).max(1e4).optional()
2664
+ parsers: z8.array(z8.enum(["pdf"])).optional(),
2665
+ pdfOptions: z8.object({
2666
+ maxPages: z8.number().int().min(1).max(1e4).optional()
2367
2667
  }).optional(),
2368
- onlyMainContent: z7.boolean().optional(),
2369
- redactPII: z7.boolean().optional(),
2370
- includeTags: z7.array(z7.string()).optional(),
2371
- excludeTags: z7.array(z7.string()).optional(),
2372
- waitFor: z7.number().optional(),
2668
+ onlyMainContent: z8.boolean().optional(),
2669
+ redactPII: z8.boolean().optional(),
2670
+ includeTags: z8.array(z8.string()).optional(),
2671
+ excludeTags: z8.array(z8.string()).optional(),
2672
+ waitFor: z8.number().optional(),
2373
2673
  ...SAFE_MODE ? {} : {
2374
- actions: z7.array(
2375
- z7.object({
2376
- type: z7.enum(allowedActionTypes),
2377
- selector: z7.string().optional(),
2378
- milliseconds: z7.number().optional(),
2379
- text: z7.string().optional(),
2380
- key: z7.string().optional(),
2381
- direction: z7.enum(["up", "down"]).optional(),
2382
- script: z7.string().optional(),
2383
- fullPage: z7.boolean().optional()
2674
+ actions: z8.array(
2675
+ z8.object({
2676
+ type: z8.enum(allowedActionTypes),
2677
+ selector: z8.string().optional(),
2678
+ milliseconds: z8.number().optional(),
2679
+ text: z8.string().optional(),
2680
+ key: z8.string().optional(),
2681
+ direction: z8.enum(["up", "down"]).optional(),
2682
+ script: z8.string().optional(),
2683
+ fullPage: z8.boolean().optional()
2384
2684
  })
2385
2685
  ).optional()
2386
2686
  },
2387
- mobile: z7.boolean().optional(),
2388
- skipTlsVerification: z7.boolean().optional(),
2389
- removeBase64Images: z7.boolean().optional(),
2390
- location: z7.object({
2391
- country: z7.string().optional(),
2392
- languages: z7.array(z7.string()).optional()
2687
+ mobile: z8.boolean().optional(),
2688
+ skipTlsVerification: z8.boolean().optional(),
2689
+ removeBase64Images: z8.boolean().optional(),
2690
+ location: z8.object({
2691
+ country: z8.string().optional(),
2692
+ languages: z8.array(z8.string()).optional()
2393
2693
  }).optional(),
2394
- storeInCache: z7.boolean().optional(),
2395
- zeroDataRetention: z7.boolean().optional(),
2396
- maxAge: z7.number().optional(),
2397
- lockdown: z7.boolean().optional(),
2398
- proxy: z7.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
2399
- profile: z7.object({
2400
- name: z7.string(),
2401
- saveChanges: z7.boolean().optional()
2694
+ storeInCache: z8.boolean().optional(),
2695
+ zeroDataRetention: z8.boolean().optional(),
2696
+ maxAge: z8.number().optional(),
2697
+ lockdown: z8.boolean().optional(),
2698
+ proxy: z8.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
2699
+ profile: z8.object({
2700
+ name: z8.string(),
2701
+ saveChanges: z8.boolean().optional()
2402
2702
  }).optional()
2403
2703
  });
2704
+ var ALEXANDRIA_IGNORED_SCRAPE_OPTIONS = /* @__PURE__ */ new Set(["toolDetail", "domainTools"]);
2404
2705
  var scrapeToolParamsSchema = scrapeParamsSchema.extend({
2405
- url: z7.string().url().optional(),
2406
- timeout: z7.number().int().positive().optional().describe("Execution timeout in milliseconds."),
2407
- requestId: z7.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe(
2408
- "Alexandria execution ID. Reuse for retries of the identical payload; generated when omitted and returned with the result."
2706
+ url: z8.string().url().optional(),
2707
+ timeout: z8.number().int().positive().optional().describe("Execution timeout in milliseconds."),
2708
+ requestId: z8.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe(
2709
+ "Identifies one logical Alexandria execution; generated when omitted and returned with the result. Repeated attempts of the identical payload require the same ID. A new ID cannot reconcile a pending or uncertain execution. A caller-supplied ID supports recovery if no response is received. Errors relay a code and may include chargeId. request_in_flight (409) means the execution is pending; request_unresolved (503) requires reconciliation under the same ID; duplicate_request (409) means the ID belongs to a different payload; unknown_provider (404), insufficient_credits (402) and billing_unavailable (503) mean nothing executed."
2409
2710
  ),
2410
- alexandria: z7.union([exchangeCallSchema, exchangeCallsSchema]).optional().describe(
2411
- "Execute catalogued Alexandria capabilities instead of scraping a URL. Exactly one of url or alexandria."
2711
+ alexandria: z8.union([exchangeCallSchema, exchangeCallsSchema]).optional().describe(
2712
+ "Catalogued Alexandria capability invocation, mutually exclusive with url. One {provider, capability, options} object or an array of 1-10, with contracts available through firecrawl_search or firecrawl_find_tools. Each call may include version to pin a published workflow; omitting it uses latest. Only requestId and timeout are supported alongside alexandria. " + ALEXANDRIA_CONTRACT_GUIDANCE + " Returns per-capability results in data.alexandria with data, records, or an error with a code; individual capabilities can fail even when the outer response succeeds. nextTool identifies access to retained results without repeating a successful provider call. Requires an API key on a team with Alexandria enabled. THIRD_PARTY_DATA_TERMS_REQUIRED (403) means execution is blocked by provider terms, with requiresAction.url for review by an organization admin. Through this tool, terms/show displays the agreement and terms/accept records acceptance; acceptance requires explicit user authorization for the reviewed version and digest and confirmed:true. A data request does not authorize acceptance. Provider execution remains blocked until acceptance is confirmed."
2412
2713
  ),
2413
- toolDetail: z7.enum(["compact", "summary", "full"]).optional().describe("URL domain discovery detail: summary by default, compact returns provider/capability/description, full includes contracts."),
2414
- domainTools: z7.boolean().optional().describe(
2415
- "URL mode only: include domain-matched Alexandria tools for the page in tools on the returned document."
2714
+ toolDetail: z8.enum(["compact", "summary", "full"]).optional().describe("URL mode only: domain discovery detail, summary by default; compact returns provider/capability/description, full includes contracts. Ignored with alexandria."),
2715
+ domainTools: z8.boolean().optional().describe(
2716
+ "URL mode only: include domain-matched Alexandria tools for the page in tools on the returned document. Ignored with alexandria."
2416
2717
  )
2417
2718
  }).refine(
2418
2719
  (args2) => Boolean(args2.url) !== Boolean(args2.alexandria),
2419
2720
  "Provide exactly one of url or alexandria"
2420
2721
  ).refine(
2421
2722
  (args2) => !args2.alexandria || Object.entries(args2).every(
2422
- ([key, value]) => key === "alexandria" || key === "requestId" || key === "timeout" || value === void 0
2723
+ ([key, value]) => key === "alexandria" || key === "requestId" || key === "timeout" || // URL-mode discovery options that mean nothing when executing a
2724
+ // provider. Agents carry toolDetail over from firecrawl_search (where
2725
+ // it selects contract detail), so accept and ignore them rather than
2726
+ // failing the call: Codex sent toolDetail on 18 of 315 Alexandria
2727
+ // executions across the AX EXP-058 runs, each one a wasted round trip.
2728
+ ALEXANDRIA_IGNORED_SCRAPE_OPTIONS.has(key) || value === void 0
2423
2729
  ),
2424
2730
  "alexandria cannot be combined with url or other scrape options"
2425
2731
  ).refine(
2426
2732
  (args2) => !args2.requestId || !!args2.alexandria,
2427
2733
  "requestId applies to Alexandria execution only"
2428
2734
  );
2429
- var parseOptionParamsSchema = z7.object({
2430
- formats: z7.array(
2431
- z7.enum([
2735
+ var parseOptionParamsSchema = z8.object({
2736
+ formats: z8.array(
2737
+ z8.enum([
2432
2738
  "markdown",
2433
2739
  "html",
2434
2740
  "rawHtml",
@@ -2438,48 +2744,48 @@ var parseOptionParamsSchema = z7.object({
2438
2744
  "query"
2439
2745
  ])
2440
2746
  ).optional(),
2441
- jsonOptions: z7.object({
2442
- prompt: z7.string().optional(),
2443
- schema: z7.record(z7.string(), z7.any()).optional()
2747
+ jsonOptions: z8.object({
2748
+ prompt: z8.string().optional(),
2749
+ schema: z8.record(z8.string(), z8.any()).optional()
2444
2750
  }).optional(),
2445
- queryOptions: z7.object({
2446
- prompt: z7.string().max(1e4),
2447
- mode: z7.enum(["directQuote", "freeform"]).default("freeform")
2751
+ queryOptions: z8.object({
2752
+ prompt: z8.string().max(1e4),
2753
+ mode: z8.enum(["directQuote", "freeform"]).default("freeform")
2448
2754
  }).optional(),
2449
- parsers: z7.array(z7.enum(["pdf"])).optional(),
2450
- pdfOptions: z7.object({
2451
- maxPages: z7.number().int().min(1).max(1e4).optional()
2755
+ parsers: z8.array(z8.enum(["pdf"])).optional(),
2756
+ pdfOptions: z8.object({
2757
+ maxPages: z8.number().int().min(1).max(1e4).optional()
2452
2758
  }).optional(),
2453
- onlyMainContent: z7.boolean().optional(),
2454
- redactPII: z7.boolean().optional(),
2455
- includeTags: z7.array(z7.string()).optional(),
2456
- excludeTags: z7.array(z7.string()).optional(),
2457
- removeBase64Images: z7.boolean().optional(),
2458
- skipTlsVerification: z7.boolean().optional(),
2459
- storeInCache: z7.boolean().optional(),
2460
- zeroDataRetention: z7.boolean().optional(),
2461
- maxAge: z7.number().optional().describe("Ignored: parse never reuses or stores indexed content."),
2462
- proxy: z7.enum(["basic", "auto"]).optional()
2759
+ onlyMainContent: z8.boolean().optional(),
2760
+ redactPII: z8.boolean().optional(),
2761
+ includeTags: z8.array(z8.string()).optional(),
2762
+ excludeTags: z8.array(z8.string()).optional(),
2763
+ removeBase64Images: z8.boolean().optional(),
2764
+ skipTlsVerification: z8.boolean().optional(),
2765
+ storeInCache: z8.boolean().optional(),
2766
+ zeroDataRetention: z8.boolean().optional(),
2767
+ maxAge: z8.number().optional().describe("Ignored: parse never reuses or stores indexed content."),
2768
+ proxy: z8.enum(["basic", "auto"]).optional()
2463
2769
  });
2464
2770
  var localParseParamsSchema = parseOptionParamsSchema.extend({
2465
- filePath: z7.string().min(1).describe(
2771
+ filePath: z8.string().min(1).describe(
2466
2772
  "Absolute or relative path to a local file to parse. Supported: .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls"
2467
2773
  ),
2468
- contentType: z7.string().optional().describe(
2774
+ contentType: z8.string().optional().describe(
2469
2775
  "Optional MIME type override. If omitted, the server infers the file kind from the extension."
2470
2776
  )
2471
2777
  });
2472
2778
  var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2473
- filePath: z7.string().min(1).optional().describe(
2779
+ filePath: z8.string().min(1).optional().describe(
2474
2780
  "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."
2475
2781
  ),
2476
- uploadRef: z7.string().min(1).optional().describe(
2782
+ uploadRef: z8.string().min(1).optional().describe(
2477
2783
  "Phase 2 only: short-lived upload reference returned by phase 1 after the local PUT upload completes."
2478
2784
  ),
2479
- contentType: z7.string().optional().describe(
2785
+ contentType: z8.string().optional().describe(
2480
2786
  "Phase 1 MIME type override. If omitted, the server infers it from the file extension without reading the file."
2481
2787
  ),
2482
- declaredSizeBytes: z7.number().int().positive().optional().describe(
2788
+ declaredSizeBytes: z8.number().int().positive().optional().describe(
2483
2789
  "Optional phase 1 size declaration. Hosted MCP does not stat the file; provide this only if the caller already knows it."
2484
2790
  )
2485
2791
  }).superRefine((value, ctx) => {
@@ -2487,7 +2793,7 @@ var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2487
2793
  const hasUploadRef = typeof value.uploadRef === "string" && value.uploadRef.length > 0;
2488
2794
  if (hasFilePath === hasUploadRef) {
2489
2795
  ctx.addIssue({
2490
- code: z7.ZodIssueCode.custom,
2796
+ code: z8.ZodIssueCode.custom,
2491
2797
  message: "Hosted firecrawl_parse requires exactly one of filePath (phase 1) or uploadRef (phase 2).",
2492
2798
  path: hasFilePath && hasUploadRef ? ["uploadRef"] : ["filePath"]
2493
2799
  });
@@ -2608,7 +2914,7 @@ async function executeHostedParse(args2, session, log, origin) {
2608
2914
  );
2609
2915
  }
2610
2916
  if (!hasCredential(session) && !isKeylessMode(session)) {
2611
- return asText3({
2917
+ return asText({
2612
2918
  success: false,
2613
2919
  mode: "hosted-upload-ref-auth-required",
2614
2920
  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."
@@ -2646,7 +2952,7 @@ async function executeHostedParse(args2, session, log, origin) {
2646
2952
  }
2647
2953
  const uploadHeaders = upload.headers && Object.keys(upload.headers).length > 0 ? upload.headers : (upload.method ?? "PUT").toUpperCase() === "POST" ? {} : { "Content-Type": contentType };
2648
2954
  const uploadForCommand = { ...upload, headers: uploadHeaders };
2649
- return asText3({
2955
+ return asText({
2650
2956
  success: true,
2651
2957
  mode: "hosted-upload-ref-awaiting-upload",
2652
2958
  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.",
@@ -2682,7 +2988,7 @@ async function executeHostedParse(args2, session, log, origin) {
2682
2988
  session,
2683
2989
  origin
2684
2990
  );
2685
- return asText3(parseJson);
2991
+ return asText(parseJson);
2686
2992
  }
2687
2993
  var scrapeTool = {
2688
2994
  name: "firecrawl_scrape",
@@ -2696,24 +3002,15 @@ var scrapeTool = {
2696
3002
  // Does not modify, delete, or write to external websites.
2697
3003
  },
2698
3004
  description: `
2699
- Retrieve and extract content from one supplied URL through Firecrawl. Use this when the request identifies a page and needs its content or defined fields. It can return markdown, HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema; JSON is useful when the requested result has defined fields, while markdown preserves readable page content.
2700
-
2701
- This tool operates on a known page. Use \`firecrawl_search\` when additional web sources are needed. For a set of pages use \`firecrawl_crawl\`, and to discover page URLs use \`firecrawl_map\`. Options include JavaScript render delay, cache age, main-content filtering, PII redaction, and lockdown cache-only retrieval. Browser actions may change the live page when interactive actions are enabled.
2702
-
2703
- Firecrawl may reuse recently indexed content instead of refetching the page, and the reuse window varies by domain. Set \`maxAge: 0\` to force a live fetch, or a smaller \`maxAge\` to bound how stale reused content may be. A successful response does not by itself confirm that the state it describes is still current.
2704
-
2705
- Returns the selected content formats and page metadata. Authenticated responses can include a \`metadata.scrapeId\` for optional scrape feedback.
2706
-
2707
- 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.
2708
- Alexandria mode: pass \`alexandria\` (one \`{provider, capability, options}\` object or an array of 1-10) instead of \`url\` to execute catalogued Alexandria capabilities found through \`firecrawl_search\` sources \`alexandria\` or \`firecrawl_find_tools\`. The optional requestId identifies one logical execution: reuse the returned ID for retries of the identical payload, never a new ID to bypass pending or uncertain execution. Each call may include version to pin a published workflow; omitting it uses latest. Only timeout also applies at the top level in this mode. Returns per-capability results in \`data.alexandria\`, including \`data\`, \`records\`, or an \`error\` with a code. Check each item for errors even when the outer response is successful. Alexandria needs an API key on a team with Alexandria enabled.
2709
- Alexandria mode: pass \`alexandria\` (one \`{provider, capability, options}\` object or an array of 1-10) instead of \`url\` to execute catalogued Alexandria capabilities found through \`firecrawl_search\` sources \`alexandria\` or \`firecrawl_find_tools\`. The optional requestId identifies one logical execution: reuse the returned ID for retries of the identical payload, never a new ID to bypass pending or uncertain execution. Each call may include version to pin a published workflow; omitting it uses latest. Only timeout also applies at the top level in this mode. Returns per-capability results in \`data.alexandria\`, including \`data\`, \`records\`, or an \`error\` with a code. Check each item for errors even when the outer response is successful. Alexandria needs an API key on a team with Alexandria enabled. 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).
3005
+ 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.
2710
3006
 
2711
- For potentially large workflow results, supply and preserve a top-level requestId before execution. If a response provides nextTool, follow its instructions to access the result without repeating a successful provider call.
3007
+ 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.
2712
3008
 
2713
- URL mode only: set \`domainTools: true\` to also return domain-matched Alexandria tools for the page in \`tools\` on the returned document.
3009
+ 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.
2714
3010
 
2715
- Alexandria 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. A terms-gated Alexandria provider returns \`THIRD_PARTY_DATA_TERMS_REQUIRED\` (403) with \`requiresAction.url\`: follow the returned terms/show and terms/accept calls through this tool, only accepting after explicit user authorization for the reviewed version and digest. An organization admin can alternatively accept at the dashboard URL. Retry only after confirmed acceptance.
3011
+ Alexandria mode, on an authenticated session with Alexandria access: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`; the \`alexandria\` and \`requestId\` parameters describe batching, retries, errors and provider terms.
2716
3012
  `,
3013
+ outputSchema: scrapeOutputSchema,
2717
3014
  parameters: scrapeToolParamsSchema,
2718
3015
  execute: async (args2, { session, log, client: mcpClient }) => {
2719
3016
  const origin = requestOrigin(mcpClient, session);
@@ -2728,7 +3025,9 @@ Alexandria execution errors relay a \`code\` and \`chargeId\`: \`request_in_flig
2728
3025
  log.info("Executing Alexandria capabilities", {
2729
3026
  count: Array.isArray(alexandria) ? alexandria.length : 1
2730
3027
  });
2731
- return executeExchangeCalls(session, alexandria, suppliedRequestId, options.timeout, origin);
3028
+ return structuredJsonText(
3029
+ await executeExchangeCalls(session, alexandria, suppliedRequestId, options.timeout, origin)
3030
+ );
2732
3031
  }
2733
3032
  const transformed = transformScrapeParams(
2734
3033
  options
@@ -2749,7 +3048,7 @@ Alexandria execution errors relay a \`code\` and \`chargeId\`: \`request_in_flig
2749
3048
  },
2750
3049
  session
2751
3050
  );
2752
- return asText3(json?.data ?? json);
3051
+ return structuredText(json?.data ?? json);
2753
3052
  }
2754
3053
  const client = getClient(session);
2755
3054
  const res = await relayTermsRequired(
@@ -2759,10 +3058,13 @@ Alexandria execution errors relay a \`code\` and \`chargeId\`: \`request_in_flig
2759
3058
  }),
2760
3059
  { tool: "firecrawl_scrape" }
2761
3060
  );
2762
- return asText3(res);
3061
+ return structuredText(res);
2763
3062
  }
2764
3063
  };
2765
- server.addTool(scrapeTool);
3064
+ server.addTool({
3065
+ ...scrapeTool,
3066
+ _meta: { "anthropic/alwaysLoad": true }
3067
+ });
2766
3068
  server.addTool({
2767
3069
  name: "firecrawl_map",
2768
3070
  annotations: {
@@ -2779,13 +3081,14 @@ Enumerate URLs indexed under one website through Firecrawl without fetching each
2779
3081
 
2780
3082
  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.
2781
3083
  `,
2782
- parameters: z7.object({
2783
- url: z7.string().url(),
2784
- search: z7.string().optional(),
2785
- sitemap: z7.enum(["include", "skip", "only"]).optional(),
2786
- includeSubdomains: z7.boolean().optional(),
2787
- limit: z7.number().optional(),
2788
- ignoreQueryParameters: z7.boolean().optional()
3084
+ outputSchema: mapOutputSchema,
3085
+ parameters: z8.object({
3086
+ url: z8.string().url(),
3087
+ search: z8.string().optional(),
3088
+ sitemap: z8.enum(["include", "skip", "only"]).optional(),
3089
+ includeSubdomains: z8.boolean().optional(),
3090
+ limit: z8.number().optional(),
3091
+ ignoreQueryParameters: z8.boolean().optional()
2789
3092
  }),
2790
3093
  execute: async (args2, { session, log, client: mcpClient }) => {
2791
3094
  const { url, ...options } = args2;
@@ -2796,11 +3099,12 @@ Returns matching URLs rather than page bodies. Retrieve one page with \`firecraw
2796
3099
  ...cleaned,
2797
3100
  origin: requestOrigin(mcpClient, session)
2798
3101
  });
2799
- return asText3(res);
3102
+ return structuredText(res);
2800
3103
  }
2801
3104
  });
2802
3105
  server.addTool({
2803
3106
  name: "firecrawl_search",
3107
+ _meta: { "anthropic/alwaysLoad": true },
2804
3108
  annotations: {
2805
3109
  title: "Firecrawl web search",
2806
3110
  readOnlyHint: true,
@@ -2811,19 +3115,18 @@ server.addTool({
2811
3115
  // Query-only; no destructive side effects on external entities.
2812
3116
  },
2813
3117
  description: `
2814
- Search web, news, or image sources and return ranked results with query-relevant highlights. Operators include quoted phrases, \`-term\`, \`site:host\`, \`inurl:term\`, \`intitle:term\`, and \`related:host\`; the set is non-exhaustive. \`includeDomains\` and \`excludeDomains\` are mutually exclusive hostname filters; categories limit results to research, PDF, or developer sources.
3118
+ Search web, news, or image sources and return ranked results with query-relevant highlights. Each web result is a title, URL, and description; use \`firecrawl_scrape\` on a result URL when the excerpt is not enough.
2815
3119
 
2816
- For a programming question, add \`categories: ["developer"]\`. It searches an index of public repositories, GitHub issues, merged pull requests, repository READMEs, and code documentation, and returns the results in \`data.web\` with \`category: "developer"\`.
2817
-
2818
- \`categories: ["research"]\` restricts these web results to research-affiliated websites and returns page snippets. The \`firecrawl_research_*\` tools are a separate surface that searches paper abstracts and full text across biomedical (PubMed, bioRxiv, medRxiv) and arXiv literature.
3120
+ ${ALEXANDRIA_SEARCH_LEAD}
2819
3121
 
2820
- Each web result is a title, URL, and description. Highlights appear in web \`description\` and news \`snippet\`; otherwise, original snippets are returned. If excerpts are insufficient, use \`firecrawl_scrape\` to retrieve content from relevant result URLs. Add \`scrapeOptions\` to attach page content in the same call; those fetches ignore \`maxAge\`, so use \`firecrawl_scrape\` when you need a live fetch. Returns source-type result groups and usage metadata. Authenticated responses can include an \`id\` for optional search feedback.
3122
+ 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.
2821
3123
 
2822
- ${ALEXANDRIA_INSTRUCTIONS}
3124
+ 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.
2823
3125
  `,
2824
- parameters: z7.object({
3126
+ outputSchema: searchOutputSchema,
3127
+ parameters: z8.object({
2825
3128
  ...searchToolBaseFields,
2826
- scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3129
+ 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.")
2827
3130
  }).refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
2828
3131
  searchQueryIsValid,
2829
3132
  "A query is required. Use Find Tools for catalogue lookup."
@@ -2858,7 +3161,7 @@ ${ALEXANDRIA_INSTRUCTIONS}
2858
3161
  const json = await keylessPost("/v2/search", searchBody, session);
2859
3162
  const keylessResponse = { ...json ?? {} };
2860
3163
  delete keylessResponse.id;
2861
- return compactText(keylessResponse);
3164
+ return structuredCompact(keylessResponse);
2862
3165
  }
2863
3166
  const client = getClient(session);
2864
3167
  const postSearch = () => postSearchWithFallback(
@@ -2868,7 +3171,7 @@ ${ALEXANDRIA_INSTRUCTIONS}
2868
3171
  );
2869
3172
  const context = { tool: "firecrawl_search" };
2870
3173
  const httpRes = exchangeSource ? await relayExchangeError(postSearch, context) : await relayTermsRequired(postSearch, context);
2871
- return compactText(httpRes?.data ?? {});
3174
+ return structuredCompact(httpRes?.data ?? {});
2872
3175
  }
2873
3176
  });
2874
3177
  async function executeExchangeCalls(session, alexandria, suppliedRequestId, timeout, origin = requestOrigin(void 0, session)) {
@@ -2906,11 +3209,11 @@ async function executeExchangeCalls(session, alexandria, suppliedRequestId, time
2906
3209
  randomUUID(),
2907
3210
  alexandriaFeedbackAvailable(session) && alexandriaCallsWarrantFeedback(calls) ? { feedbackTool: ALEXANDRIA_FEEDBACK_HINT } : {}
2908
3211
  );
2909
- } catch (error) {
2910
- if (error?.response?.status === 401) throw error;
3212
+ } catch (error2) {
3213
+ if (error2?.response?.status === 401) throw error2;
2911
3214
  throw new UserError2(
2912
- `${error instanceof Error ? error.message : String(error)} Request ID: ${requestId}. Reuse this ID only with the identical payload.`,
2913
- { ...error instanceof UserError2 ? error.extras : {}, requestId }
3215
+ `${error2 instanceof Error ? error2.message : String(error2)} Request ID: ${requestId}. Reuse this ID only with the identical payload.`,
3216
+ { ...error2 instanceof UserError2 ? error2.extras : {}, requestId }
2914
3217
  );
2915
3218
  }
2916
3219
  }
@@ -2923,15 +3226,16 @@ var findToolsTool = {
2923
3226
  destructiveHint: false,
2924
3227
  idempotentHint: true
2925
3228
  },
2926
- 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).",
3229
+ 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.",
3230
+ outputSchema: findToolsOutputSchema,
2927
3231
  parameters: findToolsSchema,
2928
3232
  execute: async (args2, { session, client: mcpClient }) => {
2929
3233
  const origin = requestOrigin(mcpClient, session);
2930
3234
  let options;
2931
3235
  try {
2932
3236
  options = findToolsOptions(args2);
2933
- } catch (error) {
2934
- throw new UserError2(error instanceof Error ? error.message : String(error));
3237
+ } catch (error2) {
3238
+ throw new UserError2(error2 instanceof Error ? error2.message : String(error2));
2935
3239
  }
2936
3240
  if (options.level === "categories") {
2937
3241
  assertExchangeCredential(session);
@@ -2951,32 +3255,26 @@ var findToolsTool = {
2951
3255
  }));
2952
3256
  const page = { level: "categories", items, total: rows.length };
2953
3257
  if (offset + options.limit < rows.length) page.nextTool = { name: "firecrawl_find_tools", arguments: { level: "categories", limit: options.limit, offset: offset + options.limit } };
2954
- return compactText(withAlexandriaFeedbackHint(withFindToolsNavigation({ success: true, data: { creditsCost: 0, alexandria: [{ provider: "firecrawl", capability: "find-tools", creditsCost: 0, data: page }] } }), alexandriaFeedbackAvailable(session)));
3258
+ return structuredCompact(withAlexandriaFeedbackHint(withFindToolsNavigation({ success: true, data: { creditsCost: 0, alexandria: [{ provider: "firecrawl", capability: "find-tools", creditsCost: 0, data: page }] } }), alexandriaFeedbackAvailable(session)));
2955
3259
  }
2956
3260
  const result = await executeExchangeCalls(session, { provider: "firecrawl", capability: "find-tools", options }, void 0, void 0, origin);
2957
- return compactText(withFindToolsNavigation(JSON.parse(result)));
3261
+ return structuredCompact(withFindToolsNavigation(JSON.parse(result)));
2958
3262
  }
2959
3263
  };
2960
3264
  server.addTool(findToolsTool);
2961
3265
  var SEARCH_SURFACE_SCRAPE_DESCRIPTION = `
2962
- Retrieve and extract content from one supplied URL through Firecrawl, or execute catalogued Alexandria capabilities. Use URL mode when the request identifies a page and needs its content or defined fields. It can return markdown, HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema; JSON is useful when the requested result has defined fields, while markdown preserves readable page content. Options include JavaScript render delay, cache age, main-content filtering, PII redaction, and lockdown cache-only retrieval. Browser actions may change the live page when interactive actions are enabled. A named browser profile can load saved session data and overwrite its stored state.
2963
-
2964
- Firecrawl may reuse recently indexed content instead of refetching the page, and the reuse window varies by domain. Set \`maxAge: 0\` to force a live fetch, or a smaller \`maxAge\` to bound how stale reused content may be. A successful response does not by itself confirm that the state it describes is still current. URL mode returns the selected content formats and page metadata.
2965
-
2966
- 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.
2967
- Alexandria mode: pass \`alexandria\` (one \`{provider, capability, options}\` object or an array of 1-10) instead of \`url\` to execute catalogued Alexandria capabilities found through \`firecrawl_search\` sources \`alexandria\` or \`firecrawl_find_tools\`. The optional requestId identifies one logical execution: reuse the returned ID for retries of the identical payload, never a new ID to bypass pending or uncertain execution. Each call may include version to pin a published workflow; omitting it uses latest. Only timeout also applies at the top level in this mode. Returns per-capability results in \`data.alexandria\`, including \`data\`, \`records\`, or an \`error\` with a code. Check each item for errors even when the outer response is successful. Alexandria execution is billed at the capability's listed price and needs a team with Alexandria enabled.
3266
+ 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.
2968
3267
 
2969
- For potentially large workflow results, supply and preserve a top-level requestId before execution. If a response provides nextTool, follow its instructions to access the result without repeating a successful provider call.
3268
+ \`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.
2970
3269
 
2971
- URL mode only: set \`domainTools: true\` to also return domain-matched Alexandria tools for the page in \`tools\` on the returned document.
2972
-
2973
- Alexandria 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. A terms-gated Alexandria provider returns \`THIRD_PARTY_DATA_TERMS_REQUIRED\` (403) with \`requiresAction.url\`: follow the returned terms/show and terms/accept calls through this tool, only accepting after explicit user authorization for the reviewed version and digest. An organization admin can alternatively accept at the dashboard URL. Retry only after confirmed acceptance.
3270
+ Alexandria mode: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`; the \`alexandria\` and \`requestId\` parameters describe batching, retries, errors and provider terms. Alexandria execution is billed at the capability's listed price and needs a team with Alexandria enabled.
2974
3271
  `;
2975
3272
  var searchSurfaceScrapeTool = {
2976
3273
  ...scrapeTool,
3274
+ _meta: { "anthropic/alwaysLoad": true },
2977
3275
  description: SEARCH_SURFACE_SCRAPE_DESCRIPTION
2978
3276
  };
2979
- 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.";
3277
+ 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.";
2980
3278
  var searchSurfaceFindToolsTool = {
2981
3279
  ...findToolsTool,
2982
3280
  description: SEARCH_SURFACE_FIND_TOOLS_DESCRIPTION
@@ -3127,17 +3425,17 @@ async function waitForCrawlCompletionWithOrigin(client, jobId, origin, pollInter
3127
3425
  );
3128
3426
  }
3129
3427
  }
3130
- var feedbackIssueSchema = z7.string().trim().min(1).max(80).regex(
3428
+ var feedbackIssueSchema = z8.string().trim().min(1).max(80).regex(
3131
3429
  /^[a-z0-9][a-z0-9_-]*$/,
3132
3430
  "Issue codes must use lowercase letters, numbers, underscores, or hyphens"
3133
3431
  );
3134
- var valuableSourceSchema = z7.object({
3135
- url: z7.string().url(),
3136
- reason: z7.string().max(1e3).optional()
3432
+ var valuableSourceSchema = z8.object({
3433
+ url: z8.string().url(),
3434
+ reason: z8.string().max(1e3).optional()
3137
3435
  });
3138
- var missingContentSchema = z7.object({
3139
- topic: z7.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3140
- description: z7.string().max(2e3).optional()
3436
+ var missingContentSchema = z8.object({
3437
+ topic: z8.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3438
+ description: z8.string().max(2e3).optional()
3141
3439
  });
3142
3440
  var FEEDBACK_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
3143
3441
  function feedbackEnvEnabled(...keys) {
@@ -3175,24 +3473,25 @@ Records schema-validated quality feedback for a prior \`firecrawl_search\` UUID
3175
3473
 
3176
3474
  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.
3177
3475
  `,
3178
- parameters: z7.object({
3179
- searchId: z7.string().uuid("searchId must be the UUID returned by firecrawl_search"),
3180
- rating: z7.enum(["good", "bad", "partial"]),
3181
- valuableSources: z7.array(
3182
- z7.object({
3183
- url: z7.string().url(),
3184
- reason: z7.string().max(1e3).optional()
3476
+ outputSchema: feedbackOutputSchema,
3477
+ parameters: z8.object({
3478
+ searchId: z8.string().uuid("searchId must be the UUID returned by firecrawl_search"),
3479
+ rating: z8.enum(["good", "bad", "partial"]),
3480
+ valuableSources: z8.array(
3481
+ z8.object({
3482
+ url: z8.string().url(),
3483
+ reason: z8.string().max(1e3).optional()
3185
3484
  })
3186
3485
  ).max(50).optional(),
3187
- missingContent: z7.array(
3188
- z7.object({
3189
- topic: z7.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3190
- description: z7.string().max(2e3).optional()
3486
+ missingContent: z8.array(
3487
+ z8.object({
3488
+ topic: z8.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3489
+ description: z8.string().max(2e3).optional()
3191
3490
  })
3192
3491
  ).max(20).optional().describe(
3193
3492
  "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`."
3194
3493
  ),
3195
- querySuggestions: z7.string().max(2e3).optional()
3494
+ querySuggestions: z8.string().max(2e3).optional()
3196
3495
  }),
3197
3496
  execute: async (args2, { session, log, client: mcpClient }) => {
3198
3497
  const origin = requestOrigin(mcpClient, session);
@@ -3252,7 +3551,7 @@ Eligibility is limited to successful searches within the feedback age window. Th
3252
3551
  status: response.status,
3253
3552
  feedbackErrorCode: parsed?.feedbackErrorCode
3254
3553
  });
3255
- return asText3({
3554
+ return structuredText({
3256
3555
  success: false,
3257
3556
  status: response.status,
3258
3557
  feedbackErrorCode: parsed?.feedbackErrorCode,
@@ -3260,7 +3559,7 @@ Eligibility is limited to successful searches within the feedback age window. Th
3260
3559
  retryable: response.status >= 500
3261
3560
  });
3262
3561
  }
3263
- return asText3(parsed);
3562
+ return structuredText(parsed);
3264
3563
  }
3265
3564
  });
3266
3565
  }
@@ -3291,20 +3590,21 @@ For an Alexandria session, set endpoint to \`alexandria\`, omit jobId, and provi
3291
3590
 
3292
3591
  Returns submission status, feedback ID, and accounting fields.
3293
3592
  `,
3294
- parameters: z7.object({
3295
- endpoint: z7.enum(["search", "scrape", "parse", "map", "alexandria"]),
3296
- jobId: z7.string().uuid("jobId must be the UUID returned by Firecrawl").optional(),
3593
+ outputSchema: feedbackOutputSchema,
3594
+ parameters: z8.object({
3595
+ endpoint: z8.enum(["search", "scrape", "parse", "map", "alexandria"]),
3596
+ jobId: z8.string().uuid("jobId must be the UUID returned by Firecrawl").optional(),
3297
3597
  ...alexandriaFeedbackFields,
3298
- rating: z7.enum(["good", "bad", "partial"]),
3299
- issues: z7.array(feedbackIssueSchema).max(20).optional(),
3300
- tags: z7.array(feedbackIssueSchema).max(20).optional(),
3301
- note: z7.string().max(4e3).optional(),
3302
- valuableSources: z7.array(valuableSourceSchema).max(50).optional(),
3303
- missingContent: z7.array(missingContentSchema).max(50).optional(),
3304
- querySuggestions: z7.string().max(2e3).optional(),
3305
- url: z7.string().url().optional(),
3306
- pageNumbers: z7.array(z7.number().int().positive()).max(100).optional(),
3307
- metadata: z7.record(z7.string(), z7.unknown()).optional()
3598
+ rating: z8.enum(["good", "bad", "partial"]),
3599
+ issues: z8.array(feedbackIssueSchema).max(20).optional(),
3600
+ tags: z8.array(feedbackIssueSchema).max(20).optional(),
3601
+ note: z8.string().max(4e3).optional(),
3602
+ valuableSources: z8.array(valuableSourceSchema).max(50).optional(),
3603
+ missingContent: z8.array(missingContentSchema).max(50).optional(),
3604
+ querySuggestions: z8.string().max(2e3).optional(),
3605
+ url: z8.string().url().optional(),
3606
+ pageNumbers: z8.array(z8.number().int().positive()).max(100).optional(),
3607
+ metadata: z8.record(z8.string(), z8.unknown()).optional()
3308
3608
  }).superRefine((value, ctx) => {
3309
3609
  if (value.endpoint === "alexandria") {
3310
3610
  const parsed = alexandriaSessionFeedbackSchema.safeParse(value);
@@ -3386,7 +3686,7 @@ Returns submission status, feedback ID, and accounting fields.
3386
3686
  status: response.status,
3387
3687
  feedbackErrorCode: parsed?.feedbackErrorCode
3388
3688
  });
3389
- return asText3({
3689
+ return structuredText({
3390
3690
  success: false,
3391
3691
  status: response.status,
3392
3692
  feedbackErrorCode: parsed?.feedbackErrorCode,
@@ -3394,7 +3694,7 @@ Returns submission status, feedback ID, and accounting fields.
3394
3694
  retryable: response.status >= 500
3395
3695
  });
3396
3696
  }
3397
- return asText3(parsed);
3697
+ return structuredText(parsed);
3398
3698
  }
3399
3699
  });
3400
3700
  }
@@ -3414,27 +3714,28 @@ Start a multi-page crawl at a website URL, poll it to a terminal state, and retu
3414
3714
 
3415
3715
  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.
3416
3716
  `,
3417
- parameters: z7.object({
3418
- url: z7.string(),
3419
- prompt: z7.string().optional(),
3420
- excludePaths: z7.array(z7.string()).optional(),
3421
- includePaths: z7.array(z7.string()).optional(),
3422
- maxDiscoveryDepth: z7.number().optional(),
3423
- sitemap: z7.enum(["skip", "include", "only"]).optional(),
3424
- limit: z7.number().optional(),
3425
- allowExternalLinks: z7.boolean().optional(),
3426
- allowSubdomains: z7.boolean().optional(),
3427
- crawlEntireDomain: z7.boolean().optional(),
3428
- delay: z7.number().optional(),
3429
- maxConcurrency: z7.number().optional(),
3717
+ parameters: z8.object({
3718
+ url: z8.string(),
3719
+ prompt: z8.string().optional(),
3720
+ excludePaths: z8.array(z8.string()).optional(),
3721
+ includePaths: z8.array(z8.string()).optional(),
3722
+ maxDiscoveryDepth: z8.number().optional(),
3723
+ sitemap: z8.enum(["skip", "include", "only"]).optional(),
3724
+ limit: z8.number().optional(),
3725
+ allowExternalLinks: z8.boolean().optional(),
3726
+ allowSubdomains: z8.boolean().optional(),
3727
+ crawlEntireDomain: z8.boolean().optional(),
3728
+ delay: z8.number().optional(),
3729
+ maxConcurrency: z8.number().optional(),
3430
3730
  ...SAFE_MODE ? {} : {
3431
- webhook: z7.string().optional(),
3432
- webhookHeaders: z7.record(z7.string(), z7.string()).optional()
3731
+ webhook: z8.string().optional(),
3732
+ webhookHeaders: z8.record(z8.string(), z8.string()).optional()
3433
3733
  },
3434
- deduplicateSimilarURLs: z7.boolean().optional(),
3435
- ignoreQueryParameters: z7.boolean().optional(),
3734
+ deduplicateSimilarURLs: z8.boolean().optional(),
3735
+ ignoreQueryParameters: z8.boolean().optional(),
3436
3736
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3437
3737
  }),
3738
+ outputSchema: crawlOutputSchema,
3438
3739
  execute: async (args2, { session, log, client: mcpClient }) => {
3439
3740
  const origin = requestOrigin(mcpClient, session);
3440
3741
  const { url, ...options } = args2;
@@ -3461,7 +3762,7 @@ Crawl results can be large; use conservative limits when full-site coverage is u
3461
3762
  });
3462
3763
  const crawlId = started?.data?.id;
3463
3764
  if (!crawlId) {
3464
- return asText3(started?.data ?? {});
3765
+ return structuredText(started?.data ?? {});
3465
3766
  }
3466
3767
  const res = await waitForCrawlCompletionWithOrigin(
3467
3768
  client,
@@ -3470,7 +3771,7 @@ Crawl results can be large; use conservative limits when full-site coverage is u
3470
3771
  pollInterval,
3471
3772
  timeout
3472
3773
  );
3473
- return asText3(res);
3774
+ return structuredText(res);
3474
3775
  }
3475
3776
  });
3476
3777
  server.addTool({
@@ -3487,7 +3788,8 @@ server.addTool({
3487
3788
  description: `
3488
3789
  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.
3489
3790
  `,
3490
- parameters: z7.object({ id: z7.string() }),
3791
+ outputSchema: crawlOutputSchema,
3792
+ parameters: z8.object({ id: z8.string() }),
3491
3793
  execute: async (args2, {
3492
3794
  session,
3493
3795
  client: mcpClient
@@ -3499,7 +3801,7 @@ Retrieve the current status, progress, and available results for an existing cra
3499
3801
  id,
3500
3802
  requestOrigin(mcpClient, session)
3501
3803
  );
3502
- return asText3(res);
3804
+ return structuredText(res);
3503
3805
  }
3504
3806
  });
3505
3807
  server.addTool({
@@ -3513,13 +3815,14 @@ server.addTool({
3513
3815
  description: `
3514
3816
  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.
3515
3817
  `,
3516
- parameters: z7.object({
3517
- urls: z7.array(z7.string()),
3518
- prompt: z7.string().optional(),
3519
- schema: z7.record(z7.string(), z7.any()).optional(),
3520
- allowExternalLinks: z7.boolean().optional(),
3521
- enableWebSearch: z7.boolean().optional(),
3522
- includeSubdomains: z7.boolean().optional()
3818
+ outputSchema: deprecatedToolOutputSchema,
3819
+ parameters: z8.object({
3820
+ urls: z8.array(z8.string()),
3821
+ prompt: z8.string().optional(),
3822
+ schema: z8.record(z8.string(), z8.any()).optional(),
3823
+ allowExternalLinks: z8.boolean().optional(),
3824
+ enableWebSearch: z8.boolean().optional(),
3825
+ includeSubdomains: z8.boolean().optional()
3523
3826
  }),
3524
3827
  canList: () => false,
3525
3828
  beforeValidate: () => {
@@ -3551,10 +3854,11 @@ Run web research that returns structured data when the URLs are not known or the
3551
3854
 
3552
3855
  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\`.
3553
3856
  `,
3554
- parameters: z7.object({
3555
- prompt: z7.string().min(1).max(1e4),
3556
- urls: z7.array(z7.string().url()).optional(),
3557
- schema: z7.record(z7.string(), z7.any()).optional()
3857
+ outputSchema: agentOutputSchema,
3858
+ parameters: z8.object({
3859
+ prompt: z8.string().min(1).max(1e4),
3860
+ urls: z8.array(z8.string().url()).optional(),
3861
+ schema: z8.record(z8.string(), z8.any()).optional()
3558
3862
  }),
3559
3863
  execute: async (args2, { session, log, client: mcpClient }) => {
3560
3864
  const client = getClient(session);
@@ -3572,7 +3876,7 @@ This call returns only a job ID, not the research result. Read the job with \`fi
3572
3876
  ...agentBody,
3573
3877
  origin: requestOrigin(mcpClient, session)
3574
3878
  });
3575
- return asText3(res);
3879
+ return structuredText(res);
3576
3880
  }
3577
3881
  });
3578
3882
  server.addTool({
@@ -3591,7 +3895,8 @@ Retrieve progress or final results for a \`firecrawl_agent\` job ID. A \`process
3591
3895
 
3592
3896
  Returns job status, progress information, and result data when completed.
3593
3897
  `,
3594
- parameters: z7.object({ id: z7.string() }),
3898
+ outputSchema: agentStatusOutputSchema,
3899
+ parameters: z8.object({ id: z8.string() }),
3595
3900
  execute: async (args2, { session, log, client: mcpClient }) => {
3596
3901
  const client = getClient(session);
3597
3902
  const { id } = args2;
@@ -3600,7 +3905,7 @@ Returns job status, progress information, and result data when completed.
3600
3905
  `/v2/agent/${encodeURIComponent(id)}`,
3601
3906
  originHeaders(requestOrigin(mcpClient, session))
3602
3907
  );
3603
- return asText3(res?.data ?? {});
3908
+ return structuredText(res?.data ?? {});
3604
3909
  }
3605
3910
  });
3606
3911
  server.addTool({
@@ -3619,13 +3924,14 @@ Open or reuse a live browser session to navigate a page, click controls, fill fi
3619
3924
 
3620
3925
  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.
3621
3926
  `,
3622
- parameters: z7.object({
3623
- scrapeId: z7.string().trim().min(1).optional(),
3624
- url: z7.string().trim().url().optional(),
3625
- prompt: z7.string().trim().min(1).optional(),
3626
- code: z7.string().trim().min(1).optional(),
3627
- language: z7.enum(["bash", "python", "node"]).optional(),
3628
- timeout: z7.number().min(1).max(300).optional(),
3927
+ outputSchema: interactOutputSchema,
3928
+ parameters: z8.object({
3929
+ scrapeId: z8.string().trim().min(1).optional(),
3930
+ url: z8.string().trim().url().optional(),
3931
+ prompt: z8.string().trim().min(1).optional(),
3932
+ code: z8.string().trim().min(1).optional(),
3933
+ language: z8.enum(["bash", "python", "node"]).optional(),
3934
+ timeout: z8.number().min(1).max(300).optional(),
3629
3935
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3630
3936
  }).refine((data) => Boolean(data.scrapeId) !== Boolean(data.url), {
3631
3937
  message: "Provide either 'url' (interact directly) or 'scrapeId' (reuse a previous scrape), not both."
@@ -3657,14 +3963,14 @@ This acts on the live site, so actions such as form submission can create persis
3657
3963
  });
3658
3964
  scrapeId = scraped?.metadata?.scrapeId;
3659
3965
  if (!scrapeId) {
3660
- return asText3({
3966
+ return structuredText({
3661
3967
  error: "Could not open an interact session: the scrape did not return a scrapeId. Try firecrawl_scrape first, then pass its scrapeId.",
3662
3968
  url
3663
3969
  });
3664
3970
  }
3665
3971
  }
3666
3972
  if (!scrapeId) {
3667
- return asText3({
3973
+ return structuredText({
3668
3974
  error: "Could not open an interact session: missing scrapeId.",
3669
3975
  url
3670
3976
  });
@@ -3678,15 +3984,15 @@ This acts on the live site, so actions such as form submission can create persis
3678
3984
  if (timeout != null) interactArgs.timeout = timeout;
3679
3985
  const res = await client.interact(activeScrapeId, interactArgs);
3680
3986
  if (openedFromUrl && res && typeof res === "object" && !Array.isArray(res)) {
3681
- return asText3({
3987
+ return structuredText({
3682
3988
  ...res,
3683
3989
  scrapeId: activeScrapeId
3684
3990
  });
3685
3991
  }
3686
3992
  if (openedFromUrl) {
3687
- return asText3({ scrapeId: activeScrapeId, result: res });
3993
+ return structuredText({ scrapeId: activeScrapeId, result: res });
3688
3994
  }
3689
- return asText3(res);
3995
+ return structuredText(res);
3690
3996
  }
3691
3997
  });
3692
3998
  server.addTool({
@@ -3703,8 +4009,9 @@ server.addTool({
3703
4009
  description: `
3704
4010
  Stop the live interact session associated with a \`scrapeId\` and release its resources. Returns a success confirmation.
3705
4011
  `,
3706
- parameters: z7.object({
3707
- scrapeId: z7.string()
4012
+ outputSchema: interactStopOutputSchema,
4013
+ parameters: z8.object({
4014
+ scrapeId: z8.string()
3708
4015
  }),
3709
4016
  execute: async (args2, { session, log, client: mcpClient }) => {
3710
4017
  const client = getClient(session);
@@ -3714,7 +4021,7 @@ Stop the live interact session associated with a \`scrapeId\` and release its re
3714
4021
  `/v2/scrape/${encodeURIComponent(scrapeId)}/interact`,
3715
4022
  originHeaders(requestOrigin(mcpClient, session))
3716
4023
  );
3717
- return asText3(res?.data ?? {});
4024
+ return structuredText(res?.data ?? {});
3718
4025
  }
3719
4026
  });
3720
4027
  server.addTool({
@@ -3735,11 +4042,14 @@ Local MCP reads \`filePath\` from the server filesystem. Hosted MCP uses two cal
3735
4042
 
3736
4043
  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.
3737
4044
  `,
4045
+ outputSchema: parseOutputSchema,
3738
4046
  parameters: parseParamsSchema,
3739
4047
  execute: async (args2, { session, log, client: mcpClient }) => {
3740
4048
  const origin = requestOrigin(mcpClient, session);
3741
4049
  if (process.env.CLOUD_SERVICE === "true") {
3742
- return executeHostedParse(args2, session, log, origin);
4050
+ return structuredJsonText(
4051
+ await executeHostedParse(args2, session, log, origin)
4052
+ );
3743
4053
  }
3744
4054
  const apiUrl = process.env.FIRECRAWL_API_URL;
3745
4055
  if (!apiUrl) {
@@ -3789,15 +4099,16 @@ Set \`redactPII\` to request redaction of personally identifiable information in
3789
4099
  );
3790
4100
  }
3791
4101
  try {
3792
- return asText3(JSON.parse(responseText));
4102
+ return structuredText(JSON.parse(responseText));
3793
4103
  } catch {
3794
- return responseText;
4104
+ return withStructured(responseText, { raw: responseText });
3795
4105
  }
3796
4106
  }
3797
4107
  });
3798
4108
  function registerMarketplaceSearchTool(registrar, getClientFn) {
3799
4109
  registrar.addTool({
3800
4110
  name: "firecrawl_search",
4111
+ _meta: { "anthropic/alwaysLoad": true },
3801
4112
  annotations: {
3802
4113
  title: "Firecrawl web search",
3803
4114
  readOnlyHint: true,
@@ -3813,7 +4124,8 @@ ${ALEXANDRIA_SEARCH_INSTRUCTIONS}
3813
4124
 
3814
4125
  Returns result groups in \`data\` and an operation \`id\`.
3815
4126
  `,
3816
- parameters: z7.object({ ...searchToolBaseFields }).strict().refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
4127
+ outputSchema: searchOutputSchema,
4128
+ parameters: z8.object({ ...searchToolBaseFields }).strict().refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
3817
4129
  searchQueryIsValid,
3818
4130
  "A query is required. Use firecrawl_find_tools to browse the catalogue."
3819
4131
  ),
@@ -3864,7 +4176,7 @@ Returns result groups in \`data\` and an operation \`id\`.
3864
4176
  );
3865
4177
  const context = { tool: "firecrawl_search" };
3866
4178
  const httpRes = exchangeSource ? await relayExchangeError(postSearch, context) : await relayTermsRequired(postSearch, context);
3867
- return compactText(httpRes?.data ?? {});
4179
+ return structuredCompact(httpRes?.data ?? {});
3868
4180
  }
3869
4181
  });
3870
4182
  }
@@ -3936,10 +4248,10 @@ if (searchProfileEnabled) {
3936
4248
  stateless: true
3937
4249
  }
3938
4250
  });
3939
- } catch (error) {
4251
+ } catch (error2) {
3940
4252
  console.error(
3941
4253
  `[search-profile] failed to start on port ${searchProfile.port}; the full surface is unaffected`,
3942
- error
4254
+ error2
3943
4255
  );
3944
4256
  }
3945
4257
  }