@findtime/mcp-server 3.25.9 → 3.25.11
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/README.md +24 -23
- package/package.json +1 -9
- package/server.js +206 -2
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Published surfaces:
|
|
|
13
13
|
|
|
14
14
|
## Tool surface
|
|
15
15
|
|
|
16
|
+
- `answer_time_question`
|
|
16
17
|
- `time_snapshot`
|
|
17
18
|
- `get_current_time`
|
|
18
19
|
- `get_dst_schedule`
|
|
@@ -22,6 +23,8 @@ Published surfaces:
|
|
|
22
23
|
- `search_timezones`
|
|
23
24
|
- `get_location_by_id`
|
|
24
25
|
|
|
26
|
+
Prefer `answer_time_question` for messy natural-language prompts such as "3pm PST to London", "what is the IANA timezone for San Francisco?", "working hours overlap for SF, Berlin, and Tokyo", or "what does CST mean?". The tool classifies the prompt through findtime.io's domain logic, dispatches to deterministic Time API behavior, and returns structured ambiguity or clarification when the world is genuinely ambiguous.
|
|
27
|
+
|
|
25
28
|
## Install in MCP clients
|
|
26
29
|
|
|
27
30
|
Use the published package through `npx`:
|
|
@@ -41,7 +44,9 @@ Optional environment variables:
|
|
|
41
44
|
- `TIME_API_BASE_URL`
|
|
42
45
|
- `TIME_API_TIMEOUT_MS`
|
|
43
46
|
- `FINDTIME_MCP_CLIENT_TYPE`
|
|
44
|
-
- `
|
|
47
|
+
- `FINDTIME_MCP_CLIENT_ID` or `FINDTIME_MCP_INSTALL_ID` to provide a stable client identifier. If omitted, the server creates one locally under the user's state directory.
|
|
48
|
+
- `FINDTIME_MCP_INSTRUMENTATION_ENABLED=false` to opt out of anonymous usage telemetry.
|
|
49
|
+
- `FINDTIME_MCP_USAGE_TELEMETRY_URL` to override the default telemetry endpoint.
|
|
45
50
|
|
|
46
51
|
### Cursor
|
|
47
52
|
|
|
@@ -55,8 +60,7 @@ Optional environment variables:
|
|
|
55
60
|
"env": {
|
|
56
61
|
"FINDTIME_MCP_CLIENT_TYPE": "cursor",
|
|
57
62
|
"FINDTIME_TIME_API_BASE_URL": "https://time-api.findtime.io",
|
|
58
|
-
"FINDTIME_TIME_API_KEY": "YOUR_FINDTIME_SECRET_KEY"
|
|
59
|
-
"FINDTIME_MCP_INSTRUMENTATION_ENABLED": "false"
|
|
63
|
+
"FINDTIME_TIME_API_KEY": "YOUR_FINDTIME_SECRET_KEY"
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
}
|
|
@@ -75,7 +79,6 @@ enabled = true
|
|
|
75
79
|
FINDTIME_MCP_CLIENT_TYPE = "codex"
|
|
76
80
|
FINDTIME_TIME_API_BASE_URL = "https://time-api.findtime.io"
|
|
77
81
|
FINDTIME_TIME_API_KEY = "YOUR_FINDTIME_SECRET_KEY"
|
|
78
|
-
FINDTIME_MCP_INSTRUMENTATION_ENABLED = "false"
|
|
79
82
|
```
|
|
80
83
|
|
|
81
84
|
### Claude Desktop
|
|
@@ -91,8 +94,7 @@ FINDTIME_MCP_INSTRUMENTATION_ENABLED = "false"
|
|
|
91
94
|
"args": ["-y", "@findtime/mcp-server"],
|
|
92
95
|
"env": {
|
|
93
96
|
"FINDTIME_TIME_API_BASE_URL": "https://time-api.findtime.io",
|
|
94
|
-
"FINDTIME_TIME_API_KEY": "YOUR_FINDTIME_SECRET_KEY"
|
|
95
|
-
"FINDTIME_MCP_INSTRUMENTATION_ENABLED": "false"
|
|
97
|
+
"FINDTIME_TIME_API_KEY": "YOUR_FINDTIME_SECRET_KEY"
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
}
|
|
@@ -115,15 +117,16 @@ Best meeting time between New York, Sydney, and Mumbai?
|
|
|
115
117
|
|
|
116
118
|
## Local development
|
|
117
119
|
|
|
118
|
-
Run the
|
|
120
|
+
Run the workspace version directly:
|
|
119
121
|
|
|
120
122
|
```bash
|
|
121
|
-
npm start
|
|
123
|
+
npm run mcp:start
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
The server attempts to load `.env.development.local`, `.env.development`, `.env.local`, and `.env` from:
|
|
125
127
|
|
|
126
128
|
- the current working directory
|
|
129
|
+
- `services/mcp-server`
|
|
127
130
|
- the repo root
|
|
128
131
|
|
|
129
132
|
## Tests
|
|
@@ -131,18 +134,19 @@ The server attempts to load `.env.development.local`, `.env.development`, `.env.
|
|
|
131
134
|
Protocol and transport tests:
|
|
132
135
|
|
|
133
136
|
```bash
|
|
134
|
-
npm test
|
|
137
|
+
npm run test:mcp-server
|
|
135
138
|
```
|
|
136
139
|
|
|
137
140
|
Live production-parity smoke tests:
|
|
138
141
|
|
|
139
142
|
```bash
|
|
140
|
-
npm run test:smoke
|
|
143
|
+
npm run test:mcp-server:smoke
|
|
141
144
|
```
|
|
142
145
|
|
|
143
146
|
The smoke suite checks:
|
|
144
147
|
|
|
145
148
|
- `search_timezones`
|
|
149
|
+
- `answer_time_question`
|
|
146
150
|
- `get_current_time`
|
|
147
151
|
- `get_dst_schedule`
|
|
148
152
|
- `convert_time`
|
|
@@ -152,17 +156,15 @@ The smoke suite checks:
|
|
|
152
156
|
|
|
153
157
|
## Maintainer release flow
|
|
154
158
|
|
|
155
|
-
|
|
159
|
+
The canonical public source for this package now lives in:
|
|
156
160
|
|
|
157
|
-
|
|
161
|
+
- GitHub: `https://github.com/hkchao/findtime-mcp-server`
|
|
162
|
+
- npm: `@findtime/mcp-server`
|
|
163
|
+
- Official MCP Registry: `https://registry.modelcontextprotocol.io/?q=io.github.hkchao%2Ffindtime-mcp-server`
|
|
158
164
|
|
|
159
|
-
|
|
160
|
-
- keep `io.github.hkchao/findtime-mcp-server` as the MCP Registry server name
|
|
161
|
-
- add `repository` and `bugs` metadata after creating the GitHub repo
|
|
162
|
-
- add an `NPM_TOKEN` secret to the GitHub repository
|
|
163
|
-
- publish through GitHub Actions or a maintainer terminal from this repo root
|
|
165
|
+
Publish and version updates should happen from that public repo, not from this private app repo.
|
|
164
166
|
|
|
165
|
-
Standard
|
|
167
|
+
Standard publish flow in the public repo:
|
|
166
168
|
|
|
167
169
|
```bash
|
|
168
170
|
npm test
|
|
@@ -170,12 +172,11 @@ npm pack --dry-run
|
|
|
170
172
|
npm publish --access public
|
|
171
173
|
```
|
|
172
174
|
|
|
173
|
-
|
|
175
|
+
The equivalent local verification checks in this repo are:
|
|
174
176
|
|
|
175
177
|
```bash
|
|
176
|
-
npm test
|
|
177
|
-
npm pack
|
|
178
|
-
npm publish --access public
|
|
178
|
+
npm run test:mcp-server
|
|
179
|
+
npm run mcp:pack
|
|
179
180
|
```
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
Treat this repo as the implementation source that originally produced the MCP package, not as the canonical public release source.
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findtime/mcp-server",
|
|
3
|
-
"version": "3.25.
|
|
4
|
-
"mcpName": "io.github.hkchao/findtime-mcp-server",
|
|
3
|
+
"version": "3.25.11",
|
|
5
4
|
"description": "Production-parity MCP server for the findtime.io Time API",
|
|
6
5
|
"bin": {
|
|
7
6
|
"findtime-mcp": "server.js"
|
|
@@ -29,14 +28,7 @@
|
|
|
29
28
|
"publishConfig": {
|
|
30
29
|
"access": "public"
|
|
31
30
|
},
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/hkchao/findtime-mcp-server.git"
|
|
35
|
-
},
|
|
36
31
|
"homepage": "https://findtime.io/developers/mcp/",
|
|
37
|
-
"bugs": {
|
|
38
|
-
"url": "https://github.com/hkchao/findtime-mcp-server/issues"
|
|
39
|
-
},
|
|
40
32
|
"keywords": [
|
|
41
33
|
"mcp",
|
|
42
34
|
"model-context-protocol",
|
package/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
4
6
|
|
|
5
7
|
const PACKAGE_ROOT = __dirname;
|
|
6
8
|
const LOCAL_PACKAGE_PATH = path.join(PACKAGE_ROOT, 'package.json');
|
|
@@ -35,6 +37,11 @@ const DEFAULT_API_BASE_URL = firstNonEmpty(
|
|
|
35
37
|
process.env.FINDTIME_TIME_API_BASE_URL
|
|
36
38
|
) || 'https://time-api.findtime.io';
|
|
37
39
|
const DEFAULT_TIMEOUT_MS = parseInteger(process.env.TIME_API_TIMEOUT_MS, 15000);
|
|
40
|
+
const MCP_USAGE_TELEMETRY_URL = firstNonEmpty(
|
|
41
|
+
process.env.FINDTIME_MCP_USAGE_TELEMETRY_URL,
|
|
42
|
+
process.env.FINDTIME_USAGE_TELEMETRY_URL
|
|
43
|
+
) || 'https://slack.findtime.io/telemetry/usage';
|
|
44
|
+
const MCP_USAGE_TELEMETRY_TIMEOUT_MS = parseInteger(process.env.FINDTIME_MCP_USAGE_TELEMETRY_TIMEOUT_MS, 1200);
|
|
38
45
|
const DEFAULT_API_KEY = firstNonEmpty(
|
|
39
46
|
process.env.FINDTIME_API_KEY,
|
|
40
47
|
process.env.TIME_API_KEY,
|
|
@@ -44,6 +51,47 @@ const DEFAULT_API_KEY = firstNonEmpty(
|
|
|
44
51
|
const TIMEZONE_HELPERS_PATH = path.join(REPO_ROOT, 'slack-bot', 'timezone-helpers.js');
|
|
45
52
|
|
|
46
53
|
const TOOL_DEFINITIONS = [
|
|
54
|
+
{
|
|
55
|
+
name: 'answer_time_question',
|
|
56
|
+
description: 'Answer a natural-language time, timezone, conversion, DST, overlap, scheduling, abbreviation, or IANA timezone question. Prefer this for messy user prompts; findtime.io will classify intent and call the right deterministic time API behavior.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
required: ['query'],
|
|
60
|
+
properties: {
|
|
61
|
+
query: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'The raw user time-related question or prompt.'
|
|
64
|
+
},
|
|
65
|
+
userTimezone: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Optional IANA timezone for the user, used for relative questions like "my time" or "tomorrow".'
|
|
68
|
+
},
|
|
69
|
+
locale: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Optional user locale hint, such as en-US.'
|
|
72
|
+
},
|
|
73
|
+
now: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Optional ISO timestamp for deterministic relative-date handling.'
|
|
76
|
+
},
|
|
77
|
+
date: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'Optional YYYY-MM-DD date context.'
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
additionalProperties: false
|
|
83
|
+
},
|
|
84
|
+
buildRequest(args) {
|
|
85
|
+
ensureRequired(args, ['query'], 'answer_time_question requires query.');
|
|
86
|
+
const params = new URLSearchParams();
|
|
87
|
+
setParam(params, 'query', args.query);
|
|
88
|
+
setParam(params, 'userTimezone', args.userTimezone);
|
|
89
|
+
setParam(params, 'locale', args.locale);
|
|
90
|
+
setParam(params, 'now', args.now);
|
|
91
|
+
setParam(params, 'date', args.date);
|
|
92
|
+
return { path: '/time/answer', params };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
47
95
|
{
|
|
48
96
|
name: 'get_api_diagnostics',
|
|
49
97
|
description: 'Return MCP and findtime Time API diagnostics, including the running MCP version, latest published MCP version, API base URL, auth configuration, and a live health check.',
|
|
@@ -335,6 +383,7 @@ const TOOL_DEFINITIONS = [
|
|
|
335
383
|
|
|
336
384
|
const TOOL_DEFINITIONS_BY_NAME = new Map(TOOL_DEFINITIONS.map((tool) => [tool.name, tool]));
|
|
337
385
|
let cachedResolveLocation;
|
|
386
|
+
let cachedMcpClientId;
|
|
338
387
|
|
|
339
388
|
function safeReadJson(filePath) {
|
|
340
389
|
try {
|
|
@@ -399,6 +448,144 @@ function parseInteger(value, fallback) {
|
|
|
399
448
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
400
449
|
}
|
|
401
450
|
|
|
451
|
+
function isMcpUsageTelemetryEnabled() {
|
|
452
|
+
const explicit = firstNonEmpty(
|
|
453
|
+
process.env.FINDTIME_MCP_USAGE_TELEMETRY_ENABLED,
|
|
454
|
+
process.env.FINDTIME_MCP_INSTRUMENTATION_ENABLED
|
|
455
|
+
);
|
|
456
|
+
const normalized = String(explicit || '').trim().toLowerCase();
|
|
457
|
+
return !['false', '0', 'off', 'no'].includes(normalized);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function getMcpClientType() {
|
|
461
|
+
return firstNonEmpty(
|
|
462
|
+
process.env.FINDTIME_MCP_CLIENT_TYPE,
|
|
463
|
+
process.env.TIME_API_CLIENT_TYPE
|
|
464
|
+
) || 'stdio';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function getMcpClientIdPath() {
|
|
468
|
+
const explicitDir = firstNonEmpty(process.env.FINDTIME_MCP_STATE_DIR);
|
|
469
|
+
if (explicitDir) {
|
|
470
|
+
return path.join(explicitDir, 'mcp-client-id');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const xdgStateHome = firstNonEmpty(process.env.XDG_STATE_HOME);
|
|
474
|
+
const baseDir = xdgStateHome
|
|
475
|
+
? path.join(xdgStateHome, 'findtime')
|
|
476
|
+
: path.join(os.homedir(), '.findtime');
|
|
477
|
+
return path.join(baseDir, 'mcp-client-id');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function getMcpClientId() {
|
|
481
|
+
const explicit = firstNonEmpty(
|
|
482
|
+
process.env.FINDTIME_MCP_CLIENT_ID,
|
|
483
|
+
process.env.FINDTIME_MCP_INSTALL_ID
|
|
484
|
+
);
|
|
485
|
+
if (explicit) return explicit.slice(0, 128);
|
|
486
|
+
|
|
487
|
+
if (cachedMcpClientId) return cachedMcpClientId;
|
|
488
|
+
|
|
489
|
+
const idPath = getMcpClientIdPath();
|
|
490
|
+
try {
|
|
491
|
+
const existing = fs.readFileSync(idPath, 'utf8').trim();
|
|
492
|
+
if (existing) {
|
|
493
|
+
cachedMcpClientId = existing.slice(0, 128);
|
|
494
|
+
return cachedMcpClientId;
|
|
495
|
+
}
|
|
496
|
+
} catch (_error) {
|
|
497
|
+
// First run or unreadable state file. Generate below.
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
cachedMcpClientId = `mcp-${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`;
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
fs.mkdirSync(path.dirname(idPath), { recursive: true, mode: 0o700 });
|
|
504
|
+
fs.writeFileSync(idPath, `${cachedMcpClientId}\n`, { mode: 0o600 });
|
|
505
|
+
} catch (_error) {
|
|
506
|
+
// Telemetry remains best-effort; an unwritable home directory should not break MCP.
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return cachedMcpClientId;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function getRuntimeLocale() {
|
|
513
|
+
return firstNonEmpty(
|
|
514
|
+
process.env.LC_ALL,
|
|
515
|
+
process.env.LC_MESSAGES,
|
|
516
|
+
process.env.LANG
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function getRuntimeTimezone() {
|
|
521
|
+
try {
|
|
522
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
523
|
+
} catch (_error) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function getMcpQueryText(args = {}) {
|
|
529
|
+
if (!args || typeof args !== 'object') return '';
|
|
530
|
+
return firstNonEmpty(
|
|
531
|
+
args.query,
|
|
532
|
+
args.city,
|
|
533
|
+
args.timezone,
|
|
534
|
+
args.from,
|
|
535
|
+
Array.isArray(args.locations) ? args.locations.join('|') : null
|
|
536
|
+
) || '';
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function buildMcpUsageTelemetryEvent({ toolName, args = {}, result = null, error = null, latencyMs = 0 } = {}) {
|
|
540
|
+
const queryText = getMcpQueryText(args);
|
|
541
|
+
const status = error
|
|
542
|
+
? 'exception'
|
|
543
|
+
: result && result.isError
|
|
544
|
+
? 'error'
|
|
545
|
+
: 'ok';
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
platform: 'mcp-server',
|
|
549
|
+
route: String(toolName || 'unknown').slice(0, 80),
|
|
550
|
+
status,
|
|
551
|
+
source: getMcpClientType().slice(0, 80),
|
|
552
|
+
textLength: Math.min(2000, queryText.length),
|
|
553
|
+
userId: getMcpClientId(),
|
|
554
|
+
appVersion: SERVER_VERSION,
|
|
555
|
+
browser: 'mcp',
|
|
556
|
+
browserVersion: MCP_INSTALL_MODE,
|
|
557
|
+
os: `${os.platform()} ${os.release()}`.slice(0, 80),
|
|
558
|
+
locale: getRuntimeLocale(),
|
|
559
|
+
timezone: getRuntimeTimezone(),
|
|
560
|
+
latencyMs: Math.max(0, Math.round(Number(latencyMs || 0)))
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function recordMcpUsageTelemetry(input) {
|
|
565
|
+
if (!isMcpUsageTelemetryEnabled() || !MCP_USAGE_TELEMETRY_URL || typeof fetch !== 'function') {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const event = buildMcpUsageTelemetryEvent(input);
|
|
570
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
571
|
+
const timeout = controller
|
|
572
|
+
? setTimeout(() => controller.abort(), MCP_USAGE_TELEMETRY_TIMEOUT_MS)
|
|
573
|
+
: null;
|
|
574
|
+
|
|
575
|
+
fetch(MCP_USAGE_TELEMETRY_URL, {
|
|
576
|
+
method: 'POST',
|
|
577
|
+
headers: { 'content-type': 'application/json' },
|
|
578
|
+
body: JSON.stringify(event),
|
|
579
|
+
signal: controller ? controller.signal : undefined
|
|
580
|
+
}).catch((error) => {
|
|
581
|
+
if (String(process.env.FINDTIME_MCP_DEBUG || '').trim().toLowerCase() === '1') {
|
|
582
|
+
console.error('[findtime-mcp telemetry] failed:', error && error.message ? error.message : error);
|
|
583
|
+
}
|
|
584
|
+
}).finally(() => {
|
|
585
|
+
if (timeout) clearTimeout(timeout);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
402
589
|
function stringOrStringArraySchema(description) {
|
|
403
590
|
return {
|
|
404
591
|
anyOf: [
|
|
@@ -1055,8 +1242,25 @@ function createFindtimeMcpServer(options = {}) {
|
|
|
1055
1242
|
if (method === 'tools/call') {
|
|
1056
1243
|
const toolName = message.params && message.params.name;
|
|
1057
1244
|
const toolArgs = (message.params && message.params.arguments) || {};
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1245
|
+
const startedAt = Date.now();
|
|
1246
|
+
try {
|
|
1247
|
+
const result = await callTool(toolName, toolArgs);
|
|
1248
|
+
recordMcpUsageTelemetry({
|
|
1249
|
+
toolName,
|
|
1250
|
+
args: toolArgs,
|
|
1251
|
+
result,
|
|
1252
|
+
latencyMs: Date.now() - startedAt
|
|
1253
|
+
});
|
|
1254
|
+
return createSuccessResponse(message.id, result);
|
|
1255
|
+
} catch (toolError) {
|
|
1256
|
+
recordMcpUsageTelemetry({
|
|
1257
|
+
toolName,
|
|
1258
|
+
args: toolArgs,
|
|
1259
|
+
error: toolError,
|
|
1260
|
+
latencyMs: Date.now() - startedAt
|
|
1261
|
+
});
|
|
1262
|
+
throw toolError;
|
|
1263
|
+
}
|
|
1060
1264
|
}
|
|
1061
1265
|
|
|
1062
1266
|
throw methodNotFoundError(method);
|