@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.93",
3
+ "version": "0.0.95",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.',
@@ -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: (table.columns || []).map((column) => ({
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
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 = await discoveryFetch('/metadata');
829
- const routesResult = await discoveryFetch('/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000');
830
- const tables = normalizeTables(metadata);
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' : inferPrimaryKeyContext(tables).dominantPrimaryKey || '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: 'SQL commonly uses id; Mongo uses _id. Use table metadata primary column when available.',
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
942
+ return jsonContent(payload);
897
943
  },
898
944
  );
899
945
 
@@ -1033,7 +1079,7 @@ server.tool(
1033
1079
  },
1034
1080
  };
1035
1081
 
1036
- return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
1123
+ return jsonContent(payload);
1076
1124
  },
1077
1125
  );
1078
1126