0nmcp 2.6.0 → 2.7.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 +233 -695
- package/cli.js +9 -1
- package/crm/objects.js +5 -69
- package/crm/users.js +5 -80
- package/engine/index.js +338 -2
- package/engine/multi-ai.js +525 -0
- package/engine/plugin-builder.js +578 -0
- package/engine/plugin-registry.js +419 -0
- package/engine/plugin.js +448 -0
- package/engine/training-feed.js +520 -0
- package/engine/training.js +875 -0
- package/index.js +9 -1
- package/lib/stats.json +1 -1
- package/package.json +12 -2
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Plugin Registry
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Central registry for all plugins — catalog-generated and custom.
|
|
5
|
+
// Loads custom plugins from ~/.0n/plugins/, provides discovery,
|
|
6
|
+
// search, and bulk operations.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// import { PluginRegistry } from './plugin-registry.js'
|
|
10
|
+
//
|
|
11
|
+
// const registry = new PluginRegistry()
|
|
12
|
+
// registry.loadCatalog() // Load all 26+ catalog services
|
|
13
|
+
// registry.loadCustomPlugins() // Load from ~/.0n/plugins/
|
|
14
|
+
//
|
|
15
|
+
// const stripe = registry.get('stripe')
|
|
16
|
+
// const emailPlugins = registry.findByType('email')
|
|
17
|
+
// const allTools = registry.generateAllMcpTools()
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
import { PluginBuilder, getPluginBuilder } from "./plugin-builder.js";
|
|
21
|
+
import { Plugin } from "./plugin.js";
|
|
22
|
+
import { SERVICE_CATALOG } from "../catalog.js";
|
|
23
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import { homedir } from "os";
|
|
26
|
+
|
|
27
|
+
const PLUGINS_DIR = join(homedir(), ".0n", "plugins");
|
|
28
|
+
const CONNECTIONS_DIR = join(homedir(), ".0n", "connections");
|
|
29
|
+
|
|
30
|
+
export class PluginRegistry {
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} [options]
|
|
33
|
+
* @param {PluginBuilder} [options.builder] — Custom builder instance
|
|
34
|
+
* @param {boolean} [options.autoLoad] — Auto-load catalog + custom plugins
|
|
35
|
+
*/
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
this.builder = options.builder || getPluginBuilder();
|
|
38
|
+
|
|
39
|
+
/** @type {Map<string, Plugin>} */
|
|
40
|
+
this.plugins = new Map();
|
|
41
|
+
|
|
42
|
+
/** @type {Map<string, object>} */
|
|
43
|
+
this.specs = new Map();
|
|
44
|
+
|
|
45
|
+
if (options.autoLoad) {
|
|
46
|
+
this.loadCatalog();
|
|
47
|
+
this.loadCustomPlugins();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Loading ───────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load all SERVICE_CATALOG entries as plugins.
|
|
55
|
+
*/
|
|
56
|
+
loadCatalog() {
|
|
57
|
+
for (const key of Object.keys(SERVICE_CATALOG)) {
|
|
58
|
+
const plugin = this.builder.build(key);
|
|
59
|
+
this.plugins.set(key, plugin);
|
|
60
|
+
}
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load custom plugins from ~/.0n/plugins/.
|
|
66
|
+
*/
|
|
67
|
+
loadCustomPlugins() {
|
|
68
|
+
if (!existsSync(PLUGINS_DIR)) return this;
|
|
69
|
+
|
|
70
|
+
const files = readdirSync(PLUGINS_DIR);
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
if (!file.endsWith(".0n") && !file.endsWith(".0n.json") && !file.endsWith(".json")) continue;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const filePath = join(PLUGINS_DIR, file);
|
|
76
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
77
|
+
const spec = JSON.parse(raw);
|
|
78
|
+
|
|
79
|
+
if (spec.$0n?.type !== "plugin") continue;
|
|
80
|
+
|
|
81
|
+
const plugin = this.builder.buildFromSpec(spec);
|
|
82
|
+
this.plugins.set(spec.service, plugin);
|
|
83
|
+
this.specs.set(spec.service, spec);
|
|
84
|
+
} catch {
|
|
85
|
+
// Skip invalid plugin files
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Auto-connect plugins using credentials from ~/.0n/connections/.
|
|
94
|
+
*/
|
|
95
|
+
connectFromDisk() {
|
|
96
|
+
if (!existsSync(CONNECTIONS_DIR)) return this;
|
|
97
|
+
|
|
98
|
+
const files = readdirSync(CONNECTIONS_DIR);
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
if (!file.endsWith(".0n") && !file.endsWith(".0n.json")) continue;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const data = JSON.parse(readFileSync(join(CONNECTIONS_DIR, file), "utf-8"));
|
|
104
|
+
if (data.$0n?.type !== "connection") continue;
|
|
105
|
+
if (data.$0n?.sealed) continue; // Skip vault-sealed
|
|
106
|
+
|
|
107
|
+
const service = data.service;
|
|
108
|
+
const plugin = this.plugins.get(service);
|
|
109
|
+
if (plugin && data.auth?.credentials) {
|
|
110
|
+
plugin.connect(data.auth.credentials);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Skip invalid
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Connect specific plugins with provided credentials.
|
|
122
|
+
*/
|
|
123
|
+
connectAll(connections) {
|
|
124
|
+
for (const [key, credentials] of Object.entries(connections)) {
|
|
125
|
+
const plugin = this.plugins.get(key);
|
|
126
|
+
if (plugin) {
|
|
127
|
+
plugin.connect(credentials);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Registration ──────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register a plugin instance directly.
|
|
137
|
+
*/
|
|
138
|
+
register(plugin) {
|
|
139
|
+
if (!(plugin instanceof Plugin)) {
|
|
140
|
+
throw new Error("register() requires a Plugin instance");
|
|
141
|
+
}
|
|
142
|
+
this.plugins.set(plugin.key, plugin);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Register a plugin from a spec object.
|
|
148
|
+
*/
|
|
149
|
+
registerFromSpec(spec, options) {
|
|
150
|
+
const plugin = this.builder.buildFromSpec(spec, options);
|
|
151
|
+
this.plugins.set(spec.service, plugin);
|
|
152
|
+
this.specs.set(spec.service, spec);
|
|
153
|
+
return plugin;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Unregister a plugin.
|
|
158
|
+
*/
|
|
159
|
+
unregister(key) {
|
|
160
|
+
this.plugins.delete(key);
|
|
161
|
+
this.specs.delete(key);
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Lookup ────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get a plugin by service key.
|
|
169
|
+
*/
|
|
170
|
+
get(key) {
|
|
171
|
+
return this.plugins.get(key) || null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if a plugin exists.
|
|
176
|
+
*/
|
|
177
|
+
has(key) {
|
|
178
|
+
return this.plugins.has(key);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get all plugin keys.
|
|
183
|
+
*/
|
|
184
|
+
keys() {
|
|
185
|
+
return [...this.plugins.keys()];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get all plugins as an array.
|
|
190
|
+
*/
|
|
191
|
+
all() {
|
|
192
|
+
return [...this.plugins.values()];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Count plugins.
|
|
197
|
+
*/
|
|
198
|
+
get size() {
|
|
199
|
+
return this.plugins.size;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Search & Discovery ────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Find plugins by type (e.g., 'email', 'payments', 'crm').
|
|
206
|
+
*/
|
|
207
|
+
findByType(type) {
|
|
208
|
+
return this.all().filter(p => p.type === type);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Find plugins that have a specific capability.
|
|
213
|
+
*/
|
|
214
|
+
findByCapability(capabilityName) {
|
|
215
|
+
return this.all().filter(p =>
|
|
216
|
+
p.capabilities.some(c => c.name === capabilityName)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Find plugins that support a specific action on any capability.
|
|
222
|
+
*/
|
|
223
|
+
findByAction(action) {
|
|
224
|
+
return this.all().filter(p =>
|
|
225
|
+
p.capabilities.some(c => c.actions.includes(action))
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Find plugins that have a specific endpoint.
|
|
231
|
+
*/
|
|
232
|
+
findByEndpoint(endpointName) {
|
|
233
|
+
return this.all().filter(p => p.endpoints[endpointName]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Search plugins by keyword in name/description.
|
|
238
|
+
*/
|
|
239
|
+
search(query) {
|
|
240
|
+
const q = query.toLowerCase();
|
|
241
|
+
return this.all().filter(p =>
|
|
242
|
+
p.name.toLowerCase().includes(q) ||
|
|
243
|
+
p.description.toLowerCase().includes(q) ||
|
|
244
|
+
p.key.toLowerCase().includes(q) ||
|
|
245
|
+
p.type.toLowerCase().includes(q)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get all connected plugins.
|
|
251
|
+
*/
|
|
252
|
+
getConnected() {
|
|
253
|
+
return this.all().filter(p => p.isConnected);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get all disconnected plugins.
|
|
258
|
+
*/
|
|
259
|
+
getDisconnected() {
|
|
260
|
+
return this.all().filter(p => !p.isConnected);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Bulk Operations ───────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Execute an endpoint across all connected plugins that have it.
|
|
267
|
+
* Returns results from all services.
|
|
268
|
+
*/
|
|
269
|
+
async executeAll(endpointName, params = {}) {
|
|
270
|
+
const plugins = this.findByEndpoint(endpointName).filter(p => p.isConnected);
|
|
271
|
+
|
|
272
|
+
if (plugins.length === 0) {
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
error: `No connected plugins have endpoint: ${endpointName}`,
|
|
276
|
+
results: [],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const results = await Promise.allSettled(
|
|
281
|
+
plugins.map(p => p.execute(endpointName, params))
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
results: results.map((r, i) => ({
|
|
287
|
+
service: plugins[i].key,
|
|
288
|
+
...(r.status === "fulfilled" ? r.value : { success: false, error: r.reason?.message }),
|
|
289
|
+
})),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── MCP Tool Generation ───────────────────────────────────
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Generate MCP tool definitions for all plugins.
|
|
297
|
+
* Returns a flat array ready for McpServer registration.
|
|
298
|
+
*/
|
|
299
|
+
generateAllMcpTools(options = {}) {
|
|
300
|
+
const serviceFilter = options.services
|
|
301
|
+
? new Set(options.services)
|
|
302
|
+
: null;
|
|
303
|
+
|
|
304
|
+
const tools = [];
|
|
305
|
+
const services = [];
|
|
306
|
+
|
|
307
|
+
for (const plugin of this.all()) {
|
|
308
|
+
if (serviceFilter && !serviceFilter.has(plugin.key)) continue;
|
|
309
|
+
const pluginTools = plugin.toMcpTools();
|
|
310
|
+
tools.push(...pluginTools);
|
|
311
|
+
services.push(plugin.key);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
tools,
|
|
316
|
+
count: tools.length,
|
|
317
|
+
services,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Register all plugin tools on an MCP server instance.
|
|
323
|
+
*
|
|
324
|
+
* @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
|
|
325
|
+
* @param {import("zod").ZodType} z
|
|
326
|
+
* @param {object} [options]
|
|
327
|
+
* @param {string[]} [options.services] — Only these services
|
|
328
|
+
*/
|
|
329
|
+
registerMcpTools(server, z, options = {}) {
|
|
330
|
+
const { tools } = this.generateAllMcpTools(options);
|
|
331
|
+
|
|
332
|
+
for (const tool of tools) {
|
|
333
|
+
const plugin = this.get(tool.service);
|
|
334
|
+
if (!plugin) continue;
|
|
335
|
+
|
|
336
|
+
// Build Zod schema from tool params
|
|
337
|
+
const schemaFields = {};
|
|
338
|
+
|
|
339
|
+
for (const p of tool.pathParams) {
|
|
340
|
+
schemaFields[p] = z.string().describe(`Path parameter: ${p} (required)`);
|
|
341
|
+
}
|
|
342
|
+
for (const p of tool.queryParams) {
|
|
343
|
+
schemaFields[p] = z.string().optional().describe(`Query parameter: ${p}`);
|
|
344
|
+
}
|
|
345
|
+
if (tool.hasBody) {
|
|
346
|
+
for (const p of tool.bodyKeys) {
|
|
347
|
+
schemaFields[p] = z.any().optional().describe(`Body parameter: ${p}`);
|
|
348
|
+
}
|
|
349
|
+
// Accept raw JSON body
|
|
350
|
+
schemaFields._body = z.record(z.any()).optional().describe("Raw JSON body (merged with named params)");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
server.tool(
|
|
354
|
+
tool.name,
|
|
355
|
+
tool.description,
|
|
356
|
+
schemaFields,
|
|
357
|
+
async (params) => {
|
|
358
|
+
const { _body, ...rest } = params;
|
|
359
|
+
const merged = _body ? { ...rest, ..._body } : rest;
|
|
360
|
+
|
|
361
|
+
const result = await plugin.execute(tool.endpointName, merged);
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
content: [{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: JSON.stringify(result, null, 2),
|
|
367
|
+
}],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return tools.length;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Introspection ─────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Get a summary of the registry state.
|
|
380
|
+
*/
|
|
381
|
+
inspect() {
|
|
382
|
+
const byType = {};
|
|
383
|
+
for (const plugin of this.all()) {
|
|
384
|
+
if (!byType[plugin.type]) byType[plugin.type] = [];
|
|
385
|
+
byType[plugin.type].push({
|
|
386
|
+
key: plugin.key,
|
|
387
|
+
name: plugin.name,
|
|
388
|
+
connected: plugin.isConnected,
|
|
389
|
+
endpoints: Object.keys(plugin.endpoints).length,
|
|
390
|
+
capabilities: plugin.capabilities.length,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
total: this.size,
|
|
396
|
+
connected: this.getConnected().length,
|
|
397
|
+
disconnected: this.getDisconnected().length,
|
|
398
|
+
customPlugins: this.specs.size,
|
|
399
|
+
byType,
|
|
400
|
+
totalEndpoints: this.all().reduce((s, p) => s + Object.keys(p.endpoints).length, 0),
|
|
401
|
+
totalCapabilities: this.all().reduce((s, p) => s + p.capabilities.length, 0),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Singleton ─────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
let _defaultRegistry = null;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get the default PluginRegistry singleton.
|
|
412
|
+
* Auto-loads catalog on first call.
|
|
413
|
+
*/
|
|
414
|
+
export function getPluginRegistry() {
|
|
415
|
+
if (!_defaultRegistry) {
|
|
416
|
+
_defaultRegistry = new PluginRegistry({ autoLoad: true });
|
|
417
|
+
}
|
|
418
|
+
return _defaultRegistry;
|
|
419
|
+
}
|