firecrawl-mcp 3.21.0 → 3.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firecrawl-mcp",
3
- "version": "3.21.0",
3
+ "version": "3.21.1",
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",
@@ -16,10 +16,20 @@
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
18
  "@mendable/firecrawl-js": "4.25.2",
19
+ "@modelcontextprotocol/sdk": "1.18.0",
20
+ "@standard-schema/spec": "^1.0.0",
19
21
  "dotenv": "^17.2.2",
20
- "firecrawl-fastmcp": "^1.0.5",
22
+ "execa": "^9.6.0",
23
+ "file-type": "^21.0.0",
24
+ "fuse.js": "^7.1.0",
25
+ "mcp-proxy": "^6.5.1",
26
+ "strict-event-emitter-types": "^2.0.0",
21
27
  "typescript": "^5.9.2",
22
- "zod": "^4.1.5"
28
+ "uri-templates": "^0.2.0",
29
+ "xsschema": "0.3.5",
30
+ "yargs": "^18.0.0",
31
+ "zod": "^4.1.5",
32
+ "zod-to-json-schema": "^3.24.6"
23
33
  },
24
34
  "engines": {
25
35
  "node": ">=18.0.0"
@@ -41,12 +51,14 @@
41
51
  },
42
52
  "homepage": "https://github.com/firecrawl/firecrawl-mcp-server#readme",
43
53
  "devDependencies": {
44
- "@types/node": "^24.3.1"
54
+ "@types/node": "^24.3.1",
55
+ "@types/uri-templates": "^0.1.34",
56
+ "@types/yargs": "^17.0.33",
57
+ "tsup": "^8.5.0"
45
58
  },
