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/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
+ }