@enfyra/mcp-server 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -106,7 +106,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
106
106
  '- Enfyra uses **Socket.IO**. Gateways and events are stored in **`websocket_definition`** and **`websocket_event_definition`**; manage via REST (MCP `create_record`, `update_record`, `query_table` on those tables).',
107
107
  '- **Gateway** (`websocket_definition`): `path` = namespace (e.g. `/chat`), `requireAuth` (JWT in `auth.token`), `connectionHandlerScript` (runs on connect), `connectionHandlerTimeout`, `isEnabled`.',
108
108
  '- **Event** (`websocket_event_definition`): `gateway` → gateway id, `eventName` (client emits), `handlerScript`, `timeout`, `isEnabled`.',
109
- '- **@SOCKET** in scripts: Connection handler `@SOCKET.emit(event, data)` this client; `@SOCKET.to(room).emit(event, data)` → room. Event handler — `@SOCKET.emit` → broadcast namespace; `@SOCKET.send` → this client; `@SOCKET.to(room).emit` → room.',
109
+ '- **@SOCKET in scripts (prefer template `@SOCKET.method()` over `$ctx.$socket.method()`):**',
110
+ '- `@SOCKET.reply(event, data)` — send to this client only (WS context only).',
111
+ '- `@SOCKET.join(room)` — join a room (WS context only).',
112
+ '- `@SOCKET.leave(room)` — leave a room (WS context only).',
113
+ '- `@SOCKET.emitToUser(userId, event, data)` — send to a specific user (across all gateways).',
114
+ '- `@SOCKET.emitToRoom(room, event, data)` — send to a named room (across all gateways).',
115
+ '- `@SOCKET.emitToGateway(path, event, data)` — broadcast to all connections on a gateway/namespace.',
116
+ '- `@SOCKET.broadcast(event, data)` — broadcast to all connections on all gateways.',
117
+ '- `@SOCKET.disconnect()` — force-disconnect the current socket from the gateway (WS context only). Use in connection handler to reject, or in event handler to kick user.',
118
+ '- In **HTTP** handler/hook context: `reply`, `join`, `leave`, `disconnect` are not available (no socket). Use `emitToUser`, `emitToRoom`, `emitToGateway`, `broadcast`.',
110
119
  '- **Context**: Connection — `@BODY` = {id, ip, headers}, `@USER` if auth. Event — `@BODY` = payload, `@USER` if auth. Both have `@SOCKET`.',
111
120
  '- **ACK + results (recommended UX):** client can emit an event with Socket.IO ack callback. Server immediately acks `{ queued: true, requestId, eventName }` (or `{ queued: false, error }`). The handler result is returned asynchronously via `ws:result` or `ws:error` with the same `requestId`.',
112
121
  '- **Client**: `io("<HTTP_ORIGIN>/namespace", {auth: {token: JWT}})`. Use the **origin where Socket.IO is served** (usually the **Nest** HTTP origin, e.g. `http://localhost:1105/chat` in local server-only setups). If Socket.IO is exposed only through the Nuxt app, use that host and your deployment’s WS path—**do not** assume port 3000 without checking `API_URL` / proxy config. Gateway `path` in metadata = Socket.IO **namespace**.',
@@ -13,6 +13,26 @@ async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
13
13
  return result?.data?.[0] || result?.[0] || null;
14
14
  }
15
15
 
16
+ /**
17
+ * PATCH table_definition with auto-confirm for schema changes.
18
+ * First PATCH returns preview + requiredConfirmHash; this helper
19
+ * automatically resends with ?schemaConfirmHash= to apply.
20
+ */
21
+ async function patchTableAutoConfirm(ENFYRA_API_URL, tableId, body) {
22
+ const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
23
+ method: 'PATCH',
24
+ body: JSON.stringify(body),
25
+ });
26
+ const preview = Array.isArray(result?.data) ? result.data[0] : result?.data;
27
+ if (preview?._preview && preview?.requiredConfirmHash) {
28
+ return fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}?schemaConfirmHash=${preview.requiredConfirmHash}`, {
29
+ method: 'PATCH',
30
+ body: JSON.stringify(body),
31
+ });
32
+ }
33
+ return result;
34
+ }
35
+
16
36
  /**
17
37
  * Register table tools with MCP server
18
38
  */
@@ -40,7 +60,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
40
60
  [
41
61
  'Create a new table definition with an auto-included `id` primary key column.',
42
62
  '**Not** for adding a custom API path or handler only — for that use **`create_route`** with an existing `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
