@enfyra/mcp-server 0.0.91 → 0.0.93

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.91",
3
+ "version": "0.0.93",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
24
24
  `GraphQL endpoints: \`${graphqlHttpUrl}\` and \`${graphqlSchemaUrl}\`.`,
25
25
  '',
26
26
  '### Work Flow',
27
- '- 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`.',
27
+ '- 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
28
  '- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
29
29
  '- 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.',
30
30
  '- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
@@ -13,6 +13,7 @@ import { createHash } from 'node:crypto';
13
13
  // Configuration
14
14
  const ENFYRA_API_URL = process.env.ENFYRA_API_URL || 'http://localhost:3000/api';
15
15
  const ENFYRA_API_TOKEN = process.env.ENFYRA_API_TOKEN || '';
16
+ const DISCOVERY_FETCH_TIMEOUT_MS = 12000;
16
17
 
17
18
  // Import modules
18
19
  import { exchangeApiToken, refreshAccessToken, getValidToken, resetTokens, getTokenExpiry, initAuth } from './lib/auth.js';
@@ -353,6 +354,43 @@ async function fetchAll(path) {
353
354
  return unwrapData(await fetchAPI(ENFYRA_API_URL, path));
354
355
  }
355
356
 
357
+ function targetInstance() {
358
+ return {
359
+ apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
360
+ source: 'ENFYRA_API_URL environment variable used by this MCP server process',
361
+ };
362
+ }
363
+
364
+ async function discoveryFetch(path, { fallbackData = [], timeoutMs = DISCOVERY_FETCH_TIMEOUT_MS } = {}) {
365
+ let timeoutId;
366
+ try {
367
+ const timeout = new Promise((_, reject) => {
368
+ timeoutId = setTimeout(() => {
369
+ reject(new Error(`Discovery request timeout after ${timeoutMs}ms for ${path}`));
370
+ }, timeoutMs);
371
+ });
372
+ return await Promise.race([
373
+ fetchAPI(ENFYRA_API_URL, path),
374
+ timeout,
375
+ ]);
376
+ } catch (error) {
377
+ return {
378
+ statusCode: null,
379
+ success: false,
380
+ error: String(error?.message || error),
381
+ data: fallbackData,
382
+ };
383
+ } finally {
384
+ if (timeoutId) clearTimeout(timeoutId);
385
+ }
386
+ }
387
+
388
+ function collectPartialErrors(results) {
389
+ return Object.entries(results)
390
+ .filter(([, result]) => result?.error)
391
+ .map(([name, result]) => ({ name, error: result.error }));
392
+ }
393
+
356
394
  async function getMetadataTables() {
357
395
  const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
358
396
  return {
@@ -617,14 +655,13 @@ server.tool(
617
655
  [
618
656
  'Call this first when you need to understand the live Enfyra instance.',
619
657
  'Returns a concise capability map from live metadata/routes/method rows, including schema management, REST route behavior, GraphQL enablement, and relation handling.',
658
+ 'Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
620
659
  ].join(' '),
621
660
  {},
622
661
  async () => {
623
- const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
624
- const [routesResult, methodsResult] = await Promise.all([
625
- fetchAPI(ENFYRA_API_URL, '/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*&limit=1000'),
626
- fetchAPI(ENFYRA_API_URL, '/enfyra_method?limit=100'),
627
- ]);
662
+ const metadata = await discoveryFetch('/metadata');
663
+ const routesResult = await discoveryFetch('/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*&limit=1000');
664
+ const methodsResult = await discoveryFetch('/enfyra_method?limit=100');
628
665
 
629
666
  const tables = normalizeTables(metadata);
630
667
  const tableNames = tables.map((table) => table?.name).filter(Boolean).sort();
@@ -637,7 +674,9 @@ server.tool(
637
674
  const routeTableList = [...routeTables].sort();
638
675
 
639
676
  const payload = {
677
+ targetInstance: targetInstance(),
640
678
  apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
679
+ partialErrors: collectPartialErrors({ metadata, routesResult, methodsResult }),
641
680
  counts: {
642
681
  tables: tableNames.length,
643
682
  routes: routes.length,
@@ -696,30 +735,19 @@ server.tool(
696
735
  'discover_runtime_context',
697
736
  [
698
737
  'Discover live runtime context that affects how an LLM should use Enfyra.',
699
- 'Reports inferred primary key/backend family, route/cache/admin surfaces, active metadata-backed runtime areas, and what is not exposed by the backend API.',
738
+ '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.',
700
739
  ].join(' '),
701
740
  {},
702
741
  async () => {
703
- const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
704
- const [
705
- routesResult,
706
- methodsResult,
707
- gqlResult,
708
- flowsResult,
709
- websocketResult,
710
- storageResult,
711
- settingsResult,
712
- meResult,
713
- ] = await Promise.all([
714
- fetchAPI(ENFYRA_API_URL, '/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000'),
715
- fetchAPI(ENFYRA_API_URL, '/enfyra_method?limit=100'),
716
- fetchAPI(ENFYRA_API_URL, '/enfyra_graphql?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
717
- fetchAPI(ENFYRA_API_URL, '/enfyra_flow?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
718
- fetchAPI(ENFYRA_API_URL, '/enfyra_websocket?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
719
- fetchAPI(ENFYRA_API_URL, '/enfyra_storage_config?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
720
- fetchAPI(ENFYRA_API_URL, '/enfyra_setting?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
721
- fetchAPI(ENFYRA_API_URL, '/me').catch((error) => ({ error: String(error.message || error), data: [] })),
722
- ]);
742
+ const metadata = await discoveryFetch('/metadata');
743
+ const routesResult = await discoveryFetch('/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000');
744
+ const methodsResult = await discoveryFetch('/enfyra_method?limit=100');
745
+ const gqlResult = await discoveryFetch('/enfyra_graphql?limit=1000');
746
+ const flowsResult = await discoveryFetch('/enfyra_flow?limit=1000');
747
+ const websocketResult = await discoveryFetch('/enfyra_websocket?limit=1000');
748
+ const storageResult = await discoveryFetch('/enfyra_storage_config?limit=1000');
749
+ const settingsResult = await discoveryFetch('/enfyra_setting?limit=1000');
750
+ const meResult = await discoveryFetch('/me', { fallbackData: null });
723
751
 
724
752
  const tables = normalizeTables(metadata);
725
753
  const routes = summarizeRoutes(routesResult);
@@ -728,7 +756,19 @@ server.tool(
728
756
  const publicRoutes = routes.filter((route) => route.publicMethods?.length);
729
757
 
730
758
  const payload = {
759
+ targetInstance: targetInstance(),
731
760
  apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
761
+ partialErrors: collectPartialErrors({
762
+ metadata,
763
+ routesResult,
764
+ methodsResult,
765
+ gqlResult,
766
+ flowsResult,
767
+ websocketResult,
768
+ storageResult,
769
+ settingsResult,
770
+ meResult,
771
+ }),
732
772
  authenticatedUser: Array.isArray(meResult?.data) ? meResult.data[0] || null : meResult?.data || null,
733
773
  database: getMetadataDatabaseContext(metadata, tables),
734
774
  counts: {
@@ -779,14 +819,14 @@ server.tool(
779
819
  'discover_query_capabilities',
780
820
  [
781
821
  'Discover Enfyra query/filter/deep-fetch capabilities for the live instance.',
782
- 'Optionally pass tableName to include columns, relations, primary key, route paths, and examples for that table.',
822
+ 'Prefer passing tableName. Without tableName this returns only generic query rules. Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
783
823
  ].join(' '),
784
824
  {
785
825
  tableName: z.string().optional().describe('Optional table name to summarize query fields and relation/deep capabilities.'),
786
826
  },
787
827
  async ({ tableName }) => {
788
- const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
789
- const routesResult = await fetchAPI(ENFYRA_API_URL, '/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000');
828
+ const metadata = await discoveryFetch('/metadata');
829
+ const routesResult = await discoveryFetch('/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000');
790
830
  const tables = normalizeTables(metadata);
791
831
  const routes = summarizeRoutes(routesResult);
792
832
  const table = tableName ? tables.find((item) => item.name === tableName) : null;
@@ -796,6 +836,8 @@ server.tool(
796
836
  : [];
797
837
 
798
838
  const payload = {
839
+ targetInstance: targetInstance(),
840
+ partialErrors: collectPartialErrors({ metadata, routesResult }),
799
841
  operators: {
800
842
  filter: FILTER_OPERATORS,
801
843
  fieldPermissionConditions: FIELD_PERMISSION_CONDITION_OPERATORS,
@@ -859,11 +901,12 @@ server.tool(
859
901
  'discover_script_contexts',
860
902
  [
861
903
  'Discover runtime script contexts and macro availability for handlers, hooks, flows, websocket scripts, GraphQL, packages, and extensions.',
862
- 'Use before writing dynamic JavaScript logic so the model does not mix context variables across surfaces.',
904
+ 'Use before writing dynamic JavaScript logic so the model does not mix context variables across surfaces. This tool is static and safe to call alone; avoid running it in parallel with other broad discovery calls.',
863
905
  ].join(' '),
864
906
  {},
865
907
  async () => {
866
908
  const payload = {
909
+ targetInstance: targetInstance(),
867
910
  transformer: {
868
911
  rule: 'Dynamic server scripts are transformed before sandbox execution. Macros expand to $ctx paths; comments are not transformed.',
869
912
  preferredSyntax: 'Prefer template macros in generated Enfyra scripts. Use macros such as @BODY/@QUERY/@PARAMS/@USER/@REQ/@RES/@REPOS/@CACHE/@HELPERS/@FETCH/@STORAGE/@UPLOADED_FILE/@SOCKET/@TRIGGER/@DATA/@ERROR/@STATUS/@ENV/@PKGS/@LOGS/@SHARE/@API/@THROW* instead of raw $ctx access whenever a macro exists. Use raw $ctx only for fields without a macro.',
@@ -1691,6 +1734,51 @@ async function collectRestDefinitionState() {
1691
1734
  };
1692
1735
  }
1693
1736
 
1737
+ async function collectFeatureSearchState() {
1738
+ const metadata = await discoveryFetch('/metadata');
1739
+ const routesResult = await discoveryFetch('/enfyra_route?limit=500');
1740
+ const handlersResult = await discoveryFetch('/enfyra_route_handler?limit=500');
1741
+ const preHooksResult = await discoveryFetch('/enfyra_pre_hook?limit=500');
1742
+ const postHooksResult = await discoveryFetch('/enfyra_post_hook?limit=500');
1743
+ const routePermissionsResult = await discoveryFetch('/enfyra_route_permission?limit=500');
1744
+ const guardsResult = await discoveryFetch('/enfyra_guard?limit=500');
1745
+ const guardRulesResult = await discoveryFetch('/enfyra_guard_rule?limit=500');
1746
+ const fieldPermissionsResult = await discoveryFetch('/enfyra_field_permission?limit=500');
1747
+ const columnRulesResult = await discoveryFetch('/enfyra_column_rule?limit=500');
1748
+ const methodsResult = await discoveryFetch('/enfyra_method?limit=100');
1749
+ const methodIdNameMap = Object.fromEntries(
1750
+ unwrapData(methodsResult).map((method) => [String(getId(method)), method.name]),
1751
+ );
1752
+
1753
+ return {
1754
+ metadata,
1755
+ tables: normalizeTables(metadata),
1756
+ routes: unwrapData(routesResult),
1757
+ handlers: unwrapData(handlersResult),
1758
+ preHooks: unwrapData(preHooksResult),
1759
+ postHooks: unwrapData(postHooksResult),
1760
+ routePermissions: unwrapData(routePermissionsResult),
1761
+ guards: unwrapData(guardsResult),
1762
+ guardRules: unwrapData(guardRulesResult),
1763
+ fieldPermissions: unwrapData(fieldPermissionsResult),
1764
+ columnRules: unwrapData(columnRulesResult),
1765
+ methodIdNameMap,
1766
+ partialErrors: collectPartialErrors({
1767
+ metadata,
1768
+ routesResult,
1769
+ handlersResult,
1770
+ preHooksResult,
1771
+ postHooksResult,
1772
+ routePermissionsResult,
1773
+ guardsResult,
1774
+ guardRulesResult,
1775
+ fieldPermissionsResult,
1776
+ columnRulesResult,
1777
+ methodsResult,
1778
+ }),
1779
+ };
1780
+ }
1781
+
1694
1782
  function enrichRoute(route, state) {
1695
1783
  const routeId = getId(route);
1696
1784
  const routeHandlers = state.handlers
@@ -1844,14 +1932,20 @@ server.tool(
1844
1932
  'inspect_feature',
1845
1933
  [
1846
1934
  'Search live REST/system metadata for a feature name, route path, table, handler, hook, guard, or permission.',
1847
- 'Use when the user mentions a capability and you need to find where it lives before editing.',
1935
+ 'Use when the user mentions a capability and you need to find where it lives before editing. Keep the query specific; broad searches return bounded summaries.',
1848
1936
  ].join(' '),
1849
1937
  {
1850
1938
  query: z.string().describe('Feature keyword, table name, route path, handler text, hook name, or guard name'),
1939
+ limit: z.number().int().positive().max(25).optional().default(8).describe('Maximum matches returned per section. Default 8 to keep output small.'),
1851
1940
  },
1852
- async ({ query }) => {
1853
- const state = await collectRestDefinitionState();
1854
- const q = query.toLowerCase();
1941
+ async ({ query, limit }) => {
1942
+ const rawQuery = String(query || '').trim();
1943
+ if (rawQuery.length < 2) {
1944
+ throw new Error('inspect_feature query must be at least 2 characters. Use a table name, route path, event name, or specific feature keyword.');
1945
+ }
1946
+ const max = Math.max(1, Math.min(Number(limit || 8), 25));
1947
+ const state = await collectFeatureSearchState();
1948
+ const q = rawQuery.toLowerCase();
1855
1949
  const matchesText = (value) => JSON.stringify(value ?? '').toLowerCase().includes(q);
1856
1950
  const tableMatches = state.tables.filter((table) => matchesText({
1857
1951
  name: table.name,
@@ -1871,7 +1965,10 @@ server.tool(
1871
1965
  ];
1872
1966
 
1873
1967
  const payload = {
1874
- query,
1968
+ targetInstance: targetInstance(),
1969
+ query: rawQuery,
1970
+ limit: max,
1971
+ partialErrors: state.partialErrors,
1875
1972
  counts: {
1876
1973
  tables: tableMatches.length,
1877
1974
  routes: routeMatches.length,
@@ -1881,13 +1978,14 @@ server.tool(
1881
1978
  guards: guardMatches.length,
1882
1979
  permissions: permissionMatches.length,
1883
1980
  },
1884
- tables: tableMatches.map(summarizeTable).slice(0, 20),
1885
- routes: routeMatches.map((route) => enrichRoute(route, state)).slice(0, 20),
1886
- handlers: handlerMatches.slice(0, 20),
1887
- preHooks: preHookMatches.slice(0, 20),
1888
- postHooks: postHookMatches.slice(0, 20),
1889
- guards: guardMatches.slice(0, 20),
1890
- permissions: permissionMatches.slice(0, 20),
1981
+ tables: tableMatches.slice(0, max).map(summarizeTable),
1982
+ routes: routeMatches.slice(0, max).map((route) => enrichRoute(route, state)),
1983
+ handlers: handlerMatches.slice(0, max),
1984
+ preHooks: preHookMatches.slice(0, max),
1985
+ postHooks: postHookMatches.slice(0, max),
1986
+ guards: guardMatches.slice(0, max),
1987
+ permissions: permissionMatches.slice(0, max),
1988
+ detailHint: 'For a specific match, call inspect_table, inspect_route, trace_metadata_usage, or get_script_source instead of broadening this search.',
1891
1989
  };
1892
1990
 
1893
1991
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };