firecrawl-mcp 3.26.0 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -686,6 +686,38 @@ function extractSingleTrustedClientIp(rawForwardedFor) {
686
686
  return net.isIP(candidate) ? candidate : void 0;
687
687
  }
688
688
 
689
+ // src/keyless-signup-link.ts
690
+ var SIGNUP_HOSTS = /* @__PURE__ */ new Set(["firecrawl.dev", "www.firecrawl.dev"]);
691
+ var TOKEN_PATH = /^\/k\/[0-9abcdefghjkmnpqrstvwxyz]{12}$/;
692
+ var SURFACES = /* @__PURE__ */ new Set(["api", "mcp", "cli"]);
693
+ var SIGNIN_PARAMS = /* @__PURE__ */ new Set(["utm_source", "utm_medium", "redirect"]);
694
+ var SIGNIN_REDIRECT = "/app/api-keys";
695
+ function checkKeylessSignupUrl(value) {
696
+ if (typeof value !== "string") return { ok: false, firecrawlHost: false };
697
+ let url;
698
+ try {
699
+ url = new URL(value);
700
+ } catch {
701
+ return { ok: false, firecrawlHost: false };
702
+ }
703
+ const firecrawlHost = SIGNUP_HOSTS.has(url.hostname);
704
+ if (url.protocol !== "https:" || !firecrawlHost || url.port || url.username || url.password || url.hash) {
705
+ return { ok: false, firecrawlHost };
706
+ }
707
+ if (TOKEN_PATH.test(url.pathname) && !url.search) {
708
+ return { ok: true, url: value };
709
+ }
710
+ if (url.pathname === "/signin") {
711
+ const params = url.searchParams;
712
+ const keys = [...params.keys()];
713
+ const known = keys.every((key) => SIGNIN_PARAMS.has(key)) && new Set(keys).size === keys.length;
714
+ if (known && params.get("utm_source") === "keyless" && SURFACES.has(params.get("utm_medium") ?? "") && (!params.has("redirect") || params.get("redirect") === SIGNIN_REDIRECT)) {
715
+ return { ok: true, url: value };
716
+ }
717
+ }
718
+ return { ok: false, firecrawlHost };
719
+ }
720
+
689
721
  // src/monitor.ts
690
722
  import { z as z5 } from "zod";
691
723
 
