crawlforge-mcp-server 3.0.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.
Files changed (75) hide show
  1. package/CLAUDE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -0
  4. package/package.json +115 -0
  5. package/server.js +1963 -0
  6. package/setup.js +112 -0
  7. package/src/constants/config.js +615 -0
  8. package/src/core/ActionExecutor.js +1104 -0
  9. package/src/core/AlertNotificationSystem.js +601 -0
  10. package/src/core/AuthManager.js +315 -0
  11. package/src/core/ChangeTracker.js +2306 -0
  12. package/src/core/JobManager.js +687 -0
  13. package/src/core/LLMsTxtAnalyzer.js +753 -0
  14. package/src/core/LocalizationManager.js +1615 -0
  15. package/src/core/PerformanceManager.js +828 -0
  16. package/src/core/ResearchOrchestrator.js +1327 -0
  17. package/src/core/SnapshotManager.js +1037 -0
  18. package/src/core/StealthBrowserManager.js +1795 -0
  19. package/src/core/WebhookDispatcher.js +745 -0
  20. package/src/core/analysis/ContentAnalyzer.js +749 -0
  21. package/src/core/analysis/LinkAnalyzer.js +972 -0
  22. package/src/core/cache/CacheManager.js +821 -0
  23. package/src/core/connections/ConnectionPool.js +553 -0
  24. package/src/core/crawlers/BFSCrawler.js +845 -0
  25. package/src/core/integrations/PerformanceIntegration.js +377 -0
  26. package/src/core/llm/AnthropicProvider.js +135 -0
  27. package/src/core/llm/LLMManager.js +415 -0
  28. package/src/core/llm/LLMProvider.js +97 -0
  29. package/src/core/llm/OpenAIProvider.js +127 -0
  30. package/src/core/processing/BrowserProcessor.js +986 -0
  31. package/src/core/processing/ContentProcessor.js +505 -0
  32. package/src/core/processing/PDFProcessor.js +448 -0
  33. package/src/core/processing/StreamProcessor.js +673 -0
  34. package/src/core/queue/QueueManager.js +98 -0
  35. package/src/core/workers/WorkerPool.js +585 -0
  36. package/src/core/workers/worker.js +743 -0
  37. package/src/monitoring/healthCheck.js +600 -0
  38. package/src/monitoring/metrics.js +761 -0
  39. package/src/optimization/wave3-optimizations.js +932 -0
  40. package/src/security/security-patches.js +120 -0
  41. package/src/security/security-tests.js +355 -0
  42. package/src/security/wave3-security.js +652 -0
  43. package/src/tools/advanced/BatchScrapeTool.js +1089 -0
  44. package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
  45. package/src/tools/crawl/crawlDeep.js +449 -0
  46. package/src/tools/crawl/mapSite.js +400 -0
  47. package/src/tools/extract/analyzeContent.js +624 -0
  48. package/src/tools/extract/extractContent.js +329 -0
  49. package/src/tools/extract/processDocument.js +503 -0
  50. package/src/tools/extract/summarizeContent.js +376 -0
  51. package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
  52. package/src/tools/research/deepResearch.js +706 -0
  53. package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
  54. package/src/tools/search/adapters/googleSearch.js +236 -0
  55. package/src/tools/search/adapters/searchProviderFactory.js +96 -0
  56. package/src/tools/search/queryExpander.js +543 -0
  57. package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
  58. package/src/tools/search/ranking/ResultRanker.js +497 -0
  59. package/src/tools/search/searchWeb.js +482 -0
  60. package/src/tools/tracking/trackChanges.js +1355 -0
  61. package/src/utils/CircuitBreaker.js +515 -0
  62. package/src/utils/ErrorHandlingConfig.js +342 -0
  63. package/src/utils/HumanBehaviorSimulator.js +569 -0
  64. package/src/utils/Logger.js +568 -0
  65. package/src/utils/MemoryMonitor.js +173 -0
  66. package/src/utils/RetryManager.js +386 -0
  67. package/src/utils/contentUtils.js +588 -0
  68. package/src/utils/domainFilter.js +612 -0
  69. package/src/utils/inputValidation.js +766 -0
  70. package/src/utils/rateLimiter.js +196 -0
  71. package/src/utils/robotsChecker.js +91 -0
  72. package/src/utils/securityMiddleware.js +416 -0
  73. package/src/utils/sitemapParser.js +678 -0
  74. package/src/utils/ssrfProtection.js +640 -0
  75. package/src/utils/urlNormalizer.js +168 -0
