@enfyra/mcp-server 0.0.93 → 0.0.95
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 +1 -1
- package/src/lib/mcp-instructions.js +1 -0
- package/src/mcp-server-entry.mjs +69 -21
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
24
24
|
`GraphQL endpoints: \`${graphqlHttpUrl}\` and \`${graphqlSchemaUrl}\`.`,
|
|
25
25
|
'',
|
|
26
26
|
'### Work Flow',
|
|
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.',
|
|
27
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.',
|
|
28
29
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
|
|
29
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.',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -192,6 +192,21 @@ function getMetadataDatabaseContext(metadata, tables) {
|
|
|
192
192
|
|
|
193
193
|
function summarizeTable(table) {
|
|
194
194
|
if (!table) return null;
|
|
195
|
+
const relationFkColumnNames = new Set((table.relations || []).flatMap((relation) => {
|
|
196
|
+
const propertyName = relation.propertyName;
|
|
197
|
+
return propertyName
|
|
198
|
+
? [
|
|
199
|
+
`${propertyName}Id`,
|
|
200
|
+
`${propertyName}_id`,
|
|
201
|
+
relation.fkCol,
|
|
202
|
+
relation.fkColumn,
|
|
203
|
+
relation.foreignKeyColumn,
|
|
204
|
+
].filter(Boolean).map((name) => String(name).toLowerCase())
|
|
205
|
+
: [];
|
|
206
|
+
}));
|
|
207
|
+
const modelFacingColumns = (table.columns || []).filter((column) => (
|
|
208
|
+
column.isPrimary || !relationFkColumnNames.has(String(column.name || '').toLowerCase())
|
|
209
|
+
));
|
|
195
210
|
return {
|
|
196
211
|
id: table.id ?? table._id,
|
|
197
212
|
name: table.name,
|
|
@@ -199,7 +214,7 @@ function summarizeTable(table) {
|
|
|
199
214
|
primaryKey: getPrimaryColumn(table)?.name || null,
|
|
200
215
|
validateBody: table.validateBody,
|
|
201
216
|
graphqlEnabled: table.graphqlEnabled,
|
|
202
|
-
columns:
|
|
217
|
+
columns: modelFacingColumns.map((column) => ({
|
|
203
218
|
id: column.id ?? column._id,
|
|
204
219
|
name: column.name,
|
|
205
220
|
type: column.type,
|
|
@@ -209,6 +224,7 @@ function summarizeTable(table) {
|
|
|
209
224
|
isUpdatable: column.isUpdatable !== false,
|
|
210
225
|
isEncrypted: column.isEncrypted === true,
|
|
211
226
|
})),
|
|
227
|
+
hiddenRelationColumnCount: (table.columns || []).length - modelFacingColumns.length,
|
|
212
228
|
relations: (table.relations || []).map((relation) => ({
|
|
213
229
|
id: relation.id ?? relation._id,
|
|
214
230
|
propertyName: relation.propertyName,
|
|
@@ -391,6 +407,10 @@ function collectPartialErrors(results) {
|
|
|
391
407
|
.map(([name, result]) => ({ name, error: result.error }));
|
|
392
408
|
}
|
|
393
409
|
|
|
410
|
+
function jsonContent(payload, { pretty = false } = {}) {
|
|
411
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, pretty ? 2 : 0) }] };
|
|
412
|
+
}
|
|
413
|
+
|
|
394
414
|
async function getMetadataTables() {
|
|
395
415
|
const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
396
416
|
return {
|
|
@@ -615,7 +635,7 @@ server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Us
|
|
|
615
635
|
...summarizeMetadata(result, { search, limit }),
|
|
616
636
|
detailHint: 'Default response is capped and minimal. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.',
|
|
617
637
|
};
|
|
618
|
-
return
|
|
638
|
+
return jsonContent(payload);
|
|
619
639
|
});
|
|
620
640
|
|
|
621
641
|
server.tool('get_table_metadata', 'Get concise metadata for a specific table by name', {
|
|
@@ -632,7 +652,7 @@ server.tool('get_table_metadata', 'Get concise metadata for a specific table by
|
|
|
632
652
|
table: summarizeTable(table),
|
|
633
653
|
queryHint: `Use query_table({ tableName: "${tableName}", fields: [...] }) for records. query_table without fields returns only the primary key.`,
|
|
634
654
|
};
|
|
635
|
-
return
|
|
655
|
+
return jsonContent(payload);
|
|
636
656
|
});
|
|
637
657
|
|
|
638
658
|
server.tool(
|
|
@@ -646,7 +666,7 @@ server.tool(
|
|
|
646
666
|
},
|
|
647
667
|
async ({ category }) => {
|
|
648
668
|
const result = getExamples(category);
|
|
649
|
-
return
|
|
669
|
+
return jsonContent(result);
|
|
650
670
|
},
|
|
651
671
|
);
|
|
652
672
|
|
|
@@ -655,6 +675,7 @@ server.tool(
|
|
|
655
675
|
[
|
|
656
676
|
'Call this first when you need to understand the live Enfyra instance.',
|
|
657
677
|
'Returns a concise capability map from live metadata/routes/method rows, including schema management, REST route behavior, GraphQL enablement, and relation handling.',
|
|
678
|
+
'Do not use this only to confirm the API base; use get_enfyra_api_context for that cheaper target check.',
|
|
658
679
|
'Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
|
|
659
680
|
].join(' '),
|
|
660
681
|
{},
|
|
@@ -672,6 +693,13 @@ server.tool(
|
|
|
672
693
|
const tableDefinition = tables.find((table) => table?.name === 'enfyra_table');
|
|
673
694
|
const gqlDefinition = tables.find((table) => table?.name === 'enfyra_graphql');
|
|
674
695
|
const routeTableList = [...routeTables].sort();
|
|
696
|
+
const noRouteTableList = noRouteTables.sort();
|
|
697
|
+
const sample = (items, max = 40) => ({
|
|
698
|
+
total: items.length,
|
|
699
|
+
returned: Math.min(items.length, max),
|
|
700
|
+
items: items.slice(0, max),
|
|
701
|
+
truncated: items.length > max,
|
|
702
|
+
});
|
|
675
703
|
|
|
676
704
|
const payload = {
|
|
677
705
|
targetInstance: targetInstance(),
|
|
@@ -692,10 +720,12 @@ server.tool(
|
|
|
692
720
|
rest: {
|
|
693
721
|
routePattern: 'Dynamic REST routes expose GET/POST at /<route-path> and PATCH/DELETE at /<route-path>/:id; there is no GET /<route-path>/:id.',
|
|
694
722
|
publicAccess: 'publicMethods controls anonymous REST access per route/method; otherwise Bearer JWT + routePermissions apply.',
|
|
695
|
-
routeTables: routeTableList,
|
|
696
|
-
noRouteTables,
|
|
723
|
+
routeTables: sample(routeTableList),
|
|
724
|
+
noRouteTables: sample(noRouteTableList),
|
|
697
725
|
canonicalCrudTools: 'query_table/create_record/update_record/delete_record use dynamic REST routes and only work for route-backed tables.',
|
|
698
726
|
customRouteWorkflow: 'For a new endpoint use create_route without mainTableId, then create_handler/create_pre_hook/create_post_hook. Do not create a table just to get a path.',
|
|
727
|
+
routeSamples: sample(routes, 25),
|
|
728
|
+
detailHint: 'Use get_all_routes({ search, limit }) or inspect_route({ path }) for route details. Use inspect_table({ tableName }) for table detail.',
|
|
699
729
|
},
|
|
700
730
|
schemaManagement: {
|
|
701
731
|
createTable: 'POST /enfyra_table supports isSingleRecord at create time and supports columns and relations arrays in the same cascade call. MCP create_table exposes isSingleRecord, columns, and relations directly. It does not accept alias at create time; table name drives the default route/schema behavior.',
|
|
@@ -723,11 +753,10 @@ server.tool(
|
|
|
723
753
|
: 'Use update_table graphqlEnabled, then reload_graphql if needed.',
|
|
724
754
|
gqlDefinitionColumns: (gqlDefinition?.columns || []).map((column) => column.name),
|
|
725
755
|
},
|
|
726
|
-
tableNames,
|
|
727
|
-
routes,
|
|
756
|
+
tableSamples: sample(tableNames, 40),
|
|
728
757
|
};
|
|
729
758
|
|
|
730
|
-
return
|
|
759
|
+
return jsonContent(payload);
|
|
731
760
|
},
|
|
732
761
|
);
|
|
733
762
|
|
|
@@ -754,6 +783,12 @@ server.tool(
|
|
|
754
783
|
const routeTables = new Set(routes.map((route) => route.mainTable).filter(Boolean));
|
|
755
784
|
const adminRoutes = routes.filter((route) => route.path?.startsWith('/admin'));
|
|
756
785
|
const publicRoutes = routes.filter((route) => route.publicMethods?.length);
|
|
786
|
+
const sample = (items, max = 25) => ({
|
|
787
|
+
total: items.length,
|
|
788
|
+
returned: Math.min(items.length, max),
|
|
789
|
+
items: items.slice(0, max),
|
|
790
|
+
truncated: items.length > max,
|
|
791
|
+
});
|
|
757
792
|
|
|
758
793
|
const payload = {
|
|
759
794
|
targetInstance: targetInstance(),
|
|
@@ -789,12 +824,12 @@ server.tool(
|
|
|
789
824
|
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name })),
|
|
790
825
|
routeRuntime: {
|
|
791
826
|
routePattern: 'GET/POST /<route-path>; PATCH/DELETE /<route-path>/:id; no dynamic GET /<route-path>/:id.',
|
|
792
|
-
adminRoutes: adminRoutes.map((route) => route.path).sort(),
|
|
793
|
-
publicRoutes: publicRoutes.map((route) => ({
|
|
827
|
+
adminRoutes: sample(adminRoutes.map((route) => route.path).sort()),
|
|
828
|
+
publicRoutes: sample(publicRoutes.map((route) => ({
|
|
794
829
|
path: route.path,
|
|
795
830
|
mainTable: route.mainTable,
|
|
796
831
|
publicMethods: route.publicMethods,
|
|
797
|
-
})),
|
|
832
|
+
}))),
|
|
798
833
|
},
|
|
799
834
|
cacheAndCluster: {
|
|
800
835
|
metadataMutationReloads: 'Metadata-backed mutations emit cache invalidation; admin reload endpoints exist for metadata/routes/graphql/guards/all.',
|
|
@@ -811,7 +846,7 @@ server.tool(
|
|
|
811
846
|
'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.',
|
|
812
847
|
].filter(Boolean),
|
|
813
848
|
};
|
|
814
|
-
return
|
|
849
|
+
return jsonContent(payload);
|
|
815
850
|
},
|
|
816
851
|
);
|
|
817
852
|
|
|
@@ -825,12 +860,21 @@ server.tool(
|
|
|
825
860
|
tableName: z.string().optional().describe('Optional table name to summarize query fields and relation/deep capabilities.'),
|
|
826
861
|
},
|
|
827
862
|
async ({ tableName }) => {
|
|
828
|
-
const metadata =
|
|
829
|
-
|
|
830
|
-
|
|
863
|
+
const metadata = tableName
|
|
864
|
+
? await discoveryFetch(`/metadata/${encodeURIComponent(tableName)}`)
|
|
865
|
+
: null;
|
|
866
|
+
const routesResult = tableName
|
|
867
|
+
? await discoveryFetch('/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000')
|
|
868
|
+
: { data: [] };
|
|
869
|
+
const tableFromMetadata = tableName && !metadata?.error
|
|
870
|
+
? metadata?.data?.table || metadata?.data || metadata?.table || metadata
|
|
871
|
+
: null;
|
|
872
|
+
const tables = tableName
|
|
873
|
+
? (tableFromMetadata ? [tableFromMetadata] : [])
|
|
874
|
+
: [];
|
|
831
875
|
const routes = summarizeRoutes(routesResult);
|
|
832
876
|
const table = tableName ? tables.find((item) => item.name === tableName) : null;
|
|
833
|
-
const primaryKey = table ? getPrimaryColumn(table)?.name || 'id' :
|
|
877
|
+
const primaryKey = table ? getPrimaryColumn(table)?.name || 'id' : 'id';
|
|
834
878
|
const tableRoutes = tableName
|
|
835
879
|
? routes.filter((route) => route.mainTable === tableName)
|
|
836
880
|
: [];
|
|
@@ -866,7 +910,9 @@ server.tool(
|
|
|
866
910
|
],
|
|
867
911
|
},
|
|
868
912
|
backendNotes: {
|
|
869
|
-
primaryKey:
|
|
913
|
+
primaryKey: tableName
|
|
914
|
+
? 'Use this table metadata primary column when available.'
|
|
915
|
+
: 'SQL commonly uses id; Mongo uses _id. Use table metadata primary column when available.',
|
|
870
916
|
relationNames: 'API relation operations use relation propertyName, not physical FK column names.',
|
|
871
917
|
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.',
|
|
872
918
|
graphql: 'GraphQL query args also accept filter/sort/page/limit, but GraphQL requires Bearer auth and table enablement via enfyra_graphql.',
|
|
@@ -893,7 +939,7 @@ server.tool(
|
|
|
893
939
|
discoveryRule: 'When building a query, inspect table metadata first, then use relation propertyName and primary column from that metadata.',
|
|
894
940
|
};
|
|
895
941
|
|
|
896
|
-
return
|
|
942
|
+
return jsonContent(payload);
|
|
897
943
|
},
|
|
898
944
|
);
|
|
899
945
|
|
|
@@ -1033,7 +1079,7 @@ server.tool(
|
|
|
1033
1079
|
},
|
|
1034
1080
|
};
|
|
1035
1081
|
|
|
1036
|
-
return
|
|
1082
|
+
return jsonContent(payload);
|
|
1037
1083
|
},
|
|
1038
1084
|
);
|
|
1039
1085
|
|
|
@@ -1045,6 +1091,7 @@ server.tool(
|
|
|
1045
1091
|
'get_enfyra_api_context',
|
|
1046
1092
|
[
|
|
1047
1093
|
'Returns the resolved API base URL for this MCP session (env ENFYRA_API_URL).',
|
|
1094
|
+
'Use this as the cheap first target sanity check before broad discovery or mutations.',
|
|
1048
1095
|
'Use when the user asks which HTTP endpoint or full URL applies: combine enfyraApiUrl with paths from server instructions (GET/POST /{table}, PATCH/DELETE /{table}/{id}, no GET /{table}/{id}).',
|
|
1049
1096
|
'Auth: publicMethods on a route can allow a method without Bearer; otherwise JWT + routePermissions — see server instructions.',
|
|
1050
1097
|
'If path might differ from table name, use get_all_routes before asserting a URL.',
|
|
@@ -1056,6 +1103,7 @@ server.tool(
|
|
|
1056
1103
|
const base = ENFYRA_API_URL.replace(/\/$/, '');
|
|
1057
1104
|
const gql = buildGraphqlUrls(ENFYRA_API_URL);
|
|
1058
1105
|
const payload = {
|
|
1106
|
+
targetInstance: targetInstance(),
|
|
1059
1107
|
enfyraApiUrl: base,
|
|
1060
1108
|
graphqlHttpUrl: gql.graphqlHttpUrl,
|
|
1061
1109
|
graphqlSchemaUrl: gql.graphqlSchemaUrl,
|
|
@@ -1072,7 +1120,7 @@ server.tool(
|
|
|
1072
1120
|
pathResolution: 'Confirm route path with get_all_routes or metadata — path may not equal table name.',
|
|
1073
1121
|
note: 'Full tool→HTTP mapping is in MCP server instructions (shown to the model at connect).',
|
|
1074
1122
|
};
|
|
1075
|
-
return
|
|
1123
|
+
return jsonContent(payload);
|
|
1076
1124
|
},
|
|
1077
1125
|
);
|
|
1078
1126
|
|