@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,292 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { validatePathWithinAllowedDirs } from '../utils/pathValidation.js';
|
|
4
|
+
import { validateJqQuery, runJqParsed } from '../utils/jq.js';
|
|
5
|
+
import { detectMadAnomalies, detectMovingAverageAnomalies, detectThresholdAnomalies, detectRollingQuantileAnomalies, } from './algorithms/index.js';
|
|
6
|
+
// Default timeout for jq execution
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
8
|
+
/**
|
|
9
|
+
* Convert a timestamp to UTC time string
|
|
10
|
+
* Handles Unix timestamps (seconds or milliseconds) and ISO strings
|
|
11
|
+
*/
|
|
12
|
+
function toUtcTime(timestamp) {
|
|
13
|
+
if (timestamp === undefined || timestamp === null) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
let date;
|
|
17
|
+
if (typeof timestamp === 'number') {
|
|
18
|
+
// Detect if timestamp is in seconds or milliseconds
|
|
19
|
+
// Unix timestamps in seconds are typically 10 digits (until 2286)
|
|
20
|
+
// Milliseconds are 13 digits
|
|
21
|
+
if (timestamp > 1e12) {
|
|
22
|
+
// Likely milliseconds
|
|
23
|
+
date = new Date(timestamp);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Likely seconds
|
|
27
|
+
date = new Date(timestamp * 1000);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (typeof timestamp === 'string') {
|
|
31
|
+
// Try parsing as number first (string representation of Unix timestamp)
|
|
32
|
+
const numericTimestamp = Number(timestamp);
|
|
33
|
+
if (!isNaN(numericTimestamp)) {
|
|
34
|
+
if (numericTimestamp > 1e12) {
|
|
35
|
+
date = new Date(numericTimestamp);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
date = new Date(numericTimestamp * 1000);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Try parsing as ISO string or other date format
|
|
43
|
+
date = new Date(timestamp);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
// Check if date is valid
|
|
50
|
+
if (isNaN(date.getTime())) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return date.toISOString();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pool consecutive anomalies together to reduce output size
|
|
57
|
+
* Anomalies are considered consecutive if their indices differ by 1
|
|
58
|
+
*/
|
|
59
|
+
function poolAnomalies(anomalies) {
|
|
60
|
+
if (anomalies.length === 0) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
// Sort anomalies by index to ensure proper grouping
|
|
64
|
+
const sorted = [...anomalies].sort((a, b) => a.index - b.index);
|
|
65
|
+
const pools = [];
|
|
66
|
+
let currentPool = [sorted[0]];
|
|
67
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
68
|
+
const current = sorted[i];
|
|
69
|
+
const previous = sorted[i - 1];
|
|
70
|
+
// Check if consecutive (index differs by 1)
|
|
71
|
+
if (current.index === previous.index + 1) {
|
|
72
|
+
currentPool.push(current);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Finalize current pool and start new one
|
|
76
|
+
pools.push(createPooledAnomaly(currentPool));
|
|
77
|
+
currentPool = [current];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Don't forget the last pool
|
|
81
|
+
pools.push(createPooledAnomaly(currentPool));
|
|
82
|
+
return pools;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a PooledAnomaly from a group of consecutive Anomaly objects
|
|
86
|
+
*/
|
|
87
|
+
function createPooledAnomaly(anomalies) {
|
|
88
|
+
const first = anomalies[0];
|
|
89
|
+
const last = anomalies[anomalies.length - 1];
|
|
90
|
+
const values = anomalies.map((a) => a.value);
|
|
91
|
+
const minValue = Math.min(...values);
|
|
92
|
+
const maxValue = Math.max(...values);
|
|
93
|
+
const avgValue = values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
94
|
+
// Collect all crossed thresholds (union)
|
|
95
|
+
const allThresholds = new Set();
|
|
96
|
+
for (const a of anomalies) {
|
|
97
|
+
if (a.crossed_thresholds) {
|
|
98
|
+
for (const t of a.crossed_thresholds) {
|
|
99
|
+
allThresholds.add(t);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Generate summary reason
|
|
104
|
+
let reason;
|
|
105
|
+
if (anomalies.length === 1) {
|
|
106
|
+
reason = first.reason;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Extract the core reason pattern from the first anomaly
|
|
110
|
+
const baseReason = first.reason;
|
|
111
|
+
if (allThresholds.size > 0) {
|
|
112
|
+
reason = `${anomalies.length} consecutive points crossing threshold(s): ${Array.from(allThresholds).join(', ')}`;
|
|
113
|
+
}
|
|
114
|
+
else if (baseReason.includes('percentile')) {
|
|
115
|
+
reason = `${anomalies.length} consecutive points outside rolling quantile bounds`;
|
|
116
|
+
}
|
|
117
|
+
else if (baseReason.includes('Modified Z-score')) {
|
|
118
|
+
reason = `${anomalies.length} consecutive points with high MAD deviation`;
|
|
119
|
+
}
|
|
120
|
+
else if (baseReason.includes('moving average')) {
|
|
121
|
+
reason = `${anomalies.length} consecutive points deviating from moving average`;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
reason = `${anomalies.length} consecutive anomalous points`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const pooled = {
|
|
128
|
+
start_index: first.index,
|
|
129
|
+
end_index: last.index,
|
|
130
|
+
count: anomalies.length,
|
|
131
|
+
min_value: minValue,
|
|
132
|
+
max_value: maxValue,
|
|
133
|
+
avg_value: Math.round(avgValue * 100) / 100, // Round to 2 decimal places
|
|
134
|
+
reason,
|
|
135
|
+
};
|
|
136
|
+
// Add timestamps if available
|
|
137
|
+
if (first.timestamp !== undefined) {
|
|
138
|
+
pooled.start_timestamp = first.timestamp;
|
|
139
|
+
const startUtc = toUtcTime(first.timestamp);
|
|
140
|
+
if (startUtc) {
|
|
141
|
+
pooled.start_utc = startUtc;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (last.timestamp !== undefined) {
|
|
145
|
+
pooled.end_timestamp = last.timestamp;
|
|
146
|
+
const endUtc = toUtcTime(last.timestamp);
|
|
147
|
+
if (endUtc) {
|
|
148
|
+
pooled.end_utc = endUtc;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Add crossed thresholds if any
|
|
152
|
+
if (allThresholds.size > 0) {
|
|
153
|
+
pooled.crossed_thresholds = Array.from(allThresholds);
|
|
154
|
+
}
|
|
155
|
+
return pooled;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Validate that the jq output is tabular (array of objects)
|
|
159
|
+
*/
|
|
160
|
+
function validateTabularData(data) {
|
|
161
|
+
if (!Array.isArray(data)) {
|
|
162
|
+
throw new Error(`jq query must return an array, but got ${typeof data}. ` +
|
|
163
|
+
'Wrap your query in brackets if needed: [.[] | ...]');
|
|
164
|
+
}
|
|
165
|
+
if (data.length === 0) {
|
|
166
|
+
throw new Error('jq query returned an empty array. No data to analyze.');
|
|
167
|
+
}
|
|
168
|
+
// Check first few items are objects
|
|
169
|
+
const sampleSize = Math.min(5, data.length);
|
|
170
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
171
|
+
if (typeof data[i] !== 'object' || data[i] === null || Array.isArray(data[i])) {
|
|
172
|
+
throw new Error(`jq query must return an array of objects, but item ${i} is ${typeof data[i]}. ` +
|
|
173
|
+
'Each item should be an object with fields.');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Extract data points from tabular data
|
|
179
|
+
*/
|
|
180
|
+
function extractDataPoints(data, valueField, timestampField) {
|
|
181
|
+
const points = [];
|
|
182
|
+
for (let i = 0; i < data.length; i++) {
|
|
183
|
+
const item = data[i];
|
|
184
|
+
const rawValue = item[valueField];
|
|
185
|
+
// Skip items without the value field or with null values
|
|
186
|
+
if (rawValue === undefined || rawValue === null) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Convert to number
|
|
190
|
+
const value = Number(rawValue);
|
|
191
|
+
if (isNaN(value)) {
|
|
192
|
+
throw new Error(`Value at index ${i} is not a valid number: ${JSON.stringify(rawValue)}. ` +
|
|
193
|
+
`Field "${valueField}" must contain numeric values.`);
|
|
194
|
+
}
|
|
195
|
+
const point = {
|
|
196
|
+
index: i,
|
|
197
|
+
value,
|
|
198
|
+
};
|
|
199
|
+
if (timestampField && item[timestampField] !== undefined) {
|
|
200
|
+
const ts = item[timestampField];
|
|
201
|
+
point.timestamp = typeof ts === 'string' || typeof ts === 'number' ? ts : String(ts);
|
|
202
|
+
}
|
|
203
|
+
points.push(point);
|
|
204
|
+
}
|
|
205
|
+
if (points.length === 0) {
|
|
206
|
+
throw new Error(`No valid numeric values found in field "${valueField}". ` +
|
|
207
|
+
'Check that the field exists and contains numbers.');
|
|
208
|
+
}
|
|
209
|
+
return points;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Run the appropriate detection algorithm
|
|
213
|
+
*/
|
|
214
|
+
function runDetection(data, detectionType, params) {
|
|
215
|
+
let rawAnomalies;
|
|
216
|
+
switch (detectionType) {
|
|
217
|
+
case 'rolling_quantile':
|
|
218
|
+
if (!params || !('window_size' in params)) {
|
|
219
|
+
throw new Error('rolling_quantile detection requires window_size parameter');
|
|
220
|
+
}
|
|
221
|
+
rawAnomalies = detectRollingQuantileAnomalies(data, params);
|
|
222
|
+
break;
|
|
223
|
+
case 'mad':
|
|
224
|
+
rawAnomalies = detectMadAnomalies(data, params);
|
|
225
|
+
break;
|
|
226
|
+
case 'moving_average':
|
|
227
|
+
if (!params || !('window_size' in params)) {
|
|
228
|
+
throw new Error('moving_average detection requires window_size parameter');
|
|
229
|
+
}
|
|
230
|
+
rawAnomalies = detectMovingAverageAnomalies(data, params);
|
|
231
|
+
break;
|
|
232
|
+
case 'threshold':
|
|
233
|
+
if (!params || !('thresholds' in params)) {
|
|
234
|
+
throw new Error('threshold detection requires thresholds parameter');
|
|
235
|
+
}
|
|
236
|
+
rawAnomalies = detectThresholdAnomalies(data, params);
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
throw new Error(`Unknown detection type: ${detectionType}`);
|
|
240
|
+
}
|
|
241
|
+
// Pool consecutive anomalies together
|
|
242
|
+
const pooledAnomalies = poolAnomalies(rawAnomalies);
|
|
243
|
+
return {
|
|
244
|
+
total_points: data.length,
|
|
245
|
+
anomaly_count: rawAnomalies.length,
|
|
246
|
+
anomaly_percentage: Math.round((rawAnomalies.length / data.length) * 10000) / 100, // Round to 2 decimal places
|
|
247
|
+
pool_count: pooledAnomalies.length,
|
|
248
|
+
anomalies: pooledAnomalies,
|
|
249
|
+
detection_type: detectionType,
|
|
250
|
+
parameters: params || {},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Main handler for time series anomaly detection
|
|
255
|
+
*/
|
|
256
|
+
export async function detectTimeseriesAnomalies(config, filePath, jqQuery, valueField, detectionType, detectionParams, timestampField) {
|
|
257
|
+
// Input validation
|
|
258
|
+
if (!filePath || !jqQuery || !valueField || !detectionType) {
|
|
259
|
+
throw new Error('file_path, jq_query, value_field, and detection_type are required');
|
|
260
|
+
}
|
|
261
|
+
// Sanitize jq query
|
|
262
|
+
validateJqQuery(jqQuery);
|
|
263
|
+
// Validate file path
|
|
264
|
+
if (!path.isAbsolute(filePath)) {
|
|
265
|
+
throw new Error(`File path must be an absolute path starting with "/": ${filePath}`);
|
|
266
|
+
}
|
|
267
|
+
if (!existsSync(filePath)) {
|
|
268
|
+
throw new Error(`File not found: ${filePath}`);
|
|
269
|
+
}
|
|
270
|
+
if (!filePath.toLowerCase().endsWith('.json')) {
|
|
271
|
+
throw new Error(`Only JSON files (.json) are supported: ${filePath}`);
|
|
272
|
+
}
|
|
273
|
+
// Validate path is within allowed directories
|
|
274
|
+
validatePathWithinAllowedDirs(filePath, config.allowedPaths);
|
|
275
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
276
|
+
// Execute jq query
|
|
277
|
+
const rawData = await runJqParsed(jqQuery, filePath, timeoutMs);
|
|
278
|
+
// Validate tabular structure
|
|
279
|
+
validateTabularData(rawData);
|
|
280
|
+
// Extract data points
|
|
281
|
+
const dataPoints = extractDataPoints(rawData, valueField, timestampField);
|
|
282
|
+
// Run detection
|
|
283
|
+
const result = runDetection(dataPoints, detectionType, detectionParams);
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: 'text',
|
|
288
|
+
text: JSON.stringify(result, null, 2),
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { TimeseriesConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a timeseries anomaly detection tool instance with the given configuration
|
|
4
|
+
* @param config - Timeseries configuration
|
|
5
|
+
* @returns Object with handler and toolDefinition
|
|
6
|
+
*/
|
|
7
|
+
export declare function createTimeseriesTool(config: TimeseriesConfig): {
|
|
8
|
+
/**
|
|
9
|
+
* Tool definition for MCP server registration
|
|
10
|
+
*/
|
|
11
|
+
toolDefinition: {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: string;
|
|
16
|
+
properties: {
|
|
17
|
+
file_path: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
jq_query: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
timestamp_field: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
value_field: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
detection_type: {
|
|
34
|
+
type: string;
|
|
35
|
+
enum: string[];
|
|
36
|
+
description: string;
|
|
37
|
+
};
|
|
38
|
+
detection_params: {
|
|
39
|
+
type: string;
|
|
40
|
+
description: string;
|
|
41
|
+
};
|
|
42
|
+
description: {
|
|
43
|
+
type: string;
|
|
44
|
+
description: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
required: string[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Handler for timeseries anomaly detection requests
|
|
52
|
+
* @param request - MCP request containing parameters
|
|
53
|
+
* @returns Promise with the detection result
|
|
54
|
+
*/
|
|
55
|
+
handler: (request: {
|
|
56
|
+
params: {
|
|
57
|
+
arguments: Record<string, unknown>;
|
|
58
|
+
};
|
|
59
|
+
}) => Promise<{
|
|
60
|
+
content: Array<{
|
|
61
|
+
type: "text";
|
|
62
|
+
text: string;
|
|
63
|
+
}>;
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
66
|
+
export type { TimeseriesConfig, DataPoint, Anomaly, AnomalyDetectionResult, DetectionType, DetectionParams, NamedThreshold, ThresholdDirection, } from './types.js';
|
|
67
|
+
export { TimeseriesAnomalySchema, TIMESERIES_ANOMALY_TOOL_DEFINITION } from './tool.js';
|
|
68
|
+
export { detectTimeseriesAnomalies } from './handler.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { detectTimeseriesAnomalies } from './handler.js';
|
|
2
|
+
import { TimeseriesAnomalySchema, TIMESERIES_ANOMALY_TOOL_DEFINITION } from './tool.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create a timeseries anomaly detection tool instance with the given configuration
|
|
5
|
+
* @param config - Timeseries configuration
|
|
6
|
+
* @returns Object with handler and toolDefinition
|
|
7
|
+
*/
|
|
8
|
+
export function createTimeseriesTool(config) {
|
|
9
|
+
return {
|
|
10
|
+
/**
|
|
11
|
+
* Tool definition for MCP server registration
|
|
12
|
+
*/
|
|
13
|
+
toolDefinition: TIMESERIES_ANOMALY_TOOL_DEFINITION,
|
|
14
|
+
/**
|
|
15
|
+
* Handler for timeseries anomaly detection requests
|
|
16
|
+
* @param request - MCP request containing parameters
|
|
17
|
+
* @returns Promise with the detection result
|
|
18
|
+
*/
|
|
19
|
+
handler: async (request) => {
|
|
20
|
+
const parsed = TimeseriesAnomalySchema.parse(request.params.arguments);
|
|
21
|
+
return detectTimeseriesAnomalies(config, parsed.file_path, parsed.jq_query, parsed.value_field, parsed.detection_type, parsed.detection_params, parsed.timestamp_field);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export { TimeseriesAnomalySchema, TIMESERIES_ANOMALY_TOOL_DEFINITION } from './tool.js';
|
|
26
|
+
export { detectTimeseriesAnomalies } from './handler.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for time series anomaly detection
|
|
4
|
+
*/
|
|
5
|
+
export declare const TimeseriesAnomalySchema: z.ZodObject<{
|
|
6
|
+
file_path: z.ZodString;
|
|
7
|
+
jq_query: z.ZodString;
|
|
8
|
+
timestamp_field: z.ZodOptional<z.ZodString>;
|
|
9
|
+
value_field: z.ZodString;
|
|
10
|
+
detection_type: z.ZodEnum<["rolling_quantile", "mad", "moving_average", "threshold"]>;
|
|
11
|
+
detection_params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
12
|
+
description: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
jq_query: string;
|
|
15
|
+
file_path: string;
|
|
16
|
+
value_field: string;
|
|
17
|
+
detection_type: "rolling_quantile" | "mad" | "moving_average" | "threshold";
|
|
18
|
+
timestamp_field?: string | undefined;
|
|
19
|
+
detection_params?: Record<string, unknown> | undefined;
|
|
20
|
+
description?: string | undefined;
|
|
21
|
+
}, {
|
|
22
|
+
jq_query: string;
|
|
23
|
+
file_path: string;
|
|
24
|
+
value_field: string;
|
|
25
|
+
detection_type: "rolling_quantile" | "mad" | "moving_average" | "threshold";
|
|
26
|
+
timestamp_field?: string | undefined;
|
|
27
|
+
detection_params?: Record<string, unknown> | undefined;
|
|
28
|
+
description?: string | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Tool definition for time series anomaly detection with comprehensive documentation
|
|
32
|
+
*/
|
|
33
|
+
export declare const TIMESERIES_ANOMALY_TOOL_DEFINITION: {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: string;
|
|
38
|
+
properties: {
|
|
39
|
+
file_path: {
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
jq_query: {
|
|
44
|
+
type: string;
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
timestamp_field: {
|
|
48
|
+
type: string;
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
value_field: {
|
|
52
|
+
type: string;
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
detection_type: {
|
|
56
|
+
type: string;
|
|
57
|
+
enum: string[];
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
60
|
+
detection_params: {
|
|
61
|
+
type: string;
|
|
62
|
+
description: string;
|
|
63
|
+
};
|
|
64
|
+
description: {
|
|
65
|
+
type: string;
|
|
66
|
+
description: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
required: string[];
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for a named threshold
|
|
4
|
+
*/
|
|
5
|
+
const NamedThresholdSchema = z.object({
|
|
6
|
+
name: z.string().describe('Name/label for this threshold (e.g., "critical", "warning", "min_healthy")'),
|
|
7
|
+
value: z.number().describe('The threshold value'),
|
|
8
|
+
direction: z.enum(['upper', 'lower']).describe('Whether values should be below (upper bound) or above (lower bound) this threshold'),
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Zod schema for time series anomaly detection
|
|
12
|
+
*/
|
|
13
|
+
export const TimeseriesAnomalySchema = z.object({
|
|
14
|
+
file_path: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('Absolute path starting with "/" pointing to the JSON file to process. Must be a valid, existing file with .json extension.'),
|
|
17
|
+
jq_query: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe('JQ query to extract tabular data from the JSON file. The query must return an array of objects with consistent fields. Use PLAIN quotes like .["field"] - DO NOT escape them.'),
|
|
20
|
+
timestamp_field: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Optional field name in the extracted data to use as timestamp. If provided, timestamps will be included in anomaly results.'),
|
|
24
|
+
value_field: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe('Field name in the extracted data containing the numeric values to analyze for anomalies.'),
|
|
27
|
+
detection_type: z
|
|
28
|
+
.enum(['rolling_quantile', 'mad', 'moving_average', 'threshold'])
|
|
29
|
+
.describe('The anomaly detection algorithm to use. See algorithm descriptions below.'),
|
|
30
|
+
detection_params: z
|
|
31
|
+
.record(z.unknown())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Algorithm-specific parameters. See parameter descriptions for each detection_type.'),
|
|
34
|
+
description: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Brief explanation of why you are calling this tool and what you expect to learn/achieve.'),
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Tool definition for time series anomaly detection with comprehensive documentation
|
|
41
|
+
*/
|
|
42
|
+
export const TIMESERIES_ANOMALY_TOOL_DEFINITION = {
|
|
43
|
+
name: 'detect_timeseries_anomalies',
|
|
44
|
+
description: 'Detect anomalies in time series data extracted from JSON files using jq queries. ' +
|
|
45
|
+
'This tool combines jq-based data extraction with statistical anomaly detection algorithms.' +
|
|
46
|
+
'\n\n## IMPORTANT: This tool has jq built-in. Do NOT prepare or transform data separately.' +
|
|
47
|
+
'\n- NO separate "transform data" step needed' +
|
|
48
|
+
'\n- NO calling execute_jq_query first' +
|
|
49
|
+
'\n- The jq_query parameter handles ALL data extraction and transformation in ONE call' +
|
|
50
|
+
'\n\n## WHAT THIS TOOL DOES (automatically, in a single call):' +
|
|
51
|
+
'\n1. Reads the JSON file' +
|
|
52
|
+
'\n2. Applies the jq query to extract tabular data (array of objects)' +
|
|
53
|
+
'\n3. Extracts numeric values from the specified value_field' +
|
|
54
|
+
'\n4. Runs the selected anomaly detection algorithm' +
|
|
55
|
+
'\n5. Returns detected anomalies with indices, values, and reasons' +
|
|
56
|
+
'\n\n## DATA REQUIREMENTS:' +
|
|
57
|
+
'\n- The jq_query MUST return an array of objects: [{...}, {...}, ...]' +
|
|
58
|
+
'\n- Each object MUST have the field specified in value_field' +
|
|
59
|
+
'\n- The value_field MUST contain numeric values' +
|
|
60
|
+
'\n- If timestamp_field is provided, it will be included in results' +
|
|
61
|
+
'\n\n## DETECTION ALGORITHMS:' +
|
|
62
|
+
'\n\n### rolling_quantile (RECOMMENDED - similar to Datadog Basic)' +
|
|
63
|
+
'\nUses a sliding window to compute rolling percentiles, flags points outside the expected range.' +
|
|
64
|
+
'\n- Best for: General purpose, works with any data distribution' +
|
|
65
|
+
'\n- Parameters: { "window_size": 10, "lower_quantile": 0.05, "upper_quantile": 0.95 }' +
|
|
66
|
+
'\n- REQUIRED: window_size (number of points in rolling window)' +
|
|
67
|
+
'\n- lower_quantile: percentile for lower bound (default: 0.05 = 5th percentile)' +
|
|
68
|
+
'\n- upper_quantile: percentile for upper bound (default: 0.95 = 95th percentile)' +
|
|
69
|
+
'\n- Non-parametric: does not assume normal distribution' +
|
|
70
|
+
'\n\n### mad (Median Absolute Deviation)' +
|
|
71
|
+
'\nUses modified Z-scores based on median instead of mean.' +
|
|
72
|
+
'\n- Best for: Global outlier detection across all data, robust to existing outliers' +
|
|
73
|
+
'\n- Parameters: { "threshold": 3 } (default: 3)' +
|
|
74
|
+
'\n- Uses median-based statistics for maximum robustness' +
|
|
75
|
+
'\n- Good when you want to find outliers relative to the entire dataset' +
|
|
76
|
+
'\n\n### moving_average' +
|
|
77
|
+
'\nDetects points that deviate from a rolling average.' +
|
|
78
|
+
'\n- Best for: Time series with trends, detecting sudden changes' +
|
|
79
|
+
'\n- Parameters: { "window_size": 10, "threshold": 2 }' +
|
|
80
|
+
'\n- REQUIRED: window_size (number of points in rolling window)' +
|
|
81
|
+
'\n- threshold: std devs from moving average (default: 2)' +
|
|
82
|
+
'\n- Assumes roughly normal distribution within each window' +
|
|
83
|
+
'\n\n### threshold' +
|
|
84
|
+
'\nDetects points crossing named upper/lower thresholds.' +
|
|
85
|
+
'\n- Best for: SLA monitoring, known limits' +
|
|
86
|
+
'\n- Parameters: { "thresholds": [...] }' +
|
|
87
|
+
'\n- REQUIRED: thresholds array with named thresholds' +
|
|
88
|
+
'\n- Each threshold: { "name": "critical", "value": 100, "direction": "upper" }' +
|
|
89
|
+
'\n- direction: "upper" = value must be below, "lower" = value must be above' +
|
|
90
|
+
'\n\n## JQ QUERY TIPS:' +
|
|
91
|
+
'\n- Use PLAIN quotes: .["field"] NOT .[\"field\"]' +
|
|
92
|
+
'\n- Array of metrics: .metrics' +
|
|
93
|
+
'\n- Nested data: .data.timeseries' +
|
|
94
|
+
'\n- Filter: .items[] | select(.type == "cpu") | {ts: .timestamp, val: .value}' +
|
|
95
|
+
'\n- Transform: [.[] | {timestamp: .ts, value: .cpu_usage}]' +
|
|
96
|
+
'\n\n## EXAMPLE CALLS:' +
|
|
97
|
+
'\n\n**Rolling Quantile detection (RECOMMENDED):**' +
|
|
98
|
+
'\n```json' +
|
|
99
|
+
'\n{' +
|
|
100
|
+
'\n "file_path": "/tmp/metrics.json",' +
|
|
101
|
+
'\n "jq_query": ".cpu_metrics",' +
|
|
102
|
+
'\n "value_field": "usage",' +
|
|
103
|
+
'\n "timestamp_field": "timestamp",' +
|
|
104
|
+
'\n "detection_type": "rolling_quantile",' +
|
|
105
|
+
'\n "detection_params": { "window_size": 10, "lower_quantile": 0.05, "upper_quantile": 0.95 }' +
|
|
106
|
+
'\n}' +
|
|
107
|
+
'\n```' +
|
|
108
|
+
'\n\n**Threshold detection with SLA limits:**' +
|
|
109
|
+
'\n```json' +
|
|
110
|
+
'\n{' +
|
|
111
|
+
'\n "file_path": "/tmp/response_times.json",' +
|
|
112
|
+
'\n "jq_query": ".api_latencies",' +
|
|
113
|
+
'\n "value_field": "latency_ms",' +
|
|
114
|
+
'\n "detection_type": "threshold",' +
|
|
115
|
+
'\n "detection_params": {' +
|
|
116
|
+
'\n "thresholds": [' +
|
|
117
|
+
'\n { "name": "warning", "value": 200, "direction": "upper" },' +
|
|
118
|
+
'\n { "name": "critical", "value": 500, "direction": "upper" },' +
|
|
119
|
+
'\n { "name": "too_fast", "value": 1, "direction": "lower" }' +
|
|
120
|
+
'\n ]' +
|
|
121
|
+
'\n }' +
|
|
122
|
+
'\n}' +
|
|
123
|
+
'\n```' +
|
|
124
|
+
'\n\n**MAD for global outlier detection:**' +
|
|
125
|
+
'\n```json' +
|
|
126
|
+
'\n{' +
|
|
127
|
+
'\n "file_path": "/tmp/sales.json",' +
|
|
128
|
+
'\n "jq_query": "[.daily_sales[] | {date: .date, amount: .total}]",' +
|
|
129
|
+
'\n "value_field": "amount",' +
|
|
130
|
+
'\n "timestamp_field": "date",' +
|
|
131
|
+
'\n "detection_type": "mad",' +
|
|
132
|
+
'\n "detection_params": { "threshold": 3 }' +
|
|
133
|
+
'\n}' +
|
|
134
|
+
'\n```',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
file_path: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Absolute path starting with "/" pointing to the JSON file to process. Must be a valid, existing file with .json extension.',
|
|
141
|
+
},
|
|
142
|
+
jq_query: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'JQ query to extract tabular data from the JSON file. The query must return an array of objects with consistent fields. Use PLAIN quotes like .["field"] - DO NOT escape them.',
|
|
145
|
+
},
|
|
146
|
+
timestamp_field: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'Optional field name in the extracted data to use as timestamp. If provided, timestamps will be included in anomaly results.',
|
|
149
|
+
},
|
|
150
|
+
value_field: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'Field name in the extracted data containing the numeric values to analyze for anomalies.',
|
|
153
|
+
},
|
|
154
|
+
detection_type: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
enum: ['rolling_quantile', 'mad', 'moving_average', 'threshold'],
|
|
157
|
+
description: 'The anomaly detection algorithm to use: rolling_quantile (recommended), mad, moving_average, or threshold.',
|
|
158
|
+
},
|
|
159
|
+
detection_params: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
description: 'Algorithm-specific parameters. For rolling_quantile: {window_size: number, lower_quantile?: number, upper_quantile?: number}. For mad: {threshold: number}. For moving_average: {window_size: number, threshold?: number}. For threshold: {thresholds: [{name, value, direction}]}.',
|
|
162
|
+
},
|
|
163
|
+
description: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
description: 'Brief explanation of why you are calling this tool and what you expect to learn/achieve.',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
required: ['file_path', 'jq_query', 'value_field', 'detection_type'],
|
|
169
|
+
},
|
|
170
|
+
};
|