@enfyra/mcp-server 0.0.48 → 0.0.49

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.48",
3
+ "version": "0.0.49",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -193,8 +193,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
193
193
  '- **`column_definition` and `session_definition` have NO route** — do NOT call `query_table("column_definition", …)` or `query_table("session_definition", …)`. They will 404.',
194
194
  '- To check which tables are accessible via MCP tools, call `get_all_routes` and look for the route whose `mainTable.id` matches the table you need, or `get_all_metadata` to see all table names.',
195
195
  '- **Tables confirmed to have REST routes (system):** `bootstrap_script_definition`, `column_rule_definition`, `cors_origin_definition`, `extension_definition`, `field_permission_definition`, `file_definition`, `file_permission_definition`, `flow_definition`, `flow_execution_definition`, `flow_step_definition`, `folder_definition`, `gql_definition`, `guard_definition`, `guard_rule_definition`, `menu_definition`, `method_definition`, `oauth_account_definition`, `oauth_config_definition`, `package_definition`, `post_hook_definition`, `pre_hook_definition`, `relation_definition`, `role_definition`, `route_definition`, `route_handler_definition`, `route_permission_definition`, `schema_migration_definition`, `setting_definition`, `storage_config_definition`, `table_definition`, `user_definition`, `websocket_definition`, `websocket_event_definition`.',
196
- '- **Tables without REST routes (internal/system only):** `column_definition`, `session_definition`. Columns are managed indirectly via cascade on `table_definition` (POST/PATCH with columns arrays). The `create_table`, `create_column`, `update_column`, and `delete_column` MCP tools handle this automatically.',
197
- '- Prefer `create_relation` / `delete_relation` for relation schema changes because they preserve the full table relation list and handle schema-confirm retry. Direct `create_record` on `relation_definition` only edits metadata and is not the canonical schema migration path.',
196
+ '- **Tables without REST routes (internal/system only):** `column_definition`, `session_definition`. Columns are managed indirectly via cascade on `table_definition` (POST/PATCH with columns arrays). The `create_table`, `create_column`/`add_column`, `update_column`, and `delete_column`/`remove_column` MCP tools handle this automatically.',
197
+ '- Use `create_column`/`add_column` for new scalar fields. These tools accept column metadata such as `isNullable`, `isUnique`, `isPublished`, `isPrimary`, `isGenerated`, `isSystem`, `defaultValue`, `description`, and `options`; set `isPublished=false` directly when creating secret/internal fields such as `*_encrypted`.',
198
+ '- Prefer `create_relation`/`add_relation` and `delete_relation`/`remove_relation` for relation schema changes because they preserve the full table relation list and handle schema-confirm retry. Direct `create_record` on `relation_definition` only edits metadata and is not the canonical schema migration path.',
198
199
  '',
199
200
  '### Body validation & column rules',
200
201
  '- Each `table_definition` has a **`validateBody`** flag (default `true` for new tables). When on, every `POST /<table>` and `PATCH /<table>/<id>` is validated server-side against the column types + any **column rules** attached to columns of that table.',
@@ -84,12 +84,147 @@ function normalizeRelationForTablePatch(relation) {
84
84
  return normalized;
85
85
  }
86
86
 
87
+ function normalizeColumnForTablePatch(column) {
88
+ const { table, ...rest } = column;
89
+ return rest;
90
+ }
91
+
92
+ function buildColumnDefinition({
93
+ name,
94
+ type,
95
+ isNullable,
96
+ isUnique,
97
+ isPublished,
98
+ isPrimary,
99
+ isGenerated,
100
+ isSystem,
101
+ defaultValue,
102
+ description,
103
+ options,
104
+ }) {
105
+ const column = { name, type };
106
+ if (isNullable !== undefined) column.isNullable = isNullable;
107
+ if (isUnique !== undefined) column.isUnique = isUnique;
108
+ if (isPublished !== undefined) column.isPublished = isPublished;
109
+ if (isPrimary !== undefined) column.isPrimary = isPrimary;
110
+ if (isGenerated !== undefined) column.isGenerated = isGenerated;
111
+ if (isSystem !== undefined) column.isSystem = isSystem;
112
+ if (defaultValue !== undefined) column.defaultValue = defaultValue;
113
+ if (description !== undefined) column.description = description;
114
+ if (options !== undefined) column.options = JSON.parse(options);
115
+ return column;
116
+ }
117
+
87
118
  /**
88
119
  * Register table tools with MCP server
89
120
  */
