@enfyra/mcp-server 0.1.32 → 0.1.34
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 -1
- package/dist/lib/mcp-examples.js +14 -27
- package/dist/lib/mcp-examples.js.map +1 -1
- package/dist/lib/mcp-instructions.js +4 -3
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/mcp-usage-telemetry.d.ts +43 -0
- package/dist/lib/mcp-usage-telemetry.js +458 -0
- package/dist/lib/mcp-usage-telemetry.js.map +1 -0
- package/dist/lib/mutation-guards.js +12 -0
- package/dist/lib/mutation-guards.js.map +1 -1
- package/dist/lib/platform-operation-tools.js +212 -3
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +1 -0
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/response-format.js +12 -2
- package/dist/lib/response-format.js.map +1 -1
- package/dist/lib/table-tools.d.ts +1 -0
- package/dist/lib/table-tools.js +120 -14
- package/dist/lib/table-tools.js.map +1 -1
- package/dist/lib/tool-routing.js +18 -11
- package/dist/lib/tool-routing.js.map +1 -1
- package/dist/lib/toolset-filter.js +2 -13
- package/dist/lib/toolset-filter.js.map +1 -1
- package/dist/mcp-server-entry.js +15 -3
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +1 -1
|
@@ -1037,6 +1037,113 @@ function planFlowSteps(steps) {
|
|
|
1037
1037
|
};
|
|
1038
1038
|
});
|
|
1039
1039
|
}
|
|
1040
|
+
function normalizeFlowWorkflowStep(step, index) {
|
|
1041
|
+
const input = typeof step === 'string' ? { intent: step } : (step || {});
|
|
1042
|
+
const intent = String(input.intent || input.name || input.key || `Step ${index + 1}`);
|
|
1043
|
+
const recommended = chooseFlowStepTool(input.type || intent);
|
|
1044
|
+
const type = String(input.type || recommended.type || 'script');
|
|
1045
|
+
const guidance = FLOW_STEP_TOOL_GUIDANCE.find((item) => item.type === type);
|
|
1046
|
+
if (!guidance) {
|
|
1047
|
+
throw new Error(`steps[${index}].type must be one of ${FLOW_STEP_TOOL_GUIDANCE.map((item) => item.type).join(', ')}.`);
|
|
1048
|
+
}
|
|
1049
|
+
const key = String(input.key || intent)
|
|
1050
|
+
.trim()
|
|
1051
|
+
.toLowerCase()
|
|
1052
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
1053
|
+
.replace(/^_+|_+$/g, '')
|
|
1054
|
+
.slice(0, 64) || `step_${index + 1}`;
|
|
1055
|
+
return {
|
|
1056
|
+
index,
|
|
1057
|
+
key,
|
|
1058
|
+
name: input.name || intent,
|
|
1059
|
+
intent,
|
|
1060
|
+
type,
|
|
1061
|
+
order: input.order ?? index * 10,
|
|
1062
|
+
config: input.config ?? guidance.config ?? {},
|
|
1063
|
+
sourceCode: input.sourceCode ?? guidance.sourceCode,
|
|
1064
|
+
scriptLanguage: input.scriptLanguage || 'javascript',
|
|
1065
|
+
timeout: input.timeout,
|
|
1066
|
+
isEnabled: input.isEnabled ?? true,
|
|
1067
|
+
chosenByIntent: !input.type,
|
|
1068
|
+
recommendedTool: guidance.tool,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
async function runFlowWorkflow(apiUrl, opts) {
|
|
1072
|
+
const steps = parseJsonArrayArg('steps', opts.steps, []);
|
|
1073
|
+
const plan = steps.map(normalizeFlowWorkflowStep);
|
|
1074
|
+
const hasDynamicCode = plan.some((step) => ['script', 'condition'].includes(step.type) && step.sourceCode);
|
|
1075
|
+
const triggerType = opts.triggerType || 'manual';
|
|
1076
|
+
const flowInput = {
|
|
1077
|
+
name: opts.name,
|
|
1078
|
+
triggerType,
|
|
1079
|
+
triggerConfig: triggerType === 'schedule' ? opts.triggerConfig : (opts.triggerConfig ?? {}),
|
|
1080
|
+
timeout: opts.timeout,
|
|
1081
|
+
maxExecutions: opts.maxExecutions,
|
|
1082
|
+
isEnabled: opts.isEnabled,
|
|
1083
|
+
description: opts.description,
|
|
1084
|
+
globalRulesAckKey: opts.globalRulesAckKey,
|
|
1085
|
+
};
|
|
1086
|
+
if (!opts.apply) {
|
|
1087
|
+
return {
|
|
1088
|
+
action: 'flow_workflow_planned',
|
|
1089
|
+
flow: {
|
|
1090
|
+
name: opts.name,
|
|
1091
|
+
triggerType,
|
|
1092
|
+
},
|
|
1093
|
+
stepCount: plan.length,
|
|
1094
|
+
plan,
|
|
1095
|
+
requiredAckParams: ['globalRulesAckKey', ...(hasDynamicCode ? ['knowledgeAckKey'] : [])],
|
|
1096
|
+
nextSteps: [
|
|
1097
|
+
'Review the plan. Prefer fixed step types; script is only for logic not covered by query/create/update/delete/http/sleep/trigger/log/condition.',
|
|
1098
|
+
'Call flow_workflow again with apply=true and the required ack params to create/update the flow and steps sequentially.',
|
|
1099
|
+
'Use test_flow_step for script, condition, or high-risk steps before triggering the flow.',
|
|
1100
|
+
],
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (!opts.name)
|
|
1104
|
+
throw new Error('name is required.');
|
|
1105
|
+
assertGlobalRulesAck(opts.globalRulesAckKey);
|
|
1106
|
+
if (hasDynamicCode)
|
|
1107
|
+
assertDynamicCodeKnowledgeAck(opts.knowledgeAckKey);
|
|
1108
|
+
const flowResult = await ensureFlow(apiUrl, flowInput);
|
|
1109
|
+
const flowId = flowResult.flow.id;
|
|
1110
|
+
const operations = [];
|
|
1111
|
+
for (const step of plan) {
|
|
1112
|
+
const result = await ensureFlowStep(apiUrl, {
|
|
1113
|
+
flowName: undefined,
|
|
1114
|
+
flowId,
|
|
1115
|
+
key: step.key,
|
|
1116
|
+
type: step.type,
|
|
1117
|
+
order: step.order,
|
|
1118
|
+
config: step.config,
|
|
1119
|
+
sourceCode: step.sourceCode,
|
|
1120
|
+
scriptLanguage: step.scriptLanguage,
|
|
1121
|
+
timeout: step.timeout,
|
|
1122
|
+
isEnabled: step.isEnabled,
|
|
1123
|
+
globalRulesAckKey: opts.globalRulesAckKey,
|
|
1124
|
+
knowledgeAckKey: opts.knowledgeAckKey,
|
|
1125
|
+
});
|
|
1126
|
+
operations.push({
|
|
1127
|
+
index: step.index,
|
|
1128
|
+
key: step.key,
|
|
1129
|
+
type: step.type,
|
|
1130
|
+
result,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
action: 'flow_workflow_applied',
|
|
1135
|
+
flow: flowResult.flow,
|
|
1136
|
+
flowResult,
|
|
1137
|
+
stepCount: plan.length,
|
|
1138
|
+
plan,
|
|
1139
|
+
operations,
|
|
1140
|
+
sequential: true,
|
|
1141
|
+
nextSteps: [
|
|
1142
|
+
'Use test_flow_step for script, condition, or high-risk steps before triggering the flow.',
|
|
1143
|
+
'Use trigger_flow only after saved behavior is verified.',
|
|
1144
|
+
],
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1040
1147
|
function normalizeEndpointAccess(anonymousAccess, makePublic) {
|
|
1041
1148
|
if (makePublic !== undefined)
|
|
1042
1149
|
return makePublic ? 'public' : 'private';
|
|
@@ -1772,7 +1879,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1772
1879
|
].join(' '), {
|
|
1773
1880
|
path: z.string().describe('Custom route path, e.g. /sum. Must not be a full URL.'),
|
|
1774
1881
|
method: z.string().describe('HTTP method for the handler, e.g. GET or POST.'),
|
|
1775
|
-
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER and #table_name. Do not send compiledCode. Do not use @REPOS.secure.<table>; use @REPOS.main for route main table or #table_name/@REPOS.table_name with explicit fields/auth checks.'),
|
|
1882
|
+
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER and #table_name. Repository calls are async: use `const result = await #table.find(...)` and read rows from `result.data || []`. Do not send compiledCode. Do not use @REPOS.secure.<table>; use @REPOS.main for route main table or #table_name/@REPOS.table_name with explicit fields/auth checks.'),
|
|
1776
1883
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
1777
1884
|
anonymousAccess: z.enum(['public', 'private']).optional().default('private').describe('public adds the method to publicMethods; private removes this method from publicMethods.'),
|
|
1778
1885
|
public: z.boolean().optional().describe('Compatibility alias for anonymousAccess. true means public, false means private.'),
|
|
@@ -1801,7 +1908,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1801
1908
|
].join(' '), {
|
|
1802
1909
|
path: z.string().describe('Custom route path, e.g. /sum. Must not be a full URL.'),
|
|
1803
1910
|
method: z.string().describe('HTTP method for the handler, e.g. GET or POST.'),
|
|
1804
|
-
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER and #table_name. Do not send compiledCode. Do not use @REPOS.secure.<table>; use @REPOS.main for route main table or #table_name/@REPOS.table_name with explicit fields/auth checks.'),
|
|
1911
|
+
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER and #table_name. Repository calls are async: use `const result = await #table.find(...)` and read rows from `result.data || []`. Do not send compiledCode. Do not use @REPOS.secure.<table>; use @REPOS.main for route main table or #table_name/@REPOS.table_name with explicit fields/auth checks.'),
|
|
1805
1912
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
1806
1913
|
public: z.boolean().optional().default(false).describe('When true, the method is added to publicMethods for anonymous access.'),
|
|
1807
1914
|
description: z.string().optional().describe('Route description.'),
|
|
@@ -2020,7 +2127,78 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2020
2127
|
reload,
|
|
2021
2128
|
});
|
|
2022
2129
|
});
|
|
2023
|
-
server.tool('
|
|
2130
|
+
server.tool('ensure_route_rate_limit', 'Business operation: create or update a route rate-limit guard through the Enfyra guard engine. Prefer this over pre-hooks or raw guard JSON for request throttling.', {
|
|
2131
|
+
name: z.string().optional().describe('Optional guard name. Defaults to a stable name based on path, methods, and scope.'),
|
|
2132
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Optional route id.'),
|
|
2133
|
+
path: z.string().optional().describe('Route path to protect, e.g. /newsletter_signup.'),
|
|
2134
|
+
methods: z.array(z.string()).default(['POST']).describe('HTTP method names to protect.'),
|
|
2135
|
+
scope: z.enum(['ip', 'user', 'route']).default('ip').describe('Rate-limit key scope. Use ip for public/pre-auth routes, user for authenticated users, route for a shared route-wide limit.'),
|
|
2136
|
+
maxRequests: z.number().int().positive().describe('Allowed request count per window.'),
|
|
2137
|
+
perSeconds: z.number().int().positive().describe('Window length in seconds.'),
|
|
2138
|
+
position: z.enum(['pre_auth', 'post_auth']).optional().describe('Optional override. Defaults to pre_auth for ip/route and post_auth for user.'),
|
|
2139
|
+
priority: z.number().optional().default(0).describe('Lower runs earlier.'),
|
|
2140
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable the guard. Defaults true.'),
|
|
2141
|
+
description: z.string().optional().describe('Admin note.'),
|
|
2142
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
2143
|
+
}, async ({ name, routeId, path, methods, scope, maxRequests, perSeconds, position, priority, isEnabled, description, globalRulesAckKey }) => {
|
|
2144
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
2145
|
+
if (path && routeId)
|
|
2146
|
+
throw new Error('Provide path or routeId, not both.');
|
|
2147
|
+
const resolvedPosition = position || (scope === 'user' ? 'post_auth' : 'pre_auth');
|
|
2148
|
+
if (scope === 'user' && resolvedPosition === 'pre_auth') {
|
|
2149
|
+
throw new Error('User-scoped rate limits require post_auth because user identity is unavailable before auth.');
|
|
2150
|
+
}
|
|
2151
|
+
const { route } = await resolveRoute(ENFYRA_API_URL, { path, routeId });
|
|
2152
|
+
const { methodMap } = await getMethodContext(ENFYRA_API_URL);
|
|
2153
|
+
const methodNames = uniqueMethodNames(methods?.length ? methods : ['POST']);
|
|
2154
|
+
const ruleType = scope === 'user' ? 'rate_limit_by_user' : scope === 'route' ? 'rate_limit_by_route' : 'rate_limit_by_ip';
|
|
2155
|
+
const guardName = name || `Rate limit ${scope} ${route.path} ${methodNames.join('_')}`;
|
|
2156
|
+
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_guard', { name: { _eq: guardName } }, 'id,_id,name');
|
|
2157
|
+
const guardBody = {
|
|
2158
|
+
name: guardName,
|
|
2159
|
+
position: resolvedPosition,
|
|
2160
|
+
combinator: 'and',
|
|
2161
|
+
priority,
|
|
2162
|
+
isGlobal: false,
|
|
2163
|
+
isEnabled,
|
|
2164
|
+
description: description || `Rate-limit ${methodNames.join(', ')} ${route.path} by ${scope}.`,
|
|
2165
|
+
route: { id: getId(route) },
|
|
2166
|
+
methods: resolveMethodRefs(methodMap, methodNames),
|
|
2167
|
+
};
|
|
2168
|
+
const guardOperation = await createOrPatch(ENFYRA_API_URL, 'enfyra_guard', existing, guardBody);
|
|
2169
|
+
const guardId = guardOperation.id || getId(existing);
|
|
2170
|
+
const existingRules = await fetchRecords(ENFYRA_API_URL, 'enfyra_guard_rule', { guard: { id: { _eq: guardId } } }, 'id,_id,isEnabled');
|
|
2171
|
+
const disabledRules = [];
|
|
2172
|
+
for (const rule of existingRules) {
|
|
2173
|
+
disabledRules.push(await fetchAPI(ENFYRA_API_URL, `/enfyra_guard_rule/${encodeURIComponent(String(getId(rule)))}`, {
|
|
2174
|
+
method: 'PATCH',
|
|
2175
|
+
body: JSON.stringify({ isEnabled: false }),
|
|
2176
|
+
}));
|
|
2177
|
+
}
|
|
2178
|
+
const rule = await fetchAPI(ENFYRA_API_URL, '/enfyra_guard_rule', {
|
|
2179
|
+
method: 'POST',
|
|
2180
|
+
body: JSON.stringify({
|
|
2181
|
+
type: ruleType,
|
|
2182
|
+
config: { maxRequests, perSeconds },
|
|
2183
|
+
priority: 0,
|
|
2184
|
+
isEnabled: true,
|
|
2185
|
+
description: `${maxRequests} request${maxRequests === 1 ? '' : 's'} per ${perSeconds} seconds by ${scope}.`,
|
|
2186
|
+
guard: { id: guardId },
|
|
2187
|
+
}),
|
|
2188
|
+
});
|
|
2189
|
+
const reload = await reloadBestEffort(ENFYRA_API_URL, '/admin/reload/guards');
|
|
2190
|
+
return jsonText({
|
|
2191
|
+
action: 'route_rate_limit_ensured',
|
|
2192
|
+
route: { id: getId(route), path: route.path },
|
|
2193
|
+
methods: methodNames,
|
|
2194
|
+
guard: { id: guardId, name: guardName, position: resolvedPosition, isEnabled },
|
|
2195
|
+
rule: { type: ruleType, config: { maxRequests, perSeconds }, result: rule },
|
|
2196
|
+
disabledRuleCount: disabledRules.length,
|
|
2197
|
+
reload,
|
|
2198
|
+
next: 'Call inspect_route({ path }) to confirm the guard is attached, then test behavior through the actual REST route if doing so will not consume a production rate-limit bucket.',
|
|
2199
|
+
});
|
|
2200
|
+
});
|
|
2201
|
+
server.tool('ensure_guard', 'Advanced business operation: create or update a custom request guard tree and optional guard rules. For simple request throttling use ensure_route_rate_limit instead.', {
|
|
2024
2202
|
name: z.string().describe('Guard name. Existing guard with this name is updated unless guardId is provided.'),
|
|
2025
2203
|
guardId: z.union([z.string(), z.number()]).optional().describe('Optional existing guard id.'),
|
|
2026
2204
|
position: z.enum(['pre_auth', 'post_auth']).optional().default('pre_auth').describe('Guard position.'),
|
|
@@ -2167,6 +2345,37 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2167
2345
|
const reload = naturalPartialReload('Websocket event writes trigger the server partial reload contract; there is no dedicated websocket reload endpoint.');
|
|
2168
2346
|
return jsonText({ action: 'websocket_event_ensured', gateway: { id: getId(gateway), path: gateway.path }, eventName, validation, operation, reload });
|
|
2169
2347
|
});
|
|
2348
|
+
server.tool('flow_workflow', [
|
|
2349
|
+
'Workflow front door for creating or updating an Enfyra flow and its steps in one guided path.',
|
|
2350
|
+
'Use apply=false first to plan step types from plain-language intents. Use apply=true only after reviewing the plan; the tool creates/updates the flow first, then steps sequentially.',
|
|
2351
|
+
'Prefer this over choosing individual ensure_*_flow_step tools in guided mode.',
|
|
2352
|
+
].join(' '), {
|
|
2353
|
+
name: z.string().describe('Flow name. Existing flow with this name is updated.'),
|
|
2354
|
+
triggerType: z.enum(['manual', 'schedule']).optional().default('manual').describe('manual for API/admin/hook/child flow usage, schedule for cron/time-based flows.'),
|
|
2355
|
+
triggerConfig: z.union([z.record(z.any()), z.string()]).optional().describe('Trigger config object or JSON string. Required for scheduled flows.'),
|
|
2356
|
+
steps: z.array(z.union([
|
|
2357
|
+
z.string(),
|
|
2358
|
+
z.object({
|
|
2359
|
+
key: z.string().optional().describe('Stable step key. Generated from intent when omitted.'),
|
|
2360
|
+
name: z.string().optional().describe('Human label. Defaults from intent.'),
|
|
2361
|
+
intent: z.string().optional().describe('Plain-language step intent. Used to choose a fixed step type when type is omitted.'),
|
|
2362
|
+
type: z.enum(['query', 'create', 'update', 'delete', 'http', 'condition', 'sleep', 'trigger_flow', 'log', 'script']).optional().describe('Explicit step type. Omit to let the workflow choose from intent.'),
|
|
2363
|
+
config: z.union([z.record(z.any()), z.string()]).optional().describe('Step config object or JSON string. For query/create/update/delete/http/sleep/trigger/log steps, prefer config over sourceCode.'),
|
|
2364
|
+
sourceCode: z.string().optional().describe('Only for script or condition steps. Use fixed step types when possible.'),
|
|
2365
|
+
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript'),
|
|
2366
|
+
order: z.number().optional().describe('Step order. Defaults to index * 10.'),
|
|
2367
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
2368
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
2369
|
+
}),
|
|
2370
|
+
])).min(1).max(30).describe('Ordered step intents/definitions. Keep one business operation per step.'),
|
|
2371
|
+
timeout: z.number().int().positive().optional().describe('Flow timeout in ms.'),
|
|
2372
|
+
maxExecutions: z.number().int().positive().optional().default(100).describe('Execution history cap.'),
|
|
2373
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable flow.'),
|
|
2374
|
+
description: z.string().optional().describe('Admin note.'),
|
|
2375
|
+
apply: z.boolean().optional().default(false).describe('false returns plan only; true applies flow and steps sequentially.'),
|
|
2376
|
+
globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when apply=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
|
|
2377
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when apply=true and any script/condition step has sourceCode.'),
|
|
2378
|
+
}, async (input) => jsonText(await runFlowWorkflow(ENFYRA_API_URL, input)));
|
|
2170
2379
|
server.tool('ensure_manual_flow', 'Business operation: create or update a manually triggered Enfyra flow. Use this when the flow is run by API, admin action, another flow, or hook.', {
|
|
2171
2380
|
name: z.string().describe('Flow name. Existing flow with this name is updated.'),
|
|
2172
2381
|
timeout: z.number().int().positive().optional().describe('Flow timeout in ms.'),
|