flowengine-mcp-app 2.0.0 → 3.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.
Files changed (85) hide show
  1. package/build/cli/api.d.ts +19 -0
  2. package/build/cli/api.d.ts.map +1 -0
  3. package/build/cli/api.js +62 -0
  4. package/build/cli/api.js.map +1 -0
  5. package/build/cli/commands/delete.d.ts +4 -0
  6. package/build/cli/commands/delete.d.ts.map +1 -0
  7. package/build/cli/commands/delete.js +16 -0
  8. package/build/cli/commands/delete.js.map +1 -0
  9. package/build/cli/commands/deploy.d.ts +24 -0
  10. package/build/cli/commands/deploy.d.ts.map +1 -0
  11. package/build/cli/commands/deploy.js +103 -0
  12. package/build/cli/commands/deploy.js.map +1 -0
  13. package/build/cli/commands/domain.d.ts +9 -0
  14. package/build/cli/commands/domain.d.ts.map +1 -0
  15. package/build/cli/commands/domain.js +27 -0
  16. package/build/cli/commands/domain.js.map +1 -0
  17. package/build/cli/commands/env.d.ts +13 -0
  18. package/build/cli/commands/env.d.ts.map +1 -0
  19. package/build/cli/commands/env.js +53 -0
  20. package/build/cli/commands/env.js.map +1 -0
  21. package/build/cli/commands/list.d.ts +4 -0
  22. package/build/cli/commands/list.d.ts.map +1 -0
  23. package/build/cli/commands/list.js +25 -0
  24. package/build/cli/commands/list.js.map +1 -0
  25. package/build/cli/commands/login.d.ts +4 -0
  26. package/build/cli/commands/login.d.ts.map +1 -0
  27. package/build/cli/commands/login.js +37 -0
  28. package/build/cli/commands/login.js.map +1 -0
  29. package/build/cli/commands/logout.d.ts +2 -0
  30. package/build/cli/commands/logout.d.ts.map +1 -0
  31. package/build/cli/commands/logout.js +8 -0
  32. package/build/cli/commands/logout.js.map +1 -0
  33. package/build/cli/commands/logs.d.ts +4 -0
  34. package/build/cli/commands/logs.d.ts.map +1 -0
  35. package/build/cli/commands/logs.js +40 -0
  36. package/build/cli/commands/logs.js.map +1 -0
  37. package/build/cli/commands/show.d.ts +4 -0
  38. package/build/cli/commands/show.d.ts.map +1 -0
  39. package/build/cli/commands/show.js +13 -0
  40. package/build/cli/commands/show.js.map +1 -0
  41. package/build/cli/commands/usage.d.ts +7 -0
  42. package/build/cli/commands/usage.d.ts.map +1 -0
  43. package/build/cli/commands/usage.js +16 -0
  44. package/build/cli/commands/usage.js.map +1 -0
  45. package/build/cli/config.d.ts +17 -0
  46. package/build/cli/config.d.ts.map +1 -0
  47. package/build/cli/config.js +42 -0
  48. package/build/cli/config.js.map +1 -0
  49. package/build/cli/pack.d.ts +25 -0
  50. package/build/cli/pack.d.ts.map +1 -0
  51. package/build/cli/pack.js +111 -0
  52. package/build/cli/pack.js.map +1 -0
  53. package/build/cli/ui.d.ts +23 -0
  54. package/build/cli/ui.d.ts.map +1 -0
  55. package/build/cli/ui.js +105 -0
  56. package/build/cli/ui.js.map +1 -0
  57. package/build/cli.d.ts +10 -0
  58. package/build/cli.d.ts.map +1 -0
  59. package/build/cli.js +251 -0
  60. package/build/cli.js.map +1 -0
  61. package/build/client.d.ts +38 -0
  62. package/build/client.d.ts.map +1 -1
  63. package/build/client.js +124 -1
  64. package/build/client.js.map +1 -1
  65. package/build/index.d.ts +1 -1
  66. package/build/index.js +905 -344
  67. package/build/index.js.map +1 -1
  68. package/build/tools/functions.d.ts +14 -0
  69. package/build/tools/functions.d.ts.map +1 -0
  70. package/build/tools/functions.js +241 -0
  71. package/build/tools/functions.js.map +1 -0
  72. package/build/types.d.ts +155 -0
  73. package/build/types.d.ts.map +1 -0
  74. package/build/types.js +5 -0
  75. package/build/types.js.map +1 -0
  76. package/build/ui/component-viewer.d.ts +2 -2
  77. package/build/ui/component-viewer.js +4 -4
  78. package/build/ui/component-viewer.js.map +1 -1
  79. package/build/ui/dashboard.js +1 -1
  80. package/build/ui/ui/components.html +312 -0
  81. package/build/ui/ui/n8n.html +124 -0
  82. package/build/ui/ui/portals.html +211 -0
  83. package/build/ui/widgets.js +1 -1
  84. package/build/ui/widgets.js.map +1 -1
  85. package/package.json +23 -11
package/build/index.js CHANGED
@@ -6,35 +6,85 @@
6
6
  * Core Features:
7
7
  * 1. Instance Management - Provision and manage FlowEngine instances
8
8
  * 2. Client Portals - Monitor and access client portals
9
- * 3. AI FlowBuilder - Create forms, chatbots, and UI components
9
+ * 3. AI FlowBuilder - Create forms, chatbots, and UI embeds
10
10
  */
11
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
13
14
  import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
