0nmcp 2.5.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/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",
@@ -0,0 +1,150 @@
1
+ // ============================================================
2
+ // 0nMCP — CRM Voice AI API Tool Definitions
3
+ // ============================================================
4
+ // AI-powered phone agents. Create agents, define actions,
5
+ // manage call logs. Each agent can use Knowledge Bases.
6
+ // ============================================================
7
+
8
+ export default [
9
+ // ── Agents ─────────────────────────────────────────────────
10
+
11
+ {
12
+ name: "crm_list_voice_agents",
13
+ description: "List all Voice AI agents for a location.",
14
+ method: "GET",
15
+ path: "/voice-ai/agents",
16
+ params: {
17
+ locationId: { type: "string", description: "Location ID", required: true, in: "query" },
18
+ },
19
+ },
20
+
21
+ {
22
+ name: "crm_create_voice_agent",
23
+ description: "Create a new Voice AI agent. Can be configured with knowledge bases, actions, and personality settings.",
24
+ method: "POST",
25
+ path: "/voice-ai/agents",
26
+ params: {
27
+ locationId: { type: "string", description: "Location ID", required: true, in: "body" },
28
+ name: { type: "string", description: "Agent name", required: true, in: "body" },
29
+ personality: { type: "string", description: "Agent personality/system prompt", required: false, in: "body" },
30
+ knowledgeBases: { type: "array", description: "Array of knowledge base IDs to attach", required: false, in: "body" },
31
+ actions: { type: "array", description: "Array of action IDs the agent can perform", required: false, in: "body" },
32
+ greeting: { type: "string", description: "Initial greeting message", required: false, in: "body" },
33
+ voiceId: { type: "string", description: "Voice ID for text-to-speech", required: false, in: "body" },
34
+ },
35
+ body: ["locationId", "name", "personality", "knowledgeBases", "actions", "greeting", "voiceId"],
36
+ },
37
+
38
+ {
39
+ name: "crm_get_voice_agent",
40
+ description: "Get a Voice AI agent by ID with full configuration.",
41
+ method: "GET",
42
+ path: "/voice-ai/agents/:agentId",
43
+ params: {
44
+ agentId: { type: "string", description: "Agent ID", required: true, in: "path" },
45
+ },
46
+ },
47
+
48
+ {
49
+ name: "crm_update_voice_agent",
50
+ description: "Update a Voice AI agent's configuration.",
51
+ method: "PATCH",
52
+ path: "/voice-ai/agents/:agentId",
53
+ params: {
54
+ agentId: { type: "string", description: "Agent ID", required: true, in: "path" },
55
+ name: { type: "string", description: "Agent name", required: false, in: "body" },
56
+ personality: { type: "string", description: "Agent personality/system prompt", required: false, in: "body" },
57
+ knowledgeBases: { type: "array", description: "Array of knowledge base IDs", required: false, in: "body" },
58
+ actions: { type: "array", description: "Array of action IDs", required: false, in: "body" },
59
+ greeting: { type: "string", description: "Initial greeting", required: false, in: "body" },
60
+ voiceId: { type: "string", description: "Voice ID", required: false, in: "body" },
61
+ active: { type: "boolean", description: "Whether agent is active", required: false, in: "body" },
62
+ },
63
+ body: ["name", "personality", "knowledgeBases", "actions", "greeting", "voiceId", "active"],
64
+ },
65
+
66
+ {
67
+ name: "crm_delete_voice_agent",
68
+ description: "Delete a Voice AI agent.",
69
+ method: "DELETE",
70
+ path: "/voice-ai/agents/:agentId",
71
+ params: {
72
+ agentId: { type: "string", description: "Agent ID", required: true, in: "path" },
73
+ },
74
+ },
75
+
76
+ // ── Actions ────────────────────────────────────────────────
77
+
78
+ {
79
+ name: "crm_create_voice_action",
80
+ description: "Create an action that a Voice AI agent can perform (e.g., book appointment, transfer call, query knowledge base).",
81
+ method: "POST",
82
+ path: "/voice-ai/actions",
83
+ params: {
84
+ locationId: { type: "string", description: "Location ID", required: true, in: "body" },
85
+ name: { type: "string", description: "Action name", required: true, in: "body" },
86
+ type: { type: "string", description: "Action type (e.g., 'knowledge_base', 'book_appointment', 'transfer_call', 'webhook')", required: true, in: "body" },
87
+ config: { type: "object", description: "Action-specific configuration (e.g., knowledgeBaseId, calendarId, phoneNumber, webhookUrl)", required: false, in: "body" },
88
+ },
89
+ body: ["locationId", "name", "type", "config"],
90
+ },
91
+
92
+ {
93
+ name: "crm_get_voice_action",
94
+ description: "Get a Voice AI action by ID.",
95
+ method: "GET",
96
+ path: "/voice-ai/actions/:actionId",
97
+ params: {
98
+ actionId: { type: "string", description: "Action ID", required: true, in: "path" },
99
+ },
100
+ },
101
+
102
+ {
103
+ name: "crm_update_voice_action",
104
+ description: "Update a Voice AI action.",
105
+ method: "PUT",
106
+ path: "/voice-ai/actions/:actionId",
107
+ params: {
108
+ actionId: { type: "string", description: "Action ID", required: true, in: "path" },
109
+ name: { type: "string", description: "Action name", required: false, in: "body" },
110
+ type: { type: "string", description: "Action type", required: false, in: "body" },
111
+ config: { type: "object", description: "Action configuration", required: false, in: "body" },
112
+ },
113
+ body: ["name", "type", "config"],
114
+ },
115
+
116
+ {
117
+ name: "crm_delete_voice_action",
118
+ description: "Delete a Voice AI action.",
119
+ method: "DELETE",
120
+ path: "/voice-ai/actions/:actionId",
121
+ params: {
122
+ actionId: { type: "string", description: "Action ID", required: true, in: "path" },
123
+ },
124
+ },
125
+
126
+ // ── Call Logs ──────────────────────────────────────────────
127
+
128
+ {
129
+ name: "crm_list_voice_call_logs",
130
+ description: "List Voice AI call logs with filters.",
131
+ method: "GET",
132
+ path: "/voice-ai/dashboard/call-logs",
133
+ params: {
134
+ locationId: { type: "string", description: "Location ID", required: true, in: "query" },
135
+ agentId: { type: "string", description: "Filter by agent ID", required: false, in: "query" },
136
+ limit: { type: "number", description: "Results per page", required: false, in: "query" },
137
+ offset: { type: "number", description: "Pagination offset", required: false, in: "query" },
138
+ },
139
+ },
140
+
141
+ {
142
+ name: "crm_get_voice_call_log",
143
+ description: "Get a specific Voice AI call log with transcript and details.",
144
+ method: "GET",
145
+ path: "/voice-ai/dashboard/call-logs/:callId",
146
+ params: {
147
+ callId: { type: "string", description: "Call ID", required: true, in: "path" },
148
+ },
149
+ },
150
+ ]
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
  /**