UNPKG

mcp-timekeeper

Version:

MCP server for timezone conversion and time utilities

721 lines 29.1 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; const ConvertTimeSchema = z.object({ source_timezone: z.string(), time: z.string().refine((val) => /^\d{1,2}:\d{2}$/.test(val) || /^\d{4}-\d{2}-\d{2}T\d{1,2}:\d{2}(:\d{2})?(\.\d{3})?([+-]\d{2}:\d{2}|Z)?$/.test(val), "Time must be in HH:MM format or ISO datetime format"), target_timezone: z.string(), }); const TimeToUnixSchema = z.object({ iso_date: z.string(), timezone: z.string().optional(), }); const GetCurrentTimeSchema = z.object({ timezone: z.string().optional(), }); const GetClientTimeSchema = z.object({}); const AddTimeSchema = z.object({ date: z.string(), amount: z.number(), unit: z.enum(['years', 'months', 'days', 'hours', 'minutes', 'seconds']), timezone: z.string().optional(), }); const TimeDifferenceSchema = z.object({ start_date: z.string(), end_date: z.string(), unit: z.enum(['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'milliseconds']).optional(), timezone: z.string().optional(), }); const FormatTimeSchema = z.object({ date: z.string(), format: z.string(), timezone: z.string().optional(), locale: z.string().optional(), }); const TimezoneInfoSchema = z.object({ timezone: z.string(), date: z.string().optional(), }); function convertTime(sourceTimezone, time, targetTimezone) { try { let sourceDate; let inputDisplay; // Check if input is ISO datetime or just time if (/^\d{4}-\d{2}-\d{2}T\d{1,2}:\d{2}/.test(time)) { // ISO datetime format sourceDate = new Date(time); if (isNaN(sourceDate.getTime())) { throw new Error('Invalid ISO datetime format'); } inputDisplay = time; } else { // HH:MM format const timeParts = time.split(':'); if (timeParts.length !== 2) { throw new Error('Invalid time format - expected HH:MM'); } const hours = parseInt(timeParts[0], 10); const minutes = parseInt(timeParts[1], 10); if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { throw new Error('Invalid time values'); } const today = new Date(); sourceDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), hours, minutes); inputDisplay = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; } // Convert to target timezone const targetFormatter = new Intl.DateTimeFormat('sv-SE', { timeZone: targetTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const targetTime = targetFormatter.format(sourceDate); // Calculate timezone offset difference for display const sourceOffset = getTimezoneOffset(sourceTimezone, sourceDate); const targetOffset = getTimezoneOffset(targetTimezone, sourceDate); const offsetDiffHours = (sourceOffset - targetOffset) / (60 * 60 * 1000); return `${inputDisplay} ${sourceTimezone} = ${targetTime.replace(' ', 'T')} ${targetTimezone} (${offsetDiffHours >= 0 ? '+' : ''}${offsetDiffHours} hours difference)`; } catch (error) { throw new Error(`Invalid timezone or time format: ${error instanceof Error ? error.message : 'Unknown error'}`); } } function getTimezoneOffset(timezone, date) { try { const referenceDate = date || new Date(); const utc = new Date(referenceDate.getTime() + (referenceDate.getTimezoneOffset() * 60000)); const target = new Date(utc.toLocaleString('en-US', { timeZone: timezone })); return utc.getTime() - target.getTime(); } catch (error) { throw new Error(`Invalid timezone: ${timezone}`); } } function toUnixTimestamp(isoDate, timezone) { try { let date; if (timezone) { const tempDate = new Date(isoDate); if (isNaN(tempDate.getTime())) { throw new Error('Invalid ISO date format'); } const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const parts = formatter.formatToParts(tempDate); const partsObj = parts.reduce((acc, part) => { acc[part.type] = part.value; return acc; }, {}); date = new Date(`${partsObj.year}-${partsObj.month}-${partsObj.day}T${partsObj.hour}:${partsObj.minute}:${partsObj.second}`); } else { date = new Date(isoDate); } if (isNaN(date.getTime())) { throw new Error('Invalid date format'); } return Math.floor(date.getTime() / 1000); } catch (error) { throw new Error(`Failed to convert to Unix timestamp: ${error instanceof Error ? error.message : 'Unknown error'}`); } } function getCurrentTime(timezone) { try { const now = new Date(); const tz = timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; const formatter = new Intl.DateTimeFormat('sv-SE', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const formattedTime = formatter.format(now).replace(' ', 'T'); return { iso: formattedTime, timezone: tz, unix: Math.floor(now.getTime() / 1000) }; } catch (error) { throw new Error(`Failed to get current time: ${error instanceof Error ? error.message : 'Unknown error'}`); } } function getClientTime() { try { const now = new Date(); const clientTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const formatter = new Intl.DateTimeFormat('sv-SE', { timeZone: clientTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const formattedTime = formatter.format(now).replace(' ', 'T'); // Get timezone offset in hours and minutes const offsetMinutes = now.getTimezoneOffset(); const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60); const offsetMins = Math.abs(offsetMinutes) % 60; const offsetSign = offsetMinutes <= 0 ? '+' : '-'; const offsetString = `${offsetSign}${offsetHours.toString().padStart(2, '0')}:${offsetMins.toString().padStart(2, '0')}`; return { iso: formattedTime, timezone: clientTimezone, unix: Math.floor(now.getTime() / 1000), offset: offsetString }; } catch (error) { throw new Error(`Failed to get client time: ${error instanceof Error ? error.message : 'Unknown error'}`); } } function addTime(dateStr, amount, unit, timezone) { try { let date = new Date(dateStr); if (isNaN(date.getTime())) { throw new Error('Invalid date format'); } // Apply timezone if specified if (timezone) { const formatter = new Intl.DateTimeFormat('sv-SE', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const parts = formatter.formatToParts(date); const partsObj = parts.reduce((acc, part) => { acc[part.type] = part.value; return acc; }, {}); date = new Date(`${partsObj.year}-${partsObj.month}-${partsObj.day}T${partsObj.hour}:${partsObj.minute}:${partsObj.second}`); } const newDate = new Date(date); switch (unit) { case 'years': newDate.setFullYear(newDate.getFullYear() + amount); break; case 'months': newDate.setMonth(newDate.getMonth() + amount); break; case 'days': newDate.setDate(newDate.getDate() + amount); break; case 'hours': newDate.setHours(newDate.getHours() + amount); break; case 'minutes': newDate.setMinutes(newDate.getMinutes() + amount); break; case 'seconds': newDate.setSeconds(newDate.getSeconds() + amount); break; default: throw new Error(`Unsupported unit: ${unit}`); } const tz = timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; const formatter = new Intl.DateTimeFormat('sv-SE', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const result = formatter.format(newDate).replace(' ', 'T'); return `${result} (${tz}) - Added ${amount} ${unit}`; } catch (error) { throw new Error(`Failed to add time: ${error instanceof Error ? error.message : 'Unknown error'}`); } } function getTimeDifference(startDateStr, endDateStr, unit, timezone) { try { let startDate = new Date(startDateStr); let endDate = new Date(endDateStr); if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { throw new Error('Invalid date format'); } const diffMs = endDate.getTime() - startDate.getTime(); const diffSeconds = diffMs / 1000; const diffMinutes = diffSeconds / 60; const diffHours = diffMinutes / 60; const diffDays = diffHours / 24; const diffMonths = diffDays / 30.44; // Average days per month const diffYears = diffDays / 365.25; // Average days per year let result; const unitToShow = unit || 'days'; switch (unitToShow) { case 'milliseconds': result = `${Math.round(diffMs)} milliseconds`; break; case 'seconds': result = `${Math.round(diffSeconds)} seconds`; break; case 'minutes': result = `${Math.round(diffMinutes)} minutes`; break; case 'hours': result = `${Math.round(diffHours * 100) / 100} hours`; break; case 'days': result = `${Math.round(diffDays * 100) / 100} days`; break; case 'months': result = `${Math.round(diffMonths * 100) / 100} months`; break; case 'years': result = `${Math.round(diffYears * 100) / 100} years`; break; default: result = `${Math.round(diffDays * 100) / 100} days`; } const direction = diffMs >= 0 ? 'later' : 'earlier'; return `${Math.abs(parseFloat(result.split(' ')[0]))} ${result.split(' ')[1]} ${direction}`; } catch (error) { throw new Error(`Failed to calculate time difference: ${error instanceof Error ? error.message : 'Unknown error'}`); } } function formatTime(dateStr, format, timezone, locale) { try { const date = new Date(dateStr); if (isNaN(date.getTime())) { throw new Error('Invalid date format'); } const tz = timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; const loc = locale || 'en-US'; // Handle custom format patterns if (format.includes('YYYY') || format.includes('MM') || format.includes('DD') || format.includes('HH') || format.includes('mm') || format.includes('ss')) { const formatter = new Intl.DateTimeFormat('sv-SE', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const formattedParts = formatter.format(date); const [datePart, timePart] = formattedParts.split(' '); const [year, month, day] = datePart.split('-'); const [hour, minute, second] = timePart.split(':'); return format .replace('YYYY', year) .replace('MM', month) .replace('DD', day) .replace('HH', hour) .replace('mm', minute) .replace('ss', second); } // Handle preset formats let options = { timeZone: tz }; switch (format.toLowerCase()) { case 'full': options = { ...options, dateStyle: 'full', timeStyle: 'full' }; break; case 'long': options = { ...options, dateStyle: 'long', timeStyle: 'long' }; break; case 'medium': options = { ...options, dateStyle: 'medium', timeStyle: 'medium' }; break; case 'short': options = { ...options, dateStyle: 'short', timeStyle: 'short' }; break; case 'date': options = { ...options, dateStyle: 'full' }; break; case 'time': options = { ...options, timeStyle: 'full' }; break; case 'iso': return new Intl.DateTimeFormat('sv-SE', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(date).replace(' ', 'T'); default: options = { ...options, dateStyle: 'medium', timeStyle: 'medium' }; } return new Intl.DateTimeFormat(loc, options).format(date); } catch (error) { throw new Error(`Failed to format time: ${error instanceof Error ? error.message : 'Unknown error'}`); } } function getTimezoneInfo(timezone, dateStr) { try { const date = dateStr ? new Date(dateStr) : new Date(); if (isNaN(date.getTime())) { throw new Error('Invalid date format'); } // Get current offset const offsetMs = getTimezoneOffset(timezone, date); const offsetHours = offsetMs / (60 * 60 * 1000); const offsetSign = offsetHours >= 0 ? '+' : '-'; const offsetFormatted = `UTC${offsetSign}${Math.abs(Math.floor(offsetHours)).toString().padStart(2, '0')}:${Math.abs((offsetHours % 1) * 60).toString().padStart(2, '0')}`; // Get timezone abbreviation const shortFormatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, timeZoneName: 'short' }); const shortParts = shortFormatter.formatToParts(date); const abbreviation = shortParts.find(part => part.type === 'timeZoneName')?.value || 'N/A'; // Get long name const longFormatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, timeZoneName: 'long' }); const longParts = longFormatter.formatToParts(date); const longName = longParts.find(part => part.type === 'timeZoneName')?.value || timezone; // Check if DST is in effect (rough estimation) const jan = new Date(date.getFullYear(), 0, 1); const jul = new Date(date.getFullYear(), 6, 1); const janOffset = getTimezoneOffset(timezone, jan) / (60 * 60 * 1000); const julOffset = getTimezoneOffset(timezone, jul) / (60 * 60 * 1000); const isDST = Math.min(janOffset, julOffset) !== offsetHours; return `Timezone: ${timezone} Long Name: ${longName} Abbreviation: ${abbreviation} Current Offset: ${offsetFormatted} DST Active: ${isDST ? 'Yes' : 'No'} Current Time: ${new Intl.DateTimeFormat('sv-SE', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(date).replace(' ', 'T')}`; } catch (error) { throw new Error(`Failed to get timezone info: ${error instanceof Error ? error.message : 'Unknown error'}`); } } const server = new Server({ name: "mcp-time-server", version: "0.1.0", }, { capabilities: { tools: {}, }, }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "convert_time", description: "Convert time from one timezone to another", inputSchema: { type: "object", properties: { source_timezone: { type: "string", description: "Source timezone (e.g., 'America/New_York', 'UTC', 'Europe/London')" }, time: { type: "string", description: "Time in HH:MM format (24-hour) or ISO datetime format (e.g., '2023-12-25T10:30:00')" }, target_timezone: { type: "string", description: "Target timezone (e.g., 'America/Los_Angeles', 'Asia/Tokyo')" } }, required: ["source_timezone", "time", "target_timezone"] } }, { name: "time_to_unix", description: "Convert ISO date string to Unix timestamp", inputSchema: { type: "object", properties: { iso_date: { type: "string", description: "ISO date string (e.g., '2023-12-25T10:30:00' or '2023-12-25T10:30:00Z')" }, timezone: { type: "string", description: "Optional timezone to interpret the date in (e.g., 'America/New_York')" } }, required: ["iso_date"] } }, { name: "get_current_time", description: "Get current time in specified timezone", inputSchema: { type: "object", properties: { timezone: { type: "string", description: "Timezone (e.g., 'America/New_York', 'UTC'). Defaults to system timezone if not provided." } }, required: [] } }, { name: "get_client_time", description: "Get the current time and timezone of the client/server system", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "add_time", description: "Add or subtract time periods from a date", inputSchema: { type: "object", properties: { date: { type: "string", description: "ISO date string (e.g., '2023-12-25T10:30:00')" }, amount: { type: "number", description: "Amount to add (positive) or subtract (negative)" }, unit: { type: "string", enum: ["years", "months", "days", "hours", "minutes", "seconds"], description: "Time unit to add/subtract" }, timezone: { type: "string", description: "Optional timezone to interpret the date in" } }, required: ["date", "amount", "unit"] } }, { name: "time_difference", description: "Calculate the time difference between two dates", inputSchema: { type: "object", properties: { start_date: { type: "string", description: "Start date in ISO format (e.g., '2023-12-25T10:30:00')" }, end_date: { type: "string", description: "End date in ISO format (e.g., '2023-12-31T10:30:00')" }, unit: { type: "string", enum: ["years", "months", "days", "hours", "minutes", "seconds", "milliseconds"], description: "Unit to express the difference in (defaults to 'days')" }, timezone: { type: "string", description: "Optional timezone to interpret the dates in" } }, required: ["start_date", "end_date"] } }, { name: "format_time", description: "Format a date/time in various formats", inputSchema: { type: "object", properties: { date: { type: "string", description: "ISO date string to format (e.g., '2023-12-25T10:30:00')" }, format: { type: "string", description: "Format pattern: 'full', 'long', 'medium', 'short', 'date', 'time', 'iso', or custom pattern like 'YYYY-MM-DD HH:mm:ss'" }, timezone: { type: "string", description: "Optional timezone to format the date in" }, locale: { type: "string", description: "Optional locale for formatting (e.g., 'en-US', 'fr-FR')" } }, required: ["date", "format"] } }, { name: "timezone_info", description: "Get detailed information about a timezone", inputSchema: { type: "object", properties: { timezone: { type: "string", description: "Timezone identifier (e.g., 'America/New_York', 'Europe/London')" }, date: { type: "string", description: "Optional date to get timezone info for (defaults to current date)" } }, required: ["timezone"] } } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "convert_time": { const parsed = ConvertTimeSchema.parse(args); const result = convertTime(parsed.source_timezone, parsed.time, parsed.target_timezone); return { content: [ { type: "text", text: result } ] }; } case "time_to_unix": { const parsed = TimeToUnixSchema.parse(args); const timestamp = toUnixTimestamp(parsed.iso_date, parsed.timezone); return { content: [ { type: "text", text: `Unix timestamp: ${timestamp}` } ] }; } case "get_current_time": { const parsed = GetCurrentTimeSchema.parse(args); const currentTime = getCurrentTime(parsed.timezone); return { content: [ { type: "text", text: `Current time: ${currentTime.iso} (${currentTime.timezone})\nUnix timestamp: ${currentTime.unix}` } ] }; } case "get_client_time": { const parsed = GetClientTimeSchema.parse(args); const clientTime = getClientTime(); return { content: [ { type: "text", text: `Client time: ${clientTime.iso} (${clientTime.timezone})\nTimezone offset: UTC${clientTime.offset}\nUnix timestamp: ${clientTime.unix}` } ] }; } case "add_time": { const parsed = AddTimeSchema.parse(args); const result = addTime(parsed.date, parsed.amount, parsed.unit, parsed.timezone); return { content: [ { type: "text", text: result } ] }; } case "time_difference": { const parsed = TimeDifferenceSchema.parse(args); const result = getTimeDifference(parsed.start_date, parsed.end_date, parsed.unit, parsed.timezone); return { content: [ { type: "text", text: result } ] }; } case "format_time": { const parsed = FormatTimeSchema.parse(args); const result = formatTime(parsed.date, parsed.format, parsed.timezone, parsed.locale); return { content: [ { type: "text", text: result } ] }; } case "timezone_info": { const parsed = TimezoneInfoSchema.parse(args); const result = getTimezoneInfo(parsed.timezone, parsed.date); return { content: [ { type: "text", text: result } ] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { if (error instanceof z.ZodError) { throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); } throw error; } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Time Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); //# sourceMappingURL=index.js.map