firecrawl-mcp 3.26.0 → 3.27.1

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
@@ -1283,10 +1283,13 @@ The existing `firecrawl_feedback` tool accepts `endpoint: "alexandria"`:
1283
1283
  "url": "https://example.com",
1284
1284
  "requestedFunctionality": "Find records and download their attachments"
1285
1285
  },
1286
+ "objective": "Compare contract requirements across agencies before bidding",
1286
1287
  "rationale": "Found summaries but could not retrieve attachments"
1287
1288
  }
1288
1289
  ```
1289
1290
 
1291
+ `objective` is the underlying goal of the session: what the agent or its user was ultimately trying to accomplish, beyond the single website in `requestedWebsite`.
1292
+
1290
1293
  This uses authenticated `POST /v2/feedback`, without a job ID, job-age deadline, or credit refund. Optional `providerFeedback` and `capabilityFeedback` arrays describe coverage gaps and execution issues; the tool schema lists supported issue values. A `new_capability_request` requires `requestedFunctionality`; `missing_capability` (the provider exists but lacks the capability) does not. Existing feedback opt-out and authentication controls apply.
1291
1294
 
1292
1295
  Eligible Alexandria execution and discovery results include a `feedbackTool` pointer with the tool name and a skeleton of the arguments. The pointer is omitted for Firecrawl-internal calls such as `bash` and when `firecrawl_feedback` is not registered (`FIRECRAWL_NO_ENDPOINT_FEEDBACK` or keyless startup).
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ var alexandriaFeedbackFields = {
10
10
  requestedFunctionality: detail
11
11
  }).optional(),
12
12
  rationale: detail.optional(),
13
+ objective: detail.optional(),
13
14
  providerFeedback: z.array(
14
15
  z.strictObject({
15
16
  name,
@@ -55,6 +56,7 @@ var ALEXANDRIA_FEEDBACK_HINT = {
55
56
  url: "<website the user needed data from>",
56
57
  requestedFunctionality: "<what they needed from it>"
57
58
  },
59
+ objective: "<underlying goal of the task>",
58
60
  rationale: "<why this rating, from observed results>",
59
61
  providerFeedback: "<optional [{name, issue, why}]: providers that were missing, thin, or unavailable>",
60
62
  capabilityFeedback: "<optional [{name, provider, issue, why, requestedFunctionality?}]: capabilities that were missing, wrong, or failed>"
@@ -74,7 +76,8 @@ var alexandriaSessionFeedbackSchema = z.strictObject({
74
76
  rating: z.enum(["good", "bad", "partial"]),
75
77
  ...alexandriaFeedbackFields,
76
78
  requestedWebsite: alexandriaFeedbackFields.requestedWebsite.unwrap(),
77
- rationale: detail
79
+ rationale: detail,
80
+ objective: detail
78
81
  });
79
82
 
80
83
  // src/index.ts
@@ -686,6 +689,38 @@ function extractSingleTrustedClientIp(rawForwardedFor) {
686
689
  return net.isIP(candidate) ? candidate : void 0;
687
690
  }
688
691
 
692
+ // src/keyless-signup-link.ts
693
+ var SIGNUP_HOSTS = /* @__PURE__ */ new Set(["firecrawl.dev", "www.firecrawl.dev"]);
694
+ var TOKEN_PATH = /^\/k\/[0-9abcdefghjkmnpqrstvwxyz]{12}$/;
695
+ var SURFACES = /* @__PURE__ */ new Set(["api", "mcp", "cli"]);
696
+ var SIGNIN_PARAMS = /* @__PURE__ */ new Set(["utm_source", "utm_medium", "redirect"]);
697
+ var SIGNIN_REDIRECT = "/app/api-keys";
698
+ function checkKeylessSignupUrl(value) {
699
+ if (typeof value !== "string") return { ok: false, firecrawlHost: false };
700
+ let url;
701
+ try {
702
+ url = new URL(value);
703
+ } catch {
704
+ return { ok: false, firecrawlHost: false };
705
+ }
706
+ const firecrawlHost = SIGNUP_HOSTS.has(url.hostname);
707
+ if (url.protocol !== "https:" || !firecrawlHost || url.port || url.username || url.password || url.hash) {
708
+ return { ok: false, firecrawlHost };
709
+ }
710
+ if (TOKEN_PATH.test(url.pathname) && !url.search) {
711
+ return { ok: true, url: value };
712
+ }
713
+ if (url.pathname === "/signin") {
714
+ const params = url.searchParams;
715
+ const keys = [...params.keys()];
716
+ const known = keys.every((key) => SIGNIN_PARAMS.has(key)) && new Set(keys).size === keys.length;
717
+ if (known && params.get("utm_source") === "keyless" && SURFACES.has(params.get("utm_medium") ?? "") && (!params.has("redirect") || params.get("redirect") === SIGNIN_REDIRECT)) {
718
+ return { ok: true, url: value };
719
+ }
720
+ }
721
+ return { ok: false, firecrawlHost };
722
+ }
723
+
689
724
  // src/monitor.ts
690
725
  import { z as z5 } from "zod";
691
726
 
@@ -2446,16 +2481,36 @@ function isHostedKeylessSession(session) {
2446
2481
  function isLocalKeylessStartup() {
2447
2482
  return process.env.CLOUD_SERVICE !== "true" && !isHttpStreamingTransport() && !resolveCredentialFromEnv() && !normalizeHeader(process.env.FIRECRAWL_API_URL);
2448
2483
  }
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.
2484
+ var KEYLESS_SIGNUP_FALLBACK_URL = "https://www.firecrawl.dev/signin?utm_source=keyless&utm_medium=mcp&redirect=%2Fapp%2Fapi-keys";
2485
+ var droppedKeylessSignupUrls = /* @__PURE__ */ new Set();
2486
+ function keylessSignupUrlFrom(value) {
2487
+ const check = checkKeylessSignupUrl(value);
2488
+ if (check.ok) return check.url;
2489
+ if (check.firecrawlHost && typeof value === "string" && droppedKeylessSignupUrls.size < 50 && !droppedKeylessSignupUrls.has(value)) {
2490
+ droppedKeylessSignupUrls.add(value);
2491
+ console.warn(
2492
+ "[WARN]",
2493
+ (/* @__PURE__ */ new Date()).toISOString(),
2494
+ "Ignoring an unrecognized Firecrawl keyless signup link from the API; using the fallback",
2495
+ { signupUrl: value }
2496
+ );
2497
+ }
2498
+ return void 0;
2499
+ }
2500
+ function keylessAccountFix(signupUrl) {
2501
+ return `Fix: Create an API key at ${signupUrl} and then:
2502
+ - Set the header: Authorization: Bearer YOUR_API_KEY on https://mcp.firecrawl.dev/v2/mcp
2503
+ Then start a new session.`;
2504
+ }
2505
+ var keylessQuotaMessage = (signupUrl) => `You've hit Firecrawl's free MCP rate limit. To continue using without limits, create a Firecrawl API key.
2451
2506
 
