Skip to main content
Glama

MCP Time Server

by jharkins
server.ts11.2 kB
/* --------------------------------------------------------------------------- * server.ts ― MCP Time Server with Streamable HTTP + legacy HTTP+SSE * ------------------------------------------------------------------------- */ import express, { Request, Response } from "express"; import { randomUUID } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { DateTime, Settings, IANAZone } from 'luxon'; /* --------------------------------------------------------------------------- * Time Handling Logic and Types * ------------------------------------------------------------------------- */ interface TimeResult { timezone: string; datetime: string; } interface TimeConversionResult { source: TimeResult; target: TimeResult; time_difference: string; } function getLocalTimezone(): string { // Luxon gets the local system timezone return Settings.defaultZone.name; } // Helper to validate IANA timezone names (basic check) function isValidTimezone(tz: string): boolean { // Use Luxon's built-in validator return IANAZone.isValidZone(tz); } function getCurrentTime(timezoneName: string): TimeResult { if (!isValidTimezone(timezoneName)) { throw new Error(`Invalid timezone: ${timezoneName}`); } const nowInZone = DateTime.now().setZone(timezoneName); return { timezone: timezoneName, datetime: nowInZone.toISO({ includeOffset: true, suppressMilliseconds: true }) ?? 'Invalid Date', }; } function convertTime( sourceTz: string, timeStr: string, targetTz: string ): TimeConversionResult { if (!isValidTimezone(sourceTz)) { throw new Error(`Invalid source timezone: ${sourceTz}`); } if (!isValidTimezone(targetTz)) { throw new Error(`Invalid target timezone: ${targetTz}`); } // Parse time string (HH:mm) using Luxon const parsedTime = DateTime.fromFormat(timeStr, 'HH:mm'); if (!parsedTime.isValid) { throw new Error("Invalid time format. Expected HH:MM [24-hour format]"); } // Create DateTime object for source time today in the source timezone const sourceDt = DateTime.now() .setZone(sourceTz) .set({ hour: parsedTime.hour, minute: parsedTime.minute, second: 0, millisecond: 0 }); // Convert to the target timezone const targetDt = sourceDt.setZone(targetTz); // Calculate time difference // Luxon handles offsets directly. Get difference in minutes and format. const offsetDiffMinutes = targetDt.offset - sourceDt.offset; const offsetDiffHours = offsetDiffMinutes / 60; let timeDiffStr: string; if (Number.isInteger(offsetDiffHours)) { timeDiffStr = `${offsetDiffHours >= 0 ? '+' : ''}${offsetDiffHours}h`; } else { // Format fractional hours (e.g., +5.75h for +5:45) timeDiffStr = `${offsetDiffHours >= 0 ? '+' : ''}${offsetDiffHours.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1')}h`; } return { source: { timezone: sourceTz, datetime: sourceDt.toISO({ includeOffset: true, suppressMilliseconds: true }) ?? 'Invalid Date', }, target: { timezone: targetTz, datetime: targetDt.toISO({ includeOffset: true, suppressMilliseconds: true }) ?? 'Invalid Date', }, time_difference: timeDiffStr, }; } /* --------------------------------------------------------------------------- * 1. Build the MCP server instance with Time tools * ------------------------------------------------------------------------- */ function buildMcpServer(): McpServer { console.log("[Server] Building new McpServer instance for a connection"); const server = new McpServer({ name: "mcp-time-srv", version: "1.0.0" }); const localTz = getLocalTimezone(); // ── Tool: get_current_time ─────────────────────────────────────────────── server.tool( "get_current_time", { timezone: z.string().optional().describe(`IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Defaults to server local: ${localTz}`) }, async ({ timezone }) => { const effectiveTimezone = timezone || localTz; console.log(`[Server] Handling tool call: get_current_time for timezone='${effectiveTimezone}'`); // Log effective TZ try { const result = getCurrentTime(effectiveTimezone); // Return result as JSON string in text content return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { console.error("[Server] Error in get_current_time:", error); let helpfulMessage = `Error processing get_current_time: ${error.message}`; if (error.message?.includes("Invalid timezone")) { helpfulMessage = `Error: Invalid timezone specified ('${effectiveTimezone}'). Please provide a valid IANA timezone name (e.g., 'America/New_York', 'Europe/London').`; } // Return helpful error as text content return { content: [{ type: "text", text: helpfulMessage }] }; } } ); // ── Tool: convert_time ─────────────────────────────────────────────────── server.tool( "convert_time", { source_timezone: z.string().optional().describe(`Source IANA timezone name. Defaults to server local: ${localTz}`), time: z.string().regex(/^\d{2}:\d{2}$/, "Expected HH:MM [24-hour format]").describe("Time to convert (HH:MM)"), target_timezone: z.string().optional().describe(`Target IANA timezone name. Defaults to server local: ${localTz}`), }, async ({ source_timezone, time, target_timezone }) => { const effectiveSourceTz = source_timezone || localTz; const effectiveTargetTz = target_timezone || localTz; console.log(`[Server] Handling tool call: convert_time from ${effectiveSourceTz} '${time}' to ${effectiveTargetTz}`); try { const result = convertTime( effectiveSourceTz, time, effectiveTargetTz ); // Return result as JSON string in text content return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { console.error("[Server] Error in convert_time:", error); let helpfulMessage = `Error processing convert_time: ${error.message}`; if (error.message?.includes("Invalid source timezone")) { helpfulMessage = `Error: Invalid source timezone specified ('${effectiveSourceTz}'). Please provide a valid IANA timezone name.`; } else if (error.message?.includes("Invalid target timezone")) { helpfulMessage = `Error: Invalid target timezone specified ('${effectiveTargetTz}'). Please provide a valid IANA timezone name.`; } else if (error.message?.includes("Invalid time format")) { helpfulMessage = `Error: Invalid time format specified ('${time}'). Please use 24-hour HH:MM format (e.g., '14:30').`; } return { content: [{ type: "text", text: helpfulMessage }] }; } } ); return server; } /* --------------------------------------------------------------------------- * 2. Transport registries – keep track of active sessions * ------------------------------------------------------------------------- */ const streamableTransports: Record<string, StreamableHTTPServerTransport> = {}; const sseTransports: Record<string, SSEServerTransport> = {}; /* --------------------------------------------------------------------------- * 3. Express wiring * ------------------------------------------------------------------------- */ const app = express(); app.use(express.json()); /* ---------- 3-A: modern Streamable HTTP endpoint -------------------------- */ app.all("/mcp", async (req: Request, res: Response) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; let transport: StreamableHTTPServerTransport; if (sessionId && streamableTransports[sessionId]) { console.log(`[Server] Reusing Streamable HTTP transport for session: ${sessionId}`); transport = streamableTransports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { console.log("[Server] Creating new Streamable HTTP transport"); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: id => { streamableTransports[id] = transport; console.log(`[Server] Streamable HTTP session initialized: ${id}`); } }); transport.onclose = () => { if (transport.sessionId) { console.log(`[Server] Streamable HTTP transport closed for session: ${transport.sessionId}`); delete streamableTransports[transport.sessionId]; } }; const server = buildMcpServer(); await server.connect(transport); } else { console.warn("[Server] Invalid Streamable HTTP handshake request"); res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: invalid MCP handshake" }, id: null }); return; } await transport.handleRequest(req, res, req.body); }); /* ---------- 3-B: legacy SSE compatibility -------------------------------- */ app.get("/sse", async (req: Request, res: Response) => { console.log("[Server] Received request for SSE connection"); const transport = new SSEServerTransport("/messages", res); sseTransports[transport.sessionId] = transport; console.log(`[Server] SSE transport created with sessionId: ${transport.sessionId}`); res.on("close", () => { console.log(`[Server] SSE connection closed for sessionId: ${transport.sessionId}`); delete sseTransports[transport.sessionId]; }); const server = buildMcpServer(); await server.connect(transport); }); app.post("/messages", async (req: Request, res: Response) => { const sessionId = req.query.sessionId as string; const transport = sseTransports[sessionId]; console.log(`[Server] Received POST /messages for SSE sessionId: ${sessionId}`); if (!transport) { console.warn(`[Server] Unknown or expired SSE sessionId: ${sessionId}`); res.status(400).send("Unknown or expired sessionId"); return; } await transport.handlePostMessage(req, res, req.body); }); /* --------------------------------------------------------------------------- * 4. Startup * ------------------------------------------------------------------------- */ const PORT = Number(process.env.PORT ?? 3000); const serverInstance = app.listen(PORT, () => { console.log(`MCP Time server listening on http://localhost:${PORT}`); }); serverInstance.on('error', (error) => { console.error("Server listening error:", error); process.exit(1); // Exit if listening fails critically });

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/jharkins/mcp-time-srv'

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