mcp-timekeeper
Version:
MCP server for timezone conversion and time utilities
721 lines • 29.1 kB
JavaScript
#!/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