Skip to content

Commit b0854c7

Browse files
committed
fixed formatting
1 parent 89fd8af commit b0854c7

9 files changed

Lines changed: 120 additions & 60 deletions

File tree

src/index.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,39 @@ server.tool(
121121
'forecast',
122122
'Perform time series forecasting using FAIM platform. Supports both point forecasting (single value) and probabilistic forecasting (confidence intervals). Can handle univariate and multivariate time series data. Currently supported models: Chronos2 (default, recommended for multivariate) and TiRex (fast, univariate only).',
123123
{
124-
model: z.enum(['chronos2', 'tirex']).describe('The forecasting model to use. Chronos2: State-of-the-art, supports univariate/multivariate, custom quantiles. TiRex: Fast alternative for univariate only, uses fixed quantiles [0.1,0.2,...,0.9], custom quantiles parameter ignored.'),
125-
x: z.any().describe('Time series data to forecast from. Can be a 1D array (single series), 2D array (multiple series/batch or multivariate per model), or 3D array (batch, sequence, features).'),
126-
horizon: z.number().describe('Number of time steps to forecast into the future. Must be a positive integer. Example: 10 means predict the next 10 steps.'),
127-
output_type: z.enum(['point', 'quantiles']).optional().describe('Type of forecast output. "point" = single value per step (fastest). "quantiles" = confidence intervals (use for uncertainty estimation). Default: "point".'),
128-
quantiles: z.array(z.number()).optional().describe('Custom quantile levels to compute (only used with output_type="quantiles" and Chronos2 model). For TiRex, this parameter is ignored and fixed quantiles [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9] are always returned. Values must be between 0 and 1. Example: [0.1, 0.5, 0.9] for 10th, 50th, 90th percentiles.'),
129-
is_multivariate: z.boolean().optional().describe('For 2D input arrays only with Chronos2: interpret as multivariate time series (true) or batch of univariate series (false, default). Ignored for 1D arrays, 3D arrays, and TiRex model.'),
124+
model: z
125+
.enum(['chronos2', 'tirex'])
126+
.describe(
127+
'The forecasting model to use. Chronos2: State-of-the-art, supports univariate/multivariate, custom quantiles. TiRex: Fast alternative for univariate only, uses fixed quantiles [0.1,0.2,...,0.9], custom quantiles parameter ignored.'
128+
),
129+
x: z
130+
.any()
131+
.describe(
132+
'Time series data to forecast from. Can be a 1D array (single series), 2D array (multiple series/batch or multivariate per model), or 3D array (batch, sequence, features).'
133+
),
134+
horizon: z
135+
.number()
136+
.describe(
137+
'Number of time steps to forecast into the future. Must be a positive integer. Example: 10 means predict the next 10 steps.'
138+
),
139+
output_type: z
140+
.enum(['point', 'quantiles'])
141+
.optional()
142+
.describe(
143+
'Type of forecast output. "point" = single value per step (fastest). "quantiles" = confidence intervals (use for uncertainty estimation). Default: "point".'
144+
),
145+
quantiles: z
146+
.array(z.number())
147+
.optional()
148+
.describe(
149+
'Custom quantile levels to compute (only used with output_type="quantiles" and Chronos2 model). For TiRex, this parameter is ignored and fixed quantiles [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9] are always returned. Values must be between 0 and 1. Example: [0.1, 0.5, 0.9] for 10th, 50th, 90th percentiles.'
150+
),
151+
is_multivariate: z
152+
.boolean()
153+
.optional()
154+
.describe(
155+
'For 2D input arrays only with Chronos2: interpret as multivariate time series (true) or batch of univariate series (false, default). Ignored for 1D arrays, 3D arrays, and TiRex model.'
156+
),
130157
},
131158
async ({ model, x, horizon, output_type, quantiles, is_multivariate }: any) => {
132159
const result = await forecast({
@@ -176,7 +203,6 @@ async function main(): Promise<void> {
176203
// Connect server to transport
177204
// The transport handles all JSON-RPC protocol details
178205
await server.connect(transport);
179-
180206
} catch (error) {
181207
console.error('[MCP] Failed to start server:', error);
182208
process.exit(1);

src/tools/forecast.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ import { transformError } from '../utils/errors.js';
3737
* @param request - Pre-validated forecast request
3838
* @returns {Promise<ToolResult<ForecastResponse>>} Either success with predictions or error
3939
*/
40-
export async function forecast(
41-
request: unknown
42-
): Promise<ToolResult<ForecastResponse>> {
40+
export async function forecast(request: unknown): Promise<ToolResult<ForecastResponse>> {
4341
try {
4442
// Validate request before processing
4543
const validationError = validateForecastRequest(request);
@@ -76,7 +74,6 @@ export async function forecast(
7674
);
7775
const outputShape = getArrayShape(normalizedX);
7876

79-
8077
// Get the FAIM client (singleton initialized at server startup)
8178
const client = getClient();
8279

@@ -151,7 +148,6 @@ export async function forecast(
151148
},
152149
};
153150

154-
155151
return {
156152
success: true,
157153
data: response,

src/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,7 @@ export interface ForecastRequest {
148148
* For point forecasts: { point: number[][][] }
149149
* For quantile forecasts: { quantiles: number[][][][] }
150150
*/
151-
export type ForecastOutput =
152-
| { point: number[][][] }
153-
| { quantiles: number[][][][] };
151+
export type ForecastOutput = { point: number[][][] } | { quantiles: number[][][][] };
154152

155153
/**
156154
* Complete response from a successful forecast operation

src/utils/client.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function initializeClient(): void {
6565
if (!apiKey || apiKey.trim().length === 0) {
6666
initializationError = new Error(
6767
'FAIM_API_KEY environment variable is not set. ' +
68-
'Please set it before starting the MCP server.'
68+
'Please set it before starting the MCP server.'
6969
);
7070
throw initializationError;
7171
}
@@ -86,7 +86,6 @@ export function initializeClient(): void {
8686
// Users can override via FAIM_API_BASE_URL environment variable
8787
baseUrl: process.env.FAIM_API_BASE_URL,
8888
});
89-
9089
} catch (error) {
9190
// Store the error for later retrieval
9291
initializationError = error instanceof Error ? error : new Error(String(error));
@@ -122,9 +121,7 @@ export function getClient(): FaimClient {
122121

123122
// If initialization hasn't been attempted, indicate this is a startup issue
124123
if (!clientInstance) {
125-
throw new Error(
126-
'FAIM client not initialized. Call initializeClient() during server startup.'
127-
);
124+
throw new Error('FAIM client not initialized. Call initializeClient() during server startup.');
128125
}
129126

130127
return clientInstance;

src/utils/errors.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ function transformJavaScriptError(
188188
logError('VALIDATION_ERROR', message, error);
189189
return {
190190
error_code: 'INVALID_PARAMETER',
191-
message: message, // Use the actual validation message
191+
message: message, // Use the actual validation message
192192
details: context?.operation ? `Operation: ${context.operation}` : undefined,
193193
field: context?.field,
194194
};
@@ -253,9 +253,7 @@ function isNetworkError(message: string): boolean {
253253
'connect',
254254
];
255255

256-
return networkPatterns.some((pattern) =>
257-
message.toLowerCase().includes(pattern.toLowerCase())
258-
);
256+
return networkPatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));
259257
}
260258

261259
/**
@@ -264,15 +262,9 @@ function isNetworkError(message: string): boolean {
264262
* @internal Helper for error classification
265263
*/
266264
function isTimeoutError(message: string): boolean {
267-
const timeoutPatterns = [
268-
'timeout',
269-
'timed out',
270-
'deadline exceeded',
271-
];
265+
const timeoutPatterns = ['timeout', 'timed out', 'deadline exceeded'];
272266

273-
return timeoutPatterns.some((pattern) =>
274-
message.toLowerCase().includes(pattern.toLowerCase())
275-
);
267+
return timeoutPatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));
276268
}
277269

278270
/**
@@ -284,12 +276,7 @@ function isTimeoutError(message: string): boolean {
284276
* @internal Helper for error classification
285277
*/
286278
function isValidationError(message: string): boolean {
287-
const validationPatterns = [
288-
'cannot be empty',
289-
'must be',
290-
'invalid',
291-
'expected',
292-
];
279+
const validationPatterns = ['cannot be empty', 'must be', 'invalid', 'expected'];
293280

294281
return validationPatterns.some((pattern) =>
295282
message.toLowerCase().includes(pattern.toLowerCase())
@@ -356,11 +343,7 @@ function logError(code: string, message: string, originalError: unknown): void {
356343
}
357344

358345
// In production, log only important errors
359-
const importantCodes = [
360-
'AUTHENTICATION_FAILED',
361-
'INTERNAL_SERVER_ERROR',
362-
'RESOURCE_EXHAUSTED',
363-
];
346+
const importantCodes = ['AUTHENTICATION_FAILED', 'INTERNAL_SERVER_ERROR', 'RESOURCE_EXHAUSTED'];
364347

365348
if (importantCodes.includes(code)) {
366349
console.error(`[${code}] ${message}`);

src/utils/validation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ export function validateForecastRequest(request: unknown): ErrorResponse | null
197197
*
198198
* @internal Internal helper for validateForecastRequest
199199
*/
200-
function validateArrayInput(x: unknown): { error_code: string; message: string; details?: string } | null {
200+
function validateArrayInput(
201+
x: unknown
202+
): { error_code: string; message: string; details?: string } | null {
201203
if (!Array.isArray(x)) {
202204
return {
203205
error_code: 'INVALID_PARAMETER',

tests/integration/forecast.integration.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,13 @@ describe('Forecast Integration Tests', () => {
347347
it('should forecast with 3D multivariate array (Chronos2)', async () => {
348348
const request = {
349349
model: 'chronos2',
350-
x: [[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]],
350+
x: [
351+
[
352+
[1.0, 2.0, 3.0],
353+
[4.0, 5.0, 6.0],
354+
[7.0, 8.0, 9.0],
355+
],
356+
],
351357
horizon: 1,
352358
output_type: 'point' as const,
353359
};
@@ -875,9 +881,7 @@ describe('Forecast Integration Tests', () => {
875881
expect(result2.success).toBe(true);
876882
if (result1.success && result2.success) {
877883
// Results should be identical for same input
878-
expect(JSON.stringify(result1.data.forecast)).toBe(
879-
JSON.stringify(result2.data.forecast)
880-
);
884+
expect(JSON.stringify(result1.data.forecast)).toBe(JSON.stringify(result2.data.forecast));
881885
}
882886
});
883887

@@ -974,4 +978,4 @@ describe('Forecast Integration Tests', () => {
974978
expect(result.data.forecast.point).toBeDefined();
975979
}
976980
});
977-
});
981+
});

tests/tools/forecast.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,25 @@ describe('forecast tool', () => {
146146
it('should accept 2D array input (multivariate)', async () => {
147147
const request = {
148148
model: 'chronos2',
149-
x: [[1, 2], [3, 4], [5, 6]],
149+
x: [
150+
[1, 2],
151+
[3, 4],
152+
[5, 6],
153+
],
150154
horizon: 10,
151155
is_multivariate: true,
152156
};
153157

154158
const result = await forecast(request);
155159

156160
expect(result.success).toBe(true);
157-
const expectedX = [[[1, 2], [3, 4], [5, 6]]];
161+
const expectedX = [
162+
[
163+
[1, 2],
164+
[3, 4],
165+
[5, 6],
166+
],
167+
];
158168
expect(mockClient.forecastChronos2).toHaveBeenCalledWith(
159169
expect.objectContaining({ x: expectedX })
160170
);

tests/utils/validation.test.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ describe('validateForecastRequest', () => {
7777
it('should accept 2D array input (multivariate)', () => {
7878
const request = {
7979
model: 'chronos2',
80-
x: [[1, 2], [3, 4], [5, 6]],
80+
x: [
81+
[1, 2],
82+
[3, 4],
83+
[5, 6],
84+
],
8185
horizon: 10,
8286
};
8387

@@ -397,7 +401,11 @@ describe('normalizeInput', () => {
397401
});
398402

399403
it('should flatten 2D array for TiRex model (is_multivariate ignored)', () => {
400-
const input = [[1, 2], [3, 4], [5, 6]];
404+
const input = [
405+
[1, 2],
406+
[3, 4],
407+
[5, 6],
408+
];
401409
const normalized = normalizeInput(input, 'tirex', true); // is_multivariate=true but should be ignored for TiRex
402410

403411
expect(normalized).toHaveLength(1); // batch size 1
@@ -409,7 +417,11 @@ describe('normalizeInput', () => {
409417

410418
describe('2D array handling with is_multivariate=true (Chronos2 only)', () => {
411419
it('should keep 2D array as multivariate for Chronos2 with is_multivariate=true', () => {
412-
const input = [[1, 2], [3, 4], [5, 6]]; // shape [3, 2] -> multivariate (3 timesteps, 2 features)
420+
const input = [
421+
[1, 2],
422+
[3, 4],
423+
[5, 6],
424+
]; // shape [3, 2] -> multivariate (3 timesteps, 2 features)
413425
const normalized = normalizeInput(input, 'chronos2', true);
414426

415427
expect(normalized).toHaveLength(1); // batch size 1
@@ -431,19 +443,33 @@ describe('normalizeInput', () => {
431443
});
432444

433445
it('should handle many features multivariate 2D array', () => {
434-
const input = [[10, 20, 30], [40, 50, 60], [70, 80, 90], [100, 110, 120]];
446+
const input = [
447+
[10, 20, 30],
448+
[40, 50, 60],
449+
[70, 80, 90],
450+
[100, 110, 120],
451+
];
435452
const normalized = normalizeInput(input, 'chronos2', true);
436453

437454
expect(normalized).toHaveLength(1); // batch size 1
438455
expect(normalized[0]).toHaveLength(4); // 4 time steps
439456
expect(normalized[0][0]).toHaveLength(3); // 3 features
440-
expect(normalized[0]).toEqual([[10, 20, 30], [40, 50, 60], [70, 80, 90], [100, 110, 120]]);
457+
expect(normalized[0]).toEqual([
458+
[10, 20, 30],
459+
[40, 50, 60],
460+
[70, 80, 90],
461+
[100, 110, 120],
462+
]);
441463
});
442464
});
443465

444466
describe('2D array handling with is_multivariate=false (explicit batch inference)', () => {
445467
it('should flatten 2D array for Chronos2 with is_multivariate=false', () => {
446-
const input = [[1, 2], [3, 4], [5, 6]];
468+
const input = [
469+
[1, 2],
470+
[3, 4],
471+
[5, 6],
472+
];
447473
const normalized = normalizeInput(input, 'chronos2', false);
448474

449475
// With is_multivariate=false, should flatten to: [1, 2, 3, 4, 5, 6]
@@ -490,7 +516,12 @@ describe('normalizeInput', () => {
490516

491517
describe('3D array handling with is_multivariate flag', () => {
492518
it('should ignore is_multivariate flag for 3D array', () => {
493-
const input = [[[1, 2], [3, 4]]];
519+
const input = [
520+
[
521+
[1, 2],
522+
[3, 4],
523+
],
524+
];
494525
const normalized = normalizeInput(input, 'chronos2', true);
495526

496527
// is_multivariate should be ignored for 3D arrays
@@ -544,12 +575,25 @@ describe('getArrayShape', () => {
544575
});
545576

546577
it('should get shape of 2D array', () => {
547-
const shape = getArrayShape([[1, 2], [3, 4], [5, 6]]);
578+
const shape = getArrayShape([
579+
[1, 2],
580+
[3, 4],
581+
[5, 6],
582+
]);
548583
expect(shape).toEqual([3, 2]);
549584
});
550585

551586
it('should get shape of 3D array', () => {
552-
const shape = getArrayShape([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]);
587+
const shape = getArrayShape([
588+
[
589+
[1, 2],
590+
[3, 4],
591+
],
592+
[
593+
[5, 6],
594+
[7, 8],
595+
],
596+
]);
553597
expect(shape).toEqual([2, 2, 2]);
554598
});
555599

0 commit comments

Comments
 (0)