@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.
- package/README.md +12 -0
- package/dist/lib/mcp-instructions.js +1 -0
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/metadata-client.d.ts +8 -0
- package/dist/lib/metadata-client.js +69 -0
- package/dist/lib/metadata-client.js.map +1 -0
- package/dist/lib/platform-operation-tools.js +8 -18
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/runtime-zone-tools.d.ts +1 -1
- package/dist/lib/runtime-zone-tools.js +17 -24
- package/dist/lib/runtime-zone-tools.js.map +1 -1
- package/dist/lib/table-tools.d.ts +4 -3
- package/dist/lib/table-tools.js +59 -43
- package/dist/lib/table-tools.js.map +1 -1
- package/dist/lib/types.d.ts +12 -0
- package/dist/mcp-server-entry.js +91 -108
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp-server-entry.js
CHANGED
|
@@ -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
|
|
263
|
-
const
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
872
|
-
includeFull: z.boolean().optional().default(false).describe('
|
|
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
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
|
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
|
-
?
|
|
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
|
|
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 =
|
|
944
|
-
const tableDefinition =
|
|
945
|
-
const gqlDefinition =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 ||
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
}
|