0nmcp 1.0.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/LICENSE +31 -0
- package/README.md +233 -0
- package/catalog.js +433 -0
- package/cli.js +324 -0
- package/connections.js +234 -0
- package/crm.js +661 -0
- package/index.js +270 -0
- package/orchestrator.js +447 -0
- package/package.json +72 -0
package/cli.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
* 0nMCP - CLI
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx 0nmcp Start MCP server (stdio)
|
|
9
|
+
* npx 0nmcp init Initialize ~/.0n directory
|
|
10
|
+
* npx 0nmcp connect Interactive connection setup
|
|
11
|
+
* npx 0nmcp list List connected services
|
|
12
|
+
* npx 0nmcp migrate Migrate from ~/.0nmcp to ~/.0n
|
|
13
|
+
*
|
|
14
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
import readline from 'readline';
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
// Colors
|
|
27
|
+
const c = {
|
|
28
|
+
reset: '\x1b[0m',
|
|
29
|
+
bright: '\x1b[1m',
|
|
30
|
+
red: '\x1b[31m',
|
|
31
|
+
green: '\x1b[32m',
|
|
32
|
+
yellow: '\x1b[33m',
|
|
33
|
+
blue: '\x1b[34m',
|
|
34
|
+
cyan: '\x1b[36m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const BANNER = `
|
|
38
|
+
${c.cyan}${c.bright}
|
|
39
|
+
██████╗ ███╗ ██╗███╗ ███╗ ██████╗██████╗
|
|
40
|
+
██╔═████╗████╗ ██║████╗ ████║██╔════╝██╔══██╗
|
|
41
|
+
██║██╔██║██╔██╗ ██║██╔████╔██║██║ ██████╔╝
|
|
42
|
+
████╔╝██║██║╚██╗██║██║╚██╔╝██║██║ ██╔═══╝
|
|
43
|
+
╚██████╔╝██║ ╚████║██║ ╚═╝ ██║╚██████╗██║
|
|
44
|
+
╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝╚═╝
|
|
45
|
+
${c.reset}
|
|
46
|
+
${c.bright}Universal AI API Orchestrator${c.reset}
|
|
47
|
+
${c.cyan}Connect your apps. Say what you want. AI does the rest.${c.reset}
|
|
48
|
+
|
|
49
|
+
${c.yellow}https://0nmcp.com${c.reset} | ${c.yellow}https://0nork.com${c.reset}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const DOT_ON_DIR = path.join(os.homedir(), '.0n');
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
const command = args[0];
|
|
57
|
+
|
|
58
|
+
// No args = start MCP server
|
|
59
|
+
if (!command) {
|
|
60
|
+
// Import and run the server
|
|
61
|
+
await import('./index.js');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Help
|
|
66
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
67
|
+
console.log(BANNER);
|
|
68
|
+
console.log(`
|
|
69
|
+
${c.bright}Usage:${c.reset}
|
|
70
|
+
|
|
71
|
+
${c.cyan}npx 0nmcp${c.reset} Start MCP server (for Claude Desktop)
|
|
72
|
+
${c.cyan}npx 0nmcp init${c.reset} Initialize ~/.0n directory
|
|
73
|
+
${c.cyan}npx 0nmcp connect${c.reset} Interactive connection setup
|
|
74
|
+
${c.cyan}npx 0nmcp list${c.reset} List connected services
|
|
75
|
+
${c.cyan}npx 0nmcp migrate${c.reset} Migrate from ~/.0nmcp to ~/.0n
|
|
76
|
+
|
|
77
|
+
${c.bright}Configure Claude Desktop:${c.reset}
|
|
78
|
+
|
|
79
|
+
Add to your claude_desktop_config.json:
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"0nmcp": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["-y", "0nmcp"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
${c.bright}Links:${c.reset}
|
|
91
|
+
|
|
92
|
+
Documentation: ${c.yellow}https://0nmcp.com/docs${c.reset}
|
|
93
|
+
GitHub: ${c.yellow}https://github.com/0nork/0nmcp${c.reset}
|
|
94
|
+
Discord: ${c.yellow}https://discord.gg/0nmcp${c.reset}
|
|
95
|
+
`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Init
|
|
100
|
+
if (command === 'init') {
|
|
101
|
+
console.log(BANNER);
|
|
102
|
+
initDotOn();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// List
|
|
107
|
+
if (command === 'list') {
|
|
108
|
+
console.log(BANNER);
|
|
109
|
+
await listConnections();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Connect
|
|
114
|
+
if (command === 'connect') {
|
|
115
|
+
console.log(BANNER);
|
|
116
|
+
await interactiveConnect();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Migrate
|
|
121
|
+
if (command === 'migrate') {
|
|
122
|
+
console.log(BANNER);
|
|
123
|
+
const { migrateLegacy } = await import('./connections.js');
|
|
124
|
+
const result = await migrateLegacy();
|
|
125
|
+
console.log(result);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Unknown command
|
|
130
|
+
console.log(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
131
|
+
console.log(`Run ${c.cyan}npx 0nmcp help${c.reset} for usage`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function initDotOn() {
|
|
136
|
+
const dirs = [
|
|
137
|
+
DOT_ON_DIR,
|
|
138
|
+
path.join(DOT_ON_DIR, 'connections'),
|
|
139
|
+
path.join(DOT_ON_DIR, 'workflows'),
|
|
140
|
+
path.join(DOT_ON_DIR, 'snapshots'),
|
|
141
|
+
path.join(DOT_ON_DIR, 'history'),
|
|
142
|
+
path.join(DOT_ON_DIR, 'cache'),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
console.log(`${c.bright}Initializing ~/.0n directory...${c.reset}\n`);
|
|
146
|
+
|
|
147
|
+
for (const dir of dirs) {
|
|
148
|
+
if (!fs.existsSync(dir)) {
|
|
149
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
150
|
+
console.log(`${c.green}✓${c.reset} Created ${dir}`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`${c.blue}○${c.reset} Exists ${dir}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create default config
|
|
157
|
+
const configPath = path.join(DOT_ON_DIR, 'config.json');
|
|
158
|
+
if (!fs.existsSync(configPath)) {
|
|
159
|
+
const config = {
|
|
160
|
+
"$0n": {
|
|
161
|
+
type: "config",
|
|
162
|
+
version: "1.0.0",
|
|
163
|
+
created: new Date().toISOString(),
|
|
164
|
+
},
|
|
165
|
+
settings: {
|
|
166
|
+
ai_provider: "anthropic",
|
|
167
|
+
fallback_mode: "keyword",
|
|
168
|
+
history_enabled: true,
|
|
169
|
+
cache_enabled: true,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
173
|
+
console.log(`${c.green}✓${c.reset} Created config.json`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`
|
|
177
|
+
${c.green}${c.bright}~/.0n initialized!${c.reset}
|
|
178
|
+
|
|
179
|
+
${c.bright}Next steps:${c.reset}
|
|
180
|
+
|
|
181
|
+
1. Connect a service:
|
|
182
|
+
${c.cyan}npx 0nmcp connect${c.reset}
|
|
183
|
+
|
|
184
|
+
2. Configure Claude Desktop:
|
|
185
|
+
Add to claude_desktop_config.json:
|
|
186
|
+
|
|
187
|
+
${c.yellow}{
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"0nmcp": {
|
|
190
|
+
"command": "npx",
|
|
191
|
+
"args": ["-y", "0nmcp"]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}${c.reset}
|
|
195
|
+
|
|
196
|
+
3. Restart Claude Desktop and start orchestrating!
|
|
197
|
+
`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function listConnections() {
|
|
201
|
+
const connectionsDir = path.join(DOT_ON_DIR, 'connections');
|
|
202
|
+
|
|
203
|
+
if (!fs.existsSync(connectionsDir)) {
|
|
204
|
+
console.log(`${c.yellow}No connections found.${c.reset}`);
|
|
205
|
+
console.log(`Run ${c.cyan}npx 0nmcp connect${c.reset} to add one.`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const files = fs.readdirSync(connectionsDir).filter(f => f.endsWith('.0n'));
|
|
210
|
+
|
|
211
|
+
if (files.length === 0) {
|
|
212
|
+
console.log(`${c.yellow}No connections found.${c.reset}`);
|
|
213
|
+
console.log(`Run ${c.cyan}npx 0nmcp connect${c.reset} to add one.`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`${c.bright}Connected Services:${c.reset}\n`);
|
|
218
|
+
|
|
219
|
+
for (const file of files) {
|
|
220
|
+
try {
|
|
221
|
+
const data = JSON.parse(fs.readFileSync(path.join(connectionsDir, file), 'utf8'));
|
|
222
|
+
console.log(` ${c.green}●${c.reset} ${c.bright}${data.$0n?.name || data.service}${c.reset}`);
|
|
223
|
+
console.log(` Service: ${data.service}`);
|
|
224
|
+
console.log(` File: ${file}`);
|
|
225
|
+
console.log('');
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.log(` ${c.red}●${c.reset} ${file} (error reading)`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function interactiveConnect() {
|
|
233
|
+
const rl = readline.createInterface({
|
|
234
|
+
input: process.stdin,
|
|
235
|
+
output: process.stdout,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
239
|
+
|
|
240
|
+
console.log(`${c.bright}Connect a new service${c.reset}\n`);
|
|
241
|
+
|
|
242
|
+
const services = [
|
|
243
|
+
'stripe', 'slack', 'discord', 'twilio', 'sendgrid', 'resend',
|
|
244
|
+
'openai', 'anthropic', 'airtable', 'notion', 'supabase',
|
|
245
|
+
'github', 'linear', 'shopify', 'hubspot', 'calendly', 'gohighlevel',
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
console.log('Available services:');
|
|
249
|
+
services.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
|
|
250
|
+
console.log('');
|
|
251
|
+
|
|
252
|
+
const serviceInput = await ask('Enter service name or number: ');
|
|
253
|
+
let service = serviceInput;
|
|
254
|
+
|
|
255
|
+
const num = parseInt(serviceInput);
|
|
256
|
+
if (!isNaN(num) && num >= 1 && num <= services.length) {
|
|
257
|
+
service = services[num - 1];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!services.includes(service)) {
|
|
261
|
+
console.log(`${c.yellow}Custom service: ${service}${c.reset}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(`\nConnecting to ${c.cyan}${service}${c.reset}\n`);
|
|
265
|
+
|
|
266
|
+
// Get credentials based on service
|
|
267
|
+
const creds = {};
|
|
268
|
+
|
|
269
|
+
if (['stripe', 'openai', 'anthropic', 'sendgrid', 'resend', 'airtable', 'notion', 'calendly'].includes(service)) {
|
|
270
|
+
creds.api_key = await ask('API Key: ');
|
|
271
|
+
} else if (service === 'slack' || service === 'discord') {
|
|
272
|
+
creds.botToken = await ask('Bot Token: ');
|
|
273
|
+
} else if (service === 'twilio') {
|
|
274
|
+
creds.accountSid = await ask('Account SID: ');
|
|
275
|
+
creds.authToken = await ask('Auth Token: ');
|
|
276
|
+
} else if (service === 'github') {
|
|
277
|
+
creds.token = await ask('Personal Access Token: ');
|
|
278
|
+
} else if (service === 'gohighlevel') {
|
|
279
|
+
creds.access_token = await ask('Access Token: ');
|
|
280
|
+
creds.locationId = await ask('Location ID: ');
|
|
281
|
+
} else if (service === 'supabase') {
|
|
282
|
+
creds.url = await ask('Supabase URL: ');
|
|
283
|
+
creds.anonKey = await ask('Anon Key: ');
|
|
284
|
+
} else {
|
|
285
|
+
creds.api_key = await ask('API Key/Token: ');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const name = await ask(`Connection name (default: ${service}): `) || service;
|
|
289
|
+
|
|
290
|
+
rl.close();
|
|
291
|
+
|
|
292
|
+
// Save connection
|
|
293
|
+
const connection = {
|
|
294
|
+
"$0n": {
|
|
295
|
+
type: "connection",
|
|
296
|
+
version: "1.0.0",
|
|
297
|
+
created: new Date().toISOString(),
|
|
298
|
+
name: name,
|
|
299
|
+
},
|
|
300
|
+
service: service,
|
|
301
|
+
environment: "production",
|
|
302
|
+
auth: {
|
|
303
|
+
type: "api_key",
|
|
304
|
+
credentials: creds,
|
|
305
|
+
},
|
|
306
|
+
metadata: {
|
|
307
|
+
connected_at: new Date().toISOString(),
|
|
308
|
+
connected_by: "0nmcp-cli",
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const connectionsDir = path.join(DOT_ON_DIR, 'connections');
|
|
313
|
+
if (!fs.existsSync(connectionsDir)) {
|
|
314
|
+
fs.mkdirSync(connectionsDir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const filePath = path.join(connectionsDir, `${service}.0n`);
|
|
318
|
+
fs.writeFileSync(filePath, JSON.stringify(connection, null, 2));
|
|
319
|
+
|
|
320
|
+
console.log(`\n${c.green}✓${c.reset} Connected to ${c.bright}${service}${c.reset}`);
|
|
321
|
+
console.log(` Saved to: ${filePath}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
main().catch(console.error);
|
package/connections.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
+
* 0nMCP - Connection Management
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
*
|
|
6
|
+
* Manages service connections using the .0n standard.
|
|
7
|
+
* Stores credentials in ~/.0n/connections/ as individual .0n files.
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2025 0nORK - https://0nork.com
|
|
10
|
+
*
|
|
11
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// Directory Setup
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
const DOT_ON_DIR = path.join(os.homedir(), '.0n');
|
|
23
|
+
const CONNECTIONS_DIR = path.join(DOT_ON_DIR, 'connections');
|
|
24
|
+
const CONFIG_FILE = path.join(DOT_ON_DIR, 'config.json');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the ~/.0n directory structure
|
|
28
|
+
*/
|
|
29
|
+
export function initDotOn() {
|
|
30
|
+
const dirs = [
|
|
31
|
+
DOT_ON_DIR,
|
|
32
|
+
CONNECTIONS_DIR,
|
|
33
|
+
path.join(DOT_ON_DIR, 'workflows'),
|
|
34
|
+
path.join(DOT_ON_DIR, 'snapshots'),
|
|
35
|
+
path.join(DOT_ON_DIR, 'history'),
|
|
36
|
+
path.join(DOT_ON_DIR, 'cache'),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const dir of dirs) {
|
|
40
|
+
if (!fs.existsSync(dir)) {
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create default config if not exists
|
|
46
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
47
|
+
const config = {
|
|
48
|
+
"$0n": {
|
|
49
|
+
type: "config",
|
|
50
|
+
version: "1.0.0",
|
|
51
|
+
created: new Date().toISOString(),
|
|
52
|
+
},
|
|
53
|
+
settings: {
|
|
54
|
+
ai_provider: "anthropic",
|
|
55
|
+
fallback_mode: "keyword",
|
|
56
|
+
history_enabled: true,
|
|
57
|
+
cache_enabled: true,
|
|
58
|
+
},
|
|
59
|
+
default_services: [],
|
|
60
|
+
};
|
|
61
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return DOT_ON_DIR;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// Connection CRUD
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get all connections as a flat object { service: credentials }
|
|
73
|
+
*/
|
|
74
|
+
export async function getConnections() {
|
|
75
|
+
initDotOn();
|
|
76
|
+
|
|
77
|
+
const connections = {};
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(CONNECTIONS_DIR)) {
|
|
80
|
+
return connections;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const files = fs.readdirSync(CONNECTIONS_DIR);
|
|
84
|
+
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
if (file.endsWith('.0n') || file.endsWith('.0n.json')) {
|
|
87
|
+
try {
|
|
88
|
+
const filePath = path.join(CONNECTIONS_DIR, file);
|
|
89
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
90
|
+
|
|
91
|
+
if (data.$0n?.type === 'connection' && data.service) {
|
|
92
|
+
// Flatten credentials for easy use
|
|
93
|
+
connections[data.service] = {
|
|
94
|
+
...data.auth?.credentials,
|
|
95
|
+
...data.options,
|
|
96
|
+
_name: data.$0n?.name,
|
|
97
|
+
_file: file,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error(`Error reading ${file}:`, e.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return connections;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Save a connection as a .0n file
|
|
111
|
+
*/
|
|
112
|
+
export async function saveConnection(service, credentials, name) {
|
|
113
|
+
initDotOn();
|
|
114
|
+
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
|
|
117
|
+
const connection = {
|
|
118
|
+
"$0n": {
|
|
119
|
+
type: "connection",
|
|
120
|
+
version: "1.0.0",
|
|
121
|
+
created: now,
|
|
122
|
+
updated: now,
|
|
123
|
+
name: name || service.charAt(0).toUpperCase() + service.slice(1),
|
|
124
|
+
},
|
|
125
|
+
service: service.toLowerCase(),
|
|
126
|
+
environment: "production",
|
|
127
|
+
auth: {
|
|
128
|
+
type: detectAuthType(credentials),
|
|
129
|
+
credentials: credentials,
|
|
130
|
+
},
|
|
131
|
+
options: {},
|
|
132
|
+
metadata: {
|
|
133
|
+
connected_at: now,
|
|
134
|
+
connected_by: "0nmcp",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const filePath = path.join(CONNECTIONS_DIR, `${service.toLowerCase()}.0n`);
|
|
139
|
+
fs.writeFileSync(filePath, JSON.stringify(connection, null, 2));
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
message: `Connected to ${service}`,
|
|
144
|
+
file: filePath,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* List all connections with status
|
|
150
|
+
*/
|
|
151
|
+
export async function listConnections() {
|
|
152
|
+
const connections = await getConnections();
|
|
153
|
+
|
|
154
|
+
return Object.entries(connections).map(([service, creds]) => ({
|
|
155
|
+
service,
|
|
156
|
+
name: creds._name || service,
|
|
157
|
+
connected: true,
|
|
158
|
+
file: creds._file,
|
|
159
|
+
has_api_key: !!creds.api_key || !!creds.apiKey,
|
|
160
|
+
has_token: !!creds.token || !!creds.access_token || !!creds.botToken,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Delete a connection
|
|
166
|
+
*/
|
|
167
|
+
export async function deleteConnection(service) {
|
|
168
|
+
const filePath = path.join(CONNECTIONS_DIR, `${service.toLowerCase()}.0n`);
|
|
169
|
+
|
|
170
|
+
if (fs.existsSync(filePath)) {
|
|
171
|
+
fs.unlinkSync(filePath);
|
|
172
|
+
return { success: true, message: `Disconnected from ${service}` };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { success: false, message: `${service} was not connected` };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get credentials for a specific service
|
|
180
|
+
*/
|
|
181
|
+
export async function getServiceCredentials(service) {
|
|
182
|
+
const connections = await getConnections();
|
|
183
|
+
return connections[service.toLowerCase()] || null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
187
|
+
// Helpers
|
|
188
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
189
|
+
|
|
190
|
+
function detectAuthType(credentials) {
|
|
191
|
+
if (credentials.access_token && credentials.refresh_token) {
|
|
192
|
+
return 'oauth2';
|
|
193
|
+
}
|
|
194
|
+
if (credentials.username && credentials.password) {
|
|
195
|
+
return 'basic';
|
|
196
|
+
}
|
|
197
|
+
if (credentials.token || credentials.botToken) {
|
|
198
|
+
return 'bearer';
|
|
199
|
+
}
|
|
200
|
+
return 'api_key';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
204
|
+
// Migration from .0nmcp (legacy)
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
|
|
207
|
+
export async function migrateLegacy() {
|
|
208
|
+
const legacyDir = path.join(os.homedir(), '.0nmcp');
|
|
209
|
+
const legacyFile = path.join(legacyDir, 'connections.json');
|
|
210
|
+
|
|
211
|
+
if (!fs.existsSync(legacyFile)) {
|
|
212
|
+
return { migrated: 0, message: 'No legacy config found' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
initDotOn();
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const legacyData = JSON.parse(fs.readFileSync(legacyFile, 'utf8'));
|
|
219
|
+
let count = 0;
|
|
220
|
+
|
|
221
|
+
for (const [service, credentials] of Object.entries(legacyData)) {
|
|
222
|
+
await saveConnection(service, credentials);
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
migrated: count,
|
|
228
|
+
message: `Migrated ${count} connections from ~/.0nmcp to ~/.0n`,
|
|
229
|
+
note: 'You can safely delete ~/.0nmcp/',
|
|
230
|
+
};
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return { error: e.message };
|
|
233
|
+
}
|
|
234
|
+
}
|