@anyshift/mcp-proxy 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/__tests__/unit/timeseries.test.d.ts +1 -0
  2. package/dist/__tests__/unit/timeseries.test.js +217 -0
  3. package/dist/fileWriter/writer.js +1 -1
  4. package/dist/index.js +75 -29
  5. package/dist/jq/handler.js +11 -56
  6. package/dist/jq/tool.d.ts +3 -9
  7. package/dist/jq/tool.js +4 -4
  8. package/dist/timeseries/algorithms/index.d.ts +8 -0
  9. package/dist/timeseries/algorithms/index.js +8 -0
  10. package/dist/timeseries/algorithms/mad.d.ts +15 -0
  11. package/dist/timeseries/algorithms/mad.js +44 -0
  12. package/dist/timeseries/algorithms/moving-average.d.ts +15 -0
  13. package/dist/timeseries/algorithms/moving-average.js +72 -0
  14. package/dist/timeseries/algorithms/rolling-quantile.d.ts +16 -0
  15. package/dist/timeseries/algorithms/rolling-quantile.js +78 -0
  16. package/dist/timeseries/algorithms/stats.d.ts +49 -0
  17. package/dist/timeseries/algorithms/stats.js +139 -0
  18. package/dist/timeseries/algorithms/threshold.d.ts +15 -0
  19. package/dist/timeseries/algorithms/threshold.js +49 -0
  20. package/dist/timeseries/handler.d.ts +10 -0
  21. package/dist/timeseries/handler.js +292 -0
  22. package/dist/timeseries/index.d.ts +68 -0
  23. package/dist/timeseries/index.js +26 -0
  24. package/dist/timeseries/tool.d.ts +71 -0
  25. package/dist/timeseries/tool.js +170 -0
  26. package/dist/timeseries/types.d.ts +147 -0
  27. package/dist/timeseries/types.js +4 -0
  28. package/dist/types/index.d.ts +0 -21
  29. package/dist/utils/filename.d.ts +0 -8
  30. package/dist/utils/filename.js +0 -10
  31. package/dist/utils/jq.d.ts +25 -0
  32. package/dist/utils/jq.js +90 -0
  33. package/package.json +1 -1
@@ -0,0 +1,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
+ };