0nmcp 1.6.0 → 2.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/engine/index.js CHANGED
@@ -2,16 +2,21 @@
2
2
  // 0nMCP — Engine Module
3
3
  // ============================================================
4
4
  // The .0n Conversion Engine — import credentials, verify keys,
5
- // generate platform configs, and create portable AI Brain
6
- // bundle files.
5
+ // generate platform configs, create portable AI Brain bundles,
6
+ // and build/run application bundles.
7
7
  //
8
- // 6 MCP Tools:
8
+ // 11 MCP Tools:
9
9
  // engine_import — Import credentials from .env/CSV/JSON
10
10
  // engine_verify — Verify API keys with test calls
11
11
  // engine_platforms — Generate platform configs
12
12
  // engine_export — Export .0n bundle from connections
13
13
  // engine_bundle — Full pipeline: import → map → bundle
14
14
  // engine_open — Open a .0n bundle file
15
+ // app_build — Build a .0n application bundle
16
+ // app_open — Open/extract a .0n application
17
+ // app_inspect — Show application metadata (no passphrase)
18
+ // app_validate — Validate application cross-references
19
+ // app_list — List installed applications
15
20
  //
16
21
  // Patent Pending: US Provisional Patent Application #63/968,814
17
22
  // ============================================================
@@ -23,6 +28,11 @@ export { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
23
28
  export { verifyCredentials, verifyAll } from "./validator.js";
24
29
  export { generatePlatformConfig, generateAllPlatformConfigs, installPlatformConfig, getPlatformInfo, listPlatforms } from "./platforms.js";
25
30
  export { createBundle, openBundle, inspectBundle, verifyBundle } from "./bundler.js";
31
+ export { OperationRegistry, validateOperations } from "./operations.js";
32
+ export { parseCron, CronScheduler } from "./scheduler.js";
33
+ export { Application } from "./application.js";
34
+ export { createApplication, openApplication, inspectApplication, validateApplication } from "./app-builder.js";
35
+ export { ApplicationServer } from "./app-server.js";
26
36
 
27
37
  // ── Imports for tool handlers ──────────────────────────────
28
38
  import { parseFile } from "./parser.js";
@@ -30,11 +40,14 @@ import { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
30
40
  import { verifyCredentials, verifyAll } from "./validator.js";
31
41
  import { generatePlatformConfig, generateAllPlatformConfigs, getPlatformInfo, listPlatforms } from "./platforms.js";
32
42
  import { createBundle, openBundle, inspectBundle, verifyBundle } from "./bundler.js";
43
+ import { createApplication, openApplication, inspectApplication, validateApplication } from "./app-builder.js";
33
44
  import { existsSync, readFileSync, readdirSync } from "fs";
34
45
  import { join } from "path";
35
46
  import { homedir } from "os";
36
47
 
37
48
  const CONNECTIONS_DIR = join(homedir(), ".0n", "connections");
49
+ const APPS_DIR = join(homedir(), ".0n", "apps");
50
+ const WORKFLOWS_DIR = join(homedir(), ".0n", "workflows");
38
51
 
39
52
  /**
40
53
  * Load all connections from ~/.0n/connections/ for bundling.
@@ -387,4 +400,269 @@ Example: engine_open({ bundle: "/path/to/bundle.0n", passphrase: "my-passphrase"
387
400
  }
388
401
  }
389
402
  );
403
+
404
+ // ═══════════════════════════════════════════════════════════
405
+ // Application Engine Tools (v1.7.0)
406
+ // ═══════════════════════════════════════════════════════════
407
+
408
+ // ─── app_build ────────────────────────────────────────────
409
+ server.tool(
410
+ "app_build",
411
+ `Build a .0n application bundle — a portable encrypted file containing
412
+ endpoints, workflows, operations, automations, and connections.
413
+ Deploy anywhere with: 0nmcp app run <file>
414
+
415
+ Example: app_build({ name: "Lead Scorer", passphrase: "secret", workflows: {...}, endpoints: {...} })`,
416
+ {
417
+ name: z.string().describe("Application name"),
418
+ passphrase: z.string().describe("Passphrase to encrypt the bundle"),
419
+ workflows: z.record(z.any()).optional().describe("Workflow definitions { id: workflowDef }"),
420
+ connections: z.record(z.any()).optional().describe("Connections to include { service: { credentials } }"),
421
+ endpoints: z.record(z.any()).optional().describe("Endpoint definitions { 'METHOD /path': def }"),
422
+ operations: z.record(z.any()).optional().describe("Reusable operation definitions { id: def }"),
423
+ automations: z.record(z.any()).optional().describe("Automation definitions { id: def }"),
424
+ environment: z.object({
425
+ variables: z.record(z.string()).optional(),
426
+ secrets: z.record(z.string()).optional(),
427
+ settings: z.record(z.any()).optional(),
428
+ feature_flags: z.record(z.boolean()).optional(),
429
+ }).optional().describe("Environment configuration"),
430
+ output: z.string().optional().describe("Output file path"),
431
+ },
432
+ async ({ name, passphrase, workflows, connections, endpoints, operations, automations, environment, output }) => {
433
+ try {
434
+ // If no connections provided, load local connections
435
+ const conns = connections || loadLocalConnections();
436
+
437
+ const result = createApplication({
438
+ name,
439
+ passphrase,
440
+ connections: conns,
441
+ workflows: workflows || {},
442
+ endpoints: endpoints || {},
443
+ operations: operations || {},
444
+ automations: automations || {},
445
+ environment: environment || {},
446
+ output,
447
+ });
448
+
449
+ return {
450
+ content: [{
451
+ type: "text",
452
+ text: JSON.stringify({
453
+ status: "built",
454
+ path: result.path,
455
+ manifest: result.manifest,
456
+ message: `Application "${name}" built at ${result.path}. Run with: 0nmcp app run ${result.path}`,
457
+ }, null, 2),
458
+ }],
459
+ };
460
+ } catch (err) {
461
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
462
+ }
463
+ }
464
+ );
465
+
466
+ // ─── app_open ─────────────────────────────────────────────
467
+ server.tool(
468
+ "app_open",
469
+ `Open a .0n application bundle file.
470
+ Decrypts and extracts the application for local use.
471
+
472
+ Example: app_open({ bundle: "/path/to/app.0n", passphrase: "secret" })`,
473
+ {
474
+ bundle: z.string().describe("Path to .0n application file"),
475
+ passphrase: z.string().optional().describe("Passphrase to decrypt — omit to inspect only"),
476
+ },
477
+ async ({ bundle, passphrase }) => {
478
+ try {
479
+ if (!passphrase) {
480
+ const info = inspectApplication(bundle);
481
+ return {
482
+ content: [{
483
+ type: "text",
484
+ text: JSON.stringify({
485
+ status: "inspected",
486
+ ...info,
487
+ message: `Application "${info.name}" has ${info.workflows.length} workflows, ${info.endpoints.length} endpoints. Provide passphrase to extract.`,
488
+ }, null, 2),
489
+ }],
490
+ };
491
+ }
492
+
493
+ const bundleData = openApplication(bundle, passphrase);
494
+ return {
495
+ content: [{
496
+ type: "text",
497
+ text: JSON.stringify({
498
+ status: "opened",
499
+ name: bundleData.$0n.name,
500
+ workflows: Object.keys(bundleData.workflows || {}),
501
+ endpoints: Object.keys(bundleData.endpoints || {}),
502
+ operations: Object.keys(bundleData.operations || {}),
503
+ automations: Object.keys(bundleData.automations || {}),
504
+ connections: (bundleData.connections || []).map(c => c.service),
505
+ message: `Application "${bundleData.$0n.name}" opened. Run with: 0nmcp app run ${bundle}`,
506
+ }, null, 2),
507
+ }],
508
+ };
509
+ } catch (err) {
510
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
511
+ }
512
+ }
513
+ );
514
+
515
+ // ─── app_inspect ──────────────────────────────────────────
516
+ server.tool(
517
+ "app_inspect",
518
+ `Inspect a .0n application bundle without passphrase.
519
+ Shows metadata, endpoints, workflows, and automations.
520
+
521
+ Example: app_inspect({ bundle: "/path/to/app.0n" })`,
522
+ {
523
+ bundle: z.string().describe("Path to .0n application file"),
524
+ },
525
+ async ({ bundle }) => {
526
+ try {
527
+ const info = inspectApplication(bundle);
528
+ return {
529
+ content: [{
530
+ type: "text",
531
+ text: JSON.stringify({
532
+ status: "inspected",
533
+ ...info,
534
+ }, null, 2),
535
+ }],
536
+ };
537
+ } catch (err) {
538
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
539
+ }
540
+ }
541
+ );
542
+
543
+ // ─── app_validate ─────────────────────────────────────────
544
+ server.tool(
545
+ "app_validate",
546
+ `Validate a .0n application bundle's structure and cross-references.
547
+ Checks that endpoints reference valid workflows, workflows reference valid operations, etc.
548
+
549
+ Example: app_validate({ bundle: "/path/to/app.0n", passphrase: "secret" })`,
550
+ {
551
+ bundle: z.string().describe("Path to .0n application file"),
552
+ passphrase: z.string().optional().describe("Passphrase to decrypt for full validation"),
553
+ },
554
+ async ({ bundle, passphrase }) => {
555
+ try {
556
+ const raw = readFileSync(bundle, "utf-8");
557
+ const bundleData = JSON.parse(raw);
558
+
559
+ const result = validateApplication(bundleData);
560
+ return {
561
+ content: [{
562
+ type: "text",
563
+ text: JSON.stringify({
564
+ status: result.valid ? "valid" : "invalid",
565
+ ...result,
566
+ message: result.valid
567
+ ? "Application bundle is valid."
568
+ : `Found ${result.errors.length} error(s).`,
569
+ }, null, 2),
570
+ }],
571
+ };
572
+ } catch (err) {
573
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
574
+ }
575
+ }
576
+ );
577
+
578
+ // ─── app_list ─────────────────────────────────────────────
579
+ server.tool(
580
+ "app_list",
581
+ `List installed .0n applications from ~/.0n/apps/.
582
+
583
+ Example: app_list({})`,
584
+ {},
585
+ async () => {
586
+ try {
587
+ if (!existsSync(APPS_DIR)) {
588
+ return {
589
+ content: [{
590
+ type: "text",
591
+ text: JSON.stringify({
592
+ status: "ok",
593
+ count: 0,
594
+ apps: [],
595
+ message: "No applications installed. Build one with app_build.",
596
+ }, null, 2),
597
+ }],
598
+ };
599
+ }
600
+
601
+ const files = readdirSync(APPS_DIR).filter(f => f.endsWith(".0n") || f.endsWith(".0n.json"));
602
+ const apps = [];
603
+
604
+ for (const file of files) {
605
+ try {
606
+ const filePath = join(APPS_DIR, file);
607
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
608
+ if (data.$0n?.type !== "application") continue;
609
+
610
+ apps.push({
611
+ name: data.$0n.name || file,
612
+ version: data.$0n.version || "1.0.0",
613
+ description: data.$0n.description || "",
614
+ author: data.$0n.author || "",
615
+ created: data.$0n.created,
616
+ file,
617
+ path: filePath,
618
+ workflows: Object.keys(data.workflows || {}).length,
619
+ endpoints: Object.keys(data.endpoints || {}).length,
620
+ operations: Object.keys(data.operations || {}).length,
621
+ automations: Object.keys(data.automations || {}).length,
622
+ connections: (data.connections || []).length,
623
+ });
624
+ } catch { /* skip invalid */ }
625
+ }
626
+
627
+ return {
628
+ content: [{
629
+ type: "text",
630
+ text: JSON.stringify({
631
+ status: "ok",
632
+ count: apps.length,
633
+ apps,
634
+ message: apps.length > 0
635
+ ? `Found ${apps.length} application(s). Run with: 0nmcp app run <file>`
636
+ : "No applications installed.",
637
+ }, null, 2),
638
+ }],
639
+ };
640
+ } catch (err) {
641
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
642
+ }
643
+ }
644
+ );
645
+ }
646
+
647
+ /**
648
+ * Load local workflows from ~/.0n/workflows/ for bundling into applications.
649
+ * @param {string[]} [ids] — Specific workflow IDs to load (default: all)
650
+ * @returns {Record<string, object>}
651
+ */
652
+ export function loadLocalWorkflows(ids) {
653
+ const workflows = {};
654
+ if (!existsSync(WORKFLOWS_DIR)) return workflows;
655
+
656
+ const files = readdirSync(WORKFLOWS_DIR);
657
+ for (const file of files) {
658
+ if (!file.endsWith(".0n") && !file.endsWith(".0n.json")) continue;
659
+ try {
660
+ const data = JSON.parse(readFileSync(join(WORKFLOWS_DIR, file), "utf-8"));
661
+ if (!data.$0n || data.$0n.type !== "workflow") continue;
662
+ const id = file.replace(/\.0n(\.json)?$/, "");
663
+ if (ids && !ids.includes(id)) continue;
664
+ workflows[id] = data;
665
+ } catch { /* skip invalid */ }
666
+ }
667
+ return workflows;
390
668
  }