@@ -2446,16 +2478,36 @@ function isHostedKeylessSession(session) {
2446
2478
  function isLocalKeylessStartup() {
2447
2479
  return process.env.CLOUD_SERVICE !== "true" && !isHttpStreamingTransport() && !resolveCredentialFromEnv() && !normalizeHeader(process.env.FIRECRAWL_API_URL);
2448
2480
  }
2449
- var KEYLESS_ACCOUNT_FIX = "Fix: Create an API key at https://www.firecrawl.dev/app/api-keys, then:\n- Set the header: Authorization: Bearer YOUR_API_KEY on https://mcp.firecrawl.dev/v2/mcp\nThen start a new session.";
2450
- var KEYLESS_QUOTA_MESSAGE = `You've hit Firecrawl's free MCP rate limit. To continue using without limits, create a Firecrawl API key.
2481
+ var KEYLESS_SIGNUP_FALLBACK_URL = "https://www.firecrawl.dev/signin?utm_source=keyless&utm_medium=mcp&redirect=%2Fapp%2Fapi-keys";
2482
+ var droppedKeylessSignupUrls = /* @__PURE__ */ new Set();
2483
+ function keylessSignupUrlFrom(value) {
2484
+ const check = checkKeylessSignupUrl(value);
2485
+ if (check.ok) return check.url;
2486
+ if (check.firecrawlHost && typeof value === "string" && droppedKeylessSignupUrls.size < 50 && !droppedKeylessSignupUrls.has(value)) {
2487
+ droppedKeylessSignupUrls.add(value);
2488
+ console.warn(
2489
+ "[WARN]",
2490
+ (/* @__PURE__ */ new Date()).toISOString(),
2491
+ "Ignoring an unrecognized Firecrawl keyless signup link from the API; using the fallback",
2492
+ { signupUrl: value }
2493
+ );
2494
+ }
2495
+ return void 0;
2496
+ }
2497
+ function keylessAccountFix(signupUrl) {
2498
+ return `Fix: Create an API key at ${signupUrl} and then:
2499
+ - Set the header: Authorization: Bearer YOUR_API_KEY on https://mcp.firecrawl.dev/v2/mcp
2500
+ Then start a new session.`;
2501
+ }
2502
+ var keylessQuotaMessage = (signupUrl) => `You've hit Firecrawl's free MCP rate limit. To continue using without limits, create a Firecrawl API key.
2451
2503
 
2452
- ${KEYLESS_ACCOUNT_FIX}`;
2453
- var KEYLESS_TOOL_MESSAGE = `This tool needs a Firecrawl account.
2504
+ ${keylessAccountFix(signupUrl)}`;
2505
+ var keylessToolMessage = (signupUrl) => `This tool needs a Firecrawl account.
2454
2506
 
2455
- ${KEYLESS_ACCOUNT_FIX}`;
2456
- var KEYLESS_ACCESS_MESSAGE = `Anonymous keyless access is unavailable for this request.
2507
+ ${keylessAccountFix(signupUrl)}`;
2508
+ var keylessAccessMessage = (signupUrl) => `Anonymous keyless access is unavailable for this request.
2457
2509
 
2458
- ${KEYLESS_ACCOUNT_FIX}`;
2510
+ ${keylessAccountFix(signupUrl)}`;
2459
2511
  var INVALID_API_KEY_MESSAGE = "The Firecrawl API key is invalid or revoked.\nFix: Replace the key on the existing Firecrawl MCP server, then start a new session. Get an API key at https://www.firecrawl.dev/app/api-keys";
2460
2512
  var INVALID_OAUTH_MESSAGE = "This Firecrawl account connection is no longer valid.\nFix: Reconnect the existing Firecrawl server in the client, or set that existing server URL to https://mcp.firecrawl.dev/v2/mcp-oauth, then start a new session.";
2461
2513
  function connectionRecoveryPayload(params) {
@@ -2512,6 +2564,7 @@ ${agentHintsText(hints)}` : message,
2512
2564
  }
2513
2565
  function recoveryPayload(code, requestId = randomUUID(), options = {}) {
2514
2566
  const retryAfterSeconds = options.retryAfterSeconds;
2567
+ const signupUrl = options.signupUrl ?? KEYLESS_SIGNUP_FALLBACK_URL;
2515
2568
  const isQuotaExhausted = code === "KEYLESS_QUOTA_EXHAUSTED" || code === "KEYLESS_LIMIT_REACHED";
2516
2569
  const isToolUnavailable = code === "KEYLESS_TOOL_NOT_AVAILABLE";
2517
2570
  const isKeylessAccessUnavailable = code === "KEYLESS_ACCESS_NOT_AVAILABLE";
@@ -2521,7 +2574,7 @@ function recoveryPayload(code, requestId = randomUUID(), options = {}) {
2521
2574
  code,
2522
2575
  request_id: requestId,
2523
2576
  auth_mode: code === "CREDENTIAL_INVALID" ? "credential_error" : "keyless",
2524
- message: code === "CREDENTIAL_INVALID" ? INVALID_API_KEY_MESSAGE : isQuotaExhausted ? KEYLESS_QUOTA_MESSAGE : isToolUnavailable ? KEYLESS_TOOL_MESSAGE : isKeylessAccessUnavailable ? KEYLESS_ACCESS_MESSAGE : isKeylessEligibilityUnavailable ? "The anonymous keyless eligibility check is temporarily unavailable. Retry shortly." : "This tool requires a Firecrawl account or API key.",
2577
+ message: code === "CREDENTIAL_INVALID" ? INVALID_API_KEY_MESSAGE : isQuotaExhausted ? keylessQuotaMessage(signupUrl) : isToolUnavailable ? keylessToolMessage(signupUrl) : isKeylessAccessUnavailable ? keylessAccessMessage(signupUrl) : isKeylessEligibilityUnavailable ? "The anonymous keyless eligibility check is temporarily unavailable. Retry shortly." : "This tool requires a Firecrawl account or API key.",
2525
2578
  // CREDENTIAL_INVALID sessions gate every tool call (including keyless
2526
2579
  // tools) on the credentialError check before the keyless branch ever
2527
2580
  // runs, so none of KEYLESS_TOOL_NAMES are actually callable here. Listing
@@ -2530,6 +2583,7 @@ function recoveryPayload(code, requestId = randomUUID(), options = {}) {
2530
2583
  // tools, and ineligible access omit available_tools and next_actions for
2531
2584
  // the same reason: those fields retry tools that cannot clear the error.
2532
2585
  ...isKeylessConversion || code === "CREDENTIAL_INVALID" ? {} : { available_tools: [...KEYLESS_TOOL_NAMES] },
2586
+ ...isKeylessConversion ? { signup_url: signupUrl } : {},
2533
2587
  docs_url: MCP_CONNECTION_GUIDE_URL,
2534
2588
  ...retryAfterSeconds ? { retry_after_seconds: retryAfterSeconds } : {},
2535
2589
  ...isKeylessEligibilityUnavailable ? { next_actions: [{ kind: "retry_later", after_seconds: 30 }] } : {}
@@ -2630,7 +2684,9 @@ function guardHostedTool(tool, { logActions }) {
2630
2684
  const code = session?.credentialError ? "CREDENTIAL_INVALID" : isHostedKeylessSession(session) && !keylessTool ? "KEYLESS_TOOL_NOT_AVAILABLE" : void 0;
2631
2685
  if (code) {
2632
2686
  const requestId = randomUUID();
2633
- const payload2 = recoveryPayload(code, requestId);
2687
+ const payload2 = recoveryPayload(code, requestId, {
2688
+ signupUrl: code === "KEYLESS_TOOL_NOT_AVAILABLE" ? await hostedKeylessSignupUrl(session) : void 0
2689
+ });
2634
2690
  if (logActions) {
2635
2691
  emitActionLog(tool.name, "error", session, new UserError2(String(payload2.message), payload2), requestId, code);
2636
2692
  }
@@ -2674,7 +2730,9 @@ function guardHostedTool(tool, { logActions }) {
2674
2730
  }
2675
2731
  if (isHostedKeylessSession(invocationSession) && !keylessTool) {
2676
2732
  const code = "KEYLESS_TOOL_NOT_AVAILABLE";
2677
- const payload = recoveryPayload(code, requestId);
2733
+ const payload = recoveryPayload(code, requestId, {
2734
+ signupUrl: await hostedKeylessSignupUrl(invocationSession)
2735
+ });
2678
2736
  if (logActions) emitActionLog(tool.name, "error", invocationSession, new UserError2(String(payload.message), payload), requestId, code);
2679
2737
  throw new UserError2(String(payload.message), payload);
2680
2738
  }
@@ -3579,12 +3637,12 @@ function extractClientIp(request) {
3579
3637
  function keylessQuotaReason(reason) {
3580
3638
  return reason === "requests" || reason === "credits";
3581
3639
  }
3582
- async function keylessEligible(clientIp, origin) {
3640
+ async function keylessEligible(clientIp, origin, { signupLink = false } = {}) {
3583
3641
  const secret = process.env.KEYLESS_PROXY_SECRET;
3584
3642
  if (!secret) return { eligible: false, unavailable: true };
3585
3643
  try {
3586
3644
  const response = await fetch(
3587
- `${resolveApiBaseUrl()}/v2/keyless/eligibility`,
3645
+ `${resolveApiBaseUrl()}/v2/keyless/eligibility${signupLink ? "?signup_link=1" : ""}`,
3588
3646
  {
3589
3647
  headers: {
3590
3648
  ...originHeaders(origin),
@@ -3601,12 +3659,22 @@ async function keylessEligible(clientIp, origin) {
3601
3659
  return {
3602
3660
  eligible: json?.eligible === true,
3603
3661
  ...typeof json?.reason === "string" ? { reason: json.reason } : {},
3604
- ...Number.isFinite(json?.retryAfterSeconds) && json.retryAfterSeconds > 0 ? { retryAfterSeconds: json.retryAfterSeconds } : {}
3662
+ ...Number.isFinite(json?.retryAfterSeconds) && json.retryAfterSeconds > 0 ? { retryAfterSeconds: json.retryAfterSeconds } : {},
3663
+ ...keylessSignupUrlFrom(json?.signupUrl) ? { signupUrl: json.signupUrl } : {}
3605
3664
  };
3606
3665
  } catch {
3607
3666
  return { eligible: false, unavailable: true };
3608
3667
  }
3609
3668
  }
3669
+ async function hostedKeylessSignupUrl(session) {
3670
+ if (!session?.keylessClientIp) return void 0;
3671
+ const eligibility = await keylessEligible(
3672
+ session.keylessClientIp,
3673
+ requestOrigin(void 0, session),
3674
+ { signupLink: true }
3675
+ );
3676
+ return eligibility.signupUrl;
3677
+ }
3610
3678
  function isKeylessMode(session) {
3611
3679
  if (hasCredential(session) || session?.credentialError) return false;
3612
3680
  if (process.env.CLOUD_SERVICE === "true") {
@@ -3621,7 +3689,8 @@ async function keylessPost(path2, body, session, originOverride) {
3621
3689
  if (!eligibility.eligible) {
3622
3690
  const code = eligibility.unavailable ? "KEYLESS_ELIGIBILITY_UNAVAILABLE" : keylessQuotaReason(eligibility.reason) ? "KEYLESS_QUOTA_EXHAUSTED" : "KEYLESS_ACCESS_NOT_AVAILABLE";
3623
3691
  const payload = recoveryPayload(code, session?.requestId, {
3624
- retryAfterSeconds: eligibility.retryAfterSeconds
3692
+ retryAfterSeconds: eligibility.retryAfterSeconds,
3693
+ signupUrl: eligibility.signupUrl
3625
3694
  });
3626
3695
  throw new UserError2(String(payload.message), payload);
3627
3696
  }
@@ -3645,7 +3714,8 @@ async function keylessPost(path2, body, session, originOverride) {
3645
3714
  if (isKeylessMode(session) && response.status === 429) {
3646
3715
  const code = keylessQuotaReason(json?.reason) ? "KEYLESS_QUOTA_EXHAUSTED" : "KEYLESS_LIMIT_REACHED";
3647
3716
  const payload = recoveryPayload(code, session?.requestId, {
3648
- retryAfterSeconds: Number.isFinite(json?.retry_after_seconds) && json.retry_after_seconds > 0 ? json.retry_after_seconds : void 0
3717
+ retryAfterSeconds: Number.isFinite(json?.retry_after_seconds) && json.retry_after_seconds > 0 ? json.retry_after_seconds : void 0,
3718
+ signupUrl: keylessSignupUrlFrom(json?.signup_url)
3649
3719
  });
3650
3720
  const hints = readAgentHints(json);
3651
3721
  if (hints) payload.agent_hints = hints;
@@ -0,0 +1,34 @@
1
+ // src/keyless-signup-link.ts
2
+ var SIGNUP_HOSTS = /* @__PURE__ */ new Set(["firecrawl.dev", "www.firecrawl.dev"]);
3
+ var TOKEN_PATH = /^\/k\/[0-9abcdefghjkmnpqrstvwxyz]{12}$/;
4
+ var SURFACES = /* @__PURE__ */ new Set(["api", "mcp", "cli"]);
5
+ var SIGNIN_PARAMS = /* @__PURE__ */ new Set(["utm_source", "utm_medium", "redirect"]);
6
+ var SIGNIN_REDIRECT = "/app/api-keys";
7
+ function checkKeylessSignupUrl(value) {
8
+ if (typeof value !== "string") return { ok: false, firecrawlHost: false };
9
+ let url;
10
+ try {
11
+ url = new URL(value);
12
+ } catch {
13
+ return { ok: false, firecrawlHost: false };
14
+ }
15
+ const firecrawlHost = SIGNUP_HOSTS.has(url.hostname);
16
+ if (url.protocol !== "https:" || !firecrawlHost || url.port || url.username || url.password || url.hash) {
17
+ return { ok: false, firecrawlHost };
18
+ }
19
+ if (TOKEN_PATH.test(url.pathname) && !url.search) {
20
+ return { ok: true, url: value };
21
+ }
22
+ if (url.pathname === "/signin") {
23
+ const params = url.searchParams;
24
+ const keys = [...params.keys()];
25
+ const known = keys.every((key) => SIGNIN_PARAMS.has(key)) && new Set(keys).size === keys.length;
26
+ if (known && params.get("utm_source") === "keyless" && SURFACES.has(params.get("utm_medium") ?? "") && (!params.has("redirect") || params.get("redirect") === SIGNIN_REDIRECT)) {
27
+ return { ok: true, url: value };
28
+ }
29
+ }
30
+ return { ok: false, firecrawlHost };
31
+ }
32
+ export {
33
+ checkKeylessSignupUrl
34
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firecrawl-mcp",
3
- "version": "3.26.0",
3
+ "version": "3.27.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",