@enfyra/mcp-server 0.0.117 → 0.0.119

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.117",
3
+ "version": "0.0.119",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -586,11 +586,30 @@ update_record({
586
586
  notes: [
587
587
  'Use enfyra_user as the user table.',
588
588
  'Use table ids for targetTable when already known; MCP can also resolve exact table names such as "enfyra_user" before schema mutation.',
589
- 'Do not add inverse relations on enfyra_user unless the user explicitly asks.',
589
+ 'Do not add inverse relations on enfyra_user unless a concrete user-to-record response, UI, or deep query will use it.',
590
+ 'Do not add inverse relations just because a parent table exists. If messages are read by querying chat_message with conversation/member filters, conversation.messages and user.messages are not needed.',
590
591
  'createdAt, updatedAt, and custom date/datetime/timestamp fields already get auto-generated single-field indexes; add only compound indexes needed by hot filters.',
591
592
  'Do not provide physical FK column names; Enfyra derives them.',
592
593
  ],
593
594
  },
595
+ {
596
+ name: 'Add an inverse only for a planned parent child-list query',
597
+ code: `create_relation({
598
+ sourceTableId: "chat_message",
599
+ targetTableId: "chat_conversation",
600
+ propertyName: "conversation",
601
+ inversePropertyName: "messages",
602
+ type: "many-to-one",
603
+ isNullable: false,
604
+ onDelete: "CASCADE"
605
+ })`,
606
+ notes: [
607
+ 'Use inversePropertyName only when the parent table will actually expose, deep-load, count, or sort by that child collection.',
608
+ 'For example, conversation.messages is justified if a conversation detail response loads the latest message page with deep.messages limit/sort, or if a list sorts by _max(messages.createdAt).',
609
+ 'If the app only filters chat_message by conversation.id, omit inversePropertyName and keep the schema one-directional.',
610
+ 'Before creating an inverse, inspect existing relations and state why the reverse traversal is needed.',
611
+ ],
612
+ },
594
613
  {
595
614
  name: 'Add chat_conversation.lastMessage after chat_message exists',
596
615
  code: `update_table({
@@ -1550,7 +1569,7 @@ ensure_page_extension({
1550
1569
  'Treat primary color as runtime-configurable by the app color picker. For Nuxt UI components, choose color="primary" by semantic intent. For custom extension UI, decide whether each element is neutral surface, runtime-primary identity, or status: regular panels/KPI cards/list rows use eapp-surface-card/eapp-surface-muted/eapp-surface-hover/eapp-divide-y and eapp-text-* classes, while selected/current identity blocks, primary tiles, progress fills, primary icons, and primary CTA fills use eapp-primary-surface, eapp-primary-soft, eapp-primary-subtle, eapp-primary-solid, or eapp-primary-text so the color picker controls them.',
1551
1570
  'Decision cases: normal decorative accents, feature icons, non-state tiles, active tabs, progress fills, selected segments, and primary actions use runtime primary/identity classes; true semantic states use their matching status colors such as error, warning, success, or info; large ordinary surfaces stay neutral and carry only small badges/icons for status; selected/current entity blocks may use eapp-primary-surface.',
1552
1571
  'Pattern examples: KPI/metric cards should be eapp-surface-card with a small eapp-primary-soft icon tile; selected/current entity cards may use eapp-primary-surface; progress bars use eapp-surface-muted tracks plus eapp-primary-solid fills; list rows use eapp-surface-card/eapp-divide-y/eapp-surface-hover and only small chips inside; primary scope actions use UButton color="primary" variant="solid"; secondary actions use neutral variants.',
1553
- 'Status colors belong only in UBadge/token-backed badges, small icons, or short status text. Do not color large panels, alert-like success blocks, KPI cards, list containers, or attention/reconciliation blocks green/yellow/red because of state; keep the block neutral and place the status badge/icon inside.',
1572
+ 'Status colors belong only in UBadge/UAlert semantic colors or eapp-status-*-soft/text/border classes for badges, small icons, or short status text. Do not read --badge-* variables directly in extension templates. Do not color large panels, alert-like success blocks, KPI cards, list containers, or attention/reconciliation blocks green/yellow/red because of state; keep the block neutral and place the status badge/icon inside.',
1554
1573
  'Use PageHeader gradient: "none" for generated operational pages unless the user explicitly asks for a decorative page accent; do not hardcode cyan/violet/purple/blue/green gradients.',
1555
1574
  'For general card grids inside the shell, use md:grid-cols-2 xl:grid-cols-3 instead of lg:grid-cols-3 because the desktop sidebar leaves tablet-width content at 1024px.',
1556
1575
  'Do not use Nuxt UI neutral semantic classes such as bg-default, text-muted, text-dimmed, border-default, or divide-default inside extension code; use eApp class tokens instead. Do not write text-[var(...)], bg-[var(...)], or border-[var(...)] in generated extension templates unless no class token exists for that exact primitive.',
@@ -27,7 +27,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
27
27
  '- For a quick target/base sanity check, call `get_enfyra_api_context`; do not call broad discovery just to confirm which instance this MCP is connected to.',
28
28
  '- Discover before deciding. For architecture/capability questions call `discover_enfyra_system`; for DB/pk/runtime/cache context call `discover_runtime_context`; for filters/deep/sort/relation query shape call `discover_query_capabilities`. Run broad discovery tools sequentially, not in parallel.',
29
29
  '- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
30
- '- Load examples only when needed. Before generating schemas, app connection code, OAuth, Socket.IO, handlers/hooks, flows, files, guards, permissions, or extensions, call `get_enfyra_examples` with the matching category. Before extension UI work, call `get_extension_theme_contract` and follow the app-shell/theme/security contract.',
30
+ '- Load examples only when needed. Before generating schemas, app connection code, OAuth, Socket.IO, handlers/hooks, flows, files, guards, permissions, or extensions, call `get_enfyra_examples` with the matching category. Before extension UI work, call `get_extension_theme_contract` and follow the app-shell/theme/security contract. For the exact class name or Nuxt UI color mapping, call `get_theme_class_reference`; this is the authoritative theme & color contract and must be followed exactly - never use raw CSS variables, Tailwind palette accents, or concrete palettes like color="violet".',
31
31
  '- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
32
32
  '- With non-root API tokens, call `get_permission_profile` before relying on admin helper tools or when debugging 403s. MCP admin helpers require ordinary route permissions for static admin routes such as `/admin/script/validate`, `/admin/test/run`, `/admin/flow/trigger/:id`, and `/admin/reload/*`.',
33
33
  '- Prefer the most specific business operation tool over raw metadata CRUD: `api_endpoint_workflow` for step-by-step endpoint work; `create_api_endpoint` only when a one-shot endpoint operation is clearly safe; route tools such as `enable_route`, `disable_route`, `delete_route`, `add_route_methods`, `public_route_methods`, and `private_route_methods`; `set_table_graphql`; `ensure_guard`; permission/rule tools; websocket tools; flow tools such as `ensure_manual_flow`, `ensure_scheduled_flow`, `choose_flow_step_tool`, and fixed-type flow step tools; and extension tools such as `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension`.',
@@ -42,6 +42,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
42
42
  '- Mutations return ids/status by default. Re-read with `find_one_record` or `query_table` and explicit `fields` when the saved row matters.',
43
43
  '- Dynamic repository reads use `filter`, not `where`: `@REPOS.table.find({ filter: {...} })`, `#table.find({ filter: {...} })`, and `exists(filter)`.',
44
44
  '- Use `enfyra_user` as the user table. Model record links as real relations using relation `propertyName` values, not physical FK fields like `userId`, `conversationId`, `senderId`, or `memberId` in generated DB code.',
45
+ '- Relation design must stay minimal. Create the owning relation needed for writes/filters first; add `inversePropertyName` only when a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal will use that reverse field. For schema work, explicitly review existing relations and mention which inverses are intentionally present or intentionally omitted.',
45
46
  '- Do not call internal/no-route system tables such as `enfyra_column` or `enfyra_session` through generic CRUD. Use table/column/relation tools and route-backed tables discovered from metadata.',
46
47
  '- Custom API paths use `api_endpoint_workflow` when a handler is needed and the model should follow returned nextSteps. Use lower-level `create_route` without `mainTableId` only when intentionally creating a route shell; `create_table` is only for new persisted data.',
47
48
  '- For canonical table reads and RLS, preserve client-controlled query shape: do not override `@QUERY.fields`, `@QUERY.deep`, `@QUERY.sort`, `@QUERY.limit`, `@QUERY.page`, `@QUERY.meta`, `@QUERY.aggregate`, or `debugMode`. Merge only security filters into `@QUERY.filter`.',
@@ -305,7 +305,7 @@ function getExtensionThemeContract() {
305
305
  'Primary color is runtime-configurable through the app color picker and must affect extension identity UI. For Nuxt UI components, choose color="primary" by semantic intent and let the app map it through the primary contract; do not choose a concrete palette. For custom extension UI, first choose whether the element is neutral surface, runtime-primary identity, or status. Regular panels, KPI cards, list rows, and large content blocks should use eapp-surface-card, eapp-surface-muted, eapp-surface-flat, eapp-surface-hover, eapp-divide-y, and eapp-text-* classes. Entity identity, selected/current state, active progress, primary tiles, primary icons, and primary CTA fills should use eapp-primary-surface, eapp-primary-soft, eapp-primary-subtle, eapp-primary-solid, eapp-primary-text, eapp-primary-border, or eapp-primary-ring so the color picker controls them.',
306
306
  'Use eapp-primary-surface only for larger entity/feature blocks, selected/current cards, tiles, or cards that should read like normal app cards with a very subtle active-primary tint; it is not a saturated selected-state fill and must not be applied broadly to every KPI/list wrapper. Add eapp-primary-surface-hover when that block is clickable. Use eapp-primary-soft for compact selected entity chips, pills, small icon tiles, and identity callouts; add eapp-primary-soft-hover when compact surfaces are clickable; use eapp-primary-subtle for a slightly stronger selected fill; use eapp-primary-solid only for primary identity fills; use eapp-primary-text for identity icons or inline text. eapp-identity-* remains an alias for the same runtime-primary intent, but eapp-primary-* is preferred in new extension code.',
307
307
  'Nuxt UI secondary is still a valid semantic color when the product intentionally wants a secondary action or state. Do not use color="secondary", from-secondary-*, bg-secondary-*, text-secondary-*, or cyan/purple/green palette utilities merely to approximate an entity accent; use eapp-primary-* and let the app decide the color.',
308
- 'Do not make custom extension blocks with Tailwind palette utilities, raw CSS variable utilities, or raw primary utilities such as text-[var(--*)], bg-[var(--*)], border-[var(--*)], text-primary, bg-primary/10, border-primary, ring-primary/20, from-cyan-*, text-violet-*, bg-green-*, bg-emerald-*, text-green-*, border-green-*, or dark:bg-zinc-950. The app owns how eapp-primary-* maps to the active color picker value.',
308
+ 'The app runs on Tailwind v4. Short Tailwind color utilities are the canonical way to apply contract colors and ARE allowed: bg-primary, text-primary, border-primary, ring-primary, bg-success, text-error, etc., including opacity modifiers (bg-primary/10, ring-success/20) which v4 resolves via color-mix. They are generated from the token-backed config (primary -> --md-primary runtime, success/error/warning/info -> --st-* status, secondary -> --md-tertiary) so they follow the color picker and dark theme. Do NOT use raw CSS-variable utilities (text-[var(--*)], bg-[var(--*)], border-[var(--*)]), hardcoded hex, inline style colors, or concrete palette substitution (color="violet", from-cyan-*, text-violet-*, bg-green-*, bg-emerald-*, text-green-*, dark:bg-zinc-950). For intent surfaces with no Tailwind equivalent (selected identity block, soft/solid/subtle surface, divider, radius, modal chrome) use the eapp-* classes below; the app owns how they map to the active color picker value.',
309
309
  'Use UButton color="primary" only for the single main action for the current scope. Refresh, back, navigation, filters, and secondary actions should be neutral variants unless they are the main mutation.',
310
310
  'PageHeader gradient must be "none" for generated operational extensions unless the user explicitly asks for a decorative page accent. Do not hardcode cyan, violet, purple, blue, or green PageHeader gradients to force color variety.',
311
311
  'Do not inject global CSS, create theme guards, redefine the app palette, or solve one extension by overriding the whole app shell.',
@@ -313,7 +313,7 @@ function getExtensionThemeContract() {
313
313
  'Never use Nuxt UI neutral semantic classes such as bg-default, bg-muted, border-default, divide-default, text-muted, text-dimmed, or hardcoded dark palettes such as dark:bg-zinc-950, bg-slate-*, text-gray-*, border-black, or black.',
314
314
  'Never use bare border/divide-y for panels or rows: pair borders with eapp-divider or use eapp-divide-y for row separators.',
315
315
  'Use radius tokens or mapped rounded utilities consistently: --radius-card for cards, --radius-panel for nested panels, --radius-control for buttons/inputs, --radius-subcontrol for compact inner controls, and --radius-pill for pills.',
316
- 'Status colors must remain readable in both themes and must stay scoped to badges, small icons, or short status text. Use UBadge or badge tokens for non-primary statuses such as --badge-success-soft-*, --badge-warning-soft-*, --badge-danger-soft-*, --badge-info-soft-*, and --badge-neutral-soft-* instead of neon translucent colors. Do not color large panels, alert-like success blocks, KPI cards, list containers, or reconciliation/attention blocks green/yellow/red because the status is good/warning/error; use neutral app surfaces for the block and place a small status badge/icon inside.',
316
+ 'Status colors must remain readable in both themes and must stay scoped to badges, small icons, or short status text. Use UBadge/UAlert semantic colors or eapp-status-success-soft/text/border, eapp-status-warning-soft/text/border, eapp-status-danger-soft/text/border, eapp-status-info-soft/text/border, and eapp-status-neutral-soft/text/border. Do not read --badge-* variables directly from extension templates. Do not color large panels, alert-like success blocks, KPI cards, list containers, or reconciliation/attention blocks green/yellow/red because the status is good/warning/error; use neutral app surfaces for the block and place a small status badge/icon inside.',
317
317
  'Keep dark and light contrast comparable. Do not make dark mode more neon or lower-contrast than light mode; prefer muted soft backgrounds with clear text and visible borders.',
318
318
  ],
319
319
  decisionCases: [
@@ -326,7 +326,7 @@ function getExtensionThemeContract() {
326
326
  {
327
327
  intent: 'True semantic state: error, danger, destructive, warning, pending attention, success, healthy, running, failed, info, or notice.',
328
328
  colorContract: 'Use the matching state/status color because the color communicates meaning, not brand identity.',
329
- use: 'UAlert/UBadge color="error|warning|success|info|neutral", state/badge tokens such as --badge-success-soft-*, or error/warning/info/success utilities that are already backed by app tokens.',
329
+ use: 'UAlert/UBadge color="error|warning|success|info|neutral", or eapp-status-success|warning|danger|info|neutral soft/text/border classes for custom compact status chips/icons.',
330
330
  avoid: 'Do not force semantic state UI to primary; an error must stay error, a warning must stay warning, success can stay success when it is a badge/icon/short status.',
331
331
  },
332
332
  {
@@ -399,6 +399,68 @@ function getExtensionThemeContract() {
399
399
  },
400
400
  ],
401
401
  compactExample: '<template><section class="min-h-full w-full space-y-4"><article class="eapp-surface-card p-4"><div class="flex items-start justify-between gap-3"><div><p class="text-sm eapp-text-tertiary">Neutral KPI</p><p class="mt-2 text-2xl font-semibold eapp-text-primary">24</p></div><span class="eapp-primary-soft eapp-radius-control p-2"><UIcon name="lucide:square-stack" class="size-5 eapp-primary-text" /></span></div><div class="mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted"><div class="eapp-primary-solid h-full w-1/2"></div></div></article><section class="eapp-surface-card p-4"><div class="flex items-center justify-between gap-3"><p class="font-semibold eapp-text-primary">Status block stays neutral</p><UBadge color="success" variant="soft">Healthy</UBadge></div></section></section></template>',
402
+ contractAuthority: [
403
+ 'This is the authoritative Enfyra theme & color contract. Source of truth: documents/app/theme-color-contract.md. The app owns color via three files only: app/assets/css/theme.css (variables + Nuxt UI mapping), tailwind.config.js (single token source for Tailwind v4 semantic colors - all token-backed, no hardcoded hex), app/app.config.ts (Nuxt UI component mapping). Pages and extensions only CONSUME classes/Nuxt UI props; they never define colors.',
404
+ 'Every color flows from two base layers: --md-* (Material, runtime primary picker) and --st-* (status). The app runs on Tailwind v4, so short utilities (bg-primary, text-success, bg-primary/10, ring-error/20) are canonical and resolve through the token-backed config via color-mix. All Nuxt UI semantic colors (primary/secondary/success/warning/error/info/neutral) are re-pointed to these, so Nuxt UI is used per its docs but colors are decided by Enfyra. This applies to the shell, system pages, and compiled dynamic extensions.',
405
+ 'Call get_theme_class_reference for the full class->variable->Nuxt UI table when you need the exact class name or variable.',
406
+ ],
407
+ classReference: {
408
+ surfaces: ['eapp-surface-card (default card; --card-bg/--card-border)', 'eapp-surface-muted (recessed/track; --surface-muted)', 'eapp-surface-flat (flush; --surface-default)', 'eapp-surface-hover (clickable row hover)'],
409
+ text: ['eapp-text-primary', 'eapp-text-secondary', 'eapp-text-tertiary', 'eapp-text-quaternary'],
410
+ primaryIdentity: ['eapp-primary-solid (solid fill/meter)', 'eapp-primary-text (inline/icon)', 'eapp-primary-soft + -hover (compact chip/tile)', 'eapp-primary-subtle (stronger selected fill)', 'eapp-primary-surface + -hover (large selected identity block)', 'eapp-primary-border', 'eapp-primary-ring'],
411
+ status: ['eapp-status-success|warning|danger|info|neutral -soft/-text/-border (badges/small icons/short text only)'],
412
+ radius: ['eapp-radius-card', 'eapp-radius-panel', 'eapp-radius-control', 'eapp-radius-subcontrol', 'eapp-radius-pill'],
413
+ dividers: ['eapp-divider', 'eapp-divide-y'],
414
+ modal: ['eapp-modal-surface (modal content chrome; never surface-card)'],
415
+ nuxtUiMapping: 'primary=--md-primary(runtime), secondary=--md-tertiary(runtime), success=--st-success, warning=--st-warning, error=--st-error, info=--st-info, neutral=neutral surfaces',
416
+ },
417
+ };
418
+ }
419
+
420
+ function getThemeClassReference() {
421
+ return {
422
+ action: 'theme_class_reference',
423
+ authority: 'Authoritative Enfyra theme & color contract. Source of truth: documents/app/theme-color-contract.md. App owns color via theme.css + main.css + app.config.ts only; pages/extensions consume classes and Nuxt UI props.',
424
+ baseLayers: {
425
+ material: '--md-* (runtime primary picker, HCT/Material You). Drives identity/brand. Never read directly in templates.',
426
+ status: '--st-success/--st-warning/--st-error/--st-info. Fixed semantic palette. Never read directly in templates.',
427
+ },
428
+ nuxtUiColors: {
429
+ primary: 'runtime --md-primary (main brand action/identity). NEVER substitute a concrete palette.',
430
+ secondary: 'runtime --md-tertiary (intentional secondary accent only).',
431
+ success: '--st-success (healthy/success).',
432
+ warning: '--st-warning (pending/attention).',
433
+ error: '--st-error (destructive/error).',
434
+ info: '--st-info (informational).',
435
+ neutral: 'neutral surfaces (secondary chrome, non-actions).',
436
+ },
437
+ classes: [
438
+ { group: 'Surfaces (large ordinary - keep neutral)', classes: 'eapp-surface-card, eapp-surface-muted, eapp-surface-flat, eapp-surface-hover' },
439
+ { group: 'Text', classes: 'eapp-text-primary, eapp-text-secondary, eapp-text-tertiary, eapp-text-quaternary' },
440
+ { group: 'Runtime primary identity', classes: 'eapp-primary-solid, eapp-primary-text, eapp-primary-soft(+hover), eapp-primary-subtle, eapp-primary-surface(+hover), eapp-primary-border, eapp-primary-ring' },
441
+ { group: 'Status (badges/small icons/short text only)', classes: 'eapp-status-{success|warning|danger|info|neutral}-{soft|text|border}' },
442
+ { group: 'Radius', classes: 'eapp-radius-card, eapp-radius-panel, eapp-radius-control, eapp-radius-subcontrol, eapp-radius-pill' },
443
+ { group: 'Dividers', classes: 'eapp-divider, eapp-divide-y' },
444
+ { group: 'Modal', classes: 'eapp-modal-surface (never surface-card as modal ui.content)' },
445
+ ],
446
+ forbidden: [
447
+ 'Raw CSS variables in templates: text-[var(--*)], bg-[var(--*)], border-[var(--*)].',
448
+ 'Tailwind palette accents: from-cyan-*, text-violet-*, bg-green-*, bg-emerald-*, text-gray-*, bg-slate-*, dark:bg-zinc-950.',
449
+ 'Concrete palette substitution (color="violet"/"cyan"/..., from-cyan-*, text-violet-*, bg-green-*, bg-emerald-*, dark:bg-zinc-950).',
450
+ 'Hardcoded hex colors or inline style="color:#..." for theme-driven surfaces.',
451
+ 'Reading --md-* / --st-* / --badge-* base variables directly from extension templates.',
452
+ ],
453
+ allowedShortUtilities: [
454
+ 'Tailwind v4 short utilities ARE canonical and preferred: bg-primary, text-primary, border-primary, ring-primary, bg-success, text-error, bg-warning, text-info, bg-secondary.',
455
+ 'Opacity modifiers work natively via v4 color-mix: bg-primary/10, ring-primary/20, text-primary/70, bg-success/15.',
456
+ 'Use eapp-* classes only for intent surfaces with no Tailwind equivalent (eapp-primary-surface/solid/soft/subtle, eapp-surface-card/muted/flat/hover, eapp-divider/divide-y, eapp-radius-*, eapp-modal-surface).',
457
+ ],
458
+ chooseByIntent: [
459
+ 'Normal accent / active tab / progress / primary CTA -> primary (eapp-primary-* or color="primary").',
460
+ 'True semantic state -> status (eapp-status-* or color="success|warning|error|info").',
461
+ 'Large ordinary surface -> eapp-surface-*; put a small status badge inside.',
462
+ 'Whole block is active identity -> eapp-primary-surface (+hover), subtle only.',
463
+ ],
402
464
  };
403
465
  }
404
466
 
@@ -1114,6 +1176,17 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1114
1176
  async () => jsonText(getExtensionThemeContract()),
1115
1177
  );
1116
1178
 
1179
+ server.tool(
1180
+ 'get_theme_class_reference',
1181
+ [
1182
+ 'Return the authoritative Enfyra theme & color class reference: class -> CSS variable -> Nuxt UI semantic color -> intent.',
1183
+ 'Call this whenever you need the exact eapp-* class name or the Nuxt UI color mapping for shell, system page, or dynamic extension UI.',
1184
+ 'Source of truth: documents/app/theme-color-contract.md.',
1185
+ ].join(' '),
1186
+ {},
1187
+ async () => jsonText(getThemeClassReference()),
1188
+ );
1189
+
1117
1190
  server.tool(
1118
1191
  'set_table_graphql',
1119
1192
  'Business operation: enable or disable GraphQL for one table through enfyra_graphql, then reload GraphQL. REST route methods do not control GraphQL.',
@@ -465,7 +465,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
465
465
  targetTableId: z.string().describe('Target table id, exact table name, or alias. MCP resolves names/aliases to ids before mutation.'),
466
466
  type: z.enum(['many-to-one', 'one-to-many', 'one-to-one', 'many-to-many']).describe('Relation type.'),
467
467
  propertyName: z.string().describe('Property name on source table (e.g., "customer", "items").'),
468
- inversePropertyName: z.string().optional().describe('Property name on target table for bidirectional relation (e.g., "orders"). Omit unless the reverse field is truly needed.'),
468
+ inversePropertyName: z.string().optional().describe('Property name on target table for bidirectional relation (e.g., "orders"). Omit unless a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal will use the reverse field. Do not add inverses merely because a parent table exists.'),
469
469
  mappedBy: z.string().optional().describe('Mapped-by property for inverse relation shapes when required by the backend. Do not use physical FK names.'),
470
470
  isNullable: z.boolean().optional().default(true).describe('Whether the relation is nullable.'),
471
471
  onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT']).optional().default('SET NULL').describe('On delete behavior.'),
@@ -546,7 +546,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
546
546
  '**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).',
547
547
  '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.',
548
548
  '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.',
549
- '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, {id}, or an exact table name that MCP resolves to an id before mutation.',
549
+ '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, {id}, or an exact table name that MCP resolves to an id before mutation. Omit inversePropertyName unless a concrete parent-to-child query or UI surface needs it.',
550
550
  'Do NOT provide physical FK/junction columns. Never include fkCol, fkColumn, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn. Enfyra derives and hides those physical columns from relation propertyName/table metadata.',
551
551
  'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
552
552
  'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
@@ -562,7 +562,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
562
562
  description: z.string().optional().describe('Description of what this table stores.'),
563
563
  isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to enfyra_table create.'),
564
564
  columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, isPublished?, isUpdatable?, isEncrypted?, defaultValue?, description?, options? }. Set isEncrypted=true for values encrypted at rest; set isUpdatable=false separately only when the field should be immutable. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"api_key","type":"varchar","isEncrypted":true,"isPublished":false}]'),
565
- relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id, {"id": <id>}, or an exact table name that MCP resolves to an id before mutation. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
565
+ relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id, {"id": <id>}, or an exact table name that MCP resolves to an id before mutation. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Omit inversePropertyName unless a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal needs the reverse field. Example only when parent posts are queried: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
566
566
  indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["member","isRead","conversation"],["conversation","member","isRead"]]'),
567
567
  uniques: z.string().optional().describe('JSON array of logical unique field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Example: [["message","member"]]'),
568
568
  },
@@ -888,7 +888,7 @@ server.tool(
888
888
  relations: routeTables.has('enfyra_relation')
889
889
  ? 'enfyra_relation has a REST route for reads/metadata, but canonical schema migration is create_relation/delete_relation or enfyra_table PATCH with the full relations array. Relation onDelete accepts CASCADE, SET NULL, or RESTRICT.'
890
890
  : 'Use create_relation/delete_relation or enfyra_table PATCH with the full relations array. Relation onDelete accepts CASCADE, SET NULL, or RESTRICT.',
891
- relationCascadeFkContract: 'Do not ask for or send physical FK/junction column names in relation create/update payloads. Enfyra derives fk/junction columns from relation propertyName/table metadata and hides FK columns from app schema/forms. Use targetTable, type, propertyName, inversePropertyName or mappedBy, isNullable, onDelete.',
891
+ relationCascadeFkContract: 'Do not ask for or send physical FK/junction column names in relation create/update payloads. Enfyra derives fk/junction columns from relation propertyName/table metadata and hides FK columns from app schema/forms. Use targetTable, type, propertyName, inversePropertyName or mappedBy, isNullable, onDelete. Add inversePropertyName only when a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal will use the reverse field.',
892
892
  tableDefinitionRelations: (tableDefinition?.relations || []).map((rel) => rel.propertyName),
893
893
  relationDefinitionRelations: (relationTable?.relations || []).map((rel) => rel.propertyName),
894
894
  },
@@ -1068,7 +1068,7 @@ server.tool(
1068
1068
  ? 'Use this table metadata primary column when available.'
1069
1069
  : 'SQL commonly uses id; Mongo uses _id. Use table metadata primary column when available.',
1070
1070
  relationNames: 'API relation operations use relation propertyName, not physical FK column names.',
1071
- relationCascadeFkContract: 'When creating relations through create_table/create_relation/enfyra_table PATCH, never provide fkCol/fkColumn/foreignKeyColumn/sourceColumn/targetColumn/junction*Column. These are physical implementation details derived by Enfyra and hidden from app schema/forms.',
1071
+ relationCascadeFkContract: 'When creating relations through create_table/create_relation/enfyra_table PATCH, never provide fkCol/fkColumn/foreignKeyColumn/sourceColumn/targetColumn/junction*Column. These are physical implementation details derived by Enfyra and hidden from app schema/forms. Add inversePropertyName only for a concrete reverse traversal such as parent deep child lists, response fields, UI sections, or aggregate sort/count.',
1072
1072
  graphql: 'GraphQL query args also accept filter/sort/page/limit. Table data requires Bearer auth and table enablement via enfyra_graphql; anonymous root/schema probes may still return 200.',
1073
1073
  },
1074
1074
  table: tableName
@@ -2088,7 +2088,7 @@ server.tool(
2088
2088
  fields: 'Use column names and relation propertyName values.',
2089
2089
  filter: 'Use query DSL operators on column names or nested relation propertyName objects.',
2090
2090
  deep: 'Deep fetch keys are relation propertyName values.',
2091
- relationMutation: 'For relation schema creation/update use targetTable/type/propertyName/inversePropertyName|mappedBy/isNullable/onDelete only. Do not provide physical FK/junction columns; Enfyra derives and hides them.',
2091
+ relationMutation: 'For relation schema creation/update use targetTable/type/propertyName/inversePropertyName|mappedBy/isNullable/onDelete only. Do not provide physical FK/junction columns; Enfyra derives and hides them. Omit inversePropertyName unless a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal needs it.',
2092
2092
  },
2093
2093
  };
2094
2094