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 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). The bundled service has a fixed deployment contract: nginx routes `/v2/mcp-search` to the instance on local port `3001`, and the OAuth protected-resource identifier is `https://mcp.firecrawl.dev/v2/mcp-search`.
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 bundled nginx routes or the authorization server allowlist and must not be used independently in the hosted deployment.
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
- **Hosted server:** On the hosted server (`CLOUD_SERVICE=true`) 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.
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 DEFAULT_CLOUD_API_URL2 = "https://api.firecrawl.dev";
4938
+ const DEFAULT_CLOUD_API_URL = "https://api.firecrawl.dev";
4938
4939
  function resolveApiBaseUrl() {
4939
- return (config.apiUrl || DEFAULT_CLOUD_API_URL2).replace(
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
- import { randomUUID as randomUUID4 } from "crypto";
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 (same as a
6822
- // real keyless session, not the full authenticated schema) so the
6823
- // client proceeds past tools/list and calling any listed tool returns
6824
- // the CREDENTIAL_INVALID recovery payload (below). An empty list would
6825
- // leave MCP clients that stop after tools/list unable to ever surface
6826
- // the recovery guidance; the full non-keyless schema would over-disclose
6827
- // to a request carrying an unrecognized or invalid credential.
6828
- (session?.credentialError || isHostedKeylessSession(session) ? keylessTool : true) && (canList?.(session) ?? true)
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
- const code = session?.credentialError ? "CREDENTIAL_INVALID" : isHostedKeylessSession(session) && !keylessTool ? "KEYLESS_TOOL_NOT_AVAILABLE" : void 0;
6832
- if (code) {
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(payload2.message) }],
6658
+ content: [{ type: "text", text: String(payload.message) }],
6840
6659
  isError: true,
6841
- structuredContent: payload2
6660
+ structuredContent: payload
6842
6661
  };
6843
6662
  }
6844
- const earlyResult = await beforeValidate?.(args2, session);
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 code = "CREDENTIAL_INVALID";
6863
- const payload = recoveryPayload(code, requestId);
6864
- logRejection(tool.name, session, new UserError2(String(payload.message), payload), requestId, code);
6865
- throw new UserError2(String(payload.message), payload);
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
- const headers = keylessForwardingHeaders(serviceSession);
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) => registerServiceRoutes(app, primaryProfile, { hosted })
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
@@ -4286,6 +4286,7 @@ function createFirecrawlMcpServer(options = {}) {
4286
4286
  "rawHtml",
4287
4287
  "screenshot",
4288
4288
  "links",
4289
+ "images",
4289
4290
  "summary",
4290
4291
  "changeTracking",
4291
4292
  "branding",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firecrawl-mcp",
3
- "version": "3.29.0",
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",