@enfyra/mcp-server 0.0.53 → 0.0.54
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 +3 -2
- package/src/lib/mcp-instructions.js +9 -8
- package/src/lib/table-tools.js +101 -14
- package/src/mcp-server-entry.mjs +11 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enfyra/mcp-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.54",
|
|
4
4
|
"description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "node src/index.mjs",
|
|
18
18
|
"dev": "node --watch src/index.mjs",
|
|
19
|
-
"mcp:config": "node src/index.mjs config"
|
|
19
|
+
"mcp:config": "node src/index.mjs config",
|
|
20
|
+
"test": "node --test"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -72,10 +72,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
72
72
|
'- REST-first workflow for any feature: **`inspect_feature`** to locate candidates → **`inspect_table`** for table/field/relation/rule context → **`inspect_route`** for handlers/hooks/guards/permissions → **`test_rest_endpoint`** to verify the actual HTTP behavior.',
|
|
73
73
|
'- Use **`create_column_rule`** for standard request validation, **`create_field_permission`** for per-field read/create/update rules, **`create_route_permission`** for authenticated route access, and **`create_guard`** for pre/post-auth request gates.',
|
|
74
74
|
'- Prefer these REST inspection/operator tools over raw `query_table` on system tables when changing route behavior. They resolve ids, methods, route paths, code previews, and cache reloads for the model.',
|
|
75
|
-
'- If the user asks for a **new route**, **URL path**, **custom API endpoint**, **handler**, **pre-hook**, **post-hook**, or to **test** that kind of logic: use MCP **`create_route`**
|
|
75
|
+
'- If the user asks for a **new route**, **URL path**, **custom API endpoint**, **handler**, **pre-hook**, **post-hook**, or to **test** that kind of logic: use MCP **`create_route`** and **omit `mainTableId`**. `mainTable` is only a marker for canonical table routes like `/orders`; custom paths such as `/orders/stats`, `/cloud/admin/hosts`, `/auth/login`, or `/me` must not set it.',
|
|
76
76
|
'- **Wrong pattern:** calling **`create_table`** just to get an HTTP path, then overriding handlers on the **default** auto route `/{table_name}`. That adds unnecessary schema and breaks the usual CRUD surface for that table.',
|
|
77
77
|
'- **`create_table`** is only when the user needs **new persisted data** (new entity + columns). It is **not** the right tool when the goal is only a new path or custom script.',
|
|
78
|
-
'- **Right pattern:** **`create_route`** → optional **`create_handler`** / **`create_pre_hook`** / **`create_post_hook`** on **that route’s id** (from **`get_all_routes`** after create).
|
|
78
|
+
'- **Right pattern:** **`create_route`** without `mainTableId` → optional **`create_handler`** / **`create_pre_hook`** / **`create_post_hook`** on **that route’s id** (from **`get_all_routes`** after create). Handler/hook code must query explicit repos such as `$ctx.$repos.orders`; do not rely on `$repos.main` for custom routes.',
|
|
79
79
|
'- **Handler contract:** `create_handler` takes `routeId`, `method` (or `methods` for batch), `sourceCode`, optional `scriptLanguage`, and optional `timeout`. Do **not** send `logic`, `name`, or `compiledCode`; backend CRUD rejects `logic` and `compiledCode` is generated by the server.',
|
|
80
80
|
'',
|
|
81
81
|
'### After a new table is created',
|
|
@@ -107,8 +107,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
107
107
|
'',
|
|
108
108
|
'### Relation field format (create_record / update_record)',
|
|
109
109
|
'- For generic MCP `create_record` and `update_record`, the `data` argument is a **JSON string**, not a JavaScript object. Example: `data: "{\\"name\\":\\"Starter\\"}"`. If the host gives a validation error saying `data` expected string, stringify the object before calling the tool.',
|
|
110
|
-
'- Relation fields (
|
|
111
|
-
'
|
|
110
|
+
'- Relation fields (publishedMethods, availableMethods, handlers, preHooks, postHooks, etc.) use **object references with `id`**:',
|
|
111
|
+
'- **mainTable warning:** do not set `mainTable` on custom routes. It is reserved for canonical table routes only.',
|
|
112
|
+
' - **Many-to-one:** `"someRelation": {"id": 4}` (single object with id)',
|
|
112
113
|
' - **One-to-many / many-to-many:** `"publishedMethods": [{"id": 1}, {"id": 2}]` (array of objects with id)',
|
|
113
114
|
'- **Method IDs** (for REST route publishedMethods, availableMethods, skipRoleGuardMethods): GET=1, POST=2, PATCH=3, DELETE=4. Query `method_definition` table if unsure.',
|
|
114
115
|
'- **Wrong:** `"publishedMethods": ["GET"]` or `"publishedMethods": [{"method": "GET"}]` — rejected or silently ignored.',
|
|
@@ -198,10 +199,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
198
199
|
'- **Not all system tables have a REST route.** `query_table`, `find_one_record`, `create_record`, etc. all go through the dynamic REST API and will return **404** if the table has no registered route.',
|
|
199
200
|
'- **`column_definition` and `session_definition` have NO route** — do NOT call `query_table("column_definition", …)` or `query_table("session_definition", …)`. They will 404.',
|
|
200
201
|
'- Do not invent singular/legacy system route names such as `hook_definition`, `oauth_provider_definition`, or physical FK tables. If a route name is not listed by `get_all_routes`, it is not a REST endpoint for generic CRUD. Use the concrete tables (`pre_hook_definition`, `post_hook_definition`, `oauth_config_definition`, etc.) or the dedicated MCP tool.',
|
|
201
|
-
'- To check which tables
|
|
202
|
+
'- To check which tables have canonical CRUD routes, call `get_all_routes` and look for `mainTable`. Custom routes intentionally have no `mainTable`; inspect their handlers/hooks to see which repos they touch.',
|
|
202
203
|
'- **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`.',
|
|
203
|
-
'- **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.',
|
|
204
|
-
'- 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`. When patching an existing table, only persisted columns with an `id`/`_id` belong in the cascade payload; metadata projections such as `createdAt`, `updatedAt`, or relation-derived FK display fields without an id are not valid column-definition patch rows.',
|
|
204
|
+
'- **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 by reading full table metadata first.',
|
|
205
|
+
'- 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`. When patching an existing table, only persisted columns with an `id`/`_id` belong in the cascade payload; metadata projections such as `createdAt`, `updatedAt`, or relation-derived FK display fields without an id are not valid column-definition patch rows. Never rebuild a schema cascade from `table_definition?fields=columns.*`, because nested relation fields may be paginated/truncated.',
|
|
205
206
|
'- 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.',
|
|
206
207
|
'',
|
|
207
208
|
'### Body validation & column rules',
|
|
@@ -220,7 +221,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
220
221
|
'',
|
|
221
222
|
'### Resolving the real REST path',
|
|
222
223
|
'- Do **not** assume `route_definition.path` always equals `table_definition.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
|
|
223
|
-
'- When unsure of the URL path, use MCP **`get_all_routes`** (or **`get_all_metadata`**) to read each route’s **path
|
|
224
|
+
'- When unsure of the URL path, use MCP **`get_all_routes`** (or **`get_all_metadata`**) to read each route’s **path**. Treat `mainTable` as canonical CRUD-route metadata only, not as the owner table for custom routes.',
|
|
224
225
|
'',
|
|
225
226
|
'### MongoDB vs SQL primary key',
|
|
226
227
|
'- On **SQL**, filters often use **`id`**. On **MongoDB**, documents may use **`_id`** — a filter for one row might be `{"_id":{"_eq":"..."}}` instead of `id`, depending on metadata.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -4,13 +4,42 @@
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { fetchAPI } from './fetch.js';
|
|
6
6
|
|
|
7
|
+
export function normalizeTablesFromMetadata(metadata) {
|
|
8
|
+
const tablesSource = metadata?.data?.tables || metadata?.tables || metadata?.data || [];
|
|
9
|
+
return Array.isArray(tablesSource)
|
|
10
|
+
? tablesSource
|
|
11
|
+
: Object.values(tablesSource || {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveTableFromMetadata(metadata, tableId) {
|
|
15
|
+
return normalizeTablesFromMetadata(metadata)
|
|
16
|
+
.find((table) => String(getId(table)) === String(tableId)) || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
|
-
* Helper: fetch table with columns and relations
|
|
20
|
+
* Helper: fetch table with full columns and relations.
|
|
21
|
+
* Dynamic table_definition relation fields can be paginated/truncated, so schema
|
|
22
|
+
* cascade tools must use /metadata as the complete source of columns/relations.
|
|
9
23
|
*/
|
|
10
|
-
async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
|
|
24
|
+
export async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
|
|
11
25
|
const filter = encodeURIComponent(JSON.stringify({ id: { _eq: tableId } }));
|
|
12
|
-
const
|
|
13
|
-
|
|
26
|
+
const [tableResult, metadata] = await Promise.all([
|
|
27
|
+
fetchAPI(ENFYRA_API_URL, `/table_definition?filter=${filter}&limit=1&fields=*`),
|
|
28
|
+
fetchAPI(ENFYRA_API_URL, '/metadata'),
|
|
29
|
+
]);
|
|
30
|
+
const tableData = tableResult?.data?.[0] || tableResult?.[0] || null;
|
|
31
|
+
const metadataTable = resolveTableFromMetadata(metadata, tableId);
|
|
32
|
+
if (!metadataTable) {
|
|
33
|
+
throw new Error(`Full metadata for table ${tableId} was not found; refusing schema cascade patch.`);
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(metadataTable.columns)) {
|
|
36
|
+
throw new Error(`Full metadata for table ${tableId} did not include columns; refusing schema cascade patch.`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...(tableData || metadataTable),
|
|
40
|
+
columns: metadataTable.columns,
|
|
41
|
+
relations: Array.isArray(metadataTable.relations) ? metadataTable.relations : [],
|
|
42
|
+
};
|
|
14
43
|
}
|
|
15
44
|
|
|
16
45
|
/**
|
|
@@ -99,6 +128,41 @@ function getPatchableColumns(columns) {
|
|
|
99
128
|
.map(normalizeColumnForTablePatch);
|
|
100
129
|
}
|
|
101
130
|
|
|
131
|
+
function getMissingIds(beforeIds, afterIds, excludedIds = []) {
|
|
132
|
+
const afterSet = new Set(afterIds.map(String));
|
|
133
|
+
const excludedSet = new Set(excludedIds.map(String));
|
|
134
|
+
return beforeIds
|
|
135
|
+
.map(String)
|
|
136
|
+
.filter((id) => !excludedSet.has(id) && !afterSet.has(id));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function verifyColumnCascade(ENFYRA_API_URL, tableId, beforeIds, {
|
|
140
|
+
action,
|
|
141
|
+
columnId,
|
|
142
|
+
columnName,
|
|
143
|
+
}) {
|
|
144
|
+
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
145
|
+
const afterColumns = getPatchableColumns(tableData.columns);
|
|
146
|
+
const afterIds = afterColumns.map((column) => String(getId(column)));
|
|
147
|
+
const excludedIds = action === 'delete' ? [columnId] : [];
|
|
148
|
+
const missingIds = getMissingIds(beforeIds, afterIds, excludedIds);
|
|
149
|
+
if (missingIds.length > 0) {
|
|
150
|
+
throw new Error(`Schema cascade verification failed: unrelated column ids disappeared: ${missingIds.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (action === 'create' && !afterColumns.some((column) => column.name === columnName)) {
|
|
154
|
+
throw new Error(`Schema cascade verification failed: column "${columnName}" was not found after create.`);
|
|
155
|
+
}
|
|
156
|
+
if (action === 'delete' && afterIds.includes(String(columnId))) {
|
|
157
|
+
throw new Error(`Schema cascade verification failed: column ${columnId} still exists after delete.`);
|
|
158
|
+
}
|
|
159
|
+
if (action === 'update' && !afterIds.includes(String(columnId))) {
|
|
160
|
+
throw new Error(`Schema cascade verification failed: column ${columnId} was not found after update.`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return afterColumns;
|
|
164
|
+
}
|
|
165
|
+
|
|
102
166
|
function buildColumnDefinition({
|
|
103
167
|
name,
|
|
104
168
|
type,
|
|
@@ -138,8 +202,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
138
202
|
}
|
|
139
203
|
|
|
140
204
|
const existingColumns = getPatchableColumns(tableData.columns);
|
|
205
|
+
const beforeIds = existingColumns.map((column) => String(getId(column)));
|
|
141
206
|
const newCol = buildColumnDefinition(args);
|
|
142
207
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, args.tableId, { columns: [...existingColumns, newCol] });
|
|
208
|
+
await verifyColumnCascade(ENFYRA_API_URL, args.tableId, beforeIds, {
|
|
209
|
+
action: 'create',
|
|
210
|
+
columnName: args.name,
|
|
211
|
+
});
|
|
143
212
|
|
|
144
213
|
return {
|
|
145
214
|
content: [{ type: 'text', text: `Column "${args.name}" added to table ${args.tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
@@ -170,12 +239,20 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
170
239
|
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
171
240
|
}
|
|
172
241
|
|
|
173
|
-
const
|
|
174
|
-
|
|
242
|
+
const existingColumns = getPatchableColumns(tableData.columns);
|
|
243
|
+
const beforeIds = existingColumns.map((column) => String(getId(column)));
|
|
244
|
+
if (!beforeIds.includes(String(columnId))) {
|
|
245
|
+
throw new Error(`Column ${columnId} was not found on table ${tableId}; refusing schema cascade patch.`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const columns = existingColumns
|
|
175
249
|
.filter(col => String(getId(col)) !== String(columnId))
|
|
176
|
-
.map(normalizeColumnForTablePatch);
|
|
177
250
|
|
|
178
251
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
|
|
252
|
+
await verifyColumnCascade(ENFYRA_API_URL, tableId, beforeIds, {
|
|
253
|
+
action: 'delete',
|
|
254
|
+
columnId,
|
|
255
|
+
});
|
|
179
256
|
|
|
180
257
|
return {
|
|
181
258
|
content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
@@ -256,7 +333,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
256
333
|
'create_table',
|
|
257
334
|
[
|
|
258
335
|
'Create a new table definition with an auto-included `id` primary key column.',
|
|
259
|
-
'**Not** for adding a custom API path or handler only — for that use **`create_route`**
|
|
336
|
+
'**Not** for adding a custom API path or handler only — for that use **`create_route`** without `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
|
|
260
337
|
'PREFERRED: pass `columns` and `relations` params as JSON arrays to create a table WITH columns and relations in one call (cascade). Only use create_column/create_relation separately when adding to an existing table later.',
|
|
261
338
|
'Indexes and uniques are first-class table metadata. Use `indexes` for query performance and `uniques` for data integrity. Each entry is a logical field group such as [["member","isRead","conversation"]] or [{"value":["message","member"]}]. Relation property names are allowed; Enfyra resolves them to physical FK columns.',
|
|
262
339
|
'Relations are supported in this same create_table call when the target table already exists. Each relation uses { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete? }; targetTable may be a table id or {id}.',
|
|
@@ -383,7 +460,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
383
460
|
[
|
|
384
461
|
'Add a column to an existing table via PATCH /table_definition/{tableId}.',
|
|
385
462
|
'Columns are managed through cascade with table_definition — there is NO direct /column_definition endpoint.',
|
|
386
|
-
'This tool
|
|
463
|
+
'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.',
|
|
387
464
|
'Generated metadata projections such as createdAt, updatedAt, or relation-derived FK display fields without id are not valid cascade rows and are skipped.',
|
|
388
465
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
389
466
|
].join(' '),
|
|
@@ -398,7 +475,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
398
475
|
[
|
|
399
476
|
'Alias for create_column. Add a column to an existing table through the canonical table_definition cascade.',
|
|
400
477
|
'Use this for schema additions, including hidden secret fields with isPublished=false.',
|
|
401
|
-
'
|
|
478
|
+
'Reads full table metadata and skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
|
|
402
479
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
403
480
|
].join(' '),
|
|
404
481
|
columnCreateSchema,
|
|
@@ -411,7 +488,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
411
488
|
'update_column',
|
|
412
489
|
[
|
|
413
490
|
'Update an existing column on a table via PATCH /table_definition/{tableId}.',
|
|
414
|
-
'
|
|
491
|
+
'Reads full table metadata, keeps only persisted rows with id/_id, modifies the target column, PATCHes the table, and verifies unrelated columns survived.',
|
|
415
492
|
'Generated metadata projections such as createdAt, updatedAt, or relation-derived FK display fields without id are skipped.',
|
|
416
493
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
417
494
|
].join(' '),
|
|
@@ -432,7 +509,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
432
509
|
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
433
510
|
}
|
|
434
511
|
|
|
435
|
-
const
|
|
512
|
+
const existingColumns = getPatchableColumns(tableData.columns);
|
|
513
|
+
const beforeIds = existingColumns.map((column) => String(getId(column)));
|
|
514
|
+
if (!beforeIds.includes(String(columnId))) {
|
|
515
|
+
throw new Error(`Column ${columnId} was not found on table ${tableId}; refusing schema cascade patch.`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const columns = existingColumns.map(col => {
|
|
436
519
|
const rest = normalizeColumnForTablePatch(col);
|
|
437
520
|
if (String(getId(col)) === String(columnId)) {
|
|
438
521
|
if (name !== undefined) rest.name = name;
|
|
@@ -447,6 +530,10 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
447
530
|
});
|
|
448
531
|
|
|
449
532
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
|
|
533
|
+
await verifyColumnCascade(ENFYRA_API_URL, tableId, beforeIds, {
|
|
534
|
+
action: 'update',
|
|
535
|
+
columnId,
|
|
536
|
+
});
|
|
450
537
|
|
|
451
538
|
return {
|
|
452
539
|
content: [{ type: 'text', text: `Column ${columnId} updated on table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
@@ -460,7 +547,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
460
547
|
'delete_column',
|
|
461
548
|
[
|
|
462
549
|
'Delete a column from a table via PATCH /table_definition/{tableId}.',
|
|
463
|
-
'
|
|
550
|
+
'Reads full table metadata, keeps only persisted rows with id/_id, removes the target, PATCHes the table, and verifies unrelated columns survived.',
|
|
464
551
|
'The physical column is dropped from the database. System columns (id, createdAt, updatedAt) cannot be deleted.',
|
|
465
552
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
466
553
|
].join(' '),
|
|
@@ -475,7 +562,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
475
562
|
[
|
|
476
563
|
'Alias for delete_column. Remove a column through the canonical table_definition cascade.',
|
|
477
564
|
'This drops the physical column. Confirm destructive schema changes before calling.',
|
|
478
|
-
'
|
|
565
|
+
'Reads full table metadata and skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
|
|
479
566
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
480
567
|
].join(' '),
|
|
481
568
|
columnDeleteSchema,
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -32,7 +32,7 @@ const CAPABILITY_AREAS = [
|
|
|
32
32
|
{
|
|
33
33
|
area: 'Dynamic REST API',
|
|
34
34
|
tables: ['route_definition', 'route_handler_definition', 'pre_hook_definition', 'post_hook_definition', 'route_permission_definition', 'method_definition'],
|
|
35
|
-
workflow: 'Create paths with create_route
|
|
35
|
+
workflow: 'Create custom paths with create_route without mainTableId, then add handlers/hooks. mainTableId is only for canonical table routes like /table_name. REST methods are GET/POST/PATCH/DELETE.',
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
area: 'Auth, roles, sessions, OAuth',
|
|
@@ -1387,15 +1387,16 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Call
|
|
|
1387
1387
|
server.tool(
|
|
1388
1388
|
'create_route',
|
|
1389
1389
|
[
|
|
1390
|
-
'**Use this when the user wants a new REST API route or path** — not `create_table`.
|
|
1391
|
-
'
|
|
1390
|
+
'**Use this when the user wants a new REST API route or path** — not `create_table`. Custom routes must omit `mainTableId`.',
|
|
1391
|
+
'`mainTableId` is only a marker for canonical table routes such as `/orders`; do not set it for `/orders/stats`, `/cloud/admin/hosts`, `/auth/login`, or any custom path.',
|
|
1392
|
+
'Do NOT create a new table_definition only to expose an endpoint; create a route without `mainTableId`, then have the handler/hook query explicit repos such as `$ctx.$repos.orders`.',
|
|
1392
1393
|
'availableMethods = which REST verbs the route responds to. publishedMethods = which REST verbs are public (no auth). GraphQL is enabled separately through gql_definition/update_table graphqlEnabled.',
|
|
1393
1394
|
'After creation the tool auto-reloads routes. Then create handlers for specific methods via create_handler on this route id.',
|
|
1394
|
-
'Flow:
|
|
1395
|
+
'Flow: create_route → create_handler (per method) → optionally create_pre_hook / create_post_hook → test via HTTP or admin test APIs (see server instructions).',
|
|
1395
1396
|
].join(' '),
|
|
1396
1397
|
{
|
|
1397
1398
|
path: z.string().describe('URL path, must start with / (e.g., "/my-endpoint")'),
|
|
1398
|
-
mainTableId: z.union([z.string(), z.number()]).describe('
|
|
1399
|
+
mainTableId: z.union([z.string(), z.number()]).optional().describe('Only set for the canonical table route `/<table_name>`. Omit for every custom route.'),
|
|
1399
1400
|
methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE']))
|
|
1400
1401
|
.describe('HTTP methods this route supports (availableMethods). Common: ["GET","POST","PATCH","DELETE"]'),
|
|
1401
1402
|
publishedMethods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
|
|
@@ -1408,12 +1409,15 @@ server.tool(
|
|
|
1408
1409
|
|
|
1409
1410
|
const body = {
|
|
1410
1411
|
path: routePath.startsWith('/') ? routePath : '/' + routePath,
|
|
1411
|
-
mainTable: { id: mainTableId },
|
|
1412
1412
|
isEnabled,
|
|
1413
1413
|
description,
|
|
1414
1414
|
availableMethods: resolveMethodIds(methodMap, methods),
|
|
1415
1415
|
};
|
|
1416
1416
|
|
|
1417
|
+
if (mainTableId !== undefined && mainTableId !== null) {
|
|
1418
|
+
body.mainTable = { id: mainTableId };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1417
1421
|
if (publishedMethods && publishedMethods.length > 0) {
|
|
1418
1422
|
body.publishedMethods = resolveMethodIds(methodMap, publishedMethods);
|
|
1419
1423
|
}
|
|
@@ -1431,7 +1435,7 @@ server.tool(
|
|
|
1431
1435
|
route: {
|
|
1432
1436
|
id: getId(created),
|
|
1433
1437
|
path: created?.path,
|
|
1434
|
-
mainTableId,
|
|
1438
|
+
mainTableId: mainTableId ?? null,
|
|
1435
1439
|
availableMethods: methods,
|
|
1436
1440
|
publishedMethods: publishedMethods || [],
|
|
1437
1441
|
},
|