@anyshift/mcp-proxy 0.3.5 → 0.4.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 (33) hide show
  1. package/dist/__tests__/unit/timeseries.test.d.ts +1 -0
  2. package/dist/__tests__/unit/timeseries.test.js +217 -0
  3. package/dist/fileWriter/writer.js +1 -1
  4. package/dist/index.js +75 -29
  5. package/dist/jq/handler.js +11 -56
  6. package/dist/jq/tool.d.ts +3 -9
  7. package/dist/jq/tool.js +4 -4
  8. package/dist/timeseries/algorithms/index.d.ts +8 -0
  9. package/dist/timeseries/algorithms/index.js +8 -0
  10. package/dist/timeseries/algorithms/mad.d.ts +15 -0
  11. package/dist/timeseries/algorithms/mad.js +44 -0
  12. package/dist/timeseries/algorithms/moving-average.d.ts +15 -0
  13. package/dist/timeseries/algorithms/moving-average.js +72 -0
  14. package/dist/timeseries/algorithms/rolling-quantile.d.ts +16 -0
  15. package/dist/timeseries/algorithms/rolling-quantile.js +78 -0
  16. package/dist/timeseries/algorithms/stats.d.ts +49 -0
  17. package/dist/timeseries/algorithms/stats.js +139 -0
  18. package/dist/timeseries/algorithms/threshold.d.ts +15 -0
  19. package/dist/timeseries/algorithms/threshold.js +49 -0
  20. package/dist/timeseries/handler.d.ts +10 -0
  21. package/dist/timeseries/handler.js +292 -0
  22. package/dist/timeseries/index.d.ts +68 -0
  23. package/dist/timeseries/index.js +26 -0
  24. package/dist/timeseries/tool.d.ts +71 -0
  25. package/dist/timeseries/tool.js +170 -0
  26. package/dist/timeseries/types.d.ts +147 -0
  27. package/dist/timeseries/types.js +4 -0
  28. package/dist/types/index.d.ts +0 -21
  29. package/dist/utils/filename.d.ts +0 -8
  30. package/dist/utils/filename.js +0 -10
  31. package/dist/utils/jq.d.ts +25 -0
  32. package/dist/utils/jq.js +90 -0
  33. package/package.json +1 -1
@@ -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';
@@ -50,6 +51,13 @@ function injectProxyParams(tool) {
50
51
  modifiedTool.inputSchema.properties[key] = value;
51
52
  }
52
53
  }
54
+ // Ensure isRetryAttempt is required (not optional)
55
+ if (!modifiedTool.inputSchema.required) {
56
+ modifiedTool.inputSchema.required = [];
57
+ }
58
+ if (!modifiedTool.inputSchema.required.includes('isRetryAttempt')) {
59
+ modifiedTool.inputSchema.required = [...modifiedTool.inputSchema.required, 'isRetryAttempt'];
60
+ }
53
61
  return modifiedTool;
54
62
  }
55
63
  /**
@@ -64,6 +72,18 @@ function addRetryMetadata(response, toolArgs) {
64
72
  }
65
73
  return response;
66
74
  }
75
+ /**
76
+ * Create an error response with consistent structure
77
+ */
78
+ function createErrorResponse(tool_id, error, toolArgs) {
79
+ return addRetryMetadata({ tool_id, wroteToFile: false, error }, toolArgs);
80
+ }
81
+ /**
82
+ * Create a success response with content (not written to file)
83
+ */
84
+ function createContentResponse(tool_id, outputContent, toolArgs) {
85
+ return addRetryMetadata({ tool_id, wroteToFile: false, outputContent }, toolArgs);
86
+ }
67
87
  /**
68
88
  * ENVIRONMENT VARIABLE CONTRACT
69
89
  * =============================
@@ -137,6 +157,12 @@ const ENABLE_JQ = process.env.MCP_PROXY_ENABLE_JQ !== 'false'; // default true
137
157
  * Timeout in milliseconds for JQ query execution
138
158
  */
