@enfyra/mcp-server 0.0.18 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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).`,
@@ -48,7 +73,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
48
73
  ` - **DELETE** \`${delOne}\` — delete one row.`,
49
74
  `- **No** **GET** \`${base}/<table_name>/<id>\`. For one row by id use **GET** \`${getOneById}\` or MCP \`query_table\` / \`find_one_record\`.`,
50
75
  '',
51
- '### Auth and publishedMethods (Enfyra server)',
76
+ '### Relation field format (create_record / update_record)',
77
+ '- Relation fields (mainTable, publishedMethods, availableMethods, handlers, preHooks, postHooks, etc.) use **object references with `id`**:',
78
+ ' - **Many-to-one:** `"mainTable": {"id": 4}` (single object with id)',
79
+ ' - **One-to-many / many-to-many:** `"publishedMethods": [{"id": 1}, {"id": 2}]` (array of objects with id)',
80
+ '- **Method IDs** (for REST route publishedMethods, availableMethods, skipRoleGuardMethods): GET=1, POST=2, PATCH=3, DELETE=4. Query `method_definition` table if unsure.',
81
+ '- **Wrong:** `"publishedMethods": ["GET"]` or `"publishedMethods": [{"method": "GET"}]` — rejected or silently ignored.',
82
+ '- **Right:** `"publishedMethods": [{"id": 1}]` (publishes GET). Multiple: `[{"id": 1}, {"id": 2}]` (publishes GET + POST).',
83
+ '- **To unset:** pass empty array `"publishedMethods": []`.',
84
+ '',
85
+ '### Auth and publishedMethods (Enfyra server)',
52
86
  '- Each route has **publishedMethods** (which HTTP verbs are “public”) and **routePermissions** (roles/users for protected access).',
53
87
  '- If the **current request method** is listed in **publishedMethods** for that route, the server allows the call **without** a Bearer token (`RoleGuard`).',
54
88
  '- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
@@ -82,10 +116,11 @@ export function buildMcpServerInstructions(apiBaseUrl) {
82
116
  '',
83
117
  '### System tables — which have REST routes',
84
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.',
85
- '- **`column_definition` has NO route** — do NOT call `query_table("column_definition", …)`. It will always 404.',
119
+ '- **`column_definition` and `session_definition` have NO route** — do NOT call `query_table("column_definition", …)` or `query_table("session_definition", …)`. They will 404.',
86
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.',
87
- '- **Tables confirmed to have REST routes (system):** `table_definition`, `route_definition`, `user_definition`, `setting_definition`, `ai_config_definition`, `role_definition`, `menu_definition`, `extension_definition`, `folder_definition`, `file_definition`, `file_permission_definition`, `package_definition`, `bootstrap_script_definition`, `storage_config_definition`, `ai_conversation_definition`, `ai_message_definition`, `websocket_definition`, `websocket_event_definition`, `oauth_config_definition`, `oauth_account_definition`, `method_definition`, `pre_hook_definition`, `post_hook_definition`, `route_handler_definition`, `route_permission_definition`, `flow_definition`, `flow_step_definition`, `flow_execution_definition`.',
88
- '- **Tables without REST routes (internal/system only):** `column_definition`, `relation_definition` these are managed indirectly via cascade on `table_definition` (PATCH /table_definition/{id} with columns/relations array). The `create_column` MCP tool handles this automatically.',
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.',
89
124
  '',
90
125
  '### Body validation & column rules',
91
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.',
@@ -99,7 +134,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
99
134
  '',
100
135
  '### Schema / table migration (sequential only)',
101
136
  '- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
102
- '- Do NOT batch multiple schema changes (e.g. create 3 tables in parallel, or create table + add columns simultaneously). Execute each `create_table`, `create_column`, sync, or drop sequentially; wait for completion before the next.',
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.',
103
138
  '',
104
139
  '### Resolving the real REST path',
105
140
  '- Do **not** assume `route_definition.path` always equals `table_definition.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
@@ -107,13 +142,22 @@ export function buildMcpServerInstructions(apiBaseUrl) {
107
142
  '',
108
143
  '### MongoDB vs SQL primary key',
109
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.',
110
153
  '',
111
154
  '### GraphQL (same prefix as REST / ENFYRA_API_URL)',
112
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\`.`,
113
156
  `- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text); same base pattern as above.`,
114
- '- A table appears in the schema if some **enabled route** for that table has **both** `GQL_QUERY` and `GQL_MUTATION` in `availableMethods` and a **`mainTable`** pointing at the table. The route **`path` does not need to be** `/<table_name>` (custom paths still qualify).',
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.',
115
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).',
116
- '- **Auth:** `publishedMethods` may include `GQL_QUERY` and/or `GQL_MUTATION` **separately** each controls anonymous access for queries vs mutations. Otherwise Bearer JWT + `routePermissions` must list the same method key (`GQL_QUERY` / `GQL_MUTATION`).',
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.',
117
161
  '- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
118
162
  '',
119
163
  '### WebSocket (Socket.IO)',
@@ -135,6 +179,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
135
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**.',
136
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.',
137
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.',
138
184
  '',
139
185
  '### Flows (Automated Workflows)',
140
186
  '- Enfyra supports automated workflows via **`flow_definition`**, **`flow_step_definition`**, and **`flow_execution_definition`** tables.',
@@ -149,7 +195,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
149
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.',
150
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}`.',
151
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.',
152
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.',
153
201
  '',
154
202
  '### Extension (Vue SFC only — NOT React)',
155
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.',
@@ -234,6 +282,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
234
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`.',
235
283
  `- \`get_all_metadata\` → GET \`${base}/metadata\``,
236
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`,
237
288
  `- \`query_table\` → GET \`${base}/<tableName>?…\` (query string from tool args)`,
238
289
  `- \`find_one_record\` (by id) → GET \`${base}/<tableName>?filter=…&limit=1\``,
239
290
  `- \`create_record\` → POST \`${base}/<tableName>\``,
@@ -241,6 +292,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
241
292
  `- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\``,
242
293
  `- \`create_extension\` → POST \`${base}/extension_definition\` (Vue SFC only; for page pass menuId). \`update_record\` on extension_definition to change code.`,
243
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>\``,
244
298
  `- Other: \`${base}/menu_definition\`, \`${base}/websocket_definition\`, \`${base}/admin/reload\`, etc.`,
245
299
  '',
246
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.',
@@ -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` param as JSON array to create table WITH columns in one call (cascade). Only use create_column separately if adding to an existing table later.',
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 (GQL_QUERY / GQL_MUTATION) may also be enabled on the route; that is separate from REST paths above.',
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, isEnabled, columns: columnsJson }) => {
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 = columnsJson ? JSON.parse(columnsJson) : [];
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, isEnabled, columns: [idColumn, ...userColumns] }),
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: ${result.id}).`;
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, uniques, indexes.',
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', 'NO ACTION']).optional().default('SET NULL').describe('On delete behavior.'),
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(({ sourceTable, targetTable, ...rel }) => rel);
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(({ sourceTable, targetTable, ...rel }) => rel);
366
+ .map(normalizeRelationForTablePatch);
330
367
 
331
368
  const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { relations });
332
369