@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.
Files changed (3) hide show
  1. package/README.md +24 -23
  2. package/package.json +1 -9
  3. 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
- - `FINDTIME_MCP_INSTRUMENTATION_ENABLED`
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 server directly from the repo root:
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
- This repository is intended to be the canonical public source for `@findtime/mcp-server`.
159
+ The canonical public source for this package now lives in:
156
160
 
157
- Recommended setup:
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
- - keep `@findtime/mcp-server` as the npm package name
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 local publish flow:
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
- GitHub Actions release flow:
175
+ The equivalent local verification checks in this repo are:
174
176
 
175
177
  ```bash
176
- npm test
177
- npm pack --dry-run
178
- npm publish --access public
178
+ npm run test:mcp-server
179
+ npm run mcp:pack
179
180
  ```
180
181
 
181
- Use the workflow in `.github/workflows/publish.yml` for repo-backed publishes.
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.9",
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 result = await callTool(toolName, toolArgs);
1059
- return createSuccessResponse(message.id, result);
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);