@datanimbus/dnio-mcp 1.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 (59) hide show
  1. package/Dockerfile +20 -0
  2. package/docs/README.md +35 -0
  3. package/docs/architecture.md +171 -0
  4. package/docs/authentication.md +74 -0
  5. package/docs/tools/apps.md +59 -0
  6. package/docs/tools/connectors.md +76 -0
  7. package/docs/tools/data-pipes.md +286 -0
  8. package/docs/tools/data-services.md +105 -0
  9. package/docs/tools/deployment-groups.md +152 -0
  10. package/docs/tools/plugins.md +94 -0
  11. package/docs/tools/records.md +97 -0
  12. package/docs/workflows.md +195 -0
  13. package/env.example +16 -0
  14. package/package.json +43 -0
  15. package/readme.md +144 -0
  16. package/src/clients/api-keys.js +10 -0
  17. package/src/clients/apps.js +13 -0
  18. package/src/clients/base-client.js +78 -0
  19. package/src/clients/bots.js +10 -0
  20. package/src/clients/connectors.js +30 -0
  21. package/src/clients/data-formats.js +40 -0
  22. package/src/clients/data-pipes.js +33 -0
  23. package/src/clients/deployment-groups.js +59 -0
  24. package/src/clients/formulas.js +10 -0
  25. package/src/clients/functions.js +10 -0
  26. package/src/clients/plugins.js +39 -0
  27. package/src/clients/records.js +51 -0
  28. package/src/clients/services.js +63 -0
  29. package/src/clients/user-groups.js +10 -0
  30. package/src/clients/users.js +10 -0
  31. package/src/examples/ai-sdk-client.js +165 -0
  32. package/src/examples/claude_desktop_config.json +34 -0
  33. package/src/examples/express-integration.js +181 -0
  34. package/src/index.js +283 -0
  35. package/src/schemas/schema-converter.js +179 -0
  36. package/src/services/auth-manager.js +277 -0
  37. package/src/services/dnio-client.js +40 -0
  38. package/src/services/service-registry.js +150 -0
  39. package/src/services/session-manager.js +161 -0
  40. package/src/stdio-bridge.js +185 -0
  41. package/src/tools/_helpers.js +32 -0
  42. package/src/tools/api-keys.js +5 -0
  43. package/src/tools/apps.js +185 -0
  44. package/src/tools/bots.js +5 -0
  45. package/src/tools/connectors.js +165 -0
  46. package/src/tools/data-formats.js +806 -0
  47. package/src/tools/data-pipes.js +1305 -0
  48. package/src/tools/data-service-registry.js +500 -0
  49. package/src/tools/deployment-groups.js +511 -0
  50. package/src/tools/formulas.js +5 -0
  51. package/src/tools/functions.js +5 -0
  52. package/src/tools/mcp-tools-registry.js +38 -0
  53. package/src/tools/plugins.js +250 -0
  54. package/src/tools/records.js +217 -0
  55. package/src/tools/services.js +476 -0
  56. package/src/tools/user-groups.js +5 -0
  57. package/src/tools/users.js +5 -0
  58. package/src/utils/constants.js +135 -0
  59. package/src/utils/logger.js +63 -0
