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