exponential-mcp 0.3.0 → 0.4.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/api.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Exponential API Client
3
+ * Handles authenticated requests to the Exponential API
4
+ */
5
+ export interface ExponentialConfig {
6
+ apiKey: string;
7
+ baseUrl?: string;
8
+ }
9
+ export interface Project {
10
+ id: string;
11
+ name: string;
12
+ description?: string;
13
+ status: string;
14
+ priority: string;
15
+ progress: number;
16
+ }
17
+ export interface Action {
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ status: string;
22
+ priority: string;
23
+ dueDate?: string;
24
+ projectId?: string;
25
+ project?: {
26
+ name: string;
27
+ };
28
+ }
29
+ export interface Goal {
30
+ id: number;
31
+ title: string;
32
+ description?: string;
33
+ period?: string;
34
+ keyResults: KeyResult[];
35
+ }
36
+ export interface KeyResult {
37
+ id: string;
38
+ title: string;
39
+ targetValue?: number;
40
+ currentValue?: number;
41
+ status: string;
42
+ }
43
+ export interface Workspace {
44
+ id: string;
45
+ name: string;
46
+ slug: string;
47
+ }
48
+ export declare class ExponentialAPI {
49
+ private apiKey;
50
+ private baseUrl;
51
+ constructor(config: ExponentialConfig);
52
+ private request;
53
+ private trpcQuery;
54
+ private trpcMutation;
55
+ getWorkspaces(): Promise<Workspace[]>;
56
+ getProjects(workspaceId?: string): Promise<Project[]>;
57
+ getProject(id: string): Promise<Project>;
58
+ getActions(params?: {
59
+ projectId?: string;
60
+ status?: string;
61
+ }): Promise<Action[]>;
62
+ createAction(data: {
63
+ name: string;
64
+ description?: string;
65
+ projectId?: string;
66
+ priority?: string;
67
+ dueDate?: string;
68
+ }): Promise<Action>;
69
+ updateAction(id: string, data: {
70
+ name?: string;
71
+ status?: string;
72
+ priority?: string;
73
+ dueDate?: string;
74
+ }): Promise<Action>;
75
+ completeAction(id: string): Promise<Action>;
76
+ getGoals(workspaceId?: string): Promise<Goal[]>;
77
+ getGoal(id: number): Promise<Goal>;
78
+ quickCreateAction(text: string): Promise<Action>;
79
+ search(query: string): Promise<{
80
+ projects: Project[];
81
+ actions: Action[];
82
+ goals: Goal[];
83
+ }>;
84
+ }
package/dist/api.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Exponential API Client
3
+ * Handles authenticated requests to the Exponential API
4
+ */
5
+ const DEFAULT_BASE_URL = 'https://www.exponential.im';
6
+ export class ExponentialAPI {
7
+ apiKey;
8
+ baseUrl;
9
+ constructor(config) {
10
+ this.apiKey = config.apiKey;
11
+ this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
12
+ }
13
+ async request(endpoint, options = {}) {
14
+ const url = `${this.baseUrl}${endpoint}`;
15
+ const response = await fetch(url, {
16
+ ...options,
17
+ headers: {
18
+ 'Authorization': `Bearer ${this.apiKey}`,
19
+ 'Content-Type': 'application/json',
20
+ ...options.headers,
21
+ },
22
+ });
23
+ if (!response.ok) {
24
+ const error = await response.text();
25
+ throw new Error(`Exponential API error (${response.status}): ${error}`);
26
+ }
27
+ return response.json();
28
+ }
29
+ // tRPC-style calls
30
+ async trpcQuery(procedure, input) {
31
+ const inputStr = input ? encodeURIComponent(JSON.stringify(input)) : '';
32
+ const url = `/api/trpc/${procedure}${inputStr ? `?input=${inputStr}` : ''}`;
33
+ return this.request(url).then(r => r.result.data);
34
+ }
35
+ async trpcMutation(procedure, input) {
36
+ const url = `/api/trpc/${procedure}`;
37
+ return this.request(url, {
38
+ method: 'POST',
39
+ body: JSON.stringify(input),
40
+ }).then(r => r.result.data);
41
+ }
42
+ // === Workspace ===
43
+ async getWorkspaces() {
44
+ return this.trpcQuery('workspace.list');
45
+ }
46
+ // === Projects ===
47
+ async getProjects(workspaceId) {
48
+ return this.trpcQuery('project.list', { workspaceId });
49
+ }
50
+ async getProject(id) {
51
+ return this.trpcQuery('project.get', { id });
52
+ }
53
+ // === Actions ===
54
+ async getActions(params) {
55
+ return this.trpcQuery('action.list', params);
56
+ }
57
+ async createAction(data) {
58
+ return this.trpcMutation('action.create', data);
59
+ }
60
+ async updateAction(id, data) {
61
+ return this.trpcMutation('action.update', { id, ...data });
62
+ }
63
+ async completeAction(id) {
64
+ return this.updateAction(id, { status: 'COMPLETED' });
65
+ }
66
+ // === Goals (OKRs) ===
67
+ async getGoals(workspaceId) {
68
+ return this.trpcQuery('goal.list', { workspaceId });
69
+ }
70
+ async getGoal(id) {
71
+ return this.trpcQuery('goal.get', { id });
72
+ }
73
+ // === Quick Actions ===
74
+ async quickCreateAction(text) {
75
+ // Uses natural language parsing (dates, project names, etc.)
76
+ return this.trpcMutation('action.quickCreate', { text });
77
+ }
78
+ // === Search ===
79
+ async search(query) {
80
+ return this.trpcQuery('search.global', { query });
81
+ }
82
+ }
package/dist/cli.js CHANGED
File without changes
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
- configStore.saveConfig({
23
- token: legacy.apiKey,
24
- apiUrl: legacy.baseUrl || 'https://www.exponential.im',
25
- });
53
+ if (!legacy?.apiKey) {
54
+ return;
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;
26
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
  /**
@@ -267,7 +311,7 @@ async function main() {
267
311
  const trpcClient = client.client;
268
312
  const server = new Server({
269
313
  name: 'exponential-mcp',
270
- version: '0.1.0',
314
+ version: PKG_VERSION,
271
315
  }, {
272
316
  capabilities: {
273
317
  tools: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exponential-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.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.0.0",
27
+ "@modelcontextprotocol/sdk": "^1.30.0",
27
28
  "commander": "^12.0.0",
28
- "exponential-sdk": "^1.10.0",
29
+ "exponential-sdk": "^1.11.0",
29
30
  "zod": "^3.22.0"
30
31
  },
31
32
  "devDependencies": {