crawlforge-mcp-server 6.1.0 → 6.3.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/package.json +1 -1
- package/server.js +5 -97
- package/src/cli/commands/monitor.js +18 -4
- package/src/core/AgentOrchestrator.js +106 -1
- package/src/core/ResearchOrchestrator.js +7 -1
- package/src/core/analysis/ContentAnalyzer.js +69 -13
- package/src/core/llm/LLMManager.js +94 -66
- package/src/schemas/toolOutputSchemas.js +1 -1
- package/src/skills/agent-skills/crawlforge-change-tracking/SKILL.md +53 -14
- package/src/tools/extract/extractStructured.js +28 -11
- package/src/tools/extract/extractWithLlm.js +1 -62
- package/src/tools/research/deepResearch.js +3 -2
- package/src/tools/tracking/trackChanges/hosted.js +176 -0
- package/src/tools/tracking/trackChanges/index.js +123 -7
- package/src/tools/tracking/trackChanges/notifier.js +5 -4
- package/src/tools/tracking/trackChanges/schema.js +36 -22
- package/src/utils/schemaValidate.js +139 -0
- package/src/core/AlertNotificationSystem.js +0 -602
|
@@ -6,8 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The raw input shape — one declaration (G5). server.js spreads it, with the
|
|
11
|
+
* shared compliance params, into the registered inputSchema, so the
|
|
12
|
+
* `.describe()` strings a client sees and the defaults the tool validates
|
|
13
|
+
* with cannot drift apart again (they had: the `email` block and the default
|
|
14
|
+
* `excludeSelectors` never reached tools/list). `.prefault({})` on the option
|
|
15
|
+
* objects fills their inner defaults when the object is omitted; zod 4's
|
|
16
|
+
* `.default({})` would not.
|
|
17
|
+
*/
|
|
18
|
+
export const TRACK_CHANGES_INPUT_SHAPE = {
|
|
19
|
+
url: z.string().url().optional().describe("The URL to track changes for (optional for list_scheduled_monitors)"),
|
|
11
20
|
operation: z.enum([
|
|
12
21
|
'create_baseline',
|
|
13
22
|
'compare',
|
|
@@ -22,13 +31,10 @@ export const TrackChangesSchema = z.object({
|
|
|
22
31
|
'create_alert_rule',
|
|
23
32
|
'generate_trend_report',
|
|
24
33
|
'get_monitoring_templates'
|
|
25
|
-
]).default('compare'),
|
|
26
|
-
|
|
27
|
-
content: z.string().optional(),
|
|
28
|
-
html: z.string().optional(),
|
|
34
|
+
]).default('compare').describe("Tracking operation to perform"),
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
content: z.string().optional().describe("Content to compare against baseline"),
|
|
37
|
+
html: z.string().optional().describe("HTML content to compare against baseline"),
|
|
32
38
|
|
|
33
39
|
trackingOptions: z.object({
|
|
34
40
|
granularity: z.enum(['page', 'section', 'element', 'text']).default('section'),
|
|
@@ -48,7 +54,7 @@ export const TrackChangesSchema = z.object({
|
|
|
48
54
|
moderate: z.number().min(0).max(1).default(0.3),
|
|
49
55
|
major: z.number().min(0).max(1).default(0.7)
|
|
50
56
|
}).optional()
|
|
51
|
-
}).optional().prefault({}),
|
|
57
|
+
}).optional().prefault({}).describe("Options for how changes are tracked and compared"),
|
|
52
58
|
|
|
53
59
|
monitoringOptions: z.object({
|
|
54
60
|
enabled: z.boolean().default(false),
|
|
@@ -59,7 +65,7 @@ export const TrackChangesSchema = z.object({
|
|
|
59
65
|
enableWebhook: z.boolean().default(false),
|
|
60
66
|
webhookUrl: z.string().url().optional(),
|
|
61
67
|
webhookSecret: z.string().optional()
|
|
62
|
-
}).optional().prefault({}),
|
|
68
|
+
}).optional().prefault({}).describe("Monitoring schedule and notification settings"),
|
|
63
69
|
|
|
64
70
|
storageOptions: z.object({
|
|
65
71
|
enableSnapshots: z.boolean().default(true),
|
|
@@ -67,7 +73,7 @@ export const TrackChangesSchema = z.object({
|
|
|
67
73
|
maxHistoryEntries: z.number().min(1).max(1000).default(100),
|
|
68
74
|
compressionEnabled: z.boolean().default(true),
|
|
69
75
|
deltaStorageEnabled: z.boolean().default(true)
|
|
70
|
-
}).optional().prefault({}),
|
|
76
|
+
}).optional().prefault({}).describe("Storage and history retention settings"),
|
|
71
77
|
|
|
72
78
|
queryOptions: z.object({
|
|
73
79
|
limit: z.number().min(1).max(500).default(50),
|
|
@@ -76,7 +82,7 @@ export const TrackChangesSchema = z.object({
|
|
|
76
82
|
endTime: z.number().optional(),
|
|
77
83
|
includeContent: z.boolean().default(false),
|
|
78
84
|
significanceFilter: z.enum(['all', 'minor', 'moderate', 'major', 'critical']).optional()
|
|
79
|
-
}).optional().prefault({}),
|
|
85
|
+
}).optional().prefault({}).describe("Query options for history and stats retrieval"),
|
|
80
86
|
|
|
81
87
|
notificationOptions: z.object({
|
|
82
88
|
email: z.object({
|
|
@@ -99,17 +105,19 @@ export const TrackChangesSchema = z.object({
|
|
|
99
105
|
channel: z.string().optional(),
|
|
100
106
|
username: z.string().optional()
|
|
101
107
|
}).optional()
|
|
102
|
-
}).optional(),
|
|
108
|
+
}).optional().describe("Notification configuration for webhooks, Slack and email (email is sent by hosted monitors only)"),
|
|
103
109
|
|
|
104
110
|
scheduledMonitorOptions: z.object({
|
|
105
|
-
schedule: z.string().optional(),
|
|
111
|
+
schedule: z.string().optional().describe("Optional cron expression (power users)"),
|
|
106
112
|
templateId: z.string().optional(),
|
|
107
113
|
enabled: z.boolean().default(true),
|
|
108
|
-
interval: z.number().min(60000).optional(),
|
|
109
|
-
goal: z.string().optional(),
|
|
110
|
-
monitorId: z.string().optional(),
|
|
111
|
-
notificationThreshold: z.enum(['minor', 'moderate', 'major', 'critical']).optional()
|
|
112
|
-
|
|
114
|
+
interval: z.number().min(60000).optional().describe("Polling interval in ms (default 1h)"),
|
|
115
|
+
goal: z.string().optional().describe("Plain-English alert goal; an LLM judges whether a change matches (degrades to threshold if no LLM)"),
|
|
116
|
+
monitorId: z.string().optional().describe("Monitor id for stop_scheduled_monitor"),
|
|
117
|
+
notificationThreshold: z.enum(['minor', 'moderate', 'major', 'critical']).optional(),
|
|
118
|
+
hosted: z.boolean().default(false).describe("Run the monitor on CrawlForge's servers: it fires from the hosted scheduler whether or not this process is alive and sends email and signed webhooks. Each check bills 3 credits per compared target from the account; blocked and errored targets are free. Default false = local, in-process."),
|
|
119
|
+
name: z.string().min(1).max(80).optional().describe("Display name for a hosted monitor (default: the URL host)")
|
|
120
|
+
}).optional().describe("Scheduled monitoring: recurring compare + notify, optional plain-English goal"),
|
|
113
121
|
|
|
114
122
|
alertRuleOptions: z.object({
|
|
115
123
|
ruleId: z.string().optional(),
|
|
@@ -117,7 +125,7 @@ export const TrackChangesSchema = z.object({
|
|
|
117
125
|
actions: z.array(z.enum(['webhook', 'email', 'slack'])).optional(),
|
|
118
126
|
throttle: z.number().min(0).optional(),
|
|
119
127
|
priority: z.enum(['low', 'medium', 'high']).optional()
|
|
120
|
-
}).optional(),
|
|
128
|
+
}).optional().describe("Alert rule configuration for change notifications"),
|
|
121
129
|
|
|
122
130
|
exportOptions: z.object({
|
|
123
131
|
format: z.enum(['json', 'csv']).default('json'),
|
|
@@ -125,11 +133,17 @@ export const TrackChangesSchema = z.object({
|
|
|
125
133
|
endTime: z.number().optional(),
|
|
126
134
|
includeContent: z.boolean().default(false),
|
|
127
135
|
includeSnapshots: z.boolean().default(false)
|
|
128
|
-
}).optional(),
|
|
136
|
+
}).optional().describe("Export options for change history data"),
|
|
129
137
|
|
|
130
138
|
dashboardOptions: z.object({
|
|
131
139
|
includeRecentAlerts: z.boolean().default(true),
|
|
132
140
|
includeTrends: z.boolean().default(true),
|
|
133
141
|
includeMonitorStatus: z.boolean().default(true)
|
|
134
|
-
}).optional()
|
|
142
|
+
}).optional().describe("Dashboard display options")
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const TrackChangesSchema = z.object({
|
|
146
|
+
...TRACK_CHANGES_INPUT_SHAPE,
|
|
147
|
+
respect_robots: z.boolean().optional(),
|
|
148
|
+
user_agent: z.string().optional()
|
|
135
149
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSON-Schema → zod validation.
|
|
3
|
+
*
|
|
4
|
+
* Lifted out of `src/tools/extract/extractWithLlm.js`, where it was local and
|
|
5
|
+
* unexported, so that every consumer of LLM-decoded JSON validates the same
|
|
6
|
+
* way. `LLMManager.validateAgainstSchema` used to hand-roll its own check that
|
|
7
|
+
* only ever looked one level deep: `{countries: ["a string", "another"]}`
|
|
8
|
+
* against `{countries: {type: 'array', items: {type: 'object'}}}` reported
|
|
9
|
+
* `valid: true` because the top-level value was, in fact, an array (R19).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a zod validator from a JSON-Schema-like hint. Best-effort: unknown
|
|
16
|
+
* shapes fall back to `z.any()` so validation never rejects on constructs the
|
|
17
|
+
* converter does not understand.
|
|
18
|
+
*/
|
|
19
|
+
export function jsonSchemaToZod(schema) {
|
|
20
|
+
if (!schema || typeof schema !== 'object') return z.any();
|
|
21
|
+
|
|
22
|
+
// Flat hint map (no `type`/`properties`) → treat values as field hints.
|
|
23
|
+
const isJsonSchema = schema.type || schema.properties || schema.items;
|
|
24
|
+
if (!isJsonSchema) {
|
|
25
|
+
const shape = {};
|
|
26
|
+
for (const [key, val] of Object.entries(schema)) {
|
|
27
|
+
shape[key] = jsonSchemaToZod(typeof val === 'string' ? { type: val } : val).nullable().optional();
|
|
28
|
+
}
|
|
29
|
+
return z.object(shape).passthrough();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
switch (schema.type) {
|
|
33
|
+
case 'string': return z.string();
|
|
34
|
+
case 'number':
|
|
35
|
+
case 'integer': return z.number();
|
|
36
|
+
case 'boolean': return z.boolean();
|
|
37
|
+
case 'null': return z.null();
|
|
38
|
+
case 'array': return z.array(schema.items ? jsonSchemaToZod(schema.items) : z.any());
|
|
39
|
+
case 'object': {
|
|
40
|
+
const shape = {};
|
|
41
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
42
|
+
for (const [key, val] of Object.entries(schema.properties || {})) {
|
|
43
|
+
const field = jsonSchemaToZod(val);
|
|
44
|
+
// The model is told to answer null for a field the content never
|
|
45
|
+
// states, so null is the honest answer for a field the schema does
|
|
46
|
+
// not require — not a type violation. A required field stays strict:
|
|
47
|
+
// null there is exactly what the caller needs to hear about.
|
|
48
|
+
shape[key] = required.includes(key) ? field : field.nullable().optional();
|
|
49
|
+
}
|
|
50
|
+
return z.object(shape).passthrough();
|
|
51
|
+
}
|
|
52
|
+
default: return z.any();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate parsed output against the schema hint.
|
|
58
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
59
|
+
*/
|
|
60
|
+
export function validateAgainstSchema(parsed, schema) {
|
|
61
|
+
try {
|
|
62
|
+
const validator = jsonSchemaToZod(schema);
|
|
63
|
+
const result = validator.safeParse(parsed);
|
|
64
|
+
if (result.success) return { valid: true, errors: [] };
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
errors: result.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
// Converter failure should not block extraction — treat as unvalidated.
|
|
71
|
+
return { valid: true, errors: [] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate against a schema and report problems per field, in the wording
|
|
77
|
+
* callers see in tool output: "Missing required field: x" and
|
|
78
|
+
* `Field "x": expected number, got string`, with a dotted path for anything
|
|
79
|
+
* nested (`Field "countries.0.capital": ...`).
|
|
80
|
+
*
|
|
81
|
+
* Same structural check as validateAgainstSchema — this differs only in how
|
|
82
|
+
* the failures are worded, and in also checking `enum`, which the zod
|
|
83
|
+
* converter does not carry.
|
|
84
|
+
*
|
|
85
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function validateFieldsAgainstSchema(data, schema) {
|
|
88
|
+
const required = Array.isArray(schema?.required) ? schema.required : [];
|
|
89
|
+
const properties = schema?.properties || {};
|
|
90
|
+
const errors = [];
|
|
91
|
+
|
|
92
|
+
let issues = [];
|
|
93
|
+
try {
|
|
94
|
+
const result = jsonSchemaToZod(schema).safeParse(data);
|
|
95
|
+
if (!result.success) issues = result.error.issues;
|
|
96
|
+
} catch {
|
|
97
|
+
// The converter is best-effort and falls back to z.any() rather than
|
|
98
|
+
// throwing, so this is unreachable in practice. Treat it as unvalidated
|
|
99
|
+
// rather than failing an extraction on a validator bug.
|
|
100
|
+
return { valid: true, errors: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const issue of issues) {
|
|
104
|
+
const path = issue.path;
|
|
105
|
+
const value = path.reduce((acc, key) => (acc == null ? undefined : acc[key]), data);
|
|
106
|
+
// A required field the decoder left null is "not filled in", the same as
|
|
107
|
+
// absent — the caller wants to hear it is missing, not that null is the
|
|
108
|
+
// wrong type. Optional nulls never reach here: the converter allows them.
|
|
109
|
+
if (path.length === 1 && required.includes(path[0]) && (value === null || value === undefined)) {
|
|
110
|
+
errors.push(`Missing required field: ${path[0]}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const where = path.length ? path.join('.') : '(root)';
|
|
114
|
+
if (issue.code === 'invalid_type' && issue.expected) {
|
|
115
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
116
|
+
errors.push(`Field "${where}": expected ${issue.expected}, got ${actualType}`);
|
|
117
|
+
} else {
|
|
118
|
+
errors.push(`Field "${where}": ${issue.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The converter carries no `enum`, so that check stays here. Top level only.
|
|
123
|
+
for (const [key, fieldSchema] of Object.entries(properties)) {
|
|
124
|
+
const value = data?.[key];
|
|
125
|
+
if (value === null || value === undefined) continue;
|
|
126
|
+
if (fieldSchema?.enum && !fieldSchema.enum.includes(value)) {
|
|
127
|
+
errors.push(`Field "${key}": value "${value}" not in enum ${JSON.stringify(fieldSchema.enum)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A malformed array can raise one issue per element; the full list is pushed
|
|
132
|
+
// into tool output, so cap it.
|
|
133
|
+
const MAX_REPORTED = 10;
|
|
134
|
+
const reported = errors.length > MAX_REPORTED
|
|
135
|
+
? [...errors.slice(0, MAX_REPORTED), `…and ${errors.length - MAX_REPORTED} more validation errors`]
|
|
136
|
+
: errors;
|
|
137
|
+
|
|
138
|
+
return { valid: errors.length === 0, errors: reported };
|
|
139
|
+
}
|