Skip to main content
Glama
time-calculator.ts37.6 kB
import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { DateTime } from "luxon"; import { z } from "zod"; import { configManager } from "../utils/config.js"; import { formatDuration, normalizeToUTC } from "../utils/date-utils.js"; const MAX_OPERATIONS = 10000; interface TimeCalculatorResponse { success: boolean; operation: string; result?: unknown; error?: string; warnings?: string[]; metadata?: { calculation_time: string; calculation_timezone: string; processed_count?: number; failed_count?: number; }; } type ParseResult<T> = | { success: true; data: T } | { success: false; error: string }; type OperationResult<T> = | { success: true; data: T; warnings?: string[] } | { success: false; error: string }; function createResponse(response: TimeCalculatorResponse) { return { content: [ { type: "text" as const, text: JSON.stringify(response, null, 2), }, ], }; } function createErrorResponse(operation: string, error: string) { const response: TimeCalculatorResponse = { success: false, operation, error, }; if (configManager.isDebugMode()) { response.metadata = { calculation_time: DateTime.now().toISO() || "", calculation_timezone: DateTime.now().zoneName || "system", }; } return createResponse(response); } interface OperationPlan { interaction_mode: string; base_times: string[]; compare_times?: string[]; } function planOperations( base_time: string | string[] | undefined, compare_time: string | string[] | undefined, interaction_mode: string, ): ParseResult<OperationPlan> { // Normalize inputs to arrays using safe parsing const baseTimes = base_time ? safelyParseTimeArray(base_time) : []; const compareTimes = compare_time ? safelyParseTimeArray(compare_time) : []; let actualMode = interaction_mode; let operationCount = 0; let finalBaseTimes = baseTimes; let finalCompareTimes = compareTimes; if (interaction_mode === "auto_detect") { // Auto-detect based on input patterns if (baseTimes.length <= 1 && compareTimes.length <= 1) { actualMode = "single_to_single"; operationCount = Math.max(baseTimes.length, compareTimes.length); } else if (baseTimes.length === 1 && compareTimes.length > 1) { actualMode = "single_to_many"; operationCount = compareTimes.length; } else if (baseTimes.length > 1 && compareTimes.length <= 1) { actualMode = "many_to_single"; operationCount = baseTimes.length; } else { // Both are arrays with multiple items - default to pairwise actualMode = "pairwise"; const minLength = Math.min(baseTimes.length, compareTimes.length); finalBaseTimes = baseTimes.slice(0, minLength); finalCompareTimes = compareTimes.slice(0, minLength); operationCount = minLength; } } else if (interaction_mode === "single_to_many") { if (baseTimes.length !== 1 || compareTimes.length <= 1) { return { success: false, error: "single_to_many mode requires exactly 1 base_time and multiple compare_times", }; } operationCount = compareTimes.length; } else if (interaction_mode === "many_to_single") { if (baseTimes.length <= 1 || compareTimes.length !== 1) { return { success: false, error: "many_to_single mode requires multiple base_times and exactly 1 compare_time", }; } operationCount = baseTimes.length; } else if (interaction_mode === "pairwise") { if (baseTimes.length === 0 || compareTimes.length === 0) { return { success: false, error: "pairwise mode requires both base_time and compare_time arrays", }; } const minLength = Math.min(baseTimes.length, compareTimes.length); finalBaseTimes = baseTimes.slice(0, minLength); finalCompareTimes = compareTimes.slice(0, minLength); operationCount = minLength; } else if (interaction_mode === "cross_product") { if (baseTimes.length === 0 || compareTimes.length === 0) { return { success: false, error: "cross_product mode requires both base_time and compare_time arrays", }; } operationCount = baseTimes.length * compareTimes.length; } else if (interaction_mode === "aggregate") { // For now, treat as pairwise until aggregate is better defined const minLength = Math.min(baseTimes.length, compareTimes.length); finalBaseTimes = baseTimes.slice(0, minLength); finalCompareTimes = compareTimes.slice(0, minLength); operationCount = minLength; } // Enforce maximum operations if (operationCount > MAX_OPERATIONS) { return { success: false, error: `Operation count (${operationCount}) exceeds maximum allowed (${MAX_OPERATIONS}) for interaction mode '${actualMode}'`, }; } return { success: true, data: { interaction_mode: actualMode, base_times: finalBaseTimes, compare_times: finalCompareTimes, }, }; } // Reusable utility functions function safelyParseTimeArray(times: string | string[] | undefined): string[] { if (!times) { return [DateTime.now().toISO() || ""]; } if (Array.isArray(times)) { return times; } // If it's a string that looks like JSON array, try to parse it if (typeof times === "string" && times.trim().startsWith("[")) { try { const parsed = JSON.parse(times); if ( Array.isArray(parsed) && parsed.every((item) => typeof item === "string") ) { return parsed; } } catch { // If JSON parsing fails, treat as single string } } // Fallback: treat as single string return [times]; } function parseTimestamps( times: string | string[] | undefined, timezone?: string, fieldName: string = "time", allowInvalidForPairwise: boolean = false, ): ParseResult<DateTime[]> { const timesArray = safelyParseTimeArray(times); const parsedTimes: DateTime[] = []; const invalidTimes: string[] = []; for (const timeStr of timesArray) { // Check for timezone indicators: Z, +, or - after the time part const hasTimezone = /[Z]$|[+-]\d{2}:?\d{2}$/.test(timeStr); let dt: DateTime; if (timezone && !hasTimezone) { dt = DateTime.fromISO(timeStr, { zone: timezone }); } else { dt = DateTime.fromISO(timeStr); // Apply timezone conversion if specified and time has timezone info if (timezone) { dt = dt.setZone(timezone); } } if (!dt.isValid) { invalidTimes.push(`${timeStr} (${dt.invalidReason})`); } // Always add to parsed times (invalid ones will be handled gracefully by callers) parsedTimes.push(dt); } // For pairwise operations, allow invalid timestamps to pass through // Individual pairs will handle errors gracefully in _executePairwise if (allowInvalidForPairwise && invalidTimes.length > 0) { return { success: true, data: parsedTimes, // Include invalid DateTime objects }; } // For non-pairwise operations, fail if any timestamps are invalid if (invalidTimes.length > 0) { return { success: false, error: `Invalid ${fieldName} format(s): ${invalidTimes.join(", ")}`, }; } return { success: true, data: parsedTimes, }; } interface DurationObject { years?: number; months?: number; days?: number; hours?: number; minutes?: number; seconds?: number; } function applyDuration( timestamps: DateTime[], duration: DurationObject, operation: "add" | "subtract", ): OperationResult<DateTime[]> { const results: DateTime[] = []; const errors: string[] = []; for (let i = 0; i < timestamps.length; i++) { const timestamp = timestamps[i]; if (!timestamp || !timestamp.isValid) { errors.push( `Timestamp at index ${i} is invalid: ${timestamp?.invalidReason || "undefined timestamp"}`, ); continue; } const resultTime = operation === "add" ? timestamp.plus(duration) : timestamp.minus(duration); if (!resultTime.isValid) { errors.push( `Invalid result from ${operation} at index ${i}: ${resultTime.invalidReason}`, ); continue; } results.push(resultTime); } if (errors.length > 0) { return { success: false, error: errors.join("; "), }; } return { success: true, data: results, }; } interface ResultFormat { count: number; results: string[]; timezone: string; } interface StatsResult { input_analysis: { base_time_count: number; compare_time_count: number; }; timestamp_analysis?: { earliest: string; latest: string; total_span_ms: number; total_span_human: string; mean_timestamp: string; median_timestamp: string; std_deviation_ms: number; }; interval_analysis?: { interval_count: number; mean_interval_ms: number; mean_interval_human: string; min_interval_ms: number; max_interval_ms: number; total_intervals_span_ms: number; }; duration_analysis?: { pair_count: number; min_duration_ms: number; min_duration_human: string; max_duration_ms: number; max_duration_human: string; mean_duration_ms: number; mean_duration_human: string; median_duration_ms: number; median_duration_human: string; std_deviation_ms: number; total_duration_ms: number; }; } function formatResults( timestamps: DateTime[], returnSingle: boolean = false, ): string | ResultFormat { const resultISOs = timestamps.map((dt) => dt.toISO() || ""); if (returnSingle && timestamps.length === 1) { return resultISOs[0] || ""; } return { count: timestamps.length, results: resultISOs, timezone: timestamps[0]?.zoneName || "unknown", }; } // Reusable interaction mode functions for complex operations type OperationFunction = (baseTime: DateTime, compareTime: DateTime) => unknown; function _executePairwise( baseTimes: DateTime[], compareTimes: DateTime[], operation: OperationFunction, ): unknown[] { const results: unknown[] = []; const minLength = Math.min(baseTimes.length, compareTimes.length); for (let i = 0; i < minLength; i++) { const baseTime = baseTimes[i]; const compareTime = compareTimes[i]; if (baseTime && compareTime) { try { if (!baseTime.isValid) { throw new Error( `Invalid base_time at index ${i}: ${baseTime.invalidReason}`, ); } if (!compareTime.isValid) { throw new Error( `Invalid compare_time at index ${i}: ${compareTime.invalidReason}`, ); } const result = operation(baseTime, compareTime); results.push(result); } catch (error) { results.push({ error: error instanceof Error ? error.message : "Unknown calculation error", index: i, base_time: baseTime.toISO() || "Invalid DateTime", compare_time: compareTime.toISO() || "Invalid DateTime", }); } } else { results.push({ error: "Missing timestamp", index: i, base_time: baseTime?.toISO() || "undefined", compare_time: compareTime?.toISO() || "undefined", }); } } return results; } function _executeCrossProduct( baseTimes: DateTime[], compareTimes: DateTime[], operation: OperationFunction, ): unknown[] { const results: unknown[] = []; for (const baseTime of baseTimes) { for (const compareTime of compareTimes) { const result = operation(baseTime, compareTime); results.push(result); } } return results; } function _executeSingleToMany( baseTimes: DateTime[], compareTimes: DateTime[], operation: OperationFunction, ): unknown[] { const results: unknown[] = []; const baseTime = baseTimes[0]; if (baseTime) { for (const compareTime of compareTimes) { const result = operation(baseTime, compareTime); results.push(result); } } return results; } function _executeManyToSingle( baseTimes: DateTime[], compareTimes: DateTime[], operation: OperationFunction, ): unknown[] { const results: unknown[] = []; const compareTime = compareTimes[0]; if (compareTime) { for (const baseTime of baseTimes) { const result = operation(baseTime, compareTime); results.push(result); } } return results; } export const TimeCalculatorSchema = z.object({ operation: z .enum(["add", "subtract", "diff", "duration_between", "stats", "sort"]) .describe("Type of calculation to perform"), interaction_mode: z .enum([ "auto_detect", "single_to_many", "many_to_single", "pairwise", "cross_product", "aggregate", ]) .optional() .describe( "How base_time and compare_time arrays interact. 'auto_detect' handles single-to-single, single-to-many, many-to-single automatically. Defaults to 'auto_detect'", ), base_time: z .union([z.string(), z.array(z.string()).min(1)]) .optional() .describe( "Base ISO datetime(s). Single string or array. Defaults to current time if not provided", ), compare_time: z .union([z.string(), z.array(z.string()).min(1)]) .optional() .describe( "Compare ISO datetime(s) for diff/duration_between operations. Single string or array", ), timezone: z .string() .optional() .describe("Timezone for base_time (e.g., 'America/New_York')"), compare_time_timezone: z .string() .optional() .describe( "Timezone for compare_time. If not provided, base_time timezone is used", ), years: z.number().optional().describe("Years to add/subtract"), months: z.number().optional().describe("Months to add/subtract"), days: z.number().optional().describe("Days to add/subtract"), hours: z.number().optional().describe("Hours to add/subtract"), minutes: z.number().optional().describe("Minutes to add/subtract"), seconds: z.number().optional().describe("Seconds to add/subtract"), }); export const timeCalculatorTool: Tool = { name: "TIME CALCULATOR", description: "Perform time arithmetic operations including duration calculations, date math, interval operations, statistical analysis, and sorting. Use for adding/subtracting time periods, calculating differences between dates, analyzing time-based datasets, or sorting arrays of timestamps.", inputSchema: { type: "object", properties: { operation: { type: "string", enum: ["add", "subtract", "diff", "duration_between", "stats", "sort"], description: "Type of calculation to perform", }, interaction_mode: { type: "string", enum: [ "auto_detect", "single_to_many", "many_to_single", "pairwise", "cross_product", "aggregate", ], description: "How base_time and compare_time arrays interact. 'auto_detect' handles single-to-single, single-to-many, many-to-single automatically. Defaults to 'auto_detect'", }, base_time: { oneOf: [ { type: "string" }, { type: "array", items: { type: "string" }, minItems: 1 }, ], description: "Base ISO datetime(s). Single string or array. Defaults to current time if not provided", }, compare_time: { oneOf: [ { type: "string" }, { type: "array", items: { type: "string" }, minItems: 1 }, ], description: "Compare ISO datetime(s) for diff/duration_between operations. Single string or array", }, timezone: { type: "string", description: "Timezone for base_time (e.g., 'America/New_York')", }, compare_time_timezone: { type: "string", description: "Timezone for compare_time. If not provided, base_time timezone is used", }, years: { type: "number", description: "Years to add/subtract", }, months: { type: "number", description: "Months to add/subtract", }, days: { type: "number", description: "Days to add/subtract", }, hours: { type: "number", description: "Hours to add/subtract", }, minutes: { type: "number", description: "Minutes to add/subtract", }, seconds: { type: "number", description: "Seconds to add/subtract", }, }, required: ["operation"], additionalProperties: false, examples: [ { description: "Add 5 days and 3 hours to a specific time", value: { operation: "add", base_time: "2024-12-25T10:00:00Z", days: 5, hours: 3, }, }, { description: "Calculate difference between two dates", value: { operation: "diff", base_time: "2024-01-01T00:00:00Z", compare_time: "2024-12-25T15:30:00Z", }, }, { description: "Multi-timezone duration calculation", value: { operation: "duration_between", base_time: "2024-12-25T09:00:00", timezone: "America/New_York", compare_time: "2024-12-25T18:00:00", compare_time_timezone: "Europe/London", }, }, { description: "Subtract 2 months from current time", value: { operation: "subtract", months: 2, timezone: "America/New_York", }, }, { description: "Compare one base time to multiple compare times", value: { operation: "diff", base_time: "2024-01-01T00:00:00Z", compare_time: [ "2024-01-15T12:00:00Z", "2024-02-01T08:30:00Z", "2024-03-01T16:45:00Z", ], }, }, { description: "Pairwise comparison of time arrays", value: { operation: "duration_between", interaction_mode: "pairwise", base_time: ["2024-01-01T09:00:00Z", "2024-02-01T10:00:00Z"], compare_time: ["2024-01-01T17:30:00Z", "2024-02-01T18:45:00Z"], }, }, { description: "Statistical analysis of time intervals", value: { operation: "stats", base_time: [ "2024-01-01T08:00:00Z", "2024-01-02T09:15:00Z", "2024-01-03T07:45:00Z", "2024-01-04T08:30:00Z", ], compare_time: [ "2024-01-01T17:00:00Z", "2024-01-02T18:30:00Z", "2024-01-03T16:15:00Z", "2024-01-04T17:45:00Z", ], }, }, { description: "Sort array of timestamps chronologically", value: { operation: "sort", base_time: [ "2024-03-15T10:30:00Z", "2024-01-01T08:00:00Z", "2024-02-14T14:45:00Z", "2024-01-15T09:30:00Z", ], }, }, ], }, }; export async function handleTimeCalculator(args: unknown) { // Validate and parse arguments with Zod const parseResult = TimeCalculatorSchema.safeParse(args); if (!parseResult.success) { return createErrorResponse( "unknown", `Invalid arguments: ${parseResult.error.message}`, ); } const validatedArgs = parseResult.data; // Plan and validate operations before processing const planResult = planOperations( validatedArgs.base_time, validatedArgs.compare_time, validatedArgs.interaction_mode || "auto_detect", ); if (!planResult.success) { return createErrorResponse(validatedArgs.operation, planResult.error); } const plan = planResult.data; // Parse base times using utility function const isPairwise = plan.interaction_mode === "pairwise"; const baseTimesResult = parseTimestamps( validatedArgs.base_time, validatedArgs.timezone, "base_time", isPairwise, ); // For non-pairwise operations, fail if any timestamps are invalid if (!baseTimesResult.success) { return createErrorResponse(validatedArgs.operation, baseTimesResult.error); } const baseTimes = baseTimesResult.data; interface CalculationResult { operation: string; interaction_mode: string; input: { base_time: string | string[]; compare_time?: string | string[]; duration?: DurationObject; }; result: unknown; result_timezone?: string; metadata?: { calculation_time: string; calculation_timezone: string; }; } const result: CalculationResult = { operation: validatedArgs.operation, interaction_mode: plan.interaction_mode, input: { base_time: baseTimes.length === 1 ? baseTimes[0]?.toISO() || (validatedArgs.base_time as string) || "" : baseTimes.map((dt, _i) => { // Try to get ISO string from parsed DateTime first const isoString = dt?.toISO(); if (isoString) { return isoString; } // If parsing failed, return a placeholder return "Invalid timestamp"; }), }, result: null, }; switch (validatedArgs.operation) { case "add": case "subtract": { // Calculate operation count for add/subtract: sum of both array lengths let operationCount = baseTimes.length; if (validatedArgs.compare_time) { const compareTimes = safelyParseTimeArray(validatedArgs.compare_time); operationCount += compareTimes.length; } // Enforce maximum operations if (operationCount > MAX_OPERATIONS) { return createErrorResponse( validatedArgs.operation, `Operation count (${operationCount}) exceeds maximum allowed (${MAX_OPERATIONS}) for ${validatedArgs.operation} operation`, ); } // Build duration object from provided values const duration: DurationObject = {}; if (validatedArgs.years !== undefined) duration.years = validatedArgs.years; if (validatedArgs.months !== undefined) duration.months = validatedArgs.months; if (validatedArgs.days !== undefined) duration.days = validatedArgs.days; if (validatedArgs.hours !== undefined) duration.hours = validatedArgs.hours; if (validatedArgs.minutes !== undefined) duration.minutes = validatedArgs.minutes; if (validatedArgs.seconds !== undefined) duration.seconds = validatedArgs.seconds; if (Object.keys(duration).length === 0) { return createErrorResponse( validatedArgs.operation, `No duration specified for ${validatedArgs.operation} operation`, ); } // Apply duration to base times const baseResults = applyDuration( baseTimes, duration, validatedArgs.operation, ); if (!baseResults.success) { return createErrorResponse(validatedArgs.operation, baseResults.error); } // Handle compare_time if provided let compareResults: DateTime[] | undefined; if (validatedArgs.compare_time) { const compareTimesResult = parseTimestamps( validatedArgs.compare_time, validatedArgs.compare_time_timezone || validatedArgs.timezone, "compare_time", ); if (!compareTimesResult.success) { return createErrorResponse( validatedArgs.operation, compareTimesResult.error, ); } const compareDurationResult = applyDuration( compareTimesResult.data, duration, validatedArgs.operation, ); if (!compareDurationResult.success) { return createErrorResponse( validatedArgs.operation, compareDurationResult.error, ); } compareResults = compareDurationResult.data; // Update input to include compare_time result.input.compare_time = compareTimesResult.data.length === 1 ? compareTimesResult.data[0]?.toISO() || "" : compareTimesResult.data.map( (dt) => dt.toISO() || "Invalid timestamp", ); } result.input.duration = duration; // Format results based on whether compare_time is provided if (compareResults) { // Both base and compare provided - return structured output result.result = { base_results: formatResults(baseResults.data, false), compare_results: formatResults(compareResults, false), }; } else { // Only base_time provided - return simple format (maintain backward compatibility) const shouldReturnSingle = baseTimes.length === 1; if (shouldReturnSingle) { result.result = formatResults(baseResults.data, true); // Always show result_timezone for single results since metadata is hidden in normal mode result.result_timezone = baseResults.data[0]?.zoneName || "unknown"; } else { result.result = formatResults(baseResults.data, false); } } break; } case "diff": case "duration_between": { if (!validatedArgs.compare_time) { return createErrorResponse( validatedArgs.operation, `compare_time is required for ${validatedArgs.operation} operation`, ); } const compareTimezone = validatedArgs.compare_time_timezone || validatedArgs.timezone; // Parse compare times using utility function const compareTimesResult = parseTimestamps( validatedArgs.compare_time, compareTimezone, "compare_time", isPairwise, ); // For non-pairwise operations, fail if any timestamps are invalid if (!compareTimesResult.success) { return createErrorResponse( validatedArgs.operation, compareTimesResult.error, ); } const compareTimes = compareTimesResult.data; // Update input to show the compare times properly result.input.compare_time = compareTimes.length === 1 ? compareTimes[0]?.toISO() || (validatedArgs.compare_time as string) || "" : compareTimes.map((dt, _i) => { // Try to get ISO string from parsed DateTime first const isoString = dt?.toISO(); if (isoString) { return isoString; } // If parsing failed, return a placeholder return "Invalid timestamp"; }); // Handle different interaction modes for batch operations const diffOperation = (baseTime: DateTime, compareTime: DateTime) => { // Normalize both times to UTC to ensure consistent calculations regardless of timezone const baseTimeUTC = normalizeToUTC(baseTime); const compareTimeUTC = normalizeToUTC(compareTime); const diff = compareTimeUTC.diff(baseTimeUTC, [ "years", "months", "days", "hours", "minutes", "seconds", "milliseconds", ]); const totalMs = compareTimeUTC.diff(baseTimeUTC).as("milliseconds"); if (validatedArgs.operation === "diff") { // Decomposed time units that add up to the total time difference return { days: Math.floor(diff.days), hours: Math.floor(diff.hours), minutes: Math.floor(diff.minutes), seconds: Math.floor(diff.seconds), milliseconds: Math.floor(diff.milliseconds), total_milliseconds: totalMs, }; } else { // Detailed duration breakdown with years/months return { years: Math.floor(diff.years), months: Math.floor(diff.months), days: Math.floor(diff.days), hours: Math.floor(diff.hours), minutes: Math.floor(diff.minutes), seconds: Math.floor(diff.seconds), milliseconds: Math.floor(diff.milliseconds), total_milliseconds: totalMs, human_readable: diff.toHuman(), }; } }; // Execute based on interaction mode let diffResults: unknown[] = []; switch (plan.interaction_mode) { case "single_to_single": if (baseTimes[0] && compareTimes[0]) { diffResults = [diffOperation(baseTimes[0], compareTimes[0])]; } break; case "single_to_many": diffResults = _executeSingleToMany( baseTimes, compareTimes, diffOperation, ); break; case "many_to_single": diffResults = _executeManyToSingle( baseTimes, compareTimes, diffOperation, ); break; case "pairwise": diffResults = _executePairwise( baseTimes, compareTimes, diffOperation, ); break; case "cross_product": diffResults = _executeCrossProduct( baseTimes, compareTimes, diffOperation, ); break; default: // Fallback to pairwise diffResults = _executePairwise( baseTimes, compareTimes, diffOperation, ); break; } // Format result based on count if (diffResults.length === 1) { result.result = diffResults[0]; } else { const batchResult: { count: number; results: unknown[]; interaction_mode: string; } = { count: diffResults.length, results: diffResults, interaction_mode: plan.interaction_mode, }; result.result = batchResult; } break; } case "stats": { // Stats operation requires arrays of times for meaningful analysis if (!validatedArgs.base_time) { throw new Error("stats operation requires base_time"); } const baseTimes = safelyParseTimeArray(validatedArgs.base_time); if (baseTimes.length < 2) { throw new Error("stats operation requires at least 2 timestamps"); } const compareTimes = validatedArgs.compare_time ? safelyParseTimeArray(validatedArgs.compare_time) : undefined; // Parse all base times const baseTimestamps: number[] = []; const parseTimezone = validatedArgs.timezone; for (const timeStr of baseTimes) { const hasTimezone = /[Z]$|[+-]\d{2}:?\d{2}$/.test(timeStr); let dt: DateTime; if (parseTimezone && !hasTimezone) { dt = DateTime.fromISO(timeStr, { zone: parseTimezone }); } else { dt = DateTime.fromISO(timeStr); if (parseTimezone) dt = dt.setZone(parseTimezone); } if (!dt.isValid) { throw new Error(`Invalid time format in base_time: ${timeStr}`); } baseTimestamps.push(dt.toMillis()); } const stats: StatsResult = { input_analysis: { base_time_count: baseTimes.length, compare_time_count: compareTimes?.length || 0, }, }; // If only base_time is provided, analyze the timestamps themselves if (!compareTimes || compareTimes.length === 0) { if (baseTimestamps.length === 0) { throw new Error("No valid timestamps found in base_time"); } const sorted = [...baseTimestamps].sort((a, b) => a - b); const min = sorted[0]; const max = sorted[sorted.length - 1]; if (min === undefined || max === undefined) { throw new Error("Invalid timestamp data for stats calculation"); } const mean = baseTimestamps.reduce((sum, val) => sum + val, 0) / baseTimestamps.length; // Calculate median const mid = Math.floor(sorted.length / 2); const median = sorted.length % 2 === 0 ? ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2 : (sorted[mid] ?? 0); // Calculate standard deviation const variance = baseTimestamps.reduce((sum, val) => sum + (val - mean) ** 2, 0) / baseTimestamps.length; const stdDev = Math.sqrt(variance); // Calculate intervals between consecutive timestamps const intervals: number[] = []; for (let i = 1; i < sorted.length; i++) { const current = sorted[i]; const previous = sorted[i - 1]; if (current !== undefined && previous !== undefined) { intervals.push(current - previous); } } stats.timestamp_analysis = { earliest: DateTime.fromMillis(min).toISO() ?? "Invalid Date", latest: DateTime.fromMillis(max).toISO() ?? "Invalid Date", total_span_ms: max - min, total_span_human: formatDuration(max - min), mean_timestamp: DateTime.fromMillis(mean).toISO() ?? "Invalid Date", median_timestamp: DateTime.fromMillis(median).toISO() ?? "Invalid Date", std_deviation_ms: Math.round(stdDev), }; if (intervals.length > 0) { const intervalMean = intervals.reduce((sum, val) => sum + val, 0) / intervals.length; const intervalMin = Math.min(...intervals); const intervalMax = Math.max(...intervals); stats.interval_analysis = { interval_count: intervals.length, mean_interval_ms: Math.round(intervalMean), mean_interval_human: formatDuration(Math.round(intervalMean)), min_interval_ms: intervalMin, max_interval_ms: intervalMax, total_intervals_span_ms: intervals.reduce( (sum, val) => sum + val, 0, ), }; } } else { // If both base_time and compare_time are provided, analyze durations const durations: number[] = []; const minLength = Math.min(baseTimes.length, compareTimes.length); const compareTimezone = validatedArgs.compare_time_timezone || validatedArgs.timezone; for (let i = 0; i < minLength; i++) { const baseTimeStr = baseTimes[i]; const compareTimeStr = compareTimes[i]; if (!baseTimeStr || !compareTimeStr) { continue; } // Parse base time const hasBaseTimezone = /[Z]$|[+-]\d{2}:?\d{2}$/.test(baseTimeStr); let baseTime: DateTime; if (parseTimezone && !hasBaseTimezone) { baseTime = DateTime.fromISO(baseTimeStr, { zone: parseTimezone }); } else { baseTime = DateTime.fromISO(baseTimeStr); if (parseTimezone) baseTime = baseTime.setZone(parseTimezone); } // Parse compare time const hasCompareTimezone = /[Z]$|[+-]\d{2}:?\d{2}$/.test( compareTimeStr, ); let compareTime: DateTime; if (compareTimezone && !hasCompareTimezone) { compareTime = DateTime.fromISO(compareTimeStr, { zone: compareTimezone, }); } else { compareTime = DateTime.fromISO(compareTimeStr); if (compareTimezone) compareTime = compareTime.setZone(compareTimezone); } if (!baseTime.isValid) { throw new Error( `Invalid base_time format at index ${i}: ${baseTimeStr}`, ); } if (!compareTime.isValid) { throw new Error( `Invalid compare_time format at index ${i}: ${compareTimeStr}`, ); } // Normalize both times to UTC to ensure consistent calculations regardless of timezone const baseTimeUTC = normalizeToUTC(baseTime); const compareTimeUTC = normalizeToUTC(compareTime); const duration = compareTimeUTC.diff(baseTimeUTC).milliseconds; durations.push(duration); } // Calculate duration statistics if (durations.length === 0) { throw new Error("No valid duration pairs found"); } const sortedDurations = [...durations].sort((a, b) => a - b); const minDuration = sortedDurations[0]; const maxDuration = sortedDurations[sortedDurations.length - 1]; if (minDuration === undefined || maxDuration === undefined) { throw new Error("Invalid duration data for stats calculation"); } const meanDuration = durations.reduce((sum, val) => sum + val, 0) / durations.length; const mid = Math.floor(sortedDurations.length / 2); const medianDuration = sortedDurations.length % 2 === 0 ? ((sortedDurations[mid - 1] ?? 0) + (sortedDurations[mid] ?? 0)) / 2 : (sortedDurations[mid] ?? 0); const varianceDuration = durations.reduce((sum, val) => sum + (val - meanDuration) ** 2, 0) / durations.length; const stdDevDuration = Math.sqrt(varianceDuration); stats.duration_analysis = { pair_count: minLength, min_duration_ms: minDuration, min_duration_human: formatDuration(minDuration), max_duration_ms: maxDuration, max_duration_human: formatDuration(maxDuration), mean_duration_ms: Math.round(meanDuration), mean_duration_human: formatDuration(Math.round(meanDuration)), median_duration_ms: Math.round(medianDuration), median_duration_human: formatDuration(Math.round(medianDuration)), std_deviation_ms: Math.round(stdDevDuration), total_duration_ms: durations.reduce((sum, val) => sum + val, 0), }; } result.input.base_time = validatedArgs.base_time; if (compareTimes) { result.input.compare_time = compareTimes; } result.result = stats; break; } case "sort": { // Sort operation requires arrays of times if (!validatedArgs.base_time) { throw new Error("sort operation requires base_time"); } const baseTimes = safelyParseTimeArray(validatedArgs.base_time); if (baseTimes.length < 2) { throw new Error("sort operation requires at least 2 timestamps"); } const parseTimezone = validatedArgs.timezone; // Parse all timestamps and create sortable objects interface SortableTime { original: string; timestamp: number; parsed: DateTime; } const sortableItems: SortableTime[] = []; for (const timeStr of baseTimes) { const hasTimezone = /[Z]$|[+-]\d{2}:?\d{2}$/.test(timeStr); let dt: DateTime; if (parseTimezone && !hasTimezone) { dt = DateTime.fromISO(timeStr, { zone: parseTimezone }); } else { dt = DateTime.fromISO(timeStr); if (parseTimezone) dt = dt.setZone(parseTimezone); } if (!dt.isValid) { throw new Error( `Invalid time format in base_time: ${timeStr} - ${dt.invalidReason}`, ); } sortableItems.push({ original: timeStr, timestamp: dt.toMillis(), parsed: dt, }); } // Sort by timestamp (chronological order) sortableItems.sort((a, b) => a.timestamp - b.timestamp); // Extract sorted results const sortedOriginal = sortableItems.map((item) => item.original); const sortedISO = sortableItems.map((item) => item.parsed.toISO()); const sortedTimestamps = sortableItems.map((item) => item.timestamp); // Calculate some useful metadata about the sort const earliest = sortableItems[0]; const latest = sortableItems[sortableItems.length - 1]; if (!earliest || !latest) { throw new Error("Invalid sortable items for metadata calculation"); } const totalSpan = latest.timestamp - earliest.timestamp; const sortResult = { input_count: baseTimes.length, sorted_original_format: sortedOriginal, sorted_iso_format: sortedISO, sorted_timestamps: sortedTimestamps, sort_metadata: { earliest_time: earliest.parsed.toISO(), latest_time: latest.parsed.toISO(), total_span_ms: totalSpan, total_span_human: formatDuration(totalSpan), timezone_used: parseTimezone || "system", }, }; result.input.base_time = validatedArgs.base_time; result.result = sortResult; break; } default: return createErrorResponse( validatedArgs.operation, `Unsupported operation: ${validatedArgs.operation}`, ); } // Set metadata for successful response only if debug mode is enabled if (configManager.isDebugMode()) { result.metadata = { calculation_time: DateTime.now().toISO() || "", calculation_timezone: DateTime.now().zoneName || "system", }; } // Return the CalculationResult directly for backward compatibility return { content: [ { type: "text" as const, text: JSON.stringify(result, null, 2), }, ], }; }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JMoak/chrono-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server