0nmcp 2.2.0 → 2.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/README.md +44 -29
- package/capability-proxy.js +199 -0
- package/catalog.js +322 -6
- package/cli.js +143 -0
- package/crm/helpers.js +9 -6
- package/crm/index.js +3 -2
- package/engine/validator.js +13 -1
- package/index.js +8 -5
- package/lib/badges.json +5 -5
- package/lib/catalog.json +653 -3
- package/lib/stats.json +647 -35
- package/orchestrator.js +6 -35
- package/package.json +13 -10
- package/tools.js +3 -36
- package/vault/sync.js +250 -0
- package/workflow.js +336 -12
package/orchestrator.js
CHANGED
|
@@ -38,9 +38,11 @@ const CAPABILITY_KEYWORDS = {
|
|
|
38
38
|
export class Orchestrator {
|
|
39
39
|
/**
|
|
40
40
|
* @param {import("./connections.js").ConnectionManager} connectionManager
|
|
41
|
+
* @param {import("./capability-proxy.js").CapabilityProxy} [proxy]
|
|
41
42
|
*/
|
|
42
|
-
constructor(connectionManager) {
|
|
43
|
+
constructor(connectionManager, proxy) {
|
|
43
44
|
this.connections = connectionManager;
|
|
45
|
+
this.proxy = proxy;
|
|
44
46
|
this.anthropic = null;
|
|
45
47
|
|
|
46
48
|
// Dynamically import Anthropic SDK if key is available
|
|
@@ -277,14 +279,7 @@ Rules:
|
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
try {
|
|
280
|
-
//
|
|
281
|
-
let url = service.baseUrl + endpoint.path;
|
|
282
|
-
|
|
283
|
-
// Substitute path params from step.params and credentials
|
|
284
|
-
const allParams = { ...creds, ...step.params };
|
|
285
|
-
url = url.replace(/\{(\w+)\}/g, (_, key) => allParams[key] || `{${key}}`);
|
|
286
|
-
|
|
287
|
-
// Substitute context references {{label.field}}
|
|
282
|
+
// Substitute context references {{label.field}} in params
|
|
288
283
|
if (step.params) {
|
|
289
284
|
const paramsStr = JSON.stringify(step.params);
|
|
290
285
|
const resolved = paramsStr.replace(/\{\{(\w+)\.(\w+)\}\}/g, (_, label, field) => {
|
|
@@ -293,32 +288,8 @@ Rules:
|
|
|
293
288
|
step.params = JSON.parse(resolved);
|
|
294
289
|
}
|
|
295
290
|
|
|
296
|
-
//
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
// Build request options
|
|
300
|
-
const options = { method: endpoint.method, headers };
|
|
301
|
-
|
|
302
|
-
if (endpoint.method !== "GET" && step.params) {
|
|
303
|
-
const contentType = endpoint.contentType || "application/json";
|
|
304
|
-
if (contentType === "application/x-www-form-urlencoded") {
|
|
305
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
306
|
-
options.body = new URLSearchParams(this._flattenParams(step.params)).toString();
|
|
307
|
-
} else {
|
|
308
|
-
headers["Content-Type"] = "application/json";
|
|
309
|
-
options.body = JSON.stringify(step.params);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Add query params for GET requests
|
|
314
|
-
if (endpoint.method === "GET" && step.params) {
|
|
315
|
-
const queryStr = new URLSearchParams(this._flattenParams(step.params)).toString();
|
|
316
|
-
if (queryStr) url += (url.includes("?") ? "&" : "?") + queryStr;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Execute
|
|
320
|
-
const response = await fetch(url, options);
|
|
321
|
-
const data = await response.json().catch(() => ({ status: response.status }));
|
|
291
|
+
// Execute through capability proxy (rate-limited, audited, zero-knowledge)
|
|
292
|
+
const { response, data } = await this.proxy.call(step.service, step.endpoint, step.params || {});
|
|
322
293
|
|
|
323
294
|
return {
|
|
324
295
|
success: response.ok,
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0nmcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"mcpName": "io.github.0nork/0nMCP",
|
|
5
|
-
"description": "Universal AI API Orchestrator —
|
|
5
|
+
"description": "Universal AI API Orchestrator — 819 tools, 48 services, portable AI Brain bundles + machine-bound vault encryption + Application Engine. The most comprehensive MCP server available. Free and open source from 0nORK.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"types": "types/index.d.ts",
|
|
@@ -246,6 +246,7 @@
|
|
|
246
246
|
"catalog.js",
|
|
247
247
|
"connections.js",
|
|
248
248
|
"orchestrator.js",
|
|
249
|
+
"capability-proxy.js",
|
|
249
250
|
"crm.js",
|
|
250
251
|
"crm/",
|
|
251
252
|
"webhooks.js",
|
|
@@ -258,17 +259,19 @@
|
|
|
258
259
|
"LICENSE"
|
|
259
260
|
],
|
|
260
261
|
"0nmcp-stats": {
|
|
261
|
-
"tools":
|
|
262
|
+
"tools": 545,
|
|
262
263
|
"crmTools": 245,
|
|
263
264
|
"vaultTools": 4,
|
|
265
|
+
"vaultContainerTools": 8,
|
|
266
|
+
"deedTools": 6,
|
|
264
267
|
"engineTools": 6,
|
|
265
268
|
"appTools": 5,
|
|
266
|
-
"totalTools":
|
|
267
|
-
"services":
|
|
268
|
-
"actions":
|
|
269
|
-
"triggers":
|
|
270
|
-
"totalCapabilities":
|
|
271
|
-
"categories":
|
|
272
|
-
"lastUpdated": "2026-
|
|
269
|
+
"totalTools": 819,
|
|
270
|
+
"services": 48,
|
|
271
|
+
"actions": 104,
|
|
272
|
+
"triggers": 155,
|
|
273
|
+
"totalCapabilities": 1078,
|
|
274
|
+
"categories": 21,
|
|
275
|
+
"lastUpdated": "2026-03-13T00:31:43.573Z"
|
|
273
276
|
}
|
|
274
277
|
}
|
package/tools.js
CHANGED
|
@@ -16,7 +16,7 @@ import { SERVICE_CATALOG, listServices, getService } from "./catalog.js";
|
|
|
16
16
|
* @param {import("./orchestrator.js").Orchestrator} orchestrator
|
|
17
17
|
* @param {import("./workflow.js").WorkflowRunner} [workflowRunner]
|
|
18
18
|
*/
|
|
19
|
-
export function registerAllTools(server, connections, orchestrator, workflowRunner) {
|
|
19
|
+
export function registerAllTools(server, connections, orchestrator, workflowRunner, proxy) {
|
|
20
20
|
// ─── execute ───────────────────────────────────────────
|
|
21
21
|
server.tool(
|
|
22
22
|
"execute",
|
|
@@ -291,41 +291,8 @@ Examples:
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
try {
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
// Substitute path params
|
|
298
|
-
url = url.replace(/\{(\w+)\}/g, (_, key) => allParams[key] || `{${key}}`);
|
|
299
|
-
|
|
300
|
-
const headers = catalog.authHeader(creds);
|
|
301
|
-
const options = { method: ep.method, headers };
|
|
302
|
-
|
|
303
|
-
if (ep.method !== "GET" && params) {
|
|
304
|
-
const contentType = ep.contentType || "application/json";
|
|
305
|
-
if (contentType === "application/x-www-form-urlencoded") {
|
|
306
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
307
|
-
const flat = {};
|
|
308
|
-
for (const [k, v] of Object.entries(params)) {
|
|
309
|
-
if (typeof v !== "object") flat[k] = String(v);
|
|
310
|
-
}
|
|
311
|
-
options.body = new URLSearchParams(flat).toString();
|
|
312
|
-
} else {
|
|
313
|
-
headers["Content-Type"] = "application/json";
|
|
314
|
-
options.body = JSON.stringify(params);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (ep.method === "GET" && params) {
|
|
319
|
-
const flat = {};
|
|
320
|
-
for (const [k, v] of Object.entries(params)) {
|
|
321
|
-
if (typeof v !== "object") flat[k] = String(v);
|
|
322
|
-
}
|
|
323
|
-
const qs = new URLSearchParams(flat).toString();
|
|
324
|
-
if (qs) url += (url.includes("?") ? "&" : "?") + qs;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const response = await fetch(url, options);
|
|
328
|
-
const data = await response.json().catch(() => ({ status: response.status, statusText: response.statusText }));
|
|
294
|
+
// Execute through capability proxy (rate-limited, audited, zero-knowledge)
|
|
295
|
+
const { response, data } = await proxy.call(service, endpoint, params || {});
|
|
329
296
|
|
|
330
297
|
return {
|
|
331
298
|
content: [{
|
package/vault/sync.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Vault: Cross-Platform E2E Encrypted Sync
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Syncs vault credentials across CLI, web console, and other
|
|
5
|
+
// platforms using Argon2id key derivation + AES-256-GCM.
|
|
6
|
+
//
|
|
7
|
+
// The server NEVER sees plaintext credentials — only encrypted
|
|
8
|
+
// blobs are stored/transmitted. The sync passphrase never
|
|
9
|
+
// leaves the user's device.
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
16
|
+
import { createRequire } from "module";
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
const DOT_ON = join(homedir(), ".0n");
|
|
21
|
+
const CONNECTIONS_DIR = join(DOT_ON, "connections");
|
|
22
|
+
const SYNC_SALT_FILE = join(DOT_ON, "sync-salt");
|
|
23
|
+
const API_BASE = "https://0nmcp.com";
|
|
24
|
+
|
|
25
|
+
// AES-256-GCM constants
|
|
26
|
+
const ALGORITHM = "aes-256-gcm";
|
|
27
|
+
const KEY_LENGTH = 32;
|
|
28
|
+
const IV_LENGTH = 12; // 96 bits (GCM recommended)
|
|
29
|
+
const TAG_LENGTH = 16;
|
|
30
|
+
|
|
31
|
+
// Argon2id parameters (same as crypto-container.js)
|
|
32
|
+
const ARGON2_MEMORY = 65536; // 64MB
|
|
33
|
+
const ARGON2_ITERATIONS = 4;
|
|
34
|
+
const ARGON2_PARALLELISM = 2;
|
|
35
|
+
const ARGON2_HASH_LENGTH = 32; // 256 bits
|
|
36
|
+
|
|
37
|
+
// ── Key Derivation ──────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Derive a 256-bit encryption key from passphrase using Argon2id.
|
|
41
|
+
* Memory-hard and GPU-resistant.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} passphrase - User's sync passphrase
|
|
44
|
+
* @param {Buffer} salt - 32-byte salt
|
|
45
|
+
* @returns {Promise<Buffer>} 32-byte AES-256 key
|
|
46
|
+
*/
|
|
47
|
+
async function deriveKey(passphrase, salt) {
|
|
48
|
+
let argon2;
|
|
49
|
+
try {
|
|
50
|
+
argon2 = await import("argon2");
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error("argon2 package required for vault sync. Install: npm i argon2");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mod = argon2.default || argon2;
|
|
56
|
+
const hash = await mod.hash(passphrase, {
|
|
57
|
+
type: 2, // argon2id
|
|
58
|
+
memoryCost: ARGON2_MEMORY,
|
|
59
|
+
timeCost: ARGON2_ITERATIONS,
|
|
60
|
+
parallelism: ARGON2_PARALLELISM,
|
|
61
|
+
hashLength: ARGON2_HASH_LENGTH,
|
|
62
|
+
salt,
|
|
63
|
+
raw: true,
|
|
64
|
+
});
|
|
65
|
+
return hash;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Encryption / Decryption ─────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Encrypt data for cloud sync.
|
|
72
|
+
* Uses Argon2id-derived key + AES-256-GCM.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} plaintext - Data to encrypt
|
|
75
|
+
* @param {string} passphrase - Sync passphrase
|
|
76
|
+
* @returns {Promise<{ encrypted_data: string, iv: string, salt: string }>}
|
|
77
|
+
*/
|
|
78
|
+
export async function encryptForSync(plaintext, passphrase) {
|
|
79
|
+
const salt = randomBytes(32);
|
|
80
|
+
const iv = randomBytes(IV_LENGTH);
|
|
81
|
+
const key = await deriveKey(passphrase, salt);
|
|
82
|
+
|
|
83
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
84
|
+
const encrypted = Buffer.concat([
|
|
85
|
+
cipher.update(plaintext, "utf8"),
|
|
86
|
+
cipher.final(),
|
|
87
|
+
]);
|
|
88
|
+
const tag = cipher.getAuthTag();
|
|
89
|
+
|
|
90
|
+
// Combine tag + ciphertext for storage
|
|
91
|
+
const combined = Buffer.concat([tag, encrypted]);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
encrypted_data: combined.toString("base64"),
|
|
95
|
+
iv: iv.toString("base64"),
|
|
96
|
+
salt: salt.toString("base64"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Decrypt data from cloud sync.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} encryptedB64 - Base64 encoded [tag + ciphertext]
|
|
104
|
+
* @param {string} ivB64 - Base64 encoded IV
|
|
105
|
+
* @param {string} saltB64 - Base64 encoded salt
|
|
106
|
+
* @param {string} passphrase - Sync passphrase
|
|
107
|
+
* @returns {Promise<string>} Decrypted plaintext
|
|
108
|
+
*/
|
|
109
|
+
export async function decryptFromSync(encryptedB64, ivB64, saltB64, passphrase) {
|
|
110
|
+
const combined = Buffer.from(encryptedB64, "base64");
|
|
111
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
112
|
+
const salt = Buffer.from(saltB64, "base64");
|
|
113
|
+
|
|
114
|
+
if (combined.length < TAG_LENGTH + 1) {
|
|
115
|
+
throw new Error("Invalid encrypted data: too short");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const tag = combined.subarray(0, TAG_LENGTH);
|
|
119
|
+
const ciphertext = combined.subarray(TAG_LENGTH);
|
|
120
|
+
|
|
121
|
+
const key = await deriveKey(passphrase, salt);
|
|
122
|
+
|
|
123
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
124
|
+
decipher.setAuthTag(tag);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const decrypted = Buffer.concat([
|
|
128
|
+
decipher.update(ciphertext),
|
|
129
|
+
decipher.final(),
|
|
130
|
+
]);
|
|
131
|
+
return decrypted.toString("utf8");
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error("Decryption failed — wrong sync passphrase.");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Sync Operations ─────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
async function apiCall(method, path, token, body = null) {
|
|
140
|
+
const headers = {
|
|
141
|
+
"Authorization": `Bearer ${token}`,
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const opts = { method, headers };
|
|
146
|
+
if (body) opts.body = JSON.stringify(body);
|
|
147
|
+
|
|
148
|
+
const res = await fetch(`${API_BASE}${path}`, opts);
|
|
149
|
+
const data = await res.json();
|
|
150
|
+
return { ok: res.ok, status: res.status, data };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Push all local connections to cloud (E2E encrypted).
|
|
155
|
+
*
|
|
156
|
+
* @param {string} token - Access token
|
|
157
|
+
* @param {string} passphrase - Sync passphrase
|
|
158
|
+
* @returns {Promise<{ pushed: number, errors: string[] }>}
|
|
159
|
+
*/
|
|
160
|
+
export async function pushToCloud(token, passphrase) {
|
|
161
|
+
if (!existsSync(CONNECTIONS_DIR)) {
|
|
162
|
+
return { pushed: 0, errors: ["No connections directory found. Run: 0nmcp init"] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const files = readdirSync(CONNECTIONS_DIR).filter(f => f.endsWith(".0n"));
|
|
166
|
+
const errors = [];
|
|
167
|
+
let pushed = 0;
|
|
168
|
+
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
const serviceKey = file.replace(".0n", "");
|
|
171
|
+
try {
|
|
172
|
+
const content = readFileSync(join(CONNECTIONS_DIR, file), "utf8");
|
|
173
|
+
const { encrypted_data, iv, salt } = await encryptForSync(content, passphrase);
|
|
174
|
+
|
|
175
|
+
const { ok, data } = await apiCall("PUT", "/api/vault/sync", token, {
|
|
176
|
+
service_key: serviceKey,
|
|
177
|
+
encrypted_data,
|
|
178
|
+
iv,
|
|
179
|
+
salt,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (ok) {
|
|
183
|
+
pushed++;
|
|
184
|
+
} else {
|
|
185
|
+
errors.push(`${serviceKey}: ${data.error || "Unknown error"}`);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
errors.push(`${serviceKey}: ${err.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { pushed, errors };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Pull all connections from cloud and write to local.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} token - Access token
|
|
199
|
+
* @param {string} passphrase - Sync passphrase
|
|
200
|
+
* @returns {Promise<{ pulled: number, errors: string[] }>}
|
|
201
|
+
*/
|
|
202
|
+
export async function pullFromCloud(token, passphrase) {
|
|
203
|
+
if (!existsSync(CONNECTIONS_DIR)) {
|
|
204
|
+
mkdirSync(CONNECTIONS_DIR, { recursive: true });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { ok, data } = await apiCall("GET", "/api/vault/sync", token);
|
|
208
|
+
if (!ok) {
|
|
209
|
+
return { pulled: 0, errors: [data.error || "Failed to fetch from cloud"] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const entries = data.entries || [];
|
|
213
|
+
const errors = [];
|
|
214
|
+
let pulled = 0;
|
|
215
|
+
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
try {
|
|
218
|
+
const plaintext = await decryptFromSync(
|
|
219
|
+
entry.encrypted_data,
|
|
220
|
+
entry.iv,
|
|
221
|
+
entry.salt,
|
|
222
|
+
passphrase
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const filePath = join(CONNECTIONS_DIR, `${entry.service_key}.0n`);
|
|
226
|
+
writeFileSync(filePath, plaintext);
|
|
227
|
+
pulled++;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
errors.push(`${entry.service_key}: ${err.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { pulled, errors };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Store sync salt locally (so the passphrase prompt
|
|
238
|
+
* can remind the user they've set one up).
|
|
239
|
+
*/
|
|
240
|
+
export function hasSyncSetup() {
|
|
241
|
+
return existsSync(SYNC_SALT_FILE);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Mark that sync has been configured.
|
|
246
|
+
*/
|
|
247
|
+
export function markSyncSetup() {
|
|
248
|
+
if (!existsSync(DOT_ON)) mkdirSync(DOT_ON, { recursive: true });
|
|
249
|
+
writeFileSync(SYNC_SALT_FILE, new Date().toISOString());
|
|
250
|
+
}
|