@@ -0,0 +1,1355 @@
1
+ /**
2
+ * TrackChanges Tool - Change Tracking MCP Tool
3
+ * Provides baseline capture, comparison, scheduled monitoring,
4
+ * and change notification capabilities
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import ChangeTracker from '../../core/ChangeTracker.js';
9
+ import SnapshotManager from '../../core/SnapshotManager.js';
10
+ import CacheManager from '../../core/cache/CacheManager.js';
11
+ import { EventEmitter } from 'events';
12
+
13
+ // Input validation schemas
14
+ const TrackChangesSchema = z.object({
15
+ url: z.string().url(),
16
+ operation: z.enum([
17
+ 'create_baseline',
18
+ 'compare',
19
+ 'monitor',
20
+ 'get_history',
21
+ 'get_stats',
22
+ // Enhanced Phase 2.4 operations
23
+ 'create_scheduled_monitor',
24
+ 'stop_scheduled_monitor',
25
+ 'get_dashboard',
26
+ 'export_history',
27
+ 'create_alert_rule',
28
+ 'generate_trend_report',
29
+ 'get_monitoring_templates'
30
+ ]).default('compare'),
31
+
32
+ // Content options
33
+ content: z.string().optional(),
34
+ html: z.string().optional(),
35
+
36
+ // Tracking options
37
+ trackingOptions: z.object({
38
+ granularity: z.enum(['page', 'section', 'element', 'text']).default('section'),
39
+ trackText: z.boolean().default(true),
40
+ trackStructure: z.boolean().default(true),
41
+ trackAttributes: z.boolean().default(false),
42
+ trackImages: z.boolean().default(false),
43
+ trackLinks: z.boolean().default(true),
44
+ ignoreWhitespace: z.boolean().default(true),
45
+ ignoreCase: z.boolean().default(false),
46
+ customSelectors: z.array(z.string()).optional(),
47
+ excludeSelectors: z.array(z.string()).optional().default([
48
+ 'script', 'style', 'noscript', '.advertisement', '.ad', '#comments'
49
+ ]),
50
+ significanceThresholds: z.object({
51
+ minor: z.number().min(0).max(1).default(0.1),
52
+ moderate: z.number().min(0).max(1).default(0.3),
53
+ major: z.number().min(0).max(1).default(0.7)
54
+ }).optional()
55
+ }).optional().default({}),
56
+
57
+ // Monitoring options
58
+ monitoringOptions: z.object({
59
+ enabled: z.boolean().default(false),
60
+ interval: z.number().min(60000).max(24 * 60 * 60 * 1000).default(300000), // 5 minutes to 24 hours
61
+ maxRetries: z.number().min(0).max(5).default(3),
62
+ retryDelay: z.number().min(1000).max(60000).default(5000),
63
+ notificationThreshold: z.enum(['minor', 'moderate', 'major', 'critical']).default('moderate'),
64
+ enableWebhook: z.boolean().default(false),
65
+ webhookUrl: z.string().url().optional(),
66
+ webhookSecret: z.string().optional()
67
+ }).optional(),
68
+
69
+ // Storage options
70
+ storageOptions: z.object({
71
+ enableSnapshots: z.boolean().default(true),
72
+ retainHistory: z.boolean().default(true),
73
+ maxHistoryEntries: z.number().min(1).max(1000).default(100),
74
+ compressionEnabled: z.boolean().default(true),
75
+ deltaStorageEnabled: z.boolean().default(true)
76
+ }).optional().default({}),
77
+
78
+ // Query options for history retrieval
79
+ queryOptions: z.object({
80
+ limit: z.number().min(1).max(500).default(50),
81
+ offset: z.number().min(0).default(0),
82
+ startTime: z.number().optional(),
83
+ endTime: z.number().optional(),
84
+ includeContent: z.boolean().default(false),
85
+ significanceFilter: z.enum(['all', 'minor', 'moderate', 'major', 'critical']).optional()
86
+ }).optional(),
87
+
88
+ // Notification options
89
+ notificationOptions: z.object({
90
+ email: z.object({
91
+ enabled: z.boolean().default(false),
92
+ recipients: z.array(z.string().email()).optional(),
93
+ subject: z.string().optional(),
94
+ includeDetails: z.boolean().default(true)
95
+ }).optional(),
96
+ webhook: z.object({
97
+ enabled: z.boolean().default(false),
98
+ url: z.string().url().optional(),
99
+ method: z.enum(['POST', 'PUT']).default('POST'),
100
+ headers: z.record(z.string()).optional(),
101
+ signingSecret: z.string().optional(),
102
+ includeContent: z.boolean().default(false)
103
+ }).optional(),
104
+ slack: z.object({
105
+ enabled: z.boolean().default(false),
106
+ webhookUrl: z.string().url().optional(),
107
+ channel: z.string().optional(),
108
+ username: z.string().optional()
109
+ }).optional()
110
+ }).optional(),
111
+
112
+ // Enhanced Phase 2.4 options
113
+ scheduledMonitorOptions: z.object({
114
+ schedule: z.string().optional(), // Cron expression
115
+ templateId: z.string().optional(), // Monitoring template ID
116
+ enabled: z.boolean().default(true)
117
+ }).optional(),
118
+
119
+ alertRuleOptions: z.object({
120
+ ruleId: z.string().optional(),
121
+ condition: z.string().optional(), // Condition description
122
+ actions: z.array(z.enum(['webhook', 'email', 'slack'])).optional(),
123
+ throttle: z.number().min(0).optional(),
124
+ priority: z.enum(['low', 'medium', 'high']).optional()
125
+ }).optional(),
126
+
127
+ exportOptions: z.object({
128
+ format: z.enum(['json', 'csv']).default('json'),
129
+ startTime: z.number().optional(),
130
+ endTime: z.number().optional(),
131
+ includeContent: z.boolean().default(false),
132
+ includeSnapshots: z.boolean().default(false)
133
+ }).optional(),
134
+
135
+ dashboardOptions: z.object({
136
+ includeRecentAlerts: z.boolean().default(true),
137
+ includeTrends: z.boolean().default(true),
138
+ includeMonitorStatus: z.boolean().default(true)
139
+ }).optional()
140
+ });
141
+
142
+ export class TrackChangesTool extends EventEmitter {
143
+ constructor(options = {}) {
144
+ super();
145
+
146
+ this.options = {
147
+ cacheEnabled: true,
148
+ cacheTTL: 3600000, // 1 hour
149
+ snapshotStorageDir: './snapshots',
150
+ enableRealTimeMonitoring: true,
151
+ maxConcurrentMonitors: 50,
152
+ defaultPollingInterval: 300000, // 5 minutes
153
+ ...options
154
+ };
155
+
156
+ // Initialize components
157
+ this.changeTracker = new ChangeTracker({
158
+ enableRealTimeTracking: this.options.enableRealTimeMonitoring,
159
+ enableSemanticAnalysis: false, // Can be enabled if needed
160
+ contentSimilarityThreshold: 0.8
161
+ });
162
+
163
+ this.snapshotManager = new SnapshotManager({
164
+ storageDir: this.options.snapshotStorageDir,
165
+ enableCompression: true,
166
+ enableDeltaStorage: true,
167
+ cacheEnabled: this.options.cacheEnabled
168
+ });
169
+
170
+ this.cache = this.options.cacheEnabled ?
171
+ new CacheManager({ ttl: this.options.cacheTTL }) : null;
172
+
173
+ // Active monitors
174
+ this.activeMonitors = new Map();
175
+ this.monitorStats = new Map();
176
+
177
+ // Notification handlers
178
+ this.notificationHandlers = {
179
+ webhook: this.sendWebhookNotification.bind(this),
180
+ email: this.sendEmailNotification.bind(this),
181
+ slack: this.sendSlackNotification.bind(this)
182
+ };
183
+
184
+ this.initialize();
185
+ }
186
+
187
+ async initialize() {
188
+ try {
189
+ await this.snapshotManager.initialize();
190
+
191
+ // Set up event handlers
192
+ this.setupEventHandlers();
193
+
194
+ this.emit('initialized');
195
+ } catch (error) {
196
+ this.emit('error', { operation: 'initialize', error: error.message });
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ setupEventHandlers() {
202
+ // Handle change tracker events
203
+ this.changeTracker.on('changeDetected', (changeRecord) => {
204
+ this.handleChangeDetected(changeRecord);
205
+ });
206
+
207
+ this.changeTracker.on('baselineCreated', (baseline) => {
208
+ this.emit('baselineCreated', baseline);
209
+ });
210
+
211
+ // Handle snapshot manager events
212
+ this.snapshotManager.on('snapshotStored', (snapshot) => {
213
+ this.emit('snapshotStored', snapshot);
214
+ });
215
+
216
+ this.snapshotManager.on('error', (error) => {
217
+ this.emit('error', error);
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Execute the track changes tool
223
+ * @param {Object} params - Tool parameters
224
+ * @returns {Object} - Execution results
225
+ */
226
+ async execute(params) {
227
+ try {
228
+ const validated = TrackChangesSchema.parse(params);
229
+ const { url, operation } = validated;
230
+
231
+ switch (operation) {
232
+ case 'create_baseline':
233
+ return await this.createBaseline(validated);
234
+
235
+ case 'compare':
236
+ return await this.compareWithBaseline(validated);
237
+
238
+ case 'monitor':
239
+ return await this.setupMonitoring(validated);
240
+
241
+ case 'get_history':
242
+ return await this.getChangeHistory(validated);
243
+
244
+ case 'get_stats':
245
+ return await this.getStatistics(validated);
246
+
247
+ // Enhanced Phase 2.4 operations
248
+ case 'create_scheduled_monitor':
249
+ return await this.createScheduledMonitor(validated);
250
+
251
+ case 'stop_scheduled_monitor':
252
+ return await this.stopScheduledMonitor(validated);
253
+
254
+ case 'get_dashboard':
255
+ return await this.getMonitoringDashboard(validated);
256
+
257
+ case 'export_history':
258
+ return await this.exportHistoricalData(validated);
259
+
260
+ case 'create_alert_rule':
261
+ return await this.createAlertRule(validated);
262
+
263
+ case 'generate_trend_report':
264
+ return await this.generateTrendReport(validated);
265
+
266
+ case 'get_monitoring_templates':
267
+ return await this.getMonitoringTemplates(validated);
268
+
269
+ default:
270
+ throw new Error(`Unknown operation: ${operation}`);
271
+ }
272
+
273
+ } catch (error) {
274
+ return {
275
+ success: false,
276
+ error: error.message,
277
+ timestamp: Date.now()
278
+ };
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Create a baseline for change tracking
284
+ * @param {Object} params - Parameters
285
+ * @returns {Object} - Baseline creation results
286
+ */
287
+ async createBaseline(params) {
288
+ const { url, content, html, trackingOptions, storageOptions } = params;
289
+
290
+ try {
291
+ // Fetch content if not provided
292
+ let sourceContent = content || html;
293
+ let fetchMetadata = {};
294
+
295
+ if (!sourceContent) {
296
+ const fetchResult = await this.fetchContent(url);
297
+ sourceContent = fetchResult.content;
298
+ fetchMetadata = fetchResult.metadata;
299
+ }
300
+
301
+ // Create baseline with change tracker
302
+ const baseline = await this.changeTracker.createBaseline(
303
+ url,
304
+ sourceContent,
305
+ trackingOptions
306
+ );
307
+
308
+ // Store snapshot if enabled
309
+ let snapshotInfo = null;
310
+ if (storageOptions.enableSnapshots) {
311
+ const snapshotResult = await this.snapshotManager.storeSnapshot(
312
+ url,
313
+ sourceContent,
314
+ {
315
+ ...fetchMetadata,
316
+ baseline: true,
317
+ trackingOptions
318
+ }
319
+ );
320
+ snapshotInfo = snapshotResult;
321
+ }
322
+
323
+ return {
324
+ success: true,
325
+ operation: 'create_baseline',
326
+ url,
327
+ baseline: {
328
+ version: baseline.version,
329
+ contentHash: baseline.analysis?.hashes?.page,
330
+ sections: Object.keys(baseline.analysis?.hashes?.sections || {}).length,
331
+ elements: Object.keys(baseline.analysis?.hashes?.elements || {}).length,
332
+ createdAt: baseline.timestamp,
333
+ options: trackingOptions
334
+ },
335
+ snapshot: snapshotInfo,
336
+ timestamp: Date.now()
337
+ };
338
+
339
+ } catch (error) {
340
+ throw new Error(`Failed to create baseline: ${error.message}`);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Compare current content with baseline
346
+ * @param {Object} params - Parameters
347
+ * @returns {Object} - Comparison results
348
+ */
349
+ async compareWithBaseline(params) {
350
+ const { url, content, html, trackingOptions, storageOptions, notificationOptions } = params;
351
+
352
+ try {
353
+ // Fetch current content if not provided
354
+ let currentContent = content || html;
355
+ let fetchMetadata = {};
356
+
357
+ if (!currentContent) {
358
+ const fetchResult = await this.fetchContent(url);
359
+ currentContent = fetchResult.content;
360
+ fetchMetadata = fetchResult.metadata;
361
+ }
362
+
363
+ // Perform comparison
364
+ const comparisonResult = await this.changeTracker.compareWithBaseline(
365
+ url,
366
+ currentContent,
367
+ trackingOptions
368
+ );
369
+
370
+ // Store snapshot if changes detected and storage enabled
371
+ let snapshotInfo = null;
372
+ if (comparisonResult.hasChanges && storageOptions.enableSnapshots) {
373
+ const snapshotResult = await this.snapshotManager.storeSnapshot(
374
+ url,
375
+ currentContent,
376
+ {
377
+ ...fetchMetadata,
378
+ changes: comparisonResult.summary,
379
+ significance: comparisonResult.significance
380
+ }
381
+ );
382
+ snapshotInfo = snapshotResult;
383
+ }
384
+
385
+ // Send notifications if significant changes detected
386
+ if (comparisonResult.hasChanges && notificationOptions) {
387
+ await this.sendNotifications(url, comparisonResult, notificationOptions);
388
+ }
389
+
390
+ return {
391
+ success: true,
392
+ operation: 'compare',
393
+ url,
394
+ hasChanges: comparisonResult.hasChanges,
395
+ significance: comparisonResult.significance,
396
+ changeType: comparisonResult.changeType,
397
+ summary: comparisonResult.summary,
398
+ details: comparisonResult.details,
399
+ metrics: comparisonResult.metrics,
400
+ recommendations: comparisonResult.recommendations,
401
+ snapshot: snapshotInfo,
402
+ timestamp: Date.now()
403
+ };
404
+
405
+ } catch (error) {
406
+ throw new Error(`Failed to compare with baseline: ${error.message}`);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Set up continuous monitoring for a URL
412
+ * @param {Object} params - Parameters
413
+ * @returns {Object} - Monitoring setup results
414
+ */
415
+ async setupMonitoring(params) {
416
+ const { url, monitoringOptions, trackingOptions, storageOptions, notificationOptions } = params;
417
+
418
+ try {
419
+ // Check if already monitoring this URL
420
+ if (this.activeMonitors.has(url)) {
421
+ const existing = this.activeMonitors.get(url);
422
+ clearInterval(existing.timer);
423
+ }
424
+
425
+ // Create monitoring configuration
426
+ const monitorConfig = {
427
+ url,
428
+ options: {
429
+ ...monitoringOptions,
430
+ trackingOptions,
431
+ storageOptions,
432
+ notificationOptions
433
+ },
434
+ stats: {
435
+ started: Date.now(),
436
+ checks: 0,
437
+ changesDetected: 0,
438
+ errors: 0,
439
+ lastCheck: null,
440
+ lastChange: null,
441
+ averageResponseTime: 0
442
+ }
443
+ };
444
+
445
+ // Set up polling timer
446
+ const timer = setInterval(async () => {
447
+ await this.performMonitoringCheck(url, monitorConfig);
448
+ }, monitoringOptions.interval);
449
+
450
+ monitorConfig.timer = timer;
451
+
452
+ // Store active monitor
453
+ this.activeMonitors.set(url, monitorConfig);
454
+ this.monitorStats.set(url, monitorConfig.stats);
455
+
456
+ // Perform initial check
457
+ await this.performMonitoringCheck(url, monitorConfig);
458
+
459
+ return {
460
+ success: true,
461
+ operation: 'monitor',
462
+ url,
463
+ monitoring: {
464
+ enabled: true,
465
+ interval: monitoringOptions.interval,
466
+ notificationThreshold: monitoringOptions.notificationThreshold,
467
+ startedAt: monitorConfig.stats.started
468
+ },
469
+ timestamp: Date.now()
470
+ };
471
+
472
+ } catch (error) {
473
+ throw new Error(`Failed to setup monitoring: ${error.message}`);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Get change history for a URL
479
+ * @param {Object} params - Parameters
480
+ * @returns {Object} - Change history
481
+ */
482
+ async getChangeHistory(params) {
483
+ const { url, queryOptions } = params;
484
+
485
+ try {
486
+ // Get change history from change tracker
487
+ const changeHistory = this.changeTracker.getChangeHistory(url, queryOptions.limit);
488
+
489
+ // Get snapshot history from snapshot manager
490
+ const snapshotHistory = await this.snapshotManager.getChangeHistory(url, queryOptions);
491
+
492
+ // Merge and enrich history data
493
+ const combinedHistory = this.mergeHistoryData(changeHistory, snapshotHistory.history);
494
+
495
+ // Apply filters
496
+ let filteredHistory = combinedHistory;
497
+ if (queryOptions.significanceFilter && queryOptions.significanceFilter !== 'all') {
498
+ filteredHistory = combinedHistory.filter(entry =>
499
+ this.matchesSignificanceFilter(entry, queryOptions.significanceFilter)
500
+ );
501
+ }
502
+
503
+ // Apply pagination
504
+ const start = queryOptions.offset || 0;
505
+ const end = start + (queryOptions.limit || 50);
506
+ const paginatedHistory = filteredHistory.slice(start, end);
507
+
508
+ return {
509
+ success: true,
510
+ operation: 'get_history',
511
+ url,
512
+ history: paginatedHistory,
513
+ pagination: {
514
+ total: filteredHistory.length,
515
+ limit: queryOptions.limit,
516
+ offset: queryOptions.offset,
517
+ hasMore: end < filteredHistory.length
518
+ },
519
+ timespan: {
520
+ earliest: combinedHistory.length > 0 ?
521
+ combinedHistory[combinedHistory.length - 1].timestamp : null,
522
+ latest: combinedHistory.length > 0 ?
523
+ combinedHistory[0].timestamp : null,
524
+ totalEntries: combinedHistory.length
525
+ },
526
+ timestamp: Date.now()
527
+ };
528
+
529
+ } catch (error) {
530
+ throw new Error(`Failed to get change history: ${error.message}`);
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Get statistics for change tracking
536
+ * @param {Object} params - Parameters
537
+ * @returns {Object} - Statistics
538
+ */
539
+ async getStatistics(params) {
540
+ const { url } = params;
541
+
542
+ try {
543
+ // Get change tracker stats
544
+ const changeTrackerStats = this.changeTracker.getStats();
545
+
546
+ // Get snapshot manager stats
547
+ const snapshotManagerStats = this.snapshotManager.getStats();
548
+
549
+ // Get monitoring stats
550
+ const monitoringStats = url ?
551
+ this.monitorStats.get(url) :
552
+ this.getAggregatedMonitoringStats();
553
+
554
+ // Get URL-specific stats if URL provided
555
+ let urlStats = null;
556
+ if (url) {
557
+ urlStats = await this.getUrlSpecificStats(url);
558
+ }
559
+
560
+ return {
561
+ success: true,
562
+ operation: 'get_stats',
563
+ url: url || 'global',
564
+ stats: {
565
+ changeTracking: changeTrackerStats,
566
+ snapshotStorage: snapshotManagerStats,
567
+ monitoring: monitoringStats,
568
+ urlSpecific: urlStats,
569
+ system: {
570
+ activeMonitors: this.activeMonitors.size,
571
+ cacheEnabled: !!this.cache,
572
+ cacheStats: this.cache ? this.cache.getStats() : null
573
+ }
574
+ },
575
+ timestamp: Date.now()
576
+ };
577
+
578
+ } catch (error) {
579
+ throw new Error(`Failed to get statistics: ${error.message}`);
580
+ }
581
+ }
582
+
583
+ // Helper methods
584
+
585
+ async fetchContent(url) {
586
+ try {
587
+ const response = await fetch(url, {
588
+ headers: {
589
+ 'User-Agent': 'MCP-WebScraper-ChangeTracker/3.0',
590
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
591
+ 'Accept-Language': 'en-US,en;q=0.5',
592
+ 'Accept-Encoding': 'gzip, deflate',
593
+ 'Cache-Control': 'no-cache'
594
+ },
595
+ timeout: 30000
596
+ });
597
+
598
+ if (!response.ok) {
599
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
600
+ }
601
+
602
+ const content = await response.text();
603
+
604
+ return {
605
+ content,
606
+ metadata: {
607
+ statusCode: response.status,
608
+ contentType: response.headers.get('content-type'),
609
+ contentLength: content.length,
610
+ lastModified: response.headers.get('last-modified'),
611
+ etag: response.headers.get('etag'),
612
+ fetchedAt: Date.now()
613
+ }
614
+ };
615
+
616
+ } catch (error) {
617
+ throw new Error(`Failed to fetch content: ${error.message}`);
618
+ }
619
+ }
620
+
621
+ async performMonitoringCheck(url, monitorConfig) {
622
+ const startTime = Date.now();
623
+
624
+ try {
625
+ monitorConfig.stats.checks++;
626
+
627
+ // Fetch current content
628
+ const fetchResult = await this.fetchContent(url);
629
+
630
+ // Perform comparison
631
+ const comparisonResult = await this.changeTracker.compareWithBaseline(
632
+ url,
633
+ fetchResult.content,
634
+ monitorConfig.options.trackingOptions
635
+ );
636
+
637
+ // Update stats
638
+ const responseTime = Date.now() - startTime;
639
+ monitorConfig.stats.averageResponseTime =
640
+ (monitorConfig.stats.averageResponseTime * (monitorConfig.stats.checks - 1) + responseTime) /
641
+ monitorConfig.stats.checks;
642
+
643
+ monitorConfig.stats.lastCheck = Date.now();
644
+
645
+ // Handle changes if detected
646
+ if (comparisonResult.hasChanges) {
647
+ monitorConfig.stats.changesDetected++;
648
+ monitorConfig.stats.lastChange = Date.now();
649
+
650
+ // Check if change meets notification threshold
651
+ if (this.meetsNotificationThreshold(
652
+ comparisonResult.significance,
653
+ monitorConfig.options.notificationThreshold
654
+ )) {
655
+ // Store snapshot if enabled
656
+ if (monitorConfig.options.storageOptions?.enableSnapshots) {
657
+ await this.snapshotManager.storeSnapshot(
658
+ url,
659
+ fetchResult.content,
660
+ {
661
+ ...fetchResult.metadata,
662
+ changes: comparisonResult.summary,
663
+ significance: comparisonResult.significance,
664
+ monitoring: true
665
+ }
666
+ );
667
+ }
668
+
669
+ // Send notifications
670
+ if (monitorConfig.options.notificationOptions) {
671
+ await this.sendNotifications(
672
+ url,
673
+ comparisonResult,
674
+ monitorConfig.options.notificationOptions
675
+ );
676
+ }
677
+ }
678
+ }
679
+
680
+ this.emit('monitoringCheck', {
681
+ url,
682
+ hasChanges: comparisonResult.hasChanges,
683
+ significance: comparisonResult.significance,
684
+ responseTime,
685
+ timestamp: Date.now()
686
+ });
687
+
688
+ } catch (error) {
689
+ monitorConfig.stats.errors++;
690
+
691
+ this.emit('monitoringError', {
692
+ url,
693
+ error: error.message,
694
+ timestamp: Date.now()
695
+ });
696
+
697
+ // If too many errors, disable monitoring
698
+ if (monitorConfig.stats.errors > monitorConfig.options.maxRetries) {
699
+ this.stopMonitoring(url);
700
+
701
+ this.emit('monitoringDisabled', {
702
+ url,
703
+ reason: 'Too many errors',
704
+ totalErrors: monitorConfig.stats.errors
705
+ });
706
+ }
707
+ }
708
+ }
709
+
710
+ async sendNotifications(url, changeResult, notificationOptions) {
711
+ const notifications = [];
712
+
713
+ if (notificationOptions.webhook?.enabled) {
714
+ notifications.push(this.sendWebhookNotification(url, changeResult, notificationOptions.webhook));
715
+ }
716
+
717
+ if (notificationOptions.email?.enabled) {
718
+ notifications.push(this.sendEmailNotification(url, changeResult, notificationOptions.email));
719
+ }
720
+
721
+ if (notificationOptions.slack?.enabled) {
722
+ notifications.push(this.sendSlackNotification(url, changeResult, notificationOptions.slack));
723
+ }
724
+
725
+ await Promise.allSettled(notifications);
726
+ }
727
+
728
+ async sendWebhookNotification(url, changeResult, webhookConfig) {
729
+ try {
730
+ const payload = {
731
+ event: 'change_detected',
732
+ url,
733
+ timestamp: Date.now(),
734
+ significance: changeResult.significance,
735
+ changeType: changeResult.changeType,
736
+ summary: changeResult.summary,
737
+ details: webhookConfig.includeContent ? changeResult.details : undefined
738
+ };
739
+
740
+ const response = await fetch(webhookConfig.url, {
741
+ method: webhookConfig.method || 'POST',
742
+ headers: {
743
+ 'Content-Type': 'application/json',
744
+ 'User-Agent': 'MCP-WebScraper-ChangeTracker/3.0',
745
+ ...webhookConfig.headers
746
+ },
747
+ body: JSON.stringify(payload)
748
+ });
749
+
750
+ if (!response.ok) {
751
+ throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
752
+ }
753
+
754
+ this.emit('notificationSent', {
755
+ type: 'webhook',
756
+ url,
757
+ success: true
758
+ });
759
+
760
+ } catch (error) {
761
+ this.emit('notificationError', {
762
+ type: 'webhook',
763
+ url,
764
+ error: error.message
765
+ });
766
+ }
767
+ }
768
+
769
+ async sendEmailNotification(url, changeResult, emailConfig) {
770
+ // Email notification implementation would go here
771
+ // This would integrate with email service providers
772
+ this.emit('notificationSent', {
773
+ type: 'email',
774
+ url,
775
+ success: true,
776
+ note: 'Email notifications require external service integration'
777
+ });
778
+ }
779
+
780
+ async sendSlackNotification(url, changeResult, slackConfig) {
781
+ try {
782
+ const payload = {
783
+ text: `🔄 Content Change Detected`,
784
+ attachments: [{
785
+ color: this.getSlackColor(changeResult.significance),
786
+ fields: [
787
+ {
788
+ title: 'URL',
789
+ value: url,
790
+ short: false
791
+ },
792
+ {
793
+ title: 'Significance',
794
+ value: changeResult.significance.toUpperCase(),
795
+ short: true
796
+ },
797
+ {
798
+ title: 'Change Type',
799
+ value: changeResult.changeType.replace('_', ' '),
800
+ short: true
801
+ },
802
+ {
803
+ title: 'Summary',
804
+ value: changeResult.summary.changeDescription,
805
+ short: false
806
+ }
807
+ ],
808
+ timestamp: Math.floor(Date.now() / 1000)
809
+ }],
810
+ channel: slackConfig.channel,
811
+ username: slackConfig.username || 'Change Tracker'
812
+ };
813
+
814
+ const response = await fetch(slackConfig.webhookUrl, {
815
+ method: 'POST',
816
+ headers: {
817
+ 'Content-Type': 'application/json'
818
+ },
819
+ body: JSON.stringify(payload)
820
+ });
821
+
822
+ if (!response.ok) {
823
+ throw new Error(`Slack notification failed: ${response.status}`);
824
+ }
825
+
826
+ this.emit('notificationSent', {
827
+ type: 'slack',
828
+ url,
829
+ success: true
830
+ });
831
+
832
+ } catch (error) {
833
+ this.emit('notificationError', {
834
+ type: 'slack',
835
+ url,
836
+ error: error.message
837
+ });
838
+ }
839
+ }
840
+
841
+ mergeHistoryData(changeHistory, snapshotHistory) {
842
+ // Merge change tracker history with snapshot history
843
+ const merged = [];
844
+
845
+ // Add change history entries
846
+ changeHistory.forEach(entry => {
847
+ merged.push({
848
+ ...entry,
849
+ source: 'change_tracker',
850
+ hasSnapshot: false
851
+ });
852
+ });
853
+
854
+ // Add snapshot history entries
855
+ snapshotHistory.forEach(entry => {
856
+ const existing = merged.find(m =>
857
+ Math.abs(m.timestamp - entry.timestamp) < 60000 // Within 1 minute
858
+ );
859
+
860
+ if (existing) {
861
+ existing.hasSnapshot = true;
862
+ existing.snapshotId = entry.snapshotId;
863
+ } else {
864
+ merged.push({
865
+ ...entry,
866
+ source: 'snapshot',
867
+ hasSnapshot: true
868
+ });
869
+ }
870
+ });
871
+
872
+ // Sort by timestamp (newest first)
873
+ return merged.sort((a, b) => b.timestamp - a.timestamp);
874
+ }
875
+
876
+ matchesSignificanceFilter(entry, filter) {
877
+ const significanceLevels = ['none', 'minor', 'moderate', 'major', 'critical'];
878
+ const entryLevel = significanceLevels.indexOf(entry.significance || 'none');
879
+ const filterLevel = significanceLevels.indexOf(filter);
880
+
881
+ return entryLevel >= filterLevel;
882
+ }
883
+
884
+ meetsNotificationThreshold(significance, threshold) {
885
+ const levels = ['none', 'minor', 'moderate', 'major', 'critical'];
886
+ const significanceLevel = levels.indexOf(significance);
887
+ const thresholdLevel = levels.indexOf(threshold);
888
+
889
+ return significanceLevel >= thresholdLevel;
890
+ }
891
+
892
+ getSlackColor(significance) {
893
+ const colors = {
894
+ 'none': '#36a64f',
895
+ 'minor': '#ffeb3b',
896
+ 'moderate': '#ff9800',
897
+ 'major': '#f44336',
898
+ 'critical': '#9c27b0'
899
+ };
900
+
901
+ return colors[significance] || '#36a64f';
902
+ }
903
+
904
+ async getUrlSpecificStats(url) {
905
+ try {
906
+ const changeHistory = this.changeTracker.getChangeHistory(url, 100);
907
+ const snapshotHistory = await this.snapshotManager.querySnapshots({
908
+ url,
909
+ limit: 100,
910
+ includeContent: false
911
+ });
912
+
913
+ return {
914
+ totalChanges: changeHistory.length,
915
+ totalSnapshots: snapshotHistory.snapshots.length,
916
+ lastChange: changeHistory.length > 0 ? changeHistory[0].timestamp : null,
917
+ averageChangeInterval: this.calculateAverageInterval(changeHistory),
918
+ significanceDistribution: this.calculateSignificanceDistribution(changeHistory),
919
+ isBeingMonitored: this.activeMonitors.has(url)
920
+ };
921
+
922
+ } catch (error) {
923
+ return {
924
+ error: error.message
925
+ };
926
+ }
927
+ }
928
+
929
+ getAggregatedMonitoringStats() {
930
+ const stats = {
931
+ totalMonitors: this.activeMonitors.size,
932
+ totalChecks: 0,
933
+ totalChanges: 0,
934
+ totalErrors: 0,
935
+ averageResponseTime: 0,
936
+ oldestMonitor: null,
937
+ newestMonitor: null
938
+ };
939
+
940
+ const allStats = Array.from(this.monitorStats.values());
941
+
942
+ if (allStats.length === 0) return stats;
943
+
944
+ stats.totalChecks = allStats.reduce((sum, s) => sum + s.checks, 0);
945
+ stats.totalChanges = allStats.reduce((sum, s) => sum + s.changesDetected, 0);
946
+ stats.totalErrors = allStats.reduce((sum, s) => sum + s.errors, 0);
947
+ stats.averageResponseTime = allStats.reduce((sum, s) => sum + s.averageResponseTime, 0) / allStats.length;
948
+ stats.oldestMonitor = Math.min(...allStats.map(s => s.started));
949
+ stats.newestMonitor = Math.max(...allStats.map(s => s.started));
950
+
951
+ return stats;
952
+ }
953
+
954
+ calculateAverageInterval(changeHistory) {
955
+ if (changeHistory.length < 2) return null;
956
+
957
+ let totalInterval = 0;
958
+ for (let i = 1; i < changeHistory.length; i++) {
959
+ totalInterval += changeHistory[i - 1].timestamp - changeHistory[i].timestamp;
960
+ }
961
+
962
+ return totalInterval / (changeHistory.length - 1);
963
+ }
964
+
965
+ calculateSignificanceDistribution(changeHistory) {
966
+ const distribution = {
967
+ none: 0,
968
+ minor: 0,
969
+ moderate: 0,
970
+ major: 0,
971
+ critical: 0
972
+ };
973
+
974
+ changeHistory.forEach(entry => {
975
+ const significance = entry.significance || 'none';
976
+ if (distribution.hasOwnProperty(significance)) {
977
+ distribution[significance]++;
978
+ }
979
+ });
980
+
981
+ return distribution;
982
+ }
983
+
984
+ async handleChangeDetected(changeRecord) {
985
+ // Store snapshot if significant change
986
+ if (changeRecord.significance !== 'none') {
987
+ try {
988
+ await this.snapshotManager.storeSnapshot(
989
+ changeRecord.url,
990
+ changeRecord.details.current || '',
991
+ {
992
+ changes: changeRecord.details,
993
+ significance: changeRecord.significance,
994
+ changeType: changeRecord.changeType
995
+ }
996
+ );
997
+ } catch (error) {
998
+ this.emit('error', {
999
+ operation: 'storeChangeSnapshot',
1000
+ url: changeRecord.url,
1001
+ error: error.message
1002
+ });
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ // Public API methods
1008
+
1009
+ stopMonitoring(url) {
1010
+ if (this.activeMonitors.has(url)) {
1011
+ const monitor = this.activeMonitors.get(url);
1012
+ clearInterval(monitor.timer);
1013
+ this.activeMonitors.delete(url);
1014
+
1015
+ this.emit('monitoringStopped', { url });
1016
+ return true;
1017
+ }
1018
+ return false;
1019
+ }
1020
+
1021
+ stopAllMonitoring() {
1022
+ const urls = Array.from(this.activeMonitors.keys());
1023
+ urls.forEach(url => this.stopMonitoring(url));
1024
+
1025
+ this.emit('allMonitoringStopped', { count: urls.length });
1026
+ return urls.length;
1027
+ }
1028
+
1029
+ getActiveMonitors() {
1030
+ return Array.from(this.activeMonitors.keys()).map(url => ({
1031
+ url,
1032
+ config: this.activeMonitors.get(url).options,
1033
+ stats: this.monitorStats.get(url)
1034
+ }));
1035
+ }
1036
+
1037
+ /**
1038
+ * Create scheduled monitor using enhanced features
1039
+ * @param {Object} params - Parameters
1040
+ * @returns {Object} - Scheduled monitor results
1041
+ */
1042
+ async createScheduledMonitor(params) {
1043
+ const { url, scheduledMonitorOptions, trackingOptions, notificationOptions } = params;
1044
+
1045
+ try {
1046
+ const schedule = scheduledMonitorOptions?.schedule || '0 */1 * * *'; // Hourly default
1047
+ const templateId = scheduledMonitorOptions?.templateId;
1048
+
1049
+ // Apply template if specified
1050
+ let monitorOptions = { ...trackingOptions };
1051
+ if (templateId && this.changeTracker.monitoringTemplates.has(templateId)) {
1052
+ const template = this.changeTracker.monitoringTemplates.get(templateId);
1053
+ monitorOptions = { ...template.options, ...monitorOptions };
1054
+ }
1055
+
1056
+ // Create scheduled monitor
1057
+ const result = await this.changeTracker.createScheduledMonitor(
1058
+ url,
1059
+ schedule,
1060
+ {
1061
+ ...monitorOptions,
1062
+ alertRules: {
1063
+ threshold: 'moderate',
1064
+ methods: ['webhook'],
1065
+ throttle: 600000,
1066
+ ...notificationOptions
1067
+ }
1068
+ }
1069
+ );
1070
+
1071
+ return {
1072
+ success: true,
1073
+ operation: 'create_scheduled_monitor',
1074
+ url,
1075
+ monitor: result,
1076
+ template: templateId ? this.changeTracker.monitoringTemplates.get(templateId)?.name : null,
1077
+ timestamp: Date.now()
1078
+ };
1079
+
1080
+ } catch (error) {
1081
+ throw new Error(`Failed to create scheduled monitor: ${error.message}`);
1082
+ }
1083
+ }
1084
+
1085
+ /**
1086
+ * Stop scheduled monitor
1087
+ * @param {Object} params - Parameters
1088
+ * @returns {Object} - Stop results
1089
+ */
1090
+ async stopScheduledMonitor(params) {
1091
+ const { url } = params;
1092
+
1093
+ try {
1094
+ // Find and stop the scheduled monitor for this URL
1095
+ let stoppedMonitors = 0;
1096
+
1097
+ for (const [id, monitor] of this.changeTracker.scheduledMonitors.entries()) {
1098
+ if (monitor.url === url) {
1099
+ if (monitor.cronJob) {
1100
+ monitor.cronJob.destroy();
1101
+ }
1102
+ monitor.status = 'stopped';
1103
+ this.changeTracker.scheduledMonitors.delete(id);
1104
+ stoppedMonitors++;
1105
+ }
1106
+ }
1107
+
1108
+ return {
1109
+ success: true,
1110
+ operation: 'stop_scheduled_monitor',
1111
+ url,
1112
+ stoppedMonitors,
1113
+ timestamp: Date.now()
1114
+ };
1115
+
1116
+ } catch (error) {
1117
+ throw new Error(`Failed to stop scheduled monitor: ${error.message}`);
1118
+ }
1119
+ }
1120
+
1121
+ /**
1122
+ * Get monitoring dashboard
1123
+ * @param {Object} params - Parameters
1124
+ * @returns {Object} - Dashboard data
1125
+ */
1126
+ async getMonitoringDashboard(params) {
1127
+ const { dashboardOptions } = params;
1128
+
1129
+ try {
1130
+ const dashboard = this.changeTracker.getMonitoringDashboard();
1131
+
1132
+ // Filter based on options
1133
+ if (!dashboardOptions?.includeRecentAlerts) {
1134
+ delete dashboard.recentAlerts;
1135
+ }
1136
+
1137
+ if (!dashboardOptions?.includeTrends) {
1138
+ delete dashboard.trends;
1139
+ }
1140
+
1141
+ if (!dashboardOptions?.includeMonitorStatus) {
1142
+ dashboard.monitors = dashboard.monitors.map(m => ({
1143
+ id: m.id,
1144
+ url: m.url,
1145
+ status: m.status
1146
+ }));
1147
+ }
1148
+
1149
+ return {
1150
+ success: true,
1151
+ operation: 'get_dashboard',
1152
+ dashboard,
1153
+ timestamp: Date.now()
1154
+ };
1155
+
1156
+ } catch (error) {
1157
+ throw new Error(`Failed to get monitoring dashboard: ${error.message}`);
1158
+ }
1159
+ }
1160
+
1161
+ /**
1162
+ * Export historical data
1163
+ * @param {Object} params - Parameters
1164
+ * @returns {Object} - Exported data
1165
+ */
1166
+ async exportHistoricalData(params) {
1167
+ const { url, exportOptions } = params;
1168
+
1169
+ try {
1170
+ const exportData = await this.changeTracker.exportHistoricalData({
1171
+ ...exportOptions,
1172
+ url
1173
+ });
1174
+
1175
+ return {
1176
+ success: true,
1177
+ operation: 'export_history',
1178
+ url: url || 'global',
1179
+ export: exportData,
1180
+ timestamp: Date.now()
1181
+ };
1182
+
1183
+ } catch (error) {
1184
+ throw new Error(`Failed to export historical data: ${error.message}`);
1185
+ }
1186
+ }
1187
+
1188
+ /**
1189
+ * Create custom alert rule
1190
+ * @param {Object} params - Parameters
1191
+ * @returns {Object} - Alert rule results
1192
+ */
1193
+ async createAlertRule(params) {
1194
+ const { alertRuleOptions } = params;
1195
+
1196
+ try {
1197
+ const ruleId = alertRuleOptions?.ruleId ||
1198
+ `custom_rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1199
+
1200
+ const rule = {
1201
+ condition: this.parseCondition(alertRuleOptions?.condition || 'significance === "major"'),
1202
+ actions: alertRuleOptions?.actions || ['webhook'],
1203
+ throttle: alertRuleOptions?.throttle || 600000,
1204
+ priority: alertRuleOptions?.priority || 'medium'
1205
+ };
1206
+
1207
+ // Store the alert rule
1208
+ this.changeTracker.alertRules.set(ruleId, rule);
1209
+
1210
+ return {
1211
+ success: true,
1212
+ operation: 'create_alert_rule',
1213
+ ruleId,
1214
+ rule,
1215
+ timestamp: Date.now()
1216
+ };
1217
+
1218
+ } catch (error) {
1219
+ throw new Error(`Failed to create alert rule: ${error.message}`);
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * Generate trend analysis report
1225
+ * @param {Object} params - Parameters
1226
+ * @returns {Object} - Trend report
1227
+ */
1228
+ async generateTrendReport(params) {
1229
+ const { url } = params;
1230
+
1231
+ try {
1232
+ const report = await this.changeTracker.generateTrendAnalysisReport(url);
1233
+
1234
+ return {
1235
+ success: true,
1236
+ operation: 'generate_trend_report',
1237
+ report,
1238
+ timestamp: Date.now()
1239
+ };
1240
+
1241
+ } catch (error) {
1242
+ throw new Error(`Failed to generate trend report: ${error.message}`);
1243
+ }
1244
+ }
1245
+
1246
+ /**
1247
+ * Get available monitoring templates
1248
+ * @param {Object} params - Parameters
1249
+ * @returns {Object} - Templates list
1250
+ */
1251
+ async getMonitoringTemplates(params) {
1252
+ try {
1253
+ const templates = {};
1254
+
1255
+ for (const [id, template] of this.changeTracker.monitoringTemplates.entries()) {
1256
+ templates[id] = {
1257
+ name: template.name,
1258
+ frequency: template.frequency,
1259
+ description: this.generateTemplateDescription(template),
1260
+ options: template.options,
1261
+ alertRules: template.alertRules
1262
+ };
1263
+ }
1264
+
1265
+ return {
1266
+ success: true,
1267
+ operation: 'get_monitoring_templates',
1268
+ templates,
1269
+ count: Object.keys(templates).length,
1270
+ timestamp: Date.now()
1271
+ };
1272
+
1273
+ } catch (error) {
1274
+ throw new Error(`Failed to get monitoring templates: ${error.message}`);
1275
+ }
1276
+ }
1277
+
1278
+ // Helper methods for enhanced features
1279
+
1280
+ parseCondition(conditionString) {
1281
+ // Simple condition parser - in production would use a proper parser
1282
+ return (changeResult, history) => {
1283
+ try {
1284
+ // Basic condition evaluation
1285
+ if (conditionString.includes('significance')) {
1286
+ const match = conditionString.match(/significance\s*===\s*["'](\w+)["']/);
1287
+ if (match) {
1288
+ return changeResult.significance === match[1];
1289
+ }
1290
+ }
1291
+
1292
+ if (conditionString.includes('frequent')) {
1293
+ const recent = history.filter(h => Date.now() - h.timestamp < 3600000);
1294
+ return recent.length > 3;
1295
+ }
1296
+
1297
+ return false;
1298
+ } catch (error) {
1299
+ return false;
1300
+ }
1301
+ };
1302
+ }
1303
+
1304
+ generateTemplateDescription(template) {
1305
+ const descriptions = {
1306
+ 'news-site': 'Optimized for news websites with frequent content updates',
1307
+ 'e-commerce': 'Tracks product pages, prices, and inventory changes',
1308
+ 'documentation': 'Monitors documentation sites with less frequent but important changes'
1309
+ };
1310
+
1311
+ return descriptions[template.name] || 'Custom monitoring template';
1312
+ }
1313
+
1314
+ async shutdown() {
1315
+ this.stopAllMonitoring();
1316
+ await this.snapshotManager.shutdown();
1317
+ await this.changeTracker.cleanup();
1318
+ this.emit('shutdown');
1319
+ }
1320
+ }
1321
+
1322
+ export default TrackChangesTool;
1323
+ // Create and export tool instance for MCP compatibility
1324
+ export const trackChangesTool = new TrackChangesTool();
1325
+
1326
+ // Add name property for MCP protocol compliance
1327
+ trackChangesTool.name = 'track_changes';
1328
+
1329
+ // Add validateParameters method for MCP protocol compliance
1330
+ trackChangesTool.validateParameters = function(params) {
1331
+ return TrackChangesSchema.parse(params);
1332
+ };
1333
+
1334
+ // Add description property for MCP protocol compliance
1335
+ trackChangesTool.description = 'Track and analyze content changes with baseline capture, comparison, and monitoring capabilities';
1336
+
1337
+ // Add inputSchema property for MCP protocol compliance
1338
+ trackChangesTool.inputSchema = {
1339
+ type: 'object',
1340
+ properties: {
1341
+ url: {
1342
+ type: 'string',
1343
+ description: 'URL to track for changes'
1344
+ },
1345
+ operation: {
1346
+ type: 'string',
1347
+ description: 'Operation to perform: create_baseline, compare, monitor, get_history, get_stats'
1348
+ },
1349
+ content: {
1350
+ type: 'string',
1351
+ description: 'Content to analyze or compare'
1352
+ }
1353
+ },
1354
+ required: ['url']
1355
+ };