exponential-mcp 0.3.0 → 0.5.0
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/dist/index.js +124 -12
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -9,21 +9,63 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
|
|
|
9
9
|
import { ExponentialClient, createConfigStore } from 'exponential-sdk';
|
|
10
10
|
import { readFileSync, existsSync } from 'fs';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
|
-
import { join } from 'path';
|
|
12
|
+
import { dirname, join } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
13
14
|
const LEGACY_CONFIG_PATH = join(homedir(), '.config', 'exponential-mcp', 'config.json');
|
|
15
|
+
// Report the real package version rather than a hand-maintained literal that drifts.
|
|
16
|
+
const PKG_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8')).version;
|
|
14
17
|
const configStore = createConfigStore({ projectName: 'exponential-mcp' });
|
|
18
|
+
/**
|
|
19
|
+
* Read the `exp` claim out of a JWT, without verifying the signature — we only
|
|
20
|
+
* want to know whether it is worth sending, not whether it is trustworthy.
|
|
21
|
+
*
|
|
22
|
+
* Returns null for opaque tokens (`exp_agent_…` keys, which never expire) and
|
|
23
|
+
* for anything that does not parse as a JWT.
|
|
24
|
+
*/
|
|
25
|
+
function getTokenExpiry(token) {
|
|
26
|
+
const parts = token.split('.');
|
|
27
|
+
if (parts.length !== 3) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
32
|
+
return typeof payload?.exp === 'number' ? new Date(payload.exp * 1000) : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertTokenIsUsable(token, source) {
|
|
39
|
+
const expiry = getTokenExpiry(token);
|
|
40
|
+
if (!expiry || expiry.getTime() > Date.now()) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.error(`Error: token expired on ${expiry.toISOString().slice(0, 10)} (${source}).`);
|
|
44
|
+
console.error('Run "npx exponential-mcp init" to store a fresh token.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
15
47
|
function migrateLegacyConfig() {
|
|
16
48
|
if (configStore.isAuthenticated() || !existsSync(LEGACY_CONFIG_PATH)) {
|
|
17
49
|
return;
|
|
18
50
|
}
|
|
19
51
|
try {
|
|
20
52
|
const legacy = JSON.parse(readFileSync(LEGACY_CONFIG_PATH, 'utf-8'));
|
|
21
|
-
if (legacy?.apiKey) {
|
|
22
|
-
|
|
23
|
-
token: legacy.apiKey,
|
|
24
|
-
apiUrl: legacy.baseUrl || 'https://www.exponential.im',
|
|
25
|
-
});
|
|
53
|
+
if (!legacy?.apiKey) {
|
|
54
|
+
return;
|
|
26
55
|
}
|
|
56
|
+
// Don't resurrect a dead token: the legacy file long outlives the JWT in it,
|
|
57
|
+
// so migrating one blindly turns every config reset into a silent 401.
|
|
58
|
+
const expiry = getTokenExpiry(legacy.apiKey);
|
|
59
|
+
if (expiry && expiry.getTime() <= Date.now()) {
|
|
60
|
+
console.error(`Note: ignoring legacy config at ${LEGACY_CONFIG_PATH} — its token expired on ${expiry
|
|
61
|
+
.toISOString()
|
|
62
|
+
.slice(0, 10)}.`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
configStore.saveConfig({
|
|
66
|
+
token: legacy.apiKey,
|
|
67
|
+
apiUrl: legacy.baseUrl || 'https://www.exponential.im',
|
|
68
|
+
});
|
|
27
69
|
}
|
|
28
70
|
catch {
|
|
29
71
|
// Ignore legacy config parsing errors.
|
|
@@ -33,6 +75,7 @@ function loadClientConfig() {
|
|
|
33
75
|
migrateLegacyConfig();
|
|
34
76
|
if (configStore.isAuthenticated()) {
|
|
35
77
|
const config = configStore.loadConfig();
|
|
78
|
+
assertTokenIsUsable(config.token, 'stored config');
|
|
36
79
|
return { token: config.token, apiUrl: config.apiUrl };
|
|
37
80
|
}
|
|
38
81
|
const token = process.env.EXPONENTIAL_API_KEY || process.env.EXPONENTIAL_API_TOKEN;
|
|
@@ -44,6 +87,7 @@ function loadClientConfig() {
|
|
|
44
87
|
console.error('Run "npx exponential-mcp init" to set up, or set EXPONENTIAL_API_KEY env var.');
|
|
45
88
|
process.exit(1);
|
|
46
89
|
}
|
|
90
|
+
assertTokenIsUsable(token, 'EXPONENTIAL_API_KEY');
|
|
47
91
|
return { token, apiUrl };
|
|
48
92
|
}
|
|
49
93
|
/**
|
|
@@ -216,13 +260,54 @@ const TOOLS = [
|
|
|
216
260
|
},
|
|
217
261
|
{
|
|
218
262
|
name: 'get_goals',
|
|
219
|
-
description:
|
|
263
|
+
description: "List objectives (goals). Each objective carries its linked projects, which is how work ladders up to an OKR: every action has a projectId. Objectives have INTEGER ids — key results, which are separate, have CUIDs (see get_key_results). Use tree: true for the annual → quarterly cascade.",
|
|
220
264
|
inputSchema: {
|
|
221
265
|
type: 'object',
|
|
222
266
|
properties: {
|
|
223
267
|
workspaceId: {
|
|
224
268
|
type: 'string',
|
|
225
|
-
description: 'Optional workspace ID'
|
|
269
|
+
description: 'Optional workspace ID. With it the list is workspace-wide (every member\'s objectives); without it you get your own.'
|
|
270
|
+
},
|
|
271
|
+
period: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: 'Optional period filter, e.g. "Q3-2026" or "Annual-2026"'
|
|
274
|
+
},
|
|
275
|
+
status: {
|
|
276
|
+
type: 'string',
|
|
277
|
+
description: 'Optional status: planned, active, completed, archived, on-hold'
|
|
278
|
+
},
|
|
279
|
+
tree: {
|
|
280
|
+
type: 'boolean',
|
|
281
|
+
description: 'Return objectives nested parent → child (up to 5 levels), each with its projects and key results, instead of a flat list'
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'get_key_results',
|
|
288
|
+
description: "List key results — the measurable half of an OKR. By default groups them under their objectives (the richest read: one call for 'how is the quarter going'). Pass flat: true for a bare list. Key result ids are CUIDs; the goalId tying one to its objective is an integer.",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
workspaceId: {
|
|
293
|
+
type: 'string',
|
|
294
|
+
description: "Optional workspace ID. With it the list is workspace-wide (every member's key results); without it you get your own."
|
|
295
|
+
},
|
|
296
|
+
period: {
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'Optional period filter, e.g. "Q3-2026"'
|
|
299
|
+
},
|
|
300
|
+
goalId: {
|
|
301
|
+
type: 'number',
|
|
302
|
+
description: 'Optional objective ID (an integer). Only applies with flat: true.'
|
|
303
|
+
},
|
|
304
|
+
status: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: 'Optional status: not-started, on-track, at-risk, off-track, achieved. Only applies with flat: true.'
|
|
307
|
+
},
|
|
308
|
+
flat: {
|
|
309
|
+
type: 'boolean',
|
|
310
|
+
description: 'Return a flat list of key results rather than grouping them by objective'
|
|
226
311
|
}
|
|
227
312
|
}
|
|
228
313
|
}
|
|
@@ -267,7 +352,7 @@ async function main() {
|
|
|
267
352
|
const trpcClient = client.client;
|
|
268
353
|
const server = new Server({
|
|
269
354
|
name: 'exponential-mcp',
|
|
270
|
-
version:
|
|
355
|
+
version: PKG_VERSION,
|
|
271
356
|
}, {
|
|
272
357
|
capabilities: {
|
|
273
358
|
tools: {},
|
|
@@ -421,10 +506,17 @@ async function main() {
|
|
|
421
506
|
],
|
|
422
507
|
};
|
|
423
508
|
}
|
|
509
|
+
// Goes through the SDK's GoalsApi rather than a raw tRPC path. The old
|
|
510
|
+
// implementation called `goal.list`, which does not exist — the tool
|
|
511
|
+
// hard-errored with `No procedure found on path "goal.list"` on every
|
|
512
|
+
// invocation.
|
|
424
513
|
case 'get_goals': {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
514
|
+
const workspaceId = args?.workspaceId;
|
|
515
|
+
const period = args?.period;
|
|
516
|
+
const status = args?.status;
|
|
517
|
+
const goals = args?.tree
|
|
518
|
+
? await client.goals.tree({ workspaceId, status })
|
|
519
|
+
: await client.goals.list({ workspaceId, period, status });
|
|
428
520
|
return {
|
|
429
521
|
content: [
|
|
430
522
|
{
|
|
@@ -434,6 +526,26 @@ async function main() {
|
|
|
434
526
|
],
|
|
435
527
|
};
|
|
436
528
|
}
|
|
529
|
+
case 'get_key_results': {
|
|
530
|
+
const workspaceId = args?.workspaceId;
|
|
531
|
+
const period = args?.period;
|
|
532
|
+
const keyResults = args?.flat
|
|
533
|
+
? await client.goals.keyResults.list({
|
|
534
|
+
workspaceId,
|
|
535
|
+
period,
|
|
536
|
+
goalId: args?.goalId,
|
|
537
|
+
status: args?.status,
|
|
538
|
+
})
|
|
539
|
+
: await client.goals.keyResults.byObjective({ workspaceId, period });
|
|
540
|
+
return {
|
|
541
|
+
content: [
|
|
542
|
+
{
|
|
543
|
+
type: 'text',
|
|
544
|
+
text: JSON.stringify(keyResults, null, 2),
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
};
|
|
548
|
+
}
|
|
437
549
|
case 'search': {
|
|
438
550
|
const results = await trpcClient.search.global.query({
|
|
439
551
|
query: args?.query,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exponential-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for Exponential - connect Claude to your projects, actions, and goals",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
|
-
"cli": "node dist/cli.js"
|
|
14
|
+
"cli": "node dist/cli.js",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|
|
17
18
|
"mcp",
|
|
@@ -23,9 +24,9 @@
|
|
|
23
24
|
"author": "Exponential",
|
|
24
25
|
"license": "MIT",
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
27
28
|
"commander": "^12.0.0",
|
|
28
|
-
"exponential-sdk": "^1.
|
|
29
|
+
"exponential-sdk": "^1.12.0",
|
|
29
30
|
"zod": "^3.22.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|