firecrawl-mcp 3.21.2 → 3.21.4
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/dist/index.js +401 -172
- package/package.json +1 -1
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: {
|
|
@@ -2880,6 +3121,61 @@ async function keylessPost(path2, body, session) {
|
|
|
2880
3121
|
}
|
|
2881
3122
|
return json;
|
|
2882
3123
|
}
|
|
3124
|
+
async function getCrawlStatusWithOrigin(client, jobId) {
|
|
3125
|
+
const res = await client.http.get(
|
|
3126
|
+
`/v2/crawl/${encodeURIComponent(jobId)}`,
|
|
3127
|
+
ORIGIN_HEADERS2
|
|
3128
|
+
);
|
|
3129
|
+
const body = res?.data ?? {};
|
|
3130
|
+
const initialDocs = Array.isArray(body.data) ? body.data : [];
|
|
3131
|
+
if (!body.next) {
|
|
3132
|
+
return {
|
|
3133
|
+
id: jobId,
|
|
3134
|
+
status: body.status,
|
|
3135
|
+
completed: body.completed ?? 0,
|
|
3136
|
+
total: body.total ?? 0,
|
|
3137
|
+
creditsUsed: body.creditsUsed,
|
|
3138
|
+
expiresAt: body.expiresAt,
|
|
3139
|
+
next: body.next ?? null,
|
|
3140
|
+
data: initialDocs
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
const docs = initialDocs.slice();
|
|
3144
|
+
let current = body.next;
|
|
3145
|
+
while (current) {
|
|
3146
|
+
const pageRes = await client.http.get(current, ORIGIN_HEADERS2);
|
|
3147
|
+
const payload = pageRes?.data ?? {};
|
|
3148
|
+
if (!payload.success) break;
|
|
3149
|
+
const pageData = Array.isArray(payload.data) ? payload.data : payload.data?.pages || [];
|
|
3150
|
+
docs.push(...pageData);
|
|
3151
|
+
current = payload.next ?? (Array.isArray(payload.data) ? null : payload.data?.next) ?? null;
|
|
3152
|
+
}
|
|
3153
|
+
return {
|
|
3154
|
+
id: jobId,
|
|
3155
|
+
status: body.status,
|
|
3156
|
+
completed: body.completed ?? 0,
|
|
3157
|
+
total: body.total ?? 0,
|
|
3158
|
+
creditsUsed: body.creditsUsed,
|
|
3159
|
+
expiresAt: body.expiresAt,
|
|
3160
|
+
next: null,
|
|
3161
|
+
data: docs
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
async function waitForCrawlCompletionWithOrigin(client, jobId, pollInterval = 2, timeout) {
|
|
3165
|
+
const startedAt = Date.now();
|
|
3166
|
+
while (true) {
|
|
3167
|
+
const status = await getCrawlStatusWithOrigin(client, jobId);
|
|
3168
|
+
if (["completed", "failed", "cancelled"].includes(String(status.status ?? ""))) {
|
|
3169
|
+
return status;
|
|
3170
|
+
}
|
|
3171
|
+
if (timeout != null && Date.now() - startedAt > timeout * 1e3) {
|
|
3172
|
+
throw new Error(`Crawl job ${jobId} did not complete within ${timeout}s`);
|
|
3173
|
+
}
|
|
3174
|
+
await new Promise(
|
|
3175
|
+
(resolve) => setTimeout(resolve, Math.max(1e3, pollInterval * 1e3))
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
2883
3179
|
var feedbackIssueSchema = z4.string().trim().min(1).max(80).regex(
|
|
2884
3180
|
/^[a-z0-9][a-z0-9_-]*$/,
|
|
2885
3181
|
"Issue codes must use lowercase letters, numbers, underscores, or hyphens"
|
|
@@ -3255,11 +3551,26 @@ server.addTool({
|
|
|
3255
3551
|
if (webhook) opts.webhook = webhook;
|
|
3256
3552
|
delete opts.webhookHeaders;
|
|
3257
3553
|
const cleaned = removeEmptyTopLevel(opts);
|
|
3554
|
+
const pollInterval = typeof cleaned.pollInterval === "number" ? cleaned.pollInterval : 2;
|
|
3555
|
+
const timeout = typeof cleaned.timeout === "number" ? cleaned.timeout : void 0;
|
|
3556
|
+
delete cleaned.pollInterval;
|
|
3557
|
+
delete cleaned.timeout;
|
|
3258
3558
|
log.info("Starting crawl", { url: String(url) });
|
|
3259
|
-
const
|
|
3559
|
+
const started = await client.http.post("/v2/crawl", {
|
|
3560
|
+
url: String(url),
|
|
3260
3561
|
...cleaned,
|
|
3261
3562
|
origin: ORIGIN
|
|
3262
3563
|
});
|
|
3564
|
+
const crawlId = started?.data?.id;
|
|
3565
|
+
if (!crawlId) {
|
|
3566
|
+
return asText2(started?.data ?? {});
|
|
3567
|
+
}
|
|
3568
|
+
const res = await waitForCrawlCompletionWithOrigin(
|
|
3569
|
+
client,
|
|
3570
|
+
crawlId,
|
|
3571
|
+
pollInterval,
|
|
3572
|
+
timeout
|
|
3573
|
+
);
|
|
3263
3574
|
return asText2(res);
|
|
3264
3575
|
}
|
|
3265
3576
|
});
|
|
@@ -3292,11 +3603,8 @@ Check the status of a crawl job.
|
|
|
3292
3603
|
execute: async (args2, { session }) => {
|
|
3293
3604
|
const client = getClient(session);
|
|
3294
3605
|
const id = args2.id;
|
|
3295
|
-
const res = await client
|
|
3296
|
-
|
|
3297
|
-
ORIGIN_HEADERS2
|
|
3298
|
-
);
|
|
3299
|
-
return asText2(res?.data ?? {});
|
|
3606
|
+
const res = await getCrawlStatusWithOrigin(client, id);
|
|
3607
|
+
return asText2(res);
|
|
3300
3608
|
}
|
|
3301
3609
|
});
|
|
3302
3610
|
server.addTool({
|
|
@@ -3635,82 +3943,29 @@ Stop an interact session for a scraped page. Call this when you are done interac
|
|
|
3635
3943
|
return asText2(res?.data ?? {});
|
|
3636
3944
|
}
|
|
3637
3945
|
});
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
"summary",
|
|
3658
|
-
"json",
|
|
3659
|
-
"query"
|
|
3660
|
-
])
|
|
3661
|
-
).optional(),
|
|
3662
|
-
jsonOptions: z4.object({
|
|
3663
|
-
prompt: z4.string().optional(),
|
|
3664
|
-
schema: z4.record(z4.string(), z4.any()).optional()
|
|
3665
|
-
}).optional(),
|
|
3666
|
-
queryOptions: z4.object({
|
|
3667
|
-
prompt: z4.string().max(1e4),
|
|
3668
|
-
mode: z4.enum(["directQuote", "freeform"]).default("freeform")
|
|
3669
|
-
}).optional(),
|
|
3670
|
-
parsers: z4.array(z4.enum(["pdf"])).optional(),
|
|
3671
|
-
pdfOptions: z4.object({
|
|
3672
|
-
maxPages: z4.number().int().min(1).max(1e4).optional()
|
|
3673
|
-
}).optional(),
|
|
3674
|
-
onlyMainContent: z4.boolean().optional(),
|
|
3675
|
-
redactPII: z4.boolean().optional(),
|
|
3676
|
-
includeTags: z4.array(z4.string()).optional(),
|
|
3677
|
-
excludeTags: z4.array(z4.string()).optional(),
|
|
3678
|
-
removeBase64Images: z4.boolean().optional(),
|
|
3679
|
-
skipTlsVerification: z4.boolean().optional(),
|
|
3680
|
-
storeInCache: z4.boolean().optional(),
|
|
3681
|
-
zeroDataRetention: z4.boolean().optional(),
|
|
3682
|
-
maxAge: z4.number().optional(),
|
|
3683
|
-
proxy: z4.enum(["basic", "auto"]).optional()
|
|
3684
|
-
});
|
|
3685
|
-
const EXTENSION_CONTENT_TYPES = {
|
|
3686
|
-
".html": "text/html",
|
|
3687
|
-
".htm": "text/html",
|
|
3688
|
-
".pdf": "application/pdf",
|
|
3689
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3690
|
-
".doc": "application/msword",
|
|
3691
|
-
".odt": "application/vnd.oasis.opendocument.text",
|
|
3692
|
-
".rtf": "application/rtf",
|
|
3693
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
3694
|
-
".xls": "application/vnd.ms-excel"
|
|
3695
|
-
};
|
|
3696
|
-
server.addTool({
|
|
3697
|
-
name: "firecrawl_parse",
|
|
3698
|
-
annotations: {
|
|
3699
|
-
title: "Parse a local file",
|
|
3700
|
-
readOnlyHint: true,
|
|
3701
|
-
// Reads and parses a local file; does not modify the file on disk.
|
|
3702
|
-
openWorldHint: false,
|
|
3703
|
-
// Operates on a local filesystem path, not the open web.
|
|
3704
|
-
destructiveHint: false
|
|
3705
|
-
// Read-only parsing; no deletion or writes to the source file.
|
|
3706
|
-
},
|
|
3707
|
-
description: `
|
|
3708
|
-
Parse a file from the local filesystem using a self-hosted Firecrawl API's /v2/parse endpoint.
|
|
3709
|
-
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.
|
|
3946
|
+
server.addTool({
|
|
3947
|
+
name: "firecrawl_parse",
|
|
3948
|
+
annotations: {
|
|
3949
|
+
title: "Parse a local file",
|
|
3950
|
+
readOnlyHint: true,
|
|
3951
|
+
// Local mode reads a file; hosted mode only returns upload instructions or parses an uploadRef.
|
|
3952
|
+
openWorldHint: false,
|
|
3953
|
+
// Operates on a local filesystem path/upload reference, not an arbitrary web URL.
|
|
3954
|
+
destructiveHint: false
|
|
3955
|
+
// Read-only parsing; no deletion or writes to the source file.
|
|
3956
|
+
},
|
|
3957
|
+
description: `
|
|
3958
|
+
Parse a file using Firecrawl's /v2/parse endpoint.
|
|
3959
|
+
|
|
3960
|
+
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.
|
|
3961
|
+
|
|
3962
|
+
In hosted CLOUD_SERVICE mode, this tool is a two-call flow because hosted MCP cannot read your local filesystem:
|
|
3963
|
+
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.
|
|
3964
|
+
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.
|
|
3710
3965
|
|
|
3711
|
-
**Best for:** Extracting content from a local document (PDF, Word, Excel, HTML, etc.)
|
|
3966
|
+
**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.
|
|
3712
3967
|
**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.
|
|
3713
|
-
**Common mistakes:**
|
|
3968
|
+
**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.
|
|
3714
3969
|
|
|
3715
3970
|
**Supported file types:** .html, .htm, .xhtml, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls
|
|
3716
3971
|
**Unsupported options:** actions, screenshot/branding/changeTracking formats, waitFor > 0, location, mobile, proxy values other than "auto" or "basic".
|
|
@@ -3719,122 +3974,96 @@ This is the fastest and most reliable way to extract content from a document on
|
|
|
3719
3974
|
**CRITICAL - Format Selection (same rules as firecrawl_scrape):**
|
|
3720
3975
|
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.
|
|
3721
3976
|
|
|
3722
|
-
**Use JSON format when the user asks for:**
|
|
3723
|
-
- Specific fields, parameters, or values from a form / PDF / spreadsheet
|
|
3724
|
-
- Prices, numbers, or other structured data
|
|
3725
|
-
- Lists of items or properties
|
|
3726
|
-
|
|
3727
|
-
**Use markdown format when:**
|
|
3728
|
-
- User wants to read, summarize, or analyze the full document
|
|
3729
|
-
- User explicitly asks for the complete content
|
|
3730
|
-
|
|
3731
3977
|
**Handling PDFs:**
|
|
3732
3978
|
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.
|
|
3733
3979
|
|
|
3734
|
-
**
|
|
3980
|
+
**Hosted phase 1 example:**
|
|
3735
3981
|
\`\`\`json
|
|
3736
3982
|
{
|
|
3737
3983
|
"name": "firecrawl_parse",
|
|
3738
3984
|
"arguments": {
|
|
3739
3985
|
"filePath": "/absolute/path/to/document.pdf",
|
|
3986
|
+
"contentType": "application/pdf",
|
|
3740
3987
|
"formats": ["markdown"],
|
|
3741
3988
|
"parsers": ["pdf"],
|
|
3742
|
-
"
|
|
3989
|
+
"zeroDataRetention": true
|
|
3743
3990
|
}
|
|
3744
3991
|
}
|
|
3745
3992
|
\`\`\`
|
|
3746
3993
|
|
|
3747
|
-
**
|
|
3994
|
+
**Hosted phase 2 example:**
|
|
3748
3995
|
\`\`\`json
|
|
3749
3996
|
{
|
|
3750
3997
|
"name": "firecrawl_parse",
|
|
3751
3998
|
"arguments": {
|
|
3752
|
-
"
|
|
3753
|
-
"formats": ["
|
|
3754
|
-
"
|
|
3755
|
-
|
|
3756
|
-
"schema": {
|
|
3757
|
-
"type": "object",
|
|
3758
|
-
"properties": {
|
|
3759
|
-
"invoiceNumber": { "type": "string" },
|
|
3760
|
-
"total": { "type": "number" },
|
|
3761
|
-
"lineItems": {
|
|
3762
|
-
"type": "array",
|
|
3763
|
-
"items": {
|
|
3764
|
-
"type": "object",
|
|
3765
|
-
"properties": {
|
|
3766
|
-
"description": { "type": "string" },
|
|
3767
|
-
"amount": { "type": "number" }
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3999
|
+
"uploadRef": "upload-ref-from-phase-1",
|
|
4000
|
+
"formats": ["markdown"],
|
|
4001
|
+
"parsers": ["pdf"],
|
|
4002
|
+
"zeroDataRetention": true
|
|
3774
4003
|
}
|
|
3775
4004
|
}
|
|
3776
4005
|
\`\`\`
|
|
3777
|
-
|
|
4006
|
+
|
|
4007
|
+
**Returns:** Phase 1 hosted upload instructions or a parsed document with markdown, html, links, summary, json, or query results depending on the requested formats.
|
|
3778
4008
|
`,
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
filePath,
|
|
3789
|
-
contentType: overrideContentType,
|
|
3790
|
-
...options
|
|
3791
|
-
} = args2;
|
|
3792
|
-
const absPath = path.resolve(filePath);
|
|
3793
|
-
const buffer = await readFile(absPath);
|
|
3794
|
-
const filename = path.basename(absPath);
|
|
3795
|
-
const fileContentType = overrideContentType && overrideContentType.length > 0 ? overrideContentType : inferContentType(filename);
|
|
3796
|
-
const transformed = transformScrapeParams(
|
|
3797
|
-
options
|
|
4009
|
+
parameters: parseParamsSchema,
|
|
4010
|
+
execute: async (args2, { session, log }) => {
|
|
4011
|
+
if (process.env.CLOUD_SERVICE === "true") {
|
|
4012
|
+
return executeHostedParse(args2, session, log);
|
|
4013
|
+
}
|
|
4014
|
+
const apiUrl = process.env.FIRECRAWL_API_URL;
|
|
4015
|
+
if (!apiUrl) {
|
|
4016
|
+
throw new Error(
|
|
4017
|
+
"firecrawl_parse requires FIRECRAWL_API_URL to be set to a self-hosted Firecrawl API instance."
|
|
3798
4018
|
);
|
|
3799
|
-
const cleaned = removeEmptyTopLevel(transformed);
|
|
3800
|
-
const optionsPayload = { origin: ORIGIN, ...cleaned };
|
|
3801
|
-
const form = new FormData();
|
|
3802
|
-
const blob = new Blob([new Uint8Array(buffer)], {
|
|
3803
|
-
type: fileContentType
|
|
3804
|
-
});
|
|
3805
|
-
form.append("file", blob, filename);
|
|
3806
|
-
form.append("options", JSON.stringify(optionsPayload));
|
|
3807
|
-
const headers = { ...ORIGIN_HEADERS2 };
|
|
3808
|
-
const apiKey = session?.firecrawlApiKey;
|
|
3809
|
-
if (apiKey) {
|
|
3810
|
-
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3811
|
-
}
|
|
3812
|
-
const endpoint = `${apiUrl.replace(/\/$/, "")}/v2/parse`;
|
|
3813
|
-
log.info("Parsing local file", {
|
|
3814
|
-
endpoint,
|
|
3815
|
-
filename,
|
|
3816
|
-
size: buffer.length
|
|
3817
|
-
});
|
|
3818
|
-
const response = await fetch(endpoint, {
|
|
3819
|
-
method: "POST",
|
|
3820
|
-
headers,
|
|
3821
|
-
body: form
|
|
3822
|
-
});
|
|
3823
|
-
const responseText = await response.text();
|
|
3824
|
-
if (!response.ok) {
|
|
3825
|
-
throw new Error(
|
|
3826
|
-
`Parse request failed with status ${response.status}: ${responseText}`
|
|
3827
|
-
);
|
|
3828
|
-
}
|
|
3829
|
-
try {
|
|
3830
|
-
return asText2(JSON.parse(responseText));
|
|
3831
|
-
} catch {
|
|
3832
|
-
return responseText;
|
|
3833
|
-
}
|
|
3834
4019
|
}
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
4020
|
+
const {
|
|
4021
|
+
filePath,
|
|
4022
|
+
contentType: overrideContentType,
|
|
4023
|
+
...options
|
|
4024
|
+
} = args2;
|
|
4025
|
+
const absPath = path.resolve(filePath);
|
|
4026
|
+
const buffer = await readFile(absPath);
|
|
4027
|
+
const filename = path.basename(absPath);
|
|
4028
|
+
const fileContentType = overrideContentType && overrideContentType.length > 0 ? overrideContentType : inferContentType(filename);
|
|
4029
|
+
const optionsPayload = buildParseOptionsPayload(
|
|
4030
|
+
options
|
|
4031
|
+
);
|
|
4032
|
+
const form = new FormData();
|
|
4033
|
+
const blob = new Blob([new Uint8Array(buffer)], {
|
|
4034
|
+
type: fileContentType
|
|
4035
|
+
});
|
|
4036
|
+
form.append("file", blob, filename);
|
|
4037
|
+
form.append("options", JSON.stringify(optionsPayload));
|
|
4038
|
+
const headers = { ...ORIGIN_HEADERS2 };
|
|
4039
|
+
const apiKey = session?.firecrawlApiKey;
|
|
4040
|
+
if (apiKey) {
|
|
4041
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
4042
|
+
}
|
|
4043
|
+
const endpoint = `${apiUrl.replace(/\/$/, "")}/v2/parse`;
|
|
4044
|
+
log.info("Parsing local file", {
|
|
4045
|
+
endpoint,
|
|
4046
|
+
filename,
|
|
4047
|
+
size: buffer.length
|
|
4048
|
+
});
|
|
4049
|
+
const response = await fetch(endpoint, {
|
|
4050
|
+
method: "POST",
|
|
4051
|
+
headers,
|
|
4052
|
+
body: form
|
|
4053
|
+
});
|
|
4054
|
+
const responseText = await response.text();
|
|
4055
|
+
if (!response.ok) {
|
|
4056
|
+
throw new Error(
|
|
4057
|
+
`Parse request failed with status ${response.status}: ${responseText}`
|
|
4058
|
+
);
|
|
4059
|
+
}
|
|
4060
|
+
try {
|
|
4061
|
+
return asText2(JSON.parse(responseText));
|
|
4062
|
+
} catch {
|
|
4063
|
+
return responseText;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
});
|
|
3838
4067
|
var PORT = Number(process.env.PORT || 3e3);
|
|
3839
4068
|
var HOST = process.env.CLOUD_SERVICE === "true" ? "0.0.0.0" : process.env.HOST || "localhost";
|
|
3840
4069
|
var args;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firecrawl-mcp",
|
|
3
|
-
"version": "3.21.
|
|
3
|
+
"version": "3.21.4",
|
|
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",
|