exponential-mcp 0.1.1
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 +132 -0
- package/dist/api.d.ts +84 -0
- package/dist/api.js +82 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +211 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +299 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Exponential MCP
|
|
2
|
+
|
|
3
|
+
Connect Claude to your [Exponential](https://exponential.im) workspace. Manage projects, actions, and OKRs directly from Claude.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Create an API Key
|
|
8
|
+
|
|
9
|
+
Go to [exponential.im/settings/api-keys](https://www.exponential.im/settings/api-keys) and create a new key:
|
|
10
|
+
- Click **Create API Key**
|
|
11
|
+
- Select **JWT Token** as the token type
|
|
12
|
+
- Copy the generated key
|
|
13
|
+
|
|
14
|
+
### 2. Set Up the MCP Server
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx exponential-mcp init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Paste your API key when prompted.
|
|
21
|
+
|
|
22
|
+
### 3. Configure for Your Claude Client
|
|
23
|
+
|
|
24
|
+
**For Claude Desktop:**
|
|
25
|
+
- `exponential-mcp init` will automatically configure Claude Desktop
|
|
26
|
+
- Restart Claude Desktop
|
|
27
|
+
|
|
28
|
+
**For Claude Code (VSCode Extension):**
|
|
29
|
+
- The server uses `.mcp.json` in your project directory
|
|
30
|
+
- Reload your VSCode window (`Cmd+Shift+P` → "Developer: Reload Window")
|
|
31
|
+
- The server will be available in your current project
|
|
32
|
+
|
|
33
|
+
### 4. Ask Claude to manage your tasks!
|
|
34
|
+
|
|
35
|
+
## What Claude Can Do
|
|
36
|
+
|
|
37
|
+
Once connected, Claude can:
|
|
38
|
+
|
|
39
|
+
- **List projects** – "What projects am I working on?"
|
|
40
|
+
- **View actions** – "Show my active tasks"
|
|
41
|
+
- **Create actions** – "Add a task to call John tomorrow"
|
|
42
|
+
- **Complete actions** – "Mark the report task as done"
|
|
43
|
+
- **View OKRs** – "What are my Q1 goals?"
|
|
44
|
+
- **Search** – "Find anything related to Kenya"
|
|
45
|
+
|
|
46
|
+
## Manual Setup
|
|
47
|
+
|
|
48
|
+
### For Claude Desktop
|
|
49
|
+
|
|
50
|
+
**macOS (recommended path):** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
51
|
+
**Legacy path:** `~/.claude/claude_desktop_config.json`
|
|
52
|
+
|
|
53
|
+
Add this to the appropriate file:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"exponential": {
|
|
59
|
+
"command": "/absolute/path/to/node",
|
|
60
|
+
"args": ["/absolute/path/to/exponential-mcp/dist/index.js"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### For Claude Code (VSCode Extension)
|
|
67
|
+
|
|
68
|
+
Create a `.mcp.json` file in your project directory:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"exponential": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["-y", "exponential-mcp", "serve"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then reload your VSCode window.
|
|
82
|
+
|
|
83
|
+
**Note:** The API key is stored in the Exponential SDK config store (created by `npx exponential-mcp init`), so you don't need to specify it in the MCP configuration. Run `exponential-mcp config` to see the current storage path and values. Run `exponential-mcp doctor` to print a recommended MCP config snippet for your machine.
|
|
84
|
+
|
|
85
|
+
## Commands
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Initialize with your API key
|
|
89
|
+
exponential-mcp init
|
|
90
|
+
|
|
91
|
+
# Show current config
|
|
92
|
+
exponential-mcp config
|
|
93
|
+
|
|
94
|
+
# Diagnose local setup
|
|
95
|
+
exponential-mcp doctor
|
|
96
|
+
|
|
97
|
+
# Start server manually (usually not needed)
|
|
98
|
+
exponential-mcp serve
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Available Tools
|
|
102
|
+
|
|
103
|
+
| Tool | Description |
|
|
104
|
+
|------|-------------|
|
|
105
|
+
| `get_workspaces` | List all workspaces |
|
|
106
|
+
| `get_projects` | List projects (optionally by workspace) |
|
|
107
|
+
| `get_actions` | List actions/tasks (filter by project or status) |
|
|
108
|
+
| `create_action` | Create a new task (supports natural language) |
|
|
109
|
+
| `complete_action` | Mark an action as done |
|
|
110
|
+
| `get_goals` | List OKRs with progress |
|
|
111
|
+
| `search` | Search across everything |
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Clone the repo
|
|
117
|
+
git clone https://github.com/your-org/exponential-mcp
|
|
118
|
+
cd exponential-mcp
|
|
119
|
+
|
|
120
|
+
# Install dependencies
|
|
121
|
+
npm install
|
|
122
|
+
|
|
123
|
+
# Build
|
|
124
|
+
npm run build
|
|
125
|
+
|
|
126
|
+
# Run locally
|
|
127
|
+
npm start
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
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.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Exponential MCP CLI
|
|
4
|
+
* Setup and configuration for the MCP server
|
|
5
|
+
*/
|
|
6
|
+
import { program } from 'commander';
|
|
7
|
+
import { createConfigStore } from 'exponential-sdk';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import * as readline from 'readline';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
const CONFIG_DIR = join(homedir(), '.config', 'exponential-mcp');
|
|
14
|
+
const LEGACY_CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
15
|
+
const configStore = createConfigStore({ projectName: 'exponential-mcp' });
|
|
16
|
+
const LEGACY_CLAUDE_CONFIG_PATH = join(homedir(), '.claude', 'claude_desktop_config.json');
|
|
17
|
+
const MACOS_CLAUDE_CONFIG_PATH = join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
18
|
+
function prompt(question) {
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
rl.question(question, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(answer.trim());
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function migrateLegacyConfig() {
|
|
31
|
+
if (configStore.isAuthenticated() || !existsSync(LEGACY_CONFIG_PATH)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const legacy = JSON.parse(readFileSync(LEGACY_CONFIG_PATH, 'utf-8'));
|
|
36
|
+
if (legacy?.apiKey) {
|
|
37
|
+
configStore.saveConfig({
|
|
38
|
+
token: legacy.apiKey,
|
|
39
|
+
apiUrl: legacy.baseUrl || 'https://www.exponential.im',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Ignore legacy config parsing errors.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function getClaudeConfigPath() {
|
|
48
|
+
if (process.platform === 'darwin') {
|
|
49
|
+
if (existsSync(MACOS_CLAUDE_CONFIG_PATH)) {
|
|
50
|
+
return MACOS_CLAUDE_CONFIG_PATH;
|
|
51
|
+
}
|
|
52
|
+
if (existsSync(LEGACY_CLAUDE_CONFIG_PATH)) {
|
|
53
|
+
return LEGACY_CLAUDE_CONFIG_PATH;
|
|
54
|
+
}
|
|
55
|
+
return MACOS_CLAUDE_CONFIG_PATH;
|
|
56
|
+
}
|
|
57
|
+
return LEGACY_CLAUDE_CONFIG_PATH;
|
|
58
|
+
}
|
|
59
|
+
function getServerEntry() {
|
|
60
|
+
const serverPath = fileURLToPath(new URL('./index.js', import.meta.url));
|
|
61
|
+
return {
|
|
62
|
+
command: process.execPath,
|
|
63
|
+
args: [serverPath],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function ensureParentDir(filePath) {
|
|
67
|
+
const dir = dirname(filePath);
|
|
68
|
+
if (!existsSync(dir)) {
|
|
69
|
+
mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
program
|
|
73
|
+
.name('exponential-mcp')
|
|
74
|
+
.description('MCP server for Exponential - connect Claude to your productivity data')
|
|
75
|
+
.version('0.1.0');
|
|
76
|
+
program
|
|
77
|
+
.command('init')
|
|
78
|
+
.description('Set up Exponential MCP with your API key')
|
|
79
|
+
.option('-k, --key <key>', 'API key (or will prompt)')
|
|
80
|
+
.option('--base-url <url>', 'Custom Exponential URL (default: https://www.exponential.im)')
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
console.log('🚀 Setting up Exponential MCP...\n');
|
|
83
|
+
// Get API key
|
|
84
|
+
let apiKey = options.key;
|
|
85
|
+
if (!apiKey) {
|
|
86
|
+
console.log('Create an API key at: https://www.exponential.im/settings/api-keys');
|
|
87
|
+
console.log(' → Select "JWT Token" as the token type\n');
|
|
88
|
+
apiKey = await prompt('Paste your API key: ');
|
|
89
|
+
}
|
|
90
|
+
if (!apiKey) {
|
|
91
|
+
console.error('❌ API key is required');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
// Create config directory for legacy migrations.
|
|
95
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
96
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
configStore.saveConfig({
|
|
99
|
+
token: apiKey,
|
|
100
|
+
apiUrl: options.baseUrl || 'https://www.exponential.im',
|
|
101
|
+
});
|
|
102
|
+
console.log(`✅ Config saved to ${configStore.getConfigPath()}\n`);
|
|
103
|
+
// Update Claude Desktop config
|
|
104
|
+
await updateClaudeConfig();
|
|
105
|
+
console.log('\n🎉 Setup complete!\n');
|
|
106
|
+
console.log('Next steps:');
|
|
107
|
+
console.log(' 1. Restart Claude Desktop');
|
|
108
|
+
console.log(' 2. Ask Claude to "list my projects" or "create a task"\n');
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command('config')
|
|
112
|
+
.description('Show current configuration')
|
|
113
|
+
.action(() => {
|
|
114
|
+
migrateLegacyConfig();
|
|
115
|
+
if (!configStore.isAuthenticated()) {
|
|
116
|
+
console.log('No config found. Run "exponential-mcp init" first.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const config = configStore.loadConfig();
|
|
120
|
+
console.log('Current config:');
|
|
121
|
+
console.log(` Base URL: ${config.apiUrl}`);
|
|
122
|
+
console.log(` API Key: ${config.token.substring(0, 10)}...`);
|
|
123
|
+
});
|
|
124
|
+
program
|
|
125
|
+
.command('serve')
|
|
126
|
+
.description('Start the MCP server (usually called by Claude Desktop)')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
// Import and run the server
|
|
129
|
+
await import('./index.js');
|
|
130
|
+
});
|
|
131
|
+
program
|
|
132
|
+
.command('doctor')
|
|
133
|
+
.description('Diagnose local MCP setup for Claude Desktop')
|
|
134
|
+
.action(() => {
|
|
135
|
+
migrateLegacyConfig();
|
|
136
|
+
const issues = [];
|
|
137
|
+
const configPath = getClaudeConfigPath();
|
|
138
|
+
const recommended = getServerEntry();
|
|
139
|
+
console.log('🩺 Exponential MCP Doctor\n');
|
|
140
|
+
if (!configStore.isAuthenticated()) {
|
|
141
|
+
issues.push('No API key found. Run "exponential-mcp init" first.');
|
|
142
|
+
}
|
|
143
|
+
if (!existsSync(configPath)) {
|
|
144
|
+
issues.push(`Claude Desktop config not found at ${configPath}`);
|
|
145
|
+
}
|
|
146
|
+
let config = null;
|
|
147
|
+
if (existsSync(configPath)) {
|
|
148
|
+
try {
|
|
149
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
issues.push(`Claude Desktop config is not valid JSON: ${configPath}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const serverConfig = config?.mcpServers?.exponential;
|
|
156
|
+
if (!serverConfig) {
|
|
157
|
+
issues.push('No "exponential" entry found in Claude Desktop config.');
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
if (!serverConfig.command) {
|
|
161
|
+
issues.push('MCP config is missing "command" for Exponential.');
|
|
162
|
+
}
|
|
163
|
+
else if (!existsSync(serverConfig.command)) {
|
|
164
|
+
issues.push(`MCP command not found: ${serverConfig.command}`);
|
|
165
|
+
}
|
|
166
|
+
const serverArg = Array.isArray(serverConfig.args) ? serverConfig.args[0] : null;
|
|
167
|
+
if (!serverArg) {
|
|
168
|
+
issues.push('MCP config args missing server entry point.');
|
|
169
|
+
}
|
|
170
|
+
else if (!existsSync(serverArg)) {
|
|
171
|
+
issues.push(`MCP server entry not found: ${serverArg}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (issues.length === 0) {
|
|
175
|
+
console.log('✅ No issues found. Claude Desktop should load the Exponential MCP server.');
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log('⚠️ Issues found:\n');
|
|
179
|
+
for (const issue of issues) {
|
|
180
|
+
console.log(`- ${issue}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
console.log('\nRecommended MCP config:\n');
|
|
184
|
+
console.log(JSON.stringify({
|
|
185
|
+
mcpServers: {
|
|
186
|
+
exponential: recommended,
|
|
187
|
+
},
|
|
188
|
+
}, null, 2));
|
|
189
|
+
});
|
|
190
|
+
async function updateClaudeConfig() {
|
|
191
|
+
const configPath = getClaudeConfigPath();
|
|
192
|
+
ensureParentDir(configPath);
|
|
193
|
+
let claudeConfig = { mcpServers: {} };
|
|
194
|
+
// Read existing config if present
|
|
195
|
+
if (existsSync(configPath)) {
|
|
196
|
+
try {
|
|
197
|
+
claudeConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
198
|
+
if (!claudeConfig.mcpServers) {
|
|
199
|
+
claudeConfig.mcpServers = {};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
console.log('⚠️ Could not parse existing Claude config, creating new one');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Add exponential server
|
|
207
|
+
claudeConfig.mcpServers.exponential = getServerEntry();
|
|
208
|
+
writeFileSync(configPath, JSON.stringify(claudeConfig, null, 2));
|
|
209
|
+
console.log(`✅ Claude Desktop config updated: ${configPath}`);
|
|
210
|
+
}
|
|
211
|
+
program.parse();
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Exponential MCP Server
|
|
4
|
+
* Connects Claude to your Exponential workspace
|
|
5
|
+
*/
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { ExponentialClient, createConfigStore } from 'exponential-sdk';
|
|
10
|
+
import { readFileSync, existsSync } from 'fs';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
const LEGACY_CONFIG_PATH = join(homedir(), '.config', 'exponential-mcp', 'config.json');
|
|
14
|
+
const configStore = createConfigStore({ projectName: 'exponential-mcp' });
|
|
15
|
+
function migrateLegacyConfig() {
|
|
16
|
+
if (configStore.isAuthenticated() || !existsSync(LEGACY_CONFIG_PATH)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
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
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore legacy config parsing errors.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function loadClientConfig() {
|
|
33
|
+
migrateLegacyConfig();
|
|
34
|
+
if (configStore.isAuthenticated()) {
|
|
35
|
+
const config = configStore.loadConfig();
|
|
36
|
+
return { token: config.token, apiUrl: config.apiUrl };
|
|
37
|
+
}
|
|
38
|
+
const token = process.env.EXPONENTIAL_API_KEY || process.env.EXPONENTIAL_API_TOKEN;
|
|
39
|
+
const apiUrl = process.env.EXPONENTIAL_API_URL ||
|
|
40
|
+
process.env.EXPONENTIAL_BASE_URL ||
|
|
41
|
+
'https://www.exponential.im';
|
|
42
|
+
if (!token) {
|
|
43
|
+
console.error('Error: No API key found.');
|
|
44
|
+
console.error('Run "npx exponential-mcp init" to set up, or set EXPONENTIAL_API_KEY env var.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
return { token, apiUrl };
|
|
48
|
+
}
|
|
49
|
+
// Tool definitions
|
|
50
|
+
const TOOLS = [
|
|
51
|
+
{
|
|
52
|
+
name: 'get_projects',
|
|
53
|
+
description: 'List all projects in Exponential. Returns project names, status, and progress.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
workspaceId: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Optional workspace ID to filter projects'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'get_actions',
|
|
66
|
+
description: 'List actions/tasks. Can filter by project or status.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
projectId: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Filter by project ID'
|
|
73
|
+
},
|
|
74
|
+
status: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
enum: ['ACTIVE', 'COMPLETED', 'CANCELLED'],
|
|
77
|
+
description: 'Filter by status (default: ACTIVE)'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'create_action',
|
|
84
|
+
description: 'Create a new action/task. Supports natural language with dates and project names.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
text: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Action description (can include "tomorrow", "next week", project names, etc.)'
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
required: ['text']
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'complete_action',
|
|
98
|
+
description: 'Mark an action as completed',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
id: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'Action ID to complete'
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
required: ['id']
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'get_goals',
|
|
112
|
+
description: 'List OKRs (Objectives and Key Results) with their progress',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
workspaceId: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'Optional workspace ID'
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'search',
|
|
125
|
+
description: 'Search across projects, actions, and goals',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
query: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Search query'
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
required: ['query']
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'get_workspaces',
|
|
139
|
+
description: 'List all workspaces the user has access to',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
async function main() {
|
|
147
|
+
const config = loadClientConfig();
|
|
148
|
+
const client = new ExponentialClient({
|
|
149
|
+
token: config.token,
|
|
150
|
+
apiUrl: config.apiUrl,
|
|
151
|
+
});
|
|
152
|
+
const trpcClient = client.client;
|
|
153
|
+
const server = new Server({
|
|
154
|
+
name: 'exponential-mcp',
|
|
155
|
+
version: '0.1.0',
|
|
156
|
+
}, {
|
|
157
|
+
capabilities: {
|
|
158
|
+
tools: {},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
// List available tools
|
|
162
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
163
|
+
tools: TOOLS,
|
|
164
|
+
}));
|
|
165
|
+
// Handle tool calls
|
|
166
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
167
|
+
const { name, arguments: args } = request.params;
|
|
168
|
+
try {
|
|
169
|
+
switch (name) {
|
|
170
|
+
case 'get_projects': {
|
|
171
|
+
const projects = await client.projects.list({
|
|
172
|
+
workspaceId: args?.workspaceId,
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: JSON.stringify(projects, null, 2),
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
case 'get_actions': {
|
|
184
|
+
const status = args?.status;
|
|
185
|
+
const projectId = args?.projectId;
|
|
186
|
+
let actions;
|
|
187
|
+
if (status === 'COMPLETED') {
|
|
188
|
+
actions = await client.actions.getKanban({
|
|
189
|
+
projectId,
|
|
190
|
+
status: 'DONE',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else if (status === 'CANCELLED') {
|
|
194
|
+
actions = await client.actions.getKanban({
|
|
195
|
+
projectId,
|
|
196
|
+
status: 'CANCELLED',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
actions = await client.actions.list({ projectId });
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: 'text',
|
|
206
|
+
text: JSON.stringify(actions, null, 2),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
case 'create_action': {
|
|
212
|
+
// SDK does not yet expose quickCreate, so we use the underlying tRPC client.
|
|
213
|
+
const action = await trpcClient.action.quickCreate.mutate({
|
|
214
|
+
text: args?.text,
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: `Created action: ${action.name} (ID: ${action.id})`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
case 'complete_action': {
|
|
226
|
+
// SDK does not yet expose action updates, so we use the underlying tRPC client.
|
|
227
|
+
const action = await trpcClient.action.update.mutate({
|
|
228
|
+
id: args?.id,
|
|
229
|
+
status: 'COMPLETED',
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: 'text',
|
|
235
|
+
text: `Completed: ${action.name}`,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
case 'get_goals': {
|
|
241
|
+
const goals = await trpcClient.goal.list.query({
|
|
242
|
+
workspaceId: args?.workspaceId,
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: JSON.stringify(goals, null, 2),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
case 'search': {
|
|
254
|
+
const results = await trpcClient.search.global.query({
|
|
255
|
+
query: args?.query,
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: JSON.stringify(results, null, 2),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case 'get_workspaces': {
|
|
267
|
+
const workspaces = await client.workspaces.list();
|
|
268
|
+
return {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: JSON.stringify(workspaces, null, 2),
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: `Error: ${message}`,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
isError: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
// Start server
|
|
295
|
+
const transport = new StdioServerTransport();
|
|
296
|
+
await server.connect(transport);
|
|
297
|
+
console.error('Exponential MCP server running');
|
|
298
|
+
}
|
|
299
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exponential-mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for Exponential - connect Claude to your projects, actions, and goals",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"exponential-mcp": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"cli": "node dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"claude",
|
|
19
|
+
"exponential",
|
|
20
|
+
"productivity",
|
|
21
|
+
"anthropic"
|
|
22
|
+
],
|
|
23
|
+
"author": "Exponential",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
27
|
+
"commander": "^12.0.0",
|
|
28
|
+
"exponential-sdk": "^1.0.0",
|
|
29
|
+
"zod": "^3.22.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
]
|
|
41
|
+
}
|