@enfyra/mcp-server 0.0.19 → 0.0.20
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-instructions.js +52 -7
- package/src/lib/table-tools.js +51 -14
- package/src/mcp-server-entry.mjs +1157 -7
package/package.json
CHANGED
|
@@ -28,18 +28,43 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
28
28
|
`**API base for this session:** \`${base}\` (from env ENFYRA_API_URL, no trailing slash).`,
|
|
29
29
|
`**Full URL:** base + path segment. Example for table \`post\`: \`${examplePost}\`.`,
|
|
30
30
|
'',
|
|
31
|
+
'### First-step rule: discover before answering architecture/capability questions',
|
|
32
|
+
'- If the user asks what Enfyra supports, how to build a feature, which API exists, or whether a tool/schema path can do something, call **`discover_enfyra_system`** first. It reads live metadata, route definitions, method rows, route-backed tables, no-route tables, and capability areas.',
|
|
33
|
+
'- If the question depends on DB type, primary key convention, cache/reload/runtime state, active GraphQL/flow/websocket/storage counts, or admin surfaces, call **`discover_runtime_context`**.',
|
|
34
|
+
'- If the question depends on filters, sorting, deep relations, relation property names, field permissions, or table-specific query examples, call **`discover_query_capabilities`**; pass `tableName` when known.',
|
|
35
|
+
'- If writing or reviewing handler/hook/flow/websocket/extension logic, call **`discover_script_contexts`** first so macros and `$ctx` fields are not mixed across runtime surfaces.',
|
|
36
|
+
'- Treat hardcoded instructions as operating rules, but use live discovery as the final check for this running instance. Do not infer missing capabilities from a narrow tool schema; check metadata/routes or the relevant specialized tool first.',
|
|
37
|
+
'- If there is no dedicated MCP tool for a subsystem, use the route-backed metadata table with `query_table` / `create_record` / `update_record` / `delete_record`, after confirming that table has a route. If the table is no-route, use the canonical specialized tool or parent table workflow instead.',
|
|
38
|
+
'',
|
|
39
|
+
'### Capability map (current Enfyra system)',
|
|
40
|
+
'- **Schema/metadata:** `table_definition`, `relation_definition`, and schema tools manage tables, columns, relations, validation, and migrations. `column_definition` is internal/no-route; columns are created/updated through table schema operations.',
|
|
41
|
+
'- **Dynamic REST API:** `route_definition`, `route_handler_definition`, `pre_hook_definition`, `post_hook_definition`, `route_permission_definition`, and `method_definition` define paths, methods, handlers, hooks, and permissions.',
|
|
42
|
+
'- **Auth/OAuth/session:** `user_definition`, `role_definition`, `oauth_config_definition`, `oauth_account_definition`; `session_definition` is internal/no-route. OAuth is browser redirect only; MCP login is email/password.',
|
|
43
|
+
'- **Guards/permissions/validation:** `guard_definition`, `guard_rule_definition`, `field_permission_definition`, and `column_rule_definition` control route guards, field access, and request body validation.',
|
|
44
|
+
'- **GraphQL:** `gql_definition` enables tables in GraphQL. GraphQL endpoint and schema share `ENFYRA_API_URL`; GraphQL requires Bearer auth.',
|
|
45
|
+
'- **Files/storage/assets:** `file_definition`, `file_permission_definition`, `folder_definition`, `storage_config_definition` plus upload/assets routes and file helpers.',
|
|
46
|
+
'- **WebSocket:** `websocket_definition` and `websocket_event_definition` define Socket.IO gateways/events. Use `run_admin_test` for websocket scripts.',
|
|
47
|
+
'- **Flows:** `flow_definition`, `flow_step_definition`, `flow_execution_definition`; use `test_flow_step`, `run_admin_test`, and `trigger_flow` for runtime checks.',
|
|
48
|
+
'- **Extensions/packages/menus:** `extension_definition`, `menu_definition`, `package_definition`, `bootstrap_script_definition`; extensions are Vue SFC only, and packages should be installed with `install_package`.',
|
|
49
|
+
'- **Platform config:** `setting_definition`, `cors_origin_definition`, reload endpoints, logs, and metadata endpoints.',
|
|
50
|
+
'',
|
|
31
51
|
'### ENFYRA_API_URL (two valid setups)',
|
|
32
52
|
'- **Via Nuxt admin (typical):** `http://localhost:3000/api` — Nuxt proxies `/api/*` to Nest (`API_URL`, e.g. `http://localhost:1105`). Use this when MCP talks to the app origin.',
|
|
33
53
|
'- **Direct to Nest:** `http://localhost:1105` — no `/api` suffix on default Nest. Wrong: `http://localhost:1105/api/table_definition` (404) unless a proxy adds `/api`.',
|
|
34
54
|
'- GraphQL: `{base}/graphql` and `{base}/graphql-schema` always share this same base.',
|
|
35
55
|
'',
|
|
36
56
|
'### Routes vs tables (custom endpoints, handlers, hooks)',
|
|
57
|
+
'- 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.',
|
|
58
|
+
'- 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.',
|
|
59
|
+
'- 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.',
|
|
37
60
|
'- 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`** with **`mainTableId`** of an **existing** table (resolve id via **`get_all_tables`**, **`get_all_metadata`**, or **`get_all_routes`**).',
|
|
38
61
|
'- **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.',
|
|
39
62
|
'- **`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.',
|
|
40
63
|
'- **Right pattern:** **`create_route`** → optional **`create_handler`** / **`create_pre_hook`** / **`create_post_hook`** on **that route’s id** (from **`get_all_routes`** after create). Same underlying table can have **multiple** routes (e.g. default CRUD at `/orders` and custom `/orders/stats` both pointing at `mainTable` orders).',
|
|
41
64
|
'',
|
|
42
65
|
'### After a new table is created',
|
|
66
|
+
'- MCP **`create_table` supports creating columns and relations in the same call**: pass `columns` and `relations` as JSON arrays. Use `create_relation` only when adding a relation to an existing table later.',
|
|
67
|
+
'- In `create_table.relations`, each relation uses `targetTable` (table id or `{id}`), `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. The target table must already exist.',
|
|
43
68
|
'- Enfyra creates a **default** route at `/{table_name}` using the table **name** from `create_table` (not the alias). Prefer **`create_route`** for additional or custom paths instead of new tables.',
|
|
44
69
|
'- **Four REST HTTP operations** on that resource:',
|
|
45
70
|
` - **GET** \`${getList}\` — list / filter (query: filter, sort, page, limit, fields, meta).`,
|
|
@@ -52,7 +77,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
52
77
|
'- Relation fields (mainTable, publishedMethods, availableMethods, handlers, preHooks, postHooks, etc.) use **object references with `id`**:',
|
|
53
78
|
' - **Many-to-one:** `"mainTable": {"id": 4}` (single object with id)',
|
|
54
79
|
' - **One-to-many / many-to-many:** `"publishedMethods": [{"id": 1}, {"id": 2}]` (array of objects with id)',
|
|
55
|
-
'- **Method IDs** (for publishedMethods, availableMethods, skipRoleGuardMethods): GET=1, POST=2, PATCH=3, DELETE=4
|
|
80
|
+
'- **Method IDs** (for REST route publishedMethods, availableMethods, skipRoleGuardMethods): GET=1, POST=2, PATCH=3, DELETE=4. Query `method_definition` table if unsure.',
|
|
56
81
|
'- **Wrong:** `"publishedMethods": ["GET"]` or `"publishedMethods": [{"method": "GET"}]` — rejected or silently ignored.',
|
|
57
82
|
'- **Right:** `"publishedMethods": [{"id": 1}]` (publishes GET). Multiple: `[{"id": 1}, {"id": 2}]` (publishes GET + POST).',
|
|
58
83
|
'- **To unset:** pass empty array `"publishedMethods": []`.',
|
|
@@ -91,10 +116,11 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
91
116
|
'',
|
|
92
117
|
'### System tables — which have REST routes',
|
|
93
118
|
'- **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.',
|
|
94
|
-
'- **`column_definition`
|
|
119
|
+
'- **`column_definition` and `session_definition` have NO route** — do NOT call `query_table("column_definition", …)` or `query_table("session_definition", …)`. They will 404.',
|
|
95
120
|
'- 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.',
|
|
96
|
-
'- **Tables confirmed to have REST routes (system):** `
|
|
97
|
-
'- **Tables without REST routes (internal/system only):** `column_definition`, `
|
|
121
|
+
'- **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`.',
|
|
122
|
+
'- **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.',
|
|
123
|
+
'- Prefer `create_relation` / `delete_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.',
|
|
98
124
|
'',
|
|
99
125
|
'### Body validation & column rules',
|
|
100
126
|
'- 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.',
|
|
@@ -108,7 +134,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
108
134
|
'',
|
|
109
135
|
'### Schema / table migration (sequential only)',
|
|
110
136
|
'- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
|
|
111
|
-
'- Do NOT batch multiple schema changes (e.g. create 3 tables
|
|
137
|
+
'- Do NOT batch multiple schema changes in parallel (e.g. create 3 tables concurrently, or create a table and separately add columns at the same time). A single `create_table` call may include its own `columns` and `relations`; treat that as one schema operation. Execute each `create_table`, `create_column`, `create_relation`, sync, or drop sequentially; wait for completion before the next.',
|
|
112
138
|
'',
|
|
113
139
|
'### Resolving the real REST path',
|
|
114
140
|
'- Do **not** assume `route_definition.path` always equals `table_definition.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
|
|
@@ -116,13 +142,22 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
116
142
|
'',
|
|
117
143
|
'### MongoDB vs SQL primary key',
|
|
118
144
|
'- 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.',
|
|
145
|
+
'- Use **`discover_runtime_context`** to read live `dbType` and `pkField` from metadata. If an older backend does not expose them, the tool falls back to primary-key inference and labels the source clearly.',
|
|
146
|
+
'',
|
|
147
|
+
'### Query DSL and deep relations',
|
|
148
|
+
'- Use **`discover_query_capabilities`** before building non-trivial filters/deep queries. It returns supported filter operators, field-permission condition operators, deep shape/rules, table columns/relations, table primary key, route paths, and examples.',
|
|
149
|
+
'- Full filter operators: `_eq`, `_neq`, `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_not_in`, `_nin`, `_contains`, `_starts_with`, `_ends_with`, `_between`, `_is_null`, `_is_not_null`, `_and`, `_or`, `_not`.',
|
|
150
|
+
'- Field permission condition DSL is narrower and does not support `_contains`, `_starts_with`, `_ends_with`, or `_between`.',
|
|
151
|
+
'- Deep shape: `{ relationName: { fields?, filter?, sort?, limit?, page?, deep? } }`. Relation keys are relation `propertyName`, not physical FK columns.',
|
|
152
|
+
'- Deep validation rejects unknown relation keys, unknown subkeys, `limit` on many-to-one/one-to-one, and invalid dotted sort through many-side relations.',
|
|
119
153
|
'',
|
|
120
154
|
'### GraphQL (same prefix as REST / ENFYRA_API_URL)',
|
|
121
155
|
`- **POST** \`${graphqlHttpUrl}\` — GraphQL endpoint (body: GraphQL query). With Nuxt base: e.g. \`http://localhost:3000/api/graphql\`. With direct Nest: e.g. \`http://localhost:1105/graphql\`.`,
|
|
122
156
|
`- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text); same base pattern as above.`,
|
|
123
|
-
'- A table appears in the schema
|
|
157
|
+
'- A table appears in the schema when `gql_definition` has an enabled row for that table. The REST route `availableMethods` list does not enable GraphQL.',
|
|
124
158
|
'- **Query** field = same string as `table_definition.name`. **Mutations** are literal concat: `create_`+tableName, `update_`+tableName, `delete_`+tableName (e.g. tableName `post` → `create_post`, input type `postInput`). See `generate-type-defs.ts`. If every column is skipped for input (only PK, or only `createdAt`/`updatedAt`, or all unpublished), the schema emits **no** `Query.<tableName>` and **no** create/update/delete mutations for that table (an output `type` may still exist for relation wiring).',
|
|
125
|
-
'- **Auth:**
|
|
159
|
+
'- **Auth:** GraphQL currently requires `Authorization: Bearer <accessToken>`. REST route `publishedMethods` does not make GraphQL anonymous.',
|
|
160
|
+
'- **Management workflow:** use `update_table` with `graphqlEnabled: true|false`, or create/update `gql_definition` with `table: {id}` and `isEnabled`. Reload GraphQL with `reload_graphql` if the cache has not refreshed yet.',
|
|
126
161
|
'- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
|
|
127
162
|
'',
|
|
128
163
|
'### WebSocket (Socket.IO)',
|
|
@@ -144,6 +179,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
144
179
|
'- **Client**: `io("<HTTP_ORIGIN>/namespace", {auth: {token: JWT}})`. Use the **origin where Socket.IO is served** (usually the **Nest** HTTP origin, e.g. `http://localhost:1105/chat` in local server-only setups). If Socket.IO is exposed only through the Nuxt app, use that host and your deployment’s WS path—**do not** assume port 3000 without checking `API_URL` / proxy config. Gateway `path` in metadata = Socket.IO **namespace**.',
|
|
145
180
|
'- **Workflow**: Create gateway → `create_record` on `websocket_definition`. Create event → `create_record` on `websocket_event_definition` with `gateway: {id}`. Changes auto-reload; test handlers before saving.',
|
|
146
181
|
'- **Test WS handler (recommended):** `POST {base}/admin/test/run` with `{ kind:"websocket_event", gatewayPath, eventName, timeoutMs, payload, script }` to run a websocket event script without a real client. Returns `{ success, result, logs, emitted }`.',
|
|
182
|
+
'- MCP wrapper: use **`run_admin_test`** with `kind:"websocket_event"` or `kind:"websocket_connection"` instead of hand-building the HTTP call.',
|
|
183
|
+
'- Before writing websocket scripts, call **`discover_script_contexts`** to confirm which `@SOCKET` methods are bound in websocket vs HTTP/flow contexts.',
|
|
147
184
|
'',
|
|
148
185
|
'### Flows (Automated Workflows)',
|
|
149
186
|
'- Enfyra supports automated workflows via **`flow_definition`**, **`flow_step_definition`**, and **`flow_execution_definition`** tables.',
|
|
@@ -158,7 +195,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
158
195
|
'- **Safety**: Max nesting depth 10 (flow triggering flow). Circular flow detection prevents A→B→A loops. HTTP steps: **SSRF hardening** — only `http`/`https`; blocks `localhost`, private IPs, and hostnames resolving to private IPs (use internet-facing URLs like `https://api.example.com`, not internal services, unless server policy changes). Default HTTP timeout 30s (AbortController). `$trigger()` available inside flow steps.',
|
|
159
196
|
'- **Workflow**: Create flow → `create_record` on `flow_definition`. Add steps → `create_record` on `flow_step_definition` with `flow: {id}`. For branch steps, set `parent: {id: conditionStepId}` and `branch: "true"` or `"false"`. Trigger manually via `POST /admin/flow/trigger/{flowId}`.',
|
|
160
197
|
'- **Test step**: `POST /admin/flow/test-step` with body `{type, config, timeout}` — runs a single step without saving, returns `{success, result, error, duration}`.',
|
|
198
|
+
'- MCP wrappers: use **`test_flow_step`** for one step, **`run_admin_test`** with `kind:"flow_step"` for the generic admin tester, and **`trigger_flow`** for saved flows.',
|
|
161
199
|
'- **In handlers/hooks**: Trigger flows via `$ctx.$trigger("flow-name", {payload})` or `$ctx.$trigger(flowId, {payload})`.',
|
|
200
|
+
'- Before writing flow scripts, call **`discover_script_contexts`** to confirm `@FLOW`, `@FLOW_PAYLOAD`, `@FLOW_LAST`, `#table_name`, `$ctx.$trigger`, and `$socket` behavior.',
|
|
162
201
|
'',
|
|
163
202
|
'### Extension (Vue SFC only — NOT React)',
|
|
164
203
|
'- **CRITICAL:** MUST call `create_record` or `update_record` on `extension_definition` — outputting Vue code in chat does NOT save it. User will NOT see it.',
|
|
@@ -243,6 +282,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
243
282
|
'- **Routes:** `create_route` / `create_handler` / `create_pre_hook` / `create_post_hook` persist to `route_definition`, `route_handler_definition`, etc. (REST CRUD on those tables). Prefer **`create_route`** for new paths — not `create_table`.',
|
|
244
283
|
`- \`get_all_metadata\` → GET \`${base}/metadata\``,
|
|
245
284
|
`- \`get_table_metadata\` → GET \`${base}/metadata/<tableName>\``,
|
|
285
|
+
`- \`discover_runtime_context\` → GET metadata/routes/method/runtime-backed tables and infer live primary key/backend context`,
|
|
286
|
+
`- \`discover_query_capabilities\` → GET metadata/routes and summarize Query DSL/deep/table-specific query contracts`,
|
|
287
|
+
`- \`discover_script_contexts\` → static runtime macro/context map for handlers/hooks/flows/websocket/GraphQL/extensions`,
|
|
246
288
|
`- \`query_table\` → GET \`${base}/<tableName>?…\` (query string from tool args)`,
|
|
247
289
|
`- \`find_one_record\` (by id) → GET \`${base}/<tableName>?filter=…&limit=1\``,
|
|
248
290
|
`- \`create_record\` → POST \`${base}/<tableName>\``,
|
|
@@ -250,6 +292,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
250
292
|
`- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\``,
|
|
251
293
|
`- \`create_extension\` → POST \`${base}/extension_definition\` (Vue SFC only; for page pass menuId). \`update_record\` on extension_definition to change code.`,
|
|
252
294
|
`- Flow tables: \`${base}/flow_definition\`, \`${base}/flow_step_definition\`, \`${base}/flow_execution_definition\` — use standard CRUD tools.`,
|
|
295
|
+
`- \`run_admin_test\` → POST \`${base}/admin/test/run\``,
|
|
296
|
+
`- \`test_flow_step\` → POST \`${base}/admin/flow/test-step\``,
|
|
297
|
+
`- \`trigger_flow\` → POST \`${base}/admin/flow/trigger/<flowIdOrName>\``,
|
|
253
298
|
`- Other: \`${base}/menu_definition\`, \`${base}/websocket_definition\`, \`${base}/admin/reload\`, etc.`,
|
|
254
299
|
'',
|
|
255
300
|
'When asked which endpoint the API calls, respond with **HTTP method + full URL** using this base. Call `get_enfyra_api_context` to confirm the resolved base if needed.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -33,6 +33,34 @@ async function patchTableAutoConfirm(ENFYRA_API_URL, tableId, body) {
|
|
|
33
33
|
return result;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function parseJsonArrayParam(name, value) {
|
|
37
|
+
if (!value) return [];
|
|
38
|
+
const parsed = JSON.parse(value);
|
|
39
|
+
if (!Array.isArray(parsed)) {
|
|
40
|
+
throw new Error(`${name} must be a JSON array.`);
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeRelationForTablePatch(relation) {
|
|
46
|
+
const { sourceTable, targetTable, targetTableId, mappedBy, ...rest } = relation;
|
|
47
|
+
const normalized = { ...rest };
|
|
48
|
+
const resolvedTargetTable =
|
|
49
|
+
targetTableId ??
|
|
50
|
+
(targetTable && typeof targetTable === 'object'
|
|
51
|
+
? targetTable.id ?? targetTable._id ?? targetTable
|
|
52
|
+
: targetTable);
|
|
53
|
+
if (resolvedTargetTable !== undefined && resolvedTargetTable !== null) {
|
|
54
|
+
normalized.targetTable = resolvedTargetTable;
|
|
55
|
+
}
|
|
56
|
+
if (mappedBy !== undefined && mappedBy !== null && mappedBy !== '') {
|
|
57
|
+
normalized.mappedBy = typeof mappedBy === 'object'
|
|
58
|
+
? mappedBy.propertyName ?? mappedBy.name ?? mappedBy.id ?? mappedBy._id
|
|
59
|
+
: mappedBy;
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
/**
|
|
37
65
|
* Register table tools with MCP server
|
|
38
66
|
*/
|
|
@@ -60,28 +88,32 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
60
88
|
[
|
|
61
89
|
'Create a new table definition with an auto-included `id` primary key column.',
|
|
62
90
|
'**Not** for adding a custom API path or handler only — for that use **`create_route`** with an existing `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
|
|
63
|
-
'PREFERRED: pass `columns`
|
|
91
|
+
'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.',
|
|
92
|
+
'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}.',
|
|
64
93
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
65
94
|
'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
66
95
|
'REST surface for that route (matches server route engine): 4 HTTP operations — GET `/<table>` (list/filter), POST `/<table>` (create), PATCH `/<table>/:id` (update), DELETE `/<table>/:id` (delete).',
|
|
67
96
|
'There is NO `GET /<table>/:id`. To fetch one row by id, use GET `/<table>?filter={"id":{"_eq":"<id>"}}&limit=1` or tool query_table / find_one_record.',
|
|
68
97
|
`Full URLs: ${apiBase}/<table_name> (example table post: ${apiBase}/post).`,
|
|
69
|
-
'GraphQL
|
|
98
|
+
'GraphQL is enabled separately per table through `gql_definition` or `update_table` with `graphqlEnabled`; it is not controlled by route availableMethods.',
|
|
70
99
|
].join(' '),
|
|
71
100
|
{
|
|
72
101
|
name: z.string().describe('Table name (e.g., "user_definition", "my_custom_table"). Must be unique, lowercase with underscores.'),
|
|
73
102
|
alias: z.string().optional().describe('Table alias for API. If not provided, the table name will be used.'),
|
|
74
103
|
description: z.string().optional().describe('Description of what this table stores.'),
|
|
75
|
-
isEnabled: z.boolean().optional().default(true).describe('Enable table. Set to false to disable.'),
|
|
76
104
|
columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, defaultValue?, description?, options? }. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"status","type":"enum","options":["draft","published"]}]'),
|
|
105
|
+
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 or {"id": <id>}. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
77
106
|
},
|
|
78
|
-
async ({ name, alias, description,
|
|
107
|
+
async ({ name, alias, description, columns: columnsJson, relations: relationsJson }) => {
|
|
79
108
|
const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
|
|
80
|
-
const userColumns =
|
|
109
|
+
const userColumns = parseJsonArrayParam('columns', columnsJson);
|
|
110
|
+
const userRelations = parseJsonArrayParam('relations', relationsJson).map(normalizeRelationForTablePatch);
|
|
81
111
|
const result = await fetchAPI(ENFYRA_API_URL, '/table_definition', {
|
|
82
112
|
method: 'POST',
|
|
83
|
-
body: JSON.stringify({ name, alias, description,
|
|
113
|
+
body: JSON.stringify({ name, alias, description, columns: [idColumn, ...userColumns], relations: userRelations }),
|
|
84
114
|
});
|
|
115
|
+
const createdTable = Array.isArray(result?.data) ? result.data[0] : result;
|
|
116
|
+
const createdTableId = createdTable?.id ?? createdTable?._id;
|
|
85
117
|
const base = ENFYRA_API_URL.replace(/\/$/, '');
|
|
86
118
|
const routePath = `/${name}`;
|
|
87
119
|
const restHint = [
|
|
@@ -90,9 +122,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
90
122
|
].join('\n');
|
|
91
123
|
const colHint = userColumns.length
|
|
92
124
|
? `Table created with ${userColumns.length} column(s) + auto id.`
|
|
93
|
-
: `Table created. Use create_column to add columns (tableId: ${
|
|
125
|
+
: `Table created. Use create_column to add columns (tableId: ${createdTableId}).`;
|
|
126
|
+
const relHint = userRelations.length
|
|
127
|
+
? `Relation(s) created in same call: ${userRelations.length}.`
|
|
128
|
+
: `No relations were included in this create_table call.`;
|
|
94
129
|
return {
|
|
95
|
-
content: [{ type: 'text', text: `${colHint}\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
|
|
130
|
+
content: [{ type: 'text', text: `${colHint}\n${relHint}\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
|
|
96
131
|
};
|
|
97
132
|
}
|
|
98
133
|
);
|
|
@@ -102,7 +137,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
102
137
|
server.tool(
|
|
103
138
|
'update_table',
|
|
104
139
|
[
|
|
105
|
-
'Update table properties: name (rename), alias, description, isSingleRecord,
|
|
140
|
+
'Update table properties: name (rename), alias, description, isSingleRecord, graphqlEnabled.',
|
|
106
141
|
'Does NOT modify columns or relations — use create_column, update_column, delete_column, create_relation for those.',
|
|
107
142
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
108
143
|
].join(' '),
|
|
@@ -112,13 +147,15 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
112
147
|
alias: z.string().optional().describe('New table alias.'),
|
|
113
148
|
description: z.string().optional().describe('New description.'),
|
|
114
149
|
isSingleRecord: z.boolean().optional().describe('Set to true for single-record table (e.g., settings/config).'),
|
|
150
|
+
graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing gql_definition.isEnabled. GraphQL still requires Bearer auth.'),
|
|
115
151
|
},
|
|
116
|
-
async ({ tableId, name, alias, description, isSingleRecord }) => {
|
|
152
|
+
async ({ tableId, name, alias, description, isSingleRecord, graphqlEnabled }) => {
|
|
117
153
|
const body = {};
|
|
118
154
|
if (name !== undefined) body.name = name;
|
|
119
155
|
if (alias !== undefined) body.alias = alias;
|
|
120
156
|
if (description !== undefined) body.description = description;
|
|
121
157
|
if (isSingleRecord !== undefined) body.isSingleRecord = isSingleRecord;
|
|
158
|
+
if (graphqlEnabled !== undefined) body.graphqlEnabled = graphqlEnabled;
|
|
122
159
|
|
|
123
160
|
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
124
161
|
method: 'PATCH',
|
|
@@ -289,15 +326,15 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
289
326
|
propertyName: z.string().describe('Property name on source table (e.g., "customer", "items").'),
|
|
290
327
|
inversePropertyName: z.string().optional().describe('Property name on target table for bidirectional relation (e.g., "orders").'),
|
|
291
328
|
isNullable: z.boolean().optional().default(true).describe('Whether the relation is nullable.'),
|
|
292
|
-
onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT'
|
|
329
|
+
onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT']).optional().default('SET NULL').describe('On delete behavior.'),
|
|
293
330
|
},
|
|
294
331
|
async ({ sourceTableId, targetTableId, type, propertyName, inversePropertyName, isNullable, onDelete }) => {
|
|
295
332
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, sourceTableId);
|
|
296
333
|
if (!tableData) {
|
|
297
334
|
return { content: [{ type: 'text', text: `Error: Table with ID ${sourceTableId} not found.` }] };
|
|
298
335
|
}
|
|
299
|
-
const existingRelations = (tableData.relations || []).map(
|
|
300
|
-
const newRelation = { targetTableId, type, propertyName, inversePropertyName: inversePropertyName || null, isNullable, onDelete };
|
|
336
|
+
const existingRelations = (tableData.relations || []).map(normalizeRelationForTablePatch);
|
|
337
|
+
const newRelation = { targetTable: targetTableId, type, propertyName, inversePropertyName: inversePropertyName || null, isNullable, onDelete };
|
|
301
338
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, sourceTableId, { relations: [...existingRelations, newRelation] });
|
|
302
339
|
return {
|
|
303
340
|
content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${sourceTableId} → ${targetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
|
|
@@ -326,7 +363,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
326
363
|
|
|
327
364
|
const relations = (tableData.relations || [])
|
|
328
365
|
.filter(rel => String(rel.id) !== String(relationId))
|
|
329
|
-
.map(
|
|
366
|
+
.map(normalizeRelationForTablePatch);
|
|
330
367
|
|
|
331
368
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { relations });
|
|
332
369
|
|