@@ -0,0 +1,227 @@
1
+ // ============================================================
2
+ // 0nMCP -Engine: Operation Registry
3
+ // ============================================================
4
+ // Reusable action definitions referenced by workflow steps
5
+ // via "operation": "id". Wraps INTERNAL_ACTIONS from workflow.js
6
+ // for type: "internal". Service operations delegate to
7
+ // catalog-based execution.
8
+ //
9
+ // Patent Pending: US Provisional Patent Application #63/968,814
10
+ // ============================================================
11
+
12
+ /**
13
+ * Registry of reusable operations for application bundles.
14
+ * Operations are defined once and referenced by ID in workflow steps.
15
+ */
16
+ export class OperationRegistry {
17
+ constructor() {
18
+ /** @type {Map<string, object>} */
19
+ this._ops = new Map();
20
+ /** @type {object|null} */
21
+ this._internalActions = null;
22
+ }
23
+
24
+ /**
25
+ * Load internal actions for use by "internal" type operations.
26
+ * @param {object} actions -INTERNAL_ACTIONS from workflow.js
27
+ */
28
+ setInternalActions(actions) {
29
+ this._internalActions = actions;
30
+ }
31
+
32
+ /**
33
+ * Register a single operation definition.
34
+ * @param {string} id
35
+ * @param {object} def -{ name, type, action, params, inputs, outputs }
36
+ */
37
+ register(id, def) {
38
+ this._ops.set(id, { id, ...def });
39
+ }
40
+
41
+ /**
42
+ * Register all operations from a definitions object.
43
+ * @param {Record<string, object>} defs -{ opId: { name, type, ... }, ... }
44
+ */
45
+ registerAll(defs) {
46
+ for (const [id, def] of Object.entries(defs)) {
47
+ this.register(id, def);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check if an operation exists.
53
+ * @param {string} id
54
+ * @returns {boolean}
55
+ */
56
+ has(id) {
57
+ return this._ops.has(id);
58
+ }
59
+
60
+ /**
61
+ * Get an operation definition.
62
+ * @param {string} id
63
+ * @returns {object|undefined}
64
+ */
65
+ get(id) {
66
+ return this._ops.get(id);
67
+ }
68
+
69
+ /**
70
+ * List all registered operations.
71
+ * @returns {Array<{ id: string, name: string, type: string }>}
72
+ */
73
+ list() {
74
+ return [...this._ops.values()].map(op => ({
75
+ id: op.id,
76
+ name: op.name || op.id,
77
+ type: op.type || "internal",
78
+ inputs: op.inputs ? Object.keys(op.inputs) : [],
79
+ outputs: op.outputs ? Object.keys(op.outputs) : [],
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * Execute an operation by ID.
85
+ *
86
+ * @param {string} id -Operation ID
87
+ * @param {object} params -Resolved parameters (after template resolution)
88
+ * @param {object} context -{ connections?, env? } for service calls
89
+ * @returns {Promise<object>} Operation result
90
+ */
91
+ async execute(id, params, context = {}) {
92
+ const op = this._ops.get(id);
93
+ if (!op) {
94
+ throw new Error(`Unknown operation: ${id}. Available: ${[...this._ops.keys()].join(", ")}`);
95
+ }
96
+
97
+ // Merge operation's default params with step-level params
98
+ const mergedParams = { ...(op.params || {}), ...params };
99
+
100
+ if (op.type === "internal" || !op.type) {
101
+ return this._executeInternal(op.action, mergedParams);
102
+ }
103
+
104
+ if (op.type === "service") {
105
+ return this._executeService(op.service, op.action, mergedParams, context);
106
+ }
107
+
108
+ throw new Error(`Unknown operation type: ${op.type} for operation ${id}`);
109
+ }
110
+
111
+ /**
112
+ * Execute an internal action.
113
+ * @private
114
+ */
115
+ _executeInternal(action, params) {
116
+ if (!this._internalActions) {
117
+ throw new Error("Internal actions not loaded. Call setInternalActions() first.");
118
+ }
119
+
120
+ const handler = this._internalActions[action];
121
+ if (!handler) {
122
+ throw new Error(`Unknown internal action: ${action}. Available: ${Object.keys(this._internalActions).join(", ")}`);
123
+ }
124
+
125
+ return handler(params);
126
+ }
127
+
128
+ /**
129
+ * Execute a service call via ConnectionManager.
130
+ * @private
131
+ */
132
+ async _executeService(service, action, params, context) {
133
+ if (!context.connections) {
134
+ throw new Error(`Service operation ${service}.${action} requires connections in context.`);
135
+ }
136
+
137
+ const creds = context.connections.getCredentials(service);
138
+ if (!creds) {
139
+ throw new Error(`Service ${service} not connected.`);
140
+ }
141
+
142
+ // Delegate to catalog-based execution via the connection manager
143
+ // This follows the same pattern as WorkflowRunner._executeService
144
+ const { SERVICE_CATALOG } = await import("../catalog.js");
145
+ const catalog = SERVICE_CATALOG[service];
146
+ if (!catalog) {
147
+ throw new Error(`Unknown service: ${service}`);
148
+ }
149
+
150
+ const endpointKeys = Object.keys(catalog.endpoints);
151
+ const ep = catalog.endpoints[action] || catalog.endpoints[endpointKeys.find(k => k.includes(action))];
152
+ if (!ep) {
153
+ throw new Error(`No endpoint found for ${service}.${action}`);
154
+ }
155
+
156
+ let url = catalog.baseUrl + ep.path;
157
+ const allParams = { ...creds, ...params };
158
+ url = url.replace(/\{(\w+)\}/g, (_, key) => allParams[key] || `{${key}}`);
159
+
160
+ const headers = catalog.authHeader(creds);
161
+ const options = { method: ep.method, headers };
162
+
163
+ if (ep.method !== "GET" && params) {
164
+ headers["Content-Type"] = "application/json";
165
+ options.body = JSON.stringify(params);
166
+ }
167
+
168
+ if (ep.method === "GET" && params) {
169
+ const flat = {};
170
+ for (const [k, v] of Object.entries(params)) {
171
+ if (typeof v !== "object") flat[k] = String(v);
172
+ }
173
+ const qs = new URLSearchParams(flat).toString();
174
+ if (qs) url += (url.includes("?") ? "&" : "?") + qs;
175
+ }
176
+
177
+ const response = await fetch(url, options);
178
+ const data = await response.json().catch(() => ({ status: response.status }));
179
+
180
+ if (!response.ok) {
181
+ throw new Error(`${service}.${action} failed (${response.status}): ${JSON.stringify(data)}`);
182
+ }
183
+
184
+ return data;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Validate operation definitions from an application bundle.
190
+ *
191
+ * @param {Record<string, object>} defs -operations section of bundle
192
+ * @returns {{ valid: boolean, errors: string[] }}
193
+ */
194
+ export function validateOperations(defs) {
195
+ const errors = [];
196
+
197
+ if (!defs || typeof defs !== "object") {
198
+ return { valid: false, errors: ["Operations must be an object"] };
199
+ }
200
+
201
+ for (const [id, def] of Object.entries(defs)) {
202
+ if (!def.type && !def.action) {
203
+ errors.push(`Operation "${id}": must have "type" or "action"`);
204
+ }
205
+
206
+ if (def.type === "internal" || !def.type) {
207
+ if (!def.action) {
208
+ errors.push(`Operation "${id}": internal operations require "action"`);
209
+ }
210
+ }
211
+
212
+ if (def.type === "service") {
213
+ if (!def.service) errors.push(`Operation "${id}": service operations require "service"`);
214
+ if (!def.action) errors.push(`Operation "${id}": service operations require "action"`);
215
+ }
216
+
217
+ if (def.inputs && typeof def.inputs !== "object") {
218
+ errors.push(`Operation "${id}": "inputs" must be an object`);
219
+ }
220
+
221
+ if (def.outputs && typeof def.outputs !== "object") {
222
+ errors.push(`Operation "${id}": "outputs" must be an object`);
223
+ }
224
+ }
225
+
226
+ return { valid: errors.length === 0, errors };
227
+ }