@enfyra/mcp-server 0.1.5 → 0.1.7
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/README.md +2 -2
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +29 -20
- package/src/lib/mcp-instructions.js +1 -1
- package/src/lib/platform-operation-tools.js +85 -21
- package/src/lib/required-knowledge.js +63 -2
- package/src/lib/table-tools.js +86 -35
- package/src/mcp-server-entry.mjs +97 -48
package/src/lib/table-tools.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { fetchAPI } from './fetch.js';
|
|
6
6
|
import { jsonContent } from './response-format.js';
|
|
7
|
+
import { assertGlobalRulesAck, globalRulesAckParam } from './required-knowledge.js';
|
|
7
8
|
|
|
8
9
|
let schemaQueue = Promise.resolve();
|
|
9
10
|
|
|
@@ -308,10 +309,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
308
309
|
const apiBase = ENFYRA_API_URL.replace(/\/$/, '');
|
|
309
310
|
|
|
310
311
|
async function appendColumnToTable(args) {
|
|
312
|
+
assertGlobalRulesAck(args.globalRulesAckKey);
|
|
311
313
|
return withSchemaQueue(async () => {
|
|
312
314
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, args.tableId);
|
|
313
315
|
if (!tableData) {
|
|
314
|
-
|
|
316
|
+
throw new Error(`Table with ID ${args.tableId} not found.`);
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
const existingColumns = getPatchableColumns(tableData.columns);
|
|
@@ -323,13 +325,17 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
323
325
|
columnName: args.name,
|
|
324
326
|
});
|
|
325
327
|
|
|
326
|
-
return {
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
return jsonContent({
|
|
329
|
+
action: 'column_created',
|
|
330
|
+
tableId: args.tableId,
|
|
331
|
+
columnName: args.name,
|
|
332
|
+
result,
|
|
333
|
+
});
|
|
329
334
|
});
|
|
330
335
|
}
|
|
331
336
|
|
|
332
337
|
async function appendRelationToTable(args) {
|
|
338
|
+
assertGlobalRulesAck(args.globalRulesAckKey);
|
|
333
339
|
return withSchemaQueue(async () => {
|
|
334
340
|
assertNoForbiddenRelationKeys(args);
|
|
335
341
|
const { sourceTableId, targetTableId, type, propertyName, inversePropertyName, mappedBy, isNullable, onDelete, description } = args;
|
|
@@ -338,7 +344,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
338
344
|
const resolvedTargetTableId = resolveTableIdentifierFromMetadata(metadata, targetTableId, 'targetTableId');
|
|
339
345
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, resolvedSourceTableId);
|
|
340
346
|
if (!tableData) {
|
|
341
|
-
|
|
347
|
+
throw new Error(`Table ${sourceTableId} not found.`);
|
|
342
348
|
}
|
|
343
349
|
const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
|
|
344
350
|
const beforeIds = existingRelations.map((relation) => String(getId(relation))).filter((id) => id !== 'null');
|
|
@@ -353,17 +359,19 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
353
359
|
action: 'create',
|
|
354
360
|
propertyName,
|
|
355
361
|
});
|
|
356
|
-
return {
|
|
357
|
-
|
|
358
|
-
|
|
362
|
+
return jsonContent({
|
|
363
|
+
action: 'relation_created',
|
|
364
|
+
relation: { propertyName, type, sourceTableId: resolvedSourceTableId, targetTableId: resolvedTargetTableId },
|
|
365
|
+
result,
|
|
366
|
+
});
|
|
359
367
|
});
|
|
360
368
|
}
|
|
361
369
|
|
|
362
|
-
async function removeColumnFromTable({ tableId, columnId, confirm }) {
|
|
370
|
+
async function removeColumnFromTable({ tableId, columnId, confirm, globalRulesAckKey }) {
|
|
363
371
|
return withSchemaQueue(async () => {
|
|
364
372
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
365
373
|
if (!tableData) {
|
|
366
|
-
|
|
374
|
+
throw new Error(`Table with ID ${tableId} not found.`);
|
|
367
375
|
}
|
|
368
376
|
|
|
369
377
|
const existingColumns = getPatchableColumns(tableData.columns);
|
|
@@ -385,6 +393,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
385
393
|
}, null, 2) }],
|
|
386
394
|
};
|
|
387
395
|
}
|
|
396
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
388
397
|
|
|
389
398
|
const columns = existingColumns
|
|
390
399
|
.filter(col => String(getId(col)) !== String(columnId))
|
|
@@ -395,17 +404,20 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
395
404
|
columnId,
|
|
396
405
|
});
|
|
397
406
|
|
|
398
|
-
return {
|
|
399
|
-
|
|
400
|
-
|
|
407
|
+
return jsonContent({
|
|
408
|
+
action: 'column_deleted',
|
|
409
|
+
tableId,
|
|
410
|
+
columnId,
|
|
411
|
+
result,
|
|
412
|
+
});
|
|
401
413
|
});
|
|
402
414
|
}
|
|
403
415
|
|
|
404
|
-
async function removeRelationFromTable({ tableId, relationId, confirm }) {
|
|
416
|
+
async function removeRelationFromTable({ tableId, relationId, confirm, globalRulesAckKey }) {
|
|
405
417
|
return withSchemaQueue(async () => {
|
|
406
418
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
407
419
|
if (!tableData) {
|
|
408
|
-
|
|
420
|
+
throw new Error(`Table with ID ${tableId} not found.`);
|
|
409
421
|
}
|
|
410
422
|
|
|
411
423
|
const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
|
|
@@ -427,6 +439,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
427
439
|
}, null, 2) }],
|
|
428
440
|
};
|
|
429
441
|
}
|
|
442
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
430
443
|
|
|
431
444
|
const relations = existingRelations
|
|
432
445
|
.filter(rel => String(getId(rel)) !== String(relationId))
|
|
@@ -437,9 +450,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
437
450
|
relationId,
|
|
438
451
|
});
|
|
439
452
|
|
|
440
|
-
return {
|
|
441
|
-
|
|
442
|
-
|
|
453
|
+
return jsonContent({
|
|
454
|
+
action: 'relation_deleted',
|
|
455
|
+
tableId,
|
|
456
|
+
relationId,
|
|
457
|
+
result,
|
|
458
|
+
});
|
|
443
459
|
});
|
|
444
460
|
}
|
|
445
461
|
|
|
@@ -458,6 +474,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
458
474
|
defaultValue: z.string().optional().describe('Default value as JSON string or backend-supported literal.'),
|
|
459
475
|
description: z.string().optional().describe('Column description.'),
|
|
460
476
|
options: z.string().optional().describe('Column options as JSON string (e.g., enum values).'),
|
|
477
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
461
478
|
};
|
|
462
479
|
|
|
463
480
|
const relationCreateSchema = {
|
|
@@ -477,18 +494,21 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
477
494
|
targetColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
|
|
478
495
|
junctionSourceColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
|
|
479
496
|
junctionTargetColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
|
|
497
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
480
498
|
};
|
|
481
499
|
|
|
482
500
|
const columnDeleteSchema = {
|
|
483
501
|
tableId: z.string().describe('Table definition ID.'),
|
|
484
502
|
columnId: z.string().describe('Column definition ID to delete.'),
|
|
485
503
|
confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
|
|
504
|
+
globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
|
|
486
505
|
};
|
|
487
506
|
|
|
488
507
|
const relationDeleteSchema = {
|
|
489
508
|
tableId: z.string().describe('Table definition ID (source table of the relation).'),
|
|
490
509
|
relationId: z.string().describe('Relation definition ID to delete.'),
|
|
491
510
|
confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
|
|
511
|
+
globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
|
|
492
512
|
};
|
|
493
513
|
|
|
494
514
|
// ─── READ ───
|
|
@@ -565,8 +585,10 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
565
585
|
relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id, {"id": <id>}, or an exact table name that MCP resolves to an id before mutation. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Omit inversePropertyName unless a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal needs the reverse field. Example only when parent posts are queried: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
566
586
|
indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["member","isRead","conversation"],["conversation","member","isRead"]]'),
|
|
567
587
|
uniques: z.string().optional().describe('JSON array of logical unique field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Example: [["message","member"]]'),
|
|
588
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
568
589
|
},
|
|
569
|
-
async ({ name, description, isSingleRecord, columns: columnsJson, relations: relationsJson, indexes: indexesJson, uniques: uniquesJson }) => withSchemaQueue(async () => {
|
|
590
|
+
async ({ name, description, isSingleRecord, columns: columnsJson, relations: relationsJson, indexes: indexesJson, uniques: uniquesJson, globalRulesAckKey }) => withSchemaQueue(async () => {
|
|
591
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
570
592
|
const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
|
|
571
593
|
const userColumns = parseJsonArrayParam('columns', columnsJson);
|
|
572
594
|
const parsedRelations = parseJsonArrayParam('relations', relationsJson).map(normalizeRelationForTablePatch);
|
|
@@ -602,9 +624,25 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
602
624
|
indexes.length ? `Index group(s): ${indexes.length}.` : null,
|
|
603
625
|
uniques.length ? `Unique group(s): ${uniques.length}.` : null,
|
|
604
626
|
].filter(Boolean).join(' ');
|
|
605
|
-
return {
|
|
606
|
-
|
|
607
|
-
|
|
627
|
+
return jsonContent({
|
|
628
|
+
action: 'table_created',
|
|
629
|
+
table: { id: createdTableId, name, routePath },
|
|
630
|
+
summary: {
|
|
631
|
+
columnCount: userColumns.length + 1,
|
|
632
|
+
createdColumnCount: userColumns.length,
|
|
633
|
+
relationCount: userRelations.length,
|
|
634
|
+
indexGroupCount: indexes.length,
|
|
635
|
+
uniqueGroupCount: uniques.length,
|
|
636
|
+
},
|
|
637
|
+
rest: {
|
|
638
|
+
base,
|
|
639
|
+
routePath,
|
|
640
|
+
operations: ['GET /<table>', 'POST /<table>', 'PATCH /<table>/:id', 'DELETE /<table>/:id'],
|
|
641
|
+
noGetById: true,
|
|
642
|
+
},
|
|
643
|
+
message: [colHint, relHint, constraintHint, restHint].filter(Boolean).join('\n'),
|
|
644
|
+
result,
|
|
645
|
+
});
|
|
608
646
|
})
|
|
609
647
|
);
|
|
610
648
|
|
|
@@ -627,8 +665,10 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
627
665
|
graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing enfyra_graphql.isEnabled. GraphQL table data still requires Bearer auth; anonymous root or schema probes may return 200.'),
|
|
628
666
|
indexes: z.string().optional().describe('Complete JSON array of logical index field groups to store on enfyra_table.indexes. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current indexes; pass [] to clear.'),
|
|
629
667
|
uniques: z.string().optional().describe('Complete JSON array of logical unique field groups to store on enfyra_table.uniques. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current uniques; pass [] to clear.'),
|
|
668
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
630
669
|
},
|
|
631
|
-
async ({ tableId, name, alias, description, isSingleRecord, graphqlEnabled, indexes: indexesJson, uniques: uniquesJson }) => withSchemaQueue(async () => {
|
|
670
|
+
async ({ tableId, name, alias, description, isSingleRecord, graphqlEnabled, indexes: indexesJson, uniques: uniquesJson, globalRulesAckKey }) => withSchemaQueue(async () => {
|
|
671
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
632
672
|
const body = {};
|
|
633
673
|
if (name !== undefined) body.name = name;
|
|
634
674
|
if (alias !== undefined) body.alias = alias;
|
|
@@ -639,9 +679,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
639
679
|
if (uniquesJson !== undefined) body.uniques = normalizeConstraintGroups('uniques', parseJsonArrayParam('uniques', uniquesJson));
|
|
640
680
|
|
|
641
681
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, body);
|
|
642
|
-
return {
|
|
643
|
-
|
|
644
|
-
|
|
682
|
+
return jsonContent({
|
|
683
|
+
action: 'table_updated',
|
|
684
|
+
tableId,
|
|
685
|
+
result,
|
|
686
|
+
});
|
|
645
687
|
})
|
|
646
688
|
);
|
|
647
689
|
|
|
@@ -657,8 +699,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
657
699
|
{
|
|
658
700
|
tableId: z.string().describe('Table definition ID to delete.'),
|
|
659
701
|
confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
|
|
702
|
+
globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
|
|
660
703
|
},
|
|
661
|
-
async ({ tableId, confirm }) => withSchemaQueue(async () => {
|
|
704
|
+
async ({ tableId, confirm, globalRulesAckKey }) => withSchemaQueue(async () => {
|
|
662
705
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
663
706
|
if (!confirm) {
|
|
664
707
|
return {
|
|
@@ -673,12 +716,15 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
673
716
|
}, null, 2) }],
|
|
674
717
|
};
|
|
675
718
|
}
|
|
719
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
676
720
|
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}`, {
|
|
677
721
|
method: 'DELETE',
|
|
678
722
|
});
|
|
679
|
-
return {
|
|
680
|
-
|
|
681
|
-
|
|
723
|
+
return jsonContent({
|
|
724
|
+
action: 'table_deleted',
|
|
725
|
+
tableId,
|
|
726
|
+
result,
|
|
727
|
+
});
|
|
682
728
|
})
|
|
683
729
|
);
|
|
684
730
|
|
|
@@ -720,11 +766,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
720
766
|
defaultValue: z.string().optional().describe('New default value as JSON string.'),
|
|
721
767
|
description: z.string().optional().describe('New description.'),
|
|
722
768
|
options: z.string().optional().describe('New options as JSON string.'),
|
|
769
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
723
770
|
},
|
|
724
|
-
async ({ tableId, columnId, name, type, isNullable, isPublished, isUpdatable, defaultValue, description, options }) => withSchemaQueue(async () => {
|
|
771
|
+
async ({ tableId, columnId, name, type, isNullable, isPublished, isUpdatable, defaultValue, description, options, globalRulesAckKey }) => withSchemaQueue(async () => {
|
|
772
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
725
773
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
726
774
|
if (!tableData) {
|
|
727
|
-
|
|
775
|
+
throw new Error(`Table with ID ${tableId} not found.`);
|
|
728
776
|
}
|
|
729
777
|
|
|
730
778
|
const existingColumns = getPatchableColumns(tableData.columns);
|
|
@@ -754,9 +802,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
754
802
|
columnId,
|
|
755
803
|
});
|
|
756
804
|
|
|
757
|
-
return {
|
|
758
|
-
|
|
759
|
-
|
|
805
|
+
return jsonContent({
|
|
806
|
+
action: 'column_updated',
|
|
807
|
+
tableId,
|
|
808
|
+
columnId,
|
|
809
|
+
result,
|
|
810
|
+
});
|
|
760
811
|
})
|
|
761
812
|
);
|
|
762
813
|
|