@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 +1 -1
- package/src/lib/mcp-instructions.js +1 -1
- package/src/mcp-server-entry.mjs +140 -42
package/package.json
CHANGED
|
@@ -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.',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -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
|
|
624
|
-
const
|
|
625
|
-
|
|
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
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
'
|
|
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
|
|
789
|
-
const routesResult = await
|
|
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
|
|
1854
|
-
|
|
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
|
-
|
|
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.
|
|
1885
|
-
routes: routeMatches.map((route) => enrichRoute(route, state))
|
|
1886
|
-
handlers: handlerMatches.slice(0,
|
|
1887
|
-
preHooks: preHookMatches.slice(0,
|
|
1888
|
-
postHooks: postHookMatches.slice(0,
|
|
1889
|
-
guards: guardMatches.slice(0,
|
|
1890
|
-
permissions: permissionMatches.slice(0,
|
|
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) }] };
|