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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/index.js +535 -0
- 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
|
+
}
|