15
+ import { z } from 'zod';
16
+ import { readFile } from 'node:fs/promises';
17
+ import { join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { dirname } from 'node:path';
20
+ import { createServer } from 'node:http';
21
+ import { randomUUID } from 'node:crypto';
22
+ import { AsyncLocalStorage } from 'node:async_hooks';
14
23
  import { FlowEngineClient } from './client.js';
15
- import { renderUnifiedDashboard } from './ui/dashboard.js';
16
- import { renderWidgetBuilder } from './ui/widgets.js';
17
- import { renderN8nViewer } from './ui/n8n-viewer.js';
18
- import { renderComponentViewer } from './ui/component-viewer.js';
19
- import { renderError } from './ui/base.js';
24
+ import { registerFunctionsTools } from './tools/functions.js';
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+ const UI_DIR = join(__dirname, 'ui', 'ui'); // Vite outputs to build/ui/ui/
20
28
  // Environment configuration
21
29
  const API_KEY = process.env.FLOWENGINE_API_KEY;
22
30
  const BASE_URL = process.env.FLOWENGINE_BASE_URL || 'https://flowengine.cloud';
23
- // Initialize FlowEngine client
24
- const flowengine = API_KEY ? new FlowEngineClient({
25
- apiKey: API_KEY,
26
- baseUrl: BASE_URL,
27
- }) : null;
31
+ const MCP_MODE = (process.env.FLOWENGINE_MCP_MODE || 'all');
32
+ // Per-request API key context (HTTP mode - each user's key is isolated)
33
+ const requestApiKey = new AsyncLocalStorage();
28
34
  function ensureClient() {
29
- if (!flowengine) {
35
+ const apiKey = requestApiKey.getStore() || API_KEY;
36
+ if (!apiKey) {
30
37
  throw new Error('FlowEngine API key not configured. Set FLOWENGINE_API_KEY in your MCP config.');
31
38
  }
32
- return flowengine;
39
+ return new FlowEngineClient({ apiKey, baseUrl: BASE_URL });
40
+ }
41
+ function parseBody(req) {
42
+ return new Promise((resolve, reject) => {
43
+ let body = '';
44
+ req.on('data', (chunk) => { body += chunk; });
45
+ req.on('end', () => {
46
+ try {
47
+ resolve(body ? JSON.parse(body) : undefined);
48
+ }
49
+ catch {
50
+ resolve(undefined);
51
+ }
52
+ });
53
+ req.on('error', reject);
54
+ });
55
+ }
56
+ // Store latest tool results for UI to retrieve
57
+ const toolResults = new Map();
58
+ function storeToolResult(type, data) {
59
+ const key = `${type}-${Date.now()}`;
60
+ toolResults.set(key, data);
61
+ // Keep only last 10 results to prevent memory leaks
62
+ if (toolResults.size > 10) {
63
+ const firstKey = toolResults.keys().next().value;
64
+ if (firstKey) {
65
+ toolResults.delete(firstKey);
66
+ }
67
+ }
68
+ return key;
69
+ }
70
+ function getLatestToolResult(type) {
71
+ // Get most recent result of given type (or any type if not specified)
72
+ let latest = null;
73
+ let latestKey = '';
74
+ for (const [key, value] of toolResults) {
75
+ if (!type || value.type?.includes(type)) {
76
+ if (!latestKey || key > latestKey) {
77
+ latest = value;
78
+ latestKey = key;
79
+ }
80
+ }
81
+ }
82
+ return latest;
33
83
  }
34
84
  // Initialize MCP Server with modern API
35
85
  const server = new McpServer({
36
86
  name: 'flowengine-mcp',
37
- version: '2.0.0',
87
+ version: '2.0.9',
38
88
  }, {
39
89
  capabilities: {
40
90
  resources: {},
@@ -43,364 +93,875 @@ const server = new McpServer({
43
93
  });
44
94
  /**
45
95
  * ===========================
46
- * APP RESOURCES (UI Components)
47
- * ===========================
48
- */
49
- // Dashboard Resource (Portals + Instances)
50
- registerAppResource(server, 'FlowEngine Dashboard', 'ui://flowengine/dashboard', {
51
- description: 'INTERACTIVE UI APP: Shows live dashboard with all portals, instances, storage stats, and management controls. USE THIS UI when user asks for: dashboard, portals, instances, client accounts, portal management, or "show me FlowEngine". This provides a rich visual interface instead of JSON data.',
52
- }, async () => {
53
- try {
54
- const portals = await ensureClient().getClientInstances();
55
- const html = renderUnifiedDashboard(portals);
56
- return {
57
- contents: [{
58
- uri: 'ui://flowengine/dashboard',
59
- mimeType: RESOURCE_MIME_TYPE,
60
- text: html,
61
- }],
62
- };
63
- }
64
- catch (error) {
65
- const html = renderError(error.message || 'Failed to load dashboard');
66
- return {
67
- contents: [{
68
- uri: 'ui://flowengine/dashboard',
69
- mimeType: RESOURCE_MIME_TYPE,
70
- text: html,
71
- }],
72
- };
73
- }
74
- });
75
- // n8n Workflow Viewer Resource
76
- registerAppResource(server, 'n8n Workflow Viewer', 'ui://flowengine/workflow-viewer', {
77
- description: 'INTERACTIVE UI APP: Full-screen n8n workflow visualization with interactive demo. USE THIS UI when user asks to: view workflow, show n8n workflow, visualize automation, or preview workflow. Shows live workflow editor with share/export capabilities.',
78
- }, async () => {
79
- const html = renderN8nViewer();
80
- return {
81
- contents: [{
82
- uri: 'ui://flowengine/workflow-viewer',
83
- mimeType: RESOURCE_MIME_TYPE,
84
- text: html,
85
- }],
86
- };
87
- });
88
- // UI Component Viewer Resource
89
- registerAppResource(server, 'UI Component Viewer', 'ui://flowengine/component-viewer', {
90
- description: 'INTERACTIVE UI APP: Live preview of forms, chatbots, widgets and UI components. USE THIS UI when user asks to: preview component, show chatbot, view form, see widget, or display UI component. Interactive preview with embed codes.',
91
- }, async () => {
92
- const html = renderComponentViewer();
93
- return {
94
- contents: [{
95
- uri: 'ui://flowengine/component-viewer',
96
- mimeType: RESOURCE_MIME_TYPE,
97
- text: html,
98
- }],
99
- };
100
- });
101
- // AI FlowBuilder Dashboard Resource
102
- registerAppResource(server, 'AI FlowBuilder Dashboard', 'ui://flowengine/ui-builder', {
103
- description: 'INTERACTIVE UI APP: Dashboard for building and managing all UI components (forms, chatbots, widgets). USE THIS UI when user asks for: UI builder, component builder, create form, build chatbot, or manage components. Shows component gallery with creation tools.',
104
- }, async () => {
105
- try {
106
- const widgets = await ensureClient().getWidgets();
107
- const html = renderWidgetBuilder(widgets);
108
- return {
109
- contents: [{
110
- uri: 'ui://flowengine/ui-builder',
111
- mimeType: RESOURCE_MIME_TYPE,
112
- text: html,
113
- }],
114
- };
115
- }
116
- catch (error) {
117
- const html = renderError(error.message || 'Failed to load UI builder');
118
- return {
119
- contents: [{
120
- uri: 'ui://flowengine/ui-builder',
121
- mimeType: RESOURCE_MIME_TYPE,
122
- text: html,
123
- }],
124
- };
125
- }
126
- });
127
- /**
128
- * ===========================
129
- * TOOLS WITH UI (MCP Apps)
96
+ * APP RESOURCES (3 Focused UIs)
130
97
  * ===========================
131
98
  */
132
- // Instance Management - List with UI
133
- registerAppTool(server, 'flowengine_list_instances', {
134
- description: 'Show the FlowEngine Dashboard with interactive UI displaying all portals, instances, storage stats, and management controls. Use this when user wants to see the dashboard, view portals, check instances, or get an overview of FlowEngine. Renders a visual dashboard interface instead of JSON.',
135
- inputSchema: {
136
- type: 'object',
137
- properties: {},
138
- },
139
- _meta: {
140
- ui: {
141
- resourceUri: 'ui://flowengine/dashboard',
99
+ if (MCP_MODE !== 'functions') {
100
+ // 1. n8n Workflow Preview - ONLY workflow builder
101
+ registerAppResource(server, 'n8n Workflow Preview', 'ui://flowengine/n8n', {
102
+ description: 'n8n workflow builder preview. Simple, focused UI showing only the n8n workflow editor.',
103
+ mimeType: RESOURCE_MIME_TYPE,
104
+ }, async () => {
105
+ try {
106
+ let html = await readFile(join(UI_DIR, 'n8n.html'), 'utf-8');
107
+ // Inject latest tool result into HTML as a global variable
108
+ const latestResult = getLatestToolResult('workflow');
109
+ if (latestResult) {
110
+ const dataScript = `<script>window.__flowengineToolResult = ${JSON.stringify(latestResult)};</script>`;
111
+ html = html.replace('</head>', `${dataScript}</head>`);
112
+ }
113
+ return {
114
+ contents: [{
115
+ uri: 'ui://flowengine/n8n',
116
+ mimeType: RESOURCE_MIME_TYPE,
117
+ text: html,
118
+ }],
119
+ };
120
+ }
121
+ catch (error) {
122
+ return {
123
+ contents: [{
124
+ uri: 'ui://flowengine/n8n',
125
+ mimeType: RESOURCE_MIME_TYPE,
126
+ text: `<!DOCTYPE html><html><body><h1>Error loading n8n preview</h1><p>${error.message}</p></body></html>`,
127
+ }],
128
+ };
129
+ }
130
+ });
131
+ // 2. UI Embed Builder - ONLY component management
132
+ registerAppResource(server, 'UI Embed Builder', 'ui://flowengine/components', {
133
+ description: 'UI embed builder and manager. Simple, focused UI for creating and managing forms, chatbots, and widgets.',
134
+ mimeType: RESOURCE_MIME_TYPE,
135
+ }, async () => {
136
+ try {
137
+ let html = await readFile(join(UI_DIR, 'components.html'), 'utf-8');
138
+ // Inject latest tool result into HTML as a global variable
139
+ const latestResult = getLatestToolResult('component');
140
+ if (latestResult) {
141
+ const dataScript = `<script>window.__flowengineToolResult = ${JSON.stringify(latestResult)};</script>`;
142
+ html = html.replace('</head>', `${dataScript}</head>`);
143
+ }
144
+ return {
145
+ contents: [{
146
+ uri: 'ui://flowengine/components',
147
+ mimeType: RESOURCE_MIME_TYPE,
148
+ text: html,
149
+ }],
150
+ };
151
+ }
152
+ catch (error) {
153
+ return {
154
+ contents: [{
155
+ uri: 'ui://flowengine/components',
156
+ mimeType: RESOURCE_MIME_TYPE,
157
+ text: `<!DOCTYPE html><html><body><h1>Error loading component builder</h1><p>${error.message}</p></body></html>`,
158
+ }],
159
+ };
160
+ }
161
+ });
162
+ // 3. Portals Dashboard - ONLY portal management
163
+ registerAppResource(server, 'Portals Dashboard', 'ui://flowengine/portals', {
164
+ description: 'Portal management dashboard. Simple, focused UI for managing client portals, instances, and storage.',
165
+ mimeType: RESOURCE_MIME_TYPE,
166
+ }, async () => {
167
+ try {
168
+ const html = await readFile(join(UI_DIR, 'portals.html'), 'utf-8');
169
+ return {
170
+ contents: [{
171
+ uri: 'ui://flowengine/portals',
172
+ mimeType: RESOURCE_MIME_TYPE,
173
+ text: html,
174
+ }],
175
+ };
176
+ }
177
+ catch (error) {
178
+ return {
179
+ contents: [{
180
+ uri: 'ui://flowengine/portals',
181
+ mimeType: RESOURCE_MIME_TYPE,
182
+ text: `<!DOCTYPE html><html><body><h1>Error loading portals dashboard</h1><p>${error.message}</p></body></html>`,
183
+ }],
184
+ };
185
+ }
186
+ });
187
+ /**
188
+ * ===========================
189
+ * SIMPLE TOOLS (3 Tools Only)
190
+ * ===========================
191
+ * 1. flowengine_create_workflow - Create n8n workflow, return preview
192
+ * 2. flowengine_create_component - Create UI embed (form/chatbot/widget), return preview(s)
193
+ * 3. flowengine_get_instances - List portals/instances (read-only, links to website)
194
+ */
195
+ // 1. Create Workflow - Creates n8n workflow and returns preview
196
+ registerAppTool(server, 'flowengine_create_workflow', {
197
+ description: 'Create and show a sample n8n workflow instantly. Returns interactive workflow preview with a pre-made workflow template. Use when user asks to create/show workflows, automations, or n8n examples.',
198
+ inputSchema: {
199
+ name: z.string().describe('Workflow name or description'),
142
200
  },
143
- },
144
- }, async () => {
145
- const instances = await ensureClient().getInstances();
146
- return {
147
- content: [{
148
- type: 'text',
149
- text: JSON.stringify(instances, null, 2),
150
- }],
151
- };
152
- });
153
- // Portal Management - List with UI
154
- registerAppTool(server, 'flowengine_list_portals', {
155
- description: 'Show the FlowEngine Dashboard with interactive UI displaying all client portals, instances, and management controls. Use this when user wants to see portals, view client accounts, check portal status, or manage portals. Renders a visual dashboard interface instead of JSON.',
156
- inputSchema: {
157
- type: 'object',
158
- properties: {},
159
- },
160
- _meta: {
161
- ui: {
162
- resourceUri: 'ui://flowengine/dashboard',
163
- },
164
- },
165
- }, async () => {
166
- const portals = await ensureClient().getClientInstances();
167
- return {
168
- content: [{
169
- type: 'text',
170
- text: JSON.stringify(portals, null, 2),
171
- }],
172
- };
173
- });
174
- // UI Components - List with UI
175
- registerAppTool(server, 'flowengine_list_components', {
176
- description: 'Show the AI FlowBuilder Dashboard with interactive UI displaying all forms, chatbots, widgets, and UI components. Use this when user wants to see the UI builder, view components, manage forms/chatbots, or build UI elements. Renders a component gallery interface instead of JSON. Optionally filter by instanceId.',
177
- inputSchema: {
178
- type: 'object',
179
- properties: {
180
- instanceId: {
181
- type: 'string',
182
- description: 'Optional: Filter by instance ID',
201
+ _meta: {
202
+ ui: {
203
+ resourceUri: 'ui://flowengine/n8n',
183
204
  },
184
205
  },
185
- },
186
- _meta: {
187
- ui: {
188
- resourceUri: 'ui://flowengine/ui-builder',
189
- },
190
- },
191
- }, async (args) => {
192
- const instanceId = args ? args.instanceId : undefined;
193
- const widgets = await ensureClient().getWidgets(instanceId);
194
- return {
195
- content: [{
196
- type: 'text',
197
- text: JSON.stringify(widgets, null, 2),
198
- }],
199
- };
200
- });
201
- /**
202
- * ===========================
203
- * REGULAR TOOLS (No UI)
204
- * ===========================
205
- */
206
- // Get Instance Status
207
- server.registerTool('flowengine_get_instance_status', {
208
- description: 'Get health and status information for a specific instance',
209
- inputSchema: {
210
- type: 'object',
211
- properties: {
212
- instanceId: {
213
- type: 'string',
214
- description: 'The instance ID to check',
206
+ }, async (args) => {
207
+ const client = ensureClient();
208
+ const name = args.name;
209
+ // Create sample workflow object
210
+ const sampleWorkflow = {
211
+ "name": name || "Sample Workflow",
212
+ "nodes": [
213
+ {
214
+ "parameters": {
215
+ "path": "webhook",
216
+ "responseMode": "onReceived",
217
+ "options": {}
218
+ },
219
+ "id": "webhook-1",
220
+ "name": "Webhook",
221
+ "type": "n8n-nodes-base.webhook",
222
+ "typeVersion": 1,
223
+ "position": [250, 300],
224
+ "webhookId": "sample-webhook"
225
+ },
226
+ {
227
+ "parameters": {
228
+ "values": {
229
+ "string": [
230
+ {
231
+ "name": "message",
232
+ "value": "Received data from webhook"
233
+ }
234
+ ]
235
+ },
236
+ "options": {}
237
+ },
238
+ "id": "set-1",
239
+ "name": "Process Data",
240
+ "type": "n8n-nodes-base.set",
241
+ "typeVersion": 1,
242
+ "position": [450, 300]
243
+ },
244
+ {
245
+ "parameters": {
246
+ "conditions": {
247
+ "string": [
248
+ {
249
+ "value1": "={{$json.email}}",
250
+ "operation": "isNotEmpty"
251
+ }
252
+ ]
253
+ }
254
+ },
255
+ "id": "if-1",
256
+ "name": "Check Email",
257
+ "type": "n8n-nodes-base.if",
258
+ "typeVersion": 1,
259
+ "position": [650, 300]
260
+ },
261
+ {
262
+ "parameters": {
263
+ "fromEmail": "noreply@example.com",
264
+ "toEmail": "={{$json.email}}",
265
+ "subject": "Notification",
266
+ "text": "={{$json.message}}"
267
+ },
268
+ "id": "email-1",
269
+ "name": "Send Email",
270
+ "type": "n8n-nodes-base.emailSend",
271
+ "typeVersion": 1,
272
+ "position": [850, 250]
273
+ }
274
+ ],
275
+ "connections": {
276
+ "Webhook": {
277
+ "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]]
278
+ },
279
+ "Process Data": {
280
+ "main": [[{ "node": "Check Email", "type": "main", "index": 0 }]]
281
+ },
282
+ "Check Email": {
283
+ "main": [[{ "node": "Send Email", "type": "main", "index": 0 }]]
284
+ }
215
285
  },
286
+ "settings": {
287
+ "executionOrder": "v1"
288
+ }
289
+ };
290
+ try {
291
+ // Try to save workflow to n8n instance
292
+ const createResult = await client.createWorkflow(sampleWorkflow);
293
+ const workflowPreview = {
294
+ type: 'workflow_preview',
295
+ workflow: sampleWorkflow,
296
+ workflowId: createResult?.workflowId,
297
+ workflowUrl: createResult?.workflowUrl,
298
+ message: `✓ Created workflow: ${sampleWorkflow.name}`,
299
+ nodes: sampleWorkflow.nodes.length,
300
+ description: `Workflow created and saved to ${createResult?.instanceName || 'n8n instance'}. Ready to activate and deploy.`,
301
+ instanceName: createResult?.instanceName,
302
+ };
303
+ // Store for UI to retrieve
304
+ storeToolResult('workflow', workflowPreview);
305
+ return {
306
+ content: [{
307
+ type: 'text',
308
+ text: JSON.stringify(workflowPreview, null, 2),
309
+ }],
310
+ };
311
+ }
312
+ catch (error) {
313
+ // Fallback: return sample workflow for preview if creation fails
314
+ console.warn('Failed to create workflow in n8n:', error.message);
315
+ const fallbackWorkflow = {
316
+ type: 'workflow_preview',
317
+ workflow: sampleWorkflow,
318
+ message: `✓ Workflow preview created: ${sampleWorkflow.name}`,
319
+ nodes: sampleWorkflow.nodes.length,
320
+ description: `Preview ready. Note: Workflow could not be saved to n8n (${error.message}). Configure API key to enable persistence.`,
321
+ warning: error.message,
322
+ };
323
+ storeToolResult('workflow', fallbackWorkflow);
324
+ return {
325
+ content: [{
326
+ type: 'text',
327
+ text: JSON.stringify(fallbackWorkflow, null, 2),
328
+ }],
329
+ };
330
+ }
331
+ });
332
+ // 2. Create Component - Creates UI embed and returns preview(s)
333
+ registerAppTool(server, 'flowengine_create_component', {
334
+ description: 'Create a UI embed (form, chatbot, widget) in FlowEngine and show preview. Use when user asks to create forms, chatbots, contact forms, etc. Returns component preview and workflow preview if component needs automation (like chatbot).',
335
+ inputSchema: {
336
+ type: z.enum(['form', 'chatbot', 'widget']).describe('Component type'),
337
+ name: z.string().describe('Component name or description'),
338
+ config: z.any().optional().describe('Optional configuration'),
216
339
  },
217
- required: ['instanceId'],
218
- },
219
- }, async (args) => {
220
- const instanceId = args.instanceId;
221
- const status = await ensureClient().getInstanceStatus(instanceId);
222
- return {
223
- content: [{
224
- type: 'text',
225
- text: JSON.stringify(status, null, 2),
226
- }],
227
- };
228
- });
229
- // Create Instance
230
- server.registerTool('flowengine_create_instance', {
231
- description: 'Provision a new FlowEngine instance for a client',
232
- inputSchema: {
233
- type: 'object',
234
- properties: {
235
- data: {
236
- type: 'object',
237
- description: 'Instance configuration with client info and settings',
340
+ _meta: {
341
+ ui: {
342
+ resourceUri: 'ui://flowengine/components',
238
343
  },
239
344
  },
240
- required: ['data'],
241
- },
242
- }, async (args) => {
243
- const data = args.data;
244
- const result = await ensureClient().provisionInstance(data);
245
- return {
246
- content: [{
247
- type: 'text',
248
- text: `Instance created successfully. ID: ${result.instanceId || result.id || 'N/A'}`,
249
- }],
250
- };
251
- });
252
- // Update Instance
253
- server.registerTool('flowengine_update_instance', {
254
- description: 'Update instance settings and configuration',
255
- inputSchema: {
256
- type: 'object',
257
- properties: {
258
- data: {
259
- type: 'object',
260
- description: 'Updated instance configuration',
345
+ }, async (args) => {
346
+ const client = ensureClient();
347
+ const type = args.type;
348
+ const name = args.name;
349
+ const config = args.config;
350
+ // Create component data
351
+ const componentData = {
352
+ type: type,
353
+ name: name || `Sample ${type}`,
354
+ config: config || {},
355
+ };
356
+ try {
357
+ // Create component in database
358
+ const createResult = await client.createComponent(componentData);
359
+ const componentPreview = {
360
+ type: 'component_preview',
361
+ component: {
362
+ id: createResult?.component?.id,
363
+ ...componentData,
364
+ slug: createResult?.component?.slug,
365
+ },
366
+ componentId: createResult?.component?.id,
367
+ componentUrl: createResult?.componentUrl,
368
+ message: `✓ Created ${type}: ${componentData.name}`,
369
+ componentType: type,
370
+ description: `${type.charAt(0).toUpperCase() + type.slice(1)} component created successfully. Ready to customize and embed.`,
371
+ };
372
+ // Store for UI to retrieve
373
+ storeToolResult('component', componentPreview);
374
+ return {
375
+ content: [{
376
+ type: 'text',
377
+ text: JSON.stringify(componentPreview, null, 2),
378
+ }],
379
+ };
380
+ }
381
+ catch (error) {
382
+ // Fallback: return component preview if creation fails
383
+ console.warn('Failed to create component:', error.message);
384
+ const fallbackComponent = {
385
+ type: 'component_preview',
386
+ component: {
387
+ id: `component-${Date.now()}`,
388
+ ...componentData,
389
+ },
390
+ message: `✓ Component preview created: ${componentData.name}`,
391
+ componentType: type,
392
+ description: `Preview ready. Note: Component could not be saved (${error.message}). Configure API key to enable persistence.`,
393
+ warning: error.message,
394
+ };
395
+ storeToolResult('component', fallbackComponent);
396
+ return {
397
+ content: [{
398
+ type: 'text',
399
+ text: JSON.stringify(fallbackComponent, null, 2),
400
+ }],
401
+ };
402
+ }
403
+ });
404
+ // 3. Get Instances - Lists portals/instances (read-only, no create/delete)
405
+ registerAppTool(server, 'flowengine_get_instances', {
406
+ description: 'Show list of FlowEngine portals and hosting instances. Use when user asks to see their instances, portals, or hosting. Returns simple list with basic info and links to website. Read-only - all actions (create/delete) link to website.',
407
+ inputSchema: {},
408
+ _meta: {
409
+ ui: {
410
+ resourceUri: 'ui://flowengine/portals',
261
411
  },
262
412
  },
263
- required: ['data'],
264
- },
265
- }, async (args) => {
266
- const data = args.data;
267
- await ensureClient().updateInstance(data);
268
- return {
269
- content: [{
270
- type: 'text',
271
- text: 'Instance updated successfully',
272
- }],
273
- };
274
- });
275
- // Delete Instance
276
- server.registerTool('flowengine_delete_instance', {
277
- description: 'Permanently delete a FlowEngine instance',
278
- inputSchema: {
279
- type: 'object',
280
- properties: {
281
- instanceId: {
282
- type: 'string',
283
- description: 'The instance ID to delete',
413
+ }, async () => {
414
+ try {
415
+ const instances = await ensureClient().getInstances();
416
+ return {
417
+ content: [{
418
+ type: 'text',
419
+ text: JSON.stringify({
420
+ type: 'instances_list',
421
+ instances: instances,
422
+ message: `Found ${instances.length} instance(s). Click links to manage on flowengine.cloud`,
423
+ }, null, 2),
424
+ }],
425
+ };
426
+ }
427
+ catch (error) {
428
+ return {
429
+ content: [{
430
+ type: 'text',
431
+ text: JSON.stringify({
432
+ type: 'instances_list',
433
+ instances: [],
434
+ error: error.message,
435
+ apiKey: API_KEY ? `${API_KEY.substring(0, 10)}...` : 'NOT SET',
436
+ baseUrl: BASE_URL,
437
+ }, null, 2),
438
+ }],
439
+ };
440
+ }
441
+ });
442
+ // 4. Get Components - Lists all UI embeds (forms, chatbots, widgets)
443
+ registerAppTool(server, 'flowengine_get_components', {
444
+ description: 'Show list of UI embeds (forms, chatbots, widgets) with share links. Use when user asks to see their components, forms, chatbots, or widgets. Returns embed list with embed codes and share options.',
445
+ inputSchema: {},
446
+ _meta: {
447
+ ui: {
448
+ resourceUri: 'ui://flowengine/components',
284
449
  },
285
450
  },
286
- required: ['instanceId'],
287
- },
288
- }, async (args) => {
289
- const instanceId = args.instanceId;
290
- await ensureClient().deleteInstance(instanceId);
291
- return {
292
- content: [{
293
- type: 'text',
294
- text: 'Instance deleted successfully',
295
- }],
296
- };
297
- });
298
- // Get Component
299
- server.registerTool('flowengine_get_component', {
300
- description: 'Get detailed configuration for a specific UI component',
301
- inputSchema: {
302
- type: 'object',
303
- properties: {
304
- componentId: {
305
- type: 'string',
306
- description: 'The component ID',
451
+ }, async () => {
452
+ try {
453
+ const components = await ensureClient().getWidgets();
454
+ return {
455
+ content: [{
456
+ type: 'text',
457
+ text: JSON.stringify(components, null, 2),
458
+ }],
459
+ };
460
+ }
461
+ catch (error) {
462
+ // Return empty list if not authenticated
463
+ return {
464
+ content: [{
465
+ type: 'text',
466
+ text: JSON.stringify([], null, 2),
467
+ }],
468
+ };
469
+ }
470
+ });
471
+ // 5. Deploy Instance - Deploy a docker image or GitHub repo to a hosting instance
472
+ registerAppTool(server, 'flowengine_deploy_instance', {
473
+ description: 'Deploy a Docker image or GitHub repo to a FlowEngine hosting instance. Saves config then triggers deployment. Use when user wants to deploy, redeploy, or change what is running on an instance. Provide EITHER dockerImage OR githubRepo (not both). Port defaults to 3000.',
474
+ inputSchema: {
475
+ instanceId: z.string().describe('Instance ID to deploy to'),
476
+ dockerImage: z.string().optional().describe('Docker image to deploy (e.g. "nginx:latest", "ghcr.io/owner/repo:latest"). Use this OR githubRepo.'),
477
+ githubRepo: z.string().optional().describe('GitHub repo to build and deploy (e.g. "owner/repo"). Requires GitHub App installed. Use this OR dockerImage.'),
478
+ port: z.number().optional().describe('Container port your app listens on (default: 3000)'),
479
+ envVars: z.record(z.string()).optional().describe('Environment variables as key-value pairs (e.g. {"NODE_ENV": "production"})'),
480
+ },
481
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
482
+ }, async (args) => {
483
+ const client = ensureClient();
484
+ const { instanceId, dockerImage, githubRepo, port, envVars } = args;
485
+ if (!dockerImage && !githubRepo) {
486
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Provide either dockerImage or githubRepo.' }, null, 2) }] };
487
+ }
488
+ if (dockerImage && githubRepo) {
489
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Provide either dockerImage or githubRepo, not both.' }, null, 2) }] };
490
+ }
491
+ try {
492
+ await client.updateInstanceConfig(instanceId, {
493
+ ...(dockerImage ? { dockerImage } : { githubRepo }),
494
+ ...(port ? { port } : {}),
495
+ ...(envVars ? { envVars } : {}),
496
+ });
497
+ const result = await client.manageInstance(instanceId, 'redeploy');
498
+ return {
499
+ content: [{
500
+ type: 'text',
501
+ text: JSON.stringify({
502
+ success: true,
503
+ instanceId,
504
+ source: githubRepo ? 'github' : 'docker',
505
+ ...(githubRepo ? { githubRepo } : { dockerImage }),
506
+ port: port || 3000,
507
+ message: result.message || 'Deployment started. Check status with flowengine_get_instance_status.',
508
+ }, null, 2),
509
+ }],
510
+ };
511
+ }
512
+ catch (error) {
513
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
514
+ }
515
+ });
516
+ // 5b. Provision n8n - Provision an n8n automation instance on an empty slot
517
+ registerAppTool(server, 'flowengine_provision_n8n', {
518
+ description: 'Provision an n8n automation instance on an empty FlowEngine hosting slot. Use when user wants to deploy n8n to an existing empty instance. This is a full n8n provisioning - separate from docker deployments.',
519
+ inputSchema: {
520
+ instanceId: z.string().describe('Instance ID of the empty slot to provision n8n on'),
521
+ },
522
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
523
+ }, async (args) => {
524
+ const client = ensureClient();
525
+ const { instanceId } = args;
526
+ try {
527
+ const result = await client.deployService(instanceId, 'n8n');
528
+ return {
529
+ content: [{
530
+ type: 'text',
531
+ text: JSON.stringify({
532
+ success: true,
533
+ instanceId,
534
+ serviceType: 'n8n',
535
+ message: result.message || 'n8n provisioning started. Check status with flowengine_get_instance_status.',
536
+ }, null, 2),
537
+ }],
538
+ };
539
+ }
540
+ catch (error) {
541
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
542
+ }
543
+ });
544
+ // 5c. Provision OpenClaw - Provision an OpenClaw AI agent on an empty slot
545
+ registerAppTool(server, 'flowengine_provision_openclaw', {
546
+ description: 'Provision an OpenClaw AI agent on an empty FlowEngine hosting slot. Requires instance with 30GB+ storage. Use when user wants to deploy OpenClaw to an existing empty instance. This is a full OpenClaw provisioning with browser and gateway components - separate from docker deployments.',
547
+ inputSchema: {
548
+ instanceId: z.string().describe('Instance ID of the empty slot to provision OpenClaw on (must have 30GB+ storage)'),
549
+ },
550
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
551
+ }, async (args) => {
552
+ const client = ensureClient();
553
+ const { instanceId } = args;
554
+ try {
555
+ const result = await client.deployService(instanceId, 'openclaw');
556
+ return {
557
+ content: [{
558
+ type: 'text',
559
+ text: JSON.stringify({
560
+ success: true,
561
+ instanceId,
562
+ serviceType: 'openclaw',
563
+ message: result.message || 'OpenClaw provisioning started. Check status with flowengine_get_instance_status.',
564
+ }, null, 2),
565
+ }],
566
+ };
567
+ }
568
+ catch (error) {
569
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
570
+ }
571
+ });
572
+ // 5b. List GitHub Repos - List repos available for deployment
573
+ registerAppTool(server, 'flowengine_list_github_repos', {
574
+ description: 'List GitHub repositories accessible for deployment via the connected GitHub App. Use when user wants to deploy from GitHub and needs to choose a repo. Requires GitHub App to be installed on flowengine.cloud.',
575
+ inputSchema: {},
576
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
577
+ }, async () => {
578
+ try {
579
+ const result = await ensureClient().listGithubRepos();
580
+ if (!result.success) {
581
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error, installUrl: result.installUrl }, null, 2) }] };
582
+ }
583
+ const repos = (result.repos || []).map((r) => ({
584
+ full_name: r.full_name,
585
+ name: r.name,
586
+ private: r.private,
587
+ default_branch: r.default_branch,
588
+ html_url: r.html_url,
589
+ }));
590
+ return {
591
+ content: [{
592
+ type: 'text',
593
+ text: JSON.stringify({ success: true, repos, count: repos.length, tip: 'Use full_name (e.g. "owner/repo") with flowengine_deploy_instance.' }, null, 2),
594
+ }],
595
+ };
596
+ }
597
+ catch (error) {
598
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
599
+ }
600
+ });
601
+ // 5c. Manage Instance - Start/stop/restart a hosting instance
602
+ registerAppTool(server, 'flowengine_manage_instance', {
603
+ description: 'Start, stop, or restart a FlowEngine hosting instance. Use when user asks to start/stop/restart a container. For deploying a new image or repo, use flowengine_deploy_instance instead.',
604
+ inputSchema: {
605
+ instanceId: z.string().describe('Instance ID'),
606
+ action: z.enum(['start', 'stop', 'restart', 'redeploy']).describe('Action: start (boot stopped container), stop (halt container), restart (stop + start), redeploy (rebuild + restart with current config)'),
607
+ },
608
+ _meta: {
609
+ ui: {
610
+ resourceUri: 'ui://flowengine/portals',
307
611
  },
308
612
  },
309
- required: ['componentId'],
310
- },
311
- }, async (args) => {
312
- const componentId = args.componentId;
313
- const widget = await ensureClient().getWidget(componentId);
314
- return {
315
- content: [{
316
- type: 'text',
317
- text: JSON.stringify(widget, null, 2),
318
- }],
319
- };
320
- });
321
- // Create Component
322
- server.registerTool('flowengine_create_component', {
323
- description: 'Create a new UI component using AI FlowBuilder (form, chatbot, or widget)',
324
- inputSchema: {
325
- type: 'object',
326
- properties: {
327
- data: {
328
- type: 'object',
329
- description: 'Component configuration with type, name, and settings',
613
+ }, async (args) => {
614
+ const client = ensureClient();
615
+ const { instanceId, action } = args;
616
+ try {
617
+ const result = await client.manageInstance(instanceId, action);
618
+ return {
619
+ content: [{
620
+ type: 'text',
621
+ text: JSON.stringify({ success: true, action, instanceId, message: result.message || `Instance ${action} triggered`, ...result }, null, 2),
622
+ }],
623
+ };
624
+ }
625
+ catch (error) {
626
+ return {
627
+ content: [{
628
+ type: 'text',
629
+ text: JSON.stringify({ success: false, error: error.message }, null, 2),
630
+ }],
631
+ };
632
+ }
633
+ });
634
+ // 6. Get Instance Logs - Fetch container logs
635
+ registerAppTool(server, 'flowengine_get_instance_logs', {
636
+ description: 'Get container logs for a FlowEngine hosting instance (docker, OpenClaw, n8n). Returns recent log output. Use when user asks to see logs, debug issues, or check what a container is doing.',
637
+ inputSchema: {
638
+ instanceId: z.string().describe('Instance ID'),
639
+ lines: z.number().optional().describe('Number of log lines to return (default 200, max 1000)'),
640
+ },
641
+ _meta: {
642
+ ui: {
643
+ resourceUri: 'ui://flowengine/portals',
330
644
  },
331
645
  },
332
- required: ['data'],
333
- },
334
- }, async (args) => {
335
- const data = args.data;
336
- const result = await ensureClient().createWidget(data);
337
- return {
338
- content: [{
339
- type: 'text',
340
- text: `Component created successfully. ID: ${result.id || result.widgetId || 'N/A'}`,
341
- }],
342
- };
343
- });
344
- // Update Component
345
- server.registerTool('flowengine_update_component', {
346
- description: 'Update an existing UI component configuration',
347
- inputSchema: {
348
- type: 'object',
349
- properties: {
350
- componentId: {
351
- type: 'string',
352
- description: 'The component ID to update',
646
+ }, async (args) => {
647
+ const client = ensureClient();
648
+ const { instanceId, lines = 200 } = args;
649
+ try {
650
+ const result = await client.getInstanceLogs(instanceId, lines);
651
+ return {
652
+ content: [{
653
+ type: 'text',
654
+ text: result.logs || '(no logs available)',
655
+ }],
656
+ };
657
+ }
658
+ catch (error) {
659
+ return {
660
+ content: [{
661
+ type: 'text',
662
+ text: `Error fetching logs: ${error.message}`,
663
+ }],
664
+ };
665
+ }
666
+ });
667
+ // 7. Get Instance Deployments - Deployment history
668
+ registerAppTool(server, 'flowengine_get_instance_deployments', {
669
+ description: 'Get deployment history for a FlowEngine hosting instance. Shows past deploys with timestamps and status. Use when user asks about deployment history, when something was last deployed, or deployment status.',
670
+ inputSchema: {
671
+ instanceId: z.string().describe('Instance ID'),
672
+ },
673
+ _meta: {
674
+ ui: {
675
+ resourceUri: 'ui://flowengine/portals',
353
676
  },
354
- data: {
355
- type: 'object',
356
- description: 'Updated component configuration',
677
+ },
678
+ }, async (args) => {
679
+ const client = ensureClient();
680
+ const { instanceId } = args;
681
+ try {
682
+ const result = await client.getInstanceDeployments(instanceId);
683
+ return {
684
+ content: [{
685
+ type: 'text',
686
+ text: JSON.stringify({ instanceId, deployments: result.deployments || [], message: result.message }, null, 2),
687
+ }],
688
+ };
689
+ }
690
+ catch (error) {
691
+ return {
692
+ content: [{
693
+ type: 'text',
694
+ text: JSON.stringify({ success: false, error: error.message }, null, 2),
695
+ }],
696
+ };
697
+ }
698
+ });
699
+ // 8. Get Instance Status - Live status check
700
+ registerAppTool(server, 'flowengine_get_instance_status', {
701
+ description: 'Get live status of a specific FlowEngine instance. Returns running/stopped/provisioning state and instance details. Use when user asks if an instance is running or wants to check its current state.',
702
+ inputSchema: {
703
+ instanceId: z.string().describe('Instance ID'),
704
+ },
705
+ _meta: {
706
+ ui: {
707
+ resourceUri: 'ui://flowengine/portals',
357
708
  },
358
709
  },
359
- required: ['componentId', 'data'],
360
- },
361
- }, async (args) => {
362
- const componentId = args.componentId;
363
- const data = args.data;
364
- await ensureClient().updateWidget(componentId, data);
365
- return {
366
- content: [{
367
- type: 'text',
368
- text: 'Component updated successfully',
369
- }],
370
- };
371
- });
372
- // Delete Component
373
- server.registerTool('flowengine_delete_component', {
374
- description: 'Delete a UI component',
375
- inputSchema: {
376
- type: 'object',
377
- properties: {
378
- componentId: {
379
- type: 'string',
380
- description: 'The component ID to delete',
710
+ }, async (args) => {
711
+ const client = ensureClient();
712
+ const { instanceId } = args;
713
+ try {
714
+ const result = await client.getInstanceStatus(instanceId);
715
+ return {
716
+ content: [{
717
+ type: 'text',
718
+ text: JSON.stringify({
719
+ instanceId,
720
+ status: result.containerStatus || result.status,
721
+ coolifyStatus: result.coolifyStatus,
722
+ instance: result.instance,
723
+ }, null, 2),
724
+ }],
725
+ };
726
+ }
727
+ catch (error) {
728
+ return {
729
+ content: [{
730
+ type: 'text',
731
+ text: JSON.stringify({ success: false, error: error.message }, null, 2),
732
+ }],
733
+ };
734
+ }
735
+ });
736
+ // 9. List Instance Backups
737
+ registerAppTool(server, 'flowengine_list_instance_backups', {
738
+ description: 'List backups for an OpenClaw instance. Returns backup history with status, file size, and timestamps.',
739
+ inputSchema: {
740
+ instanceId: z.string().describe('Instance ID'),
741
+ },
742
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
743
+ }, async (args) => {
744
+ const client = ensureClient();
745
+ const { instanceId } = args;
746
+ try {
747
+ const result = await client.listInstanceBackups(instanceId);
748
+ return {
749
+ content: [{
750
+ type: 'text',
751
+ text: JSON.stringify({ instanceId, backups: result.backups || [] }, null, 2),
752
+ }],
753
+ };
754
+ }
755
+ catch (error) {
756
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
757
+ }
758
+ });
759
+ // 10. Create Instance Backup
760
+ registerAppTool(server, 'flowengine_create_instance_backup', {
761
+ description: 'Create a manual backup of an OpenClaw instance. Backs up Docker volumes to the server. Takes up to a few minutes.',
762
+ inputSchema: {
763
+ instanceId: z.string().describe('Instance ID'),
764
+ },
765
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
766
+ }, async (args) => {
767
+ const client = ensureClient();
768
+ const { instanceId } = args;
769
+ try {
770
+ const result = await client.createInstanceBackup(instanceId);
771
+ return {
772
+ content: [{
773
+ type: 'text',
774
+ text: JSON.stringify(result, null, 2),
775
+ }],
776
+ };
777
+ }
778
+ catch (error) {
779
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
780
+ }
781
+ });
782
+ // 11. Update Instance Config
783
+ registerAppTool(server, 'flowengine_update_instance_config', {
784
+ description: 'Update the docker image, port, or environment variables for a docker/website instance. Does not redeploy automatically - call flowengine_manage_instance with action=redeploy after to apply changes.',
785
+ inputSchema: {
786
+ instanceId: z.string().describe('Instance ID'),
787
+ dockerImage: z.string().optional().describe('New Docker image (e.g. nginx:latest)'),
788
+ githubRepo: z.string().optional().describe('GitHub repo to build from (e.g. "owner/repo"). Requires GitHub App to be installed.'),
789
+ port: z.number().optional().describe('Container port to expose'),
790
+ envVars: z.record(z.string()).optional().describe('Environment variables as key-value object'),
791
+ },
792
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
793
+ }, async (args) => {
794
+ const client = ensureClient();
795
+ const { instanceId, dockerImage, githubRepo, port, envVars } = args;
796
+ try {
797
+ const result = await client.updateInstanceConfig(instanceId, { dockerImage, githubRepo, port, envVars });
798
+ return {
799
+ content: [{
800
+ type: 'text',
801
+ text: JSON.stringify(result, null, 2),
802
+ }],
803
+ };
804
+ }
805
+ catch (error) {
806
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
807
+ }
808
+ });
809
+ // 12. Update Instance Type - Reclassify between docker and website
810
+ registerAppTool(server, 'flowengine_update_instance_type', {
811
+ description: 'Change the type of a FlowEngine instance between "docker" (full management) and "website" (URL + notes tracking). Use when user wants to reclassify an instance.',
812
+ inputSchema: {
813
+ instanceId: z.string().describe('Instance ID'),
814
+ serviceType: z.enum(['docker', 'website']).describe('New type: "docker" for full management, "website" for URL/notes tracking'),
815
+ },
816
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
817
+ }, async (args) => {
818
+ const client = ensureClient();
819
+ const { instanceId, serviceType } = args;
820
+ try {
821
+ await client.updateInstanceConfig(instanceId, { serviceType });
822
+ return {
823
+ content: [{ type: 'text', text: JSON.stringify({ success: true, instanceId, serviceType, message: `Instance type updated to "${serviceType}"` }, null, 2) }],
824
+ };
825
+ }
826
+ catch (error) {
827
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
828
+ }
829
+ });
830
+ // 13. Update Instance Domain - Update tracked URL for a website/docker instance
831
+ registerAppTool(server, 'flowengine_update_instance_domain', {
832
+ description: 'Update the tracked URL/domain for a website or docker instance. Updates the instance URL in FlowEngine (does not configure Coolify/DNS - do that separately). Use after setting up a custom domain.',
833
+ inputSchema: {
834
+ instanceId: z.string().describe('Instance ID'),
835
+ domain: z.string().describe('New domain (e.g. "app.yourdomain.com" or "https://app.yourdomain.com")'),
836
+ },
837
+ _meta: { ui: { resourceUri: 'ui://flowengine/portals' } },
838
+ }, async (args) => {
839
+ const client = ensureClient();
840
+ const { instanceId, domain } = args;
841
+ try {
842
+ const result = await client.updateInstanceDomain(instanceId, domain);
843
+ return {
844
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
845
+ };
846
+ }
847
+ catch (error) {
848
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
849
+ }
850
+ });
851
+ // 15. List Enrichment Providers - returns available providers with config schemas
852
+ registerAppTool(server, 'flowengine_list_enrichment_providers', {
853
+ description: 'List available enrichment providers (FlowEngine, Apollo, Bright Data) with their config schemas and connection status. Use this to see which providers the user can configure for visitor intelligence.',
854
+ inputSchema: {},
855
+ _meta: { ui: { resourceUri: 'ui://flowengine/n8n' } },
856
+ }, async () => {
857
+ const client = ensureClient();
858
+ try {
859
+ const result = await client.listEnrichmentProviders();
860
+ storeToolResult('enrichment', result);
861
+ return {
862
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
863
+ };
864
+ }
865
+ catch (error) {
866
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
867
+ }
868
+ });
869
+ // 16. Configure Enrichment - update provider config on a site (NEVER sets enrichment_provider - user choice only)
870
+ registerAppTool(server, 'flowengine_configure_enrichment', {
871
+ description: 'Configure enrichment and auto-push settings for a visitor intel site. Can update provider config, enrichment level, and auto-push to Smartlead/Instantly. IMPORTANT: Cannot change enrichment_provider or deanon_provider - those are user-only choices.',
872
+ inputSchema: {
873
+ siteId: z.string().describe('Visitor site ID'),
874
+ providerConfig: z.record(z.unknown()).optional().describe('Provider-specific configuration options (e.g. Apollo seniority filters, Bright Data dataset ID)'),
875
+ enrichmentLevel: z.enum(['basic', 'standard', 'full']).optional().describe('Enrichment depth: basic (3 contacts), standard (5), full (10)'),
876
+ autoPushTo: z.enum(['smartlead', 'instantly']).optional().describe('Auto-push new leads to this outreach platform. Set to empty string to disable.'),
877
+ autoPushCampaignId: z.string().optional().describe('Campaign ID to push leads to (required if autoPushTo is set)'),
878
+ },
879
+ _meta: { ui: { resourceUri: 'ui://flowengine/n8n' } },
880
+ }, async (args) => {
881
+ const client = ensureClient();
882
+ const { siteId, providerConfig, enrichmentLevel, autoPushTo, autoPushCampaignId } = args;
883
+ try {
884
+ const result = await client.configureEnrichment(siteId, { providerConfig, enrichmentLevel, autoPushTo, autoPushCampaignId });
885
+ return {
886
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
887
+ };
888
+ }
889
+ catch (error) {
890
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }, null, 2) }] };
891
+ }
892
+ });
893
+ // 17. Get Latest Tool Result - Used by UI to fetch the result of the last tool call
894
+ registerAppTool(server, 'flowengine_get_latest_result', {
895
+ description: 'Internal: Get the latest tool result (used by UI to fetch preview data)',
896
+ inputSchema: {},
897
+ _meta: {
898
+ ui: {
899
+ resourceUri: 'ui://flowengine/internal',
381
900
  },
382
901
  },
383
- required: ['componentId'],
384
- },
385
- }, async (args) => {
386
- const componentId = args.componentId;
387
- await ensureClient().deleteWidget(componentId);
388
- return {
389
- content: [{
390
- type: 'text',
391
- text: 'Component deleted successfully',
392
- }],
393
- };
394
- });
902
+ }, async () => {
903
+ const latest = getLatestToolResult();
904
+ if (!latest) {
905
+ return {
906
+ content: [{
907
+ type: 'text',
908
+ text: JSON.stringify({ error: 'No result available' }, null, 2),
909
+ }],
910
+ };
911
+ }
912
+ return {
913
+ content: [{
914
+ type: 'text',
915
+ text: JSON.stringify(latest, null, 2),
916
+ }],
917
+ };
918
+ });
919
+ } // end MCP_MODE !== 'functions'
920
+ if (MCP_MODE !== 'hosting') {
921
+ registerFunctionsTools(server, {
922
+ baseUrl: BASE_URL,
923
+ getApiKey: () => {
924
+ const apiKey = requestApiKey.getStore() || API_KEY;
925
+ if (!apiKey) {
926
+ throw new Error('FlowEngine API key not configured. Set FLOWENGINE_API_KEY in your MCP config.');
927
+ }
928
+ return apiKey;
929
+ },
930
+ });
931
+ }
395
932
  /**
396
933
  * ===========================
397
934
  * SERVER STARTUP
398
935
  * ===========================
399
936
  */
400
937
  async function main() {
401
- const transport = new StdioServerTransport();
402
- await server.server.connect(transport);
403
- console.error('FlowEngine MCP Server (Modern API) running on stdio');
938
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
939
+ if (port) {
940
+ // HTTP mode - for Claude.ai online "Add custom connector"
941
+ // API key is read from Authorization: Bearer <fe_...> header per request
942
+ const transport = new StreamableHTTPServerTransport({
943
+ sessionIdGenerator: () => randomUUID(),
944
+ });
945
+ await server.server.connect(transport);
946
+ const httpServer = createServer(async (req, res) => {
947
+ const authHeader = req.headers['authorization'] ?? '';
948
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : '';
949
+ let body;
950
+ if (req.method === 'POST') {
951
+ body = await parseBody(req);
952
+ }
953
+ await requestApiKey.run(token, () => transport.handleRequest(req, res, body));
954
+ });
955
+ httpServer.listen(port, () => {
956
+ console.error(`FlowEngine MCP Server running on HTTP port ${port}`);
957
+ });
958
+ }
959
+ else {
960
+ // Stdio mode - for Claude Desktop / Claude Code CLI
961
+ const transport = new StdioServerTransport();
962
+ await server.server.connect(transport);
963
+ console.error('FlowEngine MCP Server running on stdio');
964
+ }
404
965
  }
405
966
  main().catch((error) => {
406
967
  console.error('Fatal error in MCP server:', error);