Skip to main content
Glama
pshempel

MCP Time Server Node

by pshempel
addTime.ts9.97 kB
import { addYears, addMonths, addDays, addHours, addMinutes, addSeconds } from 'date-fns'; import { formatInTimeZone } from 'date-fns-tz'; import { ValidationError, TimezoneError, DateParsingError } from '../adapters/mcp-sdk'; import { CacheTTL } from '../cache/timeCache'; import type { AddTimeParams, AddTimeResult } from '../types'; import { getConfig } from '../utils/config'; import { debug } from '../utils/debug'; import { parseTimeInput } from '../utils/parseTimeInput'; import { resolveTimezone } from '../utils/timezoneUtils'; import { validateTimezone, validateDateInput } from '../utils/validation'; import { withCache } from '../utils/withCache'; const unitFunctions = { years: addYears, months: addMonths, days: addDays, hours: addHours, minutes: addMinutes, seconds: addSeconds, }; /** * Validates that the unit is one of the allowed time units */ export function validateUnit(unit: string): void { debug.validation('validateUnit called with: unit=%s', unit); if (!Object.prototype.hasOwnProperty.call(unitFunctions, unit)) { debug.validation('Invalid unit: %s', unit); debug.error('Invalid unit: %s', unit); throw new ValidationError( `Invalid unit: ${unit}. Must be one of: years, months, days, hours, minutes, seconds`, { unit } ); } debug.validation('Unit validation passed'); } /** * Validates that the amount is a finite number */ export function validateAmount(amount: number): void { debug.validation('validateAmount called with: amount=%s', amount); if (typeof amount !== 'number' || isNaN(amount) || !isFinite(amount)) { debug.validation('Invalid amount: %s', amount); debug.error('Invalid amount: %s', amount); throw new ValidationError(`Invalid amount: ${amount}. Must be a finite number`, { amount }); } debug.validation('Amount validation passed'); } interface ParseDateResult { date: Date; displayTimezone: string; hasExplicitOffset: boolean; explicitOffset: string; } interface FormatResult extends AddTimeResult { original: string; result: string; unix_original: number; unix_result: number; } /** * Parses date input with timezone awareness using the centralized parser */ export function parseDateWithTimezone( time: string, timezone: string, paramTimezone?: string ): ParseDateResult { debug.parse( 'parseDateWithTimezone called with: time=%s, timezone=%s, paramTimezone=%s', time, timezone, paramTimezone ); try { // Use centralized parser const result = parseTimeInput(time, timezone); // Determine display timezone based on the parse result let displayTimezone = timezone; let hasExplicitOffset = false; let explicitOffset = ''; // Check if it was a Unix timestamp (detected as UTC with no explicit timezone) if (/^\d+$/.test(time) && !paramTimezone) { displayTimezone = 'UTC'; } // Check for explicit timezone offset in the original string const offsetMatch = time.match(/([+-]\d{2}:\d{2})$/); if (offsetMatch) { hasExplicitOffset = true; explicitOffset = offsetMatch[0]; } // Handle Z suffix if (time.includes('Z') && !paramTimezone) { displayTimezone = 'UTC'; } return { date: result.date, displayTimezone, hasExplicitOffset, explicitOffset, }; } catch (error) { debug.parse('Parse error: %s', error); debug.error('Invalid time format: %s, error: %O', time, error); throw new DateParsingError(`Invalid time format: ${time}`, { time, error: error instanceof Error ? error.message : String(error), }); } } /** * Formats the result of the add time operation */ export function formatAddTimeResult( inputDate: Date, resultDate: Date, time: string, params: AddTimeParams, parseInfo: ParseDateResult ): FormatResult { debug.timing('formatAddTimeResult called with dates and parseInfo: %O', parseInfo); const { displayTimezone, hasExplicitOffset, explicitOffset } = parseInfo; // Handle Unix timestamp formatting if (/^\d+$/.test(time)) { return formatUnixTimestampResult(inputDate, resultDate, params.timezone ?? undefined); } // Handle explicit offset formatting if (hasExplicitOffset) { return formatWithExplicitOffset(inputDate, resultDate, time, explicitOffset); } // Handle Z suffix with timezone override if (time.includes('Z') && params.timezone) { debug.timing('Formatting Z suffix with requested timezone: %s', displayTimezone); return { original: formatInTimeZone(inputDate, displayTimezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), result: formatInTimeZone(resultDate, displayTimezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), unix_original: Math.floor(inputDate.getTime() / 1000), unix_result: Math.floor(resultDate.getTime() / 1000), }; } // Handle Z suffix without timezone override (UTC) if (time.includes('Z')) { debug.timing('Formatting Z suffix as UTC'); return { original: inputDate.toISOString(), result: resultDate.toISOString(), unix_original: Math.floor(inputDate.getTime() / 1000), unix_result: Math.floor(resultDate.getTime() / 1000), }; } // Default formatting with specified timezone debug.timing('Formatting in timezone: %s', displayTimezone); return { original: formatInTimeZone(inputDate, displayTimezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), result: formatInTimeZone(resultDate, displayTimezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), unix_original: Math.floor(inputDate.getTime() / 1000), unix_result: Math.floor(resultDate.getTime() / 1000), }; } /** * Formats Unix timestamp results with optional timezone * @param inputDate - Original date * @param resultDate - Result date after time operation * @param timezone - Optional timezone for display * @returns Formatted result with Unix timestamps */ export function formatUnixTimestampResult( inputDate: Date, resultDate: Date, timezone?: string ): FormatResult { debug.timing( 'formatUnixTimestampResult called with: inputDate=%s, resultDate=%s, timezone=%s', inputDate.toISOString(), resultDate.toISOString(), timezone ); let original: string; let result: string; if (timezone) { original = formatInTimeZone(inputDate, timezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); result = formatInTimeZone(resultDate, timezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); } else { original = inputDate.toISOString(); result = resultDate.toISOString(); } const output: FormatResult = { original, result, unix_original: Math.floor(inputDate.getTime() / 1000), unix_result: Math.floor(resultDate.getTime() / 1000), }; debug.timing('formatUnixTimestampResult returning: %O', output); return output; } /** * Formats result preserving explicit timezone offset * @param inputDate - Original date * @param resultDate - Result date after time operation * @param originalTime - Original time string with offset * @param offset - The timezone offset to preserve * @returns Formatted result with preserved offset */ export function formatWithExplicitOffset( inputDate: Date, resultDate: Date, originalTime: string, offset: string ): FormatResult { debug.timing( 'formatWithExplicitOffset called with: originalTime=%s, offset=%s', originalTime, offset ); // Format original - add milliseconds if missing const original = originalTime.includes('.') ? originalTime : originalTime.replace(offset, '.000' + offset); // Parse the offset to calculate the result const offsetMatch = offset.match(/([+-])(\d{2}):(\d{2})/); if (!offsetMatch) { debug.error('Invalid offset format: %s', offset); throw new DateParsingError(`Invalid offset format: ${offset}`, { offset }); } const sign = offsetMatch[1] === '+' ? 1 : -1; const hours = parseInt(offsetMatch[2], 10); const minutes = parseInt(offsetMatch[3], 10); const offsetMinutes = sign * (hours * 60 + minutes); // Create a date adjusted for the offset to get the local time const resultLocal = new Date(resultDate.getTime() + offsetMinutes * 60 * 1000); const resultTimeStr = resultLocal.toISOString().substring(0, 19); const result = resultTimeStr + '.000' + offset; const output: FormatResult = { original, result, unix_original: Math.floor(inputDate.getTime() / 1000), unix_result: Math.floor(resultDate.getTime() / 1000), }; debug.timing('formatWithExplicitOffset returning: %O', output); return output; } export function addTime(params: AddTimeParams): AddTimeResult { debug.timing('addTime called with params: %O', params); const { time, amount, unit } = params; // Validate date input with strict type checking validateDateInput(time, 'time'); const config = getConfig(); const timezone = resolveTimezone(params.timezone, config.defaultTimezone); // Use withCache wrapper instead of manual cache management return withCache(`add_${time}_${amount}_${unit}_${timezone}`, CacheTTL.CALCULATIONS, () => { // Validate unit validateUnit(unit); // Validate amount validateAmount(amount); // Validate timezone if provided if (params.timezone && !validateTimezone(timezone)) { debug.error('Invalid timezone: %s', timezone); throw new TimezoneError(`Invalid timezone: ${timezone}`, timezone); } // Parse the input date with timezone handling const parseResult = parseDateWithTimezone(time, timezone, params.timezone); const { date: inputDate } = parseResult; // Perform the addition // eslint-disable-next-line security/detect-object-injection -- Unit validated earlier const addFunction = unitFunctions[unit]; const resultDate = addFunction(inputDate, amount); // Format the result const output = formatAddTimeResult(inputDate, resultDate, time, params, parseResult); debug.timing('addTime returning: %O', output); return output; }); }

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/pshempel/mcp-time-server-node'

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