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/cli.js CHANGED
@@ -76,6 +76,14 @@ async function main() {
76
76
  return;
77
77
  }
78
78
 
79
+ // ── Install (full onboarding — no login required) ─────────
80
+
81
+ if (command === 'install') {
82
+ const { install } = await import('./install.js');
83
+ await install();
84
+ return;
85
+ }
86
+
79
87
  // ── Auth commands (no login required) ──────────────────────
80
88
 
81
89
  if (command === 'login') {
@@ -194,7 +202,7 @@ ${c.bright}Links:${c.reset}
194
202
 
195
203
  // ── Auth Gate ──────────────────────────────────────────────
196
204
  // All commands below this point require authentication.
197
- const AUTH_FREE = ['help', '--help', '-h', 'login', 'logout', 'whoami', 'version', '--version', '-v'];
205
+ const AUTH_FREE = ['help', '--help', '-h', 'login', 'logout', 'whoami', 'version', '--version', '-v', 'install'];
198
206
  if (!AUTH_FREE.includes(command)) {
199
207
  try {
200
208
  const { isAuthenticated } = await import('./auth.js');
package/crm/objects.js CHANGED
@@ -155,36 +155,8 @@ export default [
155
155
 
156
156
  // ── Associations ────────────────────────────────────────────
157
157
 
158
- {
159
- name: "crm_list_associations",
160
- description: "List all association definitions for a custom object schema in a CRM location.",
161
- method: "GET",
162
- path: "/associations/",
163
- params: {
164
- locationId: { type: "string", description: "Location / sub-account ID", required: true, in: "query" },
165
- schemaKey: { type: "string", description: "Custom object schema key to list associations for", required: true, in: "query" },
166
- },
167
- query: ["locationId", "schemaKey"],
168
- body: [],
169
- },
170
-
171
- {
172
- name: "crm_create_association",
173
- description: "Create a new association definition between two custom object schemas.",
174
- method: "POST",
175
- path: "/associations/",
176
- params: {
177
- locationId: { type: "string", description: "Location / sub-account ID", required: true, in: "body" },
178
- name: { type: "string", description: "Display name for the association", required: true, in: "body" },
179
- key: { type: "string", description: "Unique key for the association (lowercase, no spaces)", required: true, in: "body" },
180
- fromSchemaKey: { type: "string", description: "Schema key of the source object", required: true, in: "body" },
181
- toSchemaKey: { type: "string", description: "Schema key of the target object", required: true, in: "body" },
182
- fromDisplayField: { type: "string", description: "Field key shown when viewing from the source side", required: false, in: "body" },
183
- toDisplayField: { type: "string", description: "Field key shown when viewing from the target side", required: false, in: "body" },
184
- },
185
- query: [],
186
- body: ["locationId", "name", "key", "fromSchemaKey", "toSchemaKey", "fromDisplayField", "toDisplayField"],
187
- },
158
+ // crm_list_associations — defined in funnels.js
159
+ // crm_create_association — defined in funnels.js
188
160
 
189
161
  // ── Email Builder ───────────────────────────────────────────
190
162
 
@@ -310,45 +282,9 @@ export default [
310
282
 
311
283
  // ── Snapshots ───────────────────────────────────────────────
312
284
 
313
- {
314
- name: "crm_list_snapshots",
315
- description: "List all snapshots available for a CRM company.",
316
- method: "GET",
317
- path: "/snapshots/",
318
- params: {
319
- companyId: { type: "string", description: "Company / agency ID", required: true, in: "query" },
320
- },
321
- query: ["companyId"],
322
- body: [],
323
- },
324
-
325
- {
326
- name: "crm_create_snapshot_share_link",
327
- description: "Create a shareable link for a snapshot so it can be pushed to other locations.",
328
- method: "POST",
329
- path: "/snapshots/share/link",
330
- params: {
331
- companyId: { type: "string", description: "Company / agency ID", required: true, in: "body" },
332
- snapshotId: { type: "string", description: "Snapshot ID to share", required: true, in: "body" },
333
- shareType: { type: "string", description: "Type of share (e.g. link, permanent)", required: true, in: "body" },
334
- relationshipNumber: { type: "string", description: "Relationship number for the share link", required: false, in: "body" },
335
- },
336
- query: [],
337
- body: ["companyId", "snapshotId", "shareType", "relationshipNumber"],
338
- },
339
-
340
- {
341
- name: "crm_get_snapshot_push_status",
342
- description: "Get the push status between a snapshot and a specific location.",
343
- method: "GET",
344
- path: "/snapshots/snapshot-status/:snapshotId/:locationId",
345
- params: {
346
- snapshotId: { type: "string", description: "Snapshot ID to check", required: true, in: "path" },
347
- locationId: { type: "string", description: "Location / sub-account ID to check status against", required: true, in: "path" },
348
- },
349
- query: [],
350
- body: [],
351
- },
285
+ // crm_list_snapshots — defined in funnels.js
286
+ // crm_create_snapshot_share_link — defined in funnels.js
287
+ // crm_get_snapshot_push_status defined in funnels.js
352
288
 
353
289
  // ── Trigger Links ───────────────────────────────────────────
354
290
 
package/crm/users.js CHANGED
@@ -105,20 +105,7 @@ const users = [
105
105
 
106
106
  // ── FORMS ─────────────────────────────────────────────────
107
107
 
108
- {
109
- name: "crm_list_forms",
110
- description: "List all forms for a CRM location with optional pagination and type filter.",
111
- method: "GET",
112
- path: "/forms/",
113
- params: {
114
- locationId: { type: "string", description: "Location / sub-account ID", required: true, in: "query" },
115
- skip: { type: "number", description: "Number of records to skip for pagination", required: false, in: "query" },
116
- limit: { type: "number", description: "Maximum number of forms to return", required: false, in: "query" },
117
- type: { type: "string", description: "Filter by form type", required: false, in: "query" },
118
- },
119
- query: ["locationId", "skip", "limit", "type"],
120
- body: [],
121
- },
108
+ // crm_list_forms — defined in funnels.js
122
109
 
123
110
  {
124
111
  name: "crm_get_form_submissions",
@@ -154,20 +141,7 @@ const users = [
154
141
 
155
142
  // ── SURVEYS ───────────────────────────────────────────────
156
143
 
157
- {
158
- name: "crm_list_surveys",
159
- description: "List all surveys for a CRM location with optional pagination and type filter.",
160
- method: "GET",
161
- path: "/surveys/",
162
- params: {
163
- locationId: { type: "string", description: "Location / sub-account ID", required: true, in: "query" },
164
- skip: { type: "number", description: "Number of records to skip for pagination", required: false, in: "query" },
165
- limit: { type: "number", description: "Maximum number of surveys to return", required: false, in: "query" },
166
- type: { type: "string", description: "Filter by survey type", required: false, in: "query" },
167
- },
168
- query: ["locationId", "skip", "limit", "type"],
169
- body: [],
170
- },
144
+ // crm_list_surveys — defined in funnels.js
171
145
 
172
146
  {
173
147
  name: "crm_get_survey_submissions",
@@ -187,60 +161,11 @@ const users = [
187
161
  body: [],
188
162
  },
189
163
 
190
- // ── FUNNELS / WEBSITES ────────────────────────────────────
191
-
192
- {
193
- name: "crm_list_funnels",
194
- description: "List funnels and websites for a CRM location with optional filters for type, name, category, and parent.",
195
- method: "GET",
196
- path: "/funnels/funnel/list",
197
- params: {
198
- locationId: { type: "string", description: "Location / sub-account ID", required: true, in: "query" },
199
- limit: { type: "number", description: "Maximum number of funnels to return", required: false, in: "query" },
200
- offset: { type: "number", description: "Number of records to skip for pagination", required: false, in: "query" },
201
- type: { type: "string", description: "Filter by funnel type", required: false, in: "query" },
202
- name: { type: "string", description: "Filter by exact funnel name", required: false, in: "query" },
203
- search: { type: "string", description: "Free-text search across funnel names", required: false, in: "query" },
204
- category: { type: "string", description: "Filter by funnel category", required: false, in: "query" },
205
- parentId: { type: "string", description: "Filter by parent funnel ID", required: false, in: "query" },
206
- },
207
- query: ["locationId", "limit", "offset", "type", "name", "search", "category", "parentId"],
208
- body: [],
209
- },
210
-
211
- {
212
- name: "crm_get_funnel_pages",
213
- description: "List pages within a funnel with optional filters for name and search.",
214
- method: "GET",
215
- path: "/funnels/page",
216
- params: {
217
- locationId: { type: "string", description: "Location / sub-account ID", required: true, in: "query" },
218
- funnelId: { type: "string", description: "Funnel ID to list pages for", required: true, in: "query" },
219
- limit: { type: "number", description: "Maximum number of pages to return", required: false, in: "query" },
220
- offset: { type: "number", description: "Number of records to skip for pagination", required: false, in: "query" },
221
- name: { type: "string", description: "Filter by exact page name", required: false, in: "query" },
222
- search: { type: "string", description: "Free-text search across page names", required: false, in: "query" },
223
- },
224
- query: ["locationId", "funnelId", "limit", "offset", "name", "search"],
225
- body: [],
226
- },
227
-
228
- {
229
- name: "crm_count_funnel_pages",
230
- description: "Get the total count of pages within a funnel, optionally filtered by name.",
231
- method: "GET",
232
- path: "/funnels/page/count",
233
- params: {
234
- locationId: { type: "string", description: "Location / sub-account ID", required: true, in: "query" },
235
- funnelId: { type: "string", description: "Funnel ID to count pages for", required: true, in: "query" },
236
- name: { type: "string", description: "Filter count by page name", required: false, in: "query" },
237
- },
238
- query: ["locationId", "funnelId", "name"],
239
- body: [],
240
- },
164
+ // ── FUNNELS / WEBSITES (defined in funnels.js) ────────────
165
+ // crm_list_funnels, crm_get_funnel_pages, crm_count_funnel_pages → see funnels.js
241
166
 
242
167
  {
243
- name: "crm_funnel_redirect_lookup",
168
+ name: "crm_funnel_redirect_lookup_by_id",
244
169
  description: "Look up a funnel redirect by its redirect ID.",
245
170
  method: "GET",
246
171
  path: "/funnels/lookup/redirect",
package/engine/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  // ============================================================
4
4
  // The .0n Conversion Engine — import credentials, verify keys,
5
5
  // generate platform configs, create portable AI Brain bundles,
6
- // and build/run application bundles.
6
+ // build/run application bundles, and the 0nEngine Plugin System.
7
7
  //
8
- // 11 MCP Tools:
8
+ // 16 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
@@ -17,6 +17,11 @@
17
17
  // app_inspect — Show application metadata (no passphrase)
18
18
  // app_validate — Validate application cross-references
19
19
  // app_list — List installed applications
20
+ // plugin_list — List all plugins (catalog + custom)
21
+ // plugin_build — Build a plugin from service key or spec
22
+ // plugin_execute — Execute a plugin endpoint with .0n fields
23
+ // plugin_inspect — Inspect a plugin's capabilities & endpoints
24
+ // plugin_create — Generate a new custom plugin spec
20
25
  //
21
26
  // Patent Pending: US Provisional Patent Application #63/968,814
22
27
  // ============================================================
@@ -34,6 +39,18 @@ export { Application } from "./application.js";
34
39
  export { createApplication, openApplication, inspectApplication, validateApplication } from "./app-builder.js";
35
40
  export { ApplicationServer } from "./app-server.js";
36
41
 
42
+ // ── 0nEngine Plugin System ──────────────────────────────────
43
+ export { Plugin } from "./plugin.js";
44
+ export { PluginBuilder, getPluginBuilder, buildPlugin, buildAllPlugins, buildFromSpec, generatePluginSpec } from "./plugin-builder.js";
45
+ export { PluginRegistry, getPluginRegistry } from "./plugin-registry.js";
46
+
47
+ // ── 0nAI Training Center ────────────────────────────────────
48
+ export { registerTrainingTools } from "./training.js";
49
+ export { TrainingFeedEngine, registerFeedTools, FEED_SOURCES } from "./training-feed.js";
50
+
51
+ // ── Multi-AI Council ────────────────────────────────────────
52
+ export { registerCouncilTools, getAvailableProviders, askAll, PROVIDERS } from "./multi-ai.js";
53
+
37
54
  // ── Imports for tool handlers ──────────────────────────────
38
55
  import { parseFile } from "./parser.js";
39
56
  import { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
@@ -41,6 +58,8 @@ import { verifyCredentials, verifyAll } from "./validator.js";
41
58
  import { generatePlatformConfig, generateAllPlatformConfigs, getPlatformInfo, listPlatforms } from "./platforms.js";
42
59
  import { createBundle, openBundle, inspectBundle, verifyBundle } from "./bundler.js";
43
60
  import { createApplication, openApplication, inspectApplication, validateApplication } from "./app-builder.js";
61
+ import { PluginBuilder, getPluginBuilder } from "./plugin-builder.js";
62
+ import { PluginRegistry, getPluginRegistry } from "./plugin-registry.js";
44
63
  import { existsSync, readFileSync, readdirSync } from "fs";
45
64
  import { join } from "path";
46
65
  import { homedir } from "os";
@@ -642,6 +661,323 @@ Example: app_list({})`,
642
661
  }
643
662
  }
644
663
  );
664
+
665
+ // ═══════════════════════════════════════════════════════════
666
+ // 0nEngine Plugin Tools
667
+ // ═══════════════════════════════════════════════════════════
668
+
669
+ // ─── plugin_list ───────────────────────────────────────────
670
+ server.tool(
671
+ "plugin_list",
672
+ `List all available plugins — catalog services + custom plugins from ~/.0n/plugins/.
673
+ Shows connection status, endpoint counts, capability counts, and field coverage.
674
+
675
+ Example: plugin_list({})
676
+ Example: plugin_list({ type: "email" })
677
+ Example: plugin_list({ connected: true })`,
678
+ {
679
+ type: z.string().optional().describe("Filter by service type (email, payments, crm, etc.)"),
680
+ connected: z.boolean().optional().describe("Filter by connection status"),
681
+ search: z.string().optional().describe("Search by keyword in name/description"),
682
+ },
683
+ async ({ type, connected, search }) => {
684
+ try {
685
+ const registry = getPluginRegistry();
686
+
687
+ let plugins = registry.all();
688
+
689
+ if (type) plugins = plugins.filter(p => p.type === type);
690
+ if (connected !== undefined) plugins = plugins.filter(p => p.isConnected === connected);
691
+ if (search) {
692
+ const q = search.toLowerCase();
693
+ plugins = plugins.filter(p =>
694
+ p.name.toLowerCase().includes(q) ||
695
+ p.description.toLowerCase().includes(q) ||
696
+ p.key.toLowerCase().includes(q)
697
+ );
698
+ }
699
+
700
+ const list = plugins.map(p => ({
701
+ key: p.key,
702
+ name: p.name,
703
+ type: p.type,
704
+ connected: p.isConnected,
705
+ endpoints: Object.keys(p.endpoints).length,
706
+ capabilities: p.capabilities.length,
707
+ }));
708
+
709
+ return {
710
+ content: [{
711
+ type: "text",
712
+ text: JSON.stringify({
713
+ status: "ok",
714
+ total: list.length,
715
+ plugins: list,
716
+ types: [...new Set(plugins.map(p => p.type))],
717
+ }, null, 2),
718
+ }],
719
+ };
720
+ } catch (err) {
721
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
722
+ }
723
+ }
724
+ );
725
+
726
+ // ─── plugin_inspect ────────────────────────────────────────
727
+ server.tool(
728
+ "plugin_inspect",
729
+ `Inspect a plugin's full details — capabilities, endpoints, field mappings, and stats.
730
+
731
+ Example: plugin_inspect({ service: "stripe" })`,
732
+ {
733
+ service: z.string().describe("Service key to inspect (e.g., stripe, crm, sendgrid)"),
734
+ },
735
+ async ({ service }) => {
736
+ try {
737
+ const builder = getPluginBuilder();
738
+ const plugin = builder.build(service);
739
+
740
+ const info = plugin.inspect();
741
+ const fields = builder.getServiceFields(service);
742
+
743
+ return {
744
+ content: [{
745
+ type: "text",
746
+ text: JSON.stringify({
747
+ status: "ok",
748
+ ...info,
749
+ fieldMappings: fields,
750
+ fieldCount: Object.keys(fields).length,
751
+ }, null, 2),
752
+ }],
753
+ };
754
+ } catch (err) {
755
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
756
+ }
757
+ }
758
+ );
759
+
760
+ // ─── plugin_execute ────────────────────────────────────────
761
+ server.tool(
762
+ "plugin_execute",
763
+ `Execute a plugin endpoint with automatic .0n field resolution.
764
+ Accepts canonical .0n fields (email.0n, fullname.0n, etc.) and auto-translates
765
+ to the service's native format. Handles auth, rate limiting, and response normalization.
766
+
767
+ Example: plugin_execute({
768
+ service: "stripe",
769
+ endpoint: "create_customer",
770
+ params: { "email.0n": "mike@rocketopp.com", "fullname.0n": "Mike Mento" }
771
+ })`,
772
+ {
773
+ service: z.string().describe("Service key (e.g., stripe, crm, sendgrid)"),
774
+ endpoint: z.string().describe("Endpoint name from the service catalog (e.g., create_customer, send_email)"),
775
+ params: z.record(z.any()).optional().describe("Parameters — supports .0n canonical fields (email.0n) and raw service fields"),
776
+ credentials: z.record(z.string()).optional().describe("One-time credentials override (otherwise uses ~/.0n/connections/)"),
777
+ },
778
+ async ({ service, endpoint, params, credentials }) => {
779
+ try {
780
+ const builder = getPluginBuilder();
781
+ const plugin = builder.build(service);
782
+
783
+ // Connect with provided or disk credentials
784
+ if (credentials) {
785
+ plugin.connect(credentials);
786
+ } else {
787
+ // Try to load from disk
788
+ const connFile = join(CONNECTIONS_DIR, `${service}.0n`);
789
+ const connFileJson = join(CONNECTIONS_DIR, `${service}.0n.json`);
790
+ const connPath = existsSync(connFile) ? connFile : existsSync(connFileJson) ? connFileJson : null;
791
+
792
+ if (connPath) {
793
+ const data = JSON.parse(readFileSync(connPath, "utf-8"));
794
+ if (data.auth?.credentials) {
795
+ plugin.connect(data.auth.credentials);
796
+ }
797
+ }
798
+ }
799
+
800
+ if (!plugin.isConnected) {
801
+ return {
802
+ content: [{
803
+ type: "text",
804
+ text: JSON.stringify({
805
+ status: "not_connected",
806
+ service,
807
+ message: `Plugin "${service}" has no credentials. Provide credentials or connect via connect_service.`,
808
+ requiredKeys: plugin.credentialKeys,
809
+ }, null, 2),
810
+ }],
811
+ };
812
+ }
813
+
814
+ const result = await plugin.execute(endpoint, params || {});
815
+
816
+ return {
817
+ content: [{
818
+ type: "text",
819
+ text: JSON.stringify(result, null, 2),
820
+ }],
821
+ };
822
+ } catch (err) {
823
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
824
+ }
825
+ }
826
+ );
827
+
828
+ // ─── plugin_build ──────────────────────────────────────────
829
+ server.tool(
830
+ "plugin_build",
831
+ `Build a plugin from a service key or custom spec.
832
+ If building from catalog, returns the plugin's full tool manifest.
833
+ If building from spec, creates a custom plugin and saves to ~/.0n/plugins/.
834
+
835
+ Example (catalog): plugin_build({ service: "stripe" })
836
+ Example (custom): plugin_build({
837
+ spec: {
838
+ service: "acme",
839
+ name: "Acme CRM",
840
+ baseUrl: "https://api.acme.com/v1",
841
+ authType: "api_key",
842
+ credentialKeys: ["apiKey"],
843
+ endpoints: {
844
+ list_contacts: { method: "GET", path: "/contacts" },
845
+ create_contact: { method: "POST", path: "/contacts", body: { email: "", name: "" } }
846
+ }
847
+ }
848
+ })`,
849
+ {
850
+ service: z.string().optional().describe("Catalog service key (e.g., stripe) — builds from catalog"),
851
+ spec: z.record(z.any()).optional().describe("Custom plugin spec — builds from definition"),
852
+ save: z.boolean().optional().describe("Save custom plugin to ~/.0n/plugins/ (default: true)"),
853
+ },
854
+ async ({ service, spec, save }) => {
855
+ try {
856
+ const builder = getPluginBuilder();
857
+
858
+ if (spec) {
859
+ // Build from custom spec
860
+ const generated = builder.generate(spec);
861
+ const plugin = builder.buildFromSpec(generated);
862
+
863
+ if (save !== false) {
864
+ builder.generateAndSave(spec);
865
+ }
866
+
867
+ return {
868
+ content: [{
869
+ type: "text",
870
+ text: JSON.stringify({
871
+ status: "built",
872
+ source: "custom_spec",
873
+ plugin: plugin.inspect(),
874
+ tools: plugin.toMcpTools().map(t => t.name),
875
+ spec: generated,
876
+ saved: save !== false,
877
+ message: `Custom plugin "${spec.service || spec.key}" built with ${Object.keys(spec.endpoints || {}).length} endpoints.`,
878
+ }, null, 2),
879
+ }],
880
+ };
881
+ }
882
+
883
+ if (service) {
884
+ // Build from catalog
885
+ const plugin = builder.build(service);
886
+ const tools = plugin.toMcpTools();
887
+
888
+ return {
889
+ content: [{
890
+ type: "text",
891
+ text: JSON.stringify({
892
+ status: "built",
893
+ source: "catalog",
894
+ plugin: plugin.inspect(),
895
+ tools: tools.map(t => t.name),
896
+ toolCount: tools.length,
897
+ message: `Plugin "${service}" built from catalog with ${tools.length} tools.`,
898
+ }, null, 2),
899
+ }],
900
+ };
901
+ }
902
+
903
+ return {
904
+ content: [{
905
+ type: "text",
906
+ text: JSON.stringify({
907
+ status: "failed",
908
+ error: "Provide either 'service' (catalog key) or 'spec' (custom definition).",
909
+ }, null, 2),
910
+ }],
911
+ };
912
+ } catch (err) {
913
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
914
+ }
915
+ }
916
+ );
917
+
918
+ // ─── plugin_create ─────────────────────────────────────────
919
+ server.tool(
920
+ "plugin_create",
921
+ `Generate a new custom plugin spec for a service not in the catalog.
922
+ Auto-infers capabilities from endpoints and maps .0n canonical fields.
923
+ Saves to ~/.0n/plugins/ for automatic loading.
924
+
925
+ Example: plugin_create({
926
+ key: "acme",
927
+ name: "Acme API",
928
+ baseUrl: "https://api.acme.com/v1",
929
+ authType: "api_key",
930
+ credentialKeys: ["apiKey"],
931
+ endpoints: {
932
+ list_users: { method: "GET", path: "/users" },
933
+ create_user: { method: "POST", path: "/users", body: { email: "", name: "" } },
934
+ get_user: { method: "GET", path: "/users/{userId}" },
935
+ update_user: { method: "PUT", path: "/users/{userId}", body: {} },
936
+ delete_user: { method: "DELETE", path: "/users/{userId}" }
937
+ }
938
+ })`,
939
+ {
940
+ key: z.string().describe("Service key (lowercase, no spaces)"),
941
+ name: z.string().optional().describe("Display name"),
942
+ baseUrl: z.string().describe("API base URL"),
943
+ authType: z.enum(["api_key", "oauth", "basic", "none"]).optional().describe("Auth type (default: api_key)"),
944
+ credentialKeys: z.array(z.string()).optional().describe("Required credential keys (default: ['apiKey'])"),
945
+ type: z.string().optional().describe("Category type (e.g., crm, email, payments)"),
946
+ description: z.string().optional().describe("Service description"),
947
+ endpoints: z.record(z.any()).describe("Endpoint definitions { name: { method, path, body?, query? } }"),
948
+ fieldMappings: z.record(z.any()).optional().describe("Custom .0n field mappings { 'email.0n': 'user_email' }"),
949
+ },
950
+ async ({ key, name, baseUrl, authType, credentialKeys, type, description, endpoints, fieldMappings }) => {
951
+ try {
952
+ const builder = getPluginBuilder();
953
+
954
+ const def = { key, name, baseUrl, authType, credentialKeys, type, description, endpoints, fieldMappings };
955
+ const { spec, path } = builder.generateAndSave(def);
956
+
957
+ // Also register in the active registry
958
+ const registry = getPluginRegistry();
959
+ const plugin = registry.registerFromSpec(spec);
960
+
961
+ return {
962
+ content: [{
963
+ type: "text",
964
+ text: JSON.stringify({
965
+ status: "created",
966
+ service: key,
967
+ path,
968
+ endpoints: Object.keys(endpoints).length,
969
+ capabilities: spec.capabilities?.length || 0,
970
+ fieldMappings: spec.fieldMappings ? Object.keys(spec.fieldMappings).length : 0,
971
+ tools: plugin.toMcpTools().map(t => t.name),
972
+ message: `Plugin "${key}" created and saved to ${path}. It's now available in the registry.`,
973
+ }, null, 2),
974
+ }],
975
+ };
976
+ } catch (err) {
977
+ return { content: [{ type: "text", text: JSON.stringify({ status: "failed", error: err.message }, null, 2) }] };
978
+ }
979
+ }
980
+ );
645
981
  }
646
982
 
647
983
  /**