0nmcp 1.4.0 → 1.6.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.
@@ -0,0 +1,94 @@
1
+ // ============================================================
2
+ // 0nMCP — Engine: Portable Cipher
3
+ // ============================================================
4
+ // AES-256-GCM encryption with PBKDF2 key derivation.
5
+ // PASSPHRASE-ONLY — no machine fingerprint binding.
6
+ // This makes bundles portable across different machines.
7
+ //
8
+ // For machine-bound encryption, use vault/cipher.js instead.
9
+ // Zero external dependencies — Node.js built-in `crypto` only.
10
+ //
11
+ // Patent Pending: US Provisional Patent Application #63/968,814
12
+ // ============================================================
13
+
14
+ import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from "crypto";
15
+
16
+ const ALGORITHM = "aes-256-gcm";
17
+ const KEY_LENGTH = 32;
18
+ const IV_LENGTH = 16;
19
+ const SALT_LENGTH = 32;
20
+ const TAG_LENGTH = 16;
21
+ const PBKDF2_ITERATIONS = 100000;
22
+ const PBKDF2_DIGEST = "sha512";
23
+
24
+ /**
25
+ * Derive encryption key from passphrase only (no fingerprint).
26
+ * This is the key difference from vault/cipher.js — bundles are portable.
27
+ */
28
+ function deriveKey(passphrase, salt) {
29
+ return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
30
+ }
31
+
32
+ /**
33
+ * Encrypt data with passphrase-only AES-256-GCM (portable).
34
+ * @param {string} plaintext
35
+ * @param {string} passphrase
36
+ * @returns {{ sealed: string }}
37
+ */
38
+ export function sealPortable(plaintext, passphrase) {
39
+ const salt = randomBytes(SALT_LENGTH);
40
+ const iv = randomBytes(IV_LENGTH);
41
+ const key = deriveKey(passphrase, salt);
42
+
43
+ const cipher = createCipheriv(ALGORITHM, key, iv);
44
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
45
+ const authTag = cipher.getAuthTag();
46
+
47
+ const combined = Buffer.concat([salt, iv, authTag, encrypted]);
48
+ return { sealed: combined.toString("base64") };
49
+ }
50
+
51
+ /**
52
+ * Decrypt portable-sealed data.
53
+ * @param {string} sealedData - Base64 sealed data
54
+ * @param {string} passphrase
55
+ * @returns {string} Decrypted plaintext
56
+ */
57
+ export function unsealPortable(sealedData, passphrase) {
58
+ const combined = Buffer.from(sealedData, "base64");
59
+
60
+ if (combined.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH + 1) {
61
+ throw new Error("Invalid sealed data: too short");
62
+ }
63
+
64
+ const salt = combined.subarray(0, SALT_LENGTH);
65
+ const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
66
+ const authTag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
67
+ const ciphertext = combined.subarray(SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
68
+
69
+ const key = deriveKey(passphrase, salt);
70
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
71
+ decipher.setAuthTag(authTag);
72
+
73
+ try {
74
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
75
+ return decrypted.toString("utf8");
76
+ } catch {
77
+ throw new Error("Unseal failed — wrong passphrase.");
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Verify sealed data can be decrypted with given passphrase.
83
+ * @param {string} sealedData
84
+ * @param {string} passphrase
85
+ * @returns {{ valid: boolean, error?: string }}
86
+ */
87
+ export function verifyPortable(sealedData, passphrase) {
88
+ try {
89
+ unsealPortable(sealedData, passphrase);
90
+ return { valid: true };
91
+ } catch (err) {
92
+ return { valid: false, error: err.message };
93
+ }
94
+ }
@@ -0,0 +1,390 @@
1
+ // ============================================================
2
+ // 0nMCP — Engine Module
3
+ // ============================================================
4
+ // The .0n Conversion Engine — import credentials, verify keys,
5
+ // generate platform configs, and create portable AI Brain
6
+ // bundle files.
7
+ //
8
+ // 6 MCP Tools:
9
+ // engine_import — Import credentials from .env/CSV/JSON
10
+ // engine_verify — Verify API keys with test calls
11
+ // engine_platforms — Generate platform configs
12
+ // engine_export — Export .0n bundle from connections
13
+ // engine_bundle — Full pipeline: import → map → bundle
14
+ // engine_open — Open a .0n bundle file
15
+ //
16
+ // Patent Pending: US Provisional Patent Application #63/968,814
17
+ // ============================================================
18
+
19
+ // ── Re-exports ─────────────────────────────────────────────
20
+ export { sealPortable, unsealPortable, verifyPortable } from "./cipher-portable.js";
21
+ export { parseFile, parseEnvFile, parseCsvFile, parseJsonFile, parseEnvString, parseCsvString, parseJsonString, detectFormat } from "./parser.js";
22
+ export { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
23
+ export { verifyCredentials, verifyAll } from "./validator.js";
24
+ export { generatePlatformConfig, generateAllPlatformConfigs, installPlatformConfig, getPlatformInfo, listPlatforms } from "./platforms.js";
25
+ export { createBundle, openBundle, inspectBundle, verifyBundle } from "./bundler.js";
26
+
27
+ // ── Imports for tool handlers ──────────────────────────────
28
+ import { parseFile } from "./parser.js";
29
+ import { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
30
+ import { verifyCredentials, verifyAll } from "./validator.js";
31
+ import { generatePlatformConfig, generateAllPlatformConfigs, getPlatformInfo, listPlatforms } from "./platforms.js";
32
+ import { createBundle, openBundle, inspectBundle, verifyBundle } from "./bundler.js";
33
+ import { existsSync, readFileSync, readdirSync } from "fs";
34
+ import { join } from "path";
35
+ import { homedir } from "os";
36
+
37
+ const CONNECTIONS_DIR = join(homedir(), ".0n", "connections");
38
+
39
+ /**
40
+ * Load all connections from ~/.0n/connections/ for bundling.
41
+ */
42
+ function loadLocalConnections(serviceFilter) {
43
+ const connections = {};
44
+ if (!existsSync(CONNECTIONS_DIR)) return connections;
45
+
46
+ const files = readdirSync(CONNECTIONS_DIR);
47
+ for (const file of files) {
48
+ if (!file.endsWith(".0n") && !file.endsWith(".0n.json")) continue;
49
+ try {
50
+ const data = JSON.parse(readFileSync(join(CONNECTIONS_DIR, file), "utf-8"));
51
+ if (!data.$0n || data.$0n.type !== "connection") continue;
52
+ const service = data.service;
53
+ if (serviceFilter && !serviceFilter.includes(service)) continue;
54
+ if (data.$0n.sealed) continue; // Skip vault-sealed connections (can't read creds)
55
+ connections[service] = {
56
+ credentials: data.auth?.credentials || {},
57
+ name: data.$0n.name || service,
58
+ authType: data.auth?.type || "api_key",
59
+ environment: data.environment || "production",
60
+ };
61
+ } catch { /* skip invalid */ }
62
+ }
63
+ return connections;
64
+ }
65
+
66
+ /**
67
+ * Register engine tools on an MCP server instance.
68
+ *
69
+ * @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
70
+ * @param {import("zod").ZodType} z
71
+ */
72
+ export function registerEngineTools(server, z) {
73
+ // ─── engine_import ──────────────────────────────────────
74
+ server.tool(
75
+ "engine_import",
76
+ `Import and map credentials from a .env, CSV, or JSON file.
77
+ Auto-detects which of 26 supported services each credential belongs to.
78
+ Returns mapped services with confidence scores and any unmapped variables.
79
+
80
+ Example: engine_import({ source: "/path/to/.env" })`,
81
+ {
82
+ source: z.string().describe("Path to credential file (.env, .csv, or .json)"),
83
+ format: z.enum(["env", "csv", "json", "auto"]).optional().describe("File format (default: auto-detect)"),
84
+ },
85
+ async ({ source, format }) => {
86
+ try {
87
+ const { format: detected, entries } = parseFile(source);
88
+ const { mapped, unmapped } = mapEnvVars(entries);
89
+ const groups = groupByService(mapped);
90
+
91
+ // Validate each service
92
+ const services = {};
93
+ for (const [service, group] of Object.entries(groups)) {
94
+ const validation = validateMapping(service, group.credentials);
95
+ services[service] = {
96
+ credentials: Object.keys(group.credentials),
97
+ envVars: group.envVars,
98
+ complete: validation.valid,
99
+ missing: validation.missing,
100
+ };
101
+ }
102
+
103
+ return {
104
+ content: [{
105
+ type: "text",
106
+ text: JSON.stringify({
107
+ status: "imported",
108
+ format: format || detected,
109
+ total_entries: entries.length,
110
+ mapped_count: mapped.length,
111
+ unmapped_count: unmapped.length,
112
+ services,
113
+ unmapped: unmapped.map(u => u.key),
114
+ message: `Found ${Object.keys(services).length} services from ${entries.length} entries. Use engine_bundle to create a portable .0n file.`,
115
+ }, null, 2),
116
+ }],
117
+ };
118
+ } catch (err) {
119
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
120
+ }
121
+ }
122
+ );
123
+
124
+ // ─── engine_verify ──────────────────────────────────────
125
+ server.tool(
126
+ "engine_verify",
127
+ `Verify API credentials by making lightweight test calls to each service.
128
+ All verification calls are read-only and non-destructive.
129
+
130
+ Example: engine_verify({ services: ["stripe", "openai"] })`,
131
+ {
132
+ services: z.array(z.string()).optional().describe("Service keys to verify (default: all connected)"),
133
+ },
134
+ async ({ services }) => {
135
+ try {
136
+ // Load connections
137
+ const connections = loadLocalConnections(services);
138
+ if (Object.keys(connections).length === 0) {
139
+ return { content: [{ type: "text", text: JSON.stringify({ status: "no_connections", message: "No connections found to verify." }, null, 2) }] };
140
+ }
141
+
142
+ const { results, summary } = await verifyAll(connections);
143
+
144
+ return {
145
+ content: [{
146
+ type: "text",
147
+ text: JSON.stringify({ status: "verified", results, summary }, null, 2),
148
+ }],
149
+ };
150
+ } catch (err) {
151
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
152
+ }
153
+ }
154
+ );
155
+
156
+ // ─── engine_platforms ────────────────────────────────────
157
+ server.tool(
158
+ "engine_platforms",
159
+ `Generate MCP server configuration for AI platforms.
160
+ Supports: Claude Desktop, Cursor, Windsurf, Gemini, Continue, Cline, OpenAI.
161
+
162
+ Example: engine_platforms({ platform: "claude_desktop" })
163
+ Example: engine_platforms({}) — generates all 7 platforms`,
164
+ {
165
+ platform: z.string().optional().describe("Platform key (claude_desktop, cursor, windsurf, gemini, continue, cline, openai) — omit for all"),
166
+ mode: z.enum(["stdio", "http"]).optional().describe("Transport mode (default: stdio)"),
167
+ },
168
+ async ({ platform, mode }) => {
169
+ try {
170
+ const options = { mode: mode || "stdio" };
171
+
172
+ if (platform) {
173
+ const config = generatePlatformConfig(platform, options);
174
+ return {
175
+ content: [{
176
+ type: "text",
177
+ text: JSON.stringify({
178
+ platform: config.name,
179
+ config_path: config.path,
180
+ format: config.format,
181
+ config: config.config,
182
+ }, null, 2),
183
+ }],
184
+ };
185
+ }
186
+
187
+ const allConfigs = generateAllPlatformConfigs(options);
188
+ const info = getPlatformInfo();
189
+
190
+ return {
191
+ content: [{
192
+ type: "text",
193
+ text: JSON.stringify({
194
+ platforms: Object.entries(allConfigs).map(([key, cfg]) => ({
195
+ key,
196
+ name: cfg.name,
197
+ config_path: cfg.path,
198
+ format: cfg.format,
199
+ installed: info.find(i => i.platform === key)?.installed || false,
200
+ config: cfg.config,
201
+ })),
202
+ message: `Generated configs for ${Object.keys(allConfigs).length} platforms. Copy the config to the appropriate file path.`,
203
+ }, null, 2),
204
+ }],
205
+ };
206
+ } catch (err) {
207
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
208
+ }
209
+ }
210
+ );
211
+
212
+ // ─── engine_export ──────────────────────────────────────
213
+ server.tool(
214
+ "engine_export",
215
+ `Export connected services as a portable .0n bundle file.
216
+ The bundle is encrypted with a passphrase (portable — works on any machine).
217
+ Includes platform configs for all major AI tools.
218
+
219
+ Example: engine_export({ passphrase: "my-secure-passphrase" })`,
220
+ {
221
+ passphrase: z.string().describe("Passphrase to encrypt the bundle"),
222
+ services: z.array(z.string()).optional().describe("Service keys to include (default: all connected)"),
223
+ output: z.string().optional().describe("Output file path (default: ~/.0n/bundles/)"),
224
+ name: z.string().optional().describe("Bundle name"),
225
+ platforms: z.array(z.string()).optional().describe("Platform configs to include (default: all)"),
226
+ },
227
+ async ({ passphrase, services, output, name, platforms }) => {
228
+ try {
229
+ const connections = loadLocalConnections(services);
230
+ if (Object.keys(connections).length === 0) {
231
+ return { content: [{ type: "text", text: JSON.stringify({ status: "no_connections", message: "No connections found to export." }, null, 2) }] };
232
+ }
233
+
234
+ const result = createBundle({
235
+ connections,
236
+ passphrase,
237
+ outputPath: output,
238
+ name: name || "0n AI Brain",
239
+ platforms: platforms || "all",
240
+ });
241
+
242
+ return {
243
+ content: [{
244
+ type: "text",
245
+ text: JSON.stringify({
246
+ status: "exported",
247
+ path: result.path,
248
+ services: result.manifest.services,
249
+ connection_count: result.manifest.connection_count,
250
+ platform_count: result.manifest.platform_count,
251
+ encryption: result.manifest.encryption.method,
252
+ message: `Bundle created at ${result.path}. Share this file — recipient opens with: engine_open`,
253
+ }, null, 2),
254
+ }],
255
+ };
256
+ } catch (err) {
257
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
258
+ }
259
+ }
260
+ );
261
+
262
+ // ─── engine_bundle ──────────────────────────────────────
263
+ server.tool(
264
+ "engine_bundle",
265
+ `Full pipeline: import credentials from file → map to services → create encrypted .0n bundle.
266
+ Combines engine_import + engine_export in one step.
267
+
268
+ Example: engine_bundle({ source: "/path/to/.env", passphrase: "my-passphrase" })`,
269
+ {
270
+ passphrase: z.string().describe("Passphrase to encrypt the bundle"),
271
+ source: z.string().optional().describe("Path to credential file — if omitted, bundles existing connections"),
272
+ output: z.string().optional().describe("Output file path"),
273
+ name: z.string().optional().describe("Bundle name"),
274
+ },
275
+ async ({ passphrase, source, output, name }) => {
276
+ try {
277
+ let connections;
278
+
279
+ if (source) {
280
+ // Import from file
281
+ const { entries } = parseFile(source);
282
+ const { mapped, unmapped } = mapEnvVars(entries);
283
+ const groups = groupByService(mapped);
284
+
285
+ connections = {};
286
+ for (const [service, group] of Object.entries(groups)) {
287
+ const validation = validateMapping(service, group.credentials);
288
+ if (validation.valid) {
289
+ connections[service] = {
290
+ credentials: group.credentials,
291
+ name: service,
292
+ };
293
+ }
294
+ }
295
+
296
+ if (Object.keys(connections).length === 0) {
297
+ return { content: [{ type: "text", text: JSON.stringify({ status: "no_services", message: "No complete service credentials found in source file.", unmapped: unmapped.map(u => u.key) }, null, 2) }] };
298
+ }
299
+ } else {
300
+ connections = loadLocalConnections();
301
+ if (Object.keys(connections).length === 0) {
302
+ return { content: [{ type: "text", text: JSON.stringify({ status: "no_connections", message: "No connections found. Provide a source file or connect services first." }, null, 2) }] };
303
+ }
304
+ }
305
+
306
+ const result = createBundle({
307
+ connections,
308
+ passphrase,
309
+ outputPath: output,
310
+ name: name || "0n AI Brain",
311
+ platforms: "all",
312
+ });
313
+
314
+ return {
315
+ content: [{
316
+ type: "text",
317
+ text: JSON.stringify({
318
+ status: "bundled",
319
+ path: result.path,
320
+ services: result.manifest.services,
321
+ connection_count: result.manifest.connection_count,
322
+ platform_count: result.manifest.platform_count,
323
+ encryption: result.manifest.encryption.method,
324
+ message: `AI Brain created with ${result.manifest.connection_count} services. Portable across any machine.`,
325
+ }, null, 2),
326
+ }],
327
+ };
328
+ } catch (err) {
329
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
330
+ }
331
+ }
332
+ );
333
+
334
+ // ─── engine_open ────────────────────────────────────────
335
+ server.tool(
336
+ "engine_open",
337
+ `Open a .0n bundle file and extract connections to this machine.
338
+ Decrypts credentials and saves as individual .0n connection files.
339
+ Optionally inspect without passphrase to see contents first.
340
+
341
+ Example: engine_open({ bundle: "/path/to/bundle.0n", passphrase: "my-passphrase" })`,
342
+ {
343
+ bundle: z.string().describe("Path to .0n bundle file"),
344
+ passphrase: z.string().optional().describe("Passphrase to decrypt — omit to inspect only"),
345
+ },
346
+ async ({ bundle, passphrase }) => {
347
+ try {
348
+ if (!passphrase) {
349
+ // Inspect only
350
+ const info = inspectBundle(bundle);
351
+ return {
352
+ content: [{
353
+ type: "text",
354
+ text: JSON.stringify({
355
+ status: "inspected",
356
+ ...info,
357
+ message: `Bundle contains ${info.services.length} services. Provide passphrase to extract.`,
358
+ }, null, 2),
359
+ }],
360
+ };
361
+ }
362
+
363
+ // Verify first
364
+ const verification = verifyBundle(bundle, passphrase);
365
+ if (!verification.valid) {
366
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", errors: verification.errors }, null, 2) }] };
367
+ }
368
+
369
+ // Open and extract
370
+ const result = openBundle(bundle, passphrase);
371
+
372
+ return {
373
+ content: [{
374
+ type: "text",
375
+ text: JSON.stringify({
376
+ status: "opened",
377
+ connections_imported: result.connections,
378
+ platforms_available: result.platforms,
379
+ includes_extracted: result.includes,
380
+ errors: result.errors.length > 0 ? result.errors : undefined,
381
+ message: `Imported ${result.connections.length} services. Use engine_platforms to install configs for your AI tools.`,
382
+ }, null, 2),
383
+ }],
384
+ };
385
+ } catch (err) {
386
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
387
+ }
388
+ }
389
+ );
390
+ }