@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.
- 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 +75 -29
- package/dist/jq/handler.js +11 -56
- package/dist/jq/tool.d.ts +3 -9
- package/dist/jq/tool.js +4 -4
- 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/types/index.d.ts +0 -21
- package/dist/utils/filename.d.ts +0 -8
- package/dist/utils/filename.js +0 -10
- 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';
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
358
|
-
}
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
};
|
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
|
}
|
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
|
+
}
|