90
121
  export function registerTableTools(server, ENFYRA_API_URL) {
91
122
  const apiBase = ENFYRA_API_URL.replace(/\/$/, '');
92
123
 
124
+ async function appendColumnToTable(args) {
125
+ const tableData = await fetchTableWithDetails(ENFYRA_API_URL, args.tableId);
126
+ if (!tableData) {
127
+ return { content: [{ type: 'text', text: `Error: Table with ID ${args.tableId} not found.` }] };
128
+ }
129
+
130
+ const existingColumns = (tableData.columns || []).map(normalizeColumnForTablePatch);
131
+ const newCol = buildColumnDefinition(args);
132
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, args.tableId, { columns: [...existingColumns, newCol] });
133
+
134
+ return {
135
+ content: [{ type: 'text', text: `Column "${args.name}" added to table ${args.tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
136
+ };
137
+ }
138
+
139
+ async function appendRelationToTable({ sourceTableId, targetTableId, type, propertyName, inversePropertyName, mappedBy, isNullable, onDelete, description }) {
140
+ const tableData = await fetchTableWithDetails(ENFYRA_API_URL, sourceTableId);
141
+ if (!tableData) {
142
+ return { content: [{ type: 'text', text: `Error: Table with ID ${sourceTableId} not found.` }] };
143
+ }
144
+ const existingRelations = (tableData.relations || []).map(normalizeRelationForTablePatch);
145
+ const newRelation = { targetTable: targetTableId, type, propertyName };
146
+ if (inversePropertyName !== undefined) newRelation.inversePropertyName = inversePropertyName || null;
147
+ if (mappedBy !== undefined) newRelation.mappedBy = mappedBy;
148
+ if (isNullable !== undefined) newRelation.isNullable = isNullable;
149
+ if (onDelete !== undefined) newRelation.onDelete = onDelete;
150
+ if (description !== undefined) newRelation.description = description;
151
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, sourceTableId, { relations: [...existingRelations, newRelation] });
152
+ return {
153
+ content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${sourceTableId} → ${targetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
154
+ };
155
+ }
156
+
157
+ async function removeColumnFromTable({ tableId, columnId }) {
158
+ const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
159
+ if (!tableData) {
160
+ return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
161
+ }
162
+
163
+ const columns = (tableData.columns || [])
164
+ .filter(col => String(col.id) !== String(columnId))
165
+ .map(normalizeColumnForTablePatch);
166
+
167
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
168
+
169
+ return {
170
+ content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
171
+ };
172
+ }
173
+
174
+ async function removeRelationFromTable({ tableId, relationId }) {
175
+ const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
176
+ if (!tableData) {
177
+ return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
178
+ }
179
+
180
+ const relations = (tableData.relations || [])
181
+ .filter(rel => String(rel.id) !== String(relationId))
182
+ .map(normalizeRelationForTablePatch);
183
+
184
+ const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { relations });
185
+
186
+ return {
187
+ content: [{ type: 'text', text: `Relation ${relationId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
188
+ };
189
+ }
190
+
191
+ const columnCreateSchema = {
192
+ tableId: z.string().describe('Table definition ID (from get_all_tables or create_table).'),
193
+ name: z.string().describe('Column name (e.g., "title", "payment_confirm_secret_encrypted"). Lowercase with underscores.'),
194
+ type: z.string().describe('Column type: varchar, int, text, boolean, datetime, json, decimal, timestamp, uuid, bigint, float, longtext, richtext, simple-json, code, enum, array-select, date.'),
195
+ isNullable: z.boolean().optional().default(true).describe('Set to false if column cannot be null.'),
196
+ isUnique: z.boolean().optional().default(false).describe('Set to true for unique constraint.'),
197
+ isPublished: z.boolean().optional().describe('Set column visibility baseline. Use false for secrets and internal fields.'),
198
+ isPrimary: z.boolean().optional().describe('Set true only for primary key columns; normally only create_table auto id uses this.'),
199
+ isGenerated: z.boolean().optional().describe('Set true only for generated columns such as auto id.'),
200
+ isSystem: z.boolean().optional().describe('Set true only for system-managed columns. Avoid for normal app fields.'),
201
+ defaultValue: z.string().optional().describe('Default value as JSON string or backend-supported literal.'),
202
+ description: z.string().optional().describe('Column description.'),
203
+ options: z.string().optional().describe('Column options as JSON string (e.g., enum values).'),
204
+ };
205
+
206
+ const relationCreateSchema = {
207
+ sourceTableId: z.string().describe('Source table ID (the table that owns the FK for many-to-one).'),
208
+ targetTableId: z.string().describe('Target table ID.'),
209
+ type: z.enum(['many-to-one', 'one-to-many', 'one-to-one', 'many-to-many']).describe('Relation type.'),
210
+ propertyName: z.string().describe('Property name on source table (e.g., "customer", "items").'),
211
+ inversePropertyName: z.string().optional().describe('Property name on target table for bidirectional relation (e.g., "orders"). Omit unless the reverse field is truly needed.'),
212
+ mappedBy: z.string().optional().describe('Mapped-by property for inverse relation shapes when required by the backend. Do not use physical FK names.'),
213
+ isNullable: z.boolean().optional().default(true).describe('Whether the relation is nullable.'),
214
+ onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT']).optional().default('SET NULL').describe('On delete behavior.'),
215
+ description: z.string().optional().describe('Relation description.'),
216
+ };
217
+
218
+ const columnDeleteSchema = {
219
+ tableId: z.string().describe('Table definition ID.'),
220
+ columnId: z.string().describe('Column definition ID to delete.'),
221
+ };
222
+
223
+ const relationDeleteSchema = {
224
+ tableId: z.string().describe('Table definition ID (source table of the relation).'),
225
+ relationId: z.string().describe('Relation definition ID to delete.'),
226
+ };
227
+
93
228
  // ─── READ ───
94
229
 
95
230
  server.tool(
@@ -241,34 +376,20 @@ export function registerTableTools(server, ENFYRA_API_URL) {
241
376
  'Run schema changes sequentially — migration locks DB per operation.',
242
377
  ].join(' '),
243
378
  {
244
- tableId: z.string().describe('Table definition ID (from get_all_tables or create_table).'),
245
- name: z.string().describe('Column name (e.g., "title", "user_id"). Lowercase with underscores.'),
246
- type: z.string().describe('Column type: varchar, int, text, boolean, datetime, json, decimal, timestamp, uuid, bigint, float, longtext, richtext, simple-json, code, enum, array-select, date.'),
247
- isNullable: z.boolean().optional().default(true).describe('Set to false if column cannot be null.'),
248
- isUnique: z.boolean().optional().default(false).describe('Set to true for unique constraint.'),
249
- defaultValue: z.string().optional().describe('Default value as JSON string.'),
250
- description: z.string().optional().describe('Column description.'),
251
- options: z.string().optional().describe('Column options as JSON string (e.g., enum values).'),
379
+ ...columnCreateSchema,
252
380
  },
253
- async ({ tableId, name, type, isNullable, isUnique, defaultValue, description, options }) => {
254
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
255
- if (!tableData) {
256
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
257
- }
258
-
259
- const existingColumns = (tableData.columns || []).map(({ table, ...col }) => col);
260
- const newCol = { name, type, isNullable: isNullable ?? true };
261
- if (isUnique) newCol.isUnique = true;
262
- if (defaultValue !== undefined) newCol.defaultValue = defaultValue;
263
- if (description) newCol.description = description;
264
- if (options) newCol.options = JSON.parse(options);
265
-
266
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns: [...existingColumns, newCol] });
381
+ appendColumnToTable
382
+ );
267
383
 
268
- return {
269
- content: [{ type: 'text', text: `Column "${name}" added to table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
270
- };
271
- }
384
+ server.tool(
385
+ 'add_column',
386
+ [
387
+ 'Alias for create_column. Add a column to an existing table through the canonical table_definition cascade.',
388
+ 'Use this for schema additions, including hidden secret fields with isPublished=false.',
389
+ 'Run schema changes sequentially — migration locks DB per operation.',
390
+ ].join(' '),
391
+ columnCreateSchema,
392
+ appendColumnToTable
272
393
  );
273
394
 
274
395
  // ─── UPDATE COLUMN ───
@@ -298,7 +419,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
298
419
  }
299
420
 
300
421
  const columns = (tableData.columns || []).map(col => {
301
- const { table, ...rest } = col;
422
+ const rest = normalizeColumnForTablePatch(col);
302
423
  if (String(col.id) === String(columnId)) {
303
424
  if (name !== undefined) rest.name = name;
304
425
  if (type !== undefined) rest.type = type;
@@ -330,25 +451,20 @@ export function registerTableTools(server, ENFYRA_API_URL) {
330
451
  'Run schema changes sequentially — migration locks DB per operation.',
331
452
  ].join(' '),
332
453
  {
333
- tableId: z.string().describe('Table definition ID.'),
334
- columnId: z.string().describe('Column definition ID to delete.'),
454
+ ...columnDeleteSchema,
335
455
  },
336
- async ({ tableId, columnId }) => {
337
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
338
- if (!tableData) {
339
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
340
- }
341
-
342
- const columns = (tableData.columns || [])
343
- .filter(col => String(col.id) !== String(columnId))
344
- .map(({ table, ...col }) => col);
345
-
346
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
456
+ removeColumnFromTable
457
+ );
347
458
 
348
- return {
349
- content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
350
- };
351
- }
459
+ server.tool(
460
+ 'remove_column',
461
+ [
462
+ 'Alias for delete_column. Remove a column through the canonical table_definition cascade.',
463
+ 'This drops the physical column. Confirm destructive schema changes before calling.',
464
+ 'Run schema changes sequentially — migration locks DB per operation.',
465
+ ].join(' '),
466
+ columnDeleteSchema,
467
+ removeColumnFromTable
352
468
  );
353
469
 
354
470
  // ─── CREATE RELATION ───
@@ -362,26 +478,20 @@ export function registerTableTools(server, ENFYRA_API_URL) {
362
478
  'Run sequentially — DB migration locks per operation.',
363
479
  ].join(' '),
364
480
  {
365
- sourceTableId: z.string().describe('Source table ID (the table that owns the FK for many-to-one).'),
366
- targetTableId: z.string().describe('Target table ID.'),
367
- type: z.enum(['many-to-one', 'one-to-many', 'one-to-one', 'many-to-many']).describe('Relation type.'),
368
- propertyName: z.string().describe('Property name on source table (e.g., "customer", "items").'),
369
- inversePropertyName: z.string().optional().describe('Property name on target table for bidirectional relation (e.g., "orders").'),
370
- isNullable: z.boolean().optional().default(true).describe('Whether the relation is nullable.'),
371
- onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT']).optional().default('SET NULL').describe('On delete behavior.'),
481
+ ...relationCreateSchema,
372
482
  },
373
- async ({ sourceTableId, targetTableId, type, propertyName, inversePropertyName, isNullable, onDelete }) => {
374
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, sourceTableId);
375
- if (!tableData) {
376
- return { content: [{ type: 'text', text: `Error: Table with ID ${sourceTableId} not found.` }] };
377
- }
378
- const existingRelations = (tableData.relations || []).map(normalizeRelationForTablePatch);
379
- const newRelation = { targetTable: targetTableId, type, propertyName, inversePropertyName: inversePropertyName || null, isNullable, onDelete };
380
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, sourceTableId, { relations: [...existingRelations, newRelation] });
381
- return {
382
- content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${sourceTableId} → ${targetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
383
- };
384
- }
483
+ appendRelationToTable
484
+ );
485
+
486
+ server.tool(
487
+ 'add_relation',
488
+ [
489
+ 'Alias for create_relation. Add a relation through the canonical table_definition cascade.',
490
+ 'Use relation propertyName only; never provide physical FK or junction column names.',
491
+ 'Run schema changes sequentially — migration locks DB per operation.',
492
+ ].join(' '),
493
+ relationCreateSchema,
494
+ appendRelationToTable
385
495
  );
386
496
 
387
497
  // ─── DELETE RELATION ───
@@ -394,24 +504,18 @@ export function registerTableTools(server, ENFYRA_API_URL) {
394
504
  'Drops FK columns and junction tables (for many-to-many).',
395
505
  ].join(' '),
396
506
  {
397
- tableId: z.string().describe('Table definition ID (source table of the relation).'),
398
- relationId: z.string().describe('Relation definition ID to delete.'),
507
+ ...relationDeleteSchema,
399
508
  },
400
- async ({ tableId, relationId }) => {
401
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
402
- if (!tableData) {
403
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
404
- }
405
-
406
- const relations = (tableData.relations || [])
407
- .filter(rel => String(rel.id) !== String(relationId))
408
- .map(normalizeRelationForTablePatch);
409
-
410
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { relations });
509
+ removeRelationFromTable
510
+ );
411
511
 
412
- return {
413
- content: [{ type: 'text', text: `Relation ${relationId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
414
- };
415
- }
512
+ server.tool(
513
+ 'remove_relation',
514
+ [
515
+ 'Alias for delete_relation. Remove a relation through the canonical table_definition cascade.',
516
+ 'This can drop FK columns or junction tables. Confirm destructive schema changes before calling.',
517
+ ].join(' '),
518
+ relationDeleteSchema,
519
+ removeRelationFromTable
416
520
  );
417
521
  }