@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.
- package/README.md +20 -23
- package/package.json +1 -9
- 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
|
-
- `
|
|
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');
|
|
@@ -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
|
|
1059
|
-
|
|
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);
|