@findtime/mcp-server 3.25.8 → 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.
- package/README.md +20 -23
- package/package.json +1 -9
- package/server.js +191 -3
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
|
-
- `
|
|
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
|
|
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
|
-
|
|
155
|
+
The canonical public source for this package now lives in:
|
|
156
156
|
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
171
|
+
The equivalent local verification checks in this repo are:
|
|
174
172
|
|
|
175
173
|
```bash
|
|
176
|
-
npm test
|
|
177
|
-
npm pack
|
|
178
|
-
npm publish --access public
|
|
174
|
+
npm run test:mcp-server
|
|
175
|
+
npm run mcp:pack
|
|
179
176
|
```
|
|
180
177
|
|
|
181
|
-
|
|
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.
|
|
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');
|
|
@@ -17,17 +19,29 @@ const SUPPORTED_PROTOCOL_VERSIONS = new Set([
|
|
|
17
19
|
|
|
18
20
|
loadEnvironmentFiles();
|
|
19
21
|
|
|
20
|
-
const
|
|
22
|
+
const LOCAL_PACKAGE_METADATA = safeReadJson(LOCAL_PACKAGE_PATH) || null;
|
|
23
|
+
const REPO_PACKAGE_METADATA = safeReadJson(REPO_PACKAGE_PATH) || null;
|
|
24
|
+
const PACKAGE_METADATA = LOCAL_PACKAGE_METADATA || REPO_PACKAGE_METADATA || {};
|
|
21
25
|
const SERVER_VERSION = PACKAGE_METADATA.version || '0.0.0';
|
|
22
26
|
const MCP_PACKAGE_NAME = PACKAGE_METADATA.name || '@findtime/mcp-server';
|
|
23
27
|
const MCP_NPM_REGISTRY_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(MCP_PACKAGE_NAME)}/latest`;
|
|
24
28
|
const MCP_NPM_URL = `https://www.npmjs.com/package/${MCP_PACKAGE_NAME}`;
|
|
25
29
|
const MCP_REGISTRY_URL = 'https://registry.modelcontextprotocol.io/?q=io.github.hkchao%2Ffindtime-mcp-server';
|
|
30
|
+
const MCP_INSTALL_MODE = determineInstallMode({
|
|
31
|
+
packageRoot: PACKAGE_ROOT,
|
|
32
|
+
localPackageMetadata: LOCAL_PACKAGE_METADATA,
|
|
33
|
+
repoPackageMetadata: REPO_PACKAGE_METADATA
|
|
34
|
+
});
|
|
26
35
|
const DEFAULT_API_BASE_URL = firstNonEmpty(
|
|
27
36
|
process.env.TIME_API_BASE_URL,
|
|
28
37
|
process.env.FINDTIME_TIME_API_BASE_URL
|
|
29
38
|
) || 'https://time-api.findtime.io';
|
|
30
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);
|
|
31
45
|
const DEFAULT_API_KEY = firstNonEmpty(
|
|
32
46
|
process.env.FINDTIME_API_KEY,
|
|
33
47
|
process.env.TIME_API_KEY,
|
|
@@ -328,6 +342,7 @@ const TOOL_DEFINITIONS = [
|
|
|
328
342
|
|
|
329
343
|
const TOOL_DEFINITIONS_BY_NAME = new Map(TOOL_DEFINITIONS.map((tool) => [tool.name, tool]));
|
|
330
344
|
let cachedResolveLocation;
|
|
345
|
+
let cachedMcpClientId;
|
|
331
346
|
|
|
332
347
|
function safeReadJson(filePath) {
|
|
333
348
|
try {
|
|
@@ -392,6 +407,144 @@ function parseInteger(value, fallback) {
|
|
|
392
407
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
393
408
|
}
|
|
394
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
|
+
|
|
395
548
|
function stringOrStringArraySchema(description) {
|
|
396
549
|
return {
|
|
397
550
|
anyOf: [
|
|
@@ -903,6 +1056,8 @@ function createFindtimeMcpServer(options = {}) {
|
|
|
903
1056
|
mcpLatestVersionHint: latestVersion
|
|
904
1057
|
? null
|
|
905
1058
|
: 'Could not verify the latest published MCP version automatically. Check the npm package page or Official MCP Registry listing directly.',
|
|
1059
|
+
mcpInstallMode: MCP_INSTALL_MODE,
|
|
1060
|
+
mcpExecutablePath: path.join(PACKAGE_ROOT, 'server.js'),
|
|
906
1061
|
mcpRegistryUrl: MCP_REGISTRY_URL,
|
|
907
1062
|
mcpNpmUrl: MCP_NPM_URL,
|
|
908
1063
|
apiBaseUrl,
|
|
@@ -1046,8 +1201,25 @@ function createFindtimeMcpServer(options = {}) {
|
|
|
1046
1201
|
if (method === 'tools/call') {
|
|
1047
1202
|
const toolName = message.params && message.params.name;
|
|
1048
1203
|
const toolArgs = (message.params && message.params.arguments) || {};
|
|
1049
|
-
const
|
|
1050
|
-
|
|
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
|
+
}
|
|
1051
1223
|
}
|
|
1052
1224
|
|
|
1053
1225
|
throw methodNotFoundError(method);
|
|
@@ -1081,6 +1253,22 @@ function tryParseJson(value) {
|
|
|
1081
1253
|
}
|
|
1082
1254
|
}
|
|
1083
1255
|
|
|
1256
|
+
function determineInstallMode({ packageRoot, localPackageMetadata, repoPackageMetadata }) {
|
|
1257
|
+
if (repoPackageMetadata && repoPackageMetadata.name === 'world-time-ai') {
|
|
1258
|
+
return 'repo_checkout';
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (localPackageMetadata && localPackageMetadata.name === '@findtime/mcp-server') {
|
|
1262
|
+
return 'npm_package';
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (typeof packageRoot === 'string' && packageRoot.includes(`${path.sep}world-time-ai${path.sep}services${path.sep}mcp-server`)) {
|
|
1266
|
+
return 'repo_checkout';
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return 'package_install';
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1084
1272
|
function startStdioServer(options = {}) {
|
|
1085
1273
|
const server = createFindtimeMcpServer(options);
|
|
1086
1274
|
const messageBuffer = new ContentLengthMessageBuffer();
|