@findtime/mcp-server 3.25.9 → 3.25.10

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 +20 -23
  2. package/package.json +1 -9
  3. package/server.js +165 -2
package/README.md CHANGED
@@ -41,7 +41,9 @@ Optional environment variables:
41
41
  - `TIME_API_BASE_URL`
42
42
  - `TIME_API_TIMEOUT_MS`
43
43
  - `FINDTIME_MCP_CLIENT_TYPE`
44
- - `FINDTIME_MCP_INSTRUMENTATION_ENABLED`
44
+ - `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.
45
+ - `FINDTIME_MCP_INSTRUMENTATION_ENABLED=false` to opt out of anonymous usage telemetry.
46
+ - `FINDTIME_MCP_USAGE_TELEMETRY_URL` to override the default telemetry endpoint.
45
47
 
46
48
  ### Cursor
47
49
 
@@ -55,8 +57,7 @@ Optional environment variables:
55
57
  "env": {
56
58
  "FINDTIME_MCP_CLIENT_TYPE": "cursor",
57
59
  "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"
60
+ "FINDTIME_TIME_API_KEY": "YOUR_FINDTIME_SECRET_KEY"
60
61
  }
61
62
  }
62
63
  }
@@ -75,7 +76,6 @@ enabled = true
75
76
  FINDTIME_MCP_CLIENT_TYPE = "codex"
76
77
  FINDTIME_TIME_API_BASE_URL = "https://time-api.findtime.io"
77
78
  FINDTIME_TIME_API_KEY = "YOUR_FINDTIME_SECRET_KEY"
78
- FINDTIME_MCP_INSTRUMENTATION_ENABLED = "false"
79
79
  ```
80
80
 
81
81
  ### Claude Desktop
@@ -91,8 +91,7 @@ FINDTIME_MCP_INSTRUMENTATION_ENABLED = "false"
91
91
  "args": ["-y", "@findtime/mcp-server"],
92
92
  "env": {
93
93
  "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"
94
+ "FINDTIME_TIME_API_KEY": "YOUR_FINDTIME_SECRET_KEY"
96
95
  }
97
96
  }
98
97
  }
@@ -115,15 +114,16 @@ Best meeting time between New York, Sydney, and Mumbai?
115
114
 
116
115
  ## Local development
117
116
 
118
- Run the server directly from the repo root:
117
+ Run the workspace version directly:
119
118
 
120
119
  ```bash
121
- npm start
120
+ npm run mcp:start
122
121
  ```
123
122
 
124
123
  The server attempts to load `.env.development.local`, `.env.development`, `.env.local`, and `.env` from:
125
124
 
126
125
  - the current working directory
126
+ - `services/mcp-server`
127
127
  - the repo root
128
128
 
129
129
  ## Tests
@@ -131,13 +131,13 @@ The server attempts to load `.env.development.local`, `.env.development`, `.env.
131
131
  Protocol and transport tests:
132
132
 
133
133
  ```bash
134
- npm test
134
+ npm run test:mcp-server
135
135
  ```
136
136
 
137
137
  Live production-parity smoke tests:
138
138
 
139
139
  ```bash
140
- npm run test:smoke
140
+ npm run test:mcp-server:smoke
141
141
  ```
142
142
 
143
143
  The smoke suite checks:
@@ -152,17 +152,15 @@ The smoke suite checks:
152
152
 
153
153
  ## Maintainer release flow
154
154
 
155
- This repository is intended to be the canonical public source for `@findtime/mcp-server`.
155
+ The canonical public source for this package now lives in:
156
156
 
157
- Recommended setup:
157
+ - GitHub: `https://github.com/hkchao/findtime-mcp-server`
158
+ - npm: `@findtime/mcp-server`
159
+ - Official MCP Registry: `https://registry.modelcontextprotocol.io/?q=io.github.hkchao%2Ffindtime-mcp-server`
158
160
 
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
161
+ Publish and version updates should happen from that public repo, not from this private app repo.
164
162
 
165
- Standard local publish flow:
163
+ Standard publish flow in the public repo:
166
164
 
