@enfyra/mcp-server 0.1.52 → 0.1.53

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.
@@ -56,6 +56,7 @@ function assertExtensionReadFields(tableName, fields) {
56
56
  // Import modules
57
57
  import { exchangeApiToken, getValidToken, getTokenExpiry, initAuth } from './lib/auth.js';
58
58
  import { fetchAPI, validateFilter, validateTableName } from './lib/fetch.js';
59
+ import { fetchMetadataContext, fetchMetadataTables, fetchTableCatalog, fetchTableMetadata, fetchTableMetadataByRef, } from './lib/metadata-client.js';
59
60
  import { buildMcpServerInstructions, buildGraphqlUrls } from './lib/mcp-instructions.js';
60
61
  import { getExamples, listExampleCategories } from './lib/mcp-examples.js';
61
62
  import { WORKFLOW_SURFACES, discoverWorkflowRoutes } from './lib/tool-routing.js';
@@ -250,43 +251,16 @@ const SCRIPT_SOURCE_FIELDS = [
250
251
  'connectionHandlerScript',
251
252
  'code',
252
253
  ];
253
- function normalizeTables(metadata) {
254
- const tablesSource = metadata?.data?.tables || metadata?.tables || metadata?.data || [];
255
- return Array.isArray(tablesSource)
256
- ? tablesSource
257
- : Object.values(tablesSource || {});
258
- }
259
254
  function getPrimaryColumn(table) {
260
255
  return (table?.columns || []).find((column) => column.isPrimary) || null;
261
256
  }
262
- function inferPrimaryKeyContext(tables) {
263
- const primaryColumns = tables
264
- .map((table) => ({ table: table.name, primaryKey: getPrimaryColumn(table)?.name || null }))
265
- .filter((item) => item.primaryKey);
266
- const counts = {};
267
- for (const item of primaryColumns) {
268
- counts[item.primaryKey] = (counts[item.primaryKey] || 0) + 1;
269
- }
270
- const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || null;
271
- return {
272
- dominantPrimaryKey: dominant,
273
- counts,
274
- inferredBackendFamily: dominant === '_id' ? 'mongodb-like' : dominant === 'id' ? 'sql-like' : 'unknown',
275
- exactDatabaseType: 'not exposed by current public/admin API; infer from metadata or add a backend context endpoint for exact mysql/postgres/mongodb',
276
- sampleTables: primaryColumns.slice(0, 12),
277
- };
278
- }
279
- function getMetadataDatabaseContext(metadata, tables) {
280
- const inferred = inferPrimaryKeyContext(tables);
257
+ function getMetadataDatabaseContext(metadata) {
258
+ const dbType = metadata?.dbType || metadata?.data?.dbType || null;
281
259
  return {
282
- dbType: metadata?.dbType || metadata?.data?.dbType || null,
283
- pkField: metadata?.pkField || metadata?.data?.pkField || inferred.dominantPrimaryKey,
284
- inferredBackendFamily: inferred.inferredBackendFamily,
285
- primaryKeyCounts: inferred.counts,
286
- source: metadata?.dbType || metadata?.data?.dbType
287
- ? 'metadata'
288
- : 'inferred from table primary columns',
289
- sampleTables: inferred.sampleTables,
260
+ dbType,
261
+ backendFamily: dbType === 'mongodb' ? 'mongodb' : dbType ? 'sql' : 'unknown',
262
+ primaryKeyConvention: dbType === 'mongodb' ? '_id' : dbType ? 'id' : null,
263
+ source: dbType ? 'GET /metadata' : 'unavailable',
290
264
  };
291
265
  }
292
266
  function summarizeTable(table) {
@@ -346,32 +320,6 @@ function summarizeRoutes(routesResult) {
346
320
  isEnabled: route.isEnabled,
347
321
  }));
348
322
  }