2452
- ${KEYLESS_ACCOUNT_FIX}`;
2453
- var KEYLESS_TOOL_MESSAGE = `This tool needs a Firecrawl account.
2507
+ ${keylessAccountFix(signupUrl)}`;
2508
+ var keylessToolMessage = (signupUrl) => `This tool needs a Firecrawl account.
2454
2509
 
2455
- ${KEYLESS_ACCOUNT_FIX}`;
2456
- var KEYLESS_ACCESS_MESSAGE = `Anonymous keyless access is unavailable for this request.
2510
+ ${keylessAccountFix(signupUrl)}`;
2511
+ var keylessAccessMessage = (signupUrl) => `Anonymous keyless access is unavailable for this request.
2457
2512
 
2458
- ${KEYLESS_ACCOUNT_FIX}`;
2513
+ ${keylessAccountFix(signupUrl)}`;
2459
2514
  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
2515
  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
2516
  function connectionRecoveryPayload(params) {
@@ -2512,6 +2567,7 @@ ${agentHintsText(hints)}` : message,
2512
2567
  }
2513
2568
  function recoveryPayload(code, requestId = randomUUID(), options = {}) {
2514
2569
  const retryAfterSeconds = options.retryAfterSeconds;
2570
+ const signupUrl = options.signupUrl ?? KEYLESS_SIGNUP_FALLBACK_URL;
2515
2571
  const isQuotaExhausted = code === "KEYLESS_QUOTA_EXHAUSTED" || code === "KEYLESS_LIMIT_REACHED";
2516
2572
  const isToolUnavailable = code === "KEYLESS_TOOL_NOT_AVAILABLE";
2517
2573
  const isKeylessAccessUnavailable = code === "KEYLESS_ACCESS_NOT_AVAILABLE";
@@ -2521,7 +2577,7 @@ function recoveryPayload(code, requestId = randomUUID(), options = {}) {
2521
2577
  code,
2522
2578
  request_id: requestId,
2523
2579
  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.",
2580
+ 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
2581
  // CREDENTIAL_INVALID sessions gate every tool call (including keyless
2526
2582
  // tools) on the credentialError check before the keyless branch ever
2527
2583
  // runs, so none of KEYLESS_TOOL_NAMES are actually callable here. Listing
@@ -2530,6 +2586,7 @@ function recoveryPayload(code, requestId = randomUUID(), options = {}) {
2530
2586
  // tools, and ineligible access omit available_tools and next_actions for
2531
2587
  // the same reason: those fields retry tools that cannot clear the error.
2532
2588
  ...isKeylessConversion || code === "CREDENTIAL_INVALID" ? {} : { available_tools: [...KEYLESS_TOOL_NAMES] },
2589
+ ...isKeylessConversion ? { signup_url: signupUrl } : {},
2533
2590
  docs_url: MCP_CONNECTION_GUIDE_URL,
2534
2591
  ...retryAfterSeconds ? { retry_after_seconds: retryAfterSeconds } : {},
2535
2592
  ...isKeylessEligibilityUnavailable ? { next_actions: [{ kind: "retry_later", after_seconds: 30 }] } : {}
@@ -2630,7 +2687,9 @@ function guardHostedTool(tool, { logActions }) {
2630
2687
  const code = session?.credentialError ? "CREDENTIAL_INVALID" : isHostedKeylessSession(session) && !keylessTool ? "KEYLESS_TOOL_NOT_AVAILABLE" : void 0;
2631
2688
  if (code) {
2632
2689
  const requestId = randomUUID();
2633
- const payload2 = recoveryPayload(code, requestId);
2690
+ const payload2 = recoveryPayload(code, requestId, {
2691
+ signupUrl: code === "KEYLESS_TOOL_NOT_AVAILABLE" ? await hostedKeylessSignupUrl(session) : void 0
2692
+ });
2634
2693
  if (logActions) {
2635
2694
  emitActionLog(tool.name, "error", session, new UserError2(String(payload2.message), payload2), requestId, code);
2636
2695
  }
@@ -2674,7 +2733,9 @@ function guardHostedTool(tool, { logActions }) {
2674
2733
  }
2675
2734
  if (isHostedKeylessSession(invocationSession) && !keylessTool) {
2676
2735
  const code = "KEYLESS_TOOL_NOT_AVAILABLE";
2677
- const payload = recoveryPayload(code, requestId);
2736
+ const payload = recoveryPayload(code, requestId, {
2737
+ signupUrl: await hostedKeylessSignupUrl(invocationSession)
2738
+ });
2678
2739
  if (logActions) emitActionLog(tool.name, "error", invocationSession, new UserError2(String(payload.message), payload), requestId, code);
2679
2740
  throw new UserError2(String(payload.message), payload);
2680
2741
  }
@@ -3579,12 +3640,12 @@ function extractClientIp(request) {
3579
3640
  function keylessQuotaReason(reason) {
3580
3641
  return reason === "requests" || reason === "credits";
3581
3642
  }
3582
- async function keylessEligible(clientIp, origin) {
3643
+ async function keylessEligible(clientIp, origin, { signupLink = false } = {}) {
3583
3644
  const secret = process.env.KEYLESS_PROXY_SECRET;
3584
3645
  if (!secret) return { eligible: false, unavailable: true };
3585
3646
  try {
3586
3647
  const response = await fetch(
3587
- `${resolveApiBaseUrl()}/v2/keyless/eligibility`,
3648
+ `${resolveApiBaseUrl()}/v2/keyless/eligibility${signupLink ? "?signup_link=1" : ""}`,
3588
3649
  {
3589
3650
  headers: {
3590
3651
  ...originHeaders(origin),
@@ -3601,12 +3662,22 @@ async function keylessEligible(clientIp, origin) {
3601
3662
  return {
3602
3663
  eligible: json?.eligible === true,
3603
3664
  ...typeof json?.reason === "string" ? { reason: json.reason } : {},
3604
- ...Number.isFinite(json?.retryAfterSeconds) && json.retryAfterSeconds > 0 ? { retryAfterSeconds: json.retryAfterSeconds } : {}
3665
+ ...Number.isFinite(json?.retryAfterSeconds) && json.retryAfterSeconds > 0 ? { retryAfterSeconds: json.retryAfterSeconds } : {},
3666
+ ...keylessSignupUrlFrom(json?.signupUrl) ? { signupUrl: json.signupUrl } : {}
3605
3667
  };
3606
3668
  } catch {
3607
3669
  return { eligible: false, unavailable: true };
3608
3670
  }
3609
3671
  }
3672
+ async function hostedKeylessSignupUrl(session) {
3673
+ if (!session?.keylessClientIp) return void 0;
3674
+ const eligibility = await keylessEligible(
3675
+ session.keylessClientIp,
3676
+ requestOrigin(void 0, session),
3677
+ { signupLink: true }
3678
+ );
3679
+ return eligibility.signupUrl;
3680
+ }
3610
3681
  function isKeylessMode(session) {
3611
3682
  if (hasCredential(session) || session?.credentialError) return false;
3612
3683
  if (process.env.CLOUD_SERVICE === "true") {
@@ -3621,7 +3692,8 @@ async function keylessPost(path2, body, session, originOverride) {
3621
3692
  if (!eligibility.eligible) {
3622
3693
  const code = eligibility.unavailable ? "KEYLESS_ELIGIBILITY_UNAVAILABLE" : keylessQuotaReason(eligibility.reason) ? "KEYLESS_QUOTA_EXHAUSTED" : "KEYLESS_ACCESS_NOT_AVAILABLE";
3623
3694
  const payload = recoveryPayload(code, session?.requestId, {
3624
- retryAfterSeconds: eligibility.retryAfterSeconds
3695
+ retryAfterSeconds: eligibility.retryAfterSeconds,
3696
+ signupUrl: eligibility.signupUrl
3625
3697
  });
3626
3698
  throw new UserError2(String(payload.message), payload);
3627
3699
  }
@@ -3645,7 +3717,8 @@ async function keylessPost(path2, body, session, originOverride) {
3645
3717
  if (isKeylessMode(session) && response.status === 429) {
3646
3718
  const code = keylessQuotaReason(json?.reason) ? "KEYLESS_QUOTA_EXHAUSTED" : "KEYLESS_LIMIT_REACHED";
3647
3719
  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
3720
+ retryAfterSeconds: Number.isFinite(json?.retry_after_seconds) && json.retry_after_seconds > 0 ? json.retry_after_seconds : void 0,
3721
+ signupUrl: keylessSignupUrlFrom(json?.signup_url)
3649
3722
  });
3650
3723
  const hints = readAgentHints(json);
3651
3724
  if (hints) payload.agent_hints = hints;
@@ -3882,7 +3955,7 @@ if (alexandriaFeedbackAvailable()) {
3882
3955
  description: `
3883
3956
  Submit concise quality feedback for a completed search, scrape, parse, or map job. Provide the endpoint, job ID, rating, and relevant issue codes or small contextual fields; omit large page contents and raw outputs.
3884
3957
 
3885
- For an Alexandria session, set endpoint to \`alexandria\`, omit jobId, and provide requestedWebsite (url and requestedFunctionality), rationale, and rating. Optional providerFeedback and capabilityFeedback describe gaps or errors. Capability issues: new_capability_request (requires requestedFunctionality), missing_capability, insufficient_functionality, incorrect_result, execution_error, other. Alexandria feedback has no job-age deadline and no credit refund.
3958
+ For an Alexandria session, set endpoint to \`alexandria\`, omit jobId, and provide requestedWebsite (url and requestedFunctionality), objective, rationale, and rating. objective is the underlying goal behind the session: what you or your user were ultimately trying to accomplish (for example, "shortlist federal IT contracts to bid on this quarter"), not only what was needed from this website. Optional providerFeedback and capabilityFeedback describe gaps or errors. Capability issues: new_capability_request (requires requestedFunctionality), missing_capability, insufficient_functionality, incorrect_result, execution_error, other. Alexandria feedback has no job-age deadline and no credit refund.
3886
3959
 
3887
3960
  Returns submission status, feedback ID, and accounting fields.
3888
3961
  `,
@@ -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.1",
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",