Skip to main content
Glama
JMoak
by JMoak

TIME CALCULATOR

Calculate time differences, add/subtract durations, analyze time intervals, and sort timestamps using date arithmetic operations.

Instructions

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.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
operationYesType of calculation to perform
interaction_modeNoHow 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_timeNoBase ISO datetime(s). Single string or array. Defaults to current time if not provided
compare_timeNoCompare ISO datetime(s) for diff/duration_between operations. Single string or array
timezoneNoTimezone for base_time (e.g., 'America/New_York')
compare_time_timezoneNoTimezone for compare_time. If not provided, base_time timezone is used
yearsNoYears to add/subtract
monthsNoMonths to add/subtract
daysNoDays to add/subtract
hoursNoHours to add/subtract
minutesNoMinutes to add/subtract
secondsNoSeconds to add/subtract

Implementation Reference

  • Main handler function that validates input using Zod schema, plans operations based on interaction_mode, parses timestamps with timezone support, and executes the specified operation (add/subtract durations, diff/duration_between with batch modes, stats analysis, sort timestamps). Returns structured JSON response with results, metadata, and errors.
    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),
    			},
    		],
    	};
    }
  • Zod schema for strict input validation, supporting complex parameters like array times, multiple interaction modes for batch operations, timezone handling, and duration components.
    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"),
    });
  • src/index.ts:20-36 (registration)
    MCP server registration: adds timeCalculatorTool to the tools list returned by ListToolsRequest, and dispatches CallToolRequest to handleTimeCalculator when name matches 'TIME CALCULATOR'.
    server.setRequestHandler(ListToolsRequestSchema, async () => ({
    	tools: [getTimeTool, timeCalculatorTool],
    }));
    
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
    	switch (request.params.name) {
    		case getTimeTool.name:
    			return await handleGetTime(request.params.arguments ?? {});
    		case timeCalculatorTool.name:
    			return await handleTimeCalculator(request.params.arguments ?? {});
    		default:
    			throw new McpError(
    				ErrorCode.MethodNotFound,
    				`Unknown tool: ${request.params.name}`,
    			);
    	}
    });
  • Tool metadata definition exporting the Tool object with name 'TIME CALCULATOR', comprehensive description, and JSON inputSchema compatible with MCP protocol including usage examples.
    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",
    					],
    				},
    			},
    		],
    	},
    };
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. While it mentions the types of operations, it doesn't disclose important behavioral traits like whether operations are read-only or mutating, error handling for invalid inputs, performance characteristics, or what the return values look like. For a complex tool with 12 parameters, this is a significant gap in behavioral transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized with two sentences that efficiently cover purpose and usage. The first sentence establishes the scope of operations, and the second provides specific use cases. There's no wasted text, and information is front-loaded, though it could be slightly more structured by grouping related operations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 12 parameters, no annotations, and no output schema, the description is insufficiently complete. It doesn't address what the tool returns, how errors are handled, performance considerations, or provide guidance on parameter combinations for different operations. The schema covers parameter definitions well, but the description fails to provide the contextual understanding needed for effective tool use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds no specific parameter semantics beyond what's in the schema - it doesn't explain how parameters interact, which parameters are required for which operations, or provide additional context about parameter usage. Baseline 3 is appropriate when the schema does all the parameter documentation work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool performs 'time arithmetic operations' and lists specific functions like duration calculations, date math, and sorting. It distinguishes from the sibling 'GET TIME' by emphasizing calculations rather than retrieval, though it doesn't explicitly contrast them. The verb+resource combination is specific but could be more precise about the computational nature.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear usage contexts with 'Use for adding/subtracting time periods, calculating differences between dates, analyzing time-based datasets, or sorting arrays of timestamps.' This gives explicit when-to-use guidance for different scenarios. However, it doesn't mention when NOT to use it or explicitly compare with the sibling 'GET TIME' tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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