firecrawl-mcp 3.21.3 → 3.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/index.js +392 -179
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,24 @@ A Model Context Protocol (MCP) server that brings [Firecrawl](https://github.com
|
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
28
28
|
|
|
29
|
+
### Hosted MCP (keyless free tier)
|
|
30
|
+
|
|
31
|
+
Connect to the remote hosted server with no setup:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
https://mcp.firecrawl.dev/v2/mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
On the keyless free tier, `scrape`, `search`, and `interact` work without an API key (rate-limited). Other tools such as `crawl`, `map`, `agent`, and `extract` still need a key.
|
|
38
|
+
|
|
39
|
+
Prefer an API key or OAuth whenever the human can sign up. It unlocks the full tool set and higher limits. With a key, use:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
https://mcp.firecrawl.dev/{FIRECRAWL_API_KEY}/v2/mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See the [MCP server docs](https://docs.firecrawl.dev/mcp-server) and the [agent onboarding guide](https://www.firecrawl.dev/agent-onboarding/SKILL.md) for setup details.
|
|
46
|
+
|
|
29
47
|
### Running with npx
|
|
30
48
|
|
|
31
49
|
```bash
|
|
@@ -326,14 +344,14 @@ Use this guide to select the right tool for your task:
|
|
|
326
344
|
- **If you want to search the web for info:** use **search**
|
|
327
345
|
- **If you need complex research across multiple unknown sources:** use **agent**
|
|
328
346
|
- **If you want to analyze a whole site or section:** use **crawl** (with limits!)
|
|
329
|
-
- **If you need interactive browser automation** (click, type, navigate): use **scrape** + **interact**
|
|
347
|
+
- **If you need interactive browser automation** (click, type, navigate): use **interact** with a URL for a fresh page, or **scrape** + **interact** when you already scraped the page or need tighter scrape control
|
|
330
348
|
|
|
331
349
|
### Quick Reference Table
|
|
332
350
|
|
|
333
351
|
| Tool | Best for | Returns |
|
|
334
352
|
| ------------ | ---------------------------------------------- | ------------------------------ |
|
|
335
353
|
| scrape | Single page content | JSON (preferred) or markdown |
|
|
336
|
-
| interact | Interact with a scraped page
|
|
354
|
+
| interact | Interact with a URL or scraped page | Execution result + scrapeId for URL mode |
|
|
337
355
|
| batch_scrape | Multiple known URLs | JSON (preferred) or markdown[] |
|
|
338
356
|
| map | Discovering URLs on a site | URL[] |
|
|
339
357
|
| crawl | Multi-page extraction (with limits) | markdown/html[] |
|
package/dist/index.js
CHANGED
|
@@ -2487,6 +2487,247 @@ var scrapeParamsSchema = z4.object({
|
|
|
2487
2487
|
saveChanges: z4.boolean().optional()
|
|
2488
2488
|
}).optional()
|
|
2489
2489
|
});
|
|
2490
|
+
var parseOptionParamsSchema = z4.object({
|
|
2491
|
+
formats: z4.array(
|
|
2492
|
+
z4.enum([
|
|
2493
|
+
"markdown",
|
|
2494
|
+
"html",
|
|
2495
|
+
"rawHtml",
|
|
2496
|
+
"links",
|
|
2497
|
+
"summary",
|
|
2498
|
+
"json",
|
|
2499
|
+
"query"
|
|
2500
|
+
])
|
|
2501
|
+
).optional(),
|
|
2502
|
+
jsonOptions: z4.object({
|
|
2503
|
+
prompt: z4.string().optional(),
|
|
2504
|
+
schema: z4.record(z4.string(), z4.any()).optional()
|
|
2505
|
+
}).optional(),
|
|
2506
|
+
queryOptions: z4.object({
|
|
2507
|
+
prompt: z4.string().max(1e4),
|
|
2508
|
+
mode: z4.enum(["directQuote", "freeform"]).default("freeform")
|
|
2509
|
+
}).optional(),
|
|
2510
|
+
parsers: z4.array(z4.enum(["pdf"])).optional(),
|
|
2511
|
+
pdfOptions: z4.object({
|
|
2512
|
+
maxPages: z4.number().int().min(1).max(1e4).optional()
|
|
2513
|
+
}).optional(),
|
|
2514
|
+
onlyMainContent: z4.boolean().optional(),
|
|
2515
|
+
redactPII: z4.boolean().optional(),
|
|
2516
|
+
includeTags: z4.array(z4.string()).optional(),
|
|
2517
|
+
excludeTags: z4.array(z4.string()).optional(),
|
|
2518
|
+
removeBase64Images: z4.boolean().optional(),
|
|
2519
|
+
skipTlsVerification: z4.boolean().optional(),
|
|
2520
|
+
storeInCache: z4.boolean().optional(),
|
|
2521
|
+
zeroDataRetention: z4.boolean().optional(),
|
|
2522
|
+
maxAge: z4.number().optional(),
|
|
2523
|
+
proxy: z4.enum(["basic", "auto"]).optional()
|
|
2524
|
+
});
|
|
2525
|
+
var localParseParamsSchema = parseOptionParamsSchema.extend({
|
|
2526
|
+
filePath: z4.string().min(1).describe(
|
|
2527
|
+
"Absolute or relative path to a local file to parse. Supported: .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls"
|
|
2528
|
+
),
|
|
2529
|
+
contentType: z4.string().optional().describe(
|
|
2530
|
+
"Optional MIME type override. If omitted, the server infers the file kind from the extension."
|
|
2531
|
+
)
|
|
2532
|
+
});
|
|
2533
|
+
var hostedParseParamsSchema = parseOptionParamsSchema.extend({
|
|
2534
|
+
filePath: z4.string().min(1).optional().describe(
|
|
2535
|
+
"Phase 1 only: path to the local file on the caller/harness machine. Hosted MCP will not read or stat this path; it is used only to produce upload instructions."
|
|
2536
|
+
),
|
|
2537
|
+
uploadRef: z4.string().min(1).optional().describe(
|
|
2538
|
+
"Phase 2 only: short-lived upload reference returned by phase 1 after the local PUT upload completes."
|
|
2539
|
+
),
|
|
2540
|
+
contentType: z4.string().optional().describe(
|
|
2541
|
+
"Phase 1 MIME type override. If omitted, the server infers it from the file extension without reading the file."
|
|
2542
|
+
),
|
|
2543
|
+
declaredSizeBytes: z4.number().int().positive().optional().describe(
|
|
2544
|
+
"Optional phase 1 size declaration. Hosted MCP does not stat the file; provide this only if the caller already knows it."
|
|
2545
|
+
)
|
|
2546
|
+
}).superRefine((value, ctx) => {
|
|
2547
|
+
const hasFilePath = typeof value.filePath === "string" && value.filePath.length > 0;
|
|
2548
|
+
const hasUploadRef = typeof value.uploadRef === "string" && value.uploadRef.length > 0;
|
|
2549
|
+
if (hasFilePath === hasUploadRef) {
|
|
2550
|
+
ctx.addIssue({
|
|
2551
|
+
code: z4.ZodIssueCode.custom,
|
|
2552
|
+
message: "Hosted firecrawl_parse requires exactly one of filePath (phase 1) or uploadRef (phase 2).",
|
|
2553
|
+
path: hasFilePath && hasUploadRef ? ["uploadRef"] : ["filePath"]
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
var parseParamsSchema = process.env.CLOUD_SERVICE === "true" ? hostedParseParamsSchema : localParseParamsSchema;
|
|
2558
|
+
var EXTENSION_CONTENT_TYPES = {
|
|
2559
|
+
".html": "text/html",
|
|
2560
|
+
".htm": "text/html",
|
|
2561
|
+
".xhtml": "application/xhtml+xml",
|
|
2562
|
+
".pdf": "application/pdf",
|
|
2563
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2564
|
+
".doc": "application/msword",
|
|
2565
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
2566
|
+
".rtf": "application/rtf",
|
|
2567
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2568
|
+
".xls": "application/vnd.ms-excel"
|
|
2569
|
+
};
|
|
2570
|
+
function inferContentType(filename) {
|
|
2571
|
+
const ext = path.extname(filename).toLowerCase();
|
|
2572
|
+
return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
2573
|
+
}
|
|
2574
|
+
function extractParseOptions(args2) {
|
|
2575
|
+
const { filePath, uploadRef, contentType, declaredSizeBytes, ...options } = args2;
|
|
2576
|
+
return options;
|
|
2577
|
+
}
|
|
2578
|
+
function buildParseOptionsPayload(options) {
|
|
2579
|
+
const transformed = transformScrapeParams(options);
|
|
2580
|
+
const cleaned = removeEmptyTopLevel(transformed);
|
|
2581
|
+
return { origin: ORIGIN, ...cleaned };
|
|
2582
|
+
}
|
|
2583
|
+
function buildContinuationArguments(uploadRef, options) {
|
|
2584
|
+
return {
|
|
2585
|
+
uploadRef,
|
|
2586
|
+
...removeEmptyTopLevel(options)
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
function shellQuote(value) {
|
|
2590
|
+
if (value.length === 0) return "''";
|
|
2591
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
2592
|
+
}
|
|
2593
|
+
function parseApiData(json) {
|
|
2594
|
+
return json && typeof json === "object" && "data" in json ? json.data : json;
|
|
2595
|
+
}
|
|
2596
|
+
async function apiPostJson(pathName, body, apiKey) {
|
|
2597
|
+
const response = await fetch(`${resolveApiBaseUrl()}${pathName}`, {
|
|
2598
|
+
method: "POST",
|
|
2599
|
+
headers: {
|
|
2600
|
+
"Content-Type": "application/json",
|
|
2601
|
+
Authorization: `Bearer ${apiKey}`
|
|
2602
|
+
},
|
|
2603
|
+
body: JSON.stringify(body)
|
|
2604
|
+
});
|
|
2605
|
+
const responseText = await response.text();
|
|
2606
|
+
let parsed;
|
|
2607
|
+
try {
|
|
2608
|
+
parsed = responseText ? JSON.parse(responseText) : {};
|
|
2609
|
+
} catch {
|
|
2610
|
+
parsed = { raw: responseText };
|
|
2611
|
+
}
|
|
2612
|
+
if (!response.ok) {
|
|
2613
|
+
throw new Error(
|
|
2614
|
+
parsed?.error || parsed?.message || `Firecrawl request failed (HTTP ${response.status})`
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
return parsed;
|
|
2618
|
+
}
|
|
2619
|
+
async function apiPostJsonForSession(pathName, body, session) {
|
|
2620
|
+
if (session?.firecrawlApiKey) {
|
|
2621
|
+
return apiPostJson(pathName, body, session.firecrawlApiKey);
|
|
2622
|
+
}
|
|
2623
|
+
if (isKeylessMode(session)) {
|
|
2624
|
+
return keylessPost(pathName, body, session);
|
|
2625
|
+
}
|
|
2626
|
+
throw new Error(
|
|
2627
|
+
"Firecrawl credentials or keyless eligibility required for hosted parse."
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
function buildCurlUploadCommand(filePath, upload) {
|
|
2631
|
+
const method = upload.method ?? "PUT";
|
|
2632
|
+
const headerArgs = Object.entries(upload.headers ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `-H ${shellQuote(`${key}: ${value}`)}`);
|
|
2633
|
+
if (method.toUpperCase() === "POST" && upload.fields) {
|
|
2634
|
+
const fieldArgs = Object.entries(upload.fields).sort(([a], [b]) => a.localeCompare(b)).flatMap(([key, value]) => ["-F", shellQuote(`${key}=${value}`)]);
|
|
2635
|
+
return [
|
|
2636
|
+
"curl",
|
|
2637
|
+
"-X",
|
|
2638
|
+
shellQuote("POST"),
|
|
2639
|
+
...headerArgs,
|
|
2640
|
+
...fieldArgs,
|
|
2641
|
+
"-F",
|
|
2642
|
+
shellQuote(`file=@${filePath}`),
|
|
2643
|
+
shellQuote(upload.uploadUrl)
|
|
2644
|
+
].join(" ");
|
|
2645
|
+
}
|
|
2646
|
+
return [
|
|
2647
|
+
"curl",
|
|
2648
|
+
"-X",
|
|
2649
|
+
shellQuote(method),
|
|
2650
|
+
...headerArgs,
|
|
2651
|
+
"--upload-file",
|
|
2652
|
+
shellQuote(filePath),
|
|
2653
|
+
shellQuote(upload.uploadUrl)
|
|
2654
|
+
].join(" ");
|
|
2655
|
+
}
|
|
2656
|
+
async function executeHostedParse(args2, session, log) {
|
|
2657
|
+
const hasFilePath = typeof args2.filePath === "string" && args2.filePath.length > 0;
|
|
2658
|
+
const hasUploadRef = typeof args2.uploadRef === "string" && args2.uploadRef.length > 0;
|
|
2659
|
+
if (hasFilePath === hasUploadRef) {
|
|
2660
|
+
throw new Error(
|
|
2661
|
+
"Hosted firecrawl_parse requires exactly one of filePath or uploadRef."
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
if (!session?.firecrawlApiKey && !isKeylessMode(session)) {
|
|
2665
|
+
return asText2({
|
|
2666
|
+
success: false,
|
|
2667
|
+
mode: "hosted-upload-ref-auth-required",
|
|
2668
|
+
message: "Hosted firecrawl_parse requires an authenticated Firecrawl session or keyless eligibility before a local file upload URL can be minted. Connect a Firecrawl account, provide an API key, or use keyless hosted MCP while eligible, then call firecrawl_parse again."
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
const options = extractParseOptions(args2);
|
|
2672
|
+
if (hasFilePath && args2.filePath) {
|
|
2673
|
+
const filename = path.basename(args2.filePath);
|
|
2674
|
+
const contentType = typeof args2.contentType === "string" && args2.contentType.length > 0 ? args2.contentType : inferContentType(filename);
|
|
2675
|
+
const uploadRequest = removeEmptyTopLevel({
|
|
2676
|
+
filename,
|
|
2677
|
+
contentType,
|
|
2678
|
+
declaredSizeBytes: args2.declaredSizeBytes
|
|
2679
|
+
});
|
|
2680
|
+
log.info("Creating hosted parse upload URL", { filename, contentType });
|
|
2681
|
+
const uploadJson = await apiPostJsonForSession(
|
|
2682
|
+
"/v2/parse/upload-url",
|
|
2683
|
+
uploadRequest,
|
|
2684
|
+
session
|
|
2685
|
+
);
|
|
2686
|
+
const upload = parseApiData(uploadJson);
|
|
2687
|
+
if (!upload?.uploadUrl || !upload?.uploadRef) {
|
|
2688
|
+
throw new Error(
|
|
2689
|
+
"Firecrawl upload-url response did not include uploadUrl and uploadRef"
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
const uploadHeaders = upload.headers && Object.keys(upload.headers).length > 0 ? upload.headers : (upload.method ?? "PUT").toUpperCase() === "POST" ? {} : { "Content-Type": contentType };
|
|
2693
|
+
const uploadForCommand = { ...upload, headers: uploadHeaders };
|
|
2694
|
+
return asText2({
|
|
2695
|
+
success: true,
|
|
2696
|
+
mode: "hosted-upload-ref-awaiting-upload",
|
|
2697
|
+
message: "Hosted MCP cannot read local files. Run the local upload command, then call firecrawl_parse again with uploadRef. No Firecrawl API key is included in this command.",
|
|
2698
|
+
upload: {
|
|
2699
|
+
command: buildCurlUploadCommand(args2.filePath, uploadForCommand),
|
|
2700
|
+
method: upload.method ?? "PUT",
|
|
2701
|
+
headers: uploadHeaders,
|
|
2702
|
+
fields: upload.fields,
|
|
2703
|
+
uploadUrl: upload.uploadUrl,
|
|
2704
|
+
uploadRef: upload.uploadRef,
|
|
2705
|
+
expiresAt: upload.expiresAt,
|
|
2706
|
+
maxSizeBytes: upload.maxSizeBytes
|
|
2707
|
+
},
|
|
2708
|
+
nextToolCall: {
|
|
2709
|
+
name: "firecrawl_parse",
|
|
2710
|
+
arguments: buildContinuationArguments(upload.uploadRef, options)
|
|
2711
|
+
},
|
|
2712
|
+
notes: [
|
|
2713
|
+
"Run the curl command on the machine that can read filePath.",
|
|
2714
|
+
"After the PUT succeeds, use nextToolCall as the second MCP tool call.",
|
|
2715
|
+
"Clients without a local upload mechanism cannot complete hosted parse for local files."
|
|
2716
|
+
]
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
const parsePayload = {
|
|
2720
|
+
uploadRef: args2.uploadRef,
|
|
2721
|
+
...buildParseOptionsPayload(options)
|
|
2722
|
+
};
|
|
2723
|
+
log.info("Parsing hosted upload reference");
|
|
2724
|
+
const parseJson = await apiPostJsonForSession(
|
|
2725
|
+
"/v2/parse",
|
|
2726
|
+
parsePayload,
|
|
2727
|
+
session
|
|
2728
|
+
);
|
|
2729
|
+
return asText2(parseJson);
|
|
2730
|
+
}
|
|
2490
2731
|
server.addTool({
|
|
2491
2732
|
name: "firecrawl_scrape",
|
|
2492
2733
|
annotations: {
|
|
@@ -3605,24 +3846,28 @@ server.addTool({
|
|
|
3605
3846
|
// Transient page interactions only; does not delete monitors, jobs, or external sites.
|
|
3606
3847
|
},
|
|
3607
3848
|
description: `
|
|
3608
|
-
Interact with a
|
|
3849
|
+
Interact with a page in a live browser session: click buttons, fill forms, extract dynamic content, or navigate deeper.
|
|
3609
3850
|
|
|
3610
3851
|
**Best for:** Multi-step workflows on a single page \u2014 searching a site, clicking through results, filling forms, extracting data that requires interaction.
|
|
3611
|
-
**
|
|
3852
|
+
**Two ways to target a page:**
|
|
3853
|
+
- Pass a \`url\` to interact directly. The session is opened for you in one call (use this for a fresh page).
|
|
3854
|
+
- Pass a \`scrapeId\` from a previous firecrawl_scrape to reuse that already-loaded page (cheaper when you just scraped it).
|
|
3612
3855
|
|
|
3613
3856
|
**Arguments:**
|
|
3614
|
-
-
|
|
3857
|
+
- url: Page to interact with; opens a session for you (use this OR scrapeId)
|
|
3858
|
+
- scrapeId: Scrape job ID from a previous scrape, found in its metadata (use this OR url)
|
|
3615
3859
|
- prompt: Natural language instruction describing the action to take (use this OR code)
|
|
3616
3860
|
- code: Code to execute in the browser session (use this OR prompt)
|
|
3617
3861
|
- language: "bash", "python", or "node" (optional, defaults to "node", only used with code)
|
|
3618
|
-
- timeout:
|
|
3862
|
+
- timeout: Interact execution timeout in seconds, 1-300 (optional, defaults to 30)
|
|
3863
|
+
- scrapeOptions: Optional scrape controls used only with url mode, such as waitFor, maxAge, proxy, or zeroDataRetention
|
|
3619
3864
|
|
|
3620
|
-
**Usage Example (prompt):**
|
|
3865
|
+
**Usage Example (prompt, direct via url):**
|
|
3621
3866
|
\`\`\`json
|
|
3622
3867
|
{
|
|
3623
3868
|
"name": "firecrawl_interact",
|
|
3624
3869
|
"arguments": {
|
|
3625
|
-
"
|
|
3870
|
+
"url": "https://example.com/products",
|
|
3626
3871
|
"prompt": "Click on the first product and tell me its price"
|
|
3627
3872
|
}
|
|
3628
3873
|
}
|
|
@@ -3642,24 +3887,71 @@ Interact with a previously scraped page in a live browser session. Scrape a page
|
|
|
3642
3887
|
**Returns:** Execution result including output, stdout, stderr, exit code, and live view URLs.
|
|
3643
3888
|
`,
|
|
3644
3889
|
parameters: z4.object({
|
|
3645
|
-
scrapeId: z4.string(),
|
|
3646
|
-
|
|
3647
|
-
|
|
3890
|
+
scrapeId: z4.string().trim().min(1).optional(),
|
|
3891
|
+
url: z4.string().trim().url().optional(),
|
|
3892
|
+
prompt: z4.string().trim().min(1).optional(),
|
|
3893
|
+
code: z4.string().trim().min(1).optional(),
|
|
3648
3894
|
language: z4.enum(["bash", "python", "node"]).optional(),
|
|
3649
|
-
timeout: z4.number().min(1).max(300).optional()
|
|
3895
|
+
timeout: z4.number().min(1).max(300).optional(),
|
|
3896
|
+
scrapeOptions: scrapeParamsSchema.omit({ url: true }).partial().optional()
|
|
3897
|
+
}).refine((data) => Boolean(data.scrapeId) !== Boolean(data.url), {
|
|
3898
|
+
message: "Provide either 'url' (interact directly) or 'scrapeId' (reuse a previous scrape), not both."
|
|
3899
|
+
}).refine((data) => !data.scrapeOptions || Boolean(data.url), {
|
|
3900
|
+
message: "scrapeOptions can only be used with 'url' mode."
|
|
3650
3901
|
}).refine((data) => data.code || data.prompt, {
|
|
3651
3902
|
message: "Either 'code' or 'prompt' must be provided."
|
|
3652
3903
|
}),
|
|
3653
3904
|
execute: async (args2, { session, log }) => {
|
|
3654
3905
|
const client = getClient(session);
|
|
3655
|
-
const {
|
|
3656
|
-
|
|
3906
|
+
const {
|
|
3907
|
+
scrapeId: providedScrapeId,
|
|
3908
|
+
url,
|
|
3909
|
+
prompt,
|
|
3910
|
+
code,
|
|
3911
|
+
language,
|
|
3912
|
+
timeout,
|
|
3913
|
+
scrapeOptions
|
|
3914
|
+
} = args2;
|
|
3915
|
+
let scrapeId = providedScrapeId;
|
|
3916
|
+
const openedFromUrl = !scrapeId;
|
|
3917
|
+
if (openedFromUrl) {
|
|
3918
|
+
log.info("Opening interact session from url", { url });
|
|
3919
|
+
const cleanedScrapeOptions = removeEmptyTopLevel(scrapeOptions ?? {});
|
|
3920
|
+
const scraped = await client.scrape(String(url), {
|
|
3921
|
+
...cleanedScrapeOptions,
|
|
3922
|
+
origin: ORIGIN
|
|
3923
|
+
});
|
|
3924
|
+
scrapeId = scraped?.metadata?.scrapeId;
|
|
3925
|
+
if (!scrapeId) {
|
|
3926
|
+
return asText2({
|
|
3927
|
+
error: "Could not open an interact session: the scrape did not return a scrapeId. Try firecrawl_scrape first, then pass its scrapeId.",
|
|
3928
|
+
url
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
if (!scrapeId) {
|
|
3933
|
+
return asText2({
|
|
3934
|
+
error: "Could not open an interact session: missing scrapeId.",
|
|
3935
|
+
url
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
const activeScrapeId = scrapeId;
|
|
3939
|
+
log.info("Interacting with page", { scrapeId: activeScrapeId });
|
|
3657
3940
|
const interactArgs = { origin: ORIGIN };
|
|
3658
3941
|
if (prompt) interactArgs.prompt = prompt;
|
|
3659
3942
|
if (code) interactArgs.code = code;
|
|
3660
3943
|
if (language) interactArgs.language = language;
|
|
3661
3944
|
if (timeout != null) interactArgs.timeout = timeout;
|
|
3662
|
-
const res = await client.interact(
|
|
3945
|
+
const res = await client.interact(activeScrapeId, interactArgs);
|
|
3946
|
+
if (openedFromUrl && res && typeof res === "object" && !Array.isArray(res)) {
|
|
3947
|
+
return asText2({
|
|
3948
|
+
...res,
|
|
3949
|
+
scrapeId: activeScrapeId
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
if (openedFromUrl) {
|
|
3953
|
+
return asText2({ scrapeId: activeScrapeId, result: res });
|
|
3954
|
+
}
|
|
3663
3955
|
return asText2(res);
|
|
3664
3956
|
}
|
|
3665
3957
|
});
|
|
@@ -3702,82 +3994,29 @@ Stop an interact session for a scraped page. Call this when you are done interac
|
|
|
3702
3994
|
return asText2(res?.data ?? {});
|
|
3703
3995
|
}
|
|
3704
3996
|
});
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
"markdown",
|
|
3721
|
-
"html",
|
|
3722
|
-
"rawHtml",
|
|
3723
|
-
"links",
|
|
3724
|
-
"summary",
|
|
3725
|
-
"json",
|
|
3726
|
-
"query"
|
|
3727
|
-
])
|
|
3728
|
-
).optional(),
|
|
3729
|
-
jsonOptions: z4.object({
|
|
3730
|
-
prompt: z4.string().optional(),
|
|
3731
|
-
schema: z4.record(z4.string(), z4.any()).optional()
|
|
3732
|
-
}).optional(),
|
|
3733
|
-
queryOptions: z4.object({
|
|
3734
|
-
prompt: z4.string().max(1e4),
|
|
3735
|
-
mode: z4.enum(["directQuote", "freeform"]).default("freeform")
|
|
3736
|
-
}).optional(),
|
|
3737
|
-
parsers: z4.array(z4.enum(["pdf"])).optional(),
|
|
3738
|
-
pdfOptions: z4.object({
|
|
3739
|
-
maxPages: z4.number().int().min(1).max(1e4).optional()
|
|
3740
|
-
}).optional(),
|
|
3741
|
-
onlyMainContent: z4.boolean().optional(),
|
|
3742
|
-
redactPII: z4.boolean().optional(),
|
|
3743
|
-
includeTags: z4.array(z4.string()).optional(),
|
|
3744
|
-
excludeTags: z4.array(z4.string()).optional(),
|
|
3745
|
-
removeBase64Images: z4.boolean().optional(),
|
|
3746
|
-
skipTlsVerification: z4.boolean().optional(),
|
|
3747
|
-
storeInCache: z4.boolean().optional(),
|
|
3748
|
-
zeroDataRetention: z4.boolean().optional(),
|
|
3749
|
-
maxAge: z4.number().optional(),
|
|
3750
|
-
proxy: z4.enum(["basic", "auto"]).optional()
|
|
3751
|
-
});
|
|
3752
|
-
const EXTENSION_CONTENT_TYPES = {
|
|
3753
|
-
".html": "text/html",
|
|
3754
|
-
".htm": "text/html",
|
|
3755
|
-
".pdf": "application/pdf",
|
|
3756
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3757
|
-
".doc": "application/msword",
|
|
3758
|
-
".odt": "application/vnd.oasis.opendocument.text",
|
|
3759
|
-
".rtf": "application/rtf",
|
|
3760
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
3761
|
-
".xls": "application/vnd.ms-excel"
|
|
3762
|
-
};
|
|
3763
|
-
server.addTool({
|
|
3764
|
-
name: "firecrawl_parse",
|
|
3765
|
-
annotations: {
|
|
3766
|
-
title: "Parse a local file",
|
|
3767
|
-
readOnlyHint: true,
|
|
3768
|
-
// Reads and parses a local file; does not modify the file on disk.
|
|
3769
|
-
openWorldHint: false,
|
|
3770
|
-
// Operates on a local filesystem path, not the open web.
|
|
3771
|
-
destructiveHint: false
|
|
3772
|
-
// Read-only parsing; no deletion or writes to the source file.
|
|
3773
|
-
},
|
|
3774
|
-
description: `
|
|
3775
|
-
Parse a file from the local filesystem using a self-hosted Firecrawl API's /v2/parse endpoint.
|
|
3776
|
-
This is the fastest and most reliable way to extract content from a document on disk \u2014 if the file lives locally and the MCP is pointed at a self-hosted Firecrawl instance, you should always prefer this tool over uploading the file elsewhere and then scraping it.
|
|
3997
|
+
server.addTool({
|
|
3998
|
+
name: "firecrawl_parse",
|
|
3999
|
+
annotations: {
|
|
4000
|
+
title: "Parse a local file",
|
|
4001
|
+
readOnlyHint: true,
|
|
4002
|
+
// Local mode reads a file; hosted mode only returns upload instructions or parses an uploadRef.
|
|
4003
|
+
openWorldHint: false,
|
|
4004
|
+
// Operates on a local filesystem path/upload reference, not an arbitrary web URL.
|
|
4005
|
+
destructiveHint: false
|
|
4006
|
+
// Read-only parsing; no deletion or writes to the source file.
|
|
4007
|
+
},
|
|
4008
|
+
description: `
|
|
4009
|
+
Parse a file using Firecrawl's /v2/parse endpoint.
|
|
4010
|
+
|
|
4011
|
+
In local/non-cloud MCP mode, this tool reads filePath from the MCP server filesystem and posts multipart data to the configured self-hosted FIRECRAWL_API_URL, preserving the existing direct-read behavior.
|
|
3777
4012
|
|
|
3778
|
-
|
|
4013
|
+
In hosted CLOUD_SERVICE mode, this tool is a two-call flow because hosted MCP cannot read your local filesystem:
|
|
4014
|
+
1. Call with filePath, contentType, parse options, and optional declaredSizeBytes. The hosted server mints a short-lived upload URL and returns a safe local curl PUT command plus nextToolCall.
|
|
4015
|
+
2. Run the returned curl command locally, then call firecrawl_parse again with uploadRef and the desired parse options. The hosted server calls /v2/parse server-side with your session credential.
|
|
4016
|
+
|
|
4017
|
+
**Best for:** Extracting content from a local document (PDF, Word, Excel, HTML, etc.); pulling structured data out of a file with JSON format; converting binary documents into markdown for downstream reasoning.
|
|
3779
4018
|
**Not recommended for:** Remote URLs (use firecrawl_scrape); multiple files at once (call parse multiple times); documents that require interactive actions, screenshots, or change tracking \u2014 those aren't supported by the parse endpoint.
|
|
3780
|
-
**Common mistakes:**
|
|
4019
|
+
**Common mistakes:** In hosted mode, do not pass both filePath and uploadRef. Phase 1 uses filePath only to generate upload instructions; phase 2 uses uploadRef only to parse server-side.
|
|
3781
4020
|
|
|
3782
4021
|
**Supported file types:** .html, .htm, .xhtml, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls
|
|
3783
4022
|
**Unsupported options:** actions, screenshot/branding/changeTracking formats, waitFor > 0, location, mobile, proxy values other than "auto" or "basic".
|
|
@@ -3786,122 +4025,96 @@ This is the fastest and most reliable way to extract content from a document on
|
|
|
3786
4025
|
**CRITICAL - Format Selection (same rules as firecrawl_scrape):**
|
|
3787
4026
|
When the user asks for SPECIFIC data points from a document, you MUST use JSON format with a schema. Only use markdown when the user needs the ENTIRE document content.
|
|
3788
4027
|
|
|
3789
|
-
**Use JSON format when the user asks for:**
|
|
3790
|
-
- Specific fields, parameters, or values from a form / PDF / spreadsheet
|
|
3791
|
-
- Prices, numbers, or other structured data
|
|
3792
|
-
- Lists of items or properties
|
|
3793
|
-
|
|
3794
|
-
**Use markdown format when:**
|
|
3795
|
-
- User wants to read, summarize, or analyze the full document
|
|
3796
|
-
- User explicitly asks for the complete content
|
|
3797
|
-
|
|
3798
4028
|
**Handling PDFs:**
|
|
3799
4029
|
Add \`"parsers": ["pdf"]\` (optionally with \`pdfOptions.maxPages\`) when parsing a PDF so the PDF engine is invoked explicitly. For very long documents, cap \`maxPages\` to keep the response within token limits.
|
|
3800
4030
|
|
|
3801
|
-
**
|
|
4031
|
+
**Hosted phase 1 example:**
|
|
3802
4032
|
\`\`\`json
|
|
3803
4033
|
{
|
|
3804
4034
|
"name": "firecrawl_parse",
|
|
3805
4035
|
"arguments": {
|
|
3806
4036
|
"filePath": "/absolute/path/to/document.pdf",
|
|
4037
|
+
"contentType": "application/pdf",
|
|
3807
4038
|
"formats": ["markdown"],
|
|
3808
4039
|
"parsers": ["pdf"],
|
|
3809
|
-
"
|
|
4040
|
+
"zeroDataRetention": true
|
|
3810
4041
|
}
|
|
3811
4042
|
}
|
|
3812
4043
|
\`\`\`
|
|
3813
4044
|
|
|
3814
|
-
**
|
|
4045
|
+
**Hosted phase 2 example:**
|
|
3815
4046
|
\`\`\`json
|
|
3816
4047
|
{
|
|
3817
4048
|
"name": "firecrawl_parse",
|
|
3818
4049
|
"arguments": {
|
|
3819
|
-
"
|
|
3820
|
-
"formats": ["
|
|
3821
|
-
"
|
|
3822
|
-
|
|
3823
|
-
"schema": {
|
|
3824
|
-
"type": "object",
|
|
3825
|
-
"properties": {
|
|
3826
|
-
"invoiceNumber": { "type": "string" },
|
|
3827
|
-
"total": { "type": "number" },
|
|
3828
|
-
"lineItems": {
|
|
3829
|
-
"type": "array",
|
|
3830
|
-
"items": {
|
|
3831
|
-
"type": "object",
|
|
3832
|
-
"properties": {
|
|
3833
|
-
"description": { "type": "string" },
|
|
3834
|
-
"amount": { "type": "number" }
|
|
3835
|
-
}
|
|
3836
|
-
}
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
4050
|
+
"uploadRef": "upload-ref-from-phase-1",
|
|
4051
|
+
"formats": ["markdown"],
|
|
4052
|
+
"parsers": ["pdf"],
|
|
4053
|
+
"zeroDataRetention": true
|
|
3841
4054
|
}
|
|
3842
4055
|
}
|
|
3843
4056
|
\`\`\`
|
|
3844
|
-
|
|
4057
|
+
|
|
4058
|
+
**Returns:** Phase 1 hosted upload instructions or a parsed document with markdown, html, links, summary, json, or query results depending on the requested formats.
|
|
3845
4059
|
`,
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
filePath,
|
|
3856
|
-
contentType: overrideContentType,
|
|
3857
|
-
...options
|
|
3858
|
-
} = args2;
|
|
3859
|
-
const absPath = path.resolve(filePath);
|
|
3860
|
-
const buffer = await readFile(absPath);
|
|
3861
|
-
const filename = path.basename(absPath);
|
|
3862
|
-
const fileContentType = overrideContentType && overrideContentType.length > 0 ? overrideContentType : inferContentType(filename);
|
|
3863
|
-
const transformed = transformScrapeParams(
|
|
3864
|
-
options
|
|
4060
|
+
parameters: parseParamsSchema,
|
|
4061
|
+
execute: async (args2, { session, log }) => {
|
|
4062
|
+
if (process.env.CLOUD_SERVICE === "true") {
|
|
4063
|
+
return executeHostedParse(args2, session, log);
|
|
4064
|
+
}
|
|
4065
|
+
const apiUrl = process.env.FIRECRAWL_API_URL;
|
|
4066
|
+
if (!apiUrl) {
|
|
4067
|
+
throw new Error(
|
|
4068
|
+
"firecrawl_parse requires FIRECRAWL_API_URL to be set to a self-hosted Firecrawl API instance."
|
|
3865
4069
|
);
|
|
3866
|
-
const cleaned = removeEmptyTopLevel(transformed);
|
|
3867
|
-
const optionsPayload = { origin: ORIGIN, ...cleaned };
|
|
3868
|
-
const form = new FormData();
|
|
3869
|
-
const blob = new Blob([new Uint8Array(buffer)], {
|
|
3870
|
-
type: fileContentType
|
|
3871
|
-
});
|
|
3872
|
-
form.append("file", blob, filename);
|
|
3873
|
-
form.append("options", JSON.stringify(optionsPayload));
|
|
3874
|
-
const headers = { ...ORIGIN_HEADERS2 };
|
|
3875
|
-
const apiKey = session?.firecrawlApiKey;
|
|
3876
|
-
if (apiKey) {
|
|
3877
|
-
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3878
|
-
}
|
|
3879
|
-
const endpoint = `${apiUrl.replace(/\/$/, "")}/v2/parse`;
|
|
3880
|
-
log.info("Parsing local file", {
|
|
3881
|
-
endpoint,
|
|
3882
|
-
filename,
|
|
3883
|
-
size: buffer.length
|
|
3884
|
-
});
|
|
3885
|
-
const response = await fetch(endpoint, {
|
|
3886
|
-
method: "POST",
|
|
3887
|
-
headers,
|
|
3888
|
-
body: form
|
|
3889
|
-
});
|
|
3890
|
-
const responseText = await response.text();
|
|
3891
|
-
if (!response.ok) {
|
|
3892
|
-
throw new Error(
|
|
3893
|
-
`Parse request failed with status ${response.status}: ${responseText}`
|
|
3894
|
-
);
|
|
3895
|
-
}
|
|
3896
|
-
try {
|
|
3897
|
-
return asText2(JSON.parse(responseText));
|
|
3898
|
-
} catch {
|
|
3899
|
-
return responseText;
|
|
3900
|
-
}
|
|
3901
4070
|
}
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4071
|
+
const {
|
|
4072
|
+
filePath,
|
|
4073
|
+
contentType: overrideContentType,
|
|
4074
|
+
...options
|
|
4075
|
+
} = args2;
|
|
4076
|
+
const absPath = path.resolve(filePath);
|
|
4077
|
+
const buffer = await readFile(absPath);
|
|
4078
|
+
const filename = path.basename(absPath);
|
|
4079
|
+
const fileContentType = overrideContentType && overrideContentType.length > 0 ? overrideContentType : inferContentType(filename);
|
|
4080
|
+
const optionsPayload = buildParseOptionsPayload(
|
|
4081
|
+
options
|
|
4082
|
+
);
|
|
4083
|
+
const form = new FormData();
|
|
4084
|
+
const blob = new Blob([new Uint8Array(buffer)], {
|
|
4085
|
+
type: fileContentType
|
|
4086
|
+
});
|
|
4087
|
+
form.append("file", blob, filename);
|
|
4088
|
+
form.append("options", JSON.stringify(optionsPayload));
|
|
4089
|
+
const headers = { ...ORIGIN_HEADERS2 };
|
|
4090
|
+
const apiKey = session?.firecrawlApiKey;
|
|
4091
|
+
if (apiKey) {
|
|
4092
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
4093
|
+
}
|
|
4094
|
+
const endpoint = `${apiUrl.replace(/\/$/, "")}/v2/parse`;
|
|
4095
|
+
log.info("Parsing local file", {
|
|
4096
|
+
endpoint,
|
|
4097
|
+
filename,
|
|
4098
|
+
size: buffer.length
|
|
4099
|
+
});
|
|
4100
|
+
const response = await fetch(endpoint, {
|
|
4101
|
+
method: "POST",
|
|
4102
|
+
headers,
|
|
4103
|
+
body: form
|
|
4104
|
+
});
|
|
4105
|
+
const responseText = await response.text();
|
|
4106
|
+
if (!response.ok) {
|
|
4107
|
+
throw new Error(
|
|
4108
|
+
`Parse request failed with status ${response.status}: ${responseText}`
|
|
4109
|
+
);
|
|
4110
|
+
}
|
|
4111
|
+
try {
|
|
4112
|
+
return asText2(JSON.parse(responseText));
|
|
4113
|
+
} catch {
|
|
4114
|
+
return responseText;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
});
|
|
3905
4118
|
var PORT = Number(process.env.PORT || 3e3);
|
|
3906
4119
|
var HOST = process.env.CLOUD_SERVICE === "true" ? "0.0.0.0" : process.env.HOST || "localhost";
|
|
3907
4120
|
var args;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firecrawl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.0",
|
|
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",
|