139
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
140
166
  /**
141
167
  * MCP_PROXY_SCHEMA_MAX_DEPTH (OPTIONAL, default: 3)
142
168
  * Maximum depth to traverse when generating query-assist schemas
@@ -224,6 +250,7 @@ if (ENABLE_LOGGING) {
224
250
  console.debug(` Min chars for write: ${MIN_CHARS_FOR_WRITE}`);
225
251
  }
226
252
  console.debug(` JQ tool enabled: ${ENABLE_JQ}`);
253
+ console.debug(` Timeseries tool enabled: ${ENABLE_TIMESERIES}`);
227
254
  if (CHILD_COMMAND) {
228
255
  console.debug(` Pass-through env vars: ${Object.keys(childEnv).length}`);
229
256
  }
@@ -308,6 +335,14 @@ async function main() {
308
335
  timeoutMs: JQ_TIMEOUT_MS
309
336
  });
310
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
+ }
311
346
  // ------------------------------------------------------------------------
312
347
  // 5. REGISTER ALL TOOLS (CHILD + PROXY) WITH DESCRIPTION INJECTION
313
348
  // ------------------------------------------------------------------------
@@ -315,9 +350,11 @@ async function main() {
315
350
  const enhancedChildTools = childToolsResponse.tools.map(injectProxyParams);
316
351
  const allTools = [
317
352
  ...enhancedChildTools,
318
- ...(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
319
355
  ];
320
- 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)`);
321
358
  // ------------------------------------------------------------------------
322
359
  // 6. HANDLE TOOL LIST REQUESTS
323
360
  // ------------------------------------------------------------------------
@@ -351,11 +388,40 @@ async function main() {
351
388
  });
352
389
  // JQ tool returns directly, wrap in unified format
353
390
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
354
- const unifiedResponse = addRetryMetadata({
391
+ const unifiedResponse = createContentResponse(tool_id, result.content?.[0]?.text, toolArgs);
392
+ return {
393
+ content: [{
394
+ type: 'text',
395
+ text: JSON.stringify(unifiedResponse, null, 2)
396
+ }],
397
+ isError: result.isError
398
+ };
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 = {
355
421
  tool_id,
356
422
  wroteToFile: false,
357
- outputContent: result.content?.[0]?.text
358
- }, toolArgs);
423
+ outputContent
424
+ };
359
425
  return {
360
426
  content: [{
361
427
  type: 'text',
@@ -367,15 +433,10 @@ async function main() {
367
433
  // Forward all other tools to child MCP (if child exists)
368
434
  if (!childClient) {
369
435
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
370
- const errorResponse = addRetryMetadata({
371
- tool_id,
372
- wroteToFile: false,
373
- error: `Tool ${toolName} not available in standalone mode (no child MCP)`
374
- }, toolArgs);
375
436
  return {
376
437
  content: [{
377
438
  type: 'text',
378
- text: JSON.stringify(errorResponse, null, 2)
439
+ text: JSON.stringify(createErrorResponse(tool_id, `Tool ${toolName} not available in standalone mode (no child MCP)`, toolArgs), null, 2)
379
440
  }],
380
441
  isError: true
381
442
  };
@@ -397,18 +458,13 @@ async function main() {
397
458
  // If child returned error, pass through directly without file writing
398
459
  if (childReturnedError) {
399
460
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
400
- const errorResponse = addRetryMetadata({
401
- tool_id,
402
- wroteToFile: false,
403
- error: item.text
404
- }, toolArgs);
405
461
  if (ENABLE_LOGGING) {
406
462
  console.debug(`[mcp-proxy] Child MCP returned error for ${toolName}: ${item.text.substring(0, 100)}...`);
407
463
  }
408
464
  return {
409
465
  content: [{
410
466
  type: 'text',
411
- text: JSON.stringify(errorResponse, null, 2)
467
+ text: JSON.stringify(createErrorResponse(tool_id, item.text, toolArgs), null, 2)
412
468
  }],
413
469
  isError: true
414
470
  };
@@ -457,15 +513,10 @@ async function main() {
457
513
  }
458
514
  // Fallback: return result with generated tool_id
459
515
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
460
- const fallbackResponse = addRetryMetadata({
461
- tool_id,
462
- wroteToFile: false,
463
- outputContent: result
464
- }, toolArgs);
465
516
  return {
466
517
  content: [{
467
518
  type: 'text',
468
- text: JSON.stringify(fallbackResponse, null, 2)
519
+ text: JSON.stringify(createContentResponse(tool_id, result, toolArgs), null, 2)
469
520
  }],
470
521
  isError: childReturnedError
471
522
  };
@@ -473,15 +524,10 @@ async function main() {
473
524
  catch (error) {
474
525
  console.error(`[mcp-proxy] Error executing tool ${toolName}:`, error);
475
526
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
476
- const errorResponse = addRetryMetadata({
477
- tool_id,
478
- wroteToFile: false,
479
- error: `Error executing ${toolName}: ${error.message || String(error)}`
480
- }, toolArgs);
481
527
  return {
482
528
  content: [{
483
529
  type: 'text',
484
- text: JSON.stringify(errorResponse, null, 2)
530
+ text: JSON.stringify(createErrorResponse(tool_id, `Error executing ${toolName}: ${error.message || String(error)}`, toolArgs), null, 2)
485
531
  }],
486
532
  isError: true
487
533
  };
@@ -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
  }
package/dist/jq/tool.d.ts CHANGED
@@ -1,25 +1,19 @@
1
1
  import { z } from 'zod';
2
2
  /**
3
3
  * Zod schema for JQ query execution
4
+ * Note: Proxy params (description, isRetryAttempt, originalToolId) are handled
5
+ * by the proxy layer, not validated here. They're defined in JQ_TOOL_DEFINITION's
6
+ * inputSchema for LLM visibility.
4
7
  */
