@enfyra/mcp-server 0.1.63 → 0.1.64
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 +1 -0
- package/dist/lib/dynamic-endpoint-contract.js +3 -0
- package/dist/lib/dynamic-endpoint-contract.js.map +1 -1
- package/dist/lib/extension-search-tools.js +2 -1
- package/dist/lib/extension-search-tools.js.map +1 -1
- package/dist/lib/extension-sfc-analyzer.d.ts +4 -0
- package/dist/lib/extension-sfc-analyzer.js +137 -0
- package/dist/lib/extension-sfc-analyzer.js.map +1 -0
- package/dist/lib/mcp-examples.js +2 -2
- package/dist/lib/mcp-instructions.js +5 -15
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/mcp-usage-telemetry.js +4 -3
- package/dist/lib/mcp-usage-telemetry.js.map +1 -1
- package/dist/lib/mutation-guards.d.ts +1 -0
- package/dist/lib/mutation-guards.js +28 -0
- package/dist/lib/mutation-guards.js.map +1 -1
- package/dist/lib/platform-operation-tools.d.ts +159 -3
- package/dist/lib/platform-operation-tools.js +361 -78
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.d.ts +9 -2
- package/dist/lib/required-knowledge.js +50 -16
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/response-format.js +39 -25
- package/dist/lib/response-format.js.map +1 -1
- package/dist/lib/runtime-zone-tools.js +4 -3
- package/dist/lib/runtime-zone-tools.js.map +1 -1
- package/dist/lib/session-safety.d.ts +9 -0
- package/dist/lib/session-safety.js +89 -0
- package/dist/lib/session-safety.js.map +1 -0
- package/dist/lib/source-artifacts.js +3 -1
- package/dist/lib/source-artifacts.js.map +1 -1
- package/dist/lib/table-tools.d.ts +5 -0
- package/dist/lib/table-tools.js +46 -5
- package/dist/lib/table-tools.js.map +1 -1
- package/dist/lib/tool-input-normalization.d.ts +4 -0
- package/dist/lib/tool-input-normalization.js +35 -0
- package/dist/lib/tool-input-normalization.js.map +1 -0
- package/dist/lib/tool-routing.d.ts +6 -2
- package/dist/lib/tool-routing.js +33 -8
- package/dist/lib/tool-routing.js.map +1 -1
- package/dist/lib/toolset-filter.d.ts +7 -3
- package/dist/lib/toolset-filter.js +144 -86
- package/dist/lib/toolset-filter.js.map +1 -1
- package/dist/lib/types.d.ts +20 -0
- package/dist/mcp-server-entry.js +49 -19
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +4 -1
package/dist/mcp-server-entry.js
CHANGED
|
@@ -65,19 +65,20 @@ import { registerPlatformOperationTools, validateExtensionCode } from './lib/pla
|
|
|
65
65
|
import { registerRuntimeZoneTools } from './lib/runtime-zone-tools.js';
|
|
66
66
|
import { registerDynamicRepositoryBuilder } from './lib/dynamic-repository-builder.js';
|
|
67
67
|
import { assertCreateHandlerRouteBoundary } from './lib/dynamic-endpoint-contract.js';
|
|
68
|
-
import { parseRecordBatchData, parseRecordData, prepareRecordBatchMutation, prepareRecordMutation, validatePortableScriptSource, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
|
|
69
|
-
import { assertDynamicCodeKnowledgeAck, assertDynamicCodeKnowledgeAckIf, assertExtensionKnowledgeAckIf, assertGlobalRulesAck, buildRequiredKnowledgePayload, dynamicCodeKnowledgeAckParam, extensionKnowledgeAckParam, globalRulesAckParam, } from './lib/required-knowledge.js';
|
|
68
|
+
import { assertGenericRecordMutationAllowed, parseRecordBatchData, parseRecordData, prepareRecordBatchMutation, prepareRecordMutation, validatePortableScriptSource, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
|
|
69
|
+
import { assertDynamicCodeKnowledgeAck, assertDynamicCodeKnowledgeAckIf, assertExtensionKnowledgeAckIf, assertGlobalRulesAck, acknowledgeRequiredKnowledge, buildRequiredKnowledgePayload, dynamicCodeKnowledgeAckParam, extensionKnowledgeAckParam, globalRulesAckParam, } from './lib/required-knowledge.js';
|
|
70
70
|
import { validateMainTableRoutePath } from './lib/route-guards.js';
|
|
71
71
|
import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
|
|
72
72
|
import { startMcpUsageTelemetry } from './lib/mcp-usage-telemetry.js';
|
|
73
73
|
import { startRuntimeCacheSocket } from './lib/runtime-cache-socket.js';
|
|
74
74
|
import { executeSequentialBatch } from './lib/sequential-batch.js';
|
|
75
75
|
import { compactSourceFields, writeSourceArtifact } from './lib/source-artifacts.js';
|
|
76
|
-
import { installToolsetFilter, normalizeMcpToolset, summarizeToolsetForInstructions } from './lib/toolset-filter.js';
|
|
76
|
+
import { installToolsetFilter, normalizeMcpProfile, normalizeMcpToolset, summarizeToolsetForInstructions } from './lib/toolset-filter.js';
|
|
77
77
|
import { findRoutePermission, mergeMethodNames, normalizeMethodNames, resolveRoleByNameOrId, routeAvailableMethodNames, routePublicMethodNames, summarizeRouteAccess, summarizeRoutePermission, validateMethodsForRoute, } from './lib/route-permission-tools.js';
|
|
78
78
|
// Initialize auth module
|
|
79
79
|
initAuth(ENFYRA_API_URL, ENFYRA_API_TOKEN);
|
|
80
80
|
const MCP_TOOLSET = normalizeMcpToolset(process.env.ENFYRA_MCP_TOOLSET);
|
|
81
|
+
const MCP_PROFILE = normalizeMcpProfile(process.env.ENFYRA_MCP_PROFILE);
|
|
81
82
|
const CAPABILITY_AREAS = [
|
|
82
83
|
{
|
|
83
84
|
area: 'Schema and metadata',
|
|
@@ -809,12 +810,12 @@ const server = new McpServer({
|
|
|
809
810
|
version: '1.0.0',
|
|
810
811
|
}, {
|
|
811
812
|
instructions: buildMcpServerInstructions(ENFYRA_API_URL, {
|
|
812
|
-
toolsetSummary: summarizeToolsetForInstructions(MCP_TOOLSET),
|
|
813
|
+
toolsetSummary: summarizeToolsetForInstructions(MCP_TOOLSET, MCP_PROFILE),
|
|
813
814
|
}),
|
|
814
815
|
});
|
|
815
816
|
installColumnarToolFormatter(server);
|
|
816
|
-
installToolsetFilter(server, MCP_TOOLSET);
|
|
817
|
-
startMcpUsageTelemetry(ENFYRA_API_URL, MCP_TOOLSET);
|
|
817
|
+
installToolsetFilter(server, MCP_TOOLSET, MCP_PROFILE);
|
|
818
|
+
startMcpUsageTelemetry(ENFYRA_API_URL, `${MCP_TOOLSET}:${MCP_PROFILE}`);
|
|
818
819
|
// ============================================================================
|
|
819
820
|
// METADATA TOOLS
|
|
820
821
|
// ============================================================================
|
|
@@ -823,8 +824,12 @@ server.tool('get_enfyra_required_knowledge', [
|
|
|
823
824
|
'Call this before creating or updating dynamic server code or Enfyra extension code. Read the returned contracts and pass the matching ack key into write tools.',
|
|
824
825
|
'Pass scope to only load rules for the current task domain: "schema" (table/data/route/permission/guard work), "dynamic-code" (handler/hook/websocket/resolver scripts), "extension" (admin UI/menu/shell), or "flow". Omitting scope returns all rules.',
|
|
825
826
|
].join(' '), {
|
|
826
|
-
scope: z.enum(['schema', 'dynamic-code', 'extension', 'flow']).optional().describe('Limit knowledge to one domain.
|
|
827
|
-
}, async ({ scope }) =>
|
|
827
|
+
scope: z.enum(['full', 'schema', 'dynamic-code', 'extension', 'flow']).optional().describe('Limit knowledge to one domain. Use full or omit scope to load all rules.'),
|
|
828
|
+
}, async ({ scope }) => {
|
|
829
|
+
const payload = buildRequiredKnowledgePayload(scope);
|
|
830
|
+
const sessionAcknowledgement = acknowledgeRequiredKnowledge(scope);
|
|
831
|
+
return jsonContent({ ...payload, sessionAcknowledgement });
|
|
832
|
+
});
|
|
828
833
|
server.tool('get_all_metadata', 'Get a lightweight table catalog. Use get_table_metadata or inspect_table to fetch one table schema.', {
|
|
829
834
|
includeFull: z.boolean().optional().default(false).describe('Fetch per-table metadata for the selected catalog entries. Default false keeps discovery lightweight.'),
|
|
830
835
|
search: z.string().optional().describe('Optional table-name/alias substring filter.'),
|
|
@@ -899,7 +904,7 @@ server.tool('discover_enfyra_workflows', [
|
|
|
899
904
|
risk: z.string().optional().default('unknown').describe('Highest expected operation risk. Preferred values: read, write, destructive, debug, unknown. Natural terms such as low, medium, or high are accepted and normalized.'),
|
|
900
905
|
detail: z.enum(['summary', 'plan', 'full']).optional().default('summary').describe('summary lists candidate workflows; plan adds tool sequence and avoidTools; full also includes matching keywords.'),
|
|
901
906
|
limit: z.number().int().positive().max(10).optional().default(5).describe('Maximum workflows to return.'),
|
|
902
|
-
}, async (input) => jsonContent(discoverWorkflowRoutes(input)));
|
|
907
|
+
}, async (input) => jsonContent(discoverWorkflowRoutes(input, MCP_PROFILE)));
|
|
903
908
|
server.tool('discover_enfyra_system', [
|
|
904
909
|
'Call this first when you need to understand the live Enfyra instance.',
|
|
905
910
|
'Returns a concise capability map from live metadata/routes/method rows, including schema management, REST route behavior, GraphQL enablement, and relation handling.',
|
|
@@ -972,7 +977,7 @@ server.tool('discover_enfyra_system', [
|
|
|
972
977
|
adminTesting: {
|
|
973
978
|
runAdminTest: 'run_admin_test wraps POST /admin/test/run for flow_step, websocket_event, and websocket_connection scripts.',
|
|
974
979
|
testFlowStep: 'test_flow_step also wraps POST /admin/test/run with kind=flow_step.',
|
|
975
|
-
triggerFlow: 'trigger_flow wraps POST /admin/flow/trigger/:id
|
|
980
|
+
triggerFlow: 'trigger_flow resolves a saved enabled flow, then wraps POST /admin/flow/trigger/:id. Use test_flow_step for disabled flows.',
|
|
976
981
|
},
|
|
977
982
|
graphql: {
|
|
978
983
|
endpoint: `${ENFYRA_API_URL.replace(/\/$/, '')}/graphql`,
|
|
@@ -1224,9 +1229,9 @@ server.tool('discover_script_contexts', [
|
|
|
1224
1229
|
},
|
|
1225
1230
|
throws: '@THROW maps to $ctx.$throw. Numeric helpers are raw HTTP message helpers: @THROW400(message), @THROW404(message), @THROW409(message), @THROW422(message, detailsObject?), @THROW500(message). Numeric helper details must be an object/array, e.g. @THROW404("Project not found", { id }); do not use @THROW404("Project", id) as a semantic shortcut. Use @THROW.http(status, message, details?) for dynamic status codes. Use @THROW.notFound(resource, id?) and @THROW.duplicate(resource, field, value) only when you intentionally want Enfyra-formatted semantic messages.',
|
|
1226
1231
|
helpers: {
|
|
1227
|
-
core: '$ctx.$helpers includes $bcrypt.hash/compare, autoSlug(text), $fetch, $sleep(ms) capped by the runtime, and $crypto. HTTP and GraphQL contexts also expose $jwt through $ctx.$helpers.',
|
|
1232
|
+
core: '$ctx.$helpers includes $bcrypt.hash/compare, autoSlug(text), $fetch, $sleep(ms) capped by the runtime, and $crypto. HTTP and GraphQL contexts also expose $jwt through $ctx.$helpers. Every helper method crosses the async executor bridge: await its result before property access, interpolation, concatenation, or persistence.',
|
|
1228
1233
|
fetch: '@FETCH maps to $ctx.$helpers.$fetch for outbound HTTP calls from server scripts. Keep secrets in encrypted fields instead of embedding them in sourceCode.',
|
|
1229
|
-
crypto: '$ctx.$helpers.$crypto exposes bounded runtime crypto helpers: randomUUID(), randomBytes(size, encoding), sha256(value, encoding), hmacSha256(value, secret, encoding), and generateSshKeyPair(comment). Use generateSshKeyPair for SSH key material. Do not use legacy $ctx.$helpers.$ssh.',
|
|
1234
|
+
crypto: '$ctx.$helpers.$crypto exposes bounded runtime crypto helpers: randomUUID(), randomBytes(size, encoding), sha256(value, encoding), hmacSha256(value, secret, encoding), and generateSshKeyPair(comment). Await every call, including helpers whose host implementation is synchronous, for example const id = await @HELPERS.$crypto.randomUUID(). Use generateSshKeyPair for SSH key material. Do not use legacy $ctx.$helpers.$ssh.',
|
|
1230
1235
|
files: '$ctx.$storage.$upload and $ctx.$storage.$update accept file: @UPLOADED_FILE for request uploads and stream from the server temp file path. $ctx.$storage.$registerFile creates a enfyra_file record for an object that already exists in storage without uploading bytes. Use buffer only for small generated/transformed files; do not use @UPLOADED_FILE.buffer.',
|
|
1231
1236
|
},
|
|
1232
1237
|
env: '$ctx.$env exposes a sanitized process env snapshot with exact sensitive keys removed: DB_URI, DB_REPLICA_URIS, REDIS_URI, SECRET_KEY, and ADMIN_PASSWORD. Store app secrets in unpublished isEncrypted fields instead of reading them from $env.',
|
|
@@ -1506,6 +1511,7 @@ server.tool('create_records', 'Create one or more route-backed records. Always p
|
|
|
1506
1511
|
}, async ({ tableName, records, queryParams, maxRecords, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
|
|
1507
1512
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
1508
1513
|
validateTableName(tableName);
|
|
1514
|
+
assertGenericRecordMutationAllowed('create', tableName);
|
|
1509
1515
|
const parsedRecords = parseRecordBatchData(records);
|
|
1510
1516
|
if (parsedRecords.length > maxRecords) {
|
|
1511
1517
|
throw new Error(`create_records received ${parsedRecords.length} records, above maxRecords=${maxRecords}. Split the batch deliberately.`);
|
|
@@ -1567,6 +1573,7 @@ server.tool('update_records', 'Update one or more records in one MCP call. Pass
|
|
|
1567
1573
|
}, async ({ tableName, items, maxItems, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
|
|
1568
1574
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
1569
1575
|
validateTableName(tableName);
|
|
1576
|
+
assertGenericRecordMutationAllowed('update', tableName);
|
|
1570
1577
|
const parsedItems = parseBulkItemsArg('items', items);
|
|
1571
1578
|
assertMaxBulkItems('update_records', parsedItems, maxItems);
|
|
1572
1579
|
assertNoDuplicateBulkIds('update_records', parsedItems);
|
|
@@ -1754,6 +1761,7 @@ server.tool('delete_records', 'Delete one or more route-backed records in one MC
|
|
|
1754
1761
|
globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
|
|
1755
1762
|
}, async ({ tableName, items, maxItems, confirm, skipNotFound, globalRulesAckKey }) => {
|
|
1756
1763
|
validateTableName(tableName);
|
|
1764
|
+
assertGenericRecordMutationAllowed('delete', tableName);
|
|
1757
1765
|
const parsedItems = parseBulkItemsArg('items', items);
|
|
1758
1766
|
assertMaxBulkItems('delete_records', parsedItems, maxItems);
|
|
1759
1767
|
assertNoDuplicateBulkIds('delete_records', parsedItems);
|
|
@@ -1967,7 +1975,7 @@ server.tool('run_admin_test', [
|
|
|
1967
1975
|
'kind=script captures logs but not socket emitted calls. Use kind=websocket_event or kind=websocket_connection when emitted capture is required; admin websocket tests still do not prove a real Socket.IO client transport/handshake.',
|
|
1968
1976
|
].join(' '), {
|
|
1969
1977
|
kind: z.enum(['script', 'flow_step', 'websocket_event', 'websocket_connection']).describe('Admin test kind'),
|
|
1970
|
-
body: z.string().describe('JSON body for the test. Include script and optional context for script; type/config for flow_step; or script/gatewayPath/eventName/payload for websocket tests. Do not include kind; the tool adds it.'),
|
|
1978
|
+
body: z.string().describe('JSON body for the test. Include script and optional context for script; type/config plus payload for flow_step; or script/gatewayPath/eventName/payload for websocket tests. Do not include kind; the tool adds it.'),
|
|
1971
1979
|
}, async ({ kind, body }) => {
|
|
1972
1980
|
const parsed = body ? JSON.parse(body) : {};
|
|
1973
1981
|
const sourceCode = kind === 'flow_step'
|
|
@@ -1981,18 +1989,28 @@ server.tool('run_admin_test', [
|
|
|
1981
1989
|
});
|
|
1982
1990
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1983
1991
|
});
|
|
1984
|
-
server.tool('test_flow_step', 'Test a single flow step without saving it. Wraps POST /admin/test/run with kind=flow_step.', {
|
|
1992
|
+
server.tool('test_flow_step', 'Test a single flow step without saving it. Wraps POST /admin/test/run with kind=flow_step. Pass runtime @FLOW_PAYLOAD data through payload; the tool forwards it using the ESV test-run contract.', {
|
|
1985
1993
|
type: z.enum(['script', 'condition', 'query', 'create', 'update', 'delete', 'http', 'trigger_flow', 'sleep', 'log']).describe('Flow step type'),
|
|
1986
1994
|
config: z.string().describe('Step config as JSON string'),
|
|
1987
1995
|
timeout: z.number().optional().describe('Timeout in ms'),
|
|
1988
1996
|
key: z.string().optional().describe('Optional step key for mock flow context'),
|
|
1989
|
-
|
|
1990
|
-
|
|
1997
|
+
payload: z.union([z.record(z.any()), z.string()]).optional().describe('Runtime payload object exposed to the script as @FLOW_PAYLOAD. A JSON object string is accepted for compatibility.'),
|
|
1998
|
+
mockFlow: z.string().optional().describe('Optional advanced mockFlow JSON object for $last/$meta or other flow context. Use payload for @FLOW_PAYLOAD.'),
|
|
1999
|
+
}, async ({ type, config, timeout, key, payload, mockFlow }) => {
|
|
2000
|
+
const parsedConfig = JSON.parse(config);
|
|
2001
|
+
const sourceCode = parsedConfig?.sourceCode ?? parsedConfig?.code;
|
|
2002
|
+
if (typeof sourceCode === 'string')
|
|
2003
|
+
validatePortableScriptSource(sourceCode);
|
|
2004
|
+
const parsedPayload = typeof payload === 'string' ? JSON.parse(payload) : payload;
|
|
2005
|
+
if (parsedPayload !== undefined && (!parsedPayload || typeof parsedPayload !== 'object' || Array.isArray(parsedPayload))) {
|
|
2006
|
+
throw new Error('payload must be a JSON object.');
|
|
2007
|
+
}
|
|
1991
2008
|
const body = {
|
|
1992
2009
|
type,
|
|
1993
|
-
config:
|
|
2010
|
+
config: parsedConfig,
|
|
1994
2011
|
...(timeout ? { timeout } : {}),
|
|
1995
2012
|
...(key ? { key } : {}),
|
|
2013
|
+
...(parsedPayload !== undefined ? { payload: parsedPayload } : {}),
|
|
1996
2014
|
...(mockFlow ? { mockFlow: JSON.parse(mockFlow) } : {}),
|
|
1997
2015
|
};
|
|
1998
2016
|
const result = await fetchAPI(ENFYRA_API_URL, '/admin/test/run', {
|
|
@@ -2001,11 +2019,23 @@ server.tool('test_flow_step', 'Test a single flow step without saving it. Wraps
|
|
|
2001
2019
|
});
|
|
2002
2020
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2003
2021
|
});
|
|
2004
|
-
server.tool('trigger_flow', 'Trigger
|
|
2022
|
+
server.tool('trigger_flow', 'Trigger an enabled saved flow by id or name. Disabled flows are not registered for execution; use test_flow_step to verify their step contract without enabling them.', {
|
|
2005
2023
|
flowIdOrName: z.union([z.string(), z.number()]).describe('Flow id or name accepted by FlowService.trigger'),
|
|
2006
2024
|
payload: z.string().optional().describe('Payload JSON object. Default {}.'),
|
|
2007
2025
|
}, async ({ flowIdOrName, payload }) => {
|
|
2008
|
-
const
|
|
2026
|
+
const rawIdentifier = String(flowIdOrName);
|
|
2027
|
+
const filter = typeof flowIdOrName === 'number' || /^\d+$/.test(rawIdentifier)
|
|
2028
|
+
? { id: { _eq: flowIdOrName } }
|
|
2029
|
+
: { name: { _eq: rawIdentifier } };
|
|
2030
|
+
const lookup = await fetchAPI(ENFYRA_API_URL, `/enfyra_flow?filter=${encodeURIComponent(JSON.stringify(filter))}&limit=1&fields=id,_id,name,isEnabled`);
|
|
2031
|
+
const flow = unwrapData(lookup)[0];
|
|
2032
|
+
if (!flow)
|
|
2033
|
+
throw new Error(`Flow not found: ${rawIdentifier}`);
|
|
2034
|
+
if (flow.isEnabled === false) {
|
|
2035
|
+
throw new Error(`Flow "${flow.name || rawIdentifier}" is disabled and is not registered for execution. Use test_flow_step to verify its saved step contract, or explicitly enable the flow before trigger_flow.`);
|
|
2036
|
+
}
|
|
2037
|
+
const flowId = flow.id ?? flow._id;
|
|
2038
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/admin/flow/trigger/${encodeURIComponent(String(flowId))}`, {
|
|
2009
2039
|
method: 'POST',
|
|
2010
2040
|
body: JSON.stringify({ payload: payload ? JSON.parse(payload) : {} }),
|
|
2011
2041
|
});
|