@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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/dist/lib/dynamic-endpoint-contract.js +3 -0
  3. package/dist/lib/dynamic-endpoint-contract.js.map +1 -1
  4. package/dist/lib/extension-search-tools.js +2 -1
  5. package/dist/lib/extension-search-tools.js.map +1 -1
  6. package/dist/lib/extension-sfc-analyzer.d.ts +4 -0
  7. package/dist/lib/extension-sfc-analyzer.js +137 -0
  8. package/dist/lib/extension-sfc-analyzer.js.map +1 -0
  9. package/dist/lib/mcp-examples.js +2 -2
  10. package/dist/lib/mcp-instructions.js +5 -15
  11. package/dist/lib/mcp-instructions.js.map +1 -1
  12. package/dist/lib/mcp-usage-telemetry.js +4 -3
  13. package/dist/lib/mcp-usage-telemetry.js.map +1 -1
  14. package/dist/lib/mutation-guards.d.ts +1 -0
  15. package/dist/lib/mutation-guards.js +28 -0
  16. package/dist/lib/mutation-guards.js.map +1 -1
  17. package/dist/lib/platform-operation-tools.d.ts +159 -3
  18. package/dist/lib/platform-operation-tools.js +361 -78
  19. package/dist/lib/platform-operation-tools.js.map +1 -1
  20. package/dist/lib/required-knowledge.d.ts +9 -2
  21. package/dist/lib/required-knowledge.js +50 -16
  22. package/dist/lib/required-knowledge.js.map +1 -1
  23. package/dist/lib/response-format.js +39 -25
  24. package/dist/lib/response-format.js.map +1 -1
  25. package/dist/lib/runtime-zone-tools.js +4 -3
  26. package/dist/lib/runtime-zone-tools.js.map +1 -1
  27. package/dist/lib/session-safety.d.ts +9 -0
  28. package/dist/lib/session-safety.js +89 -0
  29. package/dist/lib/session-safety.js.map +1 -0
  30. package/dist/lib/source-artifacts.js +3 -1
  31. package/dist/lib/source-artifacts.js.map +1 -1
  32. package/dist/lib/table-tools.d.ts +5 -0
  33. package/dist/lib/table-tools.js +46 -5
  34. package/dist/lib/table-tools.js.map +1 -1
  35. package/dist/lib/tool-input-normalization.d.ts +4 -0
  36. package/dist/lib/tool-input-normalization.js +35 -0
  37. package/dist/lib/tool-input-normalization.js.map +1 -0
  38. package/dist/lib/tool-routing.d.ts +6 -2
  39. package/dist/lib/tool-routing.js +33 -8
  40. package/dist/lib/tool-routing.js.map +1 -1
  41. package/dist/lib/toolset-filter.d.ts +7 -3
  42. package/dist/lib/toolset-filter.js +144 -86
  43. package/dist/lib/toolset-filter.js.map +1 -1
  44. package/dist/lib/types.d.ts +20 -0
  45. package/dist/mcp-server-entry.js +49 -19
  46. package/dist/mcp-server-entry.js.map +1 -1
  47. package/package.json +4 -1
@@ -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. Omit to load all rules.'),
827
- }, async ({ scope }) => jsonContent(buildRequiredKnowledgePayload(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 and enqueues a flow execution.',
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
- mockFlow: z.string().optional().describe('Optional mockFlow JSON object'),
1990
- }, async ({ type, config, timeout, key, mockFlow }) => {
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: JSON.parse(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 a saved flow by id or name. Wraps POST /admin/flow/trigger/:id.', {
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 result = await fetchAPI(ENFYRA_API_URL, `/admin/flow/trigger/${encodeURIComponent(String(flowIdOrName))}`, {
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
  });