@enfyra/mcp-server 0.0.85 → 0.0.86
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 +1 -1
- package/src/lib/mcp-examples.js +47 -47
- package/src/lib/mcp-instructions.js +64 -64
- package/src/lib/mutation-guards.js +18 -18
- package/src/lib/route-permission-tools.js +2 -2
- package/src/lib/table-tools.js +23 -23
- package/src/mcp-server-entry.mjs +143 -151
package/src/lib/table-tools.js
CHANGED
|
@@ -45,13 +45,13 @@ export function resolveTableFromMetadataByName(metadata, tableName) {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Helper: fetch table with full columns and relations.
|
|
48
|
-
* Dynamic
|
|
48
|
+
* Dynamic enfyra_table relation fields can be paginated/truncated, so schema
|
|
49
49
|
* cascade tools must use /metadata as the complete source of columns/relations.
|
|
50
50
|
*/
|
|
51
51
|
export async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
|
|
52
52
|
const filter = encodeURIComponent(JSON.stringify({ id: { _eq: tableId } }));
|
|
53
53
|
const [tableResult, metadata] = await Promise.all([
|
|
54
|
-
fetchAPI(ENFYRA_API_URL, `/
|
|
54
|
+
fetchAPI(ENFYRA_API_URL, `/enfyra_table?filter=${filter}&limit=1&fields=*`),
|
|
55
55
|
fetchAPI(ENFYRA_API_URL, '/metadata'),
|
|
56
56
|
]);
|
|
57
57
|
const tableData = tableResult?.data?.[0] || tableResult?.[0] || null;
|
|
@@ -72,18 +72,18 @@ export async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* PATCH
|
|
75
|
+
* PATCH enfyra_table with auto-confirm for schema changes.
|
|
76
76
|
* First PATCH returns preview + requiredConfirmHash; this helper
|
|
77
77
|
* automatically resends with ?schemaConfirmHash= to apply.
|
|
78
78
|
*/
|
|
79
79
|
async function patchTableAutoConfirm(ENFYRA_API_URL, tableId, body) {
|
|
80
|
-
const result = await fetchAPI(ENFYRA_API_URL, `/
|
|
80
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}`, {
|
|
81
81
|
method: 'PATCH',
|
|
82
82
|
body: JSON.stringify(body),
|
|
83
83
|
});
|
|
84
84
|
const preview = Array.isArray(result?.data) ? result.data[0] : result?.data;
|
|
85
85
|
if (preview?._preview && preview?.requiredConfirmHash) {
|
|
86
|
-
return fetchAPI(ENFYRA_API_URL, `/
|
|
86
|
+
return fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}?schemaConfirmHash=${preview.requiredConfirmHash}`, {
|
|
87
87
|
method: 'PATCH',
|
|
88
88
|
body: JSON.stringify(body),
|
|
89
89
|
});
|
|
@@ -464,7 +464,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
464
464
|
'Get all table definitions in the system',
|
|
465
465
|
{},
|
|
466
466
|
async () => {
|
|
467
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
467
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_table?limit=500');
|
|
468
468
|
return {
|
|
469
469
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
470
470
|
};
|
|
@@ -488,13 +488,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
488
488
|
'There is NO `GET /<table>/:id`. To fetch one row by id, use find_one_record or inspect metadata first and call GET `/<table>?filter={"<primaryKeyFromMetadata>":{"_eq":"<id>"}}&limit=1`.',
|
|
489
489
|
'Set `isSingleRecord: true` directly in create_table for settings/config tables that should keep only one record.',
|
|
490
490
|
`Full URLs: ${apiBase}/<table_name> (example table post: ${apiBase}/post).`,
|
|
491
|
-
'GraphQL is enabled separately per table through `
|
|
491
|
+
'GraphQL is enabled separately per table through `enfyra_graphql` or `update_table` with `graphqlEnabled`; it is not controlled by route availableMethods.',
|
|
492
492
|
'Do not set alias during create_table. The create tool accepts name, description, isSingleRecord, columns, and relations only; use update_table later only if alias really needs to change.',
|
|
493
493
|
].join(' '),
|
|
494
494
|
{
|
|
495
|
-
name: z.string().describe('Table name (e.g., "
|
|
495
|
+
name: z.string().describe('Table name (e.g., "enfyra_user", "my_custom_table"). Must be unique, lowercase with underscores.'),
|
|
496
496
|
description: z.string().optional().describe('Description of what this table stores.'),
|
|
497
|
-
isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to
|
|
497
|
+
isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to enfyra_table create.'),
|
|
498
498
|
columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, isPublished?, isUpdatable?, isEncrypted?, defaultValue?, description?, options? }. Set isEncrypted=true for values encrypted at rest; set isUpdatable=false separately only when the field should be immutable. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"api_key","type":"varchar","isEncrypted":true,"isPublished":false}]'),
|
|
499
499
|
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. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
500
500
|
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"]]'),
|
|
@@ -514,7 +514,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
514
514
|
if (isSingleRecord !== undefined) body.isSingleRecord = isSingleRecord;
|
|
515
515
|
if (indexesJson !== undefined) body.indexes = indexes;
|
|
516
516
|
if (uniquesJson !== undefined) body.uniques = uniques;
|
|
517
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
517
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_table', {
|
|
518
518
|
method: 'POST',
|
|
519
519
|
body: JSON.stringify(body),
|
|
520
520
|
});
|
|
@@ -558,9 +558,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
558
558
|
alias: z.string().optional().describe('New table alias.'),
|
|
559
559
|
description: z.string().optional().describe('New description.'),
|
|
560
560
|
isSingleRecord: z.boolean().optional().describe('Set to true for single-record table (e.g., settings/config).'),
|
|
561
|
-
graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing
|
|
562
|
-
indexes: z.string().optional().describe('Complete JSON array of logical index field groups to store on
|
|
563
|
-
uniques: z.string().optional().describe('Complete JSON array of logical unique field groups to store on
|
|
561
|
+
graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing enfyra_graphql.isEnabled. GraphQL still requires Bearer auth.'),
|
|
562
|
+
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.'),
|
|
563
|
+
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.'),
|
|
564
564
|
},
|
|
565
565
|
async ({ tableId, name, alias, description, isSingleRecord, graphqlEnabled, indexes: indexesJson, uniques: uniquesJson }) => withSchemaQueue(async () => {
|
|
566
566
|
const body = {};
|
|
@@ -607,7 +607,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
607
607
|
}, null, 2) }],
|
|
608
608
|
};
|
|
609
609
|
}
|
|
610
|
-
const result = await fetchAPI(ENFYRA_API_URL, `/
|
|
610
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}`, {
|
|
611
611
|
method: 'DELETE',
|
|
612
612
|
});
|
|
613
613
|
return {
|
|
@@ -621,8 +621,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
621
621
|
server.tool(
|
|
622
622
|
'create_column',
|
|
623
623
|
[
|
|
624
|
-
'Add a column to an existing table via PATCH /
|
|
625
|
-
'Columns are managed through cascade with
|
|
624
|
+
'Add a column to an existing table via PATCH /enfyra_table/{tableId}.',
|
|
625
|
+
'Columns are managed through cascade with enfyra_table — there is NO direct /enfyra_column endpoint.',
|
|
626
626
|
'This tool reads full table metadata, keeps only persisted column rows with id/_id, appends the new one, PATCHes the table, and verifies unrelated columns survived.',
|
|
627
627
|
'Generated metadata projections such as createdAt, updatedAt, or relation-derived FK display fields without id are not valid cascade rows and are skipped.',
|
|
628
628
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
@@ -636,7 +636,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
636
636
|
server.tool(
|
|
637
637
|
'add_column',
|
|
638
638
|
[
|
|
639
|
-
'Alias for create_column. Add a column to an existing table through the canonical
|
|
639
|
+
'Alias for create_column. Add a column to an existing table through the canonical enfyra_table cascade.',
|
|
640
640
|
'Use this for schema additions, including hidden secret fields with isPublished=false.',
|
|
641
641
|
'Reads full table metadata and skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
|
|
642
642
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
@@ -650,7 +650,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
650
650
|
server.tool(
|
|
651
651
|
'update_column',
|
|
652
652
|
[
|
|
653
|
-
'Update an existing column on a table via PATCH /
|
|
653
|
+
'Update an existing column on a table via PATCH /enfyra_table/{tableId}.',
|
|
654
654
|
'Reads full table metadata, keeps only persisted rows with id/_id, modifies the target column, PATCHes the table, and verifies unrelated columns survived.',
|
|
655
655
|
'Generated metadata projections such as createdAt, updatedAt, or relation-derived FK display fields without id are skipped.',
|
|
656
656
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
@@ -711,7 +711,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
711
711
|
server.tool(
|
|
712
712
|
'delete_column',
|
|
713
713
|
[
|
|
714
|
-
'Delete a column from a table via PATCH /
|
|
714
|
+
'Delete a column from a table via PATCH /enfyra_table/{tableId}.',
|
|
715
715
|
'Reads full table metadata, keeps only persisted rows with id/_id, removes the target, PATCHes the table, and verifies unrelated columns survived.',
|
|
716
716
|
'The physical column is dropped from the database. System columns (id, createdAt, updatedAt) cannot be deleted.',
|
|
717
717
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
@@ -725,7 +725,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
725
725
|
server.tool(
|
|
726
726
|
'remove_column',
|
|
727
727
|
[
|
|
728
|
-
'Alias for delete_column. Remove a column through the canonical
|
|
728
|
+
'Alias for delete_column. Remove a column through the canonical enfyra_table cascade.',
|
|
729
729
|
'This drops the physical column. Confirm destructive schema changes before calling.',
|
|
730
730
|
'Reads full table metadata and skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
|
|
731
731
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
@@ -753,7 +753,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
753
753
|
server.tool(
|
|
754
754
|
'add_relation',
|
|
755
755
|
[
|
|
756
|
-
'Alias for create_relation. Add a relation through the canonical
|
|
756
|
+
'Alias for create_relation. Add a relation through the canonical enfyra_table cascade.',
|
|
757
757
|
'Use relation propertyName only; never provide physical FK or junction column names.',
|
|
758
758
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
759
759
|
].join(' '),
|
|
@@ -766,7 +766,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
766
766
|
server.tool(
|
|
767
767
|
'delete_relation',
|
|
768
768
|
[
|
|
769
|
-
'Delete a relation from a table via PATCH /
|
|
769
|
+
'Delete a relation from a table via PATCH /enfyra_table/{tableId}.',
|
|
770
770
|
'Fetches all relations, removes the target, and PATCHes the table.',
|
|
771
771
|
'Drops FK columns and junction tables (for many-to-many).',
|
|
772
772
|
].join(' '),
|
|
@@ -779,7 +779,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
779
779
|
server.tool(
|
|
780
780
|
'remove_relation',
|
|
781
781
|
[
|
|
782
|
-
'Alias for delete_relation. Remove a relation through the canonical
|
|
782
|
+
'Alias for delete_relation. Remove a relation through the canonical enfyra_table cascade.',
|
|
783
783
|
'This can drop FK columns or junction tables. Confirm destructive schema changes before calling.',
|
|
784
784
|
].join(' '),
|
|
785
785
|
relationDeleteSchema,
|