5
8
  export declare const ExecuteJqQuerySchema: z.ZodObject<{
6
9
  jq_query: z.ZodString;
7
10
  file_path: z.ZodString;
8
- description: z.ZodOptional<z.ZodString>;
9
- isRetryAttempt: z.ZodOptional<z.ZodBoolean>;
10
- originalToolId: z.ZodOptional<z.ZodString>;
11
11
  }, "strip", z.ZodTypeAny, {
12
12
  jq_query: string;
13
13
  file_path: string;
14
- description?: string | undefined;
15
- isRetryAttempt?: boolean | undefined;
16
- originalToolId?: string | undefined;
17
14
  }, {
18
15
  jq_query: string;
19
16
  file_path: string;
20
- description?: string | undefined;
21
- isRetryAttempt?: boolean | undefined;
22
- originalToolId?: string | undefined;
23
17
  }>;
24
18
  /**
25
19
  * Tool definition for JQ query execution with enhanced prompts
package/dist/jq/tool.js CHANGED
@@ -2,6 +2,9 @@ import { z } from 'zod';
2
2
  import { PROXY_PARAMS } from '../types/index.js';
3
3
  /**
4
4
  * Zod schema for JQ query execution
5
+ * Note: Proxy params (description, isRetryAttempt, originalToolId) are handled
6
+ * by the proxy layer, not validated here. They're defined in JQ_TOOL_DEFINITION's
7
+ * inputSchema for LLM visibility.
5
8
  */
6
9
  export const ExecuteJqQuerySchema = z.object({
7
10
  jq_query: z
@@ -10,9 +13,6 @@ export const ExecuteJqQuerySchema = z.object({
10
13
  file_path: z
11
14
  .string()
12
15
  .describe('Absolute path starting with "/" pointing to the JSON or JSONL file to process. Must be a valid, existing file with .json or .jsonl extension. The file will be validated for existence and readability before processing.'),
13
- description: z.string().optional().describe(PROXY_PARAMS.description.description),
14
- isRetryAttempt: z.boolean().optional().describe(PROXY_PARAMS.isRetryAttempt.description),
15
- originalToolId: z.string().optional().describe(PROXY_PARAMS.originalToolId.description),
16
16
  });
17
17
  /**
18
18
  * Tool definition for JQ query execution with enhanced prompts
@@ -113,6 +113,6 @@ export const JQ_TOOL_DEFINITION = {
113
113
  },
114
114
  ...PROXY_PARAMS,
115
115
  },
116
- required: ['jq_query', 'file_path'],
116
+ required: ['jq_query', 'file_path', 'isRetryAttempt'],
117
117
  },
118
118
  };
@@ -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
+ }