mcp-datetime 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -0
  3. package/index.js +535 -0
  4. package/package.json +40 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hong Teoh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # mcp-datetime
2
+
3
+ MCP server with **10 date & time tools** for LLMs. Timezone conversion, date math, cron explanation, business days, Unix timestamps, and more. Zero dependencies beyond the MCP SDK.
4
+
5
+ ## Tools
6
+
7
+ | Tool | Description |
8
+ |------|-------------|
9
+ | `now` | Current time in multiple timezones |
10
+ | `convert_timezone` | Convert between 60+ timezones (IANA, abbreviations, UTC offsets) |
11
+ | `date_math` | Add/subtract years, months, days, hours, minutes, seconds |
12
+ | `date_diff` | Difference between dates in days, hours, weeks, business days |
13
+ | `format_date` | Format dates with custom tokens (YYYY-MM-DD HH:mm:ss) |
14
+ | `unix_timestamp` | Convert between Unix timestamps and ISO 8601 |
15
+ | `business_days` | Count or add business days (excludes weekends) |
16
+ | `explain_cron` | Explain cron expressions in plain English |
17
+ | `countdown` | Time remaining/elapsed to a target date |
18
+ | `date_info` | Day of week, ISO week, quarter, leap year, day of year |
19
+
20
+ ## Quick Start
21
+
22
+ ### Claude Desktop
23
+
24
+ Add to `claude_desktop_config.json`:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "datetime": {
30
+ "command": "npx",
31
+ "args": ["-y", "mcp-datetime"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Cursor / VS Code
38
+
39
+ Add to MCP settings:
40
+
41
+ ```json
42
+ {
43
+ "mcp": {
44
+ "servers": {
45
+ "datetime": {
46
+ "command": "npx",
47
+ "args": ["-y", "mcp-datetime"]
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Supported Timezones
55
+
56
+ 60+ timezones including:
57
+ - **Abbreviations**: UTC, GMT, EST, EDT, CST, CDT, MST, MDT, PST, PDT, CET, CEST, IST, JST, KST, SGT, MYT, AEST, NZST, and more
58
+ - **IANA names**: Asia/Tokyo, America/New_York, Europe/London, Australia/Sydney, etc.
59
+ - **UTC offsets**: +05:30, -08:00, +09, etc.
60
+
61
+ ## Examples
62
+
63
+ **"What time is it in Tokyo and New York?"**
64
+ → Uses `now` with `["JST", "EST"]`
65
+
66
+ **"Add 90 business days to March 1, 2026"**
67
+ → Uses `business_days` with `operation: "add"`
68
+
69
+ **"What does `0 */6 * * *` mean?"**
70
+ → Uses `explain_cron` → "Runs at minute 0 of every 6 hours"
71
+
72
+ **"How many days until December 25?"**
73
+ → Uses `countdown` with target date
74
+
75
+ ## Part of the MCP Tools Suite
76
+
77
+ - [mcp-devutils](https://www.npmjs.com/package/mcp-devutils) — 8 developer utility tools
78
+ - [mcp-apitools](https://www.npmjs.com/package/mcp-apitools) — 7 API & HTTP tools
79
+ - [mcp-texttools](https://www.npmjs.com/package/mcp-texttools) — 8 text processing tools
80
+ - [mcp-mathtools](https://www.npmjs.com/package/mcp-mathtools) — 12 math & statistics tools
81
+ - **mcp-datetime** — 10 date & time tools ← you are here
82
+ - [readmecraft](https://www.npmjs.com/package/readmecraft) — README generator
83
+
84
+ **55+ MCP tools** across 6 packages. Install any combination you need.
85
+
86
+ ## License
87
+
88
+ MIT
89
+
90
+ ---
91
+
92
+ [☕ Buy me a coffee](https://buymeacoffee.com/gl89tu25lp)
package/index.js ADDED
@@ -0,0 +1,535 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+
6
+ const server = new Server(
7
+ { name: "mcp-datetime", version: "1.0.0" },
8
+ { capabilities: { tools: {} } }
9
+ );
10
+
11
+ // ── Timezone offset database (minutes from UTC) ──
12
+ const TZ_OFFSETS = {
13
+ UTC: 0, GMT: 0,
14
+ EST: -300, EDT: -240, CST: -360, CDT: -300, MST: -420, MDT: -360, PST: -480, PDT: -420,
15
+ "US/Eastern": -300, "US/Central": -360, "US/Mountain": -420, "US/Pacific": -480,
16
+ CET: 60, CEST: 120, EET: 120, EEST: 180, WET: 0, WEST: 60,
17
+ IST: 330, JST: 540, KST: 540, CST_ASIA: 480, HKT: 480, SGT: 480, MYT: 480,
18
+ AEST: 600, AEDT: 660, ACST: 570, ACDT: 630, AWST: 480, NZST: 720, NZDT: 780,
19
+ BRT: -180, ART: -180, CLT: -240, COT: -300, PET: -300,
20
+ WAT: 60, CAT: 120, EAT: 180, SAST: 120,
21
+ GST: 240, PKT: 300, ICT: 420, WIB: 420,
22
+ "Asia/Tokyo": 540, "Asia/Seoul": 540, "Asia/Shanghai": 480, "Asia/Singapore": 480,
23
+ "Asia/Kuala_Lumpur": 480, "Asia/Kolkata": 330, "Asia/Dubai": 240, "Asia/Bangkok": 420,
24
+ "Europe/London": 0, "Europe/Paris": 60, "Europe/Berlin": 60, "Europe/Moscow": 180,
25
+ "America/New_York": -300, "America/Chicago": -360, "America/Denver": -420,
26
+ "America/Los_Angeles": -480, "America/Sao_Paulo": -180, "America/Toronto": -300,
27
+ "Australia/Sydney": 600, "Australia/Melbourne": 600, "Australia/Perth": 480,
28
+ "Pacific/Auckland": 720, "Pacific/Honolulu": -600,
29
+ };
30
+
31
+ function getTzOffset(tz) {
32
+ const key = tz.replace(/\s+/g, "_");
33
+ if (key in TZ_OFFSETS) return TZ_OFFSETS[key];
34
+ // Try case-insensitive
35
+ const upper = key.toUpperCase();
36
+ for (const [k, v] of Object.entries(TZ_OFFSETS)) {
37
+ if (k.toUpperCase() === upper) return v;
38
+ }
39
+ // Try parsing ±HH:MM or ±HHMM
40
+ const m = key.match(/^([+-])(\d{1,2}):?(\d{2})?$/);
41
+ if (m) return (m[1] === "-" ? -1 : 1) * (parseInt(m[2]) * 60 + parseInt(m[3] || "0"));
42
+ return null;
43
+ }
44
+
45
+ function formatDate(d, fmt) {
46
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
47
+ const tokens = {
48
+ YYYY: d.getUTCFullYear(),
49
+ MM: pad(d.getUTCMonth() + 1),
50
+ DD: pad(d.getUTCDate()),
51
+ HH: pad(d.getUTCHours()),
52
+ mm: pad(d.getUTCMinutes()),
53
+ ss: pad(d.getUTCSeconds()),
54
+ SSS: pad(d.getUTCMilliseconds(), 3),
55
+ };
56
+ let result = fmt;
57
+ for (const [tok, val] of Object.entries(tokens)) {
58
+ result = result.replace(new RegExp(tok, "g"), String(val));
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function parseFlexibleDate(input) {
64
+ if (!input || input.toLowerCase() === "now") return new Date();
65
+ const d = new Date(input);
66
+ if (!isNaN(d.getTime())) return d;
67
+ // Try Unix timestamp (seconds)
68
+ const num = Number(input);
69
+ if (!isNaN(num)) {
70
+ return num > 1e12 ? new Date(num) : new Date(num * 1000);
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function getISOWeek(d) {
76
+ const date = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
77
+ date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay() || 7));
78
+ const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
79
+ const weekNo = Math.ceil(((date - yearStart) / 86400000 + 1) / 7);
80
+ return { year: date.getUTCFullYear(), week: weekNo };
81
+ }
82
+
83
+ function isWeekend(d) {
84
+ const day = d.getUTCDay();
85
+ return day === 0 || day === 6;
86
+ }
87
+
88
+ const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
89
+ const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
90
+
91
+ // ── Cron field parser ──
92
+ function describeCronField(field, names) {
93
+ if (field === "*") return "every";
94
+ if (field.includes("/")) {
95
+ const [base, step] = field.split("/");
96
+ return `every ${step}${base !== "*" ? ` starting at ${names ? names[+base] || base : base}` : ""}`;
97
+ }
98
+ if (field.includes(",")) {
99
+ return field.split(",").map(v => names ? names[+v] || v : v).join(", ");
100
+ }
101
+ if (field.includes("-")) {
102
+ const [a, b] = field.split("-");
103
+ return `${names ? names[+a] || a : a} through ${names ? names[+b] || b : b}`;
104
+ }
105
+ return names ? names[+field] || field : field;
106
+ }
107
+
108
+ function explainCron(expr) {
109
+ const parts = expr.trim().split(/\s+/);
110
+ if (parts.length < 5 || parts.length > 6) return "Invalid cron expression (expected 5 or 6 fields)";
111
+ const [minute, hour, dom, month, dow] = parts;
112
+ const monthNames = { 1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr", 5: "May", 6: "Jun", 7: "Jul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Nov", 12: "Dec" };
113
+ const dayNames = { 0: "Sun", 1: "Mon", 2: "Tue", 3: "Wed", 4: "Thu", 5: "Fri", 6: "Sat", 7: "Sun" };
114
+
115
+ const lines = [];
116
+ lines.push(`Minute: ${describeCronField(minute)}`);
117
+ lines.push(`Hour: ${describeCronField(hour)}`);
118
+ lines.push(`Day of month: ${describeCronField(dom)}`);
119
+ lines.push(`Month: ${describeCronField(month, monthNames)}`);
120
+ lines.push(`Day of week: ${describeCronField(dow, dayNames)}`);
121
+
122
+ // Human summary
123
+ let summary = "Runs ";
124
+ if (minute === "*" && hour === "*") summary += "every minute";
125
+ else if (minute === "0" && hour === "*") summary += "every hour";
126
+ else if (minute !== "*" && hour !== "*" && !hour.includes("/") && !hour.includes(",")) {
127
+ summary += `at ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
128
+ } else {
129
+ summary += `at minute ${describeCronField(minute)} of hour ${describeCronField(hour)}`;
130
+ }
131
+ if (dom !== "*") summary += `, on day ${describeCronField(dom)} of the month`;
132
+ if (month !== "*") summary += `, in ${describeCronField(month, monthNames)}`;
133
+ if (dow !== "*") summary += `, on ${describeCronField(dow, dayNames)}`;
134
+ lines.push(`\nSummary: ${summary}`);
135
+ return lines.join("\n");
136
+ }
137
+
138
+ // ── Tool definitions ──
139
+ const tools = [
140
+ {
141
+ name: "now",
142
+ description: "Get the current date and time in one or more timezones. Returns ISO 8601, Unix timestamp, and human-readable format.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ timezones: {
147
+ type: "array", items: { type: "string" },
148
+ description: "Timezones to show (e.g. ['UTC', 'PST', 'Asia/Tokyo']). Default: ['UTC']"
149
+ }
150
+ }
151
+ }
152
+ },
153
+ {
154
+ name: "convert_timezone",
155
+ description: "Convert a date/time from one timezone to another.",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ datetime: { type: "string", description: "Date/time string (ISO 8601 or flexible). Use 'now' for current time." },
160
+ from_tz: { type: "string", description: "Source timezone (e.g. 'PST', 'Asia/Tokyo', '+05:30')" },
161
+ to_tz: { type: "string", description: "Target timezone" }
162
+ },
163
+ required: ["datetime", "from_tz", "to_tz"]
164
+ }
165
+ },
166
+ {
167
+ name: "date_math",
168
+ description: "Add or subtract time from a date. Supports years, months, days, hours, minutes, seconds.",
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ date: { type: "string", description: "Starting date (ISO 8601 or 'now')" },
173
+ add: {
174
+ type: "object",
175
+ description: "Time to add (use negative values to subtract)",
176
+ properties: {
177
+ years: { type: "number" }, months: { type: "number" }, days: { type: "number" },
178
+ hours: { type: "number" }, minutes: { type: "number" }, seconds: { type: "number" }
179
+ }
180
+ }
181
+ },
182
+ required: ["date", "add"]
183
+ }
184
+ },
185
+ {
186
+ name: "date_diff",
187
+ description: "Calculate the difference between two dates in multiple units (days, hours, minutes, seconds, weeks, business days).",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ from: { type: "string", description: "Start date (ISO 8601 or 'now')" },
192
+ to: { type: "string", description: "End date (ISO 8601 or 'now')" }
193
+ },
194
+ required: ["from", "to"]
195
+ }
196
+ },
197
+ {
198
+ name: "format_date",
199
+ description: "Format a date/time using a format string. Tokens: YYYY, MM, DD, HH, mm, ss, SSS.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ date: { type: "string", description: "Date to format (ISO 8601, unix timestamp, or 'now')" },
204
+ format: { type: "string", description: "Format string, e.g. 'YYYY-MM-DD HH:mm:ss'" }
205
+ },
206
+ required: ["date", "format"]
207
+ }
208
+ },
209
+ {
210
+ name: "unix_timestamp",
211
+ description: "Convert between Unix timestamps and ISO 8601 dates. Pass a timestamp to get ISO, or a date to get timestamp.",
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ value: { type: "string", description: "Unix timestamp (seconds or ms) OR ISO date string OR 'now'" }
216
+ },
217
+ required: ["value"]
218
+ }
219
+ },
220
+ {
221
+ name: "business_days",
222
+ description: "Calculate business days between two dates, or add N business days to a date. Excludes weekends (Sat/Sun).",
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {
226
+ operation: { type: "string", enum: ["between", "add"], description: "'between': count business days. 'add': add N business days to start." },
227
+ start: { type: "string", description: "Start date" },
228
+ end: { type: "string", description: "End date (for 'between' operation)" },
229
+ days: { type: "number", description: "Number of business days to add (for 'add' operation)" }
230
+ },
231
+ required: ["operation", "start"]
232
+ }
233
+ },
234
+ {
235
+ name: "explain_cron",
236
+ description: "Explain a cron expression in plain English. Supports standard 5-field cron format (minute hour dom month dow).",
237
+ inputSchema: {
238
+ type: "object",
239
+ properties: {
240
+ expression: { type: "string", description: "Cron expression, e.g. '0 9 * * 1-5' or '*/15 * * * *'" }
241
+ },
242
+ required: ["expression"]
243
+ }
244
+ },
245
+ {
246
+ name: "countdown",
247
+ description: "Calculate time remaining until a target date, or time elapsed since a past date.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ target: { type: "string", description: "Target date (ISO 8601)" },
252
+ from: { type: "string", description: "Reference date (default: now)" }
253
+ },
254
+ required: ["target"]
255
+ }
256
+ },
257
+ {
258
+ name: "date_info",
259
+ description: "Get detailed information about a date: day of week, ISO week number, day of year, leap year, quarter, and more.",
260
+ inputSchema: {
261
+ type: "object",
262
+ properties: {
263
+ date: { type: "string", description: "Date to analyze (ISO 8601 or 'now')" }
264
+ },
265
+ required: ["date"]
266
+ }
267
+ }
268
+ ];
269
+
270
+ // ── Tool handlers ──
271
+ function handleNow(args) {
272
+ const now = new Date();
273
+ const zones = args.timezones || ["UTC"];
274
+ const results = {};
275
+ for (const tz of zones) {
276
+ const offset = getTzOffset(tz);
277
+ if (offset === null) { results[tz] = "Unknown timezone"; continue; }
278
+ const local = new Date(now.getTime() + offset * 60000);
279
+ results[tz] = {
280
+ iso: local.toISOString().replace("Z", offset === 0 ? "Z" : `${offset >= 0 ? "+" : "-"}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0")}:${String(Math.abs(offset) % 60).padStart(2, "0")}`),
281
+ human: `${DAY_NAMES[local.getUTCDay()]}, ${MONTH_NAMES[local.getUTCMonth()]} ${local.getUTCDate()}, ${local.getUTCFullYear()} ${String(local.getUTCHours()).padStart(2, "0")}:${String(local.getUTCMinutes()).padStart(2, "0")}:${String(local.getUTCSeconds()).padStart(2, "0")}`,
282
+ unix: Math.floor(now.getTime() / 1000),
283
+ offset_hours: offset / 60
284
+ };
285
+ }
286
+ return results;
287
+ }
288
+
289
+ function handleConvertTimezone(args) {
290
+ const fromOff = getTzOffset(args.from_tz);
291
+ const toOff = getTzOffset(args.to_tz);
292
+ if (fromOff === null) return { error: `Unknown source timezone: ${args.from_tz}` };
293
+ if (toOff === null) return { error: `Unknown target timezone: ${args.to_tz}` };
294
+
295
+ const d = parseFlexibleDate(args.datetime);
296
+ if (!d) return { error: `Cannot parse date: ${args.datetime}` };
297
+
298
+ // Convert: input is in from_tz, so UTC = input - fromOff, then target = UTC + toOff
299
+ const utcMs = d.getTime() - fromOff * 60000;
300
+ const targetMs = utcMs + toOff * 60000;
301
+ const target = new Date(targetMs);
302
+
303
+ return {
304
+ original: { datetime: d.toISOString(), timezone: args.from_tz },
305
+ converted: {
306
+ datetime: formatDate(target, "YYYY-MM-DDTHH:mm:ss"),
307
+ timezone: args.to_tz,
308
+ day: DAY_NAMES[target.getUTCDay()]
309
+ },
310
+ offset_difference_hours: (toOff - fromOff) / 60
311
+ };
312
+ }
313
+
314
+ function handleDateMath(args) {
315
+ const d = parseFlexibleDate(args.date);
316
+ if (!d) return { error: `Cannot parse date: ${args.date}` };
317
+ const a = args.add || {};
318
+
319
+ const result = new Date(d);
320
+ if (a.years) result.setUTCFullYear(result.getUTCFullYear() + a.years);
321
+ if (a.months) result.setUTCMonth(result.getUTCMonth() + a.months);
322
+ if (a.days) result.setUTCDate(result.getUTCDate() + a.days);
323
+ if (a.hours) result.setUTCHours(result.getUTCHours() + a.hours);
324
+ if (a.minutes) result.setUTCMinutes(result.getUTCMinutes() + a.minutes);
325
+ if (a.seconds) result.setUTCSeconds(result.getUTCSeconds() + a.seconds);
326
+
327
+ return {
328
+ original: d.toISOString(),
329
+ added: a,
330
+ result: result.toISOString(),
331
+ day: DAY_NAMES[result.getUTCDay()]
332
+ };
333
+ }
334
+
335
+ function handleDateDiff(args) {
336
+ const from = parseFlexibleDate(args.from);
337
+ const to = parseFlexibleDate(args.to);
338
+ if (!from) return { error: `Cannot parse 'from' date: ${args.from}` };
339
+ if (!to) return { error: `Cannot parse 'to' date: ${args.to}` };
340
+
341
+ const diffMs = to.getTime() - from.getTime();
342
+ const diffSec = diffMs / 1000;
343
+ const diffMin = diffSec / 60;
344
+ const diffHr = diffMin / 60;
345
+ const diffDays = diffHr / 24;
346
+
347
+ // Count business days
348
+ let bizDays = 0;
349
+ const step = diffMs >= 0 ? 1 : -1;
350
+ const start = new Date(Math.min(from.getTime(), to.getTime()));
351
+ const end = new Date(Math.max(from.getTime(), to.getTime()));
352
+ const cursor = new Date(start);
353
+ while (cursor < end) {
354
+ if (!isWeekend(cursor)) bizDays++;
355
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
356
+ }
357
+ if (diffMs < 0) bizDays = -bizDays;
358
+
359
+ return {
360
+ from: from.toISOString(),
361
+ to: to.toISOString(),
362
+ difference: {
363
+ days: Math.round(diffDays * 100) / 100,
364
+ hours: Math.round(diffHr * 100) / 100,
365
+ minutes: Math.round(diffMin),
366
+ seconds: Math.round(diffSec),
367
+ weeks: Math.round(diffDays / 7 * 100) / 100,
368
+ business_days: bizDays
369
+ }
370
+ };
371
+ }
372
+
373
+ function handleFormatDate(args) {
374
+ const d = parseFlexibleDate(args.date);
375
+ if (!d) return { error: `Cannot parse date: ${args.date}` };
376
+ return { original: d.toISOString(), formatted: formatDate(d, args.format), format: args.format };
377
+ }
378
+
379
+ function handleUnixTimestamp(args) {
380
+ const val = args.value;
381
+ // Try as number (unix timestamp)
382
+ const num = Number(val);
383
+ if (!isNaN(num) && val !== "" && val.toLowerCase() !== "now") {
384
+ const isMs = num > 1e12;
385
+ const ms = isMs ? num : num * 1000;
386
+ const d = new Date(ms);
387
+ return {
388
+ input: val,
389
+ interpreted_as: isMs ? "milliseconds" : "seconds",
390
+ unix_seconds: Math.floor(ms / 1000),
391
+ unix_milliseconds: ms,
392
+ iso: d.toISOString(),
393
+ human: `${DAY_NAMES[d.getUTCDay()]}, ${MONTH_NAMES[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")} UTC`
394
+ };
395
+ }
396
+ // Try as date string
397
+ const d = parseFlexibleDate(val);
398
+ if (!d) return { error: `Cannot parse: ${val}` };
399
+ return {
400
+ input: val,
401
+ unix_seconds: Math.floor(d.getTime() / 1000),
402
+ unix_milliseconds: d.getTime(),
403
+ iso: d.toISOString(),
404
+ human: `${DAY_NAMES[d.getUTCDay()]}, ${MONTH_NAMES[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")} UTC`
405
+ };
406
+ }
407
+
408
+ function handleBusinessDays(args) {
409
+ const start = parseFlexibleDate(args.start);
410
+ if (!start) return { error: `Cannot parse start date: ${args.start}` };
411
+
412
+ if (args.operation === "between") {
413
+ if (!args.end) return { error: "End date required for 'between' operation" };
414
+ const end = parseFlexibleDate(args.end);
415
+ if (!end) return { error: `Cannot parse end date: ${args.end}` };
416
+ let count = 0;
417
+ const cursor = new Date(start);
418
+ while (cursor < end) {
419
+ if (!isWeekend(cursor)) count++;
420
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
421
+ }
422
+ return { start: start.toISOString(), end: end.toISOString(), business_days: count };
423
+ }
424
+
425
+ if (args.operation === "add") {
426
+ let remaining = args.days || 0;
427
+ const cursor = new Date(start);
428
+ const step = remaining >= 0 ? 1 : -1;
429
+ remaining = Math.abs(remaining);
430
+ while (remaining > 0) {
431
+ cursor.setUTCDate(cursor.getUTCDate() + step);
432
+ if (!isWeekend(cursor)) remaining--;
433
+ }
434
+ return {
435
+ start: start.toISOString(),
436
+ days_added: args.days,
437
+ result: cursor.toISOString(),
438
+ day: DAY_NAMES[cursor.getUTCDay()]
439
+ };
440
+ }
441
+
442
+ return { error: `Unknown operation: ${args.operation}` };
443
+ }
444
+
445
+ function handleExplainCron(args) {
446
+ return { expression: args.expression, explanation: explainCron(args.expression) };
447
+ }
448
+
449
+ function handleCountdown(args) {
450
+ const target = parseFlexibleDate(args.target);
451
+ if (!target) return { error: `Cannot parse target date: ${args.target}` };
452
+ const from = args.from ? parseFlexibleDate(args.from) : new Date();
453
+ if (!from) return { error: `Cannot parse from date: ${args.from}` };
454
+
455
+ const diffMs = target.getTime() - from.getTime();
456
+ const isPast = diffMs < 0;
457
+ const abs = Math.abs(diffMs);
458
+
459
+ const days = Math.floor(abs / 86400000);
460
+ const hours = Math.floor((abs % 86400000) / 3600000);
461
+ const minutes = Math.floor((abs % 3600000) / 60000);
462
+ const seconds = Math.floor((abs % 60000) / 1000);
463
+
464
+ return {
465
+ target: target.toISOString(),
466
+ from: from.toISOString(),
467
+ direction: isPast ? "elapsed" : "remaining",
468
+ breakdown: { days, hours, minutes, seconds },
469
+ human: `${days}d ${hours}h ${minutes}m ${seconds}s ${isPast ? "ago" : "remaining"}`,
470
+ total_days: Math.round(abs / 86400000 * 100) / 100
471
+ };
472
+ }
473
+
474
+ function handleDateInfo(args) {
475
+ const d = parseFlexibleDate(args.date);
476
+ if (!d) return { error: `Cannot parse date: ${args.date}` };
477
+
478
+ const year = d.getUTCFullYear();
479
+ const isLeap = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
480
+ const startOfYear = new Date(Date.UTC(year, 0, 1));
481
+ const dayOfYear = Math.floor((d.getTime() - startOfYear.getTime()) / 86400000) + 1;
482
+ const daysInYear = isLeap ? 366 : 365;
483
+ const isoWeek = getISOWeek(d);
484
+ const quarter = Math.ceil((d.getUTCMonth() + 1) / 3);
485
+
486
+ return {
487
+ date: d.toISOString(),
488
+ day_of_week: DAY_NAMES[d.getUTCDay()],
489
+ day_of_year: dayOfYear,
490
+ days_remaining_in_year: daysInYear - dayOfYear,
491
+ iso_week: `${isoWeek.year}-W${String(isoWeek.week).padStart(2, "0")}`,
492
+ quarter: `Q${quarter}`,
493
+ is_leap_year: isLeap,
494
+ is_weekend: isWeekend(d),
495
+ month_name: MONTH_NAMES[d.getUTCMonth()],
496
+ days_in_month: new Date(Date.UTC(year, d.getUTCMonth() + 1, 0)).getUTCDate(),
497
+ unix_timestamp: Math.floor(d.getTime() / 1000)
498
+ };
499
+ }
500
+
501
+ const handlers = {
502
+ now: handleNow,
503
+ convert_timezone: handleConvertTimezone,
504
+ date_math: handleDateMath,
505
+ date_diff: handleDateDiff,
506
+ format_date: handleFormatDate,
507
+ unix_timestamp: handleUnixTimestamp,
508
+ business_days: handleBusinessDays,
509
+ explain_cron: handleExplainCron,
510
+ countdown: handleCountdown,
511
+ date_info: handleDateInfo,
512
+ };
513
+
514
+ // ── MCP handlers ──
515
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
516
+
517
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
518
+ const { name, arguments: args } = request.params;
519
+ const handler = handlers[name];
520
+ if (!handler) {
521
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
522
+ }
523
+ try {
524
+ const result = handler(args || {});
525
+ if (result.error) {
526
+ return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true };
527
+ }
528
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
529
+ } catch (err) {
530
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
531
+ }
532
+ });
533
+
534
+ const transport = new StdioServerTransport();
535
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "mcp-datetime",
3
+ "version": "1.0.0",
4
+ "description": "MCP server with 10 date & time tools — timezone conversion, date math, formatting, parsing, cron explanation, countdown, unix timestamps, business days, ISO weeks, and relative time. Zero dependencies beyond MCP SDK.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "mcp-datetime": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "datetime",
17
+ "timezone",
18
+ "date",
19
+ "time",
20
+ "cron",
21
+ "timestamp",
22
+ "unix-time",
23
+ "business-days",
24
+ "date-math",
25
+ "claude",
26
+ "cursor",
27
+ "ai-tools",
28
+ "llm-tools",
29
+ "date-formatting"
30
+ ],
31
+ "author": "Hong Teoh",
32
+ "license": "MIT",
33
+ "funding": {
34
+ "type": "buymeacoffee",
35
+ "url": "https://buymeacoffee.com/gl89tu25lp"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.0.0"
39
+ }
40
+ }