firecrawl-mcp 3.23.6 → 3.23.8

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
@@ -35,11 +35,11 @@ Connect to the remote hosted server with no setup:
35
35
  https://mcp.firecrawl.dev/v2/mcp
36
36
  ```
37
37
 
38
- On the keyless free tier, `scrape`, `search`, and `interact` work without an API key (rate-limited). Other tools such as `crawl`, `map`, and `agent` still need a key.
38
+ On the keyless free tier, `scrape`, `search`, and `parse` work without an API key (rate-limited). Other tools such as `crawl`, `map`, and `agent` still need a key.
39
39
 
40
40
  Prefer OAuth or an API key whenever the human can sign up. It unlocks the full tool set and higher limits.
41
41
 
42
- For an interactive account connection, use:
42
+ For an interactive account connection, configure your MCP client to use this server URL. This is an MCP endpoint, **not a browser page**; use the client's account-connection flow and do not add a second Firecrawl server entry when reconnecting:
43
43
 
44
44
  ```
45
45
  https://mcp.firecrawl.dev/v2/mcp-oauth
@@ -57,7 +57,7 @@ Then configure the client's secure header or secret setting with:
57
57
  Authorization: Bearer <FIRECRAWL_API_KEY>
58
58
  ```
59
59
 
60
- Never put an API key in the server URL. See the [hosted MCP setup guide](https://docs.firecrawl.dev/developer-guides/mcp-setup-guides/oauth) and the [agent onboarding guide](https://www.firecrawl.dev/agent-onboarding/SKILL.md) for client-specific instructions.
60
+ Never put an API key in the server URL. Never put an API key in an agent chat. Configure it directly in the client or secret manager. See the [hosted MCP setup guide](https://docs.firecrawl.dev/mcp-server) and the [agent onboarding guide](https://www.firecrawl.dev/agent-onboarding/SKILL.md) for client-specific instructions.
61
61
 
62
62
  #### Search-only endpoint
63
63
 
package/dist/index.js CHANGED
@@ -802,6 +802,11 @@ Search indexed public GitHub issue, pull-request, and README content. Returns ra
802
802
  });
803
803
  }
804
804
 
805
+ // src/www-authenticate.ts
806
+ function escapeWWWAuthenticateValue(value) {
807
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
808
+ }
809
+
805
810
  // src/index.ts
806
811
  dotenv.config({ debug: false, quiet: true });
807
812
  var require2 = createRequire(import.meta.url);
@@ -849,6 +854,9 @@ var DEFAULT_MCP_RESOURCE_URL = "https://mcp.firecrawl.dev/v2/mcp";
849
854
  var DEFAULT_MCP_OAUTH_RESOURCE_URL = "https://mcp.firecrawl.dev/v2/mcp-oauth";
850
855
  var DEFAULT_MCP_SEARCH_RESOURCE_URL = "https://mcp.firecrawl.dev/v2/mcp-search";
851
856
  var DEFAULT_MCP_SEARCH_ENDPOINT = "/v2/mcp-search";
857
+ var MCP_CONNECTION_GUIDE_URL = "https://docs.firecrawl.dev/mcp-server";
858
+ var MCP_OAUTH_SERVER_URL = "https://mcp.firecrawl.dev/v2/mcp-oauth";
859
+ var API_KEY_SIGNUP_URL = "https://www.firecrawl.dev/app/api-keys";
852
860
  function withoutTrailingSlash(value) {
853
861
  return value.replace(/\/+$/, "");
854
862
  }
@@ -884,23 +892,23 @@ function getOAuthProtectedResourceMetadataUrl(profile) {
884
892
  const base = `${resource.origin}/.well-known/oauth-protected-resource`;
885
893
  return profile.id === "full" ? base : `${base}${resource.pathname}`;
886
894
  }
887
- function escapeWWWAuthenticateValue(value) {
888
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
889
- }
890
- function createOAuthChallengeResponse(error, profile) {
895
+ function createOAuthChallengeResponse(error, profile, details = {}) {
891
896
  if (!isMcpOAuthEnabled()) {
892
897
  return void 0;
893
898
  }
894
899
  const errorMessage = error instanceof Error ? error.message : String(error || "Unauthorized");
895
900
  const wwwAuthenticate = [
896
- `resource_metadata="${escapeWWWAuthenticateValue(getOAuthProtectedResourceMetadataUrl(profile))}"`,
901
+ ...profile.advertiseOAuth ? [
902
+ `resource_metadata="${escapeWWWAuthenticateValue(getOAuthProtectedResourceMetadataUrl(profile))}"`
903
+ ] : [],
897
904
  'error="invalid_token"',
898
905
  `error_description="${escapeWWWAuthenticateValue(errorMessage)}"`
899
906
  ].join(", ");
900
907
  return new Response(
901
908
  JSON.stringify({
902
909
  error: "invalid_token",
903
- error_description: errorMessage
910
+ error_description: errorMessage,
911
+ ...details
904
912
  }),
905
913
  {
906
914
  headers: {
@@ -911,11 +919,26 @@ function createOAuthChallengeResponse(error, profile) {
911
919
  }
912
920
  );
913
921
  }
914
- function createInvalidCredentialResponse(error) {
922
+ function createInvalidCredentialResponse(_error) {
923
+ const recovery = invalidApiKeyRecoveryPayload();
915
924
  return new Response(
916
925
  JSON.stringify({
917
926
  error: "invalid_api_key",
918
- error_description: error.message
927
+ error_description: recovery.message,
928
+ ...recovery
929
+ }),
930
+ {
931
+ headers: { "Content-Type": "application/json" },
932
+ status: 401
933
+ }
934
+ );
935
+ }
936
+ function createInvalidOAuthRecoveryResponse(recovery) {
937
+ return new Response(
938
+ JSON.stringify({
939
+ error: "invalid_token",
940
+ error_description: recovery.message,
941
+ ...recovery
919
942
  }),
920
943
  {
921
944
  headers: { "Content-Type": "application/json" },
@@ -941,6 +964,12 @@ var InvalidFirecrawlCredentialError = class extends Error {
941
964
  this.name = "InvalidFirecrawlCredentialError";
942
965
  }
943
966
  };
967
+ var InvalidOAuthCredentialError = class extends Error {
968
+ constructor() {
969
+ super("Invalid OAuth access token");
970
+ this.name = "InvalidOAuthCredentialError";
971
+ }
972
+ };
944
973
  var MCP_GLOBAL_SCOPE = "firecrawl:global";
945
974
  function values(value) {
946
975
  if (typeof value === "string") return value.split(/\s+/).filter(Boolean);
@@ -1019,7 +1048,7 @@ async function resolveCredentialFromHeaders(headers, profile) {
1019
1048
  }
1020
1049
  if (!data.active || !data.api_key) {
1021
1050
  if (isFirecrawlOAuthAccessToken(token)) {
1022
- throw new Error("Invalid OAuth access token");
1051
+ throw new InvalidOAuthCredentialError();
1023
1052
  }
1024
1053
  return { invalid: true };
1025
1054
  }
@@ -1059,6 +1088,14 @@ async function authenticateRequest(request, profile) {
1059
1088
  if (process.env.CLOUD_SERVICE === "true") {
1060
1089
  if (!headerCred && !managedCred) {
1061
1090
  if (resolved?.invalid) {
1091
+ if (profile.allowKeyless) {
1092
+ return {
1093
+ authType: "api-key",
1094
+ credentialError: "CREDENTIAL_INVALID",
1095
+ firecrawlApiKey: void 0,
1096
+ keylessClientIp: extractClientIp(request)
1097
+ };
1098
+ }
1062
1099
  throw new InvalidFirecrawlCredentialError();
1063
1100
  }
1064
1101
  if (profile.allowKeyless) {
@@ -1171,6 +1208,15 @@ function makeAuthenticate(profile) {
1171
1208
  if (error instanceof InvalidFirecrawlCredentialError) {
1172
1209
  throw createInvalidCredentialResponse(error);
1173
1210
  }
1211
+ if (error instanceof InvalidOAuthCredentialError) {
1212
+ const recovery = invalidOAuthRecoveryPayload(profile);
1213
+ const oauthChallenge2 = createOAuthChallengeResponse(
1214
+ new Error(recovery.message),
1215
+ profile,
1216
+ recovery
1217
+ );
1218
+ throw oauthChallenge2 ?? createInvalidOAuthRecoveryResponse(recovery);
1219
+ }
1174
1220
  if (error instanceof CredentialValidationUnavailableError) {
1175
1221
  throw new Response(
1176
1222
  JSON.stringify({
@@ -1296,6 +1342,7 @@ function makeFullProfile() {
1296
1342
  allowKeyless: !account,
1297
1343
  acceptApiKeys: true,
1298
1344
  acceptLegacyAudience: account && process.env.MCP_OAUTH_ACCEPT_LEGACY_V2_MCP_AUD !== "false",
1345
+ advertiseOAuth: account,
1299
1346
  primary: true
1300
1347
  };
1301
1348
  }
@@ -1323,6 +1370,7 @@ function makeSearchProfile({ primary = false } = {}) {
1323
1370
  // deployment explicitly enables the same profile flag used by primary.
1324
1371
  acceptApiKeys: !oauthOnly,
1325
1372
  requireManagedOAuth: oauthOnly,
1373
+ advertiseOAuth: true,
1326
1374
  primary
1327
1375
  };
1328
1376
  }
@@ -1337,7 +1385,7 @@ function createServer(profile) {
1337
1385
  logger: new ConsoleLogger(),
1338
1386
  roots: { enabled: false },
1339
1387
  oauth: {
1340
- enabled: isMcpOAuthEnabled(),
1388
+ enabled: isMcpOAuthEnabled() && profile.advertiseOAuth,
1341
1389
  protectedResource: {
1342
1390
  authorizationServers: [getOAuthIssuer()],
1343
1391
  bearerMethodsSupported: ["header"],
@@ -1369,27 +1417,87 @@ function isHostedKeylessSession(session) {
1369
1417
  function isLocalKeylessStartup() {
1370
1418
  return process.env.CLOUD_SERVICE !== "true" && !isHttpStreamingTransport() && !resolveCredentialFromEnv() && !normalizeHeader(process.env.FIRECRAWL_API_URL);
1371
1419
  }
1420
+ var NEVER_SHARE_API_KEY_SENTENCE = "Never ask for, accept, or put an API key in chat or in an MCP URL.";
1421
+ var RETRY_AFTER_CONFIRM_SUFFIX = "start a new client session or run and retry the original task.";
1422
+ var HUMAN_CONNECTION_GUIDANCE = `A human or operator must complete the connection because this session cannot reconnect Firecrawl itself. Ask the human to choose before changing anything: (1) in the MCP client's settings, update or replace the existing Firecrawl server entry so its URL is ${MCP_OAUTH_SERVER_URL}, then complete sign-in through the client. That URL is a client configuration value, not a page to open in a browser. Do not add a second Firecrawl server or change configuration without approval; or (2) have an operator create an API key at ${API_KEY_SIGNUP_URL} and configure it in the client or secret manager outside this chat. ${NEVER_SHARE_API_KEY_SENTENCE} After the human confirms setup, ${RETRY_AFTER_CONFIRM_SUFFIX} Connection guide: ${MCP_CONNECTION_GUIDE_URL}`;
1423
+ var ACCOUNT_ONLY_TOOL_GUIDANCE = `This tool needs a connected Firecrawl account. Search, Scrape, and Parse remain available, so continue with those if they can complete the task. Only if this task specifically requires this tool, tell the user that a human must update the existing Firecrawl connection using ${MCP_CONNECTION_GUIDE_URL}. Never ask for or accept an API key in chat. After setup, start a new client session and retry.`;
1424
+ var HUMAN_RECONNECT_ACCOUNT_ACTION = {
1425
+ kind: "human_reconnect_account",
1426
+ actor: "human",
1427
+ requires_user_consent: true,
1428
+ existing_server_only: true,
1429
+ server_url: MCP_OAUTH_SERVER_URL,
1430
+ open_server_url_in_browser: false,
1431
+ docs_url: MCP_CONNECTION_GUIDE_URL
1432
+ };
1433
+ var OPERATOR_CONFIGURE_API_KEY_ACTION = {
1434
+ kind: "operator_configure_api_key",
1435
+ actor: "human_or_operator",
1436
+ requires_user_consent: true,
1437
+ credential_delivery: "outside_agent_chat",
1438
+ signup_url: API_KEY_SIGNUP_URL
1439
+ };
1440
+ var HUMAN_CONNECTION_ACTIONS = [
1441
+ HUMAN_RECONNECT_ACCOUNT_ACTION,
1442
+ OPERATOR_CONFIGURE_API_KEY_ACTION
1443
+ ];
1444
+ function connectionRecoveryPayload(params) {
1445
+ return {
1446
+ code: params.code,
1447
+ auth_mode: params.authMode,
1448
+ message: params.message,
1449
+ docs_url: MCP_CONNECTION_GUIDE_URL,
1450
+ next_actions: params.nextActions
1451
+ };
1452
+ }
1453
+ function invalidApiKeyRecoveryPayload() {
1454
+ return connectionRecoveryPayload({
1455
+ code: "CREDENTIAL_INVALID",
1456
+ authMode: "api_key",
1457
+ message: `The Firecrawl API key configured for this server is invalid or revoked. Ask a human or operator to replace it in this existing server configuration or secret manager outside this chat. ${NEVER_SHARE_API_KEY_SENTENCE} After the human confirms the change, ${RETRY_AFTER_CONFIRM_SUFFIX}`,
1458
+ nextActions: [OPERATOR_CONFIGURE_API_KEY_ACTION]
1459
+ });
1460
+ }
1461
+ function invalidOAuthRecoveryPayload(profile) {
1462
+ if (isMcpOAuthEnabled() && profile.advertiseOAuth) {
1463
+ return connectionRecoveryPayload({
1464
+ code: "OAUTH_CONNECTION_INVALID",
1465
+ authMode: "oauth",
1466
+ message: `This Firecrawl account connection is no longer valid. Ask the human to sign in again through this MCP client's account-connection flow. Do not add a second Firecrawl server or open the MCP server URL directly in a browser. After sign-in, ${RETRY_AFTER_CONFIRM_SUFFIX}`,
1467
+ nextActions: [HUMAN_RECONNECT_ACCOUNT_ACTION, OPERATOR_CONFIGURE_API_KEY_ACTION]
1468
+ });
1469
+ }
1470
+ return connectionRecoveryPayload({
1471
+ code: "OAUTH_CONNECTION_INVALID",
1472
+ authMode: "oauth",
1473
+ message: `This Firecrawl account connection is no longer valid, and this server does not start account sign-in. Ask the human to choose before changing anything: (1) have an operator create a Firecrawl API key at ${API_KEY_SIGNUP_URL} and configure it on this existing server outside this chat; or (2) update this existing server's URL to ${MCP_OAUTH_SERVER_URL} and complete sign-in through the MCP client. That URL is a client configuration value, not a page to open in a browser. ${NEVER_SHARE_API_KEY_SENTENCE} After the human confirms the change, ${RETRY_AFTER_CONFIRM_SUFFIX}`,
1474
+ nextActions: [OPERATOR_CONFIGURE_API_KEY_ACTION, HUMAN_RECONNECT_ACCOUNT_ACTION]
1475
+ });
1476
+ }
1372
1477
  function recoveryPayload(code, requestId = randomUUID(), options = {}) {
1373
1478
  const retryAfterSeconds = options.retryAfterSeconds;
1374
1479
  const isQuotaExhausted = code === "KEYLESS_QUOTA_EXHAUSTED" || code === "KEYLESS_LIMIT_REACHED";
1375
1480
  const isToolUnavailable = code === "KEYLESS_TOOL_NOT_AVAILABLE";
1376
1481
  const isKeylessAccessUnavailable = code === "KEYLESS_ACCESS_NOT_AVAILABLE";
1377
1482
  const isKeylessEligibilityUnavailable = code === "KEYLESS_ELIGIBILITY_UNAVAILABLE";
1378
- const oauthUrl = "https://mcp.firecrawl.dev/v2/mcp-oauth";
1379
1483
  return {
1380
1484
  code,
1381
1485
  request_id: requestId,
1382
1486
  auth_mode: code === "CREDENTIAL_INVALID" ? "credential_error" : "keyless",
1383
- message: code === "CREDENTIAL_INVALID" ? `The supplied Firecrawl credential is invalid or revoked. Reconnect the account via OAuth (https://mcp.firecrawl.dev/v2/mcp-oauth) or create a new API key at https://www.firecrawl.dev/signin and send it as Authorization: Bearer <FIRECRAWL_API_KEY>, then retry.` : isQuotaExhausted ? `The free daily limit for this network has been reached${retryAfterSeconds ? `; try again in about ${retryAfterSeconds} seconds` : ""}. To continue now, connect a Firecrawl account via OAuth (https://mcp.firecrawl.dev/v2/mcp-oauth) or create a free API key at https://www.firecrawl.dev/signin and send it as Authorization: Bearer <FIRECRAWL_API_KEY>.` : isToolUnavailable ? "The free tier includes Search, Scrape, and Parse. This tool needs a connected account; Search, Scrape, and Parse still work, so no action is required." : isKeylessAccessUnavailable ? `Anonymous keyless access is unavailable for this request. To continue, connect a Firecrawl account via OAuth (https://mcp.firecrawl.dev/v2/mcp-oauth) or create a free API key at https://www.firecrawl.dev/signin and send it as Authorization: Bearer <FIRECRAWL_API_KEY>.` : isKeylessEligibilityUnavailable ? "The anonymous keyless eligibility check is temporarily unavailable. Retry shortly." : `This tool requires a Firecrawl account or API key. Connect an account via OAuth (https://mcp.firecrawl.dev/v2/mcp-oauth) or create a free API key at https://www.firecrawl.dev/signin and send it as Authorization: Bearer <FIRECRAWL_API_KEY>, then retry.`,
1384
- ...isKeylessAccessUnavailable ? {} : { available_tools: [...KEYLESS_TOOL_NAMES] },
1385
- docs_url: "https://docs.firecrawl.dev/mcp-server",
1487
+ message: code === "CREDENTIAL_INVALID" ? `The supplied Firecrawl credential is invalid or revoked. ${HUMAN_CONNECTION_GUIDANCE}` : isQuotaExhausted ? `The free daily limit for this network has been reached${retryAfterSeconds ? `; try again in about ${retryAfterSeconds} seconds` : ""}. To continue now: ${HUMAN_CONNECTION_GUIDANCE}` : isToolUnavailable ? ACCOUNT_ONLY_TOOL_GUIDANCE : isKeylessAccessUnavailable ? `Anonymous keyless access is unavailable for this request. To continue: ${HUMAN_CONNECTION_GUIDANCE}` : isKeylessEligibilityUnavailable ? "The anonymous keyless eligibility check is temporarily unavailable. Retry shortly." : `This tool requires a Firecrawl account or API key. ${HUMAN_CONNECTION_GUIDANCE}`,
1488
+ // CREDENTIAL_INVALID sessions gate every tool call (including keyless
1489
+ // tools) on the credentialError check before the keyless branch ever
1490
+ // runs, so none of KEYLESS_TOOL_NAMES are actually callable here. Listing
1491
+ // them as available_tools would send the agent into a retry loop against
1492
+ // tools that will just return this same recovery payload.
1493
+ ...isKeylessAccessUnavailable || code === "CREDENTIAL_INVALID" ? {} : { available_tools: [...KEYLESS_TOOL_NAMES] },
1494
+ docs_url: MCP_CONNECTION_GUIDE_URL,
1386
1495
  ...retryAfterSeconds ? { retry_after_seconds: retryAfterSeconds } : {},
1387
- next_actions: isKeylessEligibilityUnavailable ? [{ kind: "retry_later", after_seconds: 30 }] : isQuotaExhausted || isKeylessAccessUnavailable ? [
1388
- { kind: "connect_oauth", url: oauthUrl, client_commands: [{ client: "claude-code", command: "claude mcp add --transport http firecrawl https://mcp.firecrawl.dev/v2/mcp-oauth" }] },
1389
- { kind: "configure_api_key", header: "Authorization: Bearer <FIRECRAWL_API_KEY>" }
1390
- ] : isToolUnavailable ? [{ kind: "continue_keyless", tools: [...KEYLESS_TOOL_NAMES] }] : [
1391
- { kind: "connect_oauth", url: oauthUrl },
1392
- { kind: "configure_api_key", header: "Authorization: Bearer <FIRECRAWL_API_KEY>" }
1496
+ next_actions: isKeylessEligibilityUnavailable ? [{ kind: "retry_later", after_seconds: 30 }] : isQuotaExhausted || isKeylessAccessUnavailable ? HUMAN_CONNECTION_ACTIONS : isToolUnavailable ? [
1497
+ { kind: "continue_keyless", tools: [...KEYLESS_TOOL_NAMES] },
1498
+ ...HUMAN_CONNECTION_ACTIONS
1499
+ ] : [
1500
+ ...HUMAN_CONNECTION_ACTIONS
1393
1501
  ]
1394
1502
  };
1395
1503
  }
@@ -1454,7 +1562,16 @@ function guardHostedTool(tool, { logActions }) {
1454
1562
  const beforeValidate = tool.beforeValidate;
1455
1563
  return {
1456
1564
  ...tool,
1457
- canList: (session) => !session?.credentialError && (!isHostedKeylessSession(session) || keylessTool) && (canList?.(session) ?? true),
1565
+ canList: (session) => (
1566
+ // A credentialError session lists the keyless tool surface (same as a
1567
+ // real keyless session, not the full authenticated schema) so the
1568
+ // client proceeds past tools/list and calling any listed tool returns
1569
+ // the CREDENTIAL_INVALID recovery payload (below). An empty list would
1570
+ // leave MCP clients that stop after tools/list unable to ever surface
1571
+ // the recovery guidance; the full non-keyless schema would over-disclose
1572
+ // to a request carrying an unrecognized or invalid credential.
1573
+ (session?.credentialError || isHostedKeylessSession(session) ? keylessTool : true) && (canList?.(session) ?? true)
1574
+ ),
1458
1575
  beforeValidate: async (args2, session) => {
1459
1576
  const code = session?.credentialError ? "CREDENTIAL_INVALID" : isHostedKeylessSession(session) && !keylessTool ? "KEYLESS_TOOL_NOT_AVAILABLE" : void 0;
1460
1577
  if (code) {
@@ -0,0 +1,7 @@
1
+ // src/www-authenticate.ts
2
+ function escapeWWWAuthenticateValue(value) {
3
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4
+ }
5
+ export {
6
+ escapeWWWAuthenticateValue
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firecrawl-mcp",
3
- "version": "3.23.6",
3
+ "version": "3.23.8",
4
4
  "description": "MCP server for Firecrawl — search, scrape, and interact with the web. Supports both cloud and self-hosted instances. Features include web search, scraping, page interaction, batch processing, and LLM-powered content analysis.",
5
5
  "type": "module",
6
6
  "mcpName": "io.github.firecrawl/firecrawl-mcp-server",