43
- 'Use create_column to add more columns after creation (columns are managed via cascade PATCH on table_definition, NOT via /column_definition).',
63
+ 'PREFERRED: pass `columns` param as JSON array to create table WITH columns in one call (cascade). Only use create_column separately if adding to an existing table later.',
44
64
  'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
45
65
  'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
46
66
  'REST surface for that route (matches server route engine): 4 HTTP operations — GET `/<table>` (list/filter), POST `/<table>` (create), PATCH `/<table>/:id` (update), DELETE `/<table>/:id` (delete).',
@@ -53,12 +73,14 @@ export function registerTableTools(server, ENFYRA_API_URL) {
53
73
  alias: z.string().optional().describe('Table alias for API. If not provided, the table name will be used.'),
54
74
  description: z.string().optional().describe('Description of what this table stores.'),
55
75
  isEnabled: z.boolean().optional().default(true).describe('Enable table. Set to false to disable.'),
76
+ columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, defaultValue?, description?, options? }. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"status","type":"enum","options":["draft","published"]}]'),
56
77
  },
57
- async ({ name, alias, description, isEnabled }) => {
78
+ async ({ name, alias, description, isEnabled, columns: columnsJson }) => {
58
79
  const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
80
+ const userColumns = columnsJson ? JSON.parse(columnsJson) : [];
59
81
  const result = await fetchAPI(ENFYRA_API_URL, '/table_definition', {
60
82
  method: 'POST',
61
- body: JSON.stringify({ name, alias, description, isEnabled, columns: [idColumn] }),
83
+ body: JSON.stringify({ name, alias, description, isEnabled, columns: [idColumn, ...userColumns] }),
62
84
  });
63
85
  const base = ENFYRA_API_URL.replace(/\/$/, '');
64
86
  const routePath = `/${name}`;
@@ -66,8 +88,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
66
88
  `Auto route path: ${routePath} → full base for REST: ${base}${routePath}`,
67
89
  `REST: GET+POST on ${routePath}; PATCH+DELETE on ${routePath}/:id only. No GET ${routePath}/:id.`,
68
90
  ].join('\n');
91
+ const colHint = userColumns.length
92
+ ? `Table created with ${userColumns.length} column(s) + auto id.`
93
+ : `Table created. Use create_column to add columns (tableId: ${result.id}).`;
69
94
  return {
70
- content: [{ type: 'text', text: `Table created successfully with ID: ${result.id}. Next step: use create_column to add columns (tableId: ${result.id}).\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
95
+ content: [{ type: 'text', text: `${colHint}\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
71
96
  };
72
97
  }
73
98
  );
@@ -153,17 +178,14 @@ export function registerTableTools(server, ENFYRA_API_URL) {
153
178
  return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
154
179
  }
155
180
 
156
- const existingColumns = (tableData.columns || []).map(col => ({ id: col.id }));
181
+ const existingColumns = (tableData.columns || []).map(({ table, ...col }) => col);
157
182
  const newCol = { name, type, isNullable: isNullable ?? true };
158
183
  if (isUnique) newCol.isUnique = true;
159
184
  if (defaultValue !== undefined) newCol.defaultValue = defaultValue;
160
185
  if (description) newCol.description = description;
161
186
  if (options) newCol.options = JSON.parse(options);
162
187
 
163
- const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
164
- method: 'PATCH',
165
- body: JSON.stringify({ columns: [...existingColumns, newCol] }),
166
- });
188
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns: [...existingColumns, newCol] });
167
189
 