@@ -0,0 +1,286 @@
1
+ # Data Pipes (Flows)
2
+
3
+ Tools registered by `src/tools/data-pipes.js`. A **data pipe** (also called a **flow**) is a directed execution graph: a single **trigger node** (`inputNode`) followed by N **process nodes**, wired via `onSuccess` / `onError` arrays.
4
+
5
+ Backed by `DataPipesClient` (`src/clients/data-pipes.js`) for flow CRUD, and reuses `PluginsClient` (`plugins.listInstalled` with category filters) for trigger/process discovery.
6
+
7
+ Endpoint pattern: `/api/a/bm/${app}/flow[/…]`.
8
+
9
+ ## Mental model
10
+
11
+ ```
12
+ ┌─────────────┐ ┌───────────────┐ ┌────────────────┐
13
+ │ inputNode │ ──→ │ process node │ ──→ │ V1_RESPONSE │
14
+ │ (trigger) │ │ (parse, fetch,│ │ (optional end) │
15
+ │ │ │ save, code…) │ │ │
16
+ └─────────────┘ └───────────────┘ └────────────────┘
17
+ │ │
18
+ └─→ onError… └─→ onSuccess[].condition (branches)
19
+ ```
20
+
21
+ - The trigger is one of `V1_HTTP_SERVER`, `V1_TIMER`, `V1_TIMER_MULTIPLE`.
22
+ - Each node's `onSuccess` is an array of `{ _id, condition?, name?, color? }`. Multiple entries form parallel branches with conditions.
23
+ - Each node's `mappings[]` describe how its inputs come from previous nodes' outputs (see Mappings below).
24
+ - A flow that ends in `V1_RESPONSE` returns an HTTP body; otherwise it just terminates silently.
25
+
26
+ ## Always-full PUT
27
+
28
+ The platform rejects partial flow updates. Every mutating tool in this domain follows the same pattern: **GET the current flow → mutate in memory → PUT the full document**. The tools handle the get/put internally so the LLM never needs to call `get_flow` first (though it can, for inspection).
29
+
30
+ ## Server-side plugin resolution
31
+
32
+ `create_flow` and `add_node_to_flow` always **re-fetch** the plugin from the platform's `/my-node` endpoint on every call, so the persisted node always carries the canonical `inputSchema`, `outputSchema`, `errorSchema`, and `nodeId`. The LLM does not need to round-trip the full plugin object between tool calls — passing `{ _id: '<installed-plugin-id>' }` or `{ type: 'V1_PARSE_JSON' }` is enough. The full plugin object from `list_process_plugins` also works (and is used as-is when complete) — but if any of the schema fields is missing/empty, the tool re-resolves from the platform regardless. This guarantees the canonical schemas and prevents the LLM from accidentally clearing them by sending a slimmed-down object.
33
+
34
+ ## Mappings: simple vs full form
35
+
36
+ Every mapping in `add_node_to_flow.mappings` and `update_node_in_flow.mappings` accepts **two forms** (mix freely in one array):
37
+
38
+ **Simple form** — what the LLM should normally use:
39
+
40
+ ```json
41
+ { "from": "fetch_students.data", "to": "data" }
42
+ ```
43
+
44
+ The tool walks the source node's `outputSchema` (via `findNodeRef(flow, 'fetch_students')`) and the current node's `inputSchema` to build the full DNIO mapping object — `target` (with `_id`, `type`, `dataPath`, `dataPathSegs`), `source[]` (with `key`, `name`, `nodeId`, `dataPath`, `dataPathSegs`, `_id`, `type`, `subType`), `expression: { type: 'simple', value: " {{nodeId['key']}}" }`, plus `children`, `key`, `name`, `derivedFrom`. You don't construct any of that by hand.
45
+
46
+ For hardcoded expressions with no source, omit `from` and pass `expression`:
47
+
48
+ ```json
49
+ { "to": "authToken", "expression": "{{CONSTANTS.TOKEN}}" }
50
+ ```
51
+
52
+ **Full form** — for advanced cases (nested children, iterators):
53
+
54
+ Pass an entry that already has `target` as an object and `source` as an array. The tool detects this and uses it as-is. Use this when you need `children[]` for nested objects or `iterator` for arrays of objects.
55
+
56
+ ## Plugin selection strategy
57
+
58
+ The tool descriptions explicitly tell the agent:
59
+
60
+ 1. Search for an installed first-class plugin matching the task (`Save File` → `V1_SAVE_FILE`, `Parse JSON` → `V1_PARSE_JSON`, etc.).
61
+ 2. If nothing matches but the marketplace has it: `list_marketplace_plugins` → `install_plugins` → re-list.
62
+ 3. **Only** if no built-in covers the task and the logic is trivial / glue / one-off: fall back to `V1_CODEBLOCK` with custom code in `options.code`.
63
+
64
+ ## Tool reference
65
+
66
+ ### `list_trigger_plugins`
67
+
68
+ | | |
69
+ |---|---|
70
+ | **Purpose** | List installed plugins where `category=TRIGGER`. Source for `triggerPlugin` in `create_flow`. |
71
+ | **Inputs** | (none) |
72
+ | **Behaviour** | Calls `dnioClient.plugins.listInstalled(app, {filter: {category: 'TRIGGER'}, select: '-code'})`. |
73
+ | **Returns** | `{ app, count, plugins: [<trimmed plugin object>], usage }`. |
74
+
75
+ ### `list_process_plugins`
76
+
77
+ | | |
78
+ |---|---|
79
+ | **Purpose** | List installed plugins where `category=PROCESS`. Source for `pluginObject` in `add_node_to_flow`. |
80
+ | **Inputs** | `group` (optional, e.g. `'DataService'`, `'HTTP'`, `'Misc'`), `search` (optional). |
81
+ | **Behaviour** | Same client call as triggers but `category: 'PROCESS'`. Filtering by group/search is client-side. |
82
+ | **Returns** | Same shape as triggers. |
83
+ | **Description** | Includes the plugin-selection strategy verbatim so the LLM sees it at tool-call time. |
84
+
85
+ ### `list_flows`
86
+
87
+ | | |
88
+ |---|---|
89
+ | **Purpose** | Browse flows in the selected app. |
90
+ | **Inputs** | `name` (optional substring), `status` (optional, e.g. `'Draft'`, `'Active'`). |
91
+ | **Endpoint** | `GET /api/a/bm/${app}/flow?filter={"app":"${app}"[, "status":"…"]}&count=-1&select=_id,name,status,version,inputNode.type,description` |
92
+ | **Returns** | `{ app, count, flows: [{ flowId, name, status, version, triggerType, description }] }` |
93
+
94
+ ### `get_flow`
95
+
96
+ | | |
97
+ |---|---|
98
+ | **Purpose** | Fetch the full flow document. The mutating tools fetch internally too — this is for inspection. |
99
+ | **Inputs** | `flowId` (required, e.g. `FLOW6156`). |
100
+ | **Endpoint** | `GET /api/a/bm/${app}/flow/${flowId}` |
101
+ | **Returns** | The full flow JSON. |
102
+
103
+ ### `create_flow`
104
+
105
+ | | |
106
+ |---|---|
107
+ | **Purpose** | Create a new flow with one trigger node and zero or more initial process nodes. |
108
+ | **Inputs** | `name` (required), `description` (optional), `skipAuth` (optional, default true), `triggerPlugin` (required object), `triggerOptions` (required object), `initialNodes` (optional array). |
109
+ | **Validation** | Rejects if `triggerPlugin.category !== 'TRIGGER'`. Rejects if any `initialNodes[i].pluginObject.connectorType !== 'NONE'` and no `connector` is provided. |
110
+ | **Behaviour** | Builds `inputNode` from `triggerPlugin` + `triggerOptions`, slugifying the trigger label for `_id` (e.g. `http_server`, `timer`). For each initial node, builds the full node entry from its plugin (copying `inputSchema`, `outputSchema`, `errorSchema`, `category`, `group`, `icon`, `label`, `version`, `type`, `connectorType`), wires sequentially via `onSuccess`, places coordinates left-to-right at `x = (i+1) * 200`. POSTs `/flow`. |
111
+ | **Endpoint** | `POST /api/a/bm/${app}/flow` |
112
+ | **Returns** | `{ flowId, status, flow }` |
113
+
114
+ #### Trigger options shapes
115
+
116
+ | Trigger type | `triggerOptions` |
117
+ |---|---|
118
+ | `V1_HTTP_SERVER` | `{ method: 'POST', path: '/upload' }` |
119
+ | `V1_TIMER` | `{ cron: '0 9 * * *', timezone: 'Asia/Kolkata', holidayList?: '…' }` |
120
+ | `V1_TIMER_MULTIPLE` | `{ cronList: [{ cron, timezone }, …] }` |
121
+
122
+ ### `add_node_to_flow`
123
+
124
+ | | |
125
+ |---|---|
126
+ | **Purpose** | Append a process node to an existing flow and wire it after another node. |
127
+ | **Inputs** | `flowId`, `pluginObject` (full plugin from `list_process_plugins`), `name` (slug becomes `_id`), `afterNodeId` (the node whose `onSuccess` should point at this new one — use the inputNode's `_id` to attach right after the trigger), `options` (optional, merged on top of platform defaults), `mappings` (optional), `connector` (optional, `{_id}`), `coordinates` (optional, defaults to `{x: maxX(flow) + 200, y: 0}`), `branchCondition` (optional, see below). |
128
+ | **Validation** | Rejects if `pluginObject.connectorType !== 'NONE'` and no `connector` is provided. Rejects if `afterNodeId` doesn't exist in the flow. |
129
+ | **Behaviour** | Fetches flow → builds new node → resolves `_id` (slugified `name`, dedup-suffixed if collision) → appends to `nodes[]` → wires the edge → PUTs full document. |
130
+ | **Endpoint** | `GET /flow/${flowId}` then `PUT /flow/${flowId}` |
131
+ | **Returns** | `{ addedNodeId, flow }` |
132
+
133
+ #### Wiring rules
134
+
135
+ - Default (no `branchCondition`): **replace** `afterNodeId.onSuccess` with `[{ _id: <new node> }]`.
136
+ - With `branchCondition`: **append** `{ _id: <new node>, condition, name, color? }` to `afterNodeId.onSuccess` so multiple conditional branches coexist.
137
+
138
+ ### `update_node_in_flow`
139
+
140
+ | | |
141
+ |---|---|
142
+ | **Purpose** | Edit an existing node's options, mappings, coordinates, name, or connector. |
143
+ | **Inputs** | `flowId`, `nodeId` (or `inputNode._id` for the trigger), `options?`, `mappings?`, `coordinates?`, `name?`, `connector?`. |
144
+ | **Behaviour** | Fetches flow → finds node → merges `options` partially (does NOT touch keys not in the patch) → replaces `mappings` entirely if provided → updates the rest as given → PUTs. |
145
+ | **Returns** | `{ updatedNodeId, flow }` |
146
+
147
+ ### `connect_nodes`
148
+
149
+ | | |
150
+ |---|---|
151
+ | **Purpose** | Wire `onSuccess` / `onError` between existing nodes. Use to re-route or to add additional branches. |
152
+ | **Inputs** | `flowId`, `fromNodeId`, `toNodeId`, `edge` (optional, `'onSuccess'` default), `condition` (optional, lodash-style expression), `branchName` (required if `condition` is set), `branchColor` (optional hex without `#`), `mode` (optional `'append'` or `'replace'`). |
153
+ | **Default mode** | `replace` if no `condition`, `append` if `condition` is set. |
154
+ | **Behaviour** | GET → mutate `fromRef.node[edge]` → PUT. |
155
+ | **Returns** | `{ from, to, edge, mode, flow }` |
156
+
157
+ ### `remove_node_from_flow`
158
+
159
+ | | |
160
+ |---|---|
161
+ | **Purpose** | Delete a node and clean up dangling references in every other node's `onSuccess`/`onError`. |
162
+ | **Inputs** | `flowId`, `nodeId`, `force` (optional boolean). |
163
+ | **Validation** | Refuses if `nodeId === inputNode._id` (cannot remove the trigger). Refuses if removing this node would leave the inputNode pointing to nothing while other nodes still exist, unless `force: true`. |
164
+ | **Behaviour** | GET → splice from `nodes[]` → strip refs from every node's edges → PUT. |
165
+ | **Returns** | `{ removedNodeId, flow }` |
166
+
167
+ ### `publish_flow`
168
+
169
+ | | |
170
+ |---|---|
171
+ | **Purpose** | Move a flow out of `Draft` state. Until published, a flow can't be added to a deployment group. |
172
+ | **Inputs** | `flowId`. |
173
+ | **Endpoint** | `PUT /api/a/bm/${app}/flow/utils/${flowId}/publish` with body `{ app }` (server-injected). |
174
+ | **Returns** | `{ flowId, result }` |
175
+
176
+ ## Mappings
177
+
178
+ Each entry in `node.mappings[]` describes how a field on the **current node's input** is filled from previous nodes' outputs. Shape:
179
+
180
+ ```json
181
+ {
182
+ "key": "data",
183
+ "name": "data",
184
+ "target": {
185
+ "_id": "data",
186
+ "dataPath": "data",
187
+ "dataPathSegs": ["data"],
188
+ "type": "Buffer"
189
+ },
190
+ "source": [
191
+ {
192
+ "_id": "http_receiver.data",
193
+ "nodeId": "http_receiver",
194
+ "key": "data",
195
+ "name": "data",
196
+ "dataPath": "data",
197
+ "dataPathSegs": ["data"],
198
+ "type": "Buffer"
199
+ }
200
+ ],
201
+ "expression": {
202
+ "type": "simple",
203
+ "value": "{{http_receiver['data']}}"
204
+ },
205
+ "children": [],
206
+ "iterator": null,
207
+ "derivedFrom": null
208
+ }
209
+ ```
210
+
211
+ - `target` — field on this node's input.
212
+ - `source[]` — previous-node fields being read (can be empty for hardcoded expressions).
213
+ - `expression.value` — lodash-style template (see Runtime globals).
214
+ - `children[]` — recursive, for nested objects.
215
+ - `iterator` — set to a source array reference; `children[]` then describes the per-item mapping.
216
+
217
+ ## Runtime globals (in CODEBLOCK code, mapping `expression.value`, and branch `condition`)
218
+
219
+ The platform exposes these globals everywhere expressions are evaluated:
220
+
221
+ - **Every node's `_id`** is reachable two ways:
222
+ - Top-level bare variable in `{{ }}` expressions: `{{fetch_transactions['data']}}`, `{{route['data']['outputFormat']['_id']}}`.
223
+ - Via `node['<id>']` inside CODEBLOCK code: `node['parse_body'].data.records.length`.
224
+ - Outputs always live under `.data`.
225
+ - **`CONSTANTS`** — flow-level constants on the flow document. Example: `{{CONSTANTS.TOKEN}}`.
226
+ - **`ENV`** — deployment env vars. Example: `{{ENV.SOME_VAR}}`.
227
+
228
+ Common patterns:
229
+
230
+ ```
231
+ {{CONSTANTS.TOKEN}} ← header value
232
+ JSON.stringify({"id": {{route['data']['outputFormat']['_id']}}}) ← literal-with-interp
233
+ _.isNull({{prev.data.content}}) ← branch condition
234
+ {{fetch_transactions['data']['status']}} == "SUCCESS" ← branch equality
235
+ ```
236
+
237
+ The same documentation block is appended verbatim to the descriptions of `add_node_to_flow`, `update_node_in_flow`, and `connect_nodes`, so the LLM always has it in context when authoring expressions.
238
+
239
+ ## CODEBLOCK contract
240
+
241
+ `V1_CODEBLOCK` nodes execute custom JavaScript. The function signature is fixed:
242
+
243
+ ```js
244
+ async function executeCode(inputData, node, connectorConfig) {
245
+ try {
246
+ let data = {};
247
+ // your logic here
248
+ return data; // becomes outputSchema.data downstream
249
+ } catch (err) {
250
+ logger.error(err); // 'logger' is provided by the platform
251
+ throw err; // routes to onError
252
+ }
253
+ }
254
+ ```
255
+
256
+ Critical: the function MUST return `{ data: <whatever> }` (or assign to `data` inside the function and return it as shown). Anything else is dropped — the platform reads `outputSchema.data` only.
257
+
258
+ `V1_CODEBLOCK` has `connectorType=NONE`; do not pass a `connector`.
259
+
260
+ ## Branching example
261
+
262
+ ```
263
+ add_node_to_flow(
264
+ pluginObject: <V1_RESPONSE>,
265
+ name: 'no_txn_response',
266
+ afterNodeId: 'fetch_transactions',
267
+ branchCondition: {
268
+ condition: '_.isEmpty({{fetch_transactions["data"]["records"]}})',
269
+ name: 'No Transactions',
270
+ color: 'FF9800'
271
+ }
272
+ )
273
+
274
+ add_node_to_flow(
275
+ pluginObject: <V1_DATASERVICE_PUT>,
276
+ name: 'update_txns',
277
+ afterNodeId: 'fetch_transactions',
278
+ branchCondition: {
279
+ condition: '!_.isEmpty({{fetch_transactions["data"]["records"]}})',
280
+ name: 'Has Transactions',
281
+ color: '4CAF50'
282
+ }
283
+ )
284
+ ```
285
+
286
+ After both, `fetch_transactions.onSuccess` has two entries with conditions — the runtime evaluates them and routes accordingly.
@@ -0,0 +1,105 @@
1
+ # Data Services (Definitions)
2
+
3
+ Tools registered by `src/tools/services.js`. These manage **data-service definitions** — the schemas + hooks + workflow config that describe a Kubernetes-deployable record store. (CRUD on the records of an existing service is in [`records.md`](records.md).)
4
+
5
+ Backed by `ServicesClient` (`src/clients/services.js`).
6
+
7
+ Endpoint pattern: `/api/a/sm/${app}/service[/…]`.
8
+
9
+ ## What gets auto-injected on create/update
10
+
11
+ Three things are injected server-side into `create_data_service` (and `app` into `update_data_service`) so the LLM never has to remember them:
12
+
13
+ 1. **`payload.app = registry.selectedApp`** — the platform's create endpoint requires `app` in the body. Without this, the platform returns *"App data not found for undefined"*. The tool always overrides whatever the LLM sent.
14
+ 2. **Connectors** — if `payload.connectors.data._id` or `payload.connectors.file._id` is missing, the tool calls `dnioClient.connectors.list(app)`, picks the connector flagged `options.default && options.isValid !== false` for category `DB` (data) and `STORAGE` (file), and fills them in. The response surfaces `connectorsAutoFilled` so the user sees what was attached.
15
+ 3. **Roles & field permission map** — if `payload.role` is not already set with `roles[]` and `fields`, the tool generates the three standard roles (No Access / Manage / View) with random IDs (format `PNA_<10 digits>` for No Access, `P<10 digits>` for the others) and walks `payload.definition` to build a parallel `fields` map where every leaf field gets `{_t, _p}` with `_p` containing all three role IDs mapped to `"R"`. Object-type fields recurse into nested children with no `_t`/`_p` at the parent level. The response surfaces `rolesAutoFilled` with the generated role IDs.
16
+
17
+ `update_data_service` only injects `app`. Connectors and roles are not regenerated on update — the existing values are preserved.
18
+
19
+ ## Tool reference
20
+
21
+ ### `get_data_service_spec`
22
+
23
+ | | |
24
+ |---|---|
25
+ | **Purpose** | Returns the canonical creation spec — field types, hook formats, workflow config options, connector schema, and a full example payload. **MUST be called before `create_data_service`** so the LLM understands the required shape. |
26
+ | **Inputs** | (none) |
27
+ | **Source** | Static constant `DATA_SERVICE_CREATION_SPEC` in `src/utils/constants.js`. |
28
+ | **Returns** | The spec object. |
29
+
30
+ The spec covers:
31
+ - Supported field types: `String`, `Number`, `Boolean`, `Date`, `Object`, `Array` and the properties each accepts.
32
+ - The required first field (`_id` with prefix/counter).
33
+ - `preHooks` and `workflowHooks` shapes.
34
+ - `stateModel` (state-machine field) and `workflowConfig` (maker-checker approval).
35
+ - Defaults (`schemaFree`, `permanentDeleteData`, `disableInsights`, `enableSearchIndex`, `allowedFileTypes`).
36
+ - A connector section explaining the `connectors.data` / `connectors.file` requirement and that defaults will auto-attach.
37
+ - A complete `examplePayload`.
38
+
39
+ ### `create_data_service`
40
+
41
+ | | |
42
+ |---|---|
43
+ | **Purpose** | Create a new data service. |
44
+ | **Inputs** | `data` (required, JSON string matching the spec). |
45
+ | **Behaviour** | 1. Reject if no app is selected. 2. Parse the JSON. 3. Force `payload.app = registry.selectedApp`. 4. Auto-fill `connectors` if missing. 5. Auto-fill `role` (with roles + fields) if missing. 6. POST. |
46
+ | **Endpoint** | `POST /api/a/sm/${app}/service` |
47
+ | **Returns** | `{ result, connectorsAutoFilled?, rolesAutoFilled? }` — the auto-fill blocks are omitted when the user supplied them. |
48
+
49
+ ### `get_data_service`
50
+
51
+ | | |
52
+ |---|---|
53
+ | **Purpose** | Fetch a data service definition by ID. Use `select` to fetch only specific fields. |
54
+ | **Inputs** | `serviceId` (required, e.g. `SRVC13120`), `select` (optional CSV), `draft` (optional boolean — fetches the unsaved draft version when true). |
55
+ | **Endpoint** | `GET /api/a/sm/${app}/service/${serviceId}?filter={"app":"${app}"}[&draft=true][&select=…]` |
56
+ | **Returns** | The data-service document. |
57
+ | **Common select patterns** | Documented inline in the tool description: `_id,name,description,status,attributeCount,version` (overview); `_id,name,definition,schemaFree` (schema); `_id,name,preHooks,workflowHooks` (hooks); `_id,name,workflowConfig,stateModel` (workflow); a longer one for full-edit. |
58
+
59
+ ### `list_data_services`
60
+
61
+ | | |
62
+ |---|---|
63
+ | **Purpose** | List all data services in the selected app. |
64
+ | **Inputs** | `select` (optional CSV, default `_id,name,description,status,attributeCount`), `statusFilter` (optional, one of `Active`, `Pending`, `Draft`, `Stopped`). |
65
+ | **Endpoint** | `GET /api/a/sm/${app}/service?filter={"app":"${app}"[, "status":"…"]}&count=-1[&select=…]` |
66
+ | **Returns** | The array. |
67
+
68
+ ### `update_data_service`
69
+
70
+ | | |
71
+ |---|---|
72
+ | **Purpose** | Update an existing data service. The platform requires the **full** payload, not a partial patch — call `get_data_service` first, mutate, send back. After updating, call `deploy_data_service` to make the changes live. |
73
+ | **Inputs** | `serviceId` (required), `data` (required, JSON string). |
74
+ | **Behaviour** | Force `payload.app = registry.selectedApp`. Does **not** auto-fill connectors or roles. |
75
+ | **Endpoint** | `PUT /api/a/sm/${app}/service/${serviceId}` |
76
+ | **Returns** | The updated document. |
77
+
78
+ ### `deploy_data_service`
79
+
80
+ | | |
81
+ |---|---|
82
+ | **Purpose** | Deploy a data service after creation or update. Triggers the actual Kubernetes deployment. **Must** be called after `create_data_service` or `update_data_service` to make the service usable. |
83
+ | **Inputs** | `serviceId` (required). |
84
+ | **Endpoint** | `PUT /api/a/sm/${app}/service/utils/${serviceId}/deploy` |
85
+ | **Returns** | `{ serviceId, status: 'Deployment initiated', result }` |
86
+
87
+ ### `start_stop_data_service`
88
+
89
+ | | |
90
+ |---|---|
91
+ | **Purpose** | Start or stop a deployed data service (toggles the running pod). |
92
+ | **Inputs** | `serviceId` (required), `action` (required, `start` or `stop`). |
93
+ | **Endpoint** | `PUT /api/a/sm/${app}/service/utils/${serviceId}/${start\|stop}` |
94
+ | **Returns** | `{ serviceId, action, result }` |
95
+
96
+ ## Typical sequence
97
+
98
+ ```
99
+ get_data_service_spec # learn the shape
100
+ create_data_service(data) # tool injects app, connectors, role
101
+ deploy_data_service(serviceId) # K8s rollout
102
+ # … service appears in list_services / select_app once active
103
+ update_data_service(serviceId, …) # if you need to change the schema
104
+ deploy_data_service(serviceId) # apply the change
105
+ ```
@@ -0,0 +1,152 @@
1
+ # Deployment Groups (Kubernetes Deployment Lifecycle)
2
+
3
+ Tools registered by `src/tools/deployment-groups.js`. A **deployment group** is the unit of actual Kubernetes deployment: it bundles one or more **published** data pipes (flows) under a name and, when started, spins up real K8s pods/services for every flow in the group.
4
+
5
+ Backed by `DeploymentGroupsClient` (`src/clients/deployment-groups.js`).
6
+
7
+ Endpoint pattern: `/api/a/bm/${app}/deployment/group[/…]`.
8
+
9
+ ## Hard rule: one flow, one group
10
+
11
+ A flow can belong to **at most one** deployment group. The platform enforces this; the tools enforce it client-side too for clear error messages:
12
+
13
+ - `list_available_flows` returns only flows that are **published and not currently bound to any group**.
14
+ - `create_deployment_group` and `add_flows_to_deployment_group` validate every requested flow against `list_available_flows` and reject the entire call atomically if any are unavailable.
15
+ - `add_flows_to_deployment_group` additionally checks the target group's existing `deployments[]` to catch duplicates within the same group.
16
+ - Removing a flow from a group (or deleting the group) frees that flow up again.
17
+
18
+ ## K8s timing
19
+
20
+ `start` / `stop` / `sync` / `delete` are async on the K8s side. Tools return as soon as the platform accepts the request — they do **not** poll for pod readiness. Settle time is ~10 seconds. Re-fetch via `get_deployment_group` if you need to confirm.
21
+
22
+ ## Tool reference
23
+
24
+ ### `list_available_flows`
25
+
26
+ | | |
27
+ |---|---|
28
+ | **Purpose** | List flows in the selected app that can be added to a deployment group right now. Source of truth for `create_deployment_group` / `add_flows_to_deployment_group`. |
29
+ | **Inputs** | (none) |
30
+ | **Endpoint** | `GET /api/a/bm/${app}/deployment/group/utils/available/flows?filter={"app":"${app}"}` |
31
+ | **Returns** | `{ app, count, flows: [{ _id, name, type }] }` |
32
+
33
+ ### `list_deployment_groups`
34
+
35
+ | | |
36
+ |---|---|
37
+ | **Purpose** | List deployment groups in the selected app, with their status and bound flows. |
38
+ | **Inputs** | `name` (optional substring), `status` (optional, e.g. `'Running'`, `'Stopped'`, `'Pending'`). |
39
+ | **Endpoint** | `GET /api/a/bm/${app}/deployment/group?filter={"app":"${app}"[, "status":"…"]}&count=-1` |
40
+ | **Returns** | `{ app, count, groups: [{ groupId, name, status, deploymentCount, deployments: [{ _id, name, version, type }] }] }` |
41
+
42
+ ### `get_deployment_group`
43
+
44
+ | | |
45
+ |---|---|
46
+ | **Purpose** | Fetch the full document for a single group. |
47
+ | **Inputs** | `groupId` (required, e.g. `DG2943`). |
48
+ | **Endpoint** | `GET /api/a/bm/${app}/deployment/group/${groupId}` |
49
+ | **Returns** | The full group JSON. |
50
+
51
+ ### `create_deployment_group`
52
+
53
+ | | |
54
+ |---|---|
55
+ | **Purpose** | Create a new group bundling one or more published flows. |
56
+ | **Inputs** | `name` (required), `flowIds` (required array, length ≥ 1). |
57
+ | **Validation** | Atomic. Rejects if `flowIds` contains duplicates. Calls `list_available_flows`; rejects the whole call if any requested flow isn't available, with explicit guidance pointing at `list_deployment_groups`. |
58
+ | **Behaviour** | For each valid flow, fetches the flow document via `dnioClient.dataPipes.get(app, flowId)` to read `inputNode` + `version`. Builds `deployments[]` entries shaped `{ _id, name, type: 'FLOW', version, inputNode }`. POSTs. |
59
+ | **Endpoint** | `POST /api/a/bm/${app}/deployment/group` |
60
+ | **Returns** | `{ groupId, status, group }` |
61
+
62
+ ### `add_flows_to_deployment_group`
63
+
64
+ | | |
65
+ |---|---|
66
+ | **Purpose** | Append flows to an existing group. |
67
+ | **Inputs** | `groupId` (required), `flowIds` (required array). |
68
+ | **Validation** | Same atomic checks: no duplicates in input, no flows already in this group, all flows in `list_available_flows`. |
69
+ | **Behaviour** | GET group → fetch each flow → append entries → PUT. |
70
+ | **Endpoint** | `GET … /${groupId}` then `PUT … /${groupId}` |
71
+ | **Returns** | `{ groupId, addedFlowIds, group }` |
72
+
73
+ ### `remove_flows_from_deployment_group`
74
+
75
+ | | |
76
+ |---|---|
77
+ | **Purpose** | Detach flows from a group. The removed flows become available again. |
78
+ | **Inputs** | `groupId`, `flowIds` (required array), `force` (optional boolean). |
79
+ | **Behaviour** | GET → filter `deployments[]` → if none of the requested flowIds were in the group, return error. PUT. If the group is now empty and `force` is not set, the response includes a warning that the next `start` will fail. |
80
+ | **Endpoint** | `GET … /${groupId}` then `PUT … /${groupId}` |
81
+ | **Returns** | `{ groupId, removedCount, remainingCount, group, warning? }` |
82
+
83
+ ### `rename_deployment_group`
84
+
85
+ | | |
86
+ |---|---|
87
+ | **Purpose** | Change the name of a group. Does not affect deployments or running pods. |
88
+ | **Inputs** | `groupId`, `name`. |
89
+ | **Behaviour** | GET → mutate `name` → PUT. |
90
+ | **Returns** | `{ groupId, name, group }` |
91
+
92
+ ### `start_deployment_group`
93
+
94
+ | | |
95
+ |---|---|
96
+ | **Purpose** | Spin up Kubernetes pods + services for every flow in the group. |
97
+ | **Inputs** | `groupId`. |
98
+ | **Endpoint** | `PUT /api/a/bm/${app}/deployment/group/utils/${groupId}/start` |
99
+ | **Returns** | `{ groupId, action: 'start', note: 'Kubernetes deployment initiated; may take ~10s to settle.', result }` |
100
+
101
+ ### `stop_deployment_group`
102
+
103
+ | | |
104
+ |---|---|
105
+ | **Purpose** | Tear down K8s pods. Group definition + bound flows are preserved (flows stay unavailable to other groups until removed or the group is deleted). |
106
+ | **Inputs** | `groupId`. |
107
+ | **Endpoint** | `PUT /api/a/bm/${app}/deployment/group/utils/${groupId}/stop` |
108
+ | **Returns** | `{ groupId, action: 'stop', result }` |
109
+
110
+ ### `sync_deployment_group`
111
+
112
+ | | |
113
+ |---|---|
114
+ | **Purpose** | Re-pull the latest **published** version of every flow in the group and reconcile the running pods. Use after editing + re-publishing a flow that's already bound to a running group. Drafts are ignored. |
115
+ | **Inputs** | `groupId`. |
116
+ | **Endpoint** | `PUT /api/a/bm/${app}/deployment/group/utils/${groupId}/sync` |
117
+ | **Returns** | `{ groupId, action: 'sync', result }` |
118
+
119
+ ### `delete_deployment_group`
120
+
121
+ | | |
122
+ |---|---|
123
+ | **Purpose** | Permanently delete a group. K8s torn down, group definition removed, bound flows freed. |
124
+ | **Inputs** | `groupId`, `confirm` (optional boolean — required `true` to actually delete). |
125
+ | **Behaviour** | Two-phase. With `confirm: false` (default), GETs the group and returns `{ error: 'confirmation required', groupId, freedFlows: [{_id, name, version}], group }` so the user can see exactly which flows would be freed. With `confirm: true`, calls DELETE. |
126
+ | **Endpoint** | `DELETE /api/a/bm/${app}/deployment/group/${groupId}` |
127
+ | **Returns** | When confirmed: `{ groupId, action: 'deleted', result }`. |
128
+
129
+ ### `get_deployment_group_yamls`
130
+
131
+ | | |
132
+ |---|---|
133
+ | **Purpose** | Fetch the actual Kubernetes Deployment + Service YAMLs the platform generated. Useful for debugging or showing the user. |
134
+ | **Inputs** | `groupId`. |
135
+ | **Endpoint** | `GET /api/a/bm/${app}/deployment/group/utils/${groupId}/yamls?filter={"app":"${app}"}` |
136
+ | **Returns** | `{ groupId, yamls }` |
137
+
138
+ ## Typical sequence
139
+
140
+ ```
141
+ list_available_flows # see what's eligible
142
+ create_deployment_group(name, flowIds: [FLOW6156]) # bundle
143
+ start_deployment_group(groupId) # K8s up
144
+ get_deployment_group_yamls(groupId) # inspect generated K8s objects
145
+ # … later, after editing + republishing a flow inside the group:
146
+ sync_deployment_group(groupId) # roll forward
147
+ # … to release a flow:
148
+ remove_flows_from_deployment_group(groupId, [flowId])
149
+ # … or to tear it all down:
150
+ stop_deployment_group(groupId)
151
+ delete_deployment_group(groupId, confirm: true)
152
+ ```
@@ -0,0 +1,94 @@
1
+ # Plugins (Workflow Nodes)
2
+
3
+ Tools registered by `src/tools/plugins.js`. **Plugins** are reusable code blocks that act as **nodes inside data pipes** — parsing, HTTP calls, data-service operations, file ops, custom logic, etc.
4
+
5
+ The platform separates a global **marketplace catalog** (cross-app) from per-app **installed** plugins. A plugin must be installed into the selected app before flow tools can use it.
6
+
7
+ Backed by `PluginsClient` (`src/clients/plugins.js`).
8
+
9
+ ## Lifecycle
10
+
11
+ ```
12
+ list_marketplace_plugins → catalog of available plugins
13
+
14
+
15
+ install_plugins(marketIds) → installs into the selected app
16
+
17
+
18
+ list_installed_plugins → list / get / inspect the installed copies
19
+
20
+ ├─ update_plugins(ids) → refresh to latest marketplace version
21
+
22
+ └─ uninstall_plugins(ids) → destructive, may break flows that use them
23
+ ```
24
+
25
+ **Critical: marketplace `_id` ≠ installed `_id`.** The marketplace returns short hex `_id` values (e.g. `6859067143a8a209fc83a000`); after install, the platform assigns a different long-hex `_id` for the per-app copy. Use `marketId` for `install_plugins`; use `_id` from `list_installed_plugins` for everything else.
26
+
27
+ ## Tool reference
28
+
29
+ ### `list_marketplace_plugins`
30
+
31
+ | | |
32
+ |---|---|
33
+ | **Purpose** | Browse the cross-app catalog of plugins. |
34
+ | **Inputs** | `search` (optional, substring on label or type). |
35
+ | **Endpoint** | `GET /api/a/bm/${app}/marketplace/node?page=1&count=-1&sort=label&filter={}&select=label,type,version,icon` |
36
+ | **Returns** | `{ app, count, plugins: [{ marketId, type, label, version, icon }], usage }` |
37
+
38
+ ### `install_plugins`
39
+
40
+ | | |
41
+ |---|---|
42
+ | **Purpose** | Install one or more marketplace plugins into the selected app. Idempotent — installing an already-installed plugin returns the existing instance. |
43
+ | **Inputs** | `marketIds` (required array of strings, min length 1; pass even one as an array). |
44
+ | **Endpoint** | `POST /api/a/bm/${app}/my-node/utils/install` with body `{ marketIds: [...] }` |
45
+ | **Returns** | `{ app, requested, installed, failed, results: [...] }`. The tool buckets results by status code so the LLM can report which installs succeeded/failed. |
46
+
47
+ ### `list_installed_plugins`
48
+
49
+ | | |
50
+ |---|---|
51
+ | **Purpose** | List plugins installed in the selected app, or fetch full details for a single one. |
52
+ | **Inputs** | `pluginId` (optional, installed `_id`), `details` (optional boolean, default false), `search` (optional). |
53
+ | **Endpoint** | `GET /api/a/bm/${app}/my-node?count=-1&filter={"app":"${app}"[, "_id":"…"]}&select=…` |
54
+ | **Behaviour** | When `details: false` the select is `label,type,version,icon` (summary). When `details: true` the select is `_id,app,nodeId,version,category,group,type,label,icon,connectorType,inputSchema,outputSchema,errorSchema,code` — large payload, includes the full executeCode source. When `pluginId` is set the response collapses to a single plugin object; otherwise an array. |
55
+ | **Returns** | When listing: `{ app, count, details, plugins }`. When `pluginId` given: `{ app, plugin }`. |
56
+
57
+ ### `update_plugins`
58
+
59
+ | | |
60
+ |---|---|
61
+ | **Purpose** | Refresh installed plugins to the latest version from the marketplace. Idempotent. |
62
+ | **Inputs** | `ids` (required array, **installed** `_id` values, not marketplace IDs). |
63
+ | **Endpoint** | `POST /api/a/bm/${app}/my-node/utils/update` with body `{ app, ids }` (the `app` is server-injected). |
64
+ | **Returns** | `{ app, requested, result }` |
65
+
66
+ ### `uninstall_plugins`
67
+
68
+ | | |
69
+ |---|---|
70
+ | **Purpose** | Uninstall plugins from the selected app. **Destructive** — data pipes that reference uninstalled plugins may break. |
71
+ | **Inputs** | `ids` (required array, installed `_id`). |
72
+ | **Endpoint** | `POST /api/a/bm/${app}/my-node/utils/uninstall` with body `{ app, ids }` |
73
+ | **Returns** | `{ app, requested, result }` |
74
+
75
+ ## Plugin categories
76
+
77
+ Plugins are split into two categories the agent cares about:
78
+
79
+ - `TRIGGER` — starter nodes for flows (`V1_HTTP_SERVER`, `V1_TIMER`, `V1_TIMER_MULTIPLE`). Drive `list_trigger_plugins` in the data-pipes domain.
80
+ - `PROCESS` — downstream nodes used after the trigger (`V1_PARSE_JSON`, `V1_SAVE_FILE`, `V1_RESPONSE`, `V1_DATASERVICE_*`, `V1_CALL_FLOW`, `V1_CODEBLOCK`, …). Drive `list_process_plugins` in the data-pipes domain.
81
+
82
+ Plugins are filtered by category there — see [`data-pipes.md`](data-pipes.md).
83
+
84
+ ## Typical sequence
85
+
86
+ ```
87
+ list_marketplace_plugins(search: 'parse')
88
+ → user picks marketIds
89
+ install_plugins([marketId])
90
+ list_installed_plugins # confirm + grab the new _id
91
+ list_installed_plugins(pluginId, details: true) # if you need the schema/code
92
+ ```
93
+
94
+ After install, the plugins are usable by `add_node_to_flow` in the data-pipes domain.