firecrawl-mcp 3.29.0 → 3.29.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 +13 -3
- package/dist/index.js +38 -326
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -287,11 +287,21 @@ Hosted Firecrawl can issue OAuth **access tokens** (`fco_…`) via the authoriza
|
|
|
287
287
|
|
|
288
288
|
Use **access** tokens (`fco_…`) only. Refresh tokens (`fcr_…`) must be exchanged at the token endpoint, not passed to the scrape/search API.
|
|
289
289
|
|
|
290
|
+
#### Hardened HTTP preset (`CLOUD_SERVICE=true`)
|
|
291
|
+
|
|
292
|
+
`CLOUD_SERVICE=true` serves the tools over streamable HTTP on `PORT` (default `3000`), bound to `0.0.0.0`, with stricter defaults for a shared deployment:
|
|
293
|
+
|
|
294
|
+
- every request must carry a credential (an API key or an OAuth access token);
|
|
295
|
+
- safe mode: scrape is read-only (see the scrape tool below);
|
|
296
|
+
- `firecrawl_parse` accepts uploads only and never reads local files.
|
|
297
|
+
|
|
298
|
+
`FASTMCP_ENDPOINT` selects the surface: `/v2/mcp` (default) serves the full tool set, `/v2/mcp-oauth` the account surface, and `/v2/mcp-search` (which also requires `FIRECRAWL_MCP_SEARCH_OAUTH_ONLY=true`) the [search-only surface](#search-only-surface-hosted) described below.
|
|
299
|
+
|
|
290
300
|
#### Search-only surface (hosted)
|
|
291
301
|
|
|
292
|
-
In hosted mode (`CLOUD_SERVICE=true`) a second in-process instance serves the [search-only endpoint](#search-only-endpoint)
|
|
302
|
+
In hosted mode (`CLOUD_SERVICE=true`) a second in-process instance serves the [search-only endpoint](#search-only-endpoint) on local port `3001`. The hosted deployment's reverse proxy routes `/v2/mcp-search` to that port, and the OAuth protected-resource identifier is `https://mcp.firecrawl.dev/v2/mcp-search`.
|
|
293
303
|
|
|
294
|
-
`FIRECRAWL_MCP_SEARCH_ENABLED` (default `true`) is the supported operational toggle; set it to `false` to prevent the search instance from starting. The Node process also accepts `FIRECRAWL_MCP_SEARCH_PORT`, `FIRECRAWL_MCP_SEARCH_ENDPOINT`, and `FIRECRAWL_MCP_SEARCH_RESOURCE_URL` for isolated tests. Those overrides do not reconfigure the
|
|
304
|
+
`FIRECRAWL_MCP_SEARCH_ENABLED` (default `true`) is the supported operational toggle; set it to `false` to prevent the search instance from starting. The Node process also accepts `FIRECRAWL_MCP_SEARCH_PORT`, `FIRECRAWL_MCP_SEARCH_ENDPOINT`, and `FIRECRAWL_MCP_SEARCH_RESOURCE_URL` for isolated tests. Those overrides do not reconfigure the deployment's proxy routes or the authorization server allowlist and must not be used independently in the hosted deployment.
|
|
295
305
|
|
|
296
306
|
The search instance requires authentication for every request (including `tools/list`) and rejects OAuth tokens whose audience does not match its own resource.
|
|
297
307
|
|
|
@@ -453,7 +463,7 @@ Scrape content from a single URL with advanced options.
|
|
|
453
463
|
|
|
454
464
|
**Branding format:** Extracts comprehensive brand identity (colors, fonts, typography, spacing, logo, UI components) for design analysis or style replication.
|
|
455
465
|
**Privacy:** Set `redactPII: true` to return content with personally identifiable information redacted.
|
|
456
|
-
**
|
|
466
|
+
**Hardened HTTP preset and the hosted server:** With `CLOUD_SERVICE=true`, as on the hosted server, scrape is read-only. It takes no browser `actions` and cannot accept provider terms. A named `profile` loads saved browser state without saving changes to it, and the full endpoint's `firecrawl_search` treats `scrapeOptions.profile` the same way. To save browser state to a profile, open the page with `firecrawl_interact` (see below). An organization admin accepts terms in the dashboard.
|
|
457
467
|
|
|
458
468
|
**Returns:**
|
|
459
469
|
|
package/dist/index.js
CHANGED
|
@@ -3,17 +3,6 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import dotenv from "dotenv";
|
|
5
5
|
|
|
6
|
-
// src/headers.ts
|
|
7
|
-
function normalizeHeader(value) {
|
|
8
|
-
if (value == null) return void 0;
|
|
9
|
-
const v = Array.isArray(value) ? value[0] : value;
|
|
10
|
-
const trimmed = typeof v === "string" ? v.trim() : "";
|
|
11
|
-
return trimmed || void 0;
|
|
12
|
-
}
|
|
13
|
-
function withoutTrailingSlash(value) {
|
|
14
|
-
return value.replace(/\/+$/, "");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
6
|
// src/alexandria-feedback.ts
|
|
18
7
|
import { z } from "zod";
|
|
19
8
|
var detail = z.string().trim().min(1).max(2e3);
|
|
@@ -3530,6 +3519,17 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e
|
|
|
3530
3519
|
});
|
|
3531
3520
|
}
|
|
3532
3521
|
|
|
3522
|
+
// src/headers.ts
|
|
3523
|
+
function normalizeHeader(value) {
|
|
3524
|
+
if (value == null) return void 0;
|
|
3525
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
3526
|
+
const trimmed = typeof v === "string" ? v.trim() : "";
|
|
3527
|
+
return trimmed || void 0;
|
|
3528
|
+
}
|
|
3529
|
+
function withoutTrailingSlash(value) {
|
|
3530
|
+
return value.replace(/\/+$/, "");
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
3533
|
// src/recovery.ts
|
|
3534
3534
|
import { randomUUID } from "crypto";
|
|
3535
3535
|
|
|
@@ -4291,6 +4291,7 @@ function createFirecrawlMcpServer(options2 = {}) {
|
|
|
4291
4291
|
"rawHtml",
|
|
4292
4292
|
"screenshot",
|
|
4293
4293
|
"links",
|
|
4294
|
+
"images",
|
|
4294
4295
|
"summary",
|
|
4295
4296
|
"changeTracking",
|
|
4296
4297
|
"branding",
|
|
@@ -4934,9 +4935,9 @@ For a programming question, add \`categories: ["developer"]\`; its hits return i
|
|
|
4934
4935
|
}
|
|
4935
4936
|
};
|
|
4936
4937
|
server.addTool(findToolsTool);
|
|
4937
|
-
const
|
|
4938
|
+
const DEFAULT_CLOUD_API_URL = "https://api.firecrawl.dev";
|
|
4938
4939
|
function resolveApiBaseUrl() {
|
|
4939
|
-
return (config.apiUrl ||
|
|
4940
|
+
return (config.apiUrl || DEFAULT_CLOUD_API_URL).replace(
|
|
4940
4941
|
/\/$/,
|
|
4941
4942
|
""
|
|
4942
4943
|
);
|
|
@@ -5923,20 +5924,6 @@ function introspectionTtlMs(value, now, activeTtlMs = INTROSPECTION_ACTIVE_TTL_M
|
|
|
5923
5924
|
return Math.min(activeTtlMs, value.exp * 1e3 - now);
|
|
5924
5925
|
}
|
|
5925
5926
|
|
|
5926
|
-
// src/service/keyless-client-ip.ts
|
|
5927
|
-
import net from "net";
|
|
5928
|
-
function extractSingleTrustedClientIp(rawForwardedFor) {
|
|
5929
|
-
if (Array.isArray(rawForwardedFor) && rawForwardedFor.length !== 1) {
|
|
5930
|
-
return void 0;
|
|
5931
|
-
}
|
|
5932
|
-
const raw = Array.isArray(rawForwardedFor) ? rawForwardedFor[0] : rawForwardedFor;
|
|
5933
|
-
if (typeof raw !== "string") return void 0;
|
|
5934
|
-
const parts = raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
5935
|
-
if (parts.length !== 1) return void 0;
|
|
5936
|
-
const candidate = parts[0].replace(/^\[(.*)\]$/, "$1").toLowerCase();
|
|
5937
|
-
return net.isIP(candidate) ? candidate : void 0;
|
|
5938
|
-
}
|
|
5939
|
-
|
|
5940
5927
|
// src/service/session-credential.ts
|
|
5941
5928
|
import { createHmac } from "crypto";
|
|
5942
5929
|
var managedOAuthApiKey = /* @__PURE__ */ Symbol("firecrawlManagedOAuthApiKey");
|
|
@@ -6113,9 +6100,6 @@ function makePrimaryProfile() {
|
|
|
6113
6100
|
}
|
|
6114
6101
|
|
|
6115
6102
|
// src/service/auth.ts
|
|
6116
|
-
function extractClientIp(request) {
|
|
6117
|
-
return extractSingleTrustedClientIp(request?.headers?.["x-forwarded-for"]);
|
|
6118
|
-
}
|
|
6119
6103
|
function extractBearerToken(headers) {
|
|
6120
6104
|
const headerAuth = normalizeHeader(headers["authorization"]);
|
|
6121
6105
|
if (!headerAuth?.toLowerCase().startsWith("bearer ")) return void 0;
|
|
@@ -6128,9 +6112,6 @@ function isFirecrawlOAuthAccessToken(token) {
|
|
|
6128
6112
|
function isFirecrawlApiKey(token) {
|
|
6129
6113
|
return token.startsWith("fc-");
|
|
6130
6114
|
}
|
|
6131
|
-
function isLegacyKeyPathRequest(request) {
|
|
6132
|
-
return normalizeHeader(request?.headers?.["x-firecrawl-key-transport"]) === "path";
|
|
6133
|
-
}
|
|
6134
6115
|
function requestShouldReceiveOAuthChallenge(request, profile) {
|
|
6135
6116
|
if (!profile.acceptApiKeys) return true;
|
|
6136
6117
|
if (!request?.headers) return true;
|
|
@@ -6415,19 +6396,11 @@ async function authenticateRequest(request, profile, options2) {
|
|
|
6415
6396
|
return {
|
|
6416
6397
|
authType: "api-key",
|
|
6417
6398
|
credentialError: "CREDENTIAL_INVALID",
|
|
6418
|
-
firecrawlApiKey: void 0
|
|
6419
|
-
keylessClientIp: extractClientIp(request)
|
|
6399
|
+
firecrawlApiKey: void 0
|
|
6420
6400
|
};
|
|
6421
6401
|
}
|
|
6422
6402
|
throw new InvalidFirecrawlCredentialError();
|
|
6423
6403
|
}
|
|
6424
|
-
if (profile.allowKeyless) {
|
|
6425
|
-
return {
|
|
6426
|
-
authType: "keyless",
|
|
6427
|
-
firecrawlApiKey: void 0,
|
|
6428
|
-
keylessClientIp: extractClientIp(request)
|
|
6429
|
-
};
|
|
6430
|
-
}
|
|
6431
6404
|
if (!profile.acceptApiKeys) {
|
|
6432
6405
|
throw new Error(
|
|
6433
6406
|
`OAuth access token required for the Firecrawl MCP resource ${profile.endpoint}`
|
|
@@ -6440,7 +6413,6 @@ async function authenticateRequest(request, profile, options2) {
|
|
|
6440
6413
|
const session2 = {
|
|
6441
6414
|
authType: resolved?.source === "oauth" ? "oauth" : "api-key",
|
|
6442
6415
|
firecrawlApiKey: headerCred,
|
|
6443
|
-
...isLegacyKeyPathRequest(request) ? { keyTransport: "path" } : {},
|
|
6444
6416
|
...resolved?.metadata
|
|
6445
6417
|
};
|
|
6446
6418
|
return managedCred ? setManagedOAuthApiKey(session2, managedCred) : session2;
|
|
@@ -6489,18 +6461,6 @@ function emitSearchCompanionAuthTelemetry(hosted2, profile, request, outcome, se
|
|
|
6489
6461
|
})
|
|
6490
6462
|
);
|
|
6491
6463
|
}
|
|
6492
|
-
function emitLegacyKeyPathTelemetry(profile, request, outcome, session) {
|
|
6493
|
-
if (profile.id !== "full" || !isLegacyKeyPathRequest(request)) return;
|
|
6494
|
-
console.log(
|
|
6495
|
-
"[MCP_LEGACY_KEY_PATH]",
|
|
6496
|
-
JSON.stringify({
|
|
6497
|
-
auth_type: session?.authType ?? "none",
|
|
6498
|
-
key_transport: "path",
|
|
6499
|
-
outcome,
|
|
6500
|
-
resource: profile.resourceUrl
|
|
6501
|
-
})
|
|
6502
|
-
);
|
|
6503
|
-
}
|
|
6504
6464
|
function createServiceAuthenticate(profile, options2) {
|
|
6505
6465
|
return function authenticateWithOAuthChallenge(request) {
|
|
6506
6466
|
return authenticateRequest(request, profile, options2).then((session) => {
|
|
@@ -6512,12 +6472,6 @@ function createServiceAuthenticate(profile, options2) {
|
|
|
6512
6472
|
session.credentialError ? "rejected" : "accepted",
|
|
6513
6473
|
session
|
|
6514
6474
|
);
|
|
6515
|
-
emitLegacyKeyPathTelemetry(
|
|
6516
|
-
profile,
|
|
6517
|
-
request,
|
|
6518
|
-
session.credentialError ? "rejected" : "accepted",
|
|
6519
|
-
session
|
|
6520
|
-
);
|
|
6521
6475
|
return session;
|
|
6522
6476
|
}).catch((error2) => {
|
|
6523
6477
|
emitSearchCompanionAuthTelemetry(
|
|
@@ -6526,7 +6480,6 @@ function createServiceAuthenticate(profile, options2) {
|
|
|
6526
6480
|
request,
|
|
6527
6481
|
"rejected"
|
|
6528
6482
|
);
|
|
6529
|
-
emitLegacyKeyPathTelemetry(profile, request, "rejected");
|
|
6530
6483
|
if (error2 instanceof InvalidFirecrawlCredentialError) {
|
|
6531
6484
|
throw createInvalidCredentialResponse(error2);
|
|
6532
6485
|
}
|
|
@@ -6553,76 +6506,6 @@ function createServiceAuthenticate(profile, options2) {
|
|
|
6553
6506
|
};
|
|
6554
6507
|
}
|
|
6555
6508
|
|
|
6556
|
-
// src/service/session.ts
|
|
6557
|
-
function isHostedKeylessSession(session) {
|
|
6558
|
-
return session?.authType === "keyless" && !session.firecrawlApiKey;
|
|
6559
|
-
}
|
|
6560
|
-
|
|
6561
|
-
// src/service/keyless.ts
|
|
6562
|
-
var DEFAULT_CLOUD_API_URL = "https://api.firecrawl.dev";
|
|
6563
|
-
function apiBaseUrl(apiUrl2) {
|
|
6564
|
-
return (apiUrl2 || DEFAULT_CLOUD_API_URL).replace(/\/$/, "");
|
|
6565
|
-
}
|
|
6566
|
-
async function keylessEligible(apiBaseUrl2, clientIp, origin, { signupLink = false } = {}) {
|
|
6567
|
-
const secret = process.env.KEYLESS_PROXY_SECRET;
|
|
6568
|
-
if (!secret) return { eligible: false, unavailable: true };
|
|
6569
|
-
try {
|
|
6570
|
-
const response = await fetch(
|
|
6571
|
-
`${apiBaseUrl2}/v2/keyless/eligibility${signupLink ? "?signup_link=1" : ""}`,
|
|
6572
|
-
{
|
|
6573
|
-
headers: {
|
|
6574
|
-
...originHeaders(origin),
|
|
6575
|
-
"x-firecrawl-keyless-ip": clientIp,
|
|
6576
|
-
"x-firecrawl-keyless-secret": secret
|
|
6577
|
-
}
|
|
6578
|
-
}
|
|
6579
|
-
);
|
|
6580
|
-
if (!response.ok) return { eligible: false, unavailable: true };
|
|
6581
|
-
const json = await response.json().catch(() => null);
|
|
6582
|
-
if (typeof json?.eligible !== "boolean") {
|
|
6583
|
-
return { eligible: false, unavailable: true };
|
|
6584
|
-
}
|
|
6585
|
-
return {
|
|
6586
|
-
eligible: json?.eligible === true,
|
|
6587
|
-
...typeof json?.reason === "string" ? { reason: json.reason } : {},
|
|
6588
|
-
...Number.isFinite(json?.retryAfterSeconds) && json.retryAfterSeconds > 0 ? { retryAfterSeconds: json.retryAfterSeconds } : {},
|
|
6589
|
-
...keylessSignupUrlFrom(json?.signupUrl) ? { signupUrl: json.signupUrl } : {}
|
|
6590
|
-
};
|
|
6591
|
-
} catch {
|
|
6592
|
-
return { eligible: false, unavailable: true };
|
|
6593
|
-
}
|
|
6594
|
-
}
|
|
6595
|
-
async function hostedKeylessSignupUrl(apiBaseUrl2, session) {
|
|
6596
|
-
if (!session?.keylessClientIp) return void 0;
|
|
6597
|
-
const eligibility = await keylessEligible(
|
|
6598
|
-
apiBaseUrl2,
|
|
6599
|
-
session.keylessClientIp,
|
|
6600
|
-
requestOrigin(void 0, session),
|
|
6601
|
-
{ signupLink: true }
|
|
6602
|
-
);
|
|
6603
|
-
return eligibility.signupUrl;
|
|
6604
|
-
}
|
|
6605
|
-
async function requireKeylessEligibility(apiBase, session, origin) {
|
|
6606
|
-
if (!isHostedKeylessSession(session)) return;
|
|
6607
|
-
const eligibility = session?.keylessClientIp ? await keylessEligible(apiBase, session.keylessClientIp, origin) : { eligible: false };
|
|
6608
|
-
if (!eligibility.eligible) {
|
|
6609
|
-
const code = eligibility.unavailable ? "KEYLESS_ELIGIBILITY_UNAVAILABLE" : keylessQuotaReason(eligibility.reason) ? "KEYLESS_QUOTA_EXHAUSTED" : "KEYLESS_ACCESS_NOT_AVAILABLE";
|
|
6610
|
-
const payload = recoveryPayload(code, session?.requestId, {
|
|
6611
|
-
retryAfterSeconds: eligibility.retryAfterSeconds,
|
|
6612
|
-
signupUrl: eligibility.signupUrl
|
|
6613
|
-
});
|
|
6614
|
-
throw new UserError2(String(payload.message), payload);
|
|
6615
|
-
}
|
|
6616
|
-
}
|
|
6617
|
-
function keylessForwardingHeaders(session) {
|
|
6618
|
-
const secret = process.env.KEYLESS_PROXY_SECRET;
|
|
6619
|
-
if (!session?.keylessClientIp || !secret) return void 0;
|
|
6620
|
-
return {
|
|
6621
|
-
"x-firecrawl-keyless-ip": session.keylessClientIp,
|
|
6622
|
-
"x-firecrawl-keyless-secret": secret
|
|
6623
|
-
};
|
|
6624
|
-
}
|
|
6625
|
-
|
|
6626
6509
|
// src/service/search-surface.ts
|
|
6627
6510
|
import { z as z13 } from "zod";
|
|
6628
6511
|
function searchSurfaceScrapeDescription(safeMode) {
|
|
@@ -6750,66 +6633,7 @@ function searchSurfaceTools({
|
|
|
6750
6633
|
}
|
|
6751
6634
|
|
|
6752
6635
|
// src/service/tool-guard.ts
|
|
6753
|
-
|
|
6754
|
-
function emitActionLog(profile, apiUrl2, toolName, status, session, error2, requestId = randomUUID4(), code) {
|
|
6755
|
-
const payload = {
|
|
6756
|
-
team_id: session?.teamId,
|
|
6757
|
-
user_id: session?.userId,
|
|
6758
|
-
api_key_id: session?.apiKeyId,
|
|
6759
|
-
oauth_client_id: session?.oauthClientId,
|
|
6760
|
-
auth_type: session?.authType ?? "none",
|
|
6761
|
-
tool_name: toolName,
|
|
6762
|
-
status,
|
|
6763
|
-
request_id: requestId,
|
|
6764
|
-
resource: profile.resourceUrl,
|
|
6765
|
-
...error2 ? { error_class: error2 instanceof Error ? error2.name : typeof error2 } : {},
|
|
6766
|
-
...code ? { code } : {}
|
|
6767
|
-
};
|
|
6768
|
-
console.error("[MCP_ACTION]", JSON.stringify(payload));
|
|
6769
|
-
const secret = normalizeHeader(process.env.FIRECRAWL_MCP_ACTION_LOG_SECRET);
|
|
6770
|
-
const configuredApiUrl = normalizeHeader(apiUrl2);
|
|
6771
|
-
const endpoint = normalizeHeader(process.env.FIRECRAWL_MCP_ACTION_LOG_URL) ?? (configuredApiUrl ? `${withoutTrailingSlash(configuredApiUrl)}/v2/mcp/action-logs` : void 0);
|
|
6772
|
-
if (!secret || !endpoint || !payload.team_id || status === "started") return;
|
|
6773
|
-
const actionLogPayload = { ...payload };
|
|
6774
|
-
delete actionLogPayload.code;
|
|
6775
|
-
void fetch(endpoint, {
|
|
6776
|
-
method: "POST",
|
|
6777
|
-
headers: {
|
|
6778
|
-
Authorization: `Bearer ${secret}`,
|
|
6779
|
-
"Content-Type": "application/json"
|
|
6780
|
-
},
|
|
6781
|
-
body: JSON.stringify(actionLogPayload),
|
|
6782
|
-
signal: AbortSignal.timeout(1500)
|
|
6783
|
-
}).catch(() => void 0);
|
|
6784
|
-
}
|
|
6785
|
-
var AGENT_HINT_LOG_MAX_CHARS = 1e3;
|
|
6786
|
-
function emitAgentHintsLog(profile, toolName, status, hints, session, requestId) {
|
|
6787
|
-
if (!hints) return;
|
|
6788
|
-
console.error(
|
|
6789
|
-
"[MCP_AGENT_HINTS]",
|
|
6790
|
-
JSON.stringify({
|
|
6791
|
-
tool_name: toolName,
|
|
6792
|
-
status,
|
|
6793
|
-
request_id: requestId,
|
|
6794
|
-
auth_type: session.authType ?? "none",
|
|
6795
|
-
// The companion search server shares this process and wrapper.
|
|
6796
|
-
profile: session.profile ?? profile.id,
|
|
6797
|
-
hint_count: hints.length,
|
|
6798
|
-
hints: hints.map((hint) => hint.slice(0, AGENT_HINT_LOG_MAX_CHARS))
|
|
6799
|
-
})
|
|
6800
|
-
);
|
|
6801
|
-
}
|
|
6802
|
-
function createToolGuard({
|
|
6803
|
-
profile,
|
|
6804
|
-
apiUrl: apiUrl2,
|
|
6805
|
-
apiBaseUrl: apiBaseUrl2,
|
|
6806
|
-
logActions
|
|
6807
|
-
}) {
|
|
6808
|
-
const logRejection = (toolName, session, error2, requestId, code) => {
|
|
6809
|
-
if (logActions) {
|
|
6810
|
-
emitActionLog(profile, apiUrl2, toolName, "error", session, error2, requestId, code);
|
|
6811
|
-
}
|
|
6812
|
-
};
|
|
6636
|
+
function createToolGuard() {
|
|
6813
6637
|
return (tool) => {
|
|
6814
6638
|
const keylessTool = keylessToolNames.has(tool.name);
|
|
6815
6639
|
const execute = tool.execute;
|
|
@@ -6818,58 +6642,33 @@ function createToolGuard({
|
|
|
6818
6642
|
return {
|
|
6819
6643
|
...tool,
|
|
6820
6644
|
canList: (session) => (
|
|
6821
|
-
// A credentialError session lists the keyless tool surface (
|
|
6822
|
-
//
|
|
6823
|
-
//
|
|
6824
|
-
//
|
|
6825
|
-
//
|
|
6826
|
-
//
|
|
6827
|
-
//
|
|
6828
|
-
(session?.credentialError
|
|
6645
|
+
// A credentialError session lists the keyless tool surface (not the
|
|
6646
|
+
// full authenticated schema) so the client proceeds past tools/list and
|
|
6647
|
+
// calling any listed tool returns the CREDENTIAL_INVALID recovery
|
|
6648
|
+
// payload (below). An empty list would leave MCP clients that stop after
|
|
6649
|
+
// tools/list unable to ever surface the recovery guidance; the full
|
|
6650
|
+
// schema would over-disclose to a request carrying an unrecognized or
|
|
6651
|
+
// invalid credential.
|
|
6652
|
+
(session?.credentialError ? keylessTool : true) && (canList?.(session) ?? true)
|
|
6829
6653
|
),
|
|
6830
6654
|
beforeValidate: async (args2, session) => {
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
const requestId = randomUUID4();
|
|
6834
|
-
const payload2 = recoveryPayload(code, requestId, {
|
|
6835
|
-
signupUrl: code === "KEYLESS_TOOL_NOT_AVAILABLE" ? await hostedKeylessSignupUrl(apiBaseUrl2, session) : void 0
|
|
6836
|
-
});
|
|
6837
|
-
logRejection(tool.name, session, new UserError2(String(payload2.message), payload2), requestId, code);
|
|
6655
|
+
if (session?.credentialError) {
|
|
6656
|
+
const payload = recoveryPayload("CREDENTIAL_INVALID");
|
|
6838
6657
|
return {
|
|
6839
|
-
content: [{ type: "text", text: String(
|
|
6658
|
+
content: [{ type: "text", text: String(payload.message) }],
|
|
6840
6659
|
isError: true,
|
|
6841
|
-
structuredContent:
|
|
6660
|
+
structuredContent: payload
|
|
6842
6661
|
};
|
|
6843
6662
|
}
|
|
6844
|
-
|
|
6845
|
-
const payload = earlyResult?.structuredContent;
|
|
6846
|
-
const recoveryCode = payload && typeof payload === "object" && "code" in payload && typeof payload.code === "string" ? payload.code : void 0;
|
|
6847
|
-
if (earlyResult?.isError && recoveryCode) {
|
|
6848
|
-
logRejection(
|
|
6849
|
-
tool.name,
|
|
6850
|
-
session,
|
|
6851
|
-
new UserError2(`Tool validation failed: ${recoveryCode}`, payload),
|
|
6852
|
-
randomUUID4(),
|
|
6853
|
-
recoveryCode
|
|
6854
|
-
);
|
|
6855
|
-
}
|
|
6856
|
-
return earlyResult;
|
|
6663
|
+
return beforeValidate?.(args2, session);
|
|
6857
6664
|
},
|
|
6858
6665
|
execute: async (args2, context) => {
|
|
6859
6666
|
const session = context.session;
|
|
6860
|
-
const requestId = session.requestId;
|
|
6861
6667
|
if (session.credentialError) {
|
|
6862
|
-
const
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
}
|
|
6867
|
-
if (isHostedKeylessSession(session) && !keylessTool) {
|
|
6868
|
-
const code = "KEYLESS_TOOL_NOT_AVAILABLE";
|
|
6869
|
-
const payload = recoveryPayload(code, requestId, {
|
|
6870
|
-
signupUrl: await hostedKeylessSignupUrl(apiBaseUrl2, session)
|
|
6871
|
-
});
|
|
6872
|
-
logRejection(tool.name, session, new UserError2(String(payload.message), payload), requestId, code);
|
|
6668
|
+
const payload = recoveryPayload(
|
|
6669
|
+
"CREDENTIAL_INVALID",
|
|
6670
|
+
session.requestId
|
|
6671
|
+
);
|
|
6873
6672
|
throw new UserError2(String(payload.message), payload);
|
|
6874
6673
|
}
|
|
6875
6674
|
return execute(args2, context);
|
|
@@ -6877,76 +6676,6 @@ function createToolGuard({
|
|
|
6877
6676
|
};
|
|
6878
6677
|
};
|
|
6879
6678
|
}
|
|
6880
|
-
function createToolCallLogger({
|
|
6881
|
-
profile,
|
|
6882
|
-
apiUrl: apiUrl2,
|
|
6883
|
-
logActions
|
|
6884
|
-
}) {
|
|
6885
|
-
return (event) => {
|
|
6886
|
-
const session = event.session;
|
|
6887
|
-
if (event.status === "started") {
|
|
6888
|
-
if (logActions) {
|
|
6889
|
-
emitActionLog(profile, apiUrl2, event.tool, "started", session, void 0, event.requestId);
|
|
6890
|
-
}
|
|
6891
|
-
return;
|
|
6892
|
-
}
|
|
6893
|
-
emitAgentHintsLog(
|
|
6894
|
-
profile,
|
|
6895
|
-
event.tool,
|
|
6896
|
-
event.status === "success" && !event.result?.isError ? "success" : "error",
|
|
6897
|
-
event.agentHints,
|
|
6898
|
-
session,
|
|
6899
|
-
event.requestId
|
|
6900
|
-
);
|
|
6901
|
-
if (logActions) {
|
|
6902
|
-
emitActionLog(
|
|
6903
|
-
profile,
|
|
6904
|
-
apiUrl2,
|
|
6905
|
-
event.tool,
|
|
6906
|
-
event.status,
|
|
6907
|
-
session,
|
|
6908
|
-
event.status === "error" ? event.error : void 0,
|
|
6909
|
-
event.requestId
|
|
6910
|
-
);
|
|
6911
|
-
}
|
|
6912
|
-
};
|
|
6913
|
-
}
|
|
6914
|
-
|
|
6915
|
-
// src/service/http.ts
|
|
6916
|
-
function registerServiceRoutes(app, profile, { hosted: hosted2 }) {
|
|
6917
|
-
const openAiAppsChallengeToken = normalizeHeader(
|
|
6918
|
-
process.env.OPENAI_APPS_CHALLENGE_TOKEN
|
|
6919
|
-
);
|
|
6920
|
-
if (openAiAppsChallengeToken) {
|
|
6921
|
-
app.get(
|
|
6922
|
-
"/.well-known/openai-apps-challenge",
|
|
6923
|
-
(context) => context.text(openAiAppsChallengeToken)
|
|
6924
|
-
);
|
|
6925
|
-
}
|
|
6926
|
-
app.get("/ready", (context) => {
|
|
6927
|
-
if (!hosted2) {
|
|
6928
|
-
return context.json({ ok: true }, 200);
|
|
6929
|
-
}
|
|
6930
|
-
const searchPrimary = profile.id === "search";
|
|
6931
|
-
const required = [
|
|
6932
|
-
"FIRECRAWL_API_URL",
|
|
6933
|
-
"FIRECRAWL_OAUTH_INTROSPECT_SECRET",
|
|
6934
|
-
"MCP_DELEGATED_CREDENTIAL_SECRET"
|
|
6935
|
-
];
|
|
6936
|
-
if (profile.allowKeyless) {
|
|
6937
|
-
required.push("KEYLESS_PROXY_SECRET");
|
|
6938
|
-
}
|
|
6939
|
-
const missing = required.filter((name2) => !normalizeHeader(process.env[name2]));
|
|
6940
|
-
const configuredEndpoint = getPrimaryEndpoint();
|
|
6941
|
-
const resourceMatchesEndpoint = searchPrimary ? withoutTrailingSlash(profile.resourceUrl) === DEFAULT_MCP_SEARCH_RESOURCE_URL : withoutTrailingSlash(profile.resourceUrl).endsWith(configuredEndpoint);
|
|
6942
|
-
if (!resourceMatchesEndpoint) {
|
|
6943
|
-
missing.push(
|
|
6944
|
-
searchPrimary ? "FIRECRAWL_MCP_SEARCH_RESOURCE_URL (endpoint mismatch)" : "FIRECRAWL_MCP_RESOURCE_URL (endpoint mismatch)"
|
|
6945
|
-
);
|
|
6946
|
-
}
|
|
6947
|
-
return missing.length ? context.json({ ok: false, missing }, 503) : context.json({ ok: true }, 200);
|
|
6948
|
-
});
|
|
6949
|
-
}
|
|
6950
6679
|
|
|
6951
6680
|
// src/service/index.ts
|
|
6952
6681
|
function createServiceHooks(profile, settings) {
|
|
@@ -6963,27 +6692,13 @@ function createServiceHooks(profile, settings) {
|
|
|
6963
6692
|
hooks.outboundRequest = (session) => {
|
|
6964
6693
|
const serviceSession = session;
|
|
6965
6694
|
const credential = hasManagedOAuthCredential(serviceSession) ? credentialForOutboundRequest(serviceSession) : void 0;
|
|
6966
|
-
|
|
6967
|
-
return credential || headers ? { credential, headers } : void 0;
|
|
6695
|
+
return credential ? { credential } : void 0;
|
|
6968
6696
|
};
|
|
6969
6697
|
}
|
|
6970
6698
|
if (!settings.hosted) return hooks;
|
|
6971
|
-
const apiBase = apiBaseUrl(settings.apiUrl);
|
|
6972
|
-
const logActions = !searchSurface;
|
|
6973
6699
|
return {
|
|
6974
6700
|
...hooks,
|
|
6975
|
-
wrapTool: createToolGuard(
|
|
6976
|
-
profile,
|
|
6977
|
-
apiUrl: settings.apiUrl,
|
|
6978
|
-
apiBaseUrl: apiBase,
|
|
6979
|
-
logActions
|
|
6980
|
-
}),
|
|
6981
|
-
onToolResult: createToolCallLogger({
|
|
6982
|
-
profile,
|
|
6983
|
-
apiUrl: settings.apiUrl,
|
|
6984
|
-
logActions
|
|
6985
|
-
}),
|
|
6986
|
-
beforeKeylessRequest: (session, origin) => requireKeylessEligibility(apiBase, session, origin),
|
|
6701
|
+
wrapTool: createToolGuard(),
|
|
6987
6702
|
...profile.advertiseOAuth ? {
|
|
6988
6703
|
oauth: {
|
|
6989
6704
|
protectedResource: {
|
|
@@ -7047,7 +6762,9 @@ var { start } = createFirecrawlMcpServer({
|
|
|
7047
6762
|
...options,
|
|
7048
6763
|
unstable_hooks: {
|
|
7049
6764
|
...primaryHooks,
|
|
7050
|
-
configureHttp: (app) =>
|
|
6765
|
+
configureHttp: (app) => {
|
|
6766
|
+
app.get("/ready", (context) => context.json({ ok: true }, 200));
|
|
6767
|
+
}
|
|
7051
6768
|
}
|
|
7052
6769
|
});
|
|
7053
6770
|
var PORT = Number(process.env.PORT || 3e3);
|
|
@@ -7061,11 +6778,6 @@ var args = transport === "httpStream" ? {
|
|
|
7061
6778
|
stateless: true
|
|
7062
6779
|
}
|
|
7063
6780
|
} : { transportType: "stdio" };
|
|
7064
|
-
if (hosted && primaryProfile.allowKeyless && !normalizeHeader(process.env.KEYLESS_PROXY_SECRET)) {
|
|
7065
|
-
console.warn(
|
|
7066
|
-
"[firecrawl-mcp] KEYLESS_PROXY_SECRET is missing; keyless requests will be unavailable and /ready will fail."
|
|
7067
|
-
);
|
|
7068
|
-
}
|
|
7069
6781
|
if (transport === "stdio" && !process.env.FIRECRAWL_API_KEY && !process.env.FIRECRAWL_API_URL) {
|
|
7070
6782
|
console.error(
|
|
7071
6783
|
"No FIRECRAWL_API_KEY or FIRECRAWL_API_URL set \u2014 running in keyless mode. firecrawl_scrape and firecrawl_search are free (rate-limited per IP) against the Firecrawl cloud; other tools require an API key (get one free at https://firecrawl.dev)."
|
package/dist/server.d.ts
CHANGED
|
@@ -110,15 +110,15 @@ declare const searchToolBaseFields: {
|
|
|
110
110
|
includeDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
111
111
|
excludeDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
112
112
|
sources: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodEnum<{
|
|
113
|
-
alexandria: "alexandria";
|
|
114
113
|
images: "images";
|
|
114
|
+
alexandria: "alexandria";
|
|
115
115
|
web: "web";
|
|
116
116
|
news: "news";
|
|
117
117
|
exchange: "exchange";
|
|
118
118
|
}>, z.ZodObject<{
|
|
119
119
|
type: z.ZodEnum<{
|
|
120
|
-
alexandria: "alexandria";
|
|
121
120
|
images: "images";
|
|
121
|
+
alexandria: "alexandria";
|
|
122
122
|
web: "web";
|
|
123
123
|
news: "news";
|
|
124
124
|
exchange: "exchange";
|
package/dist/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firecrawl-mcp",
|
|
3
|
-
"version": "3.29.
|
|
3
|
+
"version": "3.29.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",
|