168
190
  return {
169
191
  content: [{ type: 'text', text: `Column "${name}" added to table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
@@ -198,24 +220,20 @@ export function registerTableTools(server, ENFYRA_API_URL) {
198
220
  }
199
221
 
200
222
  const columns = (tableData.columns || []).map(col => {
223
+ const { table, ...rest } = col;
201
224
  if (String(col.id) === String(columnId)) {
202
- const updated = { id: col.id };
203
- if (name !== undefined) updated.name = name;
204
- if (type !== undefined) updated.type = type;
205
- if (isNullable !== undefined) updated.isNullable = isNullable;
206
- if (isHidden !== undefined) updated.isHidden = isHidden;
207
- if (defaultValue !== undefined) updated.defaultValue = defaultValue;
208
- if (description !== undefined) updated.description = description;
209
- if (options !== undefined) updated.options = JSON.parse(options);
210
- return updated;
225
+ if (name !== undefined) rest.name = name;
226
+ if (type !== undefined) rest.type = type;
227
+ if (isNullable !== undefined) rest.isNullable = isNullable;
228
+ if (isHidden !== undefined) rest.isHidden = isHidden;
229
+ if (defaultValue !== undefined) rest.defaultValue = defaultValue;
230
+ if (description !== undefined) rest.description = description;
231
+ if (options !== undefined) rest.options = JSON.parse(options);
211
232
  }
212
- return { id: col.id };
233
+ return rest;
213
234
  });
214
235
 
215
- const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
216
- method: 'PATCH',
217
- body: JSON.stringify({ columns }),
218
- });
236
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
219
237
 
220
238
  return {
221
239
  content: [{ type: 'text', text: `Column ${columnId} updated on table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
@@ -245,12 +263,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
245
263
 
246
264
  const columns = (tableData.columns || [])
247
265
  .filter(col => String(col.id) !== String(columnId))
248
- .map(col => ({ id: col.id }));
266
+ .map(({ table, ...col }) => col);
249
267
 
250
- const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
251
- method: 'PATCH',
252
- body: JSON.stringify({ columns }),
253
- });
268
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
254
269
 
255
270
  return {
256
271
  content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
@@ -277,11 +292,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
277
292
  onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT', 'NO ACTION']).optional().default('SET NULL').describe('On delete behavior.'),
278
293
  },
279
294
  async ({ sourceTableId, targetTableId, type, propertyName, inversePropertyName, isNullable, onDelete }) => {
280
- const relation = { type, propertyName, inversePropertyName: inversePropertyName || null, isNullable, onDelete };
281
- const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${sourceTableId}`, {
282
- method: 'PATCH',
283
- body: JSON.stringify({ relations: [{ targetTableId, ...relation }] }),
284
- });
295
+ const tableData = await fetchTableWithDetails(ENFYRA_API_URL, sourceTableId);
296
+ if (!tableData) {
297
+ return { content: [{ type: 'text', text: `Error: Table with ID ${sourceTableId} not found.` }] };
298
+ }
299
+ const existingRelations = (tableData.relations || []).map(({ sourceTable, targetTable, ...rel }) => rel);
300
+ const newRelation = { targetTableId, type, propertyName, inversePropertyName: inversePropertyName || null, isNullable, onDelete };
301
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, sourceTableId, { relations: [...existingRelations, newRelation] });
285
302
  return {
286
303
  content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${sourceTableId} → ${targetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
287
304
  };
@@ -309,12 +326,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
309
326
 
310
327
  const relations = (tableData.relations || [])
311
328
  .filter(rel => String(rel.id) !== String(relationId))
312
- .map(rel => ({ id: rel.id }));
329
+ .map(({ sourceTable, targetTable, ...rel }) => rel);
313
330
 
314
- const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
315
- method: 'PATCH',
316
- body: JSON.stringify({ relations }),
317
- });
331
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { relations });
318
332
 
319
333
  return {
320
334
  content: [{ type: 'text', text: `Relation ${relationId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],