@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.
@@ -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
- return { content: [{ type: 'text', text: `Error: Table with ID ${args.tableId} not found.` }] };
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
- content: [{ type: 'text', text: `Column "${args.name}" added to table ${args.tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
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
- return { content: [{ type: 'text', text: `Error: Table ${sourceTableId} not found.` }] };
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
- content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${resolvedSourceTableId} → ${resolvedTargetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
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
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
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
- content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
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
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
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
- content: [{ type: 'text', text: `Relation ${relationId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
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
- content: [{ type: 'text', text: `${colHint}\n${relHint}${constraintHint ? `\n${constraintHint}` : ''}\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
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
- content: [{ type: 'text', text: `Table ${tableId} updated.\n\n${JSON.stringify(result, null, 2)}` }],
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
- content: [{ type: 'text', text: `Table ${tableId} deleted.\n\n${JSON.stringify(result, null, 2)}` }],
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
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
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
- content: [{ type: 'text', text: `Column ${columnId} updated on table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
759
- };
805
+ return jsonContent({
806
+ action: 'column_updated',
807
+ tableId,
808
+ columnId,
809
+ result,
810
+ });
760
811
  })
761
812
  );
762
813