@enfyra/mcp-server 0.0.48 → 0.0.50
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 +5 -2
- package/src/lib/mcp-instructions.js +3 -2
- package/src/lib/table-tools.js +184 -80
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enfyra/mcp-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "src/index.mjs",
|
|
8
|
-
"bin":
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-server": "src/index.mjs",
|
|
10
|
+
"enfyra-mcp-server": "src/index.mjs"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"src",
|
|
11
14
|
".codex/skills"
|
|
@@ -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
|
-
'-
|
|
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.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
|
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
|
-
|
|
334
|
-
columnId: z.string().describe('Column definition ID to delete.'),
|
|
454
|
+
...columnDeleteSchema,
|
|
335
455
|
},
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
398
|
-
relationId: z.string().describe('Relation definition ID to delete.'),
|
|
507
|
+
...relationDeleteSchema,
|
|
399
508
|
},
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
}
|