@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.
@@ -0,0 +1,78 @@
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 { rollingQuantiles } from './stats.js';
9
+ const DEFAULT_LOWER_QUANTILE = 0.05;
10
+ const DEFAULT_UPPER_QUANTILE = 0.95;
11
+ /**
12
+ * Detect anomalies using Rolling Quantile method
13
+ *
14
+ * @param data - Array of data points (should be in time order)
15
+ * @param params - Rolling Quantile parameters
16
+ * @returns Array of detected anomalies
17
+ */
18
+ export function detectRollingQuantileAnomalies(data, params) {
19
+ const { window_size: windowSize, lower_quantile: lowerQuantile = DEFAULT_LOWER_QUANTILE, upper_quantile: upperQuantile = DEFAULT_UPPER_QUANTILE, } = params;
20
+ if (windowSize <= 0) {
21
+ throw new Error('window_size must be greater than 0');
22
+ }
23
+ if (windowSize > data.length) {
24
+ throw new Error(`window_size (${windowSize}) cannot be larger than data length (${data.length})`);
25
+ }
26
+ if (lowerQuantile < 0 || lowerQuantile > 1) {
27
+ throw new Error('lower_quantile must be between 0 and 1');
28
+ }
29
+ if (upperQuantile < 0 || upperQuantile > 1) {
30
+ throw new Error('upper_quantile must be between 0 and 1');
31
+ }
32
+ if (lowerQuantile >= upperQuantile) {
33
+ throw new Error('lower_quantile must be less than upper_quantile');
34
+ }
35
+ const values = data.map((d) => d.value);
36
+ const { lower, upper } = rollingQuantiles(values, windowSize, lowerQuantile, upperQuantile);
37
+ const anomalies = [];
38
+ for (let i = 0; i < data.length; i++) {
39
+ // Skip points where we don't have enough data for the window
40
+ if (isNaN(lower[i]) || isNaN(upper[i])) {
41
+ continue;
42
+ }
43
+ const point = data[i];
44
+ const lowerBound = lower[i];
45
+ const upperBound = upper[i];
46
+ if (point.value < lowerBound) {
47
+ anomalies.push({
48
+ index: point.index,
49
+ timestamp: point.timestamp,
50
+ value: point.value,
51
+ reason: `Value ${point.value.toFixed(2)} below rolling ${(lowerQuantile * 100).toFixed(0)}th percentile ${lowerBound.toFixed(2)}`,
52
+ metadata: {
53
+ lower_bound: lowerBound,
54
+ upper_bound: upperBound,
55
+ lower_quantile: lowerQuantile,
56
+ upper_quantile: upperQuantile,
57
+ window_size: windowSize,
58
+ },
59
+ });
60
+ }
61
+ else if (point.value > upperBound) {
62
+ anomalies.push({
63
+ index: point.index,
64
+ timestamp: point.timestamp,
65
+ value: point.value,
66
+ reason: `Value ${point.value.toFixed(2)} above rolling ${(upperQuantile * 100).toFixed(0)}th percentile ${upperBound.toFixed(2)}`,
67
+ metadata: {
68
+ lower_bound: lowerBound,
69
+ upper_bound: upperBound,
70
+ lower_quantile: lowerQuantile,
71
+ upper_quantile: upperQuantile,
72
+ window_size: windowSize,
73
+ },
74
+ });
75
+ }
76
+ }
77
+ return anomalies;
78
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Statistical utility functions for anomaly detection
3
+ * Implemented in pure TypeScript for zero dependencies and fast execution
4
+ */
5
+ /**
6
+ * Calculate the mean (average) of an array of numbers
7
+ */
8
+ export declare function mean(values: number[]): number;
9
+ /**
10
+ * Calculate the median of an array of numbers
11
+ */
12
+ export declare function median(values: number[]): number;
13
+ /**
14
+ * Calculate the standard deviation of an array of numbers
15
+ */
16
+ export declare function standardDeviation(values: number[]): number;
17
+ /**
18
+ * Calculate the Median Absolute Deviation (MAD)
19
+ * MAD = median(|Xi - median(X)|)
20
+ */
21
+ export declare function medianAbsoluteDeviation(values: number[]): number;
22
+ /**
23
+ * Calculate modified Z-score using MAD
24
+ * Modified Z-score = 0.6745 * (Xi - median) / MAD
25
+ * The constant 0.6745 makes it comparable to standard Z-scores for normal distributions
26
+ */
27
+ export declare function modifiedZScore(value: number, med: number, mad: number): number;
28
+ /**
29
+ * Calculate a simple moving average for an array at each position
30
+ * Returns an array of the same length, with NaN for positions where window is incomplete
31
+ */
32
+ export declare function movingAverage(values: number[], windowSize: number): number[];
33
+ /**
34
+ * Calculate the standard deviation of a moving window at each position
35
+ */
36
+ export declare function movingStandardDeviation(values: number[], windowSize: number): number[];
37
+ /**
38
+ * Calculate a specific quantile (percentile) of an array
39
+ * Uses linear interpolation between closest ranks
40
+ */
41
+ export declare function quantile(values: number[], q: number): number;
42
+ /**
43
+ * Calculate rolling quantiles (lower and upper bounds) at each position
44
+ * Returns arrays of the same length as input, with NaN for positions where window is incomplete
45
+ */
46
+ export declare function rollingQuantiles(values: number[], windowSize: number, lowerQuantile: number, upperQuantile: number): {
47
+ lower: number[];
48
+ upper: number[];
49
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Statistical utility functions for anomaly detection
3
+ * Implemented in pure TypeScript for zero dependencies and fast execution
4
+ */
5
+ /**
6
+ * Calculate the mean (average) of an array of numbers
7
+ */
8
+ export function mean(values) {
9
+ if (values.length === 0)
10
+ return 0;
11
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
12
+ }
13
+ /**
14
+ * Calculate the median of an array of numbers
15
+ */
16
+ export function median(values) {
17
+ if (values.length === 0)
18
+ return 0;
19
+ const sorted = [...values].sort((a, b) => a - b);
20
+ const mid = Math.floor(sorted.length / 2);
21
+ return sorted.length % 2 !== 0
22
+ ? sorted[mid]
23
+ : (sorted[mid - 1] + sorted[mid]) / 2;
24
+ }
25
+ /**
26
+ * Calculate the standard deviation of an array of numbers
27
+ */
28
+ export function standardDeviation(values) {
29
+ if (values.length < 2)
30
+ return 0;
31
+ const avg = mean(values);
32
+ const squaredDiffs = values.map((v) => Math.pow(v - avg, 2));
33
+ return Math.sqrt(mean(squaredDiffs));
34
+ }
35
+ /**
36
+ * Calculate the Median Absolute Deviation (MAD)
37
+ * MAD = median(|Xi - median(X)|)
38
+ */
39
+ export function medianAbsoluteDeviation(values) {
40
+ if (values.length === 0)
41
+ return 0;
42
+ const med = median(values);
43
+ const absoluteDeviations = values.map((v) => Math.abs(v - med));
44
+ return median(absoluteDeviations);
45
+ }
46
+ /**
47
+ * Calculate modified Z-score using MAD
48
+ * Modified Z-score = 0.6745 * (Xi - median) / MAD
49
+ * The constant 0.6745 makes it comparable to standard Z-scores for normal distributions
50
+ */
51
+ export function modifiedZScore(value, med, mad) {
52
+ if (mad === 0)
53
+ return 0;
54
+ return (0.6745 * (value - med)) / mad;
55
+ }
56
+ /**
57
+ * Calculate a simple moving average for an array at each position
58
+ * Returns an array of the same length, with NaN for positions where window is incomplete
59
+ */
60
+ export function movingAverage(values, windowSize) {
61
+ if (windowSize <= 0 || values.length === 0)
62
+ return [];
63
+ const result = [];
64
+ for (let i = 0; i < values.length; i++) {
65
+ if (i < windowSize - 1) {
66
+ // Not enough data points yet
67
+ result.push(NaN);
68
+ }
69
+ else {
70
+ // Calculate average of the window
71
+ const window = values.slice(i - windowSize + 1, i + 1);
72
+ result.push(mean(window));
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+ /**
78
+ * Calculate the standard deviation of a moving window at each position
79
+ */
80
+ export function movingStandardDeviation(values, windowSize) {
81
+ if (windowSize <= 0 || values.length === 0)
82
+ return [];
83
+ const result = [];
84
+ for (let i = 0; i < values.length; i++) {
85
+ if (i < windowSize - 1) {
86
+ result.push(NaN);
87
+ }
88
+ else {
89
+ const window = values.slice(i - windowSize + 1, i + 1);
90
+ result.push(standardDeviation(window));
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ /**
96
+ * Calculate a specific quantile (percentile) of an array
97
+ * Uses linear interpolation between closest ranks
98
+ */
99
+ export function quantile(values, q) {
100
+ if (values.length === 0)
101
+ return 0;
102
+ if (q <= 0)
103
+ return Math.min(...values);
104
+ if (q >= 1)
105
+ return Math.max(...values);
106
+ const sorted = [...values].sort((a, b) => a - b);
107
+ const pos = (sorted.length - 1) * q;
108
+ const lower = Math.floor(pos);
109
+ const upper = Math.ceil(pos);
110
+ const weight = pos - lower;
111
+ if (upper >= sorted.length)
112
+ return sorted[sorted.length - 1];
113
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
114
+ }
115
+ /**
116
+ * Calculate rolling quantiles (lower and upper bounds) at each position
117
+ * Returns arrays of the same length as input, with NaN for positions where window is incomplete
118
+ */
119
+ export function rollingQuantiles(values, windowSize, lowerQuantile, upperQuantile) {
120
+ if (windowSize <= 0 || values.length === 0) {
121
+ return { lower: [], upper: [] };
122
+ }
123
+ const lower = [];
124
+ const upper = [];
125
+ for (let i = 0; i < values.length; i++) {
126
+ if (i < windowSize - 1) {
127
+ // Not enough data points yet
128
+ lower.push(NaN);
129
+ upper.push(NaN);
130
+ }
131
+ else {
132
+ // Calculate quantiles of the window
133
+ const window = values.slice(i - windowSize + 1, i + 1);
134
+ lower.push(quantile(window, lowerQuantile));
135
+ upper.push(quantile(window, upperQuantile));
136
+ }
137
+ }
138
+ return { lower, upper };
139
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Threshold-based anomaly detection
3
+ *
4
+ * Detects points that cross one or more named thresholds.
5
+ * Supports multiple thresholds with names and directions (upper/lower).
6
+ */
7
+ import type { DataPoint, Anomaly, ThresholdParams } from '../types.js';
8
+ /**
9
+ * Detect anomalies using threshold method
10
+ *
11
+ * @param data - Array of data points
12
+ * @param params - Threshold parameters with named thresholds
13
+ * @returns Array of detected anomalies
14
+ */
15
+ export declare function detectThresholdAnomalies(data: DataPoint[], params: ThresholdParams): Anomaly[];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Threshold-based anomaly detection
3
+ *
4
+ * Detects points that cross one or more named thresholds.
5
+ * Supports multiple thresholds with names and directions (upper/lower).
6
+ */
7
+ /**
8
+ * Detect anomalies using threshold method
9
+ *
10
+ * @param data - Array of data points
11
+ * @param params - Threshold parameters with named thresholds
12
+ * @returns Array of detected anomalies
13
+ */
14
+ export function detectThresholdAnomalies(data, params) {
15
+ const { thresholds } = params;
16
+ if (!thresholds || thresholds.length === 0) {
17
+ throw new Error('At least one threshold must be specified');
18
+ }
19
+ const anomalies = [];
20
+ for (const point of data) {
21
+ const crossedThresholds = [];
22
+ for (const threshold of thresholds) {
23
+ if (threshold.direction === 'upper' && point.value > threshold.value) {
24
+ crossedThresholds.push(threshold);
25
+ }
26
+ else if (threshold.direction === 'lower' && point.value < threshold.value) {
27
+ crossedThresholds.push(threshold);
28
+ }
29
+ }
30
+ if (crossedThresholds.length > 0) {
31
+ const thresholdNames = crossedThresholds.map((t) => t.name);
32
+ const thresholdDetails = crossedThresholds
33
+ .map((t) => `${t.name} (${t.direction}: ${t.value})`)
34
+ .join(', ');
35
+ anomalies.push({
36
+ index: point.index,
37
+ timestamp: point.timestamp,
38
+ value: point.value,
39
+ reason: `Value ${point.value} crossed threshold(s): ${thresholdDetails}`,
40
+ crossed_thresholds: thresholdNames,
41
+ metadata: crossedThresholds.reduce((acc, t) => {
42
+ acc[`threshold_${t.name}`] = t.value;
43
+ return acc;
44
+ }, {}),
45
+ });
46
+ }
47
+ }
48
+ return anomalies;
49
+ }
@@ -0,0 +1,10 @@
1
+ import type { TimeseriesConfig, DetectionType, DetectionParams } from './types.js';
2
+ /**
3
+ * Main handler for time series anomaly detection
4
+ */
5
+ export declare function detectTimeseriesAnomalies(config: TimeseriesConfig, filePath: string, jqQuery: string, valueField: string, detectionType: DetectionType, detectionParams?: DetectionParams, timestampField?: string): Promise<{
6
+ content: Array<{
7
+ type: 'text';
8
+ text: string;
9
+ }>;
10
+ }>;
@@ -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
+ }