46
59
  "scripts": {
47
- "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
48
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
49
- "test:endpoints": "node test-endpoints.js",
60
+ "build": "rm -rf dist && tsup && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
61
+ "test": "echo 'No tests'",
50
62
  "start": "node dist/index.js",
51
63
  "start:cloud": "CLOUD_SERVICE=true node dist/index.js",
52
64
  "lint": "eslint src/**/*.ts",
package/dist/monitor.js DELETED
@@ -1,478 +0,0 @@
1
- /**
2
- * Firecrawl Monitor tools.
3
- *
4
- * Monitors run recurring scrapes/crawls and diff each result against the last
5
- * retained snapshot. The SDK exposes monitor methods, but its HttpClient
6
- * injects a top-level `origin` field into every POST/PATCH body and
7
- * /v2/monitor rejects that with "Unrecognized key in body". Until the SDK
8
- * strips `origin` for monitor requests, we hit /v2/monitor directly via fetch
9
- * — same pattern the CLI uses.
10
- */
11
- import { z } from 'zod';
12
- const DEFAULT_API_URL = 'https://api.firecrawl.dev';
13
- function resolveAuth(session) {
14
- const apiKey = session?.firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY;
15
- const baseUrl = (process.env.FIRECRAWL_API_URL ?? DEFAULT_API_URL).replace(/\/$/, '');
16
- return { apiKey, baseUrl };
17
- }
18
- async function monitorRequest(session, path, init = {}) {
19
- const { apiKey, baseUrl } = resolveAuth(session);
20
- if (!apiKey && !process.env.FIRECRAWL_API_URL) {
21
- throw new Error('Unauthorized: API key is required for monitor requests');
22
- }
23
- let url = `${baseUrl}/v2${path}`;
24
- if (init.query) {
25
- const qs = new URLSearchParams();
26
- for (const [k, v] of Object.entries(init.query)) {
27
- if (v !== undefined && v !== null && v !== '')
28
- qs.set(k, String(v));
29
- }
30
- const s = qs.toString();
31
- if (s)
32
- url += `?${s}`;
33
- }
34
- const headers = { 'X-Origin': 'mcp' };
35
- if (apiKey)
36
- headers.Authorization = `Bearer ${apiKey}`;
37
- if (init.body !== undefined)
38
- headers['Content-Type'] = 'application/json';
39
- const response = await fetch(url, {
40
- method: init.method ?? 'GET',
41
- headers,
42
- body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
43
- });
44
- const payload = (await response.json().catch(() => ({})));
45
- if (!response.ok || payload?.success === false) {
46
- const message = payload?.error ||
47
- `HTTP ${response.status}: ${response.statusText || 'Request failed'}`;
48
- throw new Error(message);
49
- }
50
- return payload;
51
- }
52
- function asText(data) {
53
- return JSON.stringify(data, null, 2);
54
- }
55
- const pageStatusSchema = z.enum(['same', 'new', 'changed', 'removed', 'error']);
56
- const checkStatusSchema = z.enum([
57
- 'queued',
58
- 'running',
59
- 'completed',
60
- 'failed',
61
- 'partial',
62
- 'skipped_overlap',
63
- ]);
64
- function splitPages(page, pages) {
65
- return [page, ...(pages ?? [])]
66
- .filter((url) => typeof url === 'string')
67
- .map(url => url.trim())
68
- .filter(Boolean);
69
- }
70
- function buildMonitorCreateBody(args) {
71
- if (args.body && typeof args.body === 'object' && !Array.isArray(args.body)) {
72
- return args.body;
73
- }
74
- const urls = splitPages(args.page, args.pages);
75
- if (urls.length === 0) {
76
- throw new Error('firecrawl_monitor_create requires either `body`, `page`, or `pages`.');
77
- }
78
- const goal = typeof args.goal === 'string' ? args.goal.trim() : '';
79
- if (!goal) {
80
- throw new Error('firecrawl_monitor_create shorthand requires `goal`. Use `body` for advanced requests without a goal.');
81
- }
82
- const webhookUrl = typeof args.webhookUrl === 'string' ? args.webhookUrl.trim() : '';
83
- const email = typeof args.email === 'string' && args.email.trim()
84
- ? {
85
- email: {
86
- enabled: true,
87
- recipients: [args.email.trim()],
88
- includeDiffs: Boolean(args.includeDiffs),
89
- },
90
- }
91
- : undefined;
92
- return {
93
- name: typeof args.name === 'string' && args.name.trim()
94
- ? args.name.trim()
95
- : `Monitor ${urls[0]}`,
96
- schedule: {
97
- text: typeof args.scheduleText === 'string' && args.scheduleText.trim()
98
- ? args.scheduleText.trim()
99
- : 'every 30 minutes',
100
- timezone: typeof args.timezone === 'string' && args.timezone.trim()
101
- ? args.timezone.trim()
102
- : 'UTC',
103
- },
104
- goal,
105
- targets: [{ type: 'scrape', urls }],
106
- ...(email ? { notification: email } : {}),
107
- ...(webhookUrl
108
- ? {
109
- webhook: {
110
- url: webhookUrl,
111
- events: ['monitor.page', 'monitor.check.completed'],
112
- },
113
- }
114
- : {}),
115
- };
116
- }
117
- export function registerMonitorTools(server) {
118
- server.addTool({
119
- name: 'firecrawl_monitor_create',
120
- annotations: {
121
- title: 'Create monitor',
122
- readOnlyHint: false, // Creates a new recurring monitor configuration on the Firecrawl API.
123
- openWorldHint: true, // Monitors user-specified URLs on the public web on a recurring schedule.
124
- destructiveHint: false, // Additive; creates a new monitor without deleting existing monitors or external content.
125
- },
126
- description: `
127
- Create a Firecrawl monitor — a recurring scrape or crawl that diffs each result against the last retained snapshot.
128
-
129
- Prefer the simple path: pass \`page\` or \`pages\` plus \`goal\`. The tool will create a scrape monitor with a 30-minute schedule and meaningful-change judging enabled by the API. Use \`body\` only for advanced requests such as crawl targets, JSON change tracking, custom retention, or manual \`judgeEnabled\` control.
130
-
131
- Meaningful-change judge: set \`goal\` to a plain-language description of what the user actually cares about. \`judgeEnabled\` defaults to true when \`goal\` is set, so providing \`goal\` is enough. Page webhooks expose \`isMeaningful\` and \`judgment\` on \`monitor.page\` events.
132
-
133
- Simple fields:
134
- - \`page\`: one page URL to monitor.
135
- - \`pages\`: multiple page URLs to monitor.
136
- - \`goal\`: plain-English instruction for what changes matter. Required for the simple path.
137
- - \`scheduleText\`: optional natural-language schedule, default \`every 30 minutes\`.
138
- - \`email\`: optional email recipient for summaries.
139
- - \`webhookUrl\`: optional webhook URL. Configures \`monitor.page\` and \`monitor.check.completed\`.
140
-
141
- Goal guidance:
142
- - Expand the user's one-line monitoring intent into a concise 2-3 sentence monitor goal.
143
- - State what should trigger an alert, restate any scope the user gave, and include intent-specific exclusions only when obvious from the user's request.
144
- - Generic noise such as whitespace, formatting-only changes, request IDs, tracking params, generic metadata, and unrelated page chrome is already handled by the judge; do not repeat it in every goal.
145
- - If the user is vague, keep the goal broad rather than guessing exclusions. If the user asks for broad monitoring or "any change", preserve that and do not add exclusions that hide changes.
146
- - If the user says they do not care about something, include that explicitly. It is okay to ask whether they want to ignore specific noise when it is likely to matter.
147
- - Do not invent page-specific sections, thresholds, entities, or business rules unless the user mentioned them.
148
-
149
- Full \`body\` requests require: \`name\`, \`schedule\` (with \`cron\` or \`text\`), and \`targets\` (one or more \`{ type: 'scrape', urls: [...] }\` or \`{ type: 'crawl', url: '...' }\`). Optional: \`goal\`, \`judgeEnabled\`, \`webhook\`, \`notification\`, \`retentionDays\`.
150
-
151
- **Markdown-mode (default):** Each check produces a unified text diff of the page's markdown. No extra configuration needed.
152
-
153
- \`\`\`json
154
- {
155
- "name": "firecrawl_monitor_create",
156
- "arguments": {
157
- "page": "https://example.com/blog",
158
- "goal": "Alert when a new blog post is published or an existing headline changes.",
159
- "email": "alerts@example.com"
160
- }
161
- }
162
- \`\`\`
163
-
164
- **Multiple pages:**
165
-
166
- \`\`\`json
167
- {
168
- "name": "firecrawl_monitor_create",
169
- "arguments": {
170
- "pages": ["https://example.com/pricing", "https://example.com/changelog"],
171
- "goal": "Alert when pricing, packaging, or launch messaging changes.",
172
- "webhookUrl": "https://example.com/webhooks/firecrawl"
173
- }
174
- }
175
- \`\`\`
176
-
177
- **JSON-mode change tracking:** To detect changes in **specific structured fields** (price, headline, in-stock flag, list items) instead of the whole page, add a \`changeTracking\` format with \`modes: ["json"]\` and a JSON schema to the target's \`scrapeOptions.formats\`. The check response will then carry a per-field diff (keyed by JSON path, e.g. \`plans[0].price\`) and a \`snapshot.json\` with the full current extraction. See \`firecrawl_monitor_check\` for the response shape.
178
-
179
- \`\`\`json
180
- {
181
- "name": "firecrawl_monitor_create",
182
- "arguments": {
183
- "body": {
184
- "name": "Pricing watch",
185
- "schedule": { "text": "hourly", "timezone": "UTC" },
186
- "goal": "Alert when a pricing tier, price, billing period, limit, or headline feature changes. Ignore unrelated marketing copy unless it changes the pricing offer.",
187
- "targets": [{
188
- "type": "scrape",
189
- "urls": ["https://example.com/pricing"],
190
- "scrapeOptions": {
191
- "formats": [{
192
- "type": "changeTracking",
193
- "modes": ["json"],
194
- "prompt": "Extract pricing tiers and headline features for each plan.",
195
- "schema": {
196
- "type": "object",
197
- "properties": {
198
- "plans": {
199
- "type": "array",
200
- "items": {
201
- "type": "object",
202
- "properties": {
203
- "name": { "type": "string" },
204
- "price": { "type": "string" },
205
- "features": { "type": "array", "items": { "type": "string" } }
206
- }
207
- }
208
- }
209
- }
210
- }
211
- }]
212
- }
213
- }]
214
- }
215
- }
216
- }
217
- \`\`\`
218
-
219
- **Mixed mode (JSON + git-diff):** Use \`modes: ["json", "git-diff"]\` to get both per-field diffs and a markdown sidecar. The page is marked \`changed\` whenever either surface changed.
220
- `,
221
- parameters: z.object({
222
- body: z.record(z.string(), z.any()).optional(),
223
- page: z.string().optional(),
224
- pages: z.array(z.string()).optional(),
225
- goal: z.string().optional(),
226
- name: z.string().optional(),
227
- scheduleText: z.string().optional(),
228
- timezone: z.string().optional(),
229
- email: z.string().optional(),
230
- includeDiffs: z.boolean().optional(),
231
- webhookUrl: z.string().optional(),
232
- }),
233
- execute: async (args, { session, log }) => {
234
- const body = buildMonitorCreateBody(args);
235
- log.info('Creating monitor', { name: body.name });
236
- const res = await monitorRequest(session, '/monitor', {
237
- method: 'POST',
238
- body,
239
- });
240
- return asText(res);
241
- },
242
- });
243
- server.addTool({
244
- name: 'firecrawl_monitor_list',
245
- annotations: {
246
- title: 'List monitors',
247
- readOnlyHint: true, // Lists monitors for the authenticated account; no mutations.
248
- openWorldHint: false, // Returns only the user's Firecrawl monitor records, not arbitrary web content.
249
- destructiveHint: false, // Read-only listing.
250
- },
251
- description: `
252
- List all Firecrawl monitors for the authenticated account.
253
-
254
- **Usage Example:**
255
- \`\`\`json
256
- { "name": "firecrawl_monitor_list", "arguments": { "limit": 20 } }
257
- \`\`\`
258
- `,
259
- parameters: z.object({
260
- limit: z.number().int().positive().optional(),
261
- offset: z.number().int().nonnegative().optional(),
262
- }),
263
- execute: async (args, { session }) => {
264
- const { limit, offset } = args;
265
- const res = await monitorRequest(session, '/monitor', {
266
- query: { limit, offset },
267
- });
268
- return asText(res);
269
- },
270
- });
271
- server.addTool({
272
- name: 'firecrawl_monitor_get',
273
- annotations: {
274
- title: 'Get monitor',
275
- readOnlyHint: true, // Fetches a single monitor by ID; no mutations.
276
- openWorldHint: false, // Reads a specific monitor resource in the user's Firecrawl account.
277
- destructiveHint: false, // Read-only retrieval.
278
- },
279
- description: `
280
- Get a single monitor by ID.
281
-
282
- **Usage Example:**
283
- \`\`\`json
284
- { "name": "firecrawl_monitor_get", "arguments": { "id": "mon_abc123" } }
285
- \`\`\`
286
- `,
287
- parameters: z.object({ id: z.string() }),
288
- execute: async (args, { session }) => {
289
- const { id } = args;
290
- const res = await monitorRequest(session, `/monitor/${encodeURIComponent(id)}`);
291
- return asText(res);
292
- },
293
- });
294
- server.addTool({
295
- name: 'firecrawl_monitor_update',
296
- annotations: {
297
- title: 'Update monitor',
298
- readOnlyHint: false, // PATCHes an existing monitor (status, schedule, targets, webhooks, etc.).
299
- openWorldHint: true, // Can change which external URLs are monitored and how recurring scrapes run.
300
- destructiveHint: true, // Can pause, replace, or remove monitor configuration; changes overwrite prior settings.
301
- },
302
- description: `
303
- Update a monitor. Pass any subset of fields to patch: \`name\`, \`status\` ("active" | "paused"), \`schedule\`, \`targets\`, \`goal\`, \`judgeEnabled\`, \`webhook\`, \`notification\`, \`retentionDays\`.
304
-
305
- **Usage Example:**
306
- \`\`\`json
307
- {
308
- "name": "firecrawl_monitor_update",
309
- "arguments": {
310
- "id": "mon_abc123",
311
- "body": { "status": "paused" }
312
- }
313
- }
314
- \`\`\`
315
- `,
316
- parameters: z.object({
317
- id: z.string(),
318
- body: z.record(z.string(), z.any()),
319
- }),
320
- execute: async (args, { session }) => {
321
- const { id, body } = args;
322
- const res = await monitorRequest(session, `/monitor/${encodeURIComponent(id)}`, { method: 'PATCH', body });
323
- return asText(res);
324
- },
325
- });
326
- server.addTool({
327
- name: 'firecrawl_monitor_delete',
328
- annotations: {
329
- title: 'Delete monitor',
330
- readOnlyHint: false, // Permanently deletes a monitor via DELETE on the API.
331
- openWorldHint: true, // Deletes a monitor that tracked open-web URLs.
332
- destructiveHint: true, // Irreversibly removes the monitor and stops its schedule.
333
- },
334
- description: `
335
- Permanently delete a monitor and stop its schedule. This cannot be undone.
336
-
337
- **Usage Example:**
338
- \`\`\`json
339
- { "name": "firecrawl_monitor_delete", "arguments": { "id": "mon_abc123" } }
340
- \`\`\`
341
- `,
342
- parameters: z.object({ id: z.string() }),
343
- execute: async (args, { session, log }) => {
344
- const { id } = args;
345
- log.info('Deleting monitor', { id });
346
- const res = await monitorRequest(session, `/monitor/${encodeURIComponent(id)}`, { method: 'DELETE' });
347
- return asText(res);
348
- },
349
- });
350
- server.addTool({
351
- name: 'firecrawl_monitor_run',
352
- annotations: {
353
- title: 'Run monitor now',
354
- readOnlyHint: false, // Triggers an immediate monitor check, queueing a new scrape/diff run.
355
- openWorldHint: true, // The triggered check scrapes external URLs configured on the monitor.
356
- destructiveHint: false, // Starts a read-only check job; does not delete the monitor or external sites.
357
- },
358
- description: `
359
- Trigger a monitor check immediately, outside its normal schedule. Returns the queued check.
360
-
361
- **Usage Example:**
362
- \`\`\`json
363
- { "name": "firecrawl_monitor_run", "arguments": { "id": "mon_abc123" } }
364
- \`\`\`
365
- `,
366
- parameters: z.object({ id: z.string() }),
367
- execute: async (args, { session }) => {
368
- const { id } = args;
369
- const res = await monitorRequest(session, `/monitor/${encodeURIComponent(id)}/run`, { method: 'POST' });
370
- return asText(res);
371
- },
372
- });
373
- server.addTool({
374
- name: 'firecrawl_monitor_checks',
375
- annotations: {
376
- title: 'List monitor checks',
377
- readOnlyHint: true, // Lists historical check runs for a monitor; no mutations.
378
- openWorldHint: false, // Returns check history for a known monitor ID within the user's account.
379
- destructiveHint: false, // Read-only listing.
380
- },
381
- description: `
382
- List historical checks for a monitor.
383
-
384
- **Usage Example:**
385
- \`\`\`json
386
- { "name": "firecrawl_monitor_checks", "arguments": { "id": "mon_abc123", "limit": 10, "status": "completed" } }
387
- \`\`\`
388
- `,
389
- parameters: z.object({
390
- id: z.string(),
391
- limit: z.number().int().positive().optional(),
392
- offset: z.number().int().nonnegative().optional(),
393
- status: checkStatusSchema.optional(),
394
- }),
395
- execute: async (args, { session }) => {
396
- const { id, limit, offset, status } = args;
397
- const res = await monitorRequest(session, `/monitor/${encodeURIComponent(id)}/checks`, { query: { limit, offset, status } });
398
- return asText(res);
399
- },
400
- });
401
- server.addTool({
402
- name: 'firecrawl_monitor_check',
403
- annotations: {
404
- title: 'Get monitor check',
405
- readOnlyHint: true, // Retrieves a single check run with page-level diff results; no mutations.
406
- openWorldHint: false, // Reads stored check results for a known monitor/check ID in the user's account.
407
- destructiveHint: false, // Read-only retrieval of diff snapshots and judgments.
408
- },
409
- description: `
410
- Get a single check with page-level diff results. Filter \`pageStatus\` to surface only the pages that changed (or were new, removed, etc.).
411
-
412
- Each entry in \`data.pages[]\` has \`url\`, \`status\` (\`same\` | \`new\` | \`changed\` | \`removed\` | \`error\`), optional \`judgment\` when goal-based judging ran, and — when changed — a \`diff\` and possibly a \`snapshot\`. The shape of \`diff\` depends on the monitor's \`formats\` configuration:
413
-
414
- - **Markdown mode (default).** \`diff.text\` is the unified markdown diff; \`diff.json\` is a parse-diff AST (\`{ files: [...] }\`). No \`snapshot\`.
415
- - **JSON mode** (\`changeTracking\` with \`modes: ["json"]\`). \`diff.json\` is a per-field map keyed by JSON path into the extraction, e.g. \`plans[0].price\`, with each value being \`{ previous, current }\`. \`snapshot.json\` is the full current extraction. No \`diff.text\`.
416
- - **Mixed mode** (\`modes: ["json", "git-diff"]\`). Both \`diff.text\` (markdown sidecar) AND \`diff.json\` (per-field map) are present, plus \`snapshot.json\`.
417
-
418
- **Example JSON-mode response \`pages[]\` entry:**
419
-
420
- \`\`\`json
421
- {
422
- "url": "https://example.com/pricing",
423
- "status": "changed",
424
- "diff": {
425
- "json": {
426
- "plans[0].price": { "previous": "$19/mo", "current": "$24/mo" },
427
- "plans[1].features[2]": { "previous": "10 GB storage", "current": "25 GB storage" }
428
- }
429
- },
430
- "snapshot": { "json": { "plans": [/* current full extraction matching the monitor's schema */] } },
431
- "judgment": {
432
- "meaningful": true,
433
- "confidence": "high",
434
- "reason": "The pricing changed, which matches the monitor goal.",
435
- "meaningfulChanges": [
436
- {
437
- "type": "changed",
438
- "before": "$19/mo",
439
- "after": "$24/mo",
440
- "reason": "The tracked plan price changed."
441
- }
442
- ]
443
- }
444
- }
445
- \`\`\`
446
-
447
- When summarizing a check for the user, prefer \`diff.json\` paths (e.g. "plans[0].price changed from $19/mo to $24/mo") over re-printing the markdown diff — it's more concise and grounded in the schema fields they asked for.
448
-
449
- When \`judgment\` is present, use it to decide what to surface. \`judgment.meaningful: false\` means the change was classified as noise for the monitor's goal. When \`judgment.meaningfulChanges\` is present, prefer those goal-relevant changes over raw diff hunks; each item includes \`type\`, \`before\`, \`after\`, and \`reason\`.
450
-
451
- The endpoint paginates via a top-level \`next\` URL; this tool returns one page at a time. Increase \`limit\` (max 100) to fetch fewer pages.
452
-
453
- **Usage Example:**
454
- \`\`\`json
455
- {
456
- "name": "firecrawl_monitor_check",
457
- "arguments": {
458
- "id": "mon_abc123",
459
- "checkId": "chk_xyz",
460
- "pageStatus": "changed"
461
- }
462
- }
463
- \`\`\`
464
- `,
465
- parameters: z.object({
466
- id: z.string(),
467
- checkId: z.string(),
468
- limit: z.number().int().positive().optional(),
469
- skip: z.number().int().nonnegative().optional(),
470
- pageStatus: pageStatusSchema.optional(),
471
- }),
472
- execute: async (args, { session }) => {
473
- const { id, checkId, limit, skip, pageStatus } = args;
474
- const res = await monitorRequest(session, `/monitor/${encodeURIComponent(id)}/checks/${encodeURIComponent(checkId)}`, { query: { limit, skip, status: pageStatus } });
475
- return asText(res);
476
- },
477
- });
478
- }