firecrawl-mcp 3.25.4 → 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 +717 -387
  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,25 +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.';
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.';
136
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;
137
- var ALEXANDRIA_CONTRACT_GUIDANCE = "Read the selected contract before executing: required inputs and requiresOneOf groups (at least one member per group), example.request/example.response when present, and response.key (do not assume records is the result key). Follow the declared pagination input and response cursor, preserving filters; catalogue next is separate from provider pagination.";
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.";
138
418
  function findToolsOptions(args2) {
139
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");
140
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)) {
@@ -164,7 +444,7 @@ function withFindToolsNavigation(envelope) {
164
444
  if (link(page.next)) page.nextTool = link(page.next);
165
445
  return envelope;
166
446
  }
167
- var ALEXANDRIA_SEARCH_INSTRUCTIONS = 'Authenticated search combines web results, semantic tool summaries and domain matches. Use sources: ["alexandria"] for semantic tools only, or sources: ["web"] for web only. domainTools: false disables domain matching. Tool matches describe available structured-data capabilities, not executed data. toolDetail: "compact" (default) returns only provider, capability and description; "summary" adds metadata; "full" includes their input and output contracts. Execute a matched tool through firecrawl_scrape with an alexandria body; use firecrawl_find_tools to browse the catalogue or read a full contract.';
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.';
168
448
 
169
449
  // src/alexandria-output.ts
170
450
  var INLINE_TOKEN_BUDGET = 2e4;
@@ -231,7 +511,7 @@ async function alexandriaOutput(payload, calls, post, recoveryRequestId, extras
231
511
  }
232
512
 
233
513
  // src/developer.ts
234
- import { z as z3 } from "zod";
514
+ import { z as z4 } from "zod";
235
515
 
236
516
  // src/origin.ts
237
517
  import { createRequire } from "module";
@@ -297,12 +577,13 @@ Search an index of public repositories, GitHub issues, merged pull requests, rep
297
577
 
298
578
  Returns ranked results with an ID, source type, URL, title, and the matched passages in markdown.
299
579
  `,
300
- parameters: z3.object({
301
- query: z3.string().min(1).describe(
580
+ outputSchema: developerSearchOutputSchema,
581
+ parameters: z4.object({
582
+ query: z4.string().min(1).describe(
302
583
  "Natural-language developer question or search phrase, including the library, error message, or API involved when relevant."
303
584
  ),
304
- k: z3.number().int().min(1).max(100).optional().describe("Number of ranked results to return (default 10)."),
305
- skills: z3.enum(["only"]).optional().describe('Set to "only" to search only agent-skill files.')
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.')
306
587
  }),
307
588
  execute: async (args2, { session, client: mcpClient }) => {
308
589
  const { query, k, skills } = args2;
@@ -312,7 +593,8 @@ Returns ranked results with an ID, source type, URL, title, and the matched pass
312
593
  if (skills != null) params.append("skills", skills);
313
594
  const client = getClient2(session);
314
595
  const res = await client.http.get(`${BASE}?${params.toString()}`, originHeaders(requestOrigin(mcpClient, session)));
315
- return fmtDeveloper(res.data?.results);
596
+ const results = res.data?.results ?? [];
597
+ return withStructured(fmtDeveloper(results), { results });
316
598
  }
317
599
  });
318
600
  }
@@ -332,7 +614,7 @@ function extractSingleTrustedClientIp(rawForwardedFor) {
332
614
  }
333
615
 
334
616
  // src/monitor.ts
335
- import { z as z4 } from "zod";
617
+ import { z as z5 } from "zod";
336
618
 
337
619
  // src/session-credential.ts
338
620
  import { createHmac } from "crypto";
@@ -457,11 +739,8 @@ async function monitorRequest(session, origin, path2, init = {}) {
457
739
  }
458
740
  return payload;
459
741
  }
460
- function asText(data) {
461
- return JSON.stringify(data, null, 2);
462
- }
463
- var pageStatusSchema = z4.enum(["same", "new", "changed", "removed", "error"]);
464
- var checkStatusSchema = z4.enum([
742
+ var pageStatusSchema = z5.enum(["same", "new", "changed", "removed", "error"]);
743
+ var checkStatusSchema = z5.enum([
465
744
  "queued",
466
745
  "running",
467
746
  "completed",
@@ -554,22 +833,23 @@ Create a recurring scrape, crawl, or search monitor that compares each check wit
554
833
 
555
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.
556
835
  `,
557
- parameters: z4.object({
558
- body: z4.record(z4.string(), z4.any()).optional(),
559
- page: z4.string().optional(),
560
- pages: z4.array(z4.string()).optional(),
561
- queries: z4.array(z4.string()).optional(),
562
- searchWindow: z4.enum(["5m", "15m", "1h", "6h", "24h", "7d"]).optional(),
563
- maxResults: z4.number().int().min(1).max(50).optional(),
564
- includeDomains: z4.array(z4.string()).optional(),
565
- excludeDomains: z4.array(z4.string()).optional(),
566
- goal: z4.string().optional(),
567
- name: z4.string().optional(),
568
- scheduleText: z4.string().optional(),
569
- timezone: z4.string().optional(),
570
- email: z4.string().optional(),
571
- includeDiffs: z4.boolean().optional(),
572
- webhookUrl: z4.string().optional()
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()
573
853
  }),
574
854
  execute: async (args2, { session, log, client: mcpClient }) => {
575
855
  const body = buildMonitorCreateBody(args2);
@@ -583,7 +863,7 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
583
863
  body
584
864
  }
585
865
  );
586
- return asText(res);
866
+ return structuredText(res);
587
867
  }
588
868
  });
589
869
  server2.addTool({
@@ -600,9 +880,10 @@ In the simple form, a \`goal\` is required. If \`queries\` contains one or more
600
880
  description: `
601
881
  List monitors for the authenticated account with optional pagination controls. Returns one page of monitor records and pagination metadata.
602
882
  `,
603
- parameters: z4.object({
604
- limit: z4.number().int().positive().optional(),
605
- 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()
606
887
  }),
607
888
  execute: async (args2, { session, client: mcpClient }) => {
608
889
  const { limit, offset } = args2;
@@ -614,7 +895,7 @@ List monitors for the authenticated account with optional pagination controls. R
614
895
  query: { limit, offset }
615
896
  }
616
897
  );
617
- return asText(res);
898
+ return structuredText(res);
618
899
  }
619
900
  });
620
901
  server2.addTool({
@@ -631,7 +912,8 @@ List monitors for the authenticated account with optional pagination controls. R
631
912
  description: `
632
913
  Retrieve one monitor by ID, including its configuration and current state. This does not run or modify the monitor.
633
914
  `,
634
- parameters: z4.object({ id: z4.string() }),
915
+ outputSchema: monitorOutputSchema,
916
+ parameters: z5.object({ id: z5.string() }),
635
917
  execute: async (args2, { session, client: mcpClient }) => {
636
918
  const { id } = args2;
637
919
  const res = await monitorRequest(
@@ -639,7 +921,7 @@ Retrieve one monitor by ID, including its configuration and current state. This
639
921
  requestOrigin(mcpClient, session),
640
922
  `/monitor/${encodeURIComponent(id)}`
641
923
  );
642
- return asText(res);
924
+ return structuredText(res);
643
925
  }
644
926
  });
645
927
  server2.addTool({
@@ -658,9 +940,10 @@ Patch an existing monitor by ID. The body can change its name, active/paused sta
658
940
 
659
941
  Returns the updated monitor.
660
942
  `,
661
- parameters: z4.object({
662
- id: z4.string(),
663
- 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())
664
947
  }),
665
948
  execute: async (args2, { session, client: mcpClient }) => {
666
949
  const { id, body } = args2;
@@ -670,7 +953,7 @@ Returns the updated monitor.
670
953
  `/monitor/${encodeURIComponent(id)}`,
671
954
  { method: "PATCH", body }
672
955
  );
673
- return asText(res);
956
+ return structuredText(res);
674
957
  }
675
958
  });
676
959
  server2.addTool({
@@ -687,7 +970,8 @@ Returns the updated monitor.
687
970
  description: `
688
971
  Permanently delete a monitor by ID and stop its future schedule. This operation cannot be undone and returns deletion status.
689
972
  `,
690
- parameters: z4.object({ id: z4.string() }),
973
+ outputSchema: monitorDeleteOutputSchema,
974
+ parameters: z5.object({ id: z5.string() }),
691
975
  execute: async (args2, { session, log, client: mcpClient }) => {
692
976
  const { id } = args2;
693
977
  log.info("Deleting monitor", { id });
@@ -697,7 +981,7 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
697
981
  `/monitor/${encodeURIComponent(id)}`,
698
982
  { method: "DELETE" }
699
983
  );
700
- return asText(res);
984
+ return structuredText(res);
701
985
  }
702
986
  });
703
987
  server2.addTool({
@@ -714,7 +998,8 @@ Permanently delete a monitor by ID and stop its future schedule. This operation
714
998
  description: `
715
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.
716
1000
  `,
717
- parameters: z4.object({ id: z4.string() }),
1001
+ outputSchema: monitorRunOutputSchema,
1002
+ parameters: z5.object({ id: z5.string() }),
718
1003
  execute: async (args2, { session, client: mcpClient }) => {
719
1004
  const { id } = args2;
720
1005
  const res = await monitorRequest(
@@ -723,7 +1008,7 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
723
1008
  `/monitor/${encodeURIComponent(id)}/run`,
724
1009
  { method: "POST" }
725
1010
  );
726
- return asText(res);
1011
+ return structuredText(res);
727
1012
  }
728
1013
  });
729
1014
  server2.addTool({
@@ -740,10 +1025,11 @@ Queue an immediate check for a monitor outside its normal schedule. This starts
740
1025
  description: `
741
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.
742
1027
  `,
743
- parameters: z4.object({
744
- id: z4.string(),
745
- limit: z4.number().int().positive().optional(),
746
- 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(),
747
1033
  status: checkStatusSchema.optional()
748
1034
  }),
749
1035
  execute: async (args2, { session, client: mcpClient }) => {
@@ -754,7 +1040,7 @@ List historical checks for a monitor, optionally filtered by status and bounded
754
1040
  `/monitor/${encodeURIComponent(id)}/checks`,
755
1041
  { query: { limit, offset, status } }
756
1042
  );
757
- return asText(res);
1043
+ return structuredText(res);
758
1044
  }
759
1045
  });