167
165
  ```bash
168
166
  npm test
@@ -170,12 +168,11 @@ npm pack --dry-run
170
168
  npm publish --access public
171
169
  ```
172
170
 
173
- GitHub Actions release flow:
171
+ The equivalent local verification checks in this repo are:
174
172
 
175
173
  ```bash
176
- npm test
177
- npm pack --dry-run
178
- npm publish --access public
174
+ npm run test:mcp-server
175
+ npm run mcp:pack
179
176
  ```
180
177
 
181
- Use the workflow in `.github/workflows/publish.yml` for repo-backed publishes.
178
+ 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.10",
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,
@@ -335,6 +342,7 @@ const TOOL_DEFINITIONS = [
335
342
 
336
343
  const TOOL_DEFINITIONS_BY_NAME = new Map(TOOL_DEFINITIONS.map((tool) => [tool.name, tool]));
337
344
  let cachedResolveLocation;
345
+ let cachedMcpClientId;
338
346
 
339
347
  function safeReadJson(filePath) {
340
348
  try {
@@ -399,6 +407,144 @@ function parseInteger(value, fallback) {
399
407
  return Number.isFinite(parsed) ? parsed : fallback;
400
408
  }
401
409
 
410
+ function isMcpUsageTelemetryEnabled() {
411
+ const explicit = firstNonEmpty(
412
+ process.env.FINDTIME_MCP_USAGE_TELEMETRY_ENABLED,
413
+ process.env.FINDTIME_MCP_INSTRUMENTATION_ENABLED
414
+ );
415
+ const normalized = String(explicit || '').trim().toLowerCase();
416
+ return !['false', '0', 'off', 'no'].includes(normalized);
417
+ }
418
+
419
+ function getMcpClientType() {
420
+ return firstNonEmpty(
421
+ process.env.FINDTIME_MCP_CLIENT_TYPE,
422
+ process.env.TIME_API_CLIENT_TYPE
423
+ ) || 'stdio';
424
+ }
425
+
426
+ function getMcpClientIdPath() {
427
+ const explicitDir = firstNonEmpty(process.env.FINDTIME_MCP_STATE_DIR);
428
+ if (explicitDir) {
429
+ return path.join(explicitDir, 'mcp-client-id');
430
+ }
431
+
432
+ const xdgStateHome = firstNonEmpty(process.env.XDG_STATE_HOME);
433
+ const baseDir = xdgStateHome
434
+ ? path.join(xdgStateHome, 'findtime')
435
+ : path.join(os.homedir(), '.findtime');
436
+ return path.join(baseDir, 'mcp-client-id');
437
+ }
438
+
439
+ function getMcpClientId() {
440
+ const explicit = firstNonEmpty(
441
+ process.env.FINDTIME_MCP_CLIENT_ID,
442
+ process.env.FINDTIME_MCP_INSTALL_ID
443
+ );
444
+ if (explicit) return explicit.slice(0, 128);
445
+
446
+ if (cachedMcpClientId) return cachedMcpClientId;
447
+
448
+ const idPath = getMcpClientIdPath();
449
+ try {
450
+ const existing = fs.readFileSync(idPath, 'utf8').trim();
451
+ if (existing) {
452
+ cachedMcpClientId = existing.slice(0, 128);
453
+ return cachedMcpClientId;
454
+ }
455
+ } catch (_error) {
456
+ // First run or unreadable state file. Generate below.
457
+ }
458
+
459
+ cachedMcpClientId = `mcp-${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`;
460
+
461
+ try {
462
+ fs.mkdirSync(path.dirname(idPath), { recursive: true, mode: 0o700 });
463
+ fs.writeFileSync(idPath, `${cachedMcpClientId}\n`, { mode: 0o600 });
464
+ } catch (_error) {
465
+ // Telemetry remains best-effort; an unwritable home directory should not break MCP.
466
+ }
467
+
468
+ return cachedMcpClientId;
469
+ }
470
+
471
+ function getRuntimeLocale() {
472
+ return firstNonEmpty(
473
+ process.env.LC_ALL,
474
+ process.env.LC_MESSAGES,
475
+ process.env.LANG
476
+ );
477
+ }
478
+
479
+ function getRuntimeTimezone() {
480
+ try {
481
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
482
+ } catch (_error) {
483
+ return null;
484
+ }
485
+ }
486
+
487
+ function getMcpQueryText(args = {}) {
488
+ if (!args || typeof args !== 'object') return '';
489
+ return firstNonEmpty(
490
+ args.query,
491
+ args.city,
492
+ args.timezone,
493
+ args.from,
494
+ Array.isArray(args.locations) ? args.locations.join('|') : null
495
+ ) || '';
496
+ }
497
+
498
+ function buildMcpUsageTelemetryEvent({ toolName, args = {}, result = null, error = null, latencyMs = 0 } = {}) {
499
+ const queryText = getMcpQueryText(args);
500
+ const status = error
501
+ ? 'exception'
502
+ : result && result.isError
503
+ ? 'error'
504
+ : 'ok';
505
+
506
+ return {
507
+ platform: 'mcp-server',
508
+ route: String(toolName || 'unknown').slice(0, 80),
509
+ status,
510
+ source: getMcpClientType().slice(0, 80),
511
+ textLength: Math.min(2000, queryText.length),
512
+ userId: getMcpClientId(),
513
+ appVersion: SERVER_VERSION,
514
+ browser: 'mcp',
515
+ browserVersion: MCP_INSTALL_MODE,
516
+ os: `${os.platform()} ${os.release()}`.slice(0, 80),
517
+ locale: getRuntimeLocale(),
518
+ timezone: getRuntimeTimezone(),
519
+ latencyMs: Math.max(0, Math.round(Number(latencyMs || 0)))
520
+ };
521
+ }
522
+
523
+ function recordMcpUsageTelemetry(input) {
524
+ if (!isMcpUsageTelemetryEnabled() || !MCP_USAGE_TELEMETRY_URL || typeof fetch !== 'function') {
525
+ return;
526
+ }
527
+
528
+ const event = buildMcpUsageTelemetryEvent(input);
529
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
530
+ const timeout = controller
531
+ ? setTimeout(() => controller.abort(), MCP_USAGE_TELEMETRY_TIMEOUT_MS)
532
+ : null;
533
+
534
+ fetch(MCP_USAGE_TELEMETRY_URL, {
535
+ method: 'POST',
536
+ headers: { 'content-type': 'application/json' },
537
+ body: JSON.stringify(event),
538
+ signal: controller ? controller.signal : undefined
539
+ }).catch((error) => {
540
+ if (String(process.env.FINDTIME_MCP_DEBUG || '').trim().toLowerCase() === '1') {
541
+ console.error('[findtime-mcp telemetry] failed:', error && error.message ? error.message : error);
542
+ }
543
+ }).finally(() => {
544
+ if (timeout) clearTimeout(timeout);
545
+ });
546
+ }
547
+
402
548
  function stringOrStringArraySchema(description) {
403
549
  return {
404
550
  anyOf: [
@@ -1055,8 +1201,25 @@ function createFindtimeMcpServer(options = {}) {
1055
1201
  if (method === 'tools/call') {
1056
1202
  const toolName = message.params && message.params.name;
1057
1203
  const toolArgs = (message.params && message.params.arguments) || {};
1058
- const result = await callTool(toolName, toolArgs);
1059
- return createSuccessResponse(message.id, result);
1204
+ const startedAt = Date.now();
1205
+ try {
1206
+ const result = await callTool(toolName, toolArgs);
1207
+ recordMcpUsageTelemetry({
1208
+ toolName,
1209
+ args: toolArgs,
1210
+ result,
1211
+ latencyMs: Date.now() - startedAt
1212
+ });
1213
+ return createSuccessResponse(message.id, result);
1214
+ } catch (toolError) {
1215
+ recordMcpUsageTelemetry({
1216
+ toolName,
1217
+ args: toolArgs,
1218
+ error: toolError,
1219
+ latencyMs: Date.now() - startedAt
1220
+ });
1221
+ throw toolError;
1222
+ }
1060
1223
  }
1061
1224
 
1062
1225
  throw methodNotFoundError(method);