@anyshift/mcp-proxy 0.3.6 → 0.4.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { mean, median, standardDeviation, movingAverage, quantile, rollingQuantiles, } from '../../timeseries/algorithms/stats.js';
3
+ import { detectMadAnomalies } from '../../timeseries/algorithms/mad.js';
4
+ import { detectThresholdAnomalies } from '../../timeseries/algorithms/threshold.js';
5
+ import { detectRollingQuantileAnomalies } from '../../timeseries/algorithms/rolling-quantile.js';
6
+ import { detectMovingAverageAnomalies } from '../../timeseries/algorithms/moving-average.js';
7
+ describe('Statistical Functions', () => {
8
+ describe('mean', () => {
9
+ it('calculates mean correctly', () => {
10
+ expect(mean([1, 2, 3, 4, 5])).toBe(3);
11
+ expect(mean([10, 20, 30])).toBe(20);
12
+ expect(mean([])).toBe(0);
13
+ });
14
+ });
15
+ describe('median', () => {
16
+ it('calculates median for odd-length arrays', () => {
17
+ expect(median([1, 2, 3, 4, 5])).toBe(3);
18
+ expect(median([5, 1, 3])).toBe(3);
19
+ });
20
+ it('calculates median for even-length arrays', () => {
21
+ expect(median([1, 2, 3, 4])).toBe(2.5);
22
+ expect(median([1, 2, 3, 4, 5, 6])).toBe(3.5);
23
+ });
24
+ it('returns 0 for empty arrays', () => {
25
+ expect(median([])).toBe(0);
26
+ });
27
+ });
28
+ describe('standardDeviation', () => {
29
+ it('calculates standard deviation correctly', () => {
30
+ const values = [2, 4, 4, 4, 5, 5, 7, 9];
31
+ const std = standardDeviation(values);
32
+ expect(std).toBeCloseTo(2, 1);
33
+ });
34
+ it('returns 0 for arrays with less than 2 elements', () => {
35
+ expect(standardDeviation([1])).toBe(0);
36
+ expect(standardDeviation([])).toBe(0);
37
+ });
38
+ });
39
+ describe('movingAverage', () => {
40
+ it('calculates moving average correctly', () => {
41
+ const values = [1, 2, 3, 4, 5];
42
+ const ma = movingAverage(values, 3);
43
+ expect(ma[0]).toBeNaN();
44
+ expect(ma[1]).toBeNaN();
45
+ expect(ma[2]).toBe(2); // (1+2+3)/3
46
+ expect(ma[3]).toBe(3); // (2+3+4)/3
47
+ expect(ma[4]).toBe(4); // (3+4+5)/3
48
+ });
49
+ });
50
+ describe('quantile', () => {
51
+ it('calculates quantiles correctly', () => {
52
+ const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
53
+ expect(quantile(values, 0)).toBe(1);
54
+ expect(quantile(values, 0.5)).toBe(5.5);
55
+ expect(quantile(values, 1)).toBe(10);
56
+ expect(quantile(values, 0.25)).toBeCloseTo(3.25, 1);
57
+ expect(quantile(values, 0.75)).toBeCloseTo(7.75, 1);
58
+ });
59
+ it('handles edge cases', () => {
60
+ expect(quantile([], 0.5)).toBe(0);
61
+ expect(quantile([5], 0.5)).toBe(5);
62
+ });
63
+ });
64
+ describe('rollingQuantiles', () => {
65
+ it('calculates rolling quantiles correctly', () => {
66
+ const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
67
+ const { lower, upper } = rollingQuantiles(values, 5, 0.1, 0.9);
68
+ // First 4 values should be NaN (not enough window data)
69
+ expect(lower[0]).toBeNaN();
70
+ expect(lower[3]).toBeNaN();
71
+ expect(upper[0]).toBeNaN();
72
+ expect(upper[3]).toBeNaN();
73
+ // From index 4 onwards, we should have valid quantiles
74
+ expect(lower[4]).toBeDefined();
75
+ expect(upper[4]).toBeDefined();
76
+ expect(lower[4]).toBeLessThan(upper[4]);
77
+ });
78
+ });
79
+ });
80
+ describe('Anomaly Detection Algorithms', () => {
81
+ const createDataPoints = (values) => values.map((value, index) => ({ index, value }));
82
+ const createDataPointsWithTimestamp = (values, baseTimestamp) => values.map((value, index) => ({
83
+ index,
84
+ value,
85
+ timestamp: baseTimestamp + index * 60 // 1 minute apart
86
+ }));
87
+ describe('Rolling Quantile Detection', () => {
88
+ it('detects outliers using rolling quantiles', () => {
89
+ // Values around 10, then a spike to 100
90
+ const values = [10, 11, 10, 12, 11, 10, 11, 10, 100, 11, 10];
91
+ const data = createDataPoints(values);
92
+ const anomalies = detectRollingQuantileAnomalies(data, {
93
+ window_size: 5,
94
+ lower_quantile: 0.05,
95
+ upper_quantile: 0.95,
96
+ });
97
+ expect(anomalies.length).toBeGreaterThan(0);
98
+ expect(anomalies.some(a => a.value === 100)).toBe(true);
99
+ });
100
+ it('returns no anomalies for uniform data', () => {
101
+ const data = createDataPoints([5, 5, 5, 5, 5, 5, 5, 5]);
102
+ const anomalies = detectRollingQuantileAnomalies(data, {
103
+ window_size: 3,
104
+ });
105
+ expect(anomalies.length).toBe(0);
106
+ });
107
+ it('throws error for invalid window size', () => {
108
+ const data = createDataPoints([1, 2, 3]);
109
+ expect(() => detectRollingQuantileAnomalies(data, { window_size: 0 })).toThrow('window_size must be greater than 0');
110
+ });
111
+ it('throws error when window is larger than data', () => {
112
+ const data = createDataPoints([1, 2, 3]);
113
+ expect(() => detectRollingQuantileAnomalies(data, { window_size: 10 })).toThrow('window_size (10) cannot be larger than data length (3)');
114
+ });
115
+ it('throws error for invalid quantile values', () => {
116
+ const data = createDataPoints([1, 2, 3, 4, 5]);
117
+ expect(() => detectRollingQuantileAnomalies(data, {
118
+ window_size: 3,
119
+ lower_quantile: 0.9,
120
+ upper_quantile: 0.1,
121
+ })).toThrow('lower_quantile must be less than upper_quantile');
122
+ });
123
+ });
124
+ describe('MAD Detection', () => {
125
+ it('detects outliers using MAD', () => {
126
+ const values = [10, 11, 10, 12, 11, 10, 11, 100];
127
+ const data = createDataPoints(values);
128
+ const anomalies = detectMadAnomalies(data, { threshold: 2 });
129
+ expect(anomalies.length).toBeGreaterThan(0);
130
+ expect(anomalies.some(a => a.value === 100)).toBe(true);
131
+ });
132
+ it('returns no anomalies for uniform data', () => {
133
+ const data = createDataPoints([5, 5, 5, 5, 5]);
134
+ const anomalies = detectMadAnomalies(data);
135
+ expect(anomalies.length).toBe(0);
136
+ });
137
+ });
138
+ describe('Threshold Detection', () => {
139
+ it('detects values crossing upper thresholds', () => {
140
+ const data = createDataPoints([10, 20, 30, 40, 50]);
141
+ const anomalies = detectThresholdAnomalies(data, {
142
+ thresholds: [
143
+ { name: 'warning', value: 35, direction: 'upper' },
144
+ { name: 'critical', value: 45, direction: 'upper' },
145
+ ],
146
+ });
147
+ expect(anomalies.length).toBe(2);
148
+ expect(anomalies[0].value).toBe(40);
149
+ expect(anomalies[0].crossed_thresholds).toContain('warning');
150
+ expect(anomalies[1].value).toBe(50);
151
+ expect(anomalies[1].crossed_thresholds).toContain('warning');
152
+ expect(anomalies[1].crossed_thresholds).toContain('critical');
153
+ });
154
+ it('detects values crossing lower thresholds', () => {
155
+ const data = createDataPoints([50, 40, 30, 20, 10]);
156
+ const anomalies = detectThresholdAnomalies(data, {
157
+ thresholds: [
158
+ { name: 'min_healthy', value: 25, direction: 'lower' },
159
+ ],
160
+ });
161
+ expect(anomalies.length).toBe(2);
162
+ expect(anomalies[0].value).toBe(20);
163
+ expect(anomalies[1].value).toBe(10);
164
+ });
165
+ it('throws error when no thresholds provided', () => {
166
+ const data = createDataPoints([10, 20, 30]);
167
+ expect(() => detectThresholdAnomalies(data, { thresholds: [] })).toThrow('At least one threshold must be specified');
168
+ });
169
+ });
170
+ describe('Moving Average Detection', () => {
171
+ it('detects deviations from moving average', () => {
172
+ const values = [10, 10, 10, 10, 10, 500, 10, 10];
173
+ const data = createDataPoints(values);
174
+ const anomalies = detectMovingAverageAnomalies(data, {
175
+ window_size: 3,
176
+ threshold: 1,
177
+ });
178
+ expect(anomalies.length).toBeGreaterThan(0);
179
+ });
180
+ it('throws error for invalid window size', () => {
181
+ const data = createDataPoints([1, 2, 3]);
182
+ expect(() => detectMovingAverageAnomalies(data, { window_size: 0 })).toThrow('window_size must be greater than 0');
183
+ });
184
+ it('throws error when window is larger than data', () => {
185
+ const data = createDataPoints([1, 2, 3]);
186
+ expect(() => detectMovingAverageAnomalies(data, { window_size: 10 })).toThrow('window_size (10) cannot be larger than data length (3)');
187
+ });
188
+ });
189
+ });
190
+ describe('Anomaly Pooling', () => {
191
+ // Test pooling by importing the handler and running full detection
192
+ // This tests the integrated pooling behavior
193
+ const createDataPoints = (values) => values.map((value, index) => ({ index, value }));
194
+ it('pools consecutive anomalies together', () => {
195
+ // Create data where indices 3,4,5 are all above threshold
196
+ const data = createDataPoints([10, 10, 10, 100, 110, 105, 10, 10]);
197
+ const anomalies = detectThresholdAnomalies(data, {
198
+ thresholds: [{ name: 'high', value: 50, direction: 'upper' }],
199
+ });
200
+ // Should have 3 consecutive anomalies at indices 3, 4, 5
201
+ expect(anomalies.length).toBe(3);
202
+ expect(anomalies[0].index).toBe(3);
203
+ expect(anomalies[1].index).toBe(4);
204
+ expect(anomalies[2].index).toBe(5);
205
+ });
206
+ it('keeps separate pools for non-consecutive anomalies', () => {
207
+ // Create data where indices 1 and 5 are anomalies (not consecutive)
208
+ const data = createDataPoints([10, 100, 10, 10, 10, 100, 10]);
209
+ const anomalies = detectThresholdAnomalies(data, {
210
+ thresholds: [{ name: 'high', value: 50, direction: 'upper' }],
211
+ });
212
+ // Should have 2 separate anomalies at indices 1 and 5
213
+ expect(anomalies.length).toBe(2);
214
+ expect(anomalies[0].index).toBe(1);
215
+ expect(anomalies[1].index).toBe(5);
216
+ });
217
+ });
@@ -275,7 +275,7 @@ export async function handleToolResponse(config, toolName, args, responseData) {
275
275
  // Generate tool_id for all responses
276
276
  const tool_id = generateToolId(toolName, args, config.toolAbbreviations);
277
277
  // Some tools should always return directly to AI (never write to file)
278
- if (toolName === 'execute_jq_query' || toolName === 'get_label_schema') {
278
+ if (toolName === 'execute_jq_query' || toolName === 'get_label_schema' || toolName === 'detect_timeseries_anomalies') {
279
279
  const { contentToWrite, parsedForSchema } = extractContentForFile(responseData);
280
280
  return {
281
281
  tool_id,
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
20
20
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
21
21
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
22
22
  import { createJqTool } from './jq/index.js';
23
+ import { createTimeseriesTool } from './timeseries/index.js';
23
24
  import { truncateResponseIfNeeded } from './truncation/index.js';
24
25
  import { createFileWriter } from './fileWriter/index.js';
25
26
  import { generateToolId } from './utils/filename.js';
@@ -156,6 +157,12 @@ const ENABLE_JQ = process.env.MCP_PROXY_ENABLE_JQ !== 'false'; // default true
156
157
  * Timeout in milliseconds for JQ query execution
157
158
  */
158
159
  const JQ_TIMEOUT_MS = parseInt(process.env.MCP_PROXY_JQ_TIMEOUT_MS || '30000');
160
+ /**
161
+ * MCP_PROXY_ENABLE_TIMESERIES (OPTIONAL, default: true)
162
+ * Enable the time series anomaly detection tool
163
+ * This tool allows detecting anomalies in time series data extracted from JSON files
164
+ */
165
+ const ENABLE_TIMESERIES = process.env.MCP_PROXY_ENABLE_TIMESERIES !== 'false'; // default true
159
166
  /**
160
167
  * MCP_PROXY_SCHEMA_MAX_DEPTH (OPTIONAL, default: 3)
161
168
  * Maximum depth to traverse when generating query-assist schemas
@@ -243,6 +250,7 @@ if (ENABLE_LOGGING) {
243
250
  console.debug(` Min chars for write: ${MIN_CHARS_FOR_WRITE}`);
244
251
  }
245
252
  console.debug(` JQ tool enabled: ${ENABLE_JQ}`);
253
+ console.debug(` Timeseries tool enabled: ${ENABLE_TIMESERIES}`);
246
254
  if (CHILD_COMMAND) {
247
255
  console.debug(` Pass-through env vars: ${Object.keys(childEnv).length}`);
248
256
  }
@@ -327,6 +335,14 @@ async function main() {
327
335
  timeoutMs: JQ_TIMEOUT_MS
328
336
  });
329
337
  }
338
+ // Timeseries anomaly detection tool configuration
339
+ let timeseriesTool = null;
340
+ if (ENABLE_TIMESERIES) {
341
+ timeseriesTool = createTimeseriesTool({
342
+ allowedPaths: [process.cwd(), OUTPUT_PATH].filter(Boolean),
343
+ timeoutMs: JQ_TIMEOUT_MS // Uses same timeout as JQ since it runs jq internally
344
+ });
345
+ }
330
346
  // ------------------------------------------------------------------------
331
347
  // 5. REGISTER ALL TOOLS (CHILD + PROXY) WITH DESCRIPTION INJECTION
332
348
  // ------------------------------------------------------------------------
@@ -334,9 +350,11 @@ async function main() {
334
350
  const enhancedChildTools = childToolsResponse.tools.map(injectProxyParams);
335
351
  const allTools = [
336
352
  ...enhancedChildTools,
337
- ...(jqTool ? [jqTool.toolDefinition] : []) // JQ tool already has description param
353
+ ...(jqTool ? [jqTool.toolDefinition] : []), // JQ tool already has description param
354
+ ...(timeseriesTool ? [timeseriesTool.toolDefinition] : []) // Timeseries tool already has description param
338
355
  ];
339
- console.debug(`[mcp-proxy] Exposing ${allTools.length} tools total (${childToolsResponse.tools.length} from child${jqTool ? ' + 1 JQ' : ''})`);
356
+ const proxyToolCount = (jqTool ? 1 : 0) + (timeseriesTool ? 1 : 0);
357
+ console.debug(`[mcp-proxy] Exposing ${allTools.length} tools total (${childToolsResponse.tools.length} from child + ${proxyToolCount} proxy tools)`);
340
358
  // ------------------------------------------------------------------------
341
359
  // 6. HANDLE TOOL LIST REQUESTS
342
360
  // ------------------------------------------------------------------------
@@ -379,6 +397,39 @@ async function main() {
379
397
  isError: result.isError
380
398
  };
381
399
  }
400
+ // Handle Timeseries anomaly detection tool locally (if enabled)
401
+ if (toolName === 'detect_timeseries_anomalies' && timeseriesTool) {
402
+ if (ENABLE_LOGGING) {
403
+ console.debug('[mcp-proxy] Executing Timeseries anomaly detection tool locally');
404
+ }
405
+ result = await timeseriesTool.handler({
406
+ params: { arguments: toolArgs }
407
+ });
408
+ // Timeseries tool returns directly, wrap in unified format
409
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
410
+ let outputContent = null;
411
+ if (result.content?.[0]?.text) {
412
+ try {
413
+ outputContent = JSON.parse(result.content[0].text);
414
+ }
415
+ catch {
416
+ // If JSON parsing fails, use the raw text
417
+ outputContent = result.content[0].text;
418
+ }
419
+ }
420
+ const unifiedResponse = {
421
+ tool_id,
422
+ wroteToFile: false,
423
+ outputContent
424
+ };
425
+ return {
426
+ content: [{
427
+ type: 'text',
428
+ text: JSON.stringify(unifiedResponse, null, 2)
429
+ }],
430
+ isError: result.isError
431
+ };
432
+ }
382
433
  // Forward all other tools to child MCP (if child exists)
383
434
  if (!childClient) {
384
435
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
@@ -401,26 +452,72 @@ async function main() {
401
452
  const childReturnedError = !!result.isError;
402
453
  // Process result through file writer to get unified response
403
454
  if (result.content && Array.isArray(result.content) && result.content.length > 0) {
404
- const item = result.content[0];
405
- if (item.type === 'text' && typeof item.text === 'string') {
406
- const originalLength = item.text.length;
455
+ // Extract text content and embedded resources from all content items
456
+ const textContents = [];
457
+ const resources = [];
458
+ for (const item of result.content) {
459
+ if (item.type === 'text' && typeof item.text === 'string') {
460
+ textContents.push(item.text);
461
+ }
462
+ else if (item.type === 'resource' && item.resource) {
463
+ // Handle EmbeddedResource - extract content from resource field
464
+ const resource = item.resource;
465
+ const resourceObj = {};
466
+ if (resource.uri)
467
+ resourceObj.uri = resource.uri;
468
+ if (resource.mimeType)
469
+ resourceObj.mimeType = resource.mimeType;
470
+ // Content can be in 'text' (string) or 'blob' (base64 binary)
471
+ if (resource.text) {
472
+ resourceObj.content = resource.text;
473
+ }
474
+ else if (resource.blob) {
475
+ // For binary content, keep as base64 or decode based on mimeType
476
+ resourceObj.content = typeof resource.blob === 'string'
477
+ ? resource.blob
478
+ : Buffer.from(resource.blob).toString('base64');
479
+ }
480
+ resources.push(resourceObj);
481
+ }
482
+ }
483
+ // Build combined content object
484
+ let combinedContent;
485
+ if (resources.length > 0) {
486
+ // Has resources - combine text and resources into single object
487
+ combinedContent = {
488
+ text: textContents.length === 1 ? textContents[0] : textContents,
489
+ resources: resources.length === 1 ? resources[0] : resources
490
+ };
491
+ if (ENABLE_LOGGING) {
492
+ console.debug(`[mcp-proxy] Combined ${textContents.length} text items and ${resources.length} resources`);
493
+ }
494
+ }
495
+ else if (textContents.length > 0) {
496
+ // Text only - use first text content (original behavior)
497
+ combinedContent = textContents[0];
498
+ }
499
+ if (combinedContent !== undefined) {
500
+ const contentStr = typeof combinedContent === 'string'
501
+ ? combinedContent
502
+ : JSON.stringify(combinedContent);
503
+ const originalLength = contentStr.length;
407
504
  // If child returned error, pass through directly without file writing
408
505
  if (childReturnedError) {
409
506
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
410
507
  if (ENABLE_LOGGING) {
411
- console.debug(`[mcp-proxy] Child MCP returned error for ${toolName}: ${item.text.substring(0, 100)}...`);
508
+ console.debug(`[mcp-proxy] Child MCP returned error for ${toolName}: ${contentStr.substring(0, 100)}...`);
412
509
  }
413
510
  return {
414
511
  content: [{
415
512
  type: 'text',
416
- text: JSON.stringify(createErrorResponse(tool_id, item.text, toolArgs), null, 2)
513
+ text: JSON.stringify(createErrorResponse(tool_id, contentStr, toolArgs), null, 2)
417
514
  }],
418
515
  isError: true
419
516
  };
420
517
  }
421
518
  // Get unified response from file writer
422
519
  const unifiedResponse = await fileWriter.handleResponse(toolName, toolArgs, {
423
- content: [{ type: 'text', text: item.text }]
520
+ content: [{ type: 'text', text: contentStr }]
424
521
  });
425
522
  if (ENABLE_LOGGING) {
426
523
  if (unifiedResponse.wroteToFile) {
@@ -432,13 +529,13 @@ async function main() {
432
529
  }
433
530
  // If not written to file, apply truncation to outputContent
434
531
  if (!unifiedResponse.wroteToFile && unifiedResponse.outputContent) {
435
- const contentStr = typeof unifiedResponse.outputContent === 'string'
532
+ const outputStr = typeof unifiedResponse.outputContent === 'string'
436
533
  ? unifiedResponse.outputContent
437
534
  : JSON.stringify(unifiedResponse.outputContent);
438
- const truncated = truncateResponseIfNeeded(truncationConfig, contentStr);
439
- if (truncated.length < contentStr.length) {
535
+ const truncated = truncateResponseIfNeeded(truncationConfig, outputStr);
536
+ if (truncated.length < outputStr.length) {
440
537
  if (ENABLE_LOGGING) {
441
- console.debug(`[mcp-proxy] Truncated response: ${contentStr.length} → ${truncated.length} chars`);
538
+ console.debug(`[mcp-proxy] Truncated response: ${outputStr.length} → ${truncated.length} chars`);
442
539
  }
443
540
  // Re-parse if it was JSON, otherwise keep as string
444
541
  try {
@@ -1,7 +1,7 @@
1
- import { spawn } from 'child_process';
2
1
  import { existsSync } from 'fs';
3
2
  import path from 'path';
4
3
  import { validatePathWithinAllowedDirs } from '../utils/pathValidation.js';
4
+ import { validateJqQuery, runJq } from '../utils/jq.js';
5
5
  // Default timeout for JQ execution
6
6
  const DEFAULT_TIMEOUT_MS = 30000;
7
7
  /**
@@ -17,19 +17,7 @@ export async function executeJqQuery(config, jqQuery, filePath) {
17
17
  throw new Error('jq_query and file_path are required');
18
18
  }
19
19
  // Sanitize jq query to prevent environment variable access
20
- const dangerousPatterns = [
21
- /\$ENV/i, // $ENV variable access
22
- /env\./i, // env.VARIABLE access
23
- /@env/i, // @env function
24
- /\.env\[/i, // .env["VARIABLE"] access
25
- /getenv/i, // getenv function
26
- /\$__loc__/i, // location info that might leak paths
27
- /input_filename/i, // input filename access
28
- ];
29
- const isDangerous = dangerousPatterns.some((pattern) => pattern.test(jqQuery));
30
- if (isDangerous) {
31
- throw new Error('The jq query contains patterns that could access environment variables or system information. Please use a different query.');
32
- }
20
+ validateJqQuery(jqQuery);
33
21
  // Validate file path
34
22
  if (!path.isAbsolute(filePath)) {
35
23
  throw new Error(`File path must be an absolute path starting with "/": ${filePath}`);
@@ -46,46 +34,13 @@ export async function executeJqQuery(config, jqQuery, filePath) {
46
34
  validatePathWithinAllowedDirs(filePath, config.allowedPaths);
47
35
  // Execute jq query
48
36
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
49
- return new Promise((resolve, reject) => {
50
- const jqProcess = spawn('jq', [jqQuery, filePath], {
51
- stdio: ['pipe', 'pipe', 'pipe'],
52
- timeout: timeoutMs,
53
- });
54
- let stdout = '';
55
- let stderr = '';
56
- jqProcess.stdout.on('data', (data) => {
57
- stdout += data.toString();
58
- });
59
- jqProcess.stderr.on('data', (data) => {
60
- stderr += data.toString();
61
- });
62
- jqProcess.on('close', (code) => {
63
- if (code === 0) {
64
- // Success - return clean response directly to AI
65
- const responseText = stdout.trim();
66
- resolve({
67
- content: [
68
- {
69
- type: 'text',
70
- text: responseText,
71
- },
72
- ],
73
- });
74
- }
75
- else {
76
- // Error
77
- reject(new Error(`jq command failed with exit code ${code}: ${stderr.trim()}`));
78
- }
79
- });
80
- jqProcess.on('error', (error) => {
81
- reject(new Error(`Failed to execute jq command: ${error.message}`));
82
- });
83
- // Handle timeout
84
- setTimeout(() => {
85
- if (!jqProcess.killed) {
86
- jqProcess.kill('SIGTERM');
87
- reject(new Error(`jq command timed out after ${timeoutMs}ms`));
88
- }
89
- }, timeoutMs);
90
- });
37
+ const responseText = await runJq(jqQuery, filePath, timeoutMs);
38
+ return {
39
+ content: [
40
+ {
41
+ type: 'text',
42
+ text: responseText,
43
+ },
44
+ ],
45
+ };
91
46
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Anomaly detection algorithms index
3
+ */
4
+ export * from './stats.js';
5
+ export { detectMadAnomalies } from './mad.js';
6
+ export { detectMovingAverageAnomalies } from './moving-average.js';
7
+ export { detectThresholdAnomalies } from './threshold.js';
8
+ export { detectRollingQuantileAnomalies } from './rolling-quantile.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Anomaly detection algorithms index
3
+ */
4
+ export * from './stats.js';
5
+ export { detectMadAnomalies } from './mad.js';
6
+ export { detectMovingAverageAnomalies } from './moving-average.js';
7
+ export { detectThresholdAnomalies } from './threshold.js';
8
+ export { detectRollingQuantileAnomalies } from './rolling-quantile.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Median Absolute Deviation (MAD) based anomaly detection
3
+ *
4
+ * Uses modified Z-scores based on median and MAD instead of mean and std dev.
5
+ * Most robust to outliers, excellent for non-normal distributions.
6
+ */
7
+ import type { DataPoint, Anomaly, MadParams } from '../types.js';
8
+ /**
9
+ * Detect anomalies using MAD method
10
+ *
11
+ * @param data - Array of data points
12
+ * @param params - MAD parameters
13
+ * @returns Array of detected anomalies
14
+ */
15
+ export declare function detectMadAnomalies(data: DataPoint[], params?: MadParams): Anomaly[];
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Median Absolute Deviation (MAD) based anomaly detection
3
+ *
4
+ * Uses modified Z-scores based on median and MAD instead of mean and std dev.
5
+ * Most robust to outliers, excellent for non-normal distributions.
6
+ */
7
+ import { median, medianAbsoluteDeviation, modifiedZScore } from './stats.js';
8
+ const DEFAULT_THRESHOLD = 3;
9
+ /**
10
+ * Detect anomalies using MAD method
11
+ *
12
+ * @param data - Array of data points
13
+ * @param params - MAD parameters
14
+ * @returns Array of detected anomalies
15
+ */
16
+ export function detectMadAnomalies(data, params = {}) {
17
+ const threshold = params.threshold ?? DEFAULT_THRESHOLD;
18
+ const values = data.map((d) => d.value);
19
+ const med = median(values);
20
+ const mad = medianAbsoluteDeviation(values);
21
+ if (mad === 0) {
22
+ // All values are at the median, no anomalies possible
23
+ return [];
24
+ }
25
+ const anomalies = [];
26
+ for (const point of data) {
27
+ const modZ = modifiedZScore(point.value, med, mad);
28
+ const absModZ = Math.abs(modZ);
29
+ if (absModZ > threshold) {
30
+ anomalies.push({
31
+ index: point.index,
32
+ timestamp: point.timestamp,
33
+ value: point.value,
34
+ reason: `Modified Z-score ${modZ.toFixed(2)} exceeds threshold ±${threshold}`,
35
+ metadata: {
36
+ modified_zscore: modZ,
37
+ median: med,
38
+ mad,
39
+ },
40
+ });
41
+ }
42
+ }
43
+ return anomalies;
44
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Moving Average based anomaly detection
3
+ *
4
+ * Detects points that deviate significantly from a rolling moving average.
5
+ * Good for detecting sudden changes in time series with trends.
6
+ */
7
+ import type { DataPoint, Anomaly, MovingAverageParams } from '../types.js';
8
+ /**
9
+ * Detect anomalies using Moving Average method
10
+ *
11
+ * @param data - Array of data points (should be in time order)
12
+ * @param params - Moving Average parameters
13
+ * @returns Array of detected anomalies
14
+ */
15
+ export declare function detectMovingAverageAnomalies(data: DataPoint[], params: MovingAverageParams): Anomaly[];