760
1046
  server2.addTool({
@@ -773,11 +1059,12 @@ Retrieve one monitor check and its page-level results, optionally filtered by pa
773
1059
 
774
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.
775
1061
  `,
776
- parameters: z4.object({
777
- id: z4.string(),
778
- checkId: z4.string(),
779
- limit: z4.number().int().positive().optional(),
780
- skip: z4.number().int().nonnegative().optional(),
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(),
781
1068
  pageStatus: pageStatusSchema.optional()
782
1069
  }),
783
1070
  execute: async (args2, { session, client: mcpClient }) => {
@@ -788,13 +1075,13 @@ Markdown tracking returns a unified text diff, JSON tracking returns field paths
788
1075
  `/monitor/${encodeURIComponent(id)}/checks/${encodeURIComponent(checkId)}`,
789
1076
  { query: { limit, skip, status: pageStatus } }
790
1077
  );
791
- return asText(res);
1078
+ return structuredText(res);
792
1079
  }
793
1080
  });
794
1081
  }
795
1082
 
796
1083
  // src/research.ts
797
- import { z as z5 } from "zod";
1084
+ import { z as z6 } from "zod";
798
1085
  import { UserError } from "fastmcp";
799
1086
  var BASE2 = "/v2/search/research";
800
1087
  function appendParam(params, key, value) {
@@ -910,21 +1197,22 @@ Several distinct framings of the same question surface different papers than a s
910
1197
 
911
1198
  Returns ranked papers with canonical IDs, titles, authors, and abstracts.
912
1199
  `,
913
- parameters: z5.object({
914
- query: z5.string().min(1).describe(
1200
+ outputSchema: researchSearchOutputSchema,
1201
+ parameters: z6.object({
1202
+ query: z6.string().min(1).describe(
915
1203
  "Natural-language research topic or question, including methods, systems, conditions, populations, interventions, or outcomes when relevant."
916
1204
  ),
917
- k: z5.number().int().min(1).max(500).optional().describe("Number of ranked papers to return (default 40)."),
918
- authors: z5.array(z5.string()).optional().describe(
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(
919
1207
  "Author substring filter(s); ALL must match (case-insensitive)."
920
1208
  ),
921
- categories: z5.array(z5.string()).optional().describe(
1209
+ categories: z6.array(z6.string()).optional().describe(
922
1210
  "Paper category filter(s) (e.g. `cs.LG`); ALL provided values must match."
923
1211
  ),
924
- from: z5.string().optional().describe(
1212
+ from: z6.string().optional().describe(
925
1213
  "Inclusive lower bound on created/updated date (`YYYY-MM-DD`)."
926
1214
  ),
927
- to: z5.string().optional().describe(
1215
+ to: z6.string().optional().describe(
928
1216
  "Inclusive upper bound on created/updated date (`YYYY-MM-DD`)."
929
1217
  )
930
1218
  }),
@@ -942,7 +1230,8 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
942
1230
  withQuery(`${BASE2}/papers`, params),
943
1231
  originHeaders(requestOrigin(mcpClient, session))
944
1232
  );
945
- return fmtHits(res.data?.results);
1233
+ const results = res.data?.results ?? [];
1234
+ return withStructured(fmtHits(results), { results });
946
1235
  }
947
1236
  });
948
1237
  server2.addTool({
@@ -959,8 +1248,9 @@ Returns ranked papers with canonical IDs, titles, authors, and abstracts.
959
1248
  description: `
960
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.
961
1250
  `,
962
- parameters: z5.object({
963
- paperId: z5.string().min(1).describe(
1251
+ outputSchema: researchPaperOutputSchema,
1252
+ parameters: z6.object({
1253
+ paperId: z6.string().min(1).describe(
964
1254
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
965
1255
  )
966
1256
  }),
@@ -971,7 +1261,11 @@ Retrieve canonical metadata for one paper ID, such as an arXiv, PMC, PMID, or DO
971
1261
  `${BASE2}/papers/${encodeURIComponent(paperId)}`,
972
1262
  originHeaders(requestOrigin(mcpClient, session))
973
1263
  );
974
- return fmtPaperMetadata(res.data?.paper);
1264
+ const paper = res.data?.paper;
1265
+ return withStructured(
1266
+ fmtPaperMetadata(paper),
1267
+ paper ? { paper } : {}
1268
+ );
975
1269
  }
976
1270
  });
977
1271
  server2.addTool({
@@ -990,12 +1284,13 @@ Find citation-graph candidates from one to ten \`seed_ids\`; the first ID is the
990
1284
 
991
1285
  Returns ranked candidates and the evaluated pool size.
992
1286
  `,
993
- parameters: z5.object({
994
- seed_ids: z5.array(z5.string()).min(1).max(10),
995
- intent: z5.string().min(1),
996
- mode: z5.enum(["similar", "citers", "references"]).optional(),
997
- k: z5.number().int().min(1).max(500).optional(),
998
- rerank: z5.boolean().optional().describe("Apply an additional rerank over the fused candidates.")
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.")
999
1294
  }),
1000
1295
  execute: async (args2, { session, client: mcpClient }) => {
1001
1296
  const { seed_ids, intent, mode, k, rerank } = args2;
@@ -1014,10 +1309,15 @@ Returns ranked candidates and the evaluated pool size.
1014
1309
  ),
1015
1310
  originHeaders(requestOrigin(mcpClient, session))
1016
1311
  );
1312
+ const results = res.data?.results ?? [];
1313
+ const poolSize = res.data?.poolSize ?? 0;
1017
1314
  const note = res.data?.note ? `
1018
1315
  note: ${res.data.note}` : "";
1019
- return `${fmtHits(res.data?.results)}
1020
- (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
+ );
1021
1321
  }
1022
1322
  });
1023
1323
  server2.addTool({
@@ -1036,12 +1336,13 @@ Retrieve in-body passages from one paper that are relevant to a specific questio
1036
1336
 
1037
1337
  Returns matching passages or a notice when full text is unavailable.
1038
1338
  `,
1039
- parameters: z5.object({
1040
- paperId: z5.string().min(1).describe(
1339
+ outputSchema: researchReadOutputSchema,
1340
+ parameters: z6.object({
1341
+ paperId: z6.string().min(1).describe(
1041
1342
  "Canonical paperId or primaryId such as `arxiv:1706.03762`, `pmcid:PMC12530322`, `pmid:40953549`, or `doi:10.1016/j.neunet.2025.108095`."
1042
1343
  ),
1043
- question: z5.string().min(1),
1044
- 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).")
1045
1346
  }),
1046
1347
  execute: async (args2, { session, client: mcpClient }) => {
1047
1348
  const { paperId, question, k } = args2;
@@ -1054,7 +1355,10 @@ Returns matching passages or a notice when full text is unavailable.
1054
1355
  originHeaders(requestOrigin(mcpClient, session))
1055
1356
  );
1056
1357
  const passages = res.data?.passages ?? [];
1057
- return passages.length ? passages.map((p) => p.text).join("\n---\n") : "(no full-text passages available for this paper)";
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
+ );
1058
1362
  }
1059
1363
  });
1060
1364
  server2.addTool({
@@ -1068,9 +1372,10 @@ Returns matching passages or a notice when full text is unavailable.
1068
1372
  description: `
1069
1373
  Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub issues, pull requests, and READMEs, plus code documentation, returned as matched passages.
1070
1374
  `,
1071
- parameters: z5.object({
1072
- query: z5.string().min(1),
1073
- 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()
1074
1379
  }),
1075
1380
  canList: () => false,
1076
1381
  beforeValidate: () => {
@@ -1089,10 +1394,7 @@ Deprecated compatibility entry point. Use firecrawl_developer_search for GitHub
1089
1394
  }
1090
1395
 
1091
1396
  // src/usage.ts
1092
- import { z as z6 } from "zod";
1093
- function asText2(data) {
1094
- return JSON.stringify(data, null, 2);
1095
- }
1397
+ import { z as z7 } from "zod";
1096
1398
  function assertSuccessful(status, response, operation) {
1097
1399
  if (status === 200 && response.success) return;
1098
1400
  throw new Error(response.error || `Failed to ${operation}`);
@@ -1113,11 +1415,12 @@ The current view returns \`remainingCredits\`, \`planCredits\`, \`billingPeriodS
1113
1415
 
1114
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.
1115
1417
  `,
1116
- parameters: z6.object({
1117
- view: z6.enum(["current", "historical"]).optional().describe(
1418
+ outputSchema: creditUsageOutputSchema,
1419
+ parameters: z7.object({
1420
+ view: z7.enum(["current", "historical"]).optional().describe(
1118
1421
  "Select current balance or historical monthly usage. Defaults to current."
1119
1422
  ),
1120
- byApiKey: z6.boolean().optional().describe(
1423
+ byApiKey: z7.boolean().optional().describe(
1121
1424
  'Break historical usage down by API key. When view is omitted, true selects the historical view; it cannot be combined with view "current".'
1122
1425
  )
1123
1426
  }),
@@ -1142,7 +1445,7 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e
1142
1445
  response2.data,
1143
1446
  "get historical credit usage"
1144
1447
  );
1145
- return asText2(response2.data);
1448
+ return structuredText(response2.data);
1146
1449
  }
1147
1450
  const response = await client.http.get(
1148
1451
  "/v2/team/credit-usage",
@@ -1150,7 +1453,7 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e
1150
1453
  );
1151
1454
  assertSuccessful(response.status, response.data, "get credit usage");
1152
1455
  const data = response.data.data ?? response.data;
1153
- return asText2({
1456
+ return structuredText({
1154
1457
  remainingCredits: data.remainingCredits ?? data.remaining_credits ?? 0,
1155
1458
  planCredits: data.planCredits ?? data.plan_credits,
1156
1459
  billingPeriodStart: data.billingPeriodStart ?? data.billing_period_start ?? null,
@@ -1248,11 +1551,11 @@ function getOAuthProtectedResourceMetadataUrl(profile) {
1248
1551
  const base = `${resource.origin}/.well-known/oauth-protected-resource`;
1249
1552
  return profile.id === "full" ? base : `${base}${resource.pathname}`;
1250
1553
  }
1251
- function createOAuthChallengeResponse(error, profile, details = {}) {
1554
+ function createOAuthChallengeResponse(error2, profile, details = {}) {
1252
1555
  if (!isMcpOAuthEnabled()) {
1253
1556
  return void 0;
1254
1557
  }
1255
- const errorMessage = error instanceof Error ? error.message : String(error || "Unauthorized");
1558
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2 || "Unauthorized");
1256
1559
  const wwwAuthenticateDescription = errorMessage.replace(/[\r\n]+/g, " ").trim();
1257
1560
  const wwwAuthenticate = [
1258
1561
  ...profile.advertiseOAuth ? [
@@ -1304,8 +1607,8 @@ function createInvalidOAuthRecoveryResponse(recovery) {
1304
1607
  );
1305
1608
  }
1306
1609
  var CREDENTIAL_VALIDATION_RETRY_AFTER_SECONDS = 5;
1307
- function createCredentialValidationUnavailableResponse(error) {
1308
- return new Response(error.message, {
1610
+ function createCredentialValidationUnavailableResponse(error2) {
1611
+ return new Response(error2.message, {
1309
1612
  headers: {
1310
1613
  "Content-Type": "text/plain; charset=utf-8",
1311
1614
  "Retry-After": String(CREDENTIAL_VALIDATION_RETRY_AFTER_SECONDS)
@@ -1601,13 +1904,13 @@ function makeAuthenticate(profile) {
1601
1904
  session
1602
1905
  );
1603
1906
  return session;
1604
- }).catch((error) => {
1907
+ }).catch((error2) => {
1605
1908
  emitSearchCompanionAuthTelemetry(profile, request, "rejected");
1606
1909
  emitLegacyKeyPathTelemetry(profile, request, "rejected");
1607
- if (error instanceof InvalidFirecrawlCredentialError) {
1608
- throw createInvalidCredentialResponse(error);
1910
+ if (error2 instanceof InvalidFirecrawlCredentialError) {
1911
+ throw createInvalidCredentialResponse(error2);
1609
1912
  }
1610
- if (error instanceof InvalidOAuthCredentialError) {
1913
+ if (error2 instanceof InvalidOAuthCredentialError) {
1611
1914
  const recovery = invalidOAuthRecoveryPayload();
1612
1915
  const oauthChallenge2 = createOAuthChallengeResponse(
1613
1916
  new Error(recovery.message),
@@ -1616,15 +1919,15 @@ function makeAuthenticate(profile) {
1616
1919
  );
1617
1920
  throw oauthChallenge2 ?? createInvalidOAuthRecoveryResponse(recovery);
1618
1921
  }
1619
- if (error instanceof CredentialValidationUnavailableError) {
1620
- throw createCredentialValidationUnavailableResponse(error);
1922
+ if (error2 instanceof CredentialValidationUnavailableError) {
1923
+ throw createCredentialValidationUnavailableResponse(error2);
1621
1924
  }
1622
1925
  const shouldChallenge = requestShouldReceiveOAuthChallenge(request, profile);
1623
- const oauthChallenge = shouldChallenge ? createOAuthChallengeResponse(error, profile) : void 0;
1926
+ const oauthChallenge = shouldChallenge ? createOAuthChallengeResponse(error2, profile) : void 0;
1624
1927
  if (oauthChallenge) {
1625
1928
  throw oauthChallenge;
1626
1929
  }
1627
- throw error;
1930
+ throw error2;
1628
1931
  });
1629
1932
  if (request) {
1630
1933
  request[authResultByRequest] = authResult;
@@ -1644,30 +1947,30 @@ function removeEmptyTopLevel(obj) {
1644
1947
  }
1645
1948
  return out;
1646
1949
  }
1647
- var searchDomainSchema = z7.string().trim().toLowerCase().min(1).max(253).regex(
1950
+ var searchDomainSchema = z8.string().trim().toLowerCase().min(1).max(253).regex(
1648
1951
  /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/,
1649
1952
  "Domain must be a valid hostname without protocol or path"
1650
1953
  );
1651
1954
  var searchToolBaseFields = {
1652
- toolDetail: z7.enum(["compact", "summary", "full"]).optional().describe("Compact by default. Compact returns only provider, capability and description; full includes contracts. Inspect selected compact tools with firecrawl_find_tools providers and capabilities."),
1653
- query: z7.string().min(1).describe("Query for web and semantic tool discovery. Operators include quoted phrases, `-term`, `site:host`, `inurl:term`, `intitle:term`, and `related:host`; the set is non-exhaustive. Catalogue browsing is available through firecrawl_find_tools."),
1654
- domainTools: z7.boolean().optional().describe(
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(
1655
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."
1656
1959
  ),
1657
- highlights: z7.boolean().optional().describe(
1960
+ highlights: z8.boolean().optional().describe(
1658
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."
1659
1962
  ),
1660
- limit: z7.number().int().min(1).max(100).optional(),
1661
- tbs: z7.string().optional(),
1662
- filter: z7.string().optional(),
1663
- location: z7.string().optional(),
1664
- includeDomains: z7.array(searchDomainSchema).optional().describe("Hostnames to restrict results to. Mutually exclusive with excludeDomains."),
1665
- excludeDomains: z7.array(searchDomainSchema).optional().describe("Hostnames to leave out of results. Mutually exclusive with includeDomains."),
1666
- sources: z7.array(searchSourceSchema).optional().describe("Search sources; authenticated sessions default to web + alexandria, keyless sessions to web only. " + ALEXANDRIA_SOURCES_OPT_OUT + ' Use ["alexandria"] alone for provider discovery without web results.'),
1667
- categories: z7.array(z7.enum(["research", "pdf", "developer"])).optional().describe(
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(
1668
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`.'
1669
1972
  ),
1670
- enterprise: z7.array(z7.enum(["default", "anon", "zdr"])).optional()
1973
+ enterprise: z8.array(z8.enum(["default", "anon", "zdr"])).optional()
1671
1974
  };
1672
1975
  function searchDomainsAreExclusive(args2) {
1673
1976
  return !(args2.includeDomains?.length && args2.excludeDomains?.length);
@@ -1675,15 +1978,15 @@ function searchDomainsAreExclusive(args2) {
1675
1978
  var SEARCH_DOMAINS_CONFLICT_MESSAGE = "includeDomains and excludeDomains cannot both be specified";
1676
1979
  var EXCHANGE_KEY_REQUIRED_MESSAGE = "Alexandria requires an API key on a team with Alexandria access";
1677
1980
  var EXCHANGE_MAX_CALLS = 10;
1678
- var exchangeCallSchema = z7.object({
1679
- provider: z7.string().min(1).describe('Provider slug, e.g. "fred".'),
1680
- capability: z7.string().min(1).describe(
1981
+ var exchangeCallSchema = z8.object({
1982
+ provider: z8.string().min(1).describe('Provider slug, e.g. "fred".'),
1983
+ capability: z8.string().min(1).describe(
1681
1984
  'Capability address as returned by search or discover, e.g. "series/observations".'
1682
1985
  ),
1683
- version: z7.string().trim().min(1).max(128).optional().describe("Optional published workflow version. Omit to use the latest version."),
1684
- options: z7.record(z7.string(), z7.any()).optional().describe("Capability options as declared by its contract.")
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.")
1685
1988
  });
1686
- var exchangeCallsSchema = z7.array(exchangeCallSchema).min(1).max(EXCHANGE_MAX_CALLS);
1989
+ var exchangeCallsSchema = z8.array(exchangeCallSchema).min(1).max(EXCHANGE_MAX_CALLS);
1687
1990
  function assertExchangeCredential(session) {
1688
1991
  if (hasCredential(session)) return;
1689
1992
  const payload = {
@@ -1749,26 +2052,26 @@ Do not retry until acceptance is confirmed.`,
1749
2052
  }
1750
2053
  );
1751
2054
  }
1752
- function throwIfTermsRequired(error, context) {
1753
- const source = error;
2055
+ function throwIfTermsRequired(error2, context) {
2056
+ const source = error2;
1754
2057
  const action = termsRequiredAction(source?.response?.data ?? source?.details);
1755
2058
  if (action) throw termsRequiredError(action, context);
1756
2059
  }
1757
2060
  async function relayTermsRequired(run, context) {
1758
2061
  try {
1759
2062
  return await run();
1760
- } catch (error) {
1761
- throwIfTermsRequired(error, context);
1762
- throw error;
2063
+ } catch (error2) {
2064
+ throwIfTermsRequired(error2, context);
2065
+ throw error2;
1763
2066
  }
1764
2067
  }
1765
2068
  async function relayExchangeError(run, context) {
1766
2069
  try {
1767
2070
  return await run();
1768
- } catch (error) {
1769
- throwIfTermsRequired(error, context);
1770
- const response = error?.response;
1771
- 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;
1772
2075
  const data = response.data;
1773
2076
  const message = typeof data?.error === "string" ? data.error : `Alexandria request failed (HTTP ${response.status})`;
1774
2077
  const disabledProvider = response.status === 403 ? context.providers?.find((provider) => message === `Access to ${provider} is disabled for this organization.`) : void 0;
@@ -1799,14 +2102,14 @@ async function relayExchangeError(run, context) {
1799
2102
  async function postSearchWithFallback(client, body, implicitTools) {
1800
2103
  try {
1801
2104
  return await client.http.post("/v2/search", body);
1802
- } catch (error) {
1803
- const response = error?.response;
2105
+ } catch (error2) {
2106
+ const response = error2?.response;
1804
2107
  const unavailable = [
1805
2108
  "Provider discovery requires access and does not support zero data retention.",
1806
2109
  "This endpoint is not enabled for this team.",
1807
2110
  "Exchange is not enabled for this team."
1808
2111
  ].includes(response?.data?.error);
1809
- if (!implicitTools || response?.status !== 403 || !unavailable) throw error;
2112
+ if (!implicitTools || response?.status !== 403 || !unavailable) throw error2;
1810
2113
  return client.http.post("/v2/search", {
1811
2114
  ...body,
1812
2115
  sources: ["web"],
@@ -1845,10 +2148,9 @@ var ConsoleLogger = class {
1845
2148
  var openAiAppsChallengeToken = normalizeHeader(
1846
2149
  process.env.OPENAI_APPS_CHALLENGE_TOKEN
1847
2150
  );
1848
- var FULL_PROFILE_INSTRUCTIONS = `Firecrawl provides web search, page retrieval, site URL discovery, multi-page collection, structured page data, monitoring, and multi-source research that returns structured data. Match the requested operation to the tool boundary: firecrawl_scrape retrieves one supplied page and can return JSON matching a supplied schema, firecrawl_map enumerates URLs under a site without retrieving their content, and firecrawl_agent runs multi-source research and returns structured data when the URLs are not known or the answer spans several sites (an entity plus its fields, a list, a dataset); its result is read with firecrawl_agent_status. Authenticated firecrawl_search returns web results together with matching Alexandria providers in data.tools. ${ALEXANDRIA_CATALOGUE_SENTENCE} When a provider matches, prefer it over scraping pages if the task needs the same fields across several entities, provenance, exact figures or timestamps, or a large set of records; execute it through firecrawl_scrape with the returned contract. If web results already answer the question, use them. Before scraping more than one page for the same fields, spend one free firecrawl_find_tools call to check for a provider. Use firecrawl_find_tools to read a contract that was not returned in full or to browse the catalogue by category. ${ALEXANDRIA_SOURCES_OPT_OUT} If no provider fits, continue with web search or firecrawl_agent. For biomedical, life-science, clinical, or arXiv literature, the firecrawl_research_* tools search a paper index of abstracts and full text; firecrawl_search with categories: ["research"] is a website filter over ordinary web results and reaches different sources. For a programming question \u2014 code behaviour, a library or framework, an API contract, an error message, or a known bug \u2014 firecrawl_developer_search (or firecrawl_search with categories: ["developer"]) searches an index of public repositories, GitHub issues, merged pull requests, READMEs, and code documentation. Execution with firecrawl_scrape alexandria uses requestId; reuse the returned ID for retries of the same payload, never a new ID to bypass a pending or uncertain 409. firecrawl_search with sources: [{type: "alexandria"}] returns compact tool summaries in data.tools; toolDetail: "full" includes contracts, firecrawl_find_tools starts with categories, lists providers, then compact tools, and expands the selected full contract, and firecrawl_scrape with alexandria: [{provider, capability, options}] executes up to ten capabilities and returns their results. Alexandria access needs an API key on a team with it enabled. A terms-gated Alexandria provider fails with code THIRD_PARTY_DATA_TERMS_REQUIRED and a requiresAction.url. Follow the returned terms/show and terms/accept calls through firecrawl_scrape; acceptance requires explicit user authorization for the reviewed version and digest and confirmed:true. Otherwise direct an organization admin to the dashboard URL. Do not repeat successful provider calls just because the client could not display their output. Provide only the required inputs and account for stated network or external side effects.
1849
- ${ALEXANDRIA_FEEDBACK_GUIDANCE}`;
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.`;
1850
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.`;
1851
- var SEARCH_PROFILE_INSTRUCTIONS = ALEXANDRIA_SEARCH_INSTRUCTIONS + ` Firecrawl provides web, developer, and research search, and executes catalogued Alexandria data providers. Use firecrawl_search to find relevant results across the web and specialized indexes; authenticated searches also return matching Alexandria providers in data.tools. Use firecrawl_find_tools to browse the Alexandria catalogue or read a provider's full contract, and firecrawl_scrape with an alexandria body to execute a capability discovered that way; firecrawl_scrape with a url retrieves one supplied page. For a programming question, firecrawl_developer_search searches indexed public repositories, GitHub issues, merged pull requests, READMEs, and code documentation and returns the matched passages, and skills: "only" narrows it to agent-skill files; firecrawl_search with categories: ["developer"] reaches the same index beside ordinary web results, returning the hits in the web group rather than as passages and offering no skills filter. For a biomedical, life-science, clinical, or arXiv literature question, the firecrawl_research_* tools search the paper index, while categories: ["research"] on firecrawl_search filters ordinary web results to research-affiliated websites. Use the firecrawl_research_* tools to search academic and research literature, expand from anchor papers via the citation graph, and read full-text passages from a specific paper. Search and discovery tools are read-only and return ranked results. Billing: web, developer and research search are billed per request; Alexandria discovery (firecrawl_search with sources ["alexandria"] alone, and firecrawl_find_tools) is free; firecrawl_scrape is billed, as a page retrieval in url mode or at each executed capability's listed price in alexandria mode.`;
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.`;
1852
2154
  var SEARCH_PROFILE_TOOLS = /* @__PURE__ */ new Set([
1853
2155
  "firecrawl_search",
1854
2156
  "firecrawl_developer_search",
@@ -1992,17 +2294,17 @@ function invalidOAuthRecoveryPayload() {
1992
2294
  message: INVALID_OAUTH_MESSAGE
1993
2295
  });
1994
2296
  }
1995
- function isCoreCredentialRejection(error) {
1996
- if (!(error instanceof Error)) return false;
1997
- const candidate = error;
2297
+ function isCoreCredentialRejection(error2) {
2298
+ if (!(error2 instanceof Error)) return false;
2299
+ const candidate = error2;
1998
2300
  return candidate.status === 401 || candidate.response?.status === 401;
1999
2301
  }
2000
2302
  async function runWithCredentialRecovery(run, requestId, session) {
2001
2303
  try {
2002
2304
  return await run();
2003
- } catch (error) {
2004
- if (session?.authType !== "api-key" || !isCoreCredentialRejection(error)) {
2005
- throw error;
2305
+ } catch (error2) {
2306
+ if (session?.authType !== "api-key" || !isCoreCredentialRejection(error2)) {
2307
+ throw error2;
2006
2308
  }
2007
2309
  const payload = recoveryPayload("CREDENTIAL_INVALID", requestId);
2008
2310
  throw new UserError2(String(payload.message), payload);
@@ -2055,7 +2357,7 @@ function deprecatedExtractPayload() {
2055
2357
  docs_url: "https://docs.firecrawl.dev/developer-guides/usage-guides/choosing-the-data-extractor"
2056
2358
  };
2057
2359
  }
2058
- function emitActionLog(toolName, status, session, error, requestId = randomUUID(), code) {
2360
+ function emitActionLog(toolName, status, session, error2, requestId = randomUUID(), code) {
2059
2361
  if (process.env.CLOUD_SERVICE !== "true") return;
2060
2362
  const payload = {
2061
2363
  team_id: session?.teamId,
@@ -2067,7 +2369,7 @@ function emitActionLog(toolName, status, session, error, requestId = randomUUID(
2067
2369
  status,
2068
2370
  request_id: requestId,
2069
2371
  resource: primaryProfile.resourceUrl,
2070
- ...error ? { error_class: error instanceof Error ? error.name : typeof error } : {},
2372
+ ...error2 ? { error_class: error2 instanceof Error ? error2.name : typeof error2 } : {},
2071
2373
  ...code ? { code } : {}
2072
2374
  };
2073
2375
  console.error("[MCP_ACTION]", JSON.stringify(payload));
@@ -2167,9 +2469,9 @@ function guardHostedTool(tool, { logActions }) {
2167
2469
  const result = await runTool();
2168
2470
  emitActionLog(tool.name, "success", invocationSession, void 0, requestId);
2169
2471
  return result;
2170
- } catch (error) {
2171
- emitActionLog(tool.name, "error", invocationSession, error, requestId);
2172
- throw error;
2472
+ } catch (error2) {
2473
+ emitActionLog(tool.name, "error", invocationSession, error2, requestId);
2474
+ throw error2;
2173
2475
  }
2174
2476
  }
2175
2477
  };
@@ -2261,12 +2563,9 @@ function getClient(session) {
2261
2563
  });
2262
2564
  return client;
2263
2565
  }
2264
- function asText3(data) {
2566
+ function asText(data) {
2265
2567
  return JSON.stringify(data, null, 2);
2266
2568
  }
2267
- function compactText(data) {
2268
- return JSON.stringify(data);
2269
- }
2270
2569
  var safeActionTypes = ["wait", "screenshot", "scroll", "scrape"];
2271
2570
  var otherActions = [
2272
2571
  "click",
@@ -2332,10 +2631,10 @@ function transformScrapeParams(args2) {
2332
2631
  delete out.pdfOptions;
2333
2632
  return out;
2334
2633
  }
2335
- var scrapeParamsSchema = z7.object({
2336
- url: z7.string().url(),
2337
- formats: z7.array(
2338
- z7.enum([
2634
+ var scrapeParamsSchema = z8.object({
2635
+ url: z8.string().url(),
2636
+ formats: z8.array(
2637
+ z8.enum([
2339
2638
  "markdown",
2340
2639
  "html",
2341
2640
  "rawHtml",
@@ -2349,87 +2648,93 @@ var scrapeParamsSchema = z7.object({
2349
2648
  "audio"
2350
2649
  ])
2351
2650
  ).optional(),
2352
- jsonOptions: z7.object({
2353
- prompt: z7.string().optional(),
2354
- 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()
2355
2654
  }).optional(),
2356
- queryOptions: z7.object({
2357
- prompt: z7.string().max(1e4),
2358
- 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")
2359
2658
  }).optional(),
2360
- screenshotOptions: z7.object({
2361
- fullPage: z7.boolean().optional(),
2362
- quality: z7.number().optional(),
2363
- viewport: z7.object({ width: z7.number(), height: z7.number() }).optional()
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()
2364
2663
  }).optional(),
2365
- parsers: z7.array(z7.enum(["pdf"])).optional(),
2366
- pdfOptions: z7.object({
2367
- 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()
2368
2667
  }).optional(),
2369
- onlyMainContent: z7.boolean().optional(),
2370
- redactPII: z7.boolean().optional(),
2371
- includeTags: z7.array(z7.string()).optional(),
2372
- excludeTags: z7.array(z7.string()).optional(),
2373
- waitFor: z7.number().optional(),
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(),
2374
2673
  ...SAFE_MODE ? {} : {
2375
- actions: z7.array(
2376
- z7.object({
2377
- type: z7.enum(allowedActionTypes),
2378
- selector: z7.string().optional(),
2379
- milliseconds: z7.number().optional(),
2380
- text: z7.string().optional(),
2381
- key: z7.string().optional(),
2382
- direction: z7.enum(["up", "down"]).optional(),
2383
- script: z7.string().optional(),
2384
- fullPage: z7.boolean().optional()
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()
2385
2684
  })
2386
2685
  ).optional()
2387
2686
  },
2388
- mobile: z7.boolean().optional(),
2389
- skipTlsVerification: z7.boolean().optional(),
2390
- removeBase64Images: z7.boolean().optional(),
2391
- location: z7.object({
2392
- country: z7.string().optional(),
2393
- languages: z7.array(z7.string()).optional()
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()
2394
2693
  }).optional(),
2395
- storeInCache: z7.boolean().optional(),
2396
- zeroDataRetention: z7.boolean().optional(),
2397
- maxAge: z7.number().optional(),
2398
- lockdown: z7.boolean().optional(),
2399
- proxy: z7.enum(["basic", "stealth", "enhanced", "auto"]).optional(),
2400
- profile: z7.object({
2401
- name: z7.string(),
2402
- saveChanges: z7.boolean().optional()
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()
2403
2702
  }).optional()
2404
2703
  });
2704
+ var ALEXANDRIA_IGNORED_SCRAPE_OPTIONS = /* @__PURE__ */ new Set(["toolDetail", "domainTools"]);
2405
2705
  var scrapeToolParamsSchema = scrapeParamsSchema.extend({
2406
- url: z7.string().url().optional(),
2407
- timeout: z7.number().int().positive().optional().describe("Execution timeout in milliseconds."),
2408
- requestId: z7.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe(
2409
- "Alexandria execution ID. Reuse the returned ID for retries of the identical payload, never a new ID to bypass pending or uncertain execution; generated when omitted. For potentially large workflow results, supply and preserve one before execution. Errors relay a code and chargeId: request_in_flight (409) retry the same requestId later; request_unresolved (503) keep the requestId for reconciliation, never mint a new one; duplicate_request (409) the requestId belongs to a different payload; unknown_provider (404), insufficient_credits (402) and billing_unavailable (503) mean nothing executed."
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."
2410
2710
  ),
2411
- alexandria: z7.union([exchangeCallSchema, exchangeCallsSchema]).optional().describe(
2412
- "Execute catalogued Alexandria capabilities instead of scraping a URL. Exactly one of url or alexandria. One {provider, capability, options} object or an array of 1-10, found through firecrawl_search or firecrawl_find_tools. Each call may include version to pin a published workflow; omitting it uses latest. Only timeout also applies at the top level. " + ALEXANDRIA_CONTRACT_GUIDANCE + " Returns per-capability results in data.alexandria with data, records, or an error with a code; check each item even when the outer response succeeds. If a response provides nextTool, follow it to read a large result instead of repeating a successful provider call. Needs an API key on a team with Alexandria enabled. A terms-gated provider returns THIRD_PARTY_DATA_TERMS_REQUIRED (403) with requiresAction.url: follow the returned terms/show and terms/accept calls through this tool, accepting only after explicit user authorization for the reviewed version and digest; an organization admin can instead accept at the dashboard URL. Retry only after confirmed acceptance."
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."
2413
2713
  ),
2414
- toolDetail: z7.enum(["compact", "summary", "full"]).optional().describe("URL domain discovery detail: summary by default, compact returns provider/capability/description, full includes contracts."),
2415
- domainTools: z7.boolean().optional().describe(
2416
- "URL mode only: include domain-matched Alexandria tools for the page in tools on the returned document."
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."
2417
2717
  )
2418
2718
  }).refine(
2419
2719
  (args2) => Boolean(args2.url) !== Boolean(args2.alexandria),
2420
2720
  "Provide exactly one of url or alexandria"
2421
2721
  ).refine(
2422
2722
  (args2) => !args2.alexandria || Object.entries(args2).every(
2423
- ([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
2424
2729
  ),
2425
2730
  "alexandria cannot be combined with url or other scrape options"
2426
2731
  ).refine(
2427
2732
  (args2) => !args2.requestId || !!args2.alexandria,
2428
2733
  "requestId applies to Alexandria execution only"
2429
2734
  );
2430
- var parseOptionParamsSchema = z7.object({
2431
- formats: z7.array(
2432
- z7.enum([
2735
+ var parseOptionParamsSchema = z8.object({
2736
+ formats: z8.array(
2737
+ z8.enum([
2433
2738
  "markdown",
2434
2739
  "html",
2435
2740
  "rawHtml",
@@ -2439,48 +2744,48 @@ var parseOptionParamsSchema = z7.object({
2439
2744
  "query"
2440
2745
  ])
2441
2746
  ).optional(),
2442
- jsonOptions: z7.object({
2443
- prompt: z7.string().optional(),
2444
- 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()
2445
2750
  }).optional(),
2446
- queryOptions: z7.object({
2447
- prompt: z7.string().max(1e4),
2448
- 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")
2449
2754
  }).optional(),
2450
- parsers: z7.array(z7.enum(["pdf"])).optional(),
2451
- pdfOptions: z7.object({
2452
- 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()
2453
2758
  }).optional(),
2454
- onlyMainContent: z7.boolean().optional(),
2455
- redactPII: z7.boolean().optional(),
2456
- includeTags: z7.array(z7.string()).optional(),
2457
- excludeTags: z7.array(z7.string()).optional(),
2458
- removeBase64Images: z7.boolean().optional(),
2459
- skipTlsVerification: z7.boolean().optional(),
2460
- storeInCache: z7.boolean().optional(),
2461
- zeroDataRetention: z7.boolean().optional(),
2462
- maxAge: z7.number().optional().describe("Ignored: parse never reuses or stores indexed content."),
2463
- proxy: z7.enum(["basic", "auto"]).optional()
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()
2464
2769
  });
2465
2770
  var localParseParamsSchema = parseOptionParamsSchema.extend({
2466
- filePath: z7.string().min(1).describe(
2771
+ filePath: z8.string().min(1).describe(
2467
2772
  "Absolute or relative path to a local file to parse. Supported: .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls"
2468
2773
  ),
2469
- contentType: z7.string().optional().describe(
2774
+ contentType: z8.string().optional().describe(
2470
2775
  "Optional MIME type override. If omitted, the server infers the file kind from the extension."
2471
2776
  )
2472
2777
  });
2473
2778
  var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2474
- filePath: z7.string().min(1).optional().describe(
2779
+ filePath: z8.string().min(1).optional().describe(
2475
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."
2476
2781
  ),
2477
- uploadRef: z7.string().min(1).optional().describe(
2782
+ uploadRef: z8.string().min(1).optional().describe(
2478
2783
  "Phase 2 only: short-lived upload reference returned by phase 1 after the local PUT upload completes."
2479
2784
  ),
2480
- contentType: z7.string().optional().describe(
2785
+ contentType: z8.string().optional().describe(
2481
2786
  "Phase 1 MIME type override. If omitted, the server infers it from the file extension without reading the file."
2482
2787
  ),
2483
- declaredSizeBytes: z7.number().int().positive().optional().describe(
2788
+ declaredSizeBytes: z8.number().int().positive().optional().describe(
2484
2789
  "Optional phase 1 size declaration. Hosted MCP does not stat the file; provide this only if the caller already knows it."
2485
2790
  )
2486
2791
  }).superRefine((value, ctx) => {
@@ -2488,7 +2793,7 @@ var hostedParseParamsSchema = parseOptionParamsSchema.extend({
2488
2793
  const hasUploadRef = typeof value.uploadRef === "string" && value.uploadRef.length > 0;
2489
2794
  if (hasFilePath === hasUploadRef) {
2490
2795
  ctx.addIssue({
2491
- code: z7.ZodIssueCode.custom,
2796
+ code: z8.ZodIssueCode.custom,
2492
2797
  message: "Hosted firecrawl_parse requires exactly one of filePath (phase 1) or uploadRef (phase 2).",
2493
2798
  path: hasFilePath && hasUploadRef ? ["uploadRef"] : ["filePath"]
2494
2799
  });
@@ -2609,7 +2914,7 @@ async function executeHostedParse(args2, session, log, origin) {
2609
2914
  );
2610
2915
  }
2611
2916
  if (!hasCredential(session) && !isKeylessMode(session)) {
2612
- return asText3({
2917
+ return asText({
2613
2918
  success: false,
2614
2919
  mode: "hosted-upload-ref-auth-required",
2615
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."
@@ -2647,7 +2952,7 @@ async function executeHostedParse(args2, session, log, origin) {
2647
2952
  }
2648
2953
  const uploadHeaders = upload.headers && Object.keys(upload.headers).length > 0 ? upload.headers : (upload.method ?? "PUT").toUpperCase() === "POST" ? {} : { "Content-Type": contentType };
2649
2954
  const uploadForCommand = { ...upload, headers: uploadHeaders };
2650
- return asText3({
2955
+ return asText({
2651
2956
  success: true,
2652
2957
  mode: "hosted-upload-ref-awaiting-upload",
2653
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.",
@@ -2683,7 +2988,7 @@ async function executeHostedParse(args2, session, log, origin) {
2683
2988
  session,
2684
2989
  origin
2685
2990
  );
2686
- return asText3(parseJson);
2991
+ return asText(parseJson);
2687
2992
  }
2688
2993
  var scrapeTool = {
2689
2994
  name: "firecrawl_scrape",
@@ -2697,14 +3002,15 @@ var scrapeTool = {
2697
3002
  // Does not modify, delete, or write to external websites.
2698
3003
  },
2699
3004
  description: `
2700
- Scrape one URL and return its content: markdown by default, or HTML, links, screenshots, branding data, a targeted answer, or JSON matching a supplied schema. Use it when the request identifies a page and needs its content or defined fields. Use \`firecrawl_search\` when additional web sources are needed, \`firecrawl_map\` to list a site's URLs, and \`firecrawl_crawl\` for a set of pages.
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.
2701
3006
 
2702
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.
2703
3008
 
2704
- On an authenticated session with Alexandria access, if you are about to scrape the same fields from several pages, first run \`firecrawl_search\` with \`sources\` unset (or \`firecrawl_find_tools\`): a matching Alexandria provider returns those fields as typed records in one call. Keyless sessions have no provider matches; scrape directly.
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.
2705
3010
 
2706
- Alexandria mode: pass \`alexandria\` instead of \`url\` to execute catalogued capabilities; the \`alexandria\` and \`requestId\` parameters describe batching, retries, errors and provider terms. Alexandria results include a \`feedbackTool\` pointer: after the task, report how the catalogue served the website through \`firecrawl_feedback\` with endpoint \`alexandria\` (free, no job ID).
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.
2707
3012
  `,
3013
+ outputSchema: scrapeOutputSchema,
2708
3014
  parameters: scrapeToolParamsSchema,
2709
3015
  execute: async (args2, { session, log, client: mcpClient }) => {
2710
3016
  const origin = requestOrigin(mcpClient, session);
@@ -2719,7 +3025,9 @@ Alexandria mode: pass \`alexandria\` instead of \`url\` to execute catalogued ca
2719
3025
  log.info("Executing Alexandria capabilities", {
2720
3026
  count: Array.isArray(alexandria) ? alexandria.length : 1
2721
3027
  });
2722
- return executeExchangeCalls(session, alexandria, suppliedRequestId, options.timeout, origin);
3028
+ return structuredJsonText(
3029
+ await executeExchangeCalls(session, alexandria, suppliedRequestId, options.timeout, origin)
3030
+ );
2723
3031
  }
2724
3032
  const transformed = transformScrapeParams(
2725
3033
  options
@@ -2740,7 +3048,7 @@ Alexandria mode: pass \`alexandria\` instead of \`url\` to execute catalogued ca
2740
3048
  },
2741
3049
  session
2742
3050
  );
2743
- return asText3(json?.data ?? json);
3051
+ return structuredText(json?.data ?? json);
2744
3052
  }
2745
3053
  const client = getClient(session);
2746
3054
  const res = await relayTermsRequired(
@@ -2750,10 +3058,13 @@ Alexandria mode: pass \`alexandria\` instead of \`url\` to execute catalogued ca
2750
3058
  }),
2751
3059
  { tool: "firecrawl_scrape" }
2752
3060
  );
2753
- return asText3(res);
3061
+ return structuredText(res);
2754
3062
  }
2755
3063
  };
2756
- server.addTool(scrapeTool);
3064
+ server.addTool({
3065
+ ...scrapeTool,
3066
+ _meta: { "anthropic/alwaysLoad": true }
3067
+ });
2757
3068
  server.addTool({
2758
3069
  name: "firecrawl_map",
2759
3070
  annotations: {
@@ -2770,13 +3081,14 @@ Enumerate URLs indexed under one website through Firecrawl without fetching each
2770
3081
 
2771
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.
2772
3083
  `,
2773
- parameters: z7.object({
2774
- url: z7.string().url(),
2775
- search: z7.string().optional(),
2776
- sitemap: z7.enum(["include", "skip", "only"]).optional(),
2777
- includeSubdomains: z7.boolean().optional(),
2778
- limit: z7.number().optional(),
2779
- ignoreQueryParameters: z7.boolean().optional()
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()
2780
3092
  }),
2781
3093
  execute: async (args2, { session, log, client: mcpClient }) => {
2782
3094
  const { url, ...options } = args2;
@@ -2787,11 +3099,12 @@ Returns matching URLs rather than page bodies. Retrieve one page with \`firecraw
2787
3099
  ...cleaned,
2788
3100
  origin: requestOrigin(mcpClient, session)
2789
3101
  });
2790
- return asText3(res);
3102
+ return structuredText(res);
2791
3103
  }
2792
3104
  });
2793
3105
  server.addTool({
2794
3106
  name: "firecrawl_search",
3107
+ _meta: { "anthropic/alwaysLoad": true },
2795
3108
  annotations: {
2796
3109
  title: "Firecrawl web search",
2797
3110
  readOnlyHint: true,
@@ -2806,11 +3119,12 @@ Search web, news, or image sources and return ranked results with query-relevant
2806
3119
 
2807
3120
  ${ALEXANDRIA_SEARCH_LEAD}
2808
3121
 
2809
- Tool matches are discovery, not executed data: execute one through \`firecrawl_scrape\` with an \`alexandria\` body, or read its full contract with \`firecrawl_find_tools\`. After an Alexandria task, call firecrawl_feedback once per website with endpoint "alexandria" (free, no job ID).
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.
2810
3123
 
2811
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.
2812
3125
  `,
2813
- parameters: z7.object({
3126
+ outputSchema: searchOutputSchema,
3127
+ parameters: z8.object({
2814
3128
  ...searchToolBaseFields,
2815
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.")
2816
3130
  }).refine(searchDomainsAreExclusive, SEARCH_DOMAINS_CONFLICT_MESSAGE).refine(
@@ -2847,7 +3161,7 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
2847
3161
  const json = await keylessPost("/v2/search", searchBody, session);
2848
3162
  const keylessResponse = { ...json ?? {} };
2849
3163
  delete keylessResponse.id;
2850
- return compactText(keylessResponse);
3164
+ return structuredCompact(keylessResponse);
2851
3165
  }
2852
3166
  const client = getClient(session);
2853
3167
  const postSearch = () => postSearchWithFallback(
@@ -2857,7 +3171,7 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
2857
3171
  );
2858
3172
  const context = { tool: "firecrawl_search" };
2859
3173
  const httpRes = exchangeSource ? await relayExchangeError(postSearch, context) : await relayTermsRequired(postSearch, context);
2860
- return compactText(httpRes?.data ?? {});
3174
+ return structuredCompact(httpRes?.data ?? {});
2861
3175
  }
2862
3176
  });
2863
3177
  async function executeExchangeCalls(session, alexandria, suppliedRequestId, timeout, origin = requestOrigin(void 0, session)) {
@@ -2895,11 +3209,11 @@ async function executeExchangeCalls(session, alexandria, suppliedRequestId, time
2895
3209
  randomUUID(),
2896
3210
  alexandriaFeedbackAvailable(session) && alexandriaCallsWarrantFeedback(calls) ? { feedbackTool: ALEXANDRIA_FEEDBACK_HINT } : {}
2897
3211
  );
2898
- } catch (error) {
2899
- if (error?.response?.status === 401) throw error;
3212
+ } catch (error2) {
3213
+ if (error2?.response?.status === 401) throw error2;
2900
3214
  throw new UserError2(
2901
- `${error instanceof Error ? error.message : String(error)} Request ID: ${requestId}. Reuse this ID only with the identical payload.`,
2902
- { ...error instanceof UserError2 ? error.extras : {}, requestId }
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 }
2903
3217
  );
2904
3218
  }
2905
3219
  }
@@ -2912,15 +3226,16 @@ var findToolsTool = {
2912
3226
  destructiveHint: false,
2913
3227
  idempotentHint: true
2914
3228
  },
2915
- description: "Browse Alexandria data providers and workflows or read a selected contract. Alexandria covers " + ALEXANDRIA_CATALOGUE_VERTICALS + ": typed, sourced records through published contracts. Prefer normal firecrawl_search for a data task; it already returns matching providers. Use this tool when the contract you need was not returned in full, to browse a category when search found nothing, or before scraping the same fields from several pages. Discovery is free. Use query for semantic discovery or urls to find providers for a website. With no arguments, browse categories, then providers and tools. Inspect a selected contract before executing through firecrawl_scrape; reuse contracts already returned. Follow nextTool for further discovery or pagination. Discovery does not execute providers. Use firecrawl_search when you also need web results. Results include a feedbackTool pointer: after the task, report how the catalogue served the website through firecrawl_feedback with endpoint alexandria, including when nothing covered it (free, no job ID).",
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,
2916
3231
  parameters: findToolsSchema,
2917
3232
  execute: async (args2, { session, client: mcpClient }) => {
2918
3233
  const origin = requestOrigin(mcpClient, session);
2919
3234
  let options;
2920
3235
  try {
2921
3236
  options = findToolsOptions(args2);
2922
- } catch (error) {
2923
- throw new UserError2(error instanceof Error ? error.message : String(error));
3237
+ } catch (error2) {
3238
+ throw new UserError2(error2 instanceof Error ? error2.message : String(error2));
2924
3239
  }
2925
3240
  if (options.level === "categories") {
2926
3241
  assertExchangeCredential(session);
@@ -2940,25 +3255,26 @@ var findToolsTool = {
2940
3255
  }));
2941
3256
  const page = { level: "categories", items, total: rows.length };
2942
3257
  if (offset + options.limit < rows.length) page.nextTool = { name: "firecrawl_find_tools", arguments: { level: "categories", limit: options.limit, offset: offset + options.limit } };
2943
- return compactText(withAlexandriaFeedbackHint(withFindToolsNavigation({ success: true, data: { creditsCost: 0, alexandria: [{ provider: "firecrawl", capability: "find-tools", creditsCost: 0, data: page }] } }), alexandriaFeedbackAvailable(session)));
3258
+ return structuredCompact(withAlexandriaFeedbackHint(withFindToolsNavigation({ success: true, data: { creditsCost: 0, alexandria: [{ provider: "firecrawl", capability: "find-tools", creditsCost: 0, data: page }] } }), alexandriaFeedbackAvailable(session)));
2944
3259
  }
2945
3260
  const result = await executeExchangeCalls(session, { provider: "firecrawl", capability: "find-tools", options }, void 0, void 0, origin);
2946
- return compactText(withFindToolsNavigation(JSON.parse(result)));
3261
+ return structuredCompact(withFindToolsNavigation(JSON.parse(result)));
2947
3262
  }
2948
3263
  };
2949
3264
  server.addTool(findToolsTool);
2950
3265
  var SEARCH_SURFACE_SCRAPE_DESCRIPTION = `
2951
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.
2952
3267
 
2953
- Before scraping the same fields from several pages, first run \`firecrawl_search\` with \`sources\` unset (or \`firecrawl_find_tools\`): a matching Alexandria provider returns those fields as typed records in one call.
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.
2954
3269
 
2955
- Alexandria mode: pass \`alexandria\` instead of \`url\`; the \`alexandria\` and \`requestId\` parameters describe batching, retries, errors and provider terms. Alexandria execution is billed at the capability's listed price and needs a team with Alexandria enabled.
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.
2956
3271
  `;
2957
3272
  var searchSurfaceScrapeTool = {
2958
3273
  ...scrapeTool,
3274
+ _meta: { "anthropic/alwaysLoad": true },
2959
3275
  description: SEARCH_SURFACE_SCRAPE_DESCRIPTION
2960
3276
  };
2961
- var SEARCH_SURFACE_FIND_TOOLS_DESCRIPTION = "Browse Alexandria data providers and workflows or read a selected contract. Alexandria covers " + ALEXANDRIA_CATALOGUE_VERTICALS + ": typed, sourced records through published contracts. Prefer normal firecrawl_search for a data task; it already returns matching providers. Use this tool when the contract you need was not returned in full, to browse a category when search found nothing, or before scraping the same fields from several pages. Discovery is free. Use query for semantic discovery or urls to find providers for a website. With no arguments, browse categories, then providers and tools. Inspect a selected contract before executing through firecrawl_scrape; reuse contracts already returned. Follow nextTool for further discovery or pagination. Discovery does not execute providers. Use firecrawl_search when you also need web results.";
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.";
2962
3278
  var searchSurfaceFindToolsTool = {
2963
3279
  ...findToolsTool,
2964
3280
  description: SEARCH_SURFACE_FIND_TOOLS_DESCRIPTION
@@ -3109,17 +3425,17 @@ async function waitForCrawlCompletionWithOrigin(client, jobId, origin, pollInter
3109
3425
  );
3110
3426
  }
3111
3427
  }
3112
- var feedbackIssueSchema = z7.string().trim().min(1).max(80).regex(
3428
+ var feedbackIssueSchema = z8.string().trim().min(1).max(80).regex(
3113
3429
  /^[a-z0-9][a-z0-9_-]*$/,
3114
3430
  "Issue codes must use lowercase letters, numbers, underscores, or hyphens"
3115
3431
  );
3116
- var valuableSourceSchema = z7.object({
3117
- url: z7.string().url(),
3118
- reason: z7.string().max(1e3).optional()
3432
+ var valuableSourceSchema = z8.object({
3433
+ url: z8.string().url(),
3434
+ reason: z8.string().max(1e3).optional()
3119
3435
  });
3120
- var missingContentSchema = z7.object({
3121
- topic: z7.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3122
- description: z7.string().max(2e3).optional()
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()
3123
3439
  });
3124
3440
  var FEEDBACK_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
3125
3441
  function feedbackEnvEnabled(...keys) {
@@ -3157,24 +3473,25 @@ Records schema-validated quality feedback for a prior \`firecrawl_search\` UUID
3157
3473
 
3158
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.
3159
3475
  `,
3160
- parameters: z7.object({
3161
- searchId: z7.string().uuid("searchId must be the UUID returned by firecrawl_search"),
3162
- rating: z7.enum(["good", "bad", "partial"]),
3163
- valuableSources: z7.array(
3164
- z7.object({
3165
- url: z7.string().url(),
3166
- reason: z7.string().max(1e3).optional()
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()
3167
3484
  })
3168
3485
  ).max(50).optional(),
3169
- missingContent: z7.array(
3170
- z7.object({
3171
- topic: z7.string().min(1, "topic must not be empty").max(200, "topic must be 200 characters or fewer"),
3172
- description: z7.string().max(2e3).optional()
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()
3173
3490
  })
3174
3491
  ).max(20).optional().describe(
3175
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`."
3176
3493
  ),
3177
- querySuggestions: z7.string().max(2e3).optional()
3494
+ querySuggestions: z8.string().max(2e3).optional()
3178
3495
  }),
3179
3496
  execute: async (args2, { session, log, client: mcpClient }) => {
3180
3497
  const origin = requestOrigin(mcpClient, session);
@@ -3234,7 +3551,7 @@ Eligibility is limited to successful searches within the feedback age window. Th
3234
3551
  status: response.status,
3235
3552
  feedbackErrorCode: parsed?.feedbackErrorCode
3236
3553
  });
3237
- return asText3({
3554
+ return structuredText({
3238
3555
  success: false,
3239
3556
  status: response.status,
3240
3557
  feedbackErrorCode: parsed?.feedbackErrorCode,
@@ -3242,7 +3559,7 @@ Eligibility is limited to successful searches within the feedback age window. Th
3242
3559
  retryable: response.status >= 500
3243
3560
  });
3244
3561
  }
3245
- return asText3(parsed);
3562
+ return structuredText(parsed);
3246
3563
  }
3247
3564
  });
3248
3565
  }
@@ -3273,20 +3590,21 @@ For an Alexandria session, set endpoint to \`alexandria\`, omit jobId, and provi
3273
3590
 
3274
3591
  Returns submission status, feedback ID, and accounting fields.
3275
3592
  `,
3276
- parameters: z7.object({
3277
- endpoint: z7.enum(["search", "scrape", "parse", "map", "alexandria"]),
3278
- jobId: z7.string().uuid("jobId must be the UUID returned by Firecrawl").optional(),
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(),
3279
3597
  ...alexandriaFeedbackFields,
3280
- rating: z7.enum(["good", "bad", "partial"]),
3281
- issues: z7.array(feedbackIssueSchema).max(20).optional(),
3282
- tags: z7.array(feedbackIssueSchema).max(20).optional(),
3283
- note: z7.string().max(4e3).optional(),
3284
- valuableSources: z7.array(valuableSourceSchema).max(50).optional(),
3285
- missingContent: z7.array(missingContentSchema).max(50).optional(),
3286
- querySuggestions: z7.string().max(2e3).optional(),
3287
- url: z7.string().url().optional(),
3288
- pageNumbers: z7.array(z7.number().int().positive()).max(100).optional(),
3289
- metadata: z7.record(z7.string(), z7.unknown()).optional()
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()
3290
3608
  }).superRefine((value, ctx) => {
3291
3609
  if (value.endpoint === "alexandria") {
3292
3610
  const parsed = alexandriaSessionFeedbackSchema.safeParse(value);
@@ -3368,7 +3686,7 @@ Returns submission status, feedback ID, and accounting fields.
3368
3686
  status: response.status,
3369
3687
  feedbackErrorCode: parsed?.feedbackErrorCode
3370
3688
  });
3371
- return asText3({
3689
+ return structuredText({
3372
3690
  success: false,
3373
3691
  status: response.status,
3374
3692
  feedbackErrorCode: parsed?.feedbackErrorCode,
@@ -3376,7 +3694,7 @@ Returns submission status, feedback ID, and accounting fields.
3376
3694
  retryable: response.status >= 500
3377
3695
  });
3378
3696
  }
3379
- return asText3(parsed);
3697
+ return structuredText(parsed);
3380
3698
  }
3381
3699
  });
3382
3700
  }
@@ -3396,27 +3714,28 @@ Start a multi-page crawl at a website URL, poll it to a terminal state, and retu
3396
3714
 
3397
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.
3398
3716
  `,
3399
- parameters: z7.object({
3400
- url: z7.string(),
3401
- prompt: z7.string().optional(),
3402
- excludePaths: z7.array(z7.string()).optional(),
3403
- includePaths: z7.array(z7.string()).optional(),
3404
- maxDiscoveryDepth: z7.number().optional(),
3405
- sitemap: z7.enum(["skip", "include", "only"]).optional(),
3406
- limit: z7.number().optional(),
3407
- allowExternalLinks: z7.boolean().optional(),
3408
- allowSubdomains: z7.boolean().optional(),
3409
- crawlEntireDomain: z7.boolean().optional(),
3410
- delay: z7.number().optional(),
3411
- maxConcurrency: z7.number().optional(),
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(),
3412
3730
  ...SAFE_MODE ? {} : {
3413
- webhook: z7.string().optional(),
3414
- webhookHeaders: z7.record(z7.string(), z7.string()).optional()
3731
+ webhook: z8.string().optional(),
3732
+ webhookHeaders: z8.record(z8.string(), z8.string()).optional()
3415
3733
  },
3416
- deduplicateSimilarURLs: z7.boolean().optional(),
3417
- ignoreQueryParameters: z7.boolean().optional(),
3734
+ deduplicateSimilarURLs: z8.boolean().optional(),
3735
+ ignoreQueryParameters: z8.boolean().optional(),
3418
3736
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3419
3737
  }),
3738
+ outputSchema: crawlOutputSchema,
3420
3739
  execute: async (args2, { session, log, client: mcpClient }) => {
3421
3740
  const origin = requestOrigin(mcpClient, session);
3422
3741
  const { url, ...options } = args2;
@@ -3443,7 +3762,7 @@ Crawl results can be large; use conservative limits when full-site coverage is u
3443
3762
  });
3444
3763
  const crawlId = started?.data?.id;
3445
3764
  if (!crawlId) {
3446
- return asText3(started?.data ?? {});
3765
+ return structuredText(started?.data ?? {});
3447
3766
  }
3448
3767
  const res = await waitForCrawlCompletionWithOrigin(
3449
3768
  client,
@@ -3452,7 +3771,7 @@ Crawl results can be large; use conservative limits when full-site coverage is u
3452
3771
  pollInterval,
3453
3772
  timeout
3454
3773
  );
3455
- return asText3(res);
3774
+ return structuredText(res);
3456
3775
  }
3457
3776
  });
3458
3777
  server.addTool({
@@ -3469,7 +3788,8 @@ server.addTool({
3469
3788
  description: `
3470
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.
3471
3790
  `,
3472
- parameters: z7.object({ id: z7.string() }),
3791
+ outputSchema: crawlOutputSchema,
3792
+ parameters: z8.object({ id: z8.string() }),
3473
3793
  execute: async (args2, {
3474
3794
  session,
3475
3795
  client: mcpClient
@@ -3481,7 +3801,7 @@ Retrieve the current status, progress, and available results for an existing cra
3481
3801
  id,
3482
3802
  requestOrigin(mcpClient, session)
3483
3803
  );
3484
- return asText3(res);
3804
+ return structuredText(res);
3485
3805
  }
3486
3806
  });
3487
3807
  server.addTool({
@@ -3495,13 +3815,14 @@ server.addTool({
3495
3815
  description: `
3496
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.
3497
3817
  `,
3498
- parameters: z7.object({
3499
- urls: z7.array(z7.string()),
3500
- prompt: z7.string().optional(),
3501
- schema: z7.record(z7.string(), z7.any()).optional(),
3502
- allowExternalLinks: z7.boolean().optional(),
3503
- enableWebSearch: z7.boolean().optional(),
3504
- includeSubdomains: z7.boolean().optional()
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()
3505
3826
  }),
3506
3827
  canList: () => false,
3507
3828
  beforeValidate: () => {
@@ -3533,10 +3854,11 @@ Run web research that returns structured data when the URLs are not known or the
3533
3854
 
3534
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\`.
3535
3856
  `,
3536
- parameters: z7.object({
3537
- prompt: z7.string().min(1).max(1e4),
3538
- urls: z7.array(z7.string().url()).optional(),
3539
- schema: z7.record(z7.string(), z7.any()).optional()
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()
3540
3862
  }),
3541
3863
  execute: async (args2, { session, log, client: mcpClient }) => {
3542
3864
  const client = getClient(session);
@@ -3554,7 +3876,7 @@ This call returns only a job ID, not the research result. Read the job with \`fi
3554
3876
  ...agentBody,
3555
3877
  origin: requestOrigin(mcpClient, session)
3556
3878
  });
3557
- return asText3(res);
3879
+ return structuredText(res);
3558
3880
  }
3559
3881
  });
3560
3882
  server.addTool({
@@ -3573,7 +3895,8 @@ Retrieve progress or final results for a \`firecrawl_agent\` job ID. A \`process
3573
3895
 
3574
3896
  Returns job status, progress information, and result data when completed.
3575
3897
  `,
3576
- parameters: z7.object({ id: z7.string() }),
3898
+ outputSchema: agentStatusOutputSchema,
3899
+ parameters: z8.object({ id: z8.string() }),
3577
3900
  execute: async (args2, { session, log, client: mcpClient }) => {
3578
3901
  const client = getClient(session);
3579
3902
  const { id } = args2;
@@ -3582,7 +3905,7 @@ Returns job status, progress information, and result data when completed.
3582
3905
  `/v2/agent/${encodeURIComponent(id)}`,
3583
3906
  originHeaders(requestOrigin(mcpClient, session))
3584
3907
  );
3585
- return asText3(res?.data ?? {});
3908
+ return structuredText(res?.data ?? {});
3586
3909
  }
3587
3910
  });
3588
3911
  server.addTool({
@@ -3601,13 +3924,14 @@ Open or reuse a live browser session to navigate a page, click controls, fill fi
3601
3924
 
3602
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.
3603
3926
  `,
3604
- parameters: z7.object({
3605
- scrapeId: z7.string().trim().min(1).optional(),
3606
- url: z7.string().trim().url().optional(),
3607
- prompt: z7.string().trim().min(1).optional(),
3608
- code: z7.string().trim().min(1).optional(),
3609
- language: z7.enum(["bash", "python", "node"]).optional(),
3610
- timeout: z7.number().min(1).max(300).optional(),
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(),
3611
3935
  scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
3612
3936
  }).refine((data) => Boolean(data.scrapeId) !== Boolean(data.url), {
3613
3937
  message: "Provide either 'url' (interact directly) or 'scrapeId' (reuse a previous scrape), not both."
@@ -3639,14 +3963,14 @@ This acts on the live site, so actions such as form submission can create persis
3639
3963
  });
3640
3964
  scrapeId = scraped?.metadata?.scrapeId;
3641
3965
  if (!scrapeId) {
3642
- return asText3({
3966
+ return structuredText({
3643
3967
  error: "Could not open an interact session: the scrape did not return a scrapeId. Try firecrawl_scrape first, then pass its scrapeId.",
3644
3968
  url
3645
3969
  });
3646
3970
  }
3647
3971
  }
3648
3972
  if (!scrapeId) {
3649
- return asText3({
3973
+ return structuredText({
3650
3974
  error: "Could not open an interact session: missing scrapeId.",
3651
3975
  url
3652
3976
  });
@@ -3660,15 +3984,15 @@ This acts on the live site, so actions such as form submission can create persis
3660
3984
  if (timeout != null) interactArgs.timeout = timeout;
3661
3985
  const res = await client.interact(activeScrapeId, interactArgs);
3662
3986
  if (openedFromUrl && res && typeof res === "object" && !Array.isArray(res)) {
3663
- return asText3({
3987
+ return structuredText({
3664
3988
  ...res,
3665
3989
  scrapeId: activeScrapeId
3666
3990
  });
3667
3991
  }
3668
3992
  if (openedFromUrl) {
3669
- return asText3({ scrapeId: activeScrapeId, result: res });
3993
+ return structuredText({ scrapeId: activeScrapeId, result: res });
3670
3994
  }
3671
- return asText3(res);
3995
+ return structuredText(res);
3672
3996
  }
3673
3997
  });
3674
3998
  server.addTool({
@@ -3685,8 +4009,9 @@ server.addTool({
3685
4009
  description: `
3686
4010
  Stop the live interact session associated with a \`scrapeId\` and release its resources. Returns a success confirmation.
3687
4011
  `,
3688
- parameters: z7.object({
3689
- scrapeId: z7.string()
4012
+ outputSchema: interactStopOutputSchema,
4013
+ parameters: z8.object({
4014
+ scrapeId: z8.string()
3690
4015
  }),
3691
4016
  execute: async (args2, { session, log, client: mcpClient }) => {
3692
4017
  const client = getClient(session);
@@ -3696,7 +4021,7 @@ Stop the live interact session associated with a \`scrapeId\` and release its re
3696
4021
  `/v2/scrape/${encodeURIComponent(scrapeId)}/interact`,
3697
4022
  originHeaders(requestOrigin(mcpClient, session))
3698
4023
  );
3699
- return asText3(res?.data ?? {});
4024
+ return structuredText(res?.data ?? {});
3700
4025
  }
3701
4026
  });
3702
4027
  server.addTool({
@@ -3717,11 +4042,14 @@ Local MCP reads \`filePath\` from the server filesystem. Hosted MCP uses two cal
3717
4042
 
3718
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.
3719
4044
  `,
4045
+ outputSchema: parseOutputSchema,
3720
4046
  parameters: parseParamsSchema,
3721
4047
  execute: async (args2, { session, log, client: mcpClient }) => {
3722
4048
  const origin = requestOrigin(mcpClient, session);
3723
4049
  if (process.env.CLOUD_SERVICE === "true") {
3724
- return executeHostedParse(args2, session, log, origin);
4050
+ return structuredJsonText(
4051
+ await executeHostedParse(args2, session, log, origin)
4052
+ );
3725
4053
  }
3726
4054
  const apiUrl = process.env.FIRECRAWL_API_URL;
3727
4055
  if (!apiUrl) {
@@ -3771,15 +4099,16 @@ Set \`redactPII\` to request redaction of personally identifiable information in
3771
4099
  );
3772
4100
  }
3773
4101
  try {
3774
- return asText3(JSON.parse(responseText));
4102
+ return structuredText(JSON.parse(responseText));
3775
4103
  } catch {
3776
- return responseText;
4104
+ return withStructured(responseText, { raw: responseText });
3777
4105
  }
3778
4106
  }
3779
4107
  });
3780
4108
  function registerMarketplaceSearchTool(registrar, getClientFn) {
3781
4109
  registrar.addTool({
3782
4110
  name: "firecrawl_search",
4111
+ _meta: { "anthropic/alwaysLoad": true },
3783
4112
  annotations: {
3784
4113
  title: "Firecrawl web search",
3785
4114
  readOnlyHint: true,
@@ -3795,7 +4124,8 @@ ${ALEXANDRIA_SEARCH_INSTRUCTIONS}
3795
4124
 
3796
4125
  Returns result groups in \`data\` and an operation \`id\`.
3797
4126
  `,
3798
- 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(
3799
4129
  searchQueryIsValid,
3800
4130
  "A query is required. Use firecrawl_find_tools to browse the catalogue."
3801
4131
  ),
@@ -3846,7 +4176,7 @@ Returns result groups in \`data\` and an operation \`id\`.
3846
4176
  );
3847
4177
  const context = { tool: "firecrawl_search" };
3848
4178
  const httpRes = exchangeSource ? await relayExchangeError(postSearch, context) : await relayTermsRequired(postSearch, context);
3849
- return compactText(httpRes?.data ?? {});
4179
+ return structuredCompact(httpRes?.data ?? {});
3850
4180
  }
3851
4181
  });
3852
4182
  }
@@ -3918,10 +4248,10 @@ if (searchProfileEnabled) {
3918
4248
  stateless: true
3919
4249
  }
3920
4250
  });
3921
- } catch (error) {
4251
+ } catch (error2) {
3922
4252
  console.error(
3923
4253
  `[search-profile] failed to start on port ${searchProfile.port}; the full surface is unaffected`,
3924
- error
4254
+ error2
3925
4255
  );
3926
4256
  }
3927
4257
  }