@anyshift/mcp-proxy 0.3.6 → 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.
- package/dist/__tests__/unit/timeseries.test.d.ts +1 -0
- package/dist/__tests__/unit/timeseries.test.js +217 -0
- package/dist/fileWriter/writer.js +1 -1
- package/dist/index.js +53 -2
- package/dist/jq/handler.js +11 -56
- package/dist/timeseries/algorithms/index.d.ts +8 -0
- package/dist/timeseries/algorithms/index.js +8 -0
- package/dist/timeseries/algorithms/mad.d.ts +15 -0
- package/dist/timeseries/algorithms/mad.js +44 -0
- package/dist/timeseries/algorithms/moving-average.d.ts +15 -0
- package/dist/timeseries/algorithms/moving-average.js +72 -0
- package/dist/timeseries/algorithms/rolling-quantile.d.ts +16 -0
- package/dist/timeseries/algorithms/rolling-quantile.js +78 -0
- package/dist/timeseries/algorithms/stats.d.ts +49 -0
- package/dist/timeseries/algorithms/stats.js +139 -0
- package/dist/timeseries/algorithms/threshold.d.ts +15 -0
- package/dist/timeseries/algorithms/threshold.js +49 -0
- package/dist/timeseries/handler.d.ts +10 -0
- package/dist/timeseries/handler.js +292 -0
- package/dist/timeseries/index.d.ts +68 -0
- package/dist/timeseries/index.js +26 -0
- package/dist/timeseries/tool.d.ts +71 -0
- package/dist/timeseries/tool.js +170 -0
- package/dist/timeseries/types.d.ts +147 -0
- package/dist/timeseries/types.js +4 -0
- package/dist/utils/jq.d.ts +25 -0
- package/dist/utils/jq.js +90 -0
- 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';
|
|
@@ -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
|
-
|
|
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);
|
package/dist/jq/handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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[];
|
|
@@ -0,0 +1,72 @@
|
|
|
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 { movingAverage, movingStandardDeviation } from './stats.js';
|
|
8
|
+
const DEFAULT_THRESHOLD = 2;
|
|
9
|
+
/**
|
|
10
|
+
* Detect anomalies using Moving Average method
|
|
11
|
+
*
|
|
12
|
+
* @param data - Array of data points (should be in time order)
|
|
13
|
+
* @param params - Moving Average parameters
|
|
14
|
+
* @returns Array of detected anomalies
|
|
15
|
+
*/
|
|
16
|
+
export function detectMovingAverageAnomalies(data, params) {
|
|
17
|
+
const { window_size: windowSize, threshold = DEFAULT_THRESHOLD } = params;
|
|
18
|
+
if (windowSize <= 0) {
|
|
19
|
+
throw new Error('window_size must be greater than 0');
|
|
20
|
+
}
|
|
21
|
+
if (windowSize > data.length) {
|
|
22
|
+
throw new Error(`window_size (${windowSize}) cannot be larger than data length (${data.length})`);
|
|
23
|
+
}
|
|
24
|
+
const values = data.map((d) => d.value);
|
|
25
|
+
const ma = movingAverage(values, windowSize);
|
|
26
|
+
const mstd = movingStandardDeviation(values, windowSize);
|
|
27
|
+
const anomalies = [];
|
|
28
|
+
for (let i = 0; i < data.length; i++) {
|
|
29
|
+
// Skip points where we don't have enough data for the window
|
|
30
|
+
if (isNaN(ma[i]) || isNaN(mstd[i])) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const point = data[i];
|
|
34
|
+
const expectedValue = ma[i];
|
|
35
|
+
const stdDev = mstd[i];
|
|
36
|
+
// If standard deviation is 0, all values in window are the same
|
|
37
|
+
if (stdDev === 0) {
|
|
38
|
+
// Only flag as anomaly if current value differs from the constant
|
|
39
|
+
if (point.value !== expectedValue) {
|
|
40
|
+
anomalies.push({
|
|
41
|
+
index: point.index,
|
|
42
|
+
timestamp: point.timestamp,
|
|
43
|
+
value: point.value,
|
|
44
|
+
reason: `Value ${point.value} differs from constant window value ${expectedValue}`,
|
|
45
|
+
metadata: {
|
|
46
|
+
moving_average: expectedValue,
|
|
47
|
+
moving_std_dev: stdDev,
|
|
48
|
+
window_size: windowSize,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const deviation = Math.abs(point.value - expectedValue);
|
|
55
|
+
const deviationScore = deviation / stdDev;
|
|
56
|
+
if (deviationScore > threshold) {
|
|
57
|
+
anomalies.push({
|
|
58
|
+
index: point.index,
|
|
59
|
+
timestamp: point.timestamp,
|
|
60
|
+
value: point.value,
|
|
61
|
+
reason: `Value deviates ${deviationScore.toFixed(2)} std devs from moving average (expected: ${expectedValue.toFixed(2)}, threshold: ${threshold})`,
|
|
62
|
+
metadata: {
|
|
63
|
+
moving_average: expectedValue,
|
|
64
|
+
moving_std_dev: stdDev,
|
|
65
|
+
deviation_score: deviationScore,
|
|
66
|
+
window_size: windowSize,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return anomalies;
|
|
72
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rolling Quantile based anomaly detection (similar to Datadog's Basic algorithm)
|
|
3
|
+
*
|
|
4
|
+
* Uses a sliding window to compute rolling percentiles, then flags points
|
|
5
|
+
* outside the expected range. This is non-parametric and works well for
|
|
6
|
+
* any distribution without assuming normality.
|
|
7
|
+
*/
|
|
8
|
+
import type { DataPoint, Anomaly, RollingQuantileParams } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Detect anomalies using Rolling Quantile method
|
|
11
|
+
*
|
|
12
|
+
* @param data - Array of data points (should be in time order)
|
|
13
|
+
* @param params - Rolling Quantile parameters
|
|
14
|
+
* @returns Array of detected anomalies
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectRollingQuantileAnomalies(data: DataPoint[], params: RollingQuantileParams): Anomaly[];
|