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 +85 -15
- package/dist/keyless-signup-link.js +34 -0
- package/package.json +1 -1
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
|
|
2450
|
-
var
|
|
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
|
-
${
|
|
2453
|
-
var
|
|
2504
|
+
${keylessAccountFix(signupUrl)}`;
|
|
2505
|
+
var keylessToolMessage = (signupUrl) => `This tool needs a Firecrawl account.
|
|
2454
2506
|
|
|
2455
|
-
${
|
|
2456
|
-
var
|
|
2507
|
+
${keylessAccountFix(signupUrl)}`;
|
|
2508
|
+
var keylessAccessMessage = (signupUrl) => `Anonymous keyless access is unavailable for this request.
|
|
2457
2509
|
|
|
2458
|
-
${
|
|
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 ?
|
|
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.
|
|
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",
|