349
- function summarizeMetadata(metadata, { search, limit, all = false } = {}) {
350
- const tables = normalizeTables(metadata);
351
- const q = search ? search.toLowerCase() : null;
352
- const summarized = tables.map((table) => ({
353
- id: table.id ?? table._id,
354
- name: table.name,
355
- alias: table.alias,
356
- primaryKey: getPrimaryColumn(table)?.name || null,
357
- columnCount: (table.columns || []).length,
358
- relationCount: (table.relations || []).length,
359
- routeHint: `Use get_table_metadata({ tableName: "${table.name}" }) for fields and relations.`,
360
- }));
361
- const matched = q
362
- ? summarized.filter((table) => JSON.stringify(table).toLowerCase().includes(q))
363
- : summarized;
364
- const outputLimit = all ? matched.length : (limit || 30);
365
- return {
366
- tableCount: tables.length,
367
- matchedTableCount: matched.length,
368
- returnedTableCount: Math.min(matched.length, outputLimit),
369
- complete: all || outputLimit >= matched.length,
370
- hardCap: all ? null : outputLimit,
371
- search: search || null,
372
- tables: matched.slice(0, outputLimit),
373
- };
374
- }
375
323
  function unwrapData(result) {
376
324
  return Array.isArray(result?.data) ? result.data : [];
377
325
  }
@@ -549,13 +497,14 @@ function summarizeMutationResult(result, action, tableName) {
549
497
  };
550
498
  }
551
499
  async function getTableSummary(tableName) {
552
- const result = await fetchAPI(ENFYRA_API_URL, `/metadata/${tableName}`);
553
- const table = result?.data?.table || result?.data || result?.table || result;
554
- return summarizeTable(table);
500
+ return summarizeTable(await fetchTableMetadata(ENFYRA_API_URL, tableName));
555
501
  }
556
502
  async function getPrimaryFieldName(tableName) {
557
503
  const table = await getTableSummary(tableName);
558
- return table?.primaryKey || 'id';
504
+ if (table?.primaryKey)
505
+ return table.primaryKey;
506
+ const metadata = await fetchMetadataContext(ENFYRA_API_URL);
507
+ return metadata.dbType === 'mongodb' ? '_id' : 'id';
559
508
  }
560
509
  async function fetchAll(path) {
561
510
  return unwrapData(await fetchAPI(ENFYRA_API_URL, path));
@@ -597,11 +546,18 @@ function collectPartialErrors(results) {
597
546
  .filter(([, result]) => result?.error)
598
547
  .map(([name, result]) => ({ name, error: result.error }));
599
548
  }
600
- async function getMetadataTables() {
601
- const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
549
+ async function getMetadataTables(tableRef) {
550
+ const metadata = await fetchMetadataContext(ENFYRA_API_URL);
551
+ if (tableRef !== undefined && tableRef !== null && tableRef !== '') {
552
+ return {
553
+ metadata,
554
+ tables: [await fetchTableMetadataByRef(ENFYRA_API_URL, tableRef)],
555
+ };
556
+ }
557
+ const catalog = await fetchTableCatalog(ENFYRA_API_URL);
602
558
  return {
603
559
  metadata,
604
- tables: normalizeTables(metadata),
560
+ tables: catalog,
605
561
  };
606
562
  }
607
563
  function resolveTableOrThrow(tables, tableName) {
@@ -618,7 +574,7 @@ function resolveFieldOrThrow(table, fieldName, kind = 'column') {
618
574
  return field;
619
575
  }
620
576
  async function prepareGenericMutation(tableName, data) {
621
- const { tables } = await getMetadataTables();
577
+ const { tables } = await getMetadataTables(tableName);
622
578
  return prepareRecordMutation({
623
579
  fetchAPI,
624
580
  apiUrl: ENFYRA_API_URL,
@@ -628,7 +584,7 @@ async function prepareGenericMutation(tableName, data) {
628
584
  });
629
585
  }
630
586
  async function prepareGenericBatchMutation(tableName, records) {
631
- const { tables } = await getMetadataTables();
587
+ const { tables } = await getMetadataTables(tableName);
632
588
  return prepareRecordBatchMutation({
633
589
  fetchAPI,
634
590
  apiUrl: ENFYRA_API_URL,
@@ -868,8 +824,8 @@ server.tool('get_enfyra_required_knowledge', [
868
824
  ].join(' '), {
869
825
  scope: z.enum(['schema', 'dynamic-code', 'extension', 'flow']).optional().describe('Limit knowledge to one domain. Omit to load all rules.'),
870
826
  }, async ({ scope }) => jsonContent(buildRequiredKnowledgePayload(scope)));
871
- server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Use get_table_metadata or inspect_table for detail.', {
872
- includeFull: z.boolean().optional().default(false).describe('Return full raw metadata. Default false to keep MCP context small.'),
827
+ server.tool('get_all_metadata', 'Get a lightweight table catalog. Use get_table_metadata or inspect_table to fetch one table schema.', {
828
+ includeFull: z.boolean().optional().default(false).describe('Fetch per-table metadata for the selected catalog entries. Default false keeps discovery lightweight.'),
873
829
  search: z.string().optional().describe('Optional table-name/alias substring filter.'),
874
830
  limit: z.number().optional().describe('Maximum tables returned after search. Default 30.'),
875
831
  all: z.boolean().optional().default(false).describe('Return every matched table summary. Use when a complete table list is required.'),
@@ -877,30 +833,47 @@ server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Us
877
833
  if (all && limit !== undefined) {
878
834
  throw new Error('get_all_metadata accepts either all=true or limit, not both.');
879
835
  }
880
- const result = await fetchAPI(ENFYRA_API_URL, '/metadata');
881
- const payload = includeFull
882
- ? result
883
- : {
884
- statusCode: result?.statusCode,
885
- success: result?.success,
886
- ...summarizeMetadata(result, { search, limit, all }),
887
- detailHint: all
888
- ? 'Complete summary returned. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.'
889
- : 'Default response is capped and minimal. Pass all=true for a complete summary, or call get_table_metadata({ tableName }) / inspect_table({ tableName }) for detail.',
890
- };
836
+ const [context, catalog] = await Promise.all([
837
+ fetchMetadataContext(ENFYRA_API_URL),
838
+ fetchTableCatalog(ENFYRA_API_URL),
839
+ ]);
840
+ const q = search?.trim().toLowerCase();
841
+ const matched = catalog.filter((table) => !q || [table.name, table.alias, table.description]
842
+ .some((value) => String(value || '').toLowerCase().includes(q)));
843
+ const outputLimit = all ? matched.length : (limit || 30);
844
+ const selected = matched.slice(0, outputLimit);
845
+ const payload = {
846
+ context,
847
+ tableCount: catalog.length,
848
+ matchedTableCount: matched.length,
849
+ returnedTableCount: selected.length,
850
+ complete: all || outputLimit >= matched.length,
851
+ hardCap: all ? null : outputLimit,
852
+ search: search || null,
853
+ tables: includeFull
854
+ ? await fetchMetadataTables(ENFYRA_API_URL, selected)
855
+ : selected.map((table) => ({
856
+ id: table.id ?? table._id,
857
+ name: table.name,
858
+ alias: table.alias ?? null,
859
+ description: table.description ?? null,
860
+ isSingleRecord: table.isSingleRecord ?? null,
861
+ detailHint: `Use get_table_metadata({ tableName: "${table.name}" }) for columns and relations.`,
862
+ })),
863
+ detailHint: includeFull
864
+ ? 'Full permission-projected metadata was fetched per selected table.'
865
+ : 'Catalog only. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for schema detail.',
866
+ };
891
867
  return jsonContent(payload);
892
868
  });
893
869
  server.tool('get_table_metadata', 'Get concise metadata for a specific table by name', {
894
870
  tableName: z.string().describe('Table name (e.g., "enfyra_user", "enfyra_route")'),
895
871
  includeFull: z.boolean().optional().default(false).describe('Return full raw table metadata. Default false to keep MCP context small.'),
896
872
  }, async ({ tableName, includeFull }) => {
897
- const result = await fetchAPI(ENFYRA_API_URL, `/metadata/${tableName}`);
898
- const table = result?.data?.table || result?.data || result?.table || result;
873
+ const table = await fetchTableMetadata(ENFYRA_API_URL, tableName);
899
874
  const payload = includeFull
900
- ? result
875
+ ? { data: table, ...await fetchMetadataContext(ENFYRA_API_URL) }
901
876
  : {
902
- statusCode: result?.statusCode,
903
- success: result?.success,
904
877
  table: summarizeTable(table),
905
878
  queryHint: `Use query_table({ tableName: "${tableName}", fields: [...] }) for records. query_table without fields returns only the primary key.`,
906
879
  };
@@ -933,16 +906,21 @@ server.tool('discover_enfyra_system', [
933
906
  'Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
934
907
  ].join(' '), {}, async () => {
935
908
  const metadata = await discoveryFetch('/metadata');
909
+ const tableCatalogResult = await discoveryFetch('/enfyra_table?fields=id,name,alias,description,isSingleRecord&limit=0&sort=name');
936
910
  const routesResult = await discoveryFetch('/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*&limit=1000');
937
911
  const methodsResult = await discoveryFetch('/enfyra_method?limit=100');
938
- const tables = normalizeTables(metadata);
912
+ const columnMetadata = await discoveryFetch('/metadata/enfyra_column', { fallbackData: null });
913
+ const relationMetadata = await discoveryFetch('/metadata/enfyra_relation', { fallbackData: null });
914
+ const tableMetadata = await discoveryFetch('/metadata/enfyra_table', { fallbackData: null });
915
+ const graphqlMetadata = await discoveryFetch('/metadata/enfyra_graphql', { fallbackData: null });
916
+ const tables = unwrapData(tableCatalogResult);
939
917
  const tableNames = tables.map((table) => table?.name).filter(Boolean).sort();
940
918
  const routes = summarizeRoutes(routesResult);
941
919
  const routeTables = new Set(routes.map((route) => route.mainTable).filter(Boolean));
942
920
  const noRouteTables = tableNames.filter((name) => !routeTables.has(name));
943
- const relationTable = tables.find((table) => table?.name === 'enfyra_relation');
944
- const tableDefinition = tables.find((table) => table?.name === 'enfyra_table');
945
- const gqlDefinition = tables.find((table) => table?.name === 'enfyra_graphql');
921
+ const relationTable = relationMetadata?.data || null;
922
+ const tableDefinition = tableMetadata?.data || null;
923
+ const gqlDefinition = graphqlMetadata?.data || null;
946
924
  const routeTableList = [...routeTables].sort();
947
925
  const noRouteTableList = noRouteTables.sort();
948
926
  const sample = (items, max = 40) => ({
@@ -954,7 +932,7 @@ server.tool('discover_enfyra_system', [
954
932
  const payload = {
955
933
  targetInstance: targetInstance(),
956
934
  apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
957
- partialErrors: collectPartialErrors({ metadata, routesResult, methodsResult }),
935
+ partialErrors: collectPartialErrors({ metadata, tableCatalogResult, routesResult, methodsResult, columnMetadata, relationMetadata, tableMetadata, graphqlMetadata }),
958
936
  counts: {
959
937
  tables: tableNames.length,
960
938
  routes: routes.length,
@@ -981,7 +959,7 @@ server.tool('discover_enfyra_system', [
981
959
  createTable: 'POST /enfyra_table supports isSingleRecord at create time. MCP create_tables accepts a native array, creates tables/columns sequentially, then creates requested relations after all tables in the batch exist. It does not accept alias at create time; table name drives the default route/schema behavior.',
982
960
  updateTable: 'PATCH /enfyra_table/:id is the canonical path for table property changes and column/relation schema changes.',
983
961
  columns: 'enfyra_column has no REST route; use create_tables/create_columns/update_columns/delete_columns. Use liveColumnTypes below; do not invent SQL dialect names.',
984
- liveColumnTypes: getSupportedColumnTypesFromMetadata(metadata),
962
+ liveColumnTypes: getSupportedColumnTypesFromMetadata(columnMetadata),
985
963
  columnTypeGuidance: 'Use varchar for short strings, text/richtext for long prose, float for price/amount/rating/decimal-like values unless decimal is listed, simple-json for structured objects/arrays only when listed, and relations instead of *_id columns for links.',
986
964
  relations: routeTables.has('enfyra_relation')
987
965
  ? 'enfyra_relation has a REST route for reads/metadata, but canonical schema migration is create_relations/delete_relations or enfyra_table PATCH with the full relations array. Relation onDelete accepts CASCADE, SET NULL, or RESTRICT.'
@@ -1011,9 +989,10 @@ server.tool('discover_enfyra_system', [
1011
989
  });
1012
990
  server.tool('discover_runtime_context', [
1013
991
  'Discover live runtime context that affects how an LLM should use Enfyra.',
1014
- 'Reports inferred primary key/backend family, route/cache/admin surfaces, active metadata-backed runtime areas, and what is not exposed by the backend API. Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
992
+ 'Reports exact database type, the derived primary-key convention, route/cache/admin surfaces, and active metadata-backed runtime areas. Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
1015
993
  ].join(' '), {}, async () => {
1016
994
  const metadata = await discoveryFetch('/metadata');
995
+ const tableCatalogResult = await discoveryFetch('/enfyra_table?fields=id,name,alias,description,isSingleRecord&limit=0&sort=name');
1017
996
  const routesResult = await discoveryFetch('/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000');
1018
997
  const methodsResult = await discoveryFetch('/enfyra_method?limit=100');
1019
998
  const gqlResult = await discoveryFetch('/enfyra_graphql?limit=1000');
@@ -1022,7 +1001,7 @@ server.tool('discover_runtime_context', [
1022
1001
  const storageResult = await discoveryFetch('/enfyra_storage_config?limit=1000');
1023
1002
  const settingsResult = await discoveryFetch('/enfyra_setting?limit=1000');
1024
1003
  const meResult = await discoveryFetch('/me', { fallbackData: null });
1025
- const tables = normalizeTables(metadata);
1004
+ const tables = unwrapData(tableCatalogResult);
1026
1005
  const routes = summarizeRoutes(routesResult);
1027
1006
  const routeTables = new Set(routes.map((route) => route.mainTable).filter(Boolean));
1028
1007
  const adminRoutes = routes.filter((route) => route.path?.startsWith('/admin'));
@@ -1038,6 +1017,7 @@ server.tool('discover_runtime_context', [
1038
1017
  apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
1039
1018
  partialErrors: collectPartialErrors({
1040
1019
  metadata,
1020
+ tableCatalogResult,
1041
1021
  routesResult,
1042
1022
  methodsResult,
1043
1023
  gqlResult,
@@ -1048,7 +1028,7 @@ server.tool('discover_runtime_context', [
1048
1028
  meResult,
1049
1029
  }),
1050
1030
  authenticatedUser: Array.isArray(meResult?.data) ? meResult.data[0] || null : meResult?.data || null,
1051
- database: getMetadataDatabaseContext(metadata, tables),
1031
+ database: getMetadataDatabaseContext(metadata),
1052
1032
  counts: {
1053
1033
  tables: tables.length,
1054
1034
  routes: routes.length,
@@ -1082,9 +1062,7 @@ server.tool('discover_runtime_context', [
1082
1062
  flowWorkerContract: 'Flow jobs require the backend flow worker to be initialized after HTTP listen and websocket gateway init; trigger_flow only confirms enqueue/result from admin endpoint.',
1083
1063
  },
1084
1064
  runtimeGaps: [
1085
- metadata?.dbType || metadata?.data?.dbType
1086
- ? null
1087
- : 'Exact database type is not exposed by current MCP-visible API.',
1065
+ metadata?.dbType || metadata?.data?.dbType ? null : 'Exact database type was unavailable from GET /metadata.',
1088
1066
  'Redis/BullMQ/socket adapter health is not exposed by current MCP-visible API.',
1089
1067
  'MCP can test flow steps and websocket scripts through admin test endpoints, but not prove every production queue/client path without a real end-to-end client.',
1090
1068
  ].filter(Boolean),
@@ -1111,7 +1089,7 @@ server.tool('discover_query_capabilities', [
1111
1089
  : [];
1112
1090
  const routes = summarizeRoutes(routesResult);
1113
1091
  const table = tableName ? tables.find((item) => item.name === tableName) : null;
1114
- const primaryKey = table ? getPrimaryColumn(table)?.name || 'id' : 'id';
1092
+ const primaryKey = table ? getPrimaryColumn(table)?.name || null : null;
1115
1093
  const tableRoutes = tableName
1116
1094
  ? routes.filter((route) => route.mainTable === tableName)
1117
1095
  : [];
@@ -1132,7 +1110,7 @@ server.tool('discover_query_capabilities', [
1132
1110
  meta: 'Request metadata/counts where supported.',
1133
1111
  deep: 'Nested relation fetch object keyed by relation propertyName.',
1134
1112
  },
1135
- countPattern: 'For counts, query only fields=id with limit=1 and request meta. Use meta=totalCount without a filter, or meta=filterCount when a filter is supplied. MCP count_records wraps this pattern.',
1113
+ countPattern: `For counts, query only fields=${primaryKey || '<primary-key>'} with limit=1 and request meta. Use meta=totalCount without a filter, or meta=filterCount when a filter is supplied. MCP count_records resolves the live table primary key and wraps this pattern.`,
1136
1114
  security: 'Filters, sorts, counts, and aggregate values can leak information even when a field is not selected. In generated public/user-facing APIs, do not filter, sort, count, or aggregate unpublished fields or private relations unless the endpoint intentionally exposes that fact.',
1137
1115
  deep: {
1138
1116
  shape: '{ [relationName]: { fields?, filter?, sort?, limit?, page?, deep? } }',
@@ -2021,10 +1999,10 @@ function withMethodNames(records, methodIdNameMap, field = 'methods') {
2021
1999
  : record?.[field],
2022
2000
  }));
2023
2001
  }
2024
- async function collectRestDefinitionState() {
2002
+ async function collectRestDefinitionState(tableRef) {
2025
2003
  await getValidToken(ENFYRA_API_URL);
2026
2004
  const [metadataContext, routes, handlers, preHooks, postHooks, routePermissions, guards, guardRules, fieldPermissions, columnRules, methodIdNameMap,] = await Promise.all([
2027
- getMetadataTables(),
2005
+ getMetadataTables(tableRef),
2028
2006
  fetchAll('/enfyra_route?limit=1000'),
2029
2007
  fetchAll('/enfyra_route_handler?limit=1000'),
2030
2008
  fetchAll('/enfyra_pre_hook?limit=1000'),
@@ -2052,6 +2030,7 @@ async function collectRestDefinitionState() {
2052
2030
  }
2053
2031
  async function collectFeatureSearchState() {
2054
2032
  const metadata = await discoveryFetch('/metadata');
2033
+ const tableCatalogResult = await discoveryFetch('/enfyra_table?fields=id,name,alias,description,isSingleRecord&limit=0&sort=name');
2055
2034
  const routesResult = await discoveryFetch('/enfyra_route?limit=500');
2056
2035
  const handlersResult = await discoveryFetch('/enfyra_route_handler?limit=500');
2057
2036
  const preHooksResult = await discoveryFetch('/enfyra_pre_hook?limit=500');
@@ -2063,9 +2042,10 @@ async function collectFeatureSearchState() {
2063
2042
  const columnRulesResult = await discoveryFetch('/enfyra_column_rule?limit=500');
2064
2043
  const methodsResult = await discoveryFetch('/enfyra_method?limit=100');
2065
2044
  const methodIdNameMap = Object.fromEntries(unwrapData(methodsResult).map((method) => [String(getId(method)), method.name]));
2045
+ const tableCatalog = unwrapData(tableCatalogResult);
2066
2046
  return {
2067
2047
  metadata,
2068
- tables: normalizeTables(metadata),
2048
+ tables: await fetchMetadataTables(ENFYRA_API_URL, tableCatalog),
2069
2049
  routes: unwrapData(routesResult),
2070
2050
  handlers: unwrapData(handlersResult),
2071
2051
  preHooks: unwrapData(preHooksResult),
@@ -2078,6 +2058,7 @@ async function collectFeatureSearchState() {
2078
2058
  methodIdNameMap,
2079
2059
  partialErrors: collectPartialErrors({
2080
2060
  metadata,
2061
+ tableCatalogResult,
2081
2062
  routesResult,
2082
2063
  handlersResult,
2083
2064
  preHooksResult,
@@ -2142,7 +2123,7 @@ server.tool('inspect_table', [
2142
2123
  ].join(' '), {
2143
2124
  tableName: z.string().describe('Table name or alias to inspect'),
2144
2125
  }, async ({ tableName }) => {
2145
- const state = await collectRestDefinitionState();
2126
+ const state = await collectRestDefinitionState(tableName);
2146
2127
  const table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
2147
2128
  if (!table) {
2148
2129
  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.`);
@@ -2153,7 +2134,7 @@ server.tool('inspect_table', [
2153
2134
  const routes = state.routes.filter((route) => sameId(refId(route.mainTable), tableId));
2154
2135
  const payload = {
2155
2136
  table: summarizeTable(table),
2156
- database: getMetadataDatabaseContext(state.metadata, state.tables),
2137
+ database: getMetadataDatabaseContext(state.metadata),
2157
2138
  rest: {
2158
2139
  routePattern: 'GET/POST /<path>; PATCH/DELETE /<path>/:id; no dynamic GET /<path>/:id.',
2159
2140
  routes: routes.map((route) => enrichRoute(route, state)),
@@ -2188,7 +2169,9 @@ server.tool('inspect_route', [
2188
2169
  const route = state.routes.find((item) => (routeId ? sameId(getId(item), routeId) : item.path === normalizeRestPath(path)));
2189
2170
  if (!route)
2190
2171
  throw new Error(`Route not found: ${routeId || path}`);
2191
- const table = state.tables.find((item) => sameId(getId(item), refId(route.mainTable))) || null;
2172
+ const table = route.mainTable
2173
+ ? await fetchTableMetadataByRef(ENFYRA_API_URL, refId(route.mainTable))
2174
+ : null;
2192
2175
  const payload = {
2193
2176
  apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
2194
2177
  route: enrichRoute(route, state),
@@ -2270,7 +2253,7 @@ server.tool('trace_metadata_usage', [
2270
2253
  throw new Error('query is required.');
2271
2254
  const lower = q.toLowerCase();
2272
2255
  const max = Math.max(1, Math.min(Number(limit || 25), 100));
2273
- const state = await collectRestDefinitionState();
2256
+ const state = await collectFeatureSearchState();
2274
2257
  const contains = (value) => JSON.stringify(value ?? '').toLowerCase().includes(lower);
2275
2258
  const sourceContains = (record) => getRecordSource(record).sourceCode.toLowerCase().includes(lower);
2276
2259
  const scriptTableResults = await Promise.all(SCRIPT_BACKED_TABLES.map(async (tableName) => {
@@ -2465,7 +2448,7 @@ server.tool('create_route', [
2465
2448
  availableMethods: resolveMethodIds(methodMap, methods),
2466
2449
  };
2467
2450
  if (mainTableId !== undefined && mainTableId !== null) {
2468
- const { tables } = await getMetadataTables();
2451
+ const { tables } = await getMetadataTables(mainTableId);
2469
2452
  validateMainTableRoutePath(tables, mainTableId, normalizedPath);
2470
2453
  body.mainTable = { id: mainTableId };
2471
2454
  }