@enfyra/mcp-server 0.1.6 → 0.1.7

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.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1597,26 +1597,14 @@ ensure_page_extension({
1597
1597
  'Use enfyra_menu.label, not title.',
1598
1598
  'Sensitive admin menus should include a permission condition at creation time.',
1599
1599
  'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
1600
- 'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI.',
1600
+ 'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI; that tool is the authority for theme, color, layout, modal, drawer, and shell registry details.',
1601
1601
  'Call get_enfyra_required_knowledge before saving extension code, pass globalRulesAckKey as globalRulesAckKey, and pass extensionAckKey as extensionKnowledgeAckKey.',
1602
1602
  'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
1603
- 'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
1604
- 'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
1605
1603
  'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry, destructure register first, then call it with one action or an array.',
1606
- 'Page extensions should be full-bleed by default and responsive from the first version.',
1607
- 'The extension root is already inside Enfyra admin page main; do not add root-level page padding.',
1608
- 'Use eApp theme class tokens for panels, rows, badges, borders, controls, radius, and text. Generated extension templates should prefer eapp-surface-*, eapp-text-*, eapp-radius-*, eapp-divide-y, and eapp-primary-* over raw CSS variable utilities.',
1609
- '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. eapp-primary-surface supplies selected identity color but does not replace card chrome; keep border/radius classes on selected blocks.',
1610
- '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.',
1611
- 'Pattern examples: KPI/metric cards should be eapp-surface-card with a small icon tile using eapp-primary-soft eapp-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.',
1612
- '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.',
1613
- '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.',
1614
- '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.',
1615
- '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.',
1616
- 'Do not pass ui.content: "eapp-surface-card" to UModal/CommonModal; modal content uses the app modal surface and caller content classes should only append z-index or width.',
1617
- 'CommonModal and CommonDrawer own action-only footers through cancelAction, primaryAction, dangerAction, leadingActions, and footerHint. Pass action intent through props instead of styling footer buttons manually; cancelAction defaults to neutral outline, dangerAction is for irreversible destructive work, and Keep editing should use tone: "primary" in discard dialogs.',
1618
- 'Use UTabs for page sections instead of custom tab bars so the app-level active/inactive indicators, spacing, focus rings, and theme contrast stay consistent.',
1619
- 'Do not inject global CSS, create theme guards, redefine the app palette, or solve one extension by overriding the whole app shell.',
1604
+ 'Page extensions should be full-bleed and responsive from the first version; the extension root is already inside the Enfyra admin page main.',
1605
+ 'Render ordinary metrics and lists in the body, not PageHeader.stats, unless the user explicitly wants a compact overview header.',
1606
+ 'Use app theme tokens and Nuxt UI semantic colors by intent; do not hard-code concrete palettes or redefine the app palette inside extension code.',
1607
+ 'Use app-owned primitives such as UTabs, CommonModal, CommonDrawer, Widget, useMenuNotificationRegistry, and useAccountPanelRegistry when the workflow matches them.',
1620
1608
  'Keep list selection local and fetch detail rows only; do not refetch the whole list after a row click unless the list data changed.',
1621
1609
  'Page extension paths are admin app UI routes. Do not verify them with test_rest_endpoint against ENFYRA_API_URL unless inspect_route shows an API route with the same path.',
1622
1610
  'After saving, open Enfyra admin tabs should update through the server/Enfyra admin UI realtime reload contract; do not tell the user to refresh unless that contract is proven broken.',
@@ -1441,7 +1441,8 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1441
1441
  'create_api_endpoint',
1442
1442
  [
1443
1443
  'Business operation: create or update a custom REST endpoint with a handler in one safe operation.',
1444
- 'Use this when the user asks for a new route/endpoint/API path that computes or orchestrates behavior, such as GET /sum or POST /webhook.',
1444
+ 'Prefer api_endpoint_workflow when route access, role/user permissions, overwrite decisions, or multi-step planning matter.',
1445
+ 'Use this one-shot helper only when the endpoint contract is already clear and no authenticated route-permission step is needed in the same operation, such as a simple public webhook or private admin-only utility that will be granted separately.',
1445
1446
  'It creates the route without mainTableId, ensures the method is available, validates sourceCode, creates or overwrites the route handler, optionally makes the method public, reloads routes, and can smoke-test the endpoint.',
1446
1447
  'Use table/schema tools separately when the user needs persisted data. This tool is for custom behavior endpoints.',
1447
1448
  ].join(' '),
@@ -1591,8 +1592,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1591
1592
  message: z.string().optional().describe('Custom validation error message.'),
1592
1593
  description: z.string().optional().describe('Admin note.'),
1593
1594
  isEnabled: z.boolean().optional().default(true).describe('Enable the rule.'),
1595
+ globalRulesAckKey: globalRulesAckParam(z),
1594
1596
  },
1595
- async ({ tableName, columnName, ruleType, value, message, description, isEnabled }) => {
1597
+ async ({ tableName, columnName, ruleType, value, message, description, isEnabled, globalRulesAckKey }) => {
1598
+ assertGlobalRulesAck(globalRulesAckKey);
1596
1599
  const table = resolveTable(await getMetadataTables(ENFYRA_API_URL), tableName);
1597
1600
  const column = resolveColumn(table, columnName);
1598
1601
  const existing = await findRecord(ENFYRA_API_URL, 'enfyra_column_rule', {
@@ -1632,8 +1635,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1632
1635
  condition: z.string().optional().describe('Condition JSON object using field permission DSL.'),
1633
1636
  description: z.string().optional().describe('Admin note.'),
1634
1637
  isEnabled: z.boolean().optional().default(true).describe('Enable the permission.'),
1638
+ globalRulesAckKey: globalRulesAckParam(z),
1635
1639
  },
1636
- async ({ tableName, columnName, relationName, action, effect, roleId, roleName, allowedUserIds, condition, description, isEnabled }) => {
1640
+ async ({ tableName, columnName, relationName, action, effect, roleId, roleName, allowedUserIds, condition, description, isEnabled, globalRulesAckKey }) => {
1641
+ assertGlobalRulesAck(globalRulesAckKey);
1637
1642
  if (!!columnName === !!relationName) throw new Error('Provide exactly one of columnName or relationName.');
1638
1643
  assertOneScope({ roleId, roleName, allowedUserIds });
1639
1644
  const [tables, role] = await Promise.all([
@@ -1691,8 +1696,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1691
1696
  description: z.string().optional().describe('Admin note.'),
1692
1697
  rules: z.string().optional().describe('Rules JSON array: [{type, config, priority, isEnabled, description, userIds}].'),
1693
1698
  rulesMode: z.enum(['append', 'replace', 'none']).optional().default('append').describe('append creates rules, replace disables existing rules first, none leaves rules unchanged.'),
1699
+ globalRulesAckKey: globalRulesAckParam(z),
1694
1700
  },
1695
- async ({ name, guardId, position, routeId, path, methods, combinator, priority, isGlobal, isEnabled, description, rules, rulesMode }) => {
1701
+ async ({ name, guardId, position, routeId, path, methods, combinator, priority, isGlobal, isEnabled, description, rules, rulesMode, globalRulesAckKey }) => {
1702
+ assertGlobalRulesAck(globalRulesAckKey);
1696
1703
  if (path && routeId) throw new Error('Provide path or routeId, not both.');
1697
1704
  const ruleInputs = parseJsonArrayArg('rules', rules, []);
1698
1705
  if (position === 'pre_auth') {
@@ -49,7 +49,7 @@ export function assertExtensionKnowledgeAckIf(condition, key) {
49
49
  export function buildRequiredKnowledgePayload() {
50
50
  return {
51
51
  version: REQUIRED_KNOWLEDGE_VERSION,
52
- purpose: 'Read this before mutating Enfyra metadata, schema, routes, permissions, dynamic server code, or extension UI through MCP.',
52
+ purpose: 'Read this before mutating Enfyra metadata, schema, routes, permissions, menus, packages, cache state, dynamic server code, or extension UI through MCP.',
53
53
  globalRulesAckKey: GLOBAL_RULES_ACK_KEY,
54
54
  dynamicCodeAckKey: DYNAMIC_CODE_KNOWLEDGE_ACK_KEY,
55
55
  extensionAckKey: EXTENSION_KNOWLEDGE_ACK_KEY,
@@ -313,7 +313,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
313
313
  return withSchemaQueue(async () => {
314
314
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, args.tableId);
315
315
  if (!tableData) {
316
- return { content: [{ type: 'text', text: `Error: Table with ID ${args.tableId} not found.` }] };
316
+ throw new Error(`Table with ID ${args.tableId} not found.`);
317
317
  }
318
318
 
319
319
  const existingColumns = getPatchableColumns(tableData.columns);
@@ -325,9 +325,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
325
325
  columnName: args.name,
326
326
  });
327
327
 
328
- return {
329
- content: [{ type: 'text', text: `Column "${args.name}" added to table ${args.tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
330
- };
328
+ return jsonContent({
329
+ action: 'column_created',
330
+ tableId: args.tableId,
331
+ columnName: args.name,
332
+ result,
333
+ });
331
334
  });
332
335
  }
333
336
 
@@ -341,7 +344,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
341
344
  const resolvedTargetTableId = resolveTableIdentifierFromMetadata(metadata, targetTableId, 'targetTableId');
342
345
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, resolvedSourceTableId);
343
346
  if (!tableData) {
344
- return { content: [{ type: 'text', text: `Error: Table ${sourceTableId} not found.` }] };
347
+ throw new Error(`Table ${sourceTableId} not found.`);
345
348
  }
346
349
  const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
347
350
  const beforeIds = existingRelations.map((relation) => String(getId(relation))).filter((id) => id !== 'null');
@@ -356,9 +359,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
356
359
  action: 'create',
357
360
  propertyName,
358
361
  });
359
- return {
360
- content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${resolvedSourceTableId} → ${resolvedTargetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
361
- };
362
+ return jsonContent({
363
+ action: 'relation_created',
364
+ relation: { propertyName, type, sourceTableId: resolvedSourceTableId, targetTableId: resolvedTargetTableId },
365
+ result,
366
+ });
362
367
  });
363
368
  }
364
369
 
@@ -366,7 +371,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
366
371
  return withSchemaQueue(async () => {
367
372
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
368
373
  if (!tableData) {
369
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
374
+ throw new Error(`Table with ID ${tableId} not found.`);
370
375
  }
371
376
 
372
377
  const existingColumns = getPatchableColumns(tableData.columns);
@@ -399,9 +404,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
399
404
  columnId,
400
405
  });
401
406
 
402
- return {
403
- content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
404
- };
407
+ return jsonContent({
408
+ action: 'column_deleted',
409
+ tableId,
410
+ columnId,
411
+ result,
412
+ });
405
413
  });
406
414
  }
407
415
 
@@ -409,7 +417,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
409
417
  return withSchemaQueue(async () => {
410
418
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
411
419
  if (!tableData) {
412
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
420
+ throw new Error(`Table with ID ${tableId} not found.`);
413
421
  }
414
422
 
415
423
  const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
@@ -442,9 +450,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
442
450
  relationId,
443
451
  });
444
452
 
445
- return {
446
- content: [{ type: 'text', text: `Relation ${relationId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
447
- };
453
+ return jsonContent({
454
+ action: 'relation_deleted',
455
+ tableId,
456
+ relationId,
457
+ result,
458
+ });
448
459
  });
449
460
  }
450
461
 
@@ -613,9 +624,25 @@ export function registerTableTools(server, ENFYRA_API_URL) {
613
624
  indexes.length ? `Index group(s): ${indexes.length}.` : null,
614
625
  uniques.length ? `Unique group(s): ${uniques.length}.` : null,
615
626
  ].filter(Boolean).join(' ');
616
- return {
617
- content: [{ type: 'text', text: `${colHint}\n${relHint}${constraintHint ? `\n${constraintHint}` : ''}\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
618
- };
627
+ return jsonContent({
628
+ action: 'table_created',
629
+ table: { id: createdTableId, name, routePath },
630
+ summary: {
631
+ columnCount: userColumns.length + 1,
632
+ createdColumnCount: userColumns.length,
633
+ relationCount: userRelations.length,
634
+ indexGroupCount: indexes.length,
635
+ uniqueGroupCount: uniques.length,
636
+ },
637
+ rest: {
638
+ base,
639
+ routePath,
640
+ operations: ['GET /<table>', 'POST /<table>', 'PATCH /<table>/:id', 'DELETE /<table>/:id'],
641
+ noGetById: true,
642
+ },
643
+ message: [colHint, relHint, constraintHint, restHint].filter(Boolean).join('\n'),
644
+ result,
645
+ });
619
646
  })
620
647
  );
621
648
 
@@ -652,9 +679,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
652
679
  if (uniquesJson !== undefined) body.uniques = normalizeConstraintGroups('uniques', parseJsonArrayParam('uniques', uniquesJson));
653
680
 
654
681
  const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, body);
655
- return {
656
- content: [{ type: 'text', text: `Table ${tableId} updated.\n\n${JSON.stringify(result, null, 2)}` }],
657
- };
682
+ return jsonContent({
683
+ action: 'table_updated',
684
+ tableId,
685
+ result,
686
+ });
658
687
  })
659
688
  );
660
689
 
@@ -691,9 +720,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
691
720
  const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}`, {
692
721
  method: 'DELETE',
693
722
  });
694
- return {
695
- content: [{ type: 'text', text: `Table ${tableId} deleted.\n\n${JSON.stringify(result, null, 2)}` }],
696
- };
723
+ return jsonContent({
724
+ action: 'table_deleted',
725
+ tableId,
726
+ result,
727
+ });
697
728
  })
698
729
  );
699
730
 
@@ -741,7 +772,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
741
772
  assertGlobalRulesAck(globalRulesAckKey);
742
773
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
743
774
  if (!tableData) {
744
- return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
775
+ throw new Error(`Table with ID ${tableId} not found.`);
745
776
  }
746
777
 
747
778
  const existingColumns = getPatchableColumns(tableData.columns);
@@ -771,9 +802,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
771
802
  columnId,
772
803
  });
773
804
 
774
- return {
775
- content: [{ type: 'text', text: `Column ${columnId} updated on table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
776
- };
805
+ return jsonContent({
806
+ action: 'column_updated',
807
+ tableId,
808
+ columnId,
809
+ result,
810
+ });
777
811
  })
778
812
  );
779
813
 
@@ -331,7 +331,7 @@ function summarizeRoutes(routesResult) {
331
331
  }));
332
332
  }
333
333
 
334
- function summarizeMetadata(metadata, { search, limit } = {}) {
334
+ function summarizeMetadata(metadata, { search, limit, all = false } = {}) {
335
335
  const tables = normalizeTables(metadata);
336
336
  const q = search ? search.toLowerCase() : null;
337
337
  const summarized = tables.map((table) => ({
@@ -346,11 +346,13 @@ function summarizeMetadata(metadata, { search, limit } = {}) {
346
346
  const matched = q
347
347
  ? summarized.filter((table) => JSON.stringify(table).toLowerCase().includes(q))
348
348
  : summarized;
349
- const outputLimit = limit || 30;
349
+ const outputLimit = all ? matched.length : (limit || 30);
350
350
  return {
351
351
  tableCount: tables.length,
352
352
  matchedTableCount: matched.length,
353
353
  returnedTableCount: Math.min(matched.length, outputLimit),
354
+ complete: all || outputLimit >= matched.length,
355
+ hardCap: all ? null : outputLimit,
354
356
  search: search || null,
355
357
  tables: matched.slice(0, outputLimit),
356
358
  };
@@ -807,15 +809,21 @@ server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Us
807
809
  includeFull: z.boolean().optional().default(false).describe('Return full raw metadata. Default false to keep MCP context small.'),
808
810
  search: z.string().optional().describe('Optional table-name/alias substring filter.'),
809
811
  limit: z.number().optional().describe('Maximum tables returned after search. Default 30.'),
810
- }, async ({ includeFull, search, limit }) => {
812
+ all: z.boolean().optional().default(false).describe('Return every matched table summary. Use when a complete table list is required.'),
813
+ }, async ({ includeFull, search, limit, all }) => {
814
+ if (all && limit !== undefined) {
815
+ throw new Error('get_all_metadata accepts either all=true or limit, not both.');
816
+ }
811
817
  const result = await fetchAPI(ENFYRA_API_URL, '/metadata');
812
818
  const payload = includeFull
813
819
  ? result
814
820
  : {
815
821
  statusCode: result?.statusCode,
816
822
  success: result?.success,
817
- ...summarizeMetadata(result, { search, limit }),
818
- detailHint: 'Default response is capped and minimal. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.',
823
+ ...summarizeMetadata(result, { search, limit, all }),
824
+ detailHint: all
825
+ ? 'Complete summary returned. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.'
826
+ : 'Default response is capped and minimal. Pass all=true for a complete summary, or call get_table_metadata({ tableName }) / inspect_table({ tableName }) for detail.',
819
827
  };
820
828
  return jsonContent(payload);
821
829
  });
@@ -2109,16 +2117,11 @@ server.tool(
2109
2117
  tableName: z.string().describe('Table name or alias to inspect'),
2110
2118
  },
2111
2119
  async ({ tableName }) => {
2112
- let state = await collectRestDefinitionState();
2113
- let table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
2120
+ const state = await collectRestDefinitionState();
2121
+ const table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
2114
2122
  if (!table) {
2115
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' }).catch(() => {});
2116
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
2117
- await new Promise((resolve) => setTimeout(resolve, 150));
2118
- state = await collectRestDefinitionState();
2119
- table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
2123
+ throw new Error(`Unknown table "${tableName}". Use get_all_tables({ search, limit }) or get_all_metadata({ search, all: true }) to confirm the table name. If a just-created table is missing, verify the create response/reload event before calling manual reload tools.`);
2120
2124
  }
2121
- if (!table) throw new Error(`Unknown table "${tableName}"`);
2122
2125
  const tableId = getId(table);
2123
2126
  const columnIds = new Set((table.columns || []).map((column) => String(getId(column))));
2124
2127
  const relationIds = new Set((table.relations || []).map((relation) => String(getId(relation))));
@@ -2420,7 +2423,7 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
2420
2423
  const queryParams = new URLSearchParams({
2421
2424
  filter: JSON.stringify(filter),
2422
2425
  fields: 'id,path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled',
2423
- limit: '1000',
2426
+ limit: all ? '0' : '1000',
2424
2427
  });
2425
2428
  const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_route?${queryParams.toString()}`);
2426
2429
  const q = search ? search.toLowerCase() : null;
@@ -2439,6 +2442,8 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
2439
2442
  matchedRouteCount: matchedRoutes.length,
2440
2443
  returnedRouteCount: Math.min(matchedRoutes.length, routeLimit),
2441
2444
  all: !!all,
2445
+ complete: all || routeLimit >= matchedRoutes.length,
2446
+ hardCap: all ? null : routeLimit,
2442
2447
  search: search || null,
2443
2448
  routes: matchedRoutes.slice(0, routeLimit),
2444
2449
  detailHint: matchedRoutes.length > routeLimit
@@ -2885,7 +2890,7 @@ server.tool('reload_all', 'Reload all caches (metadata, routes, GraphQL)', {
2885
2890
  }, async ({ globalRulesAckKey }) => {
2886
2891
  assertGlobalRulesAck(globalRulesAckKey);
2887
2892
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload', { method: 'POST' });
2888
- return { content: [{ type: 'text', text: `System reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2893
+ return jsonContent({ action: 'reloaded_all', result });
2889
2894
  });
2890
2895
 
2891
2896
  server.tool('reload_metadata', 'Reload metadata cache only', {
@@ -2893,7 +2898,7 @@ server.tool('reload_metadata', 'Reload metadata cache only', {
2893
2898
  }, async ({ globalRulesAckKey }) => {
2894
2899
  assertGlobalRulesAck(globalRulesAckKey);
2895
2900
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' });
2896
- return { content: [{ type: 'text', text: `Metadata reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2901
+ return jsonContent({ action: 'reloaded_metadata', result });
2897
2902
  });
2898
2903
 
2899
2904
  server.tool('reload_routes', 'Reload routes cache only', {
@@ -2901,7 +2906,7 @@ server.tool('reload_routes', 'Reload routes cache only', {
2901
2906
  }, async ({ globalRulesAckKey }) => {
2902
2907
  assertGlobalRulesAck(globalRulesAckKey);
2903
2908
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' });
2904
- return { content: [{ type: 'text', text: `Routes reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2909
+ return jsonContent({ action: 'reloaded_routes', result });
2905
2910
  });
2906
2911
 
2907
2912
  server.tool('reload_graphql', 'Reload GraphQL schema', {
@@ -2909,7 +2914,7 @@ server.tool('reload_graphql', 'Reload GraphQL schema', {
2909
2914
  }, async ({ globalRulesAckKey }) => {
2910
2915
  assertGlobalRulesAck(globalRulesAckKey);
2911
2916
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/graphql', { method: 'POST' });
2912
- return { content: [{ type: 'text', text: `GraphQL reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2917
+ return jsonContent({ action: 'reloaded_graphql', result });
2913
2918
  });
2914
2919
 
2915
2920
  // ============================================================================
@@ -3080,12 +3085,15 @@ server.tool(
3080
3085
  const checkFilter = JSON.stringify({ name: { _eq: name }, type: { _eq: type } });
3081
3086
  const existing = await fetchAPI(ENFYRA_API_URL, `/enfyra_package?filter=${encodeURIComponent(checkFilter)}&limit=1`);
3082
3087
  if (existing.data && existing.data.length > 0) {
3083
- return {
3084
- content: [{
3085
- type: 'text',
3086
- text: `Package "${name}" is already installed (version: ${existing.data[0].version}, type: ${existing.data[0].type}).\n${JSON.stringify(existing.data[0], null, 2)}`,
3087
- }],
3088
- };
3088
+ return jsonContent({
3089
+ action: 'package_already_installed',
3090
+ package: {
3091
+ name,
3092
+ version: existing.data[0].version,
3093
+ type: existing.data[0].type,
3094
+ },
3095
+ record: existing.data[0],
3096
+ });
3089
3097
  }
3090
3098
 
3091
3099
  // Step 3: Get current user for installedBy
@@ -3107,12 +3115,11 @@ server.tool(
3107
3115
  body: JSON.stringify(body),
3108
3116
  });
3109
3117
 
3110
- return {
3111
- content: [{
3112
- type: 'text',
3113
- text: `Package "${name}@${pkgVersion}" installed successfully (type: ${type}).\n${JSON.stringify(result, null, 2)}`,
3114
- }],
3115
- };
3118
+ return jsonContent({
3119
+ action: 'package_installed',
3120
+ package: { name, version: pkgVersion, type },
3121
+ result,
3122
+ });
3116
3123
  },
3117
3124
  );
3118
3125