firecrawl-mcp 3.30.2 → 3.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,6 +73,10 @@ Authorization: Bearer <FIRECRAWL_API_KEY>
73
73
 
74
74
  Never put an API key in the server URL. Never put an API key in an agent chat. Configure it directly in the client or secret manager. See the [hosted MCP setup guide](https://docs.firecrawl.dev/mcp-server) and the [agent onboarding guide](https://www.firecrawl.dev/agent-onboarding/SKILL.md) for client-specific instructions.
75
75
 
76
+ #### Naming your integration
77
+
78
+ An integration that connects over HTTP can name itself with an `X-Firecrawl-Origin` header, for example `X-Firecrawl-Origin: my-agent`. The server reports it to Firecrawl as the request origin `mcp-my-agent@<server version>`, in place of the client name or User-Agent it would otherwise report. Use letters, digits and dashes: each run of other characters becomes one dash, leading and trailing dashes are dropped, and the name is cut at 48 characters. Tool results are unaffected; only the origin sent to the Firecrawl API (and passed to an embedder's `beforeKeylessRequest`) changes.
79
+
76
80
  #### Search-only endpoint
77
81
 
78
82
  A fixed-scope search surface is also hosted at:
@@ -1246,7 +1250,7 @@ Options mirror the CLI's environment variables: `apiUrl`, `apiKey`, `transport`,
1246
1250
 
1247
1251
  FastMCP is bundled inside the package, so use the types exported from `firecrawl-mcp/server` (`Session`, `ToolDefinition`, `ToolRegistrar`, `FirecrawlMcpServerHooks` and others) rather than importing `fastmcp` yourself.
1248
1252
 
1249
- The entry point also exports the helpers the built-in tools use, such as `recoveryPayload`, `normalizeHeader`, `requestOrigin`, `searchToolBaseFields`, `searchOutputSchema` and `structuredCompact`, so extra tools and hooks can match the built-in behaviour. They are unstable as well.
1253
+ The entry point also exports the helpers the built-in tools use, such as `recoveryPayload`, `normalizeHeader`, `requestOrigin`, `isRuntimeUserAgent`, `initializeClientSlug`, `searchToolBaseFields`, `searchOutputSchema` and `structuredCompact`, so extra tools and hooks can match the built-in behaviour. They are unstable as well.
1250
1254
 
1251
1255
  ## Logging System
1252
1256
 
package/dist/index.js CHANGED
@@ -2401,6 +2401,14 @@ function defaultDomainTools(sources) {
2401
2401
  (source) => ["web", "news", "images"].includes(typeof source === "string" ? source : source?.type)
2402
2402
  );
2403
2403
  }
2404
+ function withAlexandriaSource(sources) {
2405
+ if (!Array.isArray(sources) || sources.length === 0) return ["web", "alexandria"];
2406
+ if (hasAlexandria(sources)) return sources;
2407
+ return [...sources, typeof sources[0] === "string" ? "alexandria" : { type: "alexandria" }];
2408
+ }
2409
+ function enterpriseAllowsAlexandria(enterprise) {
2410
+ return !(Array.isArray(enterprise) && enterprise.some((mode) => mode === "zdr" || mode === "anon"));
2411
+ }
2404
2412
  function normalizeSearchSources(sources) {
2405
2413
  if (!Array.isArray(sources)) return sources;
2406
2414
  return sources.map(
@@ -2427,8 +2435,8 @@ var findToolsSchema = z5.object({
2427
2435
  }).strict();
2428
2436
  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";
2429
2437
  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.";
2430
- 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.';
2431
- 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;
2438
+ var ALEXANDRIA_SOURCES_ALWAYS_ON = 'Authenticated searches always add alexandria to the sources, so sources: ["web"] also returns providers; domainTools: false turns off website-matched tools only.';
2439
+ 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_ALWAYS_ON;
2432
2440
  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.";
2433
2441
  function findToolsOptions(args2) {
2434
2442
  const level = args2.level ?? (args2.query || args2.capabilities?.length || args2.providers?.length || args2.groups?.length || args2.urls?.length ? "tools" : args2.categories?.length ? "providers" : "categories");
@@ -2460,7 +2468,7 @@ function withFindToolsNavigation(envelope, carry = {}) {
2460
2468
  if (link(page.next)) page.nextTool = link(page.next);
2461
2469
  return envelope;
2462
2470
  }
2463
- 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.';
2471
+ var ALEXANDRIA_SEARCH_INSTRUCTIONS = 'Authenticated search combines web results, semantic tool summaries and domain matches. Use sources: ["alexandria"] for semantic tools only. ' + ALEXANDRIA_SOURCES_ALWAYS_ON + ' 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.';
2464
2472
  var TOOL_PRICE_KEYS = ["creditsCost", "perRecord", "billing", "recordsPerUnit", "zeroDataRetentionCreditsCost"];
2465
2473
  function withoutToolPrice(tool) {
2466
2474
  if (!tool || typeof tool !== "object" || Array.isArray(tool)) return tool;
@@ -2575,19 +2583,79 @@ import { z as z6 } from "zod";
2575
2583
  // src/origin.ts
2576
2584
  import { createRequire } from "module";
2577
2585
  var ORIGIN_SERVER = "mcp-fastmcp";
2586
+ var DECLARED_ORIGIN_HEADER = "x-firecrawl-origin";
2578
2587
  var NAME_MAX = 48;
2588
+ var RUNTIME_PRODUCTS = /* @__PURE__ */ new Set([
2589
+ "apache-httpasyncclient",
2590
+ "apache-httpclient",
2591
+ "aiohttp",
2592
+ "axios",
2593
+ "bun",
2594
+ "curl",
2595
+ "dart",
2596
+ "deno",
2597
+ "faraday",
2598
+ "go-http-client",
2599
+ "got",
2600
+ "guzzlehttp",
2601
+ "java-http-client",
2602
+ "ktor-client",
2603
+ "libcurl-agent",
2604
+ "mozilla",
2605
+ "node",
2606
+ "node-fetch",
2607
+ "okhttp",
2608
+ "python",
2609
+ "python-httpx",
2610
+ "python-httpx2",
2611
+ "python-requests",
2612
+ "python-urllib",
2613
+ "reqwest",
2614
+ "ruby",
2615
+ "undici",
2616
+ "wget"
2617
+ ]);
2618
+ var GENERIC_CLIENT_NAMES = /* @__PURE__ */ new Set([
2619
+ "client",
2620
+ "example-client",
2621
+ "mcp",
2622
+ "mcp-client",
2623
+ "test",
2624
+ "test-client"
2625
+ ]);
2579
2626
  function slugOf(name2) {
2580
2627
  return (name2 ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, NAME_MAX).replace(/-+$/, "");
2581
2628
  }
2629
+ function isOwnSlug(slug) {
2630
+ return slug === "" || slug === "fastmcp" || slug === "firecrawl-fastmcp";
2631
+ }
2582
2632
  function userAgentProduct(userAgent) {
2583
2633
  const first = (userAgent ?? "").trim().split(/\s+/)[0] ?? "";
2584
2634
  return slugOf(first.split("/")[0]);
2585
2635
  }
2586
- function originForClient(client, serverVersion2, userAgent) {
2636
+ function initializeClientSlug(name2) {
2637
+ const slug = slugOf(name2);
2638
+ return isOwnSlug(slug) || GENERIC_CLIENT_NAMES.has(slug) ? "" : slug;
2639
+ }
2640
+ function referrerSlug(referrer) {
2641
+ let host;
2642
+ try {
2643
+ host = new URL(referrer ?? "").hostname.toLowerCase().replace(/\.$/, "");
2644
+ } catch {
2645
+ return "";
2646
+ }
2647
+ if (!host.includes(".") || host.endsWith(".localhost") || host.endsWith(".local") || /^[\d.]+$/.test(host) || host.startsWith("[")) {
2648
+ return "";
2649
+ }
2650
+ return slugOf(host.replace(/^www\./, ""));
2651
+ }
2652
+ function originForClient(client, serverVersion2, userAgent, signals = {}) {
2653
+ const declared = slugOf(signals.declaredOrigin);
2587
2654
  const slug = slugOf(client?.name);
2588
- const own = slug === "" || slug === "fastmcp" || slug === "firecrawl-fastmcp";
2589
- const product = own ? userAgentProduct(userAgent) : "";
2590
- const base = !own ? `mcp-${slug}` : product ? `mcp-ua-${product}` : ORIGIN_SERVER;
2655
+ const product = userAgentProduct(userAgent);
2656
+ const remembered = initializeClientSlug(signals.initializeClient?.name);
2657
+ const referrer = referrerSlug(signals.referrer);
2658
+ const base = !isOwnSlug(declared) ? `mcp-${declared}` : !isOwnSlug(slug) ? `mcp-${slug}` : product && !RUNTIME_PRODUCTS.has(product) ? `mcp-ua-${product}` : remembered ? `mcp-${remembered}` : referrer ? `mcp-ref-${referrer}` : product ? `mcp-ua-${product}` : ORIGIN_SERVER;
2591
2659
  return serverVersion2 ? `${base}@${serverVersion2}` : base;
2592
2660
  }
2593
2661
  function originHeaders(origin) {
@@ -2600,7 +2668,12 @@ function requestOrigin(client, session) {
2600
2668
  return originForClient(
2601
2669
  client?.version ?? null,
2602
2670
  serverVersion,
2603
- session?.clientUserAgent
2671
+ session?.clientUserAgent,
2672
+ {
2673
+ declaredOrigin: session?.clientOrigin,
2674
+ initializeClient: session?.initializeClient,
2675
+ referrer: session?.clientReferrer
2676
+ }
2604
2677
  );
2605
2678
  }
2606
2679
 
@@ -3677,7 +3750,7 @@ var searchToolBaseFields = {
3677
3750
  location: z11.string().optional(),
3678
3751
  includeDomains: z11.array(searchDomainSchema).optional().describe("Hostnames to restrict results to. Mutually exclusive with excludeDomains."),
3679
3752
  excludeDomains: z11.array(searchDomainSchema).optional().describe("Hostnames to leave out of results. Mutually exclusive with includeDomains."),
3680
- sources: z11.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.'),
3753
+ sources: z11.array(searchSourceSchema).optional().describe("Search sources; authenticated sessions default to web + alexandria, keyless sessions to web only. " + ALEXANDRIA_SOURCES_ALWAYS_ON + ' enterprise zdr or anon searches do not add it. Use ["alexandria"] alone for provider discovery without web results.'),
3681
3754
  categories: z11.array(z11.enum(["research", "pdf", "developer", "gov"])).optional().describe(
3682
3755
  "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; `gov` searches the Government Index of US federal, state, and local legal and regulatory sources, the index behind firecrawl_gov_search, and cannot be combined with other categories; gov hits can include historical versions, bill text, agency guidance, and third-party reproductions, so verify the issuer, jurisdiction, enacted/effective status, and version before citing current governing law, clarify a missing jurisdiction or ambiguous as-of date, and use general search for non-US questions or missing coverage (no hits does not establish that no applicable law exists). `developer` and `gov` return hits in `data.web` with `category` set to their name; the other categories also filter `data.web`."
3683
3756
  ),
@@ -3819,7 +3892,7 @@ async function relayExchangeError(run, context) {
3819
3892
  });
3820
3893
  }
3821
3894
  }
3822
- async function postSearchWithFallback(client, body, implicitTools) {
3895
+ async function postSearchWithFallback(client, body, implicitTools, fallbackSources = ["web"]) {
3823
3896
  try {
3824
3897
  return await client.http.post("/v2/search", body);
3825
3898
  } catch (error2) {
@@ -3832,7 +3905,7 @@ async function postSearchWithFallback(client, body, implicitTools) {
3832
3905
  if (!implicitTools || response?.status !== 403 || !unavailable) throw error2;
3833
3906
  return client.http.post("/v2/search", {
3834
3907
  ...body,
3835
- sources: ["web"],
3908
+ sources: fallbackSources,
3836
3909
  domainTools: false
3837
3910
  });
3838
3911
  }
@@ -3892,7 +3965,7 @@ var ConsoleLogger = class {
3892
3965
  }
3893
3966
  }
3894
3967
  };
3895
- 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. For a legal or regulatory question, firecrawl_gov_search searches primary law and regulatory material from US federal, state, and local government sources: statutes, regulations, codes, court opinions, and other government publications; firecrawl_search with categories: ["gov"] reaches the same sources and cannot be combined with other categories. firecrawl_search with sources: [{type: "alexandria"}] returns compact tool summaries in data.tools; toolDetail: "full" includes contracts, firecrawl_find_tools starts with categories, lists providers, then compact tools, and expands the selected full contract, and firecrawl_scrape with alexandria: [{provider, capability, options}] executes up to ten capabilities and returns their results. Alexandria access needs an API key on a team with it enabled. Provide only the required inputs and account for stated network or external side effects.`;
3968
+ 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_ALWAYS_ON} 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. For a legal or regulatory question, firecrawl_gov_search searches primary law and regulatory material from US federal, state, and local government sources: statutes, regulations, codes, court opinions, and other government publications; firecrawl_search with categories: ["gov"] reaches the same sources and cannot be combined with other categories. firecrawl_search with sources: [{type: "alexandria"}] returns compact tool summaries in data.tools; toolDetail: "full" includes contracts, firecrawl_find_tools starts with categories, lists providers, then compact tools, and expands the selected full contract, and firecrawl_scrape with alexandria: [{provider, capability, options}] executes up to ten capabilities and returns their results. Alexandria access needs an API key on a team with it enabled. Provide only the required inputs and account for stated network or external side effects.`;
3896
3969
  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 legal or regulatory questions, firecrawl_search with categories: ["gov"] searches US government legal and regulatory sources and cannot be combined with other categories. 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.`;
3897
3970
  function deprecatedExtractPayload() {
3898
3971
  return {
@@ -3953,10 +4026,18 @@ function createFirecrawlMcpServer(options2 = {}) {
3953
4026
  const cached = request?.[authResultByRequest];
3954
4027
  if (cached) return cached;
3955
4028
  const authResult = authenticateSession(request).then((session) => {
3956
- const userAgent = request?.headers?.["user-agent"];
4029
+ const headers = request?.headers;
4030
+ const userAgent = headers?.["user-agent"];
3957
4031
  if (typeof userAgent === "string" && userAgent !== "") {
3958
4032
  session.clientUserAgent = userAgent;
3959
4033
  }
4034
+ const declaredOrigin = normalizeHeader(headers?.[DECLARED_ORIGIN_HEADER]);
4035
+ if (declaredOrigin) session.clientOrigin = declaredOrigin;
4036
+ const referrer = [
4037
+ normalizeHeader(headers?.["origin"]),
4038
+ normalizeHeader(headers?.["referer"])
4039
+ ].find((value) => value && value !== "null");
4040
+ if (referrer) session.clientReferrer = referrer;
3960
4041
  return session;
3961
4042
  });
3962
4043
  if (request) request[authResultByRequest] = authResult;
@@ -4561,7 +4642,7 @@ Scrape one URL and return its content: markdown by default, or HTML, links, scre
4561
4642
 
4562
4643
  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. ${SAFE_MODE ? "A named browser profile loads saved session data without saving changes to it." : "Browser actions can change the live page when interactive actions are enabled."} Authenticated responses can include a \`metadata.scrapeId\` for optional scrape feedback.
4563
4644
 
4564
- 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.
4645
+ On an authenticated session with Alexandria access, \`firecrawl_search\` 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.
4565
4646
 
4566
4647
  Alexandria mode, on an authenticated session with Alexandria access: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`.
4567
4648
  `,
@@ -4691,11 +4772,13 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
4691
4772
  ),
4692
4773
  execute: async (args2, { session, log, client: mcpClient }) => {
4693
4774
  const { query, ...opts } = args2;
4775
+ const requestedSources = normalizeSearchSources(
4776
+ Array.isArray(opts.sources) && opts.sources.length ? opts.sources : ["web"]
4777
+ );
4778
+ const addsAlexandria = hasCredential(session) && enterpriseAllowsAlexandria(opts.enterprise) && !hasAlexandria(requestedSources);
4694
4779
  const searchOpts = {
4695
4780
  ...opts,
4696
- sources: normalizeSearchSources(
4697
- opts.sources ?? (hasCredential(session) ? ["web", "alexandria"] : ["web"])
4698
- )
4781
+ sources: addsAlexandria ? withAlexandriaSource(requestedSources) : requestedSources
4699
4782
  };
4700
4783
  searchOpts.domainTools ??= defaultDomainTools(searchOpts.sources);
4701
4784
  searchOpts.toolDetail ??= "compact";
@@ -4725,7 +4808,8 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
4725
4808
  const postSearch = () => postSearchWithFallback(
4726
4809
  client,
4727
4810
  searchBody,
4728
- opts.sources === void 0 && opts.domainTools !== true
4811
+ addsAlexandria && opts.domainTools !== true,
4812
+ requestedSources
4729
4813
  );
4730
4814
  const httpRes = await relayExchangeError(postSearch, { tool: "firecrawl_search" });
4731
4815
  return structuredCompact(withoutDiscoveredToolPrices(httpRes?.data ?? {}));
@@ -6384,7 +6468,7 @@ function searchSurfaceScrapeDescription(safeMode) {
6384
6468
  return `
6385
6469
  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. ${safeMode ? "A named browser profile loads saved session data without saving changes to it." : "Browser actions can change the live page, and a named browser profile can load saved session data and overwrite its stored state."}
6386
6470
 
6387
- \`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.
6471
+ \`firecrawl_search\` and \`firecrawl_find_tools\` can discover providers for the same fields across several pages; a matching Alexandria provider returns typed records in one call.
6388
6472
 
6389
6473
  Alexandria mode: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`. Alexandria execution is billed per capability (pass includeCosts to firecrawl_find_tools to see prices if you need) and needs a team with Alexandria enabled.
6390
6474
  `;
@@ -6440,6 +6524,11 @@ Returns result groups in \`data\` and an operation \`id\`.
6440
6524
  toolDetail
6441
6525
  } = args2;
6442
6526
  const searchQuery = query ?? "";
6527
+ const requestedSources = normalizeSearchSources(
6528
+ sources?.length ? sources : ["web"]
6529
+ );
6530
+ const addsAlexandria = hasCredential(session) && enterpriseAllowsAlexandria(enterprise) && !hasAlexandria(requestedSources);
6531
+ const searchSources = addsAlexandria ? withAlexandriaSource(requestedSources) : requestedSources;
6443
6532
  const searchBody = {
6444
6533
  query: searchQuery,
6445
6534
  ...removeEmptyTopLevel({
@@ -6449,12 +6538,12 @@ Returns result groups in \`data\` and an operation \`id\`.
6449
6538
  tbs,
6450
6539
  filter,
6451
6540
  location,
6452
- sources: normalizeSearchSources(sources ?? ["web", "alexandria"]),
6541
+ sources: searchSources,
6453
6542
  categories,
6454
6543
  highlights,
6455
6544
  enterprise,
6456
6545
  toolDetail: toolDetail ?? "compact",
6457
- domainTools: domainTools ?? defaultDomainTools(sources ?? ["web", "alexandria"])
6546
+ domainTools: domainTools ?? defaultDomainTools(searchSources)
6458
6547
  }),
6459
6548
  origin: requestOrigin(mcpClient, session)
6460
6549
  };
@@ -6466,7 +6555,8 @@ Returns result groups in \`data\` and an operation \`id\`.
6466
6555
  const postSearch = () => postSearchWithFallback(
6467
6556
  client,
6468
6557
  searchBody,
6469
- sources === void 0 && domainTools !== true
6558
+ addsAlexandria && domainTools !== true,
6559
+ requestedSources
6470
6560
  );
6471
6561
  const httpRes = await relayExchangeError(postSearch, { tool: "firecrawl_search" });
6472
6562
  return structuredCompactResult(httpRes?.data ?? {});
package/dist/origin.js CHANGED
@@ -1,19 +1,83 @@
1
1
  // src/origin.ts
2
2
  import { createRequire } from "module";
3
3
  var ORIGIN_SERVER = "mcp-fastmcp";
4
+ var DECLARED_ORIGIN_HEADER = "x-firecrawl-origin";
4
5
  var NAME_MAX = 48;
6
+ var RUNTIME_PRODUCTS = /* @__PURE__ */ new Set([
7
+ "apache-httpasyncclient",
8
+ "apache-httpclient",
9
+ "aiohttp",
10
+ "axios",
11
+ "bun",
12
+ "curl",
13
+ "dart",
14
+ "deno",
15
+ "faraday",
16
+ "go-http-client",
17
+ "got",
18
+ "guzzlehttp",
19
+ "java-http-client",
20
+ "ktor-client",
21
+ "libcurl-agent",
22
+ "mozilla",
23
+ "node",
24
+ "node-fetch",
25
+ "okhttp",
26
+ "python",
27
+ "python-httpx",
28
+ "python-httpx2",
29
+ "python-requests",
30
+ "python-urllib",
31
+ "reqwest",
32
+ "ruby",
33
+ "undici",
34
+ "wget"
35
+ ]);
36
+ var GENERIC_CLIENT_NAMES = /* @__PURE__ */ new Set([
37
+ "client",
38
+ "example-client",
39
+ "mcp",
40
+ "mcp-client",
41
+ "test",
42
+ "test-client"
43
+ ]);
5
44
  function slugOf(name) {
6
45
  return (name ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, NAME_MAX).replace(/-+$/, "");
7
46
  }
47
+ function isOwnSlug(slug) {
48
+ return slug === "" || slug === "fastmcp" || slug === "firecrawl-fastmcp";
49
+ }
8
50
  function userAgentProduct(userAgent) {
9
51
  const first = (userAgent ?? "").trim().split(/\s+/)[0] ?? "";
10
52
  return slugOf(first.split("/")[0]);
11
53
  }
12
- function originForClient(client, serverVersion2, userAgent) {
54
+ function isRuntimeUserAgent(userAgent) {
55
+ const product = userAgentProduct(userAgent);
56
+ return product === "" || RUNTIME_PRODUCTS.has(product);
57
+ }
58
+ function initializeClientSlug(name) {
59
+ const slug = slugOf(name);
60
+ return isOwnSlug(slug) || GENERIC_CLIENT_NAMES.has(slug) ? "" : slug;
61
+ }
62
+ function referrerSlug(referrer) {
63
+ let host;
64
+ try {
65
+ host = new URL(referrer ?? "").hostname.toLowerCase().replace(/\.$/, "");
66
+ } catch {
67
+ return "";
68
+ }
69
+ if (!host.includes(".") || host.endsWith(".localhost") || host.endsWith(".local") || /^[\d.]+$/.test(host) || host.startsWith("[")) {
70
+ return "";
71
+ }
72
+ return slugOf(host.replace(/^www\./, ""));
73
+ }
74
+ function originForClient(client, serverVersion2, userAgent, signals = {}) {
75
+ const declared = slugOf(signals.declaredOrigin);
13
76
  const slug = slugOf(client?.name);
14
- const own = slug === "" || slug === "fastmcp" || slug === "firecrawl-fastmcp";
15
- const product = own ? userAgentProduct(userAgent) : "";
16
- const base = !own ? `mcp-${slug}` : product ? `mcp-ua-${product}` : ORIGIN_SERVER;
77
+ const product = userAgentProduct(userAgent);
78
+ const remembered = initializeClientSlug(signals.initializeClient?.name);
79
+ const referrer = referrerSlug(signals.referrer);
80
+ const base = !isOwnSlug(declared) ? `mcp-${declared}` : !isOwnSlug(slug) ? `mcp-${slug}` : product && !RUNTIME_PRODUCTS.has(product) ? `mcp-ua-${product}` : remembered ? `mcp-${remembered}` : referrer ? `mcp-ref-${referrer}` : product ? `mcp-ua-${product}` : ORIGIN_SERVER;
17
81
  return serverVersion2 ? `${base}@${serverVersion2}` : base;
18
82
  }
19
83
  function originHeaders(origin) {
@@ -26,11 +90,19 @@ function requestOrigin(client, session) {
26
90
  return originForClient(
27
91
  client?.version ?? null,
28
92
  serverVersion,
29
- session?.clientUserAgent
93
+ session?.clientUserAgent,
94
+ {
95
+ declaredOrigin: session?.clientOrigin,
96
+ initializeClient: session?.initializeClient,
97
+ referrer: session?.clientReferrer
98
+ }
30
99
  );
31
100
  }
32
101
  export {
102
+ DECLARED_ORIGIN_HEADER,
33
103
  ORIGIN_SERVER,
104
+ initializeClientSlug,
105
+ isRuntimeUserAgent,
34
106
  originForClient,
35
107
  originHeaders,
36
108
  requestOrigin,
package/dist/server.d.ts CHANGED
@@ -6,10 +6,20 @@ import { z } from 'zod';
6
6
  declare function normalizeHeader(value: string | string[] | undefined): string | undefined;
7
7
  declare function withoutTrailingSlash(value: string): string;
8
8
 
9
+ /** The header a connection sets to name the integration it comes from. */
10
+ declare const DECLARED_ORIGIN_HEADER = "x-firecrawl-origin";
9
11
  type ClientImplementation = {
10
12
  name?: string;
11
13
  version?: string;
12
14
  };
15
+ /** Whether a User-Agent is absent or names an HTTP runtime rather than an MCP client. */
16
+ declare function isRuntimeUserAgent(userAgent: string | null | undefined): boolean;
17
+ /**
18
+ * The token an initialize client name stamps when it reaches a call only
19
+ * through `ClientSignals.initializeClient`; empty for the server's own names
20
+ * and the defaults SDKs send when the integration sets none.
21
+ */
22
+ declare function initializeClientSlug(name: string | null | undefined): string;
13
23
  /** The header form of an origin, for the requests that reach the API without a body. */
14
24
  declare function originHeaders(origin: string): Record<string, string>;
15
25
  /**
@@ -19,14 +29,19 @@ declare function originHeaders(origin: string): Record<string, string>;
19
29
  type McpClient = {
20
30
  version?: ClientImplementation | undefined;
21
31
  } | undefined;
32
+ /** The client signals the authenticated session carries (see SessionData). */
33
+ type OriginSession = {
34
+ clientUserAgent?: string;
35
+ clientOrigin?: string;
36
+ clientReferrer?: string;
37
+ initializeClient?: ClientImplementation;
38
+ };
22
39
  /**
23
40
  * The origin of every request a tool call makes: the call's client, the
24
- * User-Agent the session was authenticated with (stateless HTTP calls carry
25
- * no client name), and this server's version.
41
+ * client signals of the request the session was authenticated with
42
+ * (stateless HTTP calls carry no client name), and this server's version.
26
43
  */
27
- declare function requestOrigin(client: McpClient, session?: {
28
- clientUserAgent?: string;
29
- } | undefined): string;
44
+ declare function requestOrigin(client: McpClient, session?: OriginSession | undefined): string;
30
45
 
31
46
  /** An API-issued keyless signup link, or undefined for anything else. */
32
47
  declare function keylessSignupUrlFrom(value: unknown): string | undefined;
@@ -44,6 +59,10 @@ declare function keylessQuotaReason(reason: unknown): reason is 'requests' | 'cr
44
59
 
45
60
  declare function hasAlexandria(sources: unknown): boolean;
46
61
  declare function defaultDomainTools(sources: unknown): boolean;
62
+ /** Adds alexandria to the requested sources, keeping the caller's entries and their string or object form. */
63
+ declare function withAlexandriaSource(sources: unknown): unknown[];
64
+ /** Zero data retention and anonymous searches cannot run provider discovery. */
65
+ declare function enterpriseAllowsAlexandria(enterprise: unknown): boolean;
47
66
  declare function normalizeSearchSources(sources: unknown): unknown;
48
67
  declare function searchQueryIsValid(args: {
49
68
  query?: string;
@@ -146,7 +165,8 @@ type ExchangeErrorContext = {
146
165
  };
147
166
  declare function relayTermsRequired<T>(run: () => Promise<T>, context: ExchangeErrorContext): Promise<T>;
148
167
  declare function relayExchangeError(run: () => Promise<any>, context: ExchangeErrorContext): Promise<any>;
149
- declare function postSearchWithFallback(client: any, body: Record<string, unknown>, implicitTools: boolean): Promise<any>;
168
+ /** Retries without Alexandria, on fallbackSources, when tools the caller did not ask for are refused. */
169
+ declare function postSearchWithFallback(client: any, body: Record<string, unknown>, implicitTools: boolean, fallbackSources?: unknown[]): Promise<any>;
150
170
 
151
171
  /**
152
172
  * Library entry point: `import { createFirecrawlMcpServer } from 'firecrawl-mcp/server'`.
@@ -204,10 +224,23 @@ interface SessionData {
204
224
  */
205
225
  firecrawlApiKey?: string;
206
226
  /**
207
- * The User-Agent of the HTTP request that opened the session, the only
208
- * client signal a stateless HTTP tool call carries (see src/origin.ts).
227
+ * The User-Agent of the HTTP request that opened the session, one of the
228
+ * client signals a stateless HTTP tool call carries (see src/origin.ts).
209
229
  */
210
230
  clientUserAgent?: string;
231
+ /** The `X-Firecrawl-Origin` header: the integration the connection names. */
232
+ clientOrigin?: string;
233
+ /** The `Origin` or `Referer` header of that request. */
234
+ clientReferrer?: string;
235
+ /**
236
+ * The client an earlier initialize named, for stateless HTTP calls that do
237
+ * not carry it. Set by an embedder that can recover it; read only when the
238
+ * User-Agent names a runtime rather than a client.
239
+ */
240
+ initializeClient?: {
241
+ name?: string;
242
+ version?: string;
243
+ };
211
244
  authType?: 'api-key' | 'oauth' | 'env' | 'keyless' | 'none';
212
245
  /** Admitted, but the supplied credential cannot be used. */
213
246
  credentialError?: 'CREDENTIAL_INVALID';
@@ -423,4 +456,4 @@ declare const structuredCompactResult: (data: unknown) => ToolResult;
423
456
 
424
457
  declare function createFirecrawlMcpServer(options?: FirecrawlMcpServerOptions): FirecrawlMcpServer;
425
458
 
426
- export { ALEXANDRIA_CATALOGUE_VERTICALS, ALEXANDRIA_SEARCH_INSTRUCTIONS, type AuthenticationRequest, type ExtraToolsContext, type FirecrawlMcpServer, type FirecrawlMcpServerHooks, type FirecrawlMcpServerOptions, type FirecrawlMcpServerStartArgs, type HttpApp, type InstructionDefaults, keylessToolNames as KEYLESS_TOOL_NAMES, type OutboundRequest, type ProtectedResourceMetadata, type ResourceContent, type ResourceDefinition, type ResourceRegistrar, SEARCH_DOMAINS_CONFLICT_MESSAGE, type Session, type SessionData, type ToolCallEvent, type ToolContext, type ToolDefinition, type ToolIcon, type ToolLogger, type ToolRegistrar, type ToolResult, UserError, assertExchangeCredential, createFirecrawlMcpServer, defaultDomainTools, hasAlexandria, invalidApiKeyRecoveryPayload, invalidOAuthRecoveryPayload, keylessQuotaReason, keylessSignupUrlFrom, normalizeHeader, normalizeSearchSources, originHeaders, postSearchWithFallback, recoveryPayload, relayExchangeError, relayTermsRequired, removeEmptyTopLevel, requestOrigin, searchDomainsAreExclusive, searchOutputSchema, searchQueryIsValid, searchToolBaseFields, structuredCompactResult as structuredCompact, withoutTrailingSlash };
459
+ export { ALEXANDRIA_CATALOGUE_VERTICALS, ALEXANDRIA_SEARCH_INSTRUCTIONS, type AuthenticationRequest, DECLARED_ORIGIN_HEADER, type ExtraToolsContext, type FirecrawlMcpServer, type FirecrawlMcpServerHooks, type FirecrawlMcpServerOptions, type FirecrawlMcpServerStartArgs, type HttpApp, type InstructionDefaults, keylessToolNames as KEYLESS_TOOL_NAMES, type OutboundRequest, type ProtectedResourceMetadata, type ResourceContent, type ResourceDefinition, type ResourceRegistrar, SEARCH_DOMAINS_CONFLICT_MESSAGE, type Session, type SessionData, type ToolCallEvent, type ToolContext, type ToolDefinition, type ToolIcon, type ToolLogger, type ToolRegistrar, type ToolResult, UserError, assertExchangeCredential, createFirecrawlMcpServer, defaultDomainTools, enterpriseAllowsAlexandria, hasAlexandria, initializeClientSlug, invalidApiKeyRecoveryPayload, invalidOAuthRecoveryPayload, isRuntimeUserAgent, keylessQuotaReason, keylessSignupUrlFrom, normalizeHeader, normalizeSearchSources, originHeaders, postSearchWithFallback, recoveryPayload, relayExchangeError, relayTermsRequired, removeEmptyTopLevel, requestOrigin, searchDomainsAreExclusive, searchOutputSchema, searchQueryIsValid, searchToolBaseFields, structuredCompactResult as structuredCompact, withAlexandriaSource, withoutTrailingSlash };
package/dist/server.js CHANGED
@@ -2396,6 +2396,14 @@ function defaultDomainTools(sources) {
2396
2396
  (source) => ["web", "news", "images"].includes(typeof source === "string" ? source : source?.type)
2397
2397
  );
2398
2398
  }
2399
+ function withAlexandriaSource(sources) {
2400
+ if (!Array.isArray(sources) || sources.length === 0) return ["web", "alexandria"];
2401
+ if (hasAlexandria(sources)) return sources;
2402
+ return [...sources, typeof sources[0] === "string" ? "alexandria" : { type: "alexandria" }];
2403
+ }
2404
+ function enterpriseAllowsAlexandria(enterprise) {
2405
+ return !(Array.isArray(enterprise) && enterprise.some((mode) => mode === "zdr" || mode === "anon"));
2406
+ }
2399
2407
  function normalizeSearchSources(sources) {
2400
2408
  if (!Array.isArray(sources)) return sources;
2401
2409
  return sources.map(
@@ -2422,8 +2430,8 @@ var findToolsSchema = z5.object({
2422
2430
  }).strict();
2423
2431
  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";
2424
2432
  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.";
2425
- 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.';
2426
- 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;
2433
+ var ALEXANDRIA_SOURCES_ALWAYS_ON = 'Authenticated searches always add alexandria to the sources, so sources: ["web"] also returns providers; domainTools: false turns off website-matched tools only.';
2434
+ 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_ALWAYS_ON;
2427
2435
  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.";
2428
2436
  function findToolsOptions(args) {
2429
2437
  const level = args.level ?? (args.query || args.capabilities?.length || args.providers?.length || args.groups?.length || args.urls?.length ? "tools" : args.categories?.length ? "providers" : "categories");
@@ -2455,7 +2463,7 @@ function withFindToolsNavigation(envelope, carry = {}) {
2455
2463
  if (link(page.next)) page.nextTool = link(page.next);
2456
2464
  return envelope;
2457
2465
  }
2458
- 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.';
2466
+ var ALEXANDRIA_SEARCH_INSTRUCTIONS = 'Authenticated search combines web results, semantic tool summaries and domain matches. Use sources: ["alexandria"] for semantic tools only. ' + ALEXANDRIA_SOURCES_ALWAYS_ON + ' 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.';
2459
2467
  var TOOL_PRICE_KEYS = ["creditsCost", "perRecord", "billing", "recordsPerUnit", "zeroDataRetentionCreditsCost"];
2460
2468
  function withoutToolPrice(tool) {
2461
2469
  if (!tool || typeof tool !== "object" || Array.isArray(tool)) return tool;
@@ -2570,19 +2578,83 @@ import { z as z6 } from "zod";
2570
2578
  // src/origin.ts
2571
2579
  import { createRequire } from "module";
2572
2580
  var ORIGIN_SERVER = "mcp-fastmcp";
2581
+ var DECLARED_ORIGIN_HEADER = "x-firecrawl-origin";
2573
2582
  var NAME_MAX = 48;
2583
+ var RUNTIME_PRODUCTS = /* @__PURE__ */ new Set([
2584
+ "apache-httpasyncclient",
2585
+ "apache-httpclient",
2586
+ "aiohttp",
2587
+ "axios",
2588
+ "bun",
2589
+ "curl",
2590
+ "dart",
2591
+ "deno",
2592
+ "faraday",
2593
+ "go-http-client",
2594
+ "got",
2595
+ "guzzlehttp",
2596
+ "java-http-client",
2597
+ "ktor-client",
2598
+ "libcurl-agent",
2599
+ "mozilla",
2600
+ "node",
2601
+ "node-fetch",
2602
+ "okhttp",
2603
+ "python",
2604
+ "python-httpx",
2605
+ "python-httpx2",
2606
+ "python-requests",
2607
+ "python-urllib",
2608
+ "reqwest",
2609
+ "ruby",
2610
+ "undici",
2611
+ "wget"
2612
+ ]);
2613
+ var GENERIC_CLIENT_NAMES = /* @__PURE__ */ new Set([
2614
+ "client",
2615
+ "example-client",
2616
+ "mcp",
2617
+ "mcp-client",
2618
+ "test",
2619
+ "test-client"
2620
+ ]);
2574
2621
  function slugOf(name2) {
2575
2622
  return (name2 ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, NAME_MAX).replace(/-+$/, "");
2576
2623
  }
2624
+ function isOwnSlug(slug) {
2625
+ return slug === "" || slug === "fastmcp" || slug === "firecrawl-fastmcp";
2626
+ }
2577
2627
  function userAgentProduct(userAgent) {
2578
2628
  const first = (userAgent ?? "").trim().split(/\s+/)[0] ?? "";
2579
2629
  return slugOf(first.split("/")[0]);
2580
2630
  }
2581
- function originForClient(client, serverVersion2, userAgent) {
2631
+ function isRuntimeUserAgent(userAgent) {
2632
+ const product = userAgentProduct(userAgent);
2633
+ return product === "" || RUNTIME_PRODUCTS.has(product);
2634
+ }
2635
+ function initializeClientSlug(name2) {
2636
+ const slug = slugOf(name2);
2637
+ return isOwnSlug(slug) || GENERIC_CLIENT_NAMES.has(slug) ? "" : slug;
2638
+ }
2639
+ function referrerSlug(referrer) {
2640
+ let host;
2641
+ try {
2642
+ host = new URL(referrer ?? "").hostname.toLowerCase().replace(/\.$/, "");
2643
+ } catch {
2644
+ return "";
2645
+ }
2646
+ if (!host.includes(".") || host.endsWith(".localhost") || host.endsWith(".local") || /^[\d.]+$/.test(host) || host.startsWith("[")) {
2647
+ return "";
2648
+ }
2649
+ return slugOf(host.replace(/^www\./, ""));
2650
+ }
2651
+ function originForClient(client, serverVersion2, userAgent, signals = {}) {
2652
+ const declared = slugOf(signals.declaredOrigin);
2582
2653
  const slug = slugOf(client?.name);
2583
- const own = slug === "" || slug === "fastmcp" || slug === "firecrawl-fastmcp";
2584
- const product = own ? userAgentProduct(userAgent) : "";
2585
- const base = !own ? `mcp-${slug}` : product ? `mcp-ua-${product}` : ORIGIN_SERVER;
2654
+ const product = userAgentProduct(userAgent);
2655
+ const remembered = initializeClientSlug(signals.initializeClient?.name);
2656
+ const referrer = referrerSlug(signals.referrer);
2657
+ const base = !isOwnSlug(declared) ? `mcp-${declared}` : !isOwnSlug(slug) ? `mcp-${slug}` : product && !RUNTIME_PRODUCTS.has(product) ? `mcp-ua-${product}` : remembered ? `mcp-${remembered}` : referrer ? `mcp-ref-${referrer}` : product ? `mcp-ua-${product}` : ORIGIN_SERVER;
2586
2658
  return serverVersion2 ? `${base}@${serverVersion2}` : base;
2587
2659
  }
2588
2660
  function originHeaders(origin) {
@@ -2595,7 +2667,12 @@ function requestOrigin(client, session) {
2595
2667
  return originForClient(
2596
2668
  client?.version ?? null,
2597
2669
  serverVersion,
2598
- session?.clientUserAgent
2670
+ session?.clientUserAgent,
2671
+ {
2672
+ declaredOrigin: session?.clientOrigin,
2673
+ initializeClient: session?.initializeClient,
2674
+ referrer: session?.clientReferrer
2675
+ }
2599
2676
  );
2600
2677
  }
2601
2678
 
@@ -3672,7 +3749,7 @@ var searchToolBaseFields = {
3672
3749
  location: z11.string().optional(),
3673
3750
  includeDomains: z11.array(searchDomainSchema).optional().describe("Hostnames to restrict results to. Mutually exclusive with excludeDomains."),
3674
3751
  excludeDomains: z11.array(searchDomainSchema).optional().describe("Hostnames to leave out of results. Mutually exclusive with includeDomains."),
3675
- sources: z11.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.'),
3752
+ sources: z11.array(searchSourceSchema).optional().describe("Search sources; authenticated sessions default to web + alexandria, keyless sessions to web only. " + ALEXANDRIA_SOURCES_ALWAYS_ON + ' enterprise zdr or anon searches do not add it. Use ["alexandria"] alone for provider discovery without web results.'),
3676
3753
  categories: z11.array(z11.enum(["research", "pdf", "developer", "gov"])).optional().describe(
3677
3754
  "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; `gov` searches the Government Index of US federal, state, and local legal and regulatory sources, the index behind firecrawl_gov_search, and cannot be combined with other categories; gov hits can include historical versions, bill text, agency guidance, and third-party reproductions, so verify the issuer, jurisdiction, enacted/effective status, and version before citing current governing law, clarify a missing jurisdiction or ambiguous as-of date, and use general search for non-US questions or missing coverage (no hits does not establish that no applicable law exists). `developer` and `gov` return hits in `data.web` with `category` set to their name; the other categories also filter `data.web`."
3678
3755
  ),
@@ -3814,7 +3891,7 @@ async function relayExchangeError(run, context) {
3814
3891
  });
3815
3892
  }
3816
3893
  }
3817
- async function postSearchWithFallback(client, body, implicitTools) {
3894
+ async function postSearchWithFallback(client, body, implicitTools, fallbackSources = ["web"]) {
3818
3895
  try {
3819
3896
  return await client.http.post("/v2/search", body);
3820
3897
  } catch (error2) {
@@ -3827,7 +3904,7 @@ async function postSearchWithFallback(client, body, implicitTools) {
3827
3904
  if (!implicitTools || response?.status !== 403 || !unavailable) throw error2;
3828
3905
  return client.http.post("/v2/search", {
3829
3906
  ...body,
3830
- sources: ["web"],
3907
+ sources: fallbackSources,
3831
3908
  domainTools: false
3832
3909
  });
3833
3910
  }
@@ -3887,7 +3964,7 @@ var ConsoleLogger = class {
3887
3964
  }
3888
3965
  }
3889
3966
  };
3890
- 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. For a legal or regulatory question, firecrawl_gov_search searches primary law and regulatory material from US federal, state, and local government sources: statutes, regulations, codes, court opinions, and other government publications; firecrawl_search with categories: ["gov"] reaches the same sources and cannot be combined with other categories. firecrawl_search with sources: [{type: "alexandria"}] returns compact tool summaries in data.tools; toolDetail: "full" includes contracts, firecrawl_find_tools starts with categories, lists providers, then compact tools, and expands the selected full contract, and firecrawl_scrape with alexandria: [{provider, capability, options}] executes up to ten capabilities and returns their results. Alexandria access needs an API key on a team with it enabled. Provide only the required inputs and account for stated network or external side effects.`;
3967
+ 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_ALWAYS_ON} 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. For a legal or regulatory question, firecrawl_gov_search searches primary law and regulatory material from US federal, state, and local government sources: statutes, regulations, codes, court opinions, and other government publications; firecrawl_search with categories: ["gov"] reaches the same sources and cannot be combined with other categories. firecrawl_search with sources: [{type: "alexandria"}] returns compact tool summaries in data.tools; toolDetail: "full" includes contracts, firecrawl_find_tools starts with categories, lists providers, then compact tools, and expands the selected full contract, and firecrawl_scrape with alexandria: [{provider, capability, options}] executes up to ten capabilities and returns their results. Alexandria access needs an API key on a team with it enabled. Provide only the required inputs and account for stated network or external side effects.`;
3891
3968
  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 legal or regulatory questions, firecrawl_search with categories: ["gov"] searches US government legal and regulatory sources and cannot be combined with other categories. 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.`;
3892
3969
  function deprecatedExtractPayload() {
3893
3970
  return {
@@ -3948,10 +4025,18 @@ function createFirecrawlMcpServer(options = {}) {
3948
4025
  const cached = request?.[authResultByRequest];
3949
4026
  if (cached) return cached;
3950
4027
  const authResult = authenticateSession(request).then((session) => {
3951
- const userAgent = request?.headers?.["user-agent"];
4028
+ const headers = request?.headers;
4029
+ const userAgent = headers?.["user-agent"];
3952
4030
  if (typeof userAgent === "string" && userAgent !== "") {
3953
4031
  session.clientUserAgent = userAgent;
3954
4032
  }
4033
+ const declaredOrigin = normalizeHeader(headers?.[DECLARED_ORIGIN_HEADER]);
4034
+ if (declaredOrigin) session.clientOrigin = declaredOrigin;
4035
+ const referrer = [
4036
+ normalizeHeader(headers?.["origin"]),
4037
+ normalizeHeader(headers?.["referer"])
4038
+ ].find((value) => value && value !== "null");
4039
+ if (referrer) session.clientReferrer = referrer;
3955
4040
  return session;
3956
4041
  });
3957
4042
  if (request) request[authResultByRequest] = authResult;
@@ -4556,7 +4641,7 @@ Scrape one URL and return its content: markdown by default, or HTML, links, scre
4556
4641
 
4557
4642
  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. ${SAFE_MODE ? "A named browser profile loads saved session data without saving changes to it." : "Browser actions can change the live page when interactive actions are enabled."} Authenticated responses can include a \`metadata.scrapeId\` for optional scrape feedback.
4558
4643
 
4559
- 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.
4644
+ On an authenticated session with Alexandria access, \`firecrawl_search\` 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.
4560
4645
 
4561
4646
  Alexandria mode, on an authenticated session with Alexandria access: \`alexandria\` selects catalogued capability execution and is mutually exclusive with \`url\`.
4562
4647
  `,
@@ -4686,11 +4771,13 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
4686
4771
  ),
4687
4772
  execute: async (args, { session, log, client: mcpClient }) => {
4688
4773
  const { query, ...opts } = args;
4774
+ const requestedSources = normalizeSearchSources(
4775
+ Array.isArray(opts.sources) && opts.sources.length ? opts.sources : ["web"]
4776
+ );
4777
+ const addsAlexandria = hasCredential(session) && enterpriseAllowsAlexandria(opts.enterprise) && !hasAlexandria(requestedSources);
4689
4778
  const searchOpts = {
4690
4779
  ...opts,
4691
- sources: normalizeSearchSources(
4692
- opts.sources ?? (hasCredential(session) ? ["web", "alexandria"] : ["web"])
4693
- )
4780
+ sources: addsAlexandria ? withAlexandriaSource(requestedSources) : requestedSources
4694
4781
  };
4695
4782
  searchOpts.domainTools ??= defaultDomainTools(searchOpts.sources);
4696
4783
  searchOpts.toolDetail ??= "compact";
@@ -4720,7 +4807,8 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
4720
4807
  const postSearch = () => postSearchWithFallback(
4721
4808
  client,
4722
4809
  searchBody,
4723
- opts.sources === void 0 && opts.domainTools !== true
4810
+ addsAlexandria && opts.domainTools !== true,
4811
+ requestedSources
4724
4812
  );
4725
4813
  const httpRes = await relayExchangeError(postSearch, { tool: "firecrawl_search" });
4726
4814
  return structuredCompact(withoutDiscoveredToolPrices(httpRes?.data ?? {}));
@@ -5723,15 +5811,19 @@ Set \`redactPII\` to request redaction of personally identifiable information in
5723
5811
  export {
5724
5812
  ALEXANDRIA_CATALOGUE_VERTICALS,
5725
5813
  ALEXANDRIA_SEARCH_INSTRUCTIONS,
5814
+ DECLARED_ORIGIN_HEADER,
5726
5815
  keylessToolNames as KEYLESS_TOOL_NAMES,
5727
5816
  SEARCH_DOMAINS_CONFLICT_MESSAGE,
5728
5817
  UserError2 as UserError,
5729
5818
  assertExchangeCredential,
5730
5819
  createFirecrawlMcpServer,
5731
5820
  defaultDomainTools,
5821
+ enterpriseAllowsAlexandria,
5732
5822
  hasAlexandria,
5823
+ initializeClientSlug,
5733
5824
  invalidApiKeyRecoveryPayload,
5734
5825
  invalidOAuthRecoveryPayload,
5826
+ isRuntimeUserAgent,
5735
5827
  keylessQuotaReason,
5736
5828
  keylessSignupUrlFrom,
5737
5829
  normalizeHeader,
@@ -5748,5 +5840,6 @@ export {
5748
5840
  searchQueryIsValid,
5749
5841
  searchToolBaseFields,
5750
5842
  structuredCompactResult as structuredCompact,
5843
+ withAlexandriaSource,
5751
5844
  withoutTrailingSlash
5752
5845
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firecrawl-mcp",
3
- "version": "3.30.2",
3
+ "version": "3.31.0",
4
4
  "description": "MCP server for Firecrawl — search, scrape, and interact with the web, and search scientific papers. Supports both cloud and self-hosted instances. Features include web search, scraping, page interaction, batch processing, LLM-powered content analysis, and research paper search over biomedical and arXiv literature (PubMed, bioRxiv, medRxiv, arXiv) with citation-graph expansion and full-text reading.",
5
5
  "type": "module",
6
6
  "mcpName": "io.github.firecrawl/firecrawl-mcp-server",