Skip to main content
Glama
pshempel

MCP Time Server Node

by pshempel
index.ts14.5 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 type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import { mapToMcpError, McpError } from './adapters/mcp-sdk'; import { getCurrentTime, convertTimezone, addTime, subtractTime, calculateDuration, getBusinessDays, getServerInfo, nextOccurrence, formatTime, calculateBusinessHours, daysUntil, } from './tools'; import { debug, logEnvironment } from './utils/debug'; import { SlidingWindowRateLimiter } from './utils/rateLimit'; import { configureServer } from './utils/serverConfig'; // Configure server settings to prevent warnings configureServer(); // Log environment at startup logEnvironment(); // Tool definitions with metadata (keeping same as before) export const TOOL_DEFINITIONS = [ { name: 'get_server_info', description: 'Get server version and build information', inputSchema: { type: 'object' as const, properties: {}, }, }, { name: 'get_current_time', description: 'Get current time in specified timezone with formatting options', inputSchema: { type: 'object' as const, properties: { timezone: { type: 'string' as const, description: 'IANA timezone (default: system timezone)', }, format: { type: 'string' as const, description: 'date-fns format string' }, include_offset: { type: 'boolean' as const, description: 'Include UTC offset (default: true)', }, }, }, }, { name: 'convert_timezone', description: 'Convert time between timezones', inputSchema: { type: 'object' as const, properties: { time: { type: 'string' as const, description: 'Input time' }, from_timezone: { type: 'string' as const, description: 'Source IANA timezone' }, to_timezone: { type: 'string' as const, description: 'Target IANA timezone' }, format: { type: 'string' as const, description: 'Output format' }, }, required: ['time', 'from_timezone', 'to_timezone'], }, }, { name: 'add_time', description: 'Add duration to a date/time', inputSchema: { type: 'object' as const, properties: { time: { type: 'string' as const, description: 'Base time' }, amount: { type: 'number' as const, description: 'Amount to add' }, unit: { type: 'string' as const, enum: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], description: 'Unit of time', }, timezone: { type: 'string' as const, description: 'Timezone for calculation (default: system timezone)', }, }, required: ['time', 'amount', 'unit'], }, }, { name: 'subtract_time', description: 'Subtract duration from a date/time', inputSchema: { type: 'object' as const, properties: { time: { type: 'string' as const, description: 'Base time' }, amount: { type: 'number' as const, description: 'Amount to subtract' }, unit: { type: 'string' as const, enum: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], description: 'Unit of time', }, timezone: { type: 'string' as const, description: 'Timezone for calculation (default: system timezone)', }, }, required: ['time', 'amount', 'unit'], }, }, { name: 'calculate_duration', description: 'Calculate duration between two times', inputSchema: { type: 'object' as const, properties: { start_time: { type: 'string' as const, description: 'Start time' }, end_time: { type: 'string' as const, description: 'End time' }, unit: { type: 'string' as const, description: 'Output unit (default: "auto")' }, timezone: { type: 'string' as const, description: 'Timezone for parsing (default: system timezone)', }, }, required: ['start_time', 'end_time'], }, }, { name: 'get_business_days', description: 'Calculate business days between dates', inputSchema: { type: 'object' as const, properties: { start_date: { type: 'string' as const, description: 'Start date' }, end_date: { type: 'string' as const, description: 'End date' }, exclude_weekends: { type: 'boolean' as const, description: 'Exclude weekends (default: true)', }, holidays: { type: 'array' as const, items: { type: 'string' as const }, description: 'Array of holiday dates', }, timezone: { type: 'string' as const, description: 'Timezone for calculation (default: system timezone)', }, }, required: ['start_date', 'end_date'], }, }, { name: 'next_occurrence', description: 'Find next occurrence of a recurring event', inputSchema: { type: 'object' as const, properties: { pattern: { type: 'string' as const, enum: ['daily', 'weekly', 'monthly', 'yearly'], description: 'Recurrence pattern', }, start_from: { type: 'string' as const, description: 'Start searching from' }, day_of_week: { type: 'number' as const, description: 'For weekly (0-6, 0=Sunday)' }, day_of_month: { type: 'number' as const, description: 'For monthly (1-31)' }, time: { type: 'string' as const, description: 'Time in HH:mm format' }, timezone: { type: 'string' as const, description: 'Timezone for calculation (default: system timezone)', }, }, required: ['pattern'], }, }, { name: 'format_time', description: 'Format time in various human-readable formats', inputSchema: { type: 'object' as const, properties: { time: { type: 'string' as const, description: 'Time to format' }, format: { type: 'string' as const, enum: ['relative', 'calendar', 'custom'], description: 'Format type', }, custom_format: { type: 'string' as const, description: 'For custom format' }, timezone: { type: 'string' as const, description: 'Timezone for display (default: system timezone)', }, }, required: ['time', 'format'], }, }, { name: 'calculate_business_hours', description: 'Calculate business hours between two times', inputSchema: { type: 'object' as const, properties: { start_time: { type: 'string' as const, description: 'Start time' }, end_time: { type: 'string' as const, description: 'End time' }, business_hours: { type: 'object' as const, description: 'Business hours definition (default: 9 AM - 5 PM)', }, timezone: { type: 'string' as const, description: 'Timezone for calculation (default: system timezone)', }, holidays: { type: 'array' as const, items: { type: 'string' as const }, description: 'Array of holiday dates', }, include_weekends: { type: 'boolean' as const, description: 'Include weekends in calculation (default: false)', }, }, required: ['start_time', 'end_time'], }, }, { name: 'days_until', description: 'Calculate days until a target date/event', inputSchema: { type: 'object' as const, properties: { target_date: { type: ['string', 'number'] as const, description: 'Target date (ISO string, natural language, or Unix timestamp)', }, timezone: { type: 'string' as const, description: 'Timezone for calculation (default: system timezone)', }, format_result: { type: 'boolean' as const, description: 'Return formatted string (e.g., "in 5 days") instead of number (default: false)', }, }, required: ['target_date'], }, }, ]; // Tool function mapping - wrapping each function to handle unknown params const TOOL_FUNCTIONS: Record<string, (params: unknown) => unknown> = { get_server_info: (params: unknown) => getServerInfo(params), get_current_time: (params: unknown) => getCurrentTime(params as Parameters<typeof getCurrentTime>[0]), convert_timezone: (params: unknown) => convertTimezone(params as Parameters<typeof convertTimezone>[0]), add_time: (params: unknown) => addTime(params as Parameters<typeof addTime>[0]), subtract_time: (params: unknown) => subtractTime(params as Parameters<typeof subtractTime>[0]), calculate_duration: (params: unknown) => calculateDuration(params as Parameters<typeof calculateDuration>[0]), get_business_days: (params: unknown) => getBusinessDays(params as Parameters<typeof getBusinessDays>[0]), next_occurrence: (params: unknown) => nextOccurrence(params as Parameters<typeof nextOccurrence>[0]), format_time: (params: unknown) => formatTime(params as Parameters<typeof formatTime>[0]), calculate_business_hours: (params: unknown) => calculateBusinessHours(params as Parameters<typeof calculateBusinessHours>[0]), days_until: (params: unknown) => daysUntil(params as Parameters<typeof daysUntil>[0]), }; // Create the MCP server instance export function createServer(): Server { debug.server('Creating MCP server instance'); return new Server( { name: 'mcp-time-server-node', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); } // Check rate limit and return appropriate response export function handleRateLimit( rateLimiter: SlidingWindowRateLimiter ): | { limited: false } | { limited: true; error: { code: number; message: string; data?: unknown } } { if (!rateLimiter.checkLimit()) { debug.rateLimit('Rate limit exceeded'); const retryAfter = rateLimiter.getRetryAfter(); const info = rateLimiter.getInfo(); return { limited: true, error: { code: -32000, // JSON-RPC server-defined error message: 'Rate limit exceeded', data: { limit: info.limit, window: info.window, retryAfter: retryAfter, }, }, }; } debug.rateLimit('Rate limit check passed'); return { limited: false }; } // Execute a tool function and format the result // eslint-disable-next-line max-lines-per-function /** * Helper function to format tool execution errors * Now uses our adapter to properly map errors to MCP format */ function formatToolError( error: unknown, toolName: string ): { error: { code: number; message: string; data?: unknown } } { debug.trace('Tool %s execution failed: %O', toolName, error); // If it's already an McpError, return it in the expected format if (error instanceof McpError) { return { error: { code: error.code, message: error.message, data: error.data, }, }; } // Map any other error to McpError using our adapter const mcpError = mapToMcpError(error, toolName); return { error: { code: mcpError.code, message: mcpError.message, data: mcpError.data, }, }; } export async function executeToolFunction( name: string, args: unknown ): Promise< | { content: Array<{ type: string; text: string }> } | { error: { code: number; message: string; data?: unknown } } > { debug.trace('Executing tool: %s with args: %O', name, args); try { // Get the tool function - validate against known tools if (!Object.prototype.hasOwnProperty.call(TOOL_FUNCTIONS, name)) { debug.error('Unknown tool: %s', name); throw new Error(`Unknown tool: ${name}`); } // eslint-disable-next-line security/detect-object-injection -- Tool name validated above const toolFunction = TOOL_FUNCTIONS[name]; if (!toolFunction) { debug.error('Tool function is null for: %s', name); throw new Error(`Unknown tool: ${name}`); } // Execute the tool const result = await toolFunction(args); debug.trace('Tool %s executed successfully', name); // Return the result return { content: [ { type: 'text', text: JSON.stringify(result), }, ], }; } catch (error: unknown) { return formatToolError(error, name); } } // Handle a tool call request export async function handleToolCall( request: CallToolRequest, rateLimiter: SlidingWindowRateLimiter ): Promise< | { content: Array<{ type: string; text: string }> } | { error: { code: number; message: string; data?: unknown } } > { debug.server('Handling tool call: %s', request.params.name); // Check rate limit const rateLimitResult = handleRateLimit(rateLimiter); if (rateLimitResult.limited) { return { error: rateLimitResult.error }; } const { name, arguments: args } = request.params; return executeToolFunction(name, args); } // Register all request handlers export function registerHandlers(server: Server, rateLimiter: SlidingWindowRateLimiter): void { debug.server('Registering request handlers'); // Register tools/list handler server.setRequestHandler(ListToolsRequestSchema, () => { debug.server('Handling tools/list request'); return Promise.resolve({ tools: TOOL_DEFINITIONS, }); }); // Register tools/call handler server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => { return handleToolCall(request, rateLimiter); }); } // Main function - now simple orchestration async function main(): Promise<void> { debug.server('Starting MCP Time Server...'); const rateLimiter = new SlidingWindowRateLimiter(); const server = createServer(); registerHandlers(server, rateLimiter); const transport = new StdioServerTransport(); await server.connect(transport); debug.server('MCP Time Server connected to stdio transport'); console.error('MCP Time Server Node running on stdio'); } // Only run the server if this is the main module if (require.main === module) { main().catch((error) => { console.error('Fatal error:', error); process.exit(1); }); }

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