@enfyra/mcp-server 0.1.62 → 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.d.ts +12 -0
- package/dist/lib/dynamic-endpoint-contract.js +148 -0
- package/dist/lib/dynamic-endpoint-contract.js.map +1 -0
- package/dist/lib/dynamic-repository-builder.d.ts +7 -0
- package/dist/lib/dynamic-repository-builder.js +8 -0
- package/dist/lib/dynamic-repository-builder.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 +117 -8
- package/dist/lib/mcp-examples.js.map +1 -1
- package/dist/lib/mcp-instructions.js +5 -15
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/mcp-usage-telemetry.js +41 -2
- 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 +42 -0
- package/dist/lib/mutation-guards.js.map +1 -1
- package/dist/lib/platform-operation-tools.d.ts +160 -3
- package/dist/lib/platform-operation-tools.js +454 -95
- 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 +60 -17
- 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 +9 -1
- package/dist/lib/table-tools.js +69 -9
- 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 +52 -16
- 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 +62 -0
- package/dist/mcp-server-entry.js +79 -23
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +4 -1
package/dist/mcp-server-entry.js
CHANGED
|
@@ -64,19 +64,21 @@ import { getSupportedColumnTypesFromMetadata, registerTableTools } from './lib/t
|
|
|
64
64
|
import { registerPlatformOperationTools, validateExtensionCode } from './lib/platform-operation-tools.js';
|
|
65
65
|
import { registerRuntimeZoneTools } from './lib/runtime-zone-tools.js';
|
|
66
66
|
import { registerDynamicRepositoryBuilder } from './lib/dynamic-repository-builder.js';
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
67
|
+
import { assertCreateHandlerRouteBoundary } from './lib/dynamic-endpoint-contract.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';
|
|
69
70
|
import { validateMainTableRoutePath } from './lib/route-guards.js';
|
|
70
71
|
import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
|
|
71
72
|
import { startMcpUsageTelemetry } from './lib/mcp-usage-telemetry.js';
|
|
72
73
|
import { startRuntimeCacheSocket } from './lib/runtime-cache-socket.js';
|
|
73
74
|
import { executeSequentialBatch } from './lib/sequential-batch.js';
|
|
74
75
|
import { compactSourceFields, writeSourceArtifact } from './lib/source-artifacts.js';
|
|
75
|
-
import { installToolsetFilter, normalizeMcpToolset, summarizeToolsetForInstructions } from './lib/toolset-filter.js';
|
|
76
|
+
import { installToolsetFilter, normalizeMcpProfile, normalizeMcpToolset, summarizeToolsetForInstructions } from './lib/toolset-filter.js';
|
|
76
77
|
import { findRoutePermission, mergeMethodNames, normalizeMethodNames, resolveRoleByNameOrId, routeAvailableMethodNames, routePublicMethodNames, summarizeRouteAccess, summarizeRoutePermission, validateMethodsForRoute, } from './lib/route-permission-tools.js';
|
|
77
78
|
// Initialize auth module
|
|
78
79
|
initAuth(ENFYRA_API_URL, ENFYRA_API_TOKEN);
|
|
79
80
|
const MCP_TOOLSET = normalizeMcpToolset(process.env.ENFYRA_MCP_TOOLSET);
|
|
81
|
+
const MCP_PROFILE = normalizeMcpProfile(process.env.ENFYRA_MCP_PROFILE);
|
|
80
82
|
const CAPABILITY_AREAS = [
|
|
81
83
|
{
|
|
82
84
|
area: 'Schema and metadata',
|
|
@@ -808,12 +810,12 @@ const server = new McpServer({
|
|
|
808
810
|
version: '1.0.0',
|
|
809
811
|
}, {
|
|
810
812
|
instructions: buildMcpServerInstructions(ENFYRA_API_URL, {
|
|
811
|
-
toolsetSummary: summarizeToolsetForInstructions(MCP_TOOLSET),
|
|
813
|
+
toolsetSummary: summarizeToolsetForInstructions(MCP_TOOLSET, MCP_PROFILE),
|
|
812
814
|
}),
|
|
813
815
|
});
|
|
814
816
|
installColumnarToolFormatter(server);
|
|
815
|
-
installToolsetFilter(server, MCP_TOOLSET);
|
|
816
|
-
startMcpUsageTelemetry(ENFYRA_API_URL, MCP_TOOLSET);
|
|
817
|
+
installToolsetFilter(server, MCP_TOOLSET, MCP_PROFILE);
|
|
818
|
+
startMcpUsageTelemetry(ENFYRA_API_URL, `${MCP_TOOLSET}:${MCP_PROFILE}`);
|
|
817
819
|
// ============================================================================
|
|
818
820
|
// METADATA TOOLS
|
|
819
821
|
// ============================================================================
|
|
@@ -822,8 +824,12 @@ server.tool('get_enfyra_required_knowledge', [
|
|
|
822
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.',
|
|
823
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.',
|
|
824
826
|
].join(' '), {
|
|
825
|
-
scope: z.enum(['schema', 'dynamic-code', 'extension', 'flow']).optional().describe('Limit knowledge to one domain.
|
|
826
|
-
}, 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
|
+
});
|
|
827
833
|
server.tool('get_all_metadata', 'Get a lightweight table catalog. Use get_table_metadata or inspect_table to fetch one table schema.', {
|
|
828
834
|
includeFull: z.boolean().optional().default(false).describe('Fetch per-table metadata for the selected catalog entries. Default false keeps discovery lightweight.'),
|
|
829
835
|
search: z.string().optional().describe('Optional table-name/alias substring filter.'),
|
|
@@ -898,7 +904,7 @@ server.tool('discover_enfyra_workflows', [
|
|
|
898
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.'),
|
|
899
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.'),
|
|
900
906
|
limit: z.number().int().positive().max(10).optional().default(5).describe('Maximum workflows to return.'),
|
|
901
|
-
}, async (input) => jsonContent(discoverWorkflowRoutes(input)));
|
|
907
|
+
}, async (input) => jsonContent(discoverWorkflowRoutes(input, MCP_PROFILE)));
|
|
902
908
|
server.tool('discover_enfyra_system', [
|
|
903
909
|
'Call this first when you need to understand the live Enfyra instance.',
|
|
904
910
|
'Returns a concise capability map from live metadata/routes/method rows, including schema management, REST route behavior, GraphQL enablement, and relation handling.',
|
|
@@ -971,7 +977,7 @@ server.tool('discover_enfyra_system', [
|
|
|
971
977
|
adminTesting: {
|
|
972
978
|
runAdminTest: 'run_admin_test wraps POST /admin/test/run for flow_step, websocket_event, and websocket_connection scripts.',
|
|
973
979
|
testFlowStep: 'test_flow_step also wraps POST /admin/test/run with kind=flow_step.',
|
|
974
|
-
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.',
|
|
975
981
|
},
|
|
976
982
|
graphql: {
|
|
977
983
|
endpoint: `${ENFYRA_API_URL.replace(/\/$/, '')}/graphql`,
|
|
@@ -1203,6 +1209,12 @@ server.tool('discover_script_contexts', [
|
|
|
1203
1209
|
'@STATUS': '$ctx.$statusCode',
|
|
1204
1210
|
'@ERROR': '$ctx.$error',
|
|
1205
1211
|
},
|
|
1212
|
+
logging: '@LOGS is a callable function. Use @LOGS(message, details?) such as @LOGS("Approval requested", { requestId }); do not use @LOGS.info, @LOGS.warn, @LOGS.error, or @LOGS.debug.',
|
|
1213
|
+
socket: {
|
|
1214
|
+
contract: '@SOCKET has no generic emit() method.',
|
|
1215
|
+
boundWebsocketMethods: ['reply(event, data)', 'join(room)', 'leave(room)', 'emitToCurrentRoom(room, event, data)', 'broadcastToRoom(room, event, data)', 'disconnect()'],
|
|
1216
|
+
globalMethods: ['emitToGateway(path, event, data)', 'emitToRoom(path, room, event, data)', 'emitToUser(userId, event, data)', 'broadcast(event, data)', 'roomSize(room)'],
|
|
1217
|
+
},
|
|
1206
1218
|
flowMacros: {
|
|
1207
1219
|
'@FLOW': '$ctx.$flow',
|
|
1208
1220
|
'@FLOW_PAYLOAD': '$ctx.$flow.$payload',
|
|
@@ -1217,9 +1229,9 @@ server.tool('discover_script_contexts', [
|
|
|
1217
1229
|
},
|
|
1218
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.',
|
|
1219
1231
|
helpers: {
|
|
1220
|
-
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.',
|
|
1221
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.',
|
|
1222
|
-
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.',
|
|
1223
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.',
|
|
1224
1236
|
},
|
|
1225
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.',
|
|
@@ -1499,6 +1511,7 @@ server.tool('create_records', 'Create one or more route-backed records. Always p
|
|
|
1499
1511
|
}, async ({ tableName, records, queryParams, maxRecords, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
|
|
1500
1512
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
1501
1513
|
validateTableName(tableName);
|
|
1514
|
+
assertGenericRecordMutationAllowed('create', tableName);
|
|
1502
1515
|
const parsedRecords = parseRecordBatchData(records);
|
|
1503
1516
|
if (parsedRecords.length > maxRecords) {
|
|
1504
1517
|
throw new Error(`create_records received ${parsedRecords.length} records, above maxRecords=${maxRecords}. Split the batch deliberately.`);
|
|
@@ -1560,6 +1573,7 @@ server.tool('update_records', 'Update one or more records in one MCP call. Pass
|
|
|
1560
1573
|
}, async ({ tableName, items, maxItems, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
|
|
1561
1574
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
1562
1575
|
validateTableName(tableName);
|
|
1576
|
+
assertGenericRecordMutationAllowed('update', tableName);
|
|
1563
1577
|
const parsedItems = parseBulkItemsArg('items', items);
|
|
1564
1578
|
assertMaxBulkItems('update_records', parsedItems, maxItems);
|
|
1565
1579
|
assertNoDuplicateBulkIds('update_records', parsedItems);
|
|
@@ -1747,6 +1761,7 @@ server.tool('delete_records', 'Delete one or more route-backed records in one MC
|
|
|
1747
1761
|
globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
|
|
1748
1762
|
}, async ({ tableName, items, maxItems, confirm, skipNotFound, globalRulesAckKey }) => {
|
|
1749
1763
|
validateTableName(tableName);
|
|
1764
|
+
assertGenericRecordMutationAllowed('delete', tableName);
|
|
1750
1765
|
const parsedItems = parseBulkItemsArg('items', items);
|
|
1751
1766
|
assertMaxBulkItems('delete_records', parsedItems, maxItems);
|
|
1752
1767
|
assertNoDuplicateBulkIds('delete_records', parsedItems);
|
|
@@ -1957,29 +1972,45 @@ server.tool('delete_method', 'Preview or delete a enfyra_method record. Only del
|
|
|
1957
1972
|
server.tool('run_admin_test', [
|
|
1958
1973
|
'Run an Enfyra admin test without saving metadata. Wraps POST /admin/test/run.',
|
|
1959
1974
|
'Kinds: script, flow_step, websocket_event, websocket_connection. Use this to validate dynamic script, flow, or websocket behavior before creating records.',
|
|
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.',
|
|
1960
1976
|
].join(' '), {
|
|
1961
1977
|
kind: z.enum(['script', 'flow_step', 'websocket_event', 'websocket_connection']).describe('Admin test kind'),
|
|
1962
|
-
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.'),
|
|
1963
1979
|
}, async ({ kind, body }) => {
|
|
1964
1980
|
const parsed = body ? JSON.parse(body) : {};
|
|
1981
|
+
const sourceCode = kind === 'flow_step'
|
|
1982
|
+
? parsed?.config?.sourceCode ?? parsed?.config?.code
|
|
1983
|
+
: parsed?.script ?? parsed?.sourceCode;
|
|
1984
|
+
if (typeof sourceCode === 'string')
|
|
1985
|
+
validatePortableScriptSource(sourceCode);
|
|
1965
1986
|
const result = await fetchAPI(ENFYRA_API_URL, '/admin/test/run', {
|
|
1966
1987
|
method: 'POST',
|
|
1967
1988
|
body: JSON.stringify({ ...parsed, kind }),
|
|
1968
1989
|
});
|
|
1969
1990
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1970
1991
|
});
|
|
1971
|
-
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.', {
|
|
1972
1993
|
type: z.enum(['script', 'condition', 'query', 'create', 'update', 'delete', 'http', 'trigger_flow', 'sleep', 'log']).describe('Flow step type'),
|
|
1973
1994
|
config: z.string().describe('Step config as JSON string'),
|
|
1974
1995
|
timeout: z.number().optional().describe('Timeout in ms'),
|
|
1975
1996
|
key: z.string().optional().describe('Optional step key for mock flow context'),
|
|
1976
|
-
|
|
1977
|
-
|
|
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
|
+
}
|
|
1978
2008
|
const body = {
|
|
1979
2009
|
type,
|
|
1980
|
-
config:
|
|
2010
|
+
config: parsedConfig,
|
|
1981
2011
|
...(timeout ? { timeout } : {}),
|
|
1982
2012
|
...(key ? { key } : {}),
|
|
2013
|
+
...(parsedPayload !== undefined ? { payload: parsedPayload } : {}),
|
|
1983
2014
|
...(mockFlow ? { mockFlow: JSON.parse(mockFlow) } : {}),
|
|
1984
2015
|
};
|
|
1985
2016
|
const result = await fetchAPI(ENFYRA_API_URL, '/admin/test/run', {
|
|
@@ -1988,11 +2019,23 @@ server.tool('test_flow_step', 'Test a single flow step without saving it. Wraps
|
|
|
1988
2019
|
});
|
|
1989
2020
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1990
2021
|
});
|
|
1991
|
-
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.', {
|
|
1992
2023
|
flowIdOrName: z.union([z.string(), z.number()]).describe('Flow id or name accepted by FlowService.trigger'),
|
|
1993
2024
|
payload: z.string().optional().describe('Payload JSON object. Default {}.'),
|
|
1994
2025
|
}, async ({ flowIdOrName, payload }) => {
|
|
1995
|
-
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))}`, {
|
|
1996
2039
|
method: 'POST',
|
|
1997
2040
|
body: JSON.stringify({ payload: payload ? JSON.parse(payload) : {} }),
|
|
1998
2041
|
});
|
|
@@ -2508,7 +2551,7 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Compl
|
|
|
2508
2551
|
server.tool('create_route', [
|
|
2509
2552
|
'**Use this when the user wants a new REST API route or path** — not `create_tables`. Custom routes must omit `mainTableId`.',
|
|
2510
2553
|
'`mainTableId` is only a marker for canonical table routes such as `/orders`; do not set it for `/orders/stats`, `/reports/summary`, `/auth/login`, or any custom path.',
|
|
2511
|
-
'Do NOT create a new enfyra_table only to expose an endpoint; create a route without `mainTableId`, then have the handler/hook query explicit repos such as `$ctx.$repos.orders`.',
|
|
2554
|
+
'Do NOT create a new enfyra_table only to expose an endpoint; create a route without `mainTableId`, then have the handler/hook query user-facing tables through secure explicit repos such as `#secure.orders` or `$ctx.$repos.secure.orders`.',
|
|
2512
2555
|
'availableMethods = which REST verbs the route responds to. publicMethods = which REST verbs are public (no auth). GraphQL is enabled separately through enfyra_graphql/update_tables graphqlEnabled.',
|
|
2513
2556
|
'After creation the tool auto-reloads routes. Then create handlers for specific methods via create_handler on this route id.',
|
|
2514
2557
|
'Flow: create_route → create_handler (per method) → optionally create_pre_hook / create_post_hook → test via HTTP or admin test APIs (see server instructions).',
|
|
@@ -2561,7 +2604,8 @@ server.tool('create_route', [
|
|
|
2561
2604
|
});
|
|
2562
2605
|
server.tool('create_handler', [
|
|
2563
2606
|
'Create a handler for a route+method. One handler per (route, method) pair.',
|
|
2564
|
-
'Attach to
|
|
2607
|
+
'Attach to a custom route from `create_route` for endpoint-specific or third-party behavior. Do not use this low-level tool to bypass api_endpoint_workflow canonical-collision checks.',
|
|
2608
|
+
'Canonical table routes are shared with eApp/admin CRUD. Adding a new canonical handler requires allowCanonicalRoute=true and main-table repository access; otherwise create a separate custom path.',
|
|
2565
2609
|
'Use sourceCode, not logic/name. Enfyra compiles sourceCode into compiledCode; do not send compiledCode.',
|
|
2566
2610
|
'Handler code runs inside a sandbox with $ctx. Use macros: @BODY, @QUERY, @PARAMS, @USER, @REPOS, @HELPERS, @THROW400..@THROW503, @SOCKET, @PKGS, @LOGS, @SHARE.',
|
|
2567
2611
|
'Call discover_script_contexts first. For explicit user-facing table repos use #secure.table_name or @REPOS.secure.table_name; use #table_name/@REPOS.table_name only for intentional trusted internal access.',
|
|
@@ -2578,9 +2622,20 @@ server.tool('create_handler', [
|
|
|
2578
2622
|
timeout: z.number().optional().describe('Timeout in ms (default: system DEFAULT_HANDLER_TIMEOUT, usually 30000)'),
|
|
2579
2623
|
globalRulesAckKey: globalRulesAckParam(z),
|
|
2580
2624
|
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
2581
|
-
|
|
2625
|
+
allowCanonicalRoute: z.boolean().optional().default(false).describe('Explicit acknowledgement for adding a new handler to a canonical main-table route. Use only when the new method intentionally belongs to the shared eApp/admin CRUD surface; third-party endpoint-specific behavior must use a separate custom route.'),
|
|
2626
|
+
}, async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout, globalRulesAckKey, knowledgeAckKey, allowCanonicalRoute }) => {
|
|
2582
2627
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
2583
2628
|
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
2629
|
+
const routeQuery = new URLSearchParams({
|
|
2630
|
+
filter: JSON.stringify({ id: { _eq: routeId } }),
|
|
2631
|
+
fields: 'id,path,mainTable.id,mainTable.name',
|
|
2632
|
+
limit: '1',
|
|
2633
|
+
});
|
|
2634
|
+
const routeResult = await fetchAPI(ENFYRA_API_URL, `/enfyra_route?${routeQuery.toString()}`);
|
|
2635
|
+
const targetRoute = unwrapData(routeResult)[0];
|
|
2636
|
+
if (!targetRoute)
|
|
2637
|
+
throw new Error(`Route not found: ${String(routeId)}`);
|
|
2638
|
+
assertCreateHandlerRouteBoundary(targetRoute, sourceCode, allowCanonicalRoute);
|
|
2584
2639
|
const methodNames = methods && methods.length > 0 ? methods : method ? [method] : [];
|
|
2585
2640
|
if (methodNames.length === 0)
|
|
2586
2641
|
throw new Error('Provide method or methods');
|
|
@@ -2614,6 +2669,7 @@ server.tool('create_handler', [
|
|
|
2614
2669
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2615
2670
|
action: 'created',
|
|
2616
2671
|
handlers: results,
|
|
2672
|
+
canonicalRouteAcknowledged: Boolean(targetRoute?.mainTable && allowCanonicalRoute),
|
|
2617
2673
|
scriptValidation,
|
|
2618
2674
|
routeReload,
|
|
2619
2675
|
detailHint: 'Use inspect_route with the same routeId/path to inspect saved handlers.',
|
|
@@ -2880,7 +2936,7 @@ server.tool('ensure_route_access', [
|
|
|
2880
2936
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
2881
2937
|
});
|
|
2882
2938
|
// Register table tools
|
|
2883
|
-
registerTableTools(server, ENFYRA_API_URL);
|
|
2939
|
+
registerTableTools(server, ENFYRA_API_URL, { toolset: MCP_TOOLSET });
|
|
2884
2940
|
registerPlatformOperationTools(server, ENFYRA_API_URL);
|
|
2885
2941
|
registerRuntimeZoneTools(server, ENFYRA_API_URL);
|
|
2886
2942
|
registerDynamicRepositoryBuilder(server);
|