@elastic.io/mcp 1.0.0 → 1.0.1-beta
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 +128 -126
- package/README.npm.md +173 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +8 -3
- package/dist/api/client.js.map +1 -1
- package/dist/api/jsonapi.d.ts +2 -9
- package/dist/api/jsonapi.d.ts.map +1 -1
- package/dist/api/jsonapi.js +1 -14
- package/dist/api/jsonapi.js.map +1 -1
- package/dist/api/logger.d.ts +0 -1
- package/dist/api/logger.d.ts.map +1 -1
- package/dist/api/logger.js +2 -5
- package/dist/api/logger.js.map +1 -1
- package/dist/api/samples.d.ts +0 -6
- package/dist/api/samples.d.ts.map +1 -1
- package/dist/api/samples.js +8 -8
- package/dist/api/samples.js.map +1 -1
- package/dist/api/verification.d.ts +6 -0
- package/dist/api/verification.d.ts.map +1 -0
- package/dist/api/verification.js +71 -0
- package/dist/api/verification.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +0 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -0
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +3 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +338 -0
- package/dist/setup.js.map +1 -0
- package/dist/tools/confirmation.d.ts +0 -4
- package/dist/tools/confirmation.d.ts.map +1 -1
- package/dist/tools/confirmation.js +1 -1
- package/dist/tools/confirmation.js.map +1 -1
- package/dist/tools/constants.d.ts +6 -9
- package/dist/tools/constants.d.ts.map +1 -1
- package/dist/tools/constants.js +48 -41
- package/dist/tools/constants.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +37 -5
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/flow-build.d.ts +18 -0
- package/dist/tools/flow-build.d.ts.map +1 -0
- package/dist/tools/flow-build.js +295 -0
- package/dist/tools/flow-build.js.map +1 -0
- package/dist/tools/flow-evidence.d.ts +62 -0
- package/dist/tools/flow-evidence.d.ts.map +1 -0
- package/dist/tools/flow-evidence.js +128 -0
- package/dist/tools/flow-evidence.js.map +1 -0
- package/dist/tools/flow-state-machine.d.ts +69 -0
- package/dist/tools/flow-state-machine.d.ts.map +1 -0
- package/dist/tools/flow-state-machine.js +292 -0
- package/dist/tools/flow-state-machine.js.map +1 -0
- package/dist/tools/handlers/auth.d.ts.map +1 -1
- package/dist/tools/handlers/auth.js +18 -148
- package/dist/tools/handlers/auth.js.map +1 -1
- package/dist/tools/handlers/component.d.ts.map +1 -1
- package/dist/tools/handlers/component.js +28 -8
- package/dist/tools/handlers/component.js.map +1 -1
- package/dist/tools/handlers/data-sample.d.ts.map +1 -1
- package/dist/tools/handlers/data-sample.js +37 -5
- package/dist/tools/handlers/data-sample.js.map +1 -1
- package/dist/tools/handlers/dynamic.d.ts.map +1 -1
- package/dist/tools/handlers/dynamic.js +43 -14
- package/dist/tools/handlers/dynamic.js.map +1 -1
- package/dist/tools/handlers/flow.d.ts.map +1 -1
- package/dist/tools/handlers/flow.js +147 -619
- package/dist/tools/handlers/flow.js.map +1 -1
- package/dist/tools/handlers/log.d.ts.map +1 -1
- package/dist/tools/handlers/log.js +31 -0
- package/dist/tools/handlers/log.js.map +1 -1
- package/dist/tools/handlers/raw.d.ts.map +1 -1
- package/dist/tools/handlers/raw.js +5 -2
- package/dist/tools/handlers/raw.js.map +1 -1
- package/dist/tools/handlers/search.d.ts.map +1 -1
- package/dist/tools/handlers/search.js +67 -44
- package/dist/tools/handlers/search.js.map +1 -1
- package/dist/tools/handlers/session.d.ts.map +1 -1
- package/dist/tools/handlers/session.js +8 -1
- package/dist/tools/handlers/session.js.map +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +7 -2
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/helpers.d.ts +0 -1
- package/dist/tools/helpers.d.ts.map +1 -1
- package/dist/tools/helpers.js +2 -2
- package/dist/tools/helpers.js.map +1 -1
- package/dist/tools/types.d.ts +8 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/validation.d.ts +16 -5
- package/dist/tools/validation.d.ts.map +1 -1
- package/dist/tools/validation.js +220 -168
- package/dist/tools/validation.js.map +1 -1
- package/dist/vectorstore/qdrant.js +1 -1
- package/dist/vectorstore/qdrant.js.map +1 -1
- package/package.json +22 -7
- package/resources/docs/flow-building-guide.md +18 -2
- package/resources/docs/rest-api-pagination.md +58 -0
- package/resources/docs/system-instructions.md +35 -5
- package/resources/docs/component-test-scenarios.md +0 -2468
- package/resources/docs/credential-validation-gaps.md +0 -35
- package/resources/docs/debug-issues.md +0 -71
- package/resources/docs/deployment-and-scaling.md +0 -136
- package/resources/docs/packaging-plan.md +0 -84
- package/resources/docs/unique-components.md +0 -60
- package/resources/docs/verified-creds-flow-ids.md +0 -193
package/dist/tools/validation.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { verifyCredentials } from '../api/verification.js';
|
|
2
|
+
import { credentialKey, descriptorFor, evidence, nodeCredentialArgs, nodeMetadataKey, operation } from './flow-evidence.js';
|
|
1
3
|
import { logger } from '../api/logger.js';
|
|
4
|
+
import { deserializeJsonApi } from '../api/jsonapi.js';
|
|
2
5
|
import { resolveCredentialId } from './helpers.js';
|
|
3
|
-
import { DEPRECATED_COMPONENTS,
|
|
4
|
-
export { DEPRECATED_COMPONENTS, DEPRECATED_ACTIONS } from './constants.js';
|
|
5
|
-
export function checkFlowLifecycle(flowId, discoveryState, existingFlow,
|
|
6
|
+
import { DEPRECATED_COMPONENTS, getDeprecatedActionReplacement, BARE_FIELD_NAMES, UNSUPPORTED_ROUTER_JSONATA_FUNCTIONS, REST_API_READER_FIELDS, PLACEHOLDER_FIELD_VALUES, } from './constants.js';
|
|
7
|
+
export { DEPRECATED_COMPONENTS, DEPRECATED_ACTIONS, DEPRECATED_COMPONENT_ACTIONS, getDeprecatedActionReplacement } from './constants.js';
|
|
8
|
+
export function checkFlowLifecycle(flowId, discoveryState, existingFlow, _operation) {
|
|
6
9
|
const errors = [];
|
|
7
10
|
if (!discoveryState.inspectedFlows.has(flowId)) {
|
|
8
11
|
errors.push(`You have not inspected flow '${flowId}' using 'get_flow' in this session. ` +
|
|
@@ -29,7 +32,7 @@ export function checkFlowBuildingGuideRead(discoveryState) {
|
|
|
29
32
|
}
|
|
30
33
|
return [];
|
|
31
34
|
}
|
|
32
|
-
|
|
35
|
+
function stripStringLiterals(str) {
|
|
33
36
|
let result = '';
|
|
34
37
|
let inDoubleQuote = false;
|
|
35
38
|
let inSingleQuote = false;
|
|
@@ -123,6 +126,15 @@ function validateNodes(nodes, nodeMap, errors, warnings) {
|
|
|
123
126
|
errors.push("Node definition is missing the 'id' field.");
|
|
124
127
|
continue;
|
|
125
128
|
}
|
|
129
|
+
const stepIdRegex = /^step_[1-9]\d*$/;
|
|
130
|
+
if (!stepIdRegex.test(node.id)) {
|
|
131
|
+
errors.push(`Node ID '${node.id}' has an invalid format. ` +
|
|
132
|
+
'Every step_id must follow the format "step_N" where N is a positive integer starting from 1 ' +
|
|
133
|
+
'(e.g., "step_1", "step_2", "step_3"). ' +
|
|
134
|
+
'Do not use formats like "step1", "Step_1", "step_0", "step_01", or custom names like "step_lk_from".');
|
|
135
|
+
}
|
|
136
|
+
if (nodeMap.has(node.id))
|
|
137
|
+
errors.push(`Duplicate node ID '${node.id}'.`);
|
|
126
138
|
nodeMap.set(node.id, node);
|
|
127
139
|
const command = node.command;
|
|
128
140
|
if (!command) {
|
|
@@ -132,14 +144,14 @@ function validateNodes(nodes, nodeMap, errors, warnings) {
|
|
|
132
144
|
const formatRegex = /^[^/]+\/[^:]+:[^@]+@[^@]+$/;
|
|
133
145
|
if (!formatRegex.test(command)) {
|
|
134
146
|
errors.push(`Node '${node.id}' has an invalid command format: "${command}". ` +
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
'It must be formatted as "{team_name}/{repo_name}:{action_or_trigger_id}@{latestVersionId}" ' +
|
|
148
|
+
'(e.g., "elasticio/salesforce-v2:getUpdatedObjectsPolling@latestVersionId"). ' +
|
|
149
|
+
'Retrieve this from the \'get_component\' tool response.');
|
|
138
150
|
}
|
|
139
151
|
// Check for deprecated components by command prefix
|
|
140
152
|
const prefixMatch = command.match(/^([^:]+):/);
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
const commandPrefix = prefixMatch ? prefixMatch[1] : '';
|
|
154
|
+
if (commandPrefix) {
|
|
143
155
|
const successor = DEPRECATED_COMPONENTS[commandPrefix];
|
|
144
156
|
if (successor) {
|
|
145
157
|
errors.push(`Node '${node.id}' uses deprecated component '${commandPrefix}'. ` +
|
|
@@ -147,11 +159,11 @@ function validateNodes(nodes, nodeMap, errors, warnings) {
|
|
|
147
159
|
`Use '${successor}' instead. Call 'list_components' to find the replacement.`);
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
|
-
// Check for deprecated actions/triggers by action ID
|
|
162
|
+
// Check for deprecated actions/triggers by action ID scoped to component
|
|
151
163
|
const actionMatch = command.match(/^[^:]+:([^@]+)@/);
|
|
152
|
-
if (actionMatch) {
|
|
164
|
+
if (actionMatch && commandPrefix) {
|
|
153
165
|
const actionId = actionMatch[1];
|
|
154
|
-
const replacement =
|
|
166
|
+
const replacement = getDeprecatedActionReplacement(commandPrefix, actionId);
|
|
155
167
|
if (replacement) {
|
|
156
168
|
errors.push(`Node '${node.id}' uses deprecated action '${actionId}'. ` +
|
|
157
169
|
`This action is marked as deprecated in the component descriptor and shows a "Deprecated" badge in the UI. ` +
|
|
@@ -173,7 +185,7 @@ function validateNodes(nodes, nodeMap, errors, warnings) {
|
|
|
173
185
|
`(e.g., "$getPassthrough().step_1.${trimmedField}").`);
|
|
174
186
|
}
|
|
175
187
|
// Warn about deep $getPassthrough() paths in JSONata fields (e.g. Splitter expression).
|
|
176
|
-
const deepPathMatch =
|
|
188
|
+
const deepPathMatch = /^\$getPassthrough\(\)\.\w+\.body\.[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)+$/.exec(trimmedField);
|
|
177
189
|
if (deepPathMatch) {
|
|
178
190
|
warnings.push(`Node '${node.id}' field '${fieldKey}' uses a deep JSONata path "${trimmedField}". ` +
|
|
179
191
|
`The UI form validator only accepts paths up to "$getPassthrough().step_X.body" — deeper paths ` +
|
|
@@ -263,25 +275,50 @@ function validateEdges(edges, nodeMap, errors, warnings) {
|
|
|
263
275
|
function validateMapper(mapper, edgeId, errors) {
|
|
264
276
|
const mapperValues = [];
|
|
265
277
|
if (typeof mapper === 'string') {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
278
|
+
const trimmed = mapper.trim();
|
|
279
|
+
// Allow root JSONata expressions: object constructors ({...}), blocks/parentheses ((...)), or array constructors ([...])
|
|
280
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('(') || trimmed.startsWith('[')) {
|
|
281
|
+
mapperValues.push({ key: '_root', value: mapper });
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const hasOuterQuotes = (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
285
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"));
|
|
286
|
+
if (!hasOuterQuotes) {
|
|
287
|
+
errors.push(`Edge '${edgeId}' mapper is a string but does not start with '{', '(', or '['. ` +
|
|
288
|
+
`A root JSONata mapper expression must evaluate to an object or array ` +
|
|
289
|
+
`(e.g., "{\\n \\"fieldName\\": $getPassthrough().step_1.body.value\\n}").`);
|
|
290
|
+
}
|
|
291
|
+
mapperValues.push({ key: '_root', value: mapper });
|
|
292
|
+
}
|
|
270
293
|
}
|
|
271
294
|
else if (typeof mapper === 'object' && mapper !== null) {
|
|
272
295
|
for (const [key, val] of Object.entries(mapper)) {
|
|
273
296
|
if (Array.isArray(val)) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
297
|
+
for (let i = 0; i < val.length; i++) {
|
|
298
|
+
const item = val[i];
|
|
299
|
+
if (typeof item === 'string') {
|
|
300
|
+
mapperValues.push({ key: `${key}[${i}]`, value: item });
|
|
301
|
+
}
|
|
302
|
+
else if (item && typeof item === 'object') {
|
|
303
|
+
validateMapper(item, `${edgeId}.${key}[${i}]`, errors);
|
|
304
|
+
}
|
|
305
|
+
else if (typeof item === 'number' || typeof item === 'boolean') {
|
|
306
|
+
errors.push(`Edge '${edgeId}' mapper field '${key}[${i}]' must be a JSONata expression string.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
277
309
|
}
|
|
278
310
|
else if (typeof val === 'number' || typeof val === 'boolean') {
|
|
279
|
-
mapper
|
|
280
|
-
mapperValues.push({ key, value: String(val) });
|
|
311
|
+
errors.push(`Edge '${edgeId}' mapper field '${key}' must be a JSONata expression string.`);
|
|
281
312
|
}
|
|
282
313
|
else if (typeof val === 'string') {
|
|
283
314
|
mapperValues.push({ key, value: val });
|
|
284
315
|
}
|
|
316
|
+
else if (val && typeof val === 'object') {
|
|
317
|
+
validateMapper(val, `${edgeId}.${key}`, errors);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
errors.push(`Edge '${edgeId}' mapper field '${key}' must contain a JSONata expression.`);
|
|
321
|
+
}
|
|
285
322
|
}
|
|
286
323
|
}
|
|
287
324
|
for (const { key, value } of mapperValues) {
|
|
@@ -325,6 +362,66 @@ function validateRouterCondition(condition, edgeId, errors) {
|
|
|
325
362
|
}
|
|
326
363
|
}
|
|
327
364
|
}
|
|
365
|
+
/** Validate graph topology: exactly one root, DAG (no cycles), all nodes reachable. */
|
|
366
|
+
function validateTopology(nodes, edges, nodeMap, errors) {
|
|
367
|
+
if (nodes.length === 0)
|
|
368
|
+
return;
|
|
369
|
+
// Find root nodes (not a target of any edge)
|
|
370
|
+
const targets = new Set(edges.map(e => e.target));
|
|
371
|
+
const roots = nodes.filter(n => !targets.has(n.id));
|
|
372
|
+
if (roots.length === 0) {
|
|
373
|
+
errors.push('Flow graph has no root node (every node is a target of some edge — possible cycle).');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// Exactly one node should have first:true
|
|
377
|
+
const firstNodes = nodes.filter(n => n.first);
|
|
378
|
+
if (firstNodes.length > 1) {
|
|
379
|
+
errors.push(`Multiple nodes are marked as 'first': ${firstNodes.map(n => n.id).join(', ')}. Only one trigger node should be first.`);
|
|
380
|
+
}
|
|
381
|
+
// Check for cycles using DFS
|
|
382
|
+
const adjacency = new Map();
|
|
383
|
+
for (const node of nodes)
|
|
384
|
+
adjacency.set(node.id, []);
|
|
385
|
+
for (const edge of edges)
|
|
386
|
+
adjacency.get(edge.source)?.push(edge.target);
|
|
387
|
+
const visited = new Set();
|
|
388
|
+
const inStack = new Set();
|
|
389
|
+
const hasCycle = (id) => {
|
|
390
|
+
if (inStack.has(id))
|
|
391
|
+
return true;
|
|
392
|
+
if (visited.has(id))
|
|
393
|
+
return false;
|
|
394
|
+
visited.add(id);
|
|
395
|
+
inStack.add(id);
|
|
396
|
+
for (const next of adjacency.get(id) || []) {
|
|
397
|
+
if (hasCycle(next))
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
inStack.delete(id);
|
|
401
|
+
return false;
|
|
402
|
+
};
|
|
403
|
+
for (const node of nodes) {
|
|
404
|
+
if (hasCycle(node.id)) {
|
|
405
|
+
errors.push('Flow graph contains a cycle, which is not allowed.');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Check all nodes are reachable from roots
|
|
410
|
+
const reachable = new Set();
|
|
411
|
+
const queue = roots.map(r => r.id);
|
|
412
|
+
while (queue.length > 0) {
|
|
413
|
+
const id = queue.shift();
|
|
414
|
+
if (reachable.has(id))
|
|
415
|
+
continue;
|
|
416
|
+
reachable.add(id);
|
|
417
|
+
for (const next of adjacency.get(id) || [])
|
|
418
|
+
queue.push(next);
|
|
419
|
+
}
|
|
420
|
+
const unreachable = nodes.filter(n => !reachable.has(n.id));
|
|
421
|
+
if (unreachable.length > 0) {
|
|
422
|
+
errors.push(`Nodes not reachable from any root: ${unreachable.map(n => n.id).join(', ')}.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
328
425
|
export function validateFlowGraph(body) {
|
|
329
426
|
const errors = [];
|
|
330
427
|
const warnings = [];
|
|
@@ -334,24 +431,26 @@ export function validateFlowGraph(body) {
|
|
|
334
431
|
}
|
|
335
432
|
const nodes = graph.nodes || [];
|
|
336
433
|
const edges = graph.edges || [];
|
|
434
|
+
if (!Array.isArray(nodes) || !Array.isArray(edges))
|
|
435
|
+
return { errors: ['graph.nodes and graph.edges must be arrays.'], warnings };
|
|
337
436
|
const nodeMap = new Map();
|
|
338
437
|
validateNodes(nodes, nodeMap, errors, warnings);
|
|
339
438
|
validateEdges(edges, nodeMap, errors, warnings);
|
|
439
|
+
validateTopology(nodes, edges, nodeMap, errors);
|
|
340
440
|
return { errors, warnings };
|
|
341
441
|
}
|
|
342
442
|
export function checkDiscoveryState(body, discoveryState, existingFlow) {
|
|
343
443
|
const errors = [];
|
|
444
|
+
const warnings = [];
|
|
344
445
|
const graph = body?.data?.attributes?.graph || body?.attributes?.graph || body?.graph;
|
|
345
446
|
if (!graph)
|
|
346
|
-
return errors;
|
|
447
|
+
return { errors, warnings };
|
|
347
448
|
const nodes = graph.nodes || [];
|
|
348
|
-
const existingNodeIds = new Set();
|
|
349
449
|
const existingComponentIds = new Set();
|
|
350
450
|
const existingCredSecretIds = new Set();
|
|
351
451
|
if (existingFlow) {
|
|
352
452
|
const existingNodes = existingFlow?.graph?.nodes || [];
|
|
353
453
|
existingNodes.forEach((n) => {
|
|
354
|
-
existingNodeIds.add(n.id);
|
|
355
454
|
if (n.component_id)
|
|
356
455
|
existingComponentIds.add(n.component_id);
|
|
357
456
|
const credId = resolveCredentialId(n.credentials_id);
|
|
@@ -379,7 +478,9 @@ export function checkDiscoveryState(body, discoveryState, existingFlow) {
|
|
|
379
478
|
errors.push(`Node '${node.id}' has invalid secret_id '${rawSecId}' — must be a 24-character hex string. ` +
|
|
380
479
|
`Call 'list_secrets' to discover real secret IDs.`);
|
|
381
480
|
}
|
|
382
|
-
const
|
|
481
|
+
const oldNode = existingFlow?.graph?.nodes?.find((n) => n.id === node.id);
|
|
482
|
+
const isExistingNode = oldNode && oldNode.component_id === node.component_id && oldNode.command === node.command
|
|
483
|
+
&& resolveCredentialId(oldNode.credentials_id) === rawCredId && resolveCredentialId(oldNode.secret_id) === rawSecId;
|
|
383
484
|
// Discovery checks only apply to new nodes
|
|
384
485
|
if (!isExistingNode) {
|
|
385
486
|
if (node.component_id && !existingComponentIds.has(node.component_id) && !discoveryState.components.has(node.component_id)) {
|
|
@@ -422,92 +523,59 @@ export function checkDiscoveryState(body, discoveryState, existingFlow) {
|
|
|
422
523
|
`You MUST first find or create credentials/secrets using 'list_credentials'/'list_secrets' (or 'create_credentials'/'create_secret'), ` +
|
|
423
524
|
`then assign them to the node before creating or updating the flow.`);
|
|
424
525
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
async function verifyCredentialForHealthCheck(client, componentId, credOrSecretId, isSecret, workspaceId) {
|
|
433
|
-
let verifyFields = {};
|
|
434
|
-
if (isSecret && workspaceId) {
|
|
435
|
-
const secretRes = await client.get(`/workspaces/${workspaceId}/secrets/${credOrSecretId}`);
|
|
436
|
-
const rawSecretSubtype = secretRes.data?.data?.attributes?.type;
|
|
437
|
-
const secretData = secretRes.data?.data?.attributes || {};
|
|
438
|
-
const existingFields = secretData.credentials || {};
|
|
439
|
-
const secretSubtype = rawSecretSubtype || secretData.type || 'noauth';
|
|
440
|
-
if (secretSubtype === 'oauth2' && existingFields.undefined_params !== undefined) {
|
|
441
|
-
const { undefined_params: existingParams = {}, ...existingOAuth } = existingFields;
|
|
442
|
-
verifyFields = { oauth: { ...existingOAuth, undefined_params: existingParams } };
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
verifyFields = { auth: { type: secretSubtype, ...existingFields } };
|
|
526
|
+
// Warn when a Webhook step has no credentials — auth is optional but should always be proposed
|
|
527
|
+
if (node.command && typeof node.command === 'string' && /webhook.*:receive/.test(node.command)
|
|
528
|
+
&& !resolveCredentialId(node.credentials_id) && !resolveCredentialId(node.secret_id)) {
|
|
529
|
+
warnings.push(`Node '${node.id}' is a Webhook trigger with no credentials attached. ` +
|
|
530
|
+
`Webhook authentication is optional but strongly recommended. Consider attaching credentials ` +
|
|
531
|
+
`to enable HMAC signature verification, API key validation, or basic auth. ` +
|
|
532
|
+
`Call 'list_credentials' for the Webhook component and ask the user which credential to attach.`);
|
|
446
533
|
}
|
|
447
534
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
};
|
|
462
|
-
const verifyRes = await client.post(`/components/${componentId}/versions/latest/verify-credential`, requestBody);
|
|
463
|
-
const execId = verifyRes.data?.data?.id;
|
|
464
|
-
if (!execId)
|
|
465
|
-
return { ok: true }; // Can't poll, assume OK
|
|
466
|
-
// Poll for result (max 3 attempts, 3s delay)
|
|
467
|
-
const maxAttempts = 3;
|
|
468
|
-
const delayMs = 3000;
|
|
469
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
470
|
-
if (attempt > 1) {
|
|
471
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
472
|
-
}
|
|
473
|
-
try {
|
|
474
|
-
const resultRes = await client.get(`/exec/result/${execId}`);
|
|
475
|
-
const resultData = resultRes.data?.data?.attributes || resultRes.data?.data || {};
|
|
476
|
-
const isPending = typeof resultData.status === 'string' && resultData.status.toLowerCase().includes('pending');
|
|
477
|
-
if (isPending)
|
|
535
|
+
return { errors, warnings };
|
|
536
|
+
}
|
|
537
|
+
export function checkMapperMetadataRetrieved(body, discoveryState, workspaceId = '') {
|
|
538
|
+
const graph = body?.data?.attributes?.graph || body?.attributes?.graph || body?.graph;
|
|
539
|
+
if (!graph)
|
|
540
|
+
return [];
|
|
541
|
+
const errors = [];
|
|
542
|
+
for (const edge of graph.edges || []) {
|
|
543
|
+
if (!edge.config?.mapper)
|
|
544
|
+
continue;
|
|
545
|
+
for (const id of [edge.source, edge.target]) {
|
|
546
|
+
const node = graph.nodes?.find((n) => n.id === id);
|
|
547
|
+
if (!node)
|
|
478
548
|
continue;
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
549
|
+
const op = operation(node);
|
|
550
|
+
const descriptor = descriptorFor(discoveryState, node);
|
|
551
|
+
const definition = descriptor?.actions?.[op.method] || descriptor?.triggers?.[op.method];
|
|
552
|
+
if (!definition)
|
|
553
|
+
errors.push(`Call get_component for ${op.componentId} at version ${op.version} before mapping step ${id}.`);
|
|
554
|
+
else if (definition.dynamicMetadata && evidence(discoveryState).metadata.get(nodeMetadataKey(workspaceId, node))?.status !== 'VERIFIED') {
|
|
555
|
+
errors.push(`Step ${id}: retrieve successful dynamic metadata for this exact version, connection and fields before mapping.`);
|
|
483
556
|
}
|
|
484
|
-
return { ok: true };
|
|
485
|
-
}
|
|
486
|
-
catch (err) {
|
|
487
|
-
const isNotReady = err.message && (err.message.includes('409') || err.message.includes('not ready'));
|
|
488
|
-
if (isNotReady)
|
|
489
|
-
continue;
|
|
490
|
-
throw err;
|
|
491
557
|
}
|
|
492
558
|
}
|
|
493
|
-
|
|
494
|
-
return { ok: true };
|
|
559
|
+
return [...new Set(errors)];
|
|
495
560
|
}
|
|
496
561
|
/**
|
|
497
562
|
* Runtime health check for a flow — validates credentials, required fields, URLs, and field values.
|
|
498
563
|
* Runs after create_flow/update_flow and before start_flow to catch config issues
|
|
499
564
|
* that would show as red indicators in the UI.
|
|
500
565
|
*/
|
|
501
|
-
export async function healthCheckFlow(flow, client) {
|
|
566
|
+
export async function healthCheckFlow(flow, client, discoveryState) {
|
|
502
567
|
const errors = [];
|
|
503
568
|
const warnings = [];
|
|
569
|
+
const stepStatuses = [];
|
|
504
570
|
const nodes = flow?.graph?.nodes || [];
|
|
505
571
|
if (nodes.length === 0) {
|
|
506
572
|
errors.push('Flow has no steps configured');
|
|
507
|
-
return { errors, warnings };
|
|
573
|
+
return { errors, warnings, stepStatuses };
|
|
508
574
|
}
|
|
509
575
|
for (const node of nodes) {
|
|
510
576
|
const stepLabel = `Step '${node.id}' (${node.name || 'unnamed'})`;
|
|
577
|
+
const errCountBefore = errors.length;
|
|
578
|
+
const warnCountBefore = warnings.length;
|
|
511
579
|
// 1. Verify credentials exist and are accessible
|
|
512
580
|
const healthCredId = resolveCredentialId(node.credentials_id);
|
|
513
581
|
if (healthCredId) {
|
|
@@ -542,8 +610,11 @@ export async function healthCheckFlow(flow, client) {
|
|
|
542
610
|
const credOrSecretId = healthCredId || healthSecId;
|
|
543
611
|
if (credOrSecretId) {
|
|
544
612
|
try {
|
|
545
|
-
const
|
|
546
|
-
|
|
613
|
+
const args = nodeCredentialArgs(flow?.workspace?.id || flow?.workspace?.data?.id, node);
|
|
614
|
+
const verifyResult = await verifyCredentials(client, args);
|
|
615
|
+
if (discoveryState)
|
|
616
|
+
evidence(discoveryState).credentials.set(credentialKey(args), verifyResult);
|
|
617
|
+
if (!verifyResult.ok) {
|
|
547
618
|
errors.push(`${stepLabel}: credential verification FAILED for '${credOrSecretId}'. ` +
|
|
548
619
|
`${verifyResult.error} ` +
|
|
549
620
|
`The credential may be expired, revoked, or the user no longer has access. ` +
|
|
@@ -551,17 +622,31 @@ export async function healthCheckFlow(flow, client) {
|
|
|
551
622
|
}
|
|
552
623
|
}
|
|
553
624
|
catch (err) {
|
|
554
|
-
|
|
625
|
+
errors.push(`${stepLabel}: credential verification could not complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
555
626
|
}
|
|
556
627
|
}
|
|
557
628
|
}
|
|
558
629
|
// 2. Check component and required fields
|
|
559
630
|
if (node.component_id) {
|
|
560
631
|
try {
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
632
|
+
// Use the descriptor endpoint to get actions/triggers definitions
|
|
633
|
+
const compRes = await client.get(`/components/${operation(node).componentId}/versions/${operation(node).version}/descriptor`);
|
|
634
|
+
const comp = deserializeJsonApi(compRes.data);
|
|
635
|
+
if (comp && discoveryState)
|
|
636
|
+
evidence(discoveryState).descriptors.set(`${operation(node).componentId}@${operation(node).version}`, comp);
|
|
637
|
+
if (!comp) {
|
|
638
|
+
errors.push(`${stepLabel}: component '${node.component_id}' returned empty data — it may be unavailable`);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
564
641
|
const actionOrTrigger = findActionOrTrigger(comp, node.command);
|
|
642
|
+
if (node.command && !actionOrTrigger) {
|
|
643
|
+
const commandMatch = node.command.match(/^[^:]+:([^@]+)@/);
|
|
644
|
+
const methodId = commandMatch ? commandMatch[1] : node.command;
|
|
645
|
+
const availableActions = Object.keys(comp.actions || {});
|
|
646
|
+
const availableTriggers = Object.keys(comp.triggers || {});
|
|
647
|
+
errors.push(`${stepLabel}: action/trigger '${methodId}' not found in component '${comp.name || node.component_id}'. ` +
|
|
648
|
+
`Available actions: [${availableActions.join(', ')}], triggers: [${availableTriggers.join(', ')}]`);
|
|
649
|
+
}
|
|
565
650
|
if (actionOrTrigger?.fields) {
|
|
566
651
|
for (const [fieldId, fieldDef] of Object.entries(actionOrTrigger.fields)) {
|
|
567
652
|
// Check required fields
|
|
@@ -587,6 +672,13 @@ export async function healthCheckFlow(flow, client) {
|
|
|
587
672
|
warnings.push(`${stepLabel}: component '${compName || compCommandPrefix}' is DEPRECATED. ` +
|
|
588
673
|
`Use '${successor}' instead.`);
|
|
589
674
|
}
|
|
675
|
+
if (actionOrTrigger?.deprecated) {
|
|
676
|
+
const commandMatch = node.command?.match(/^[^:]+:([^@]+)@/);
|
|
677
|
+
const methodId = commandMatch ? commandMatch[1] : (node.command || '');
|
|
678
|
+
const replacement = getDeprecatedActionReplacement(compCommandPrefix || compName, methodId);
|
|
679
|
+
const hint = replacement ? ` Use '${replacement}' instead.` : '';
|
|
680
|
+
warnings.push(`${stepLabel}: action/trigger '${methodId}' is marked as DEPRECATED in component '${comp.name || node.component_id}'.${hint}`);
|
|
681
|
+
}
|
|
590
682
|
// Check if component requires auth but none is set
|
|
591
683
|
const credFields = comp.credentials?.fields;
|
|
592
684
|
const hasCredFields = credFields && Object.keys(credFields).length > 0;
|
|
@@ -596,6 +688,7 @@ export async function healthCheckFlow(flow, client) {
|
|
|
596
688
|
}
|
|
597
689
|
}
|
|
598
690
|
catch (err) {
|
|
691
|
+
errors.push(`${stepLabel}: component '${node.component_id}' could not be fetched — it may have been deleted or is not accessible`);
|
|
599
692
|
logger.warn(`[healthCheck] Could not fetch component '${node.component_id}': ${err instanceof Error ? err.message : String(err)}`);
|
|
600
693
|
}
|
|
601
694
|
}
|
|
@@ -631,6 +724,18 @@ export async function healthCheckFlow(flow, client) {
|
|
|
631
724
|
}
|
|
632
725
|
}
|
|
633
726
|
}
|
|
727
|
+
// Collect per-step status
|
|
728
|
+
const stepErrors = errors.slice(errCountBefore);
|
|
729
|
+
const stepWarnings = warnings.slice(warnCountBefore);
|
|
730
|
+
const commandMatch = node.command?.match(/^([^:]+):([^@]+)@/);
|
|
731
|
+
const componentName = commandMatch ? commandMatch[1] : (node.component_id || 'unknown');
|
|
732
|
+
stepStatuses.push({
|
|
733
|
+
stepId: node.id,
|
|
734
|
+
name: node.name || 'unnamed',
|
|
735
|
+
component: componentName,
|
|
736
|
+
status: stepErrors.length > 0 ? 'ERROR' : stepWarnings.length > 0 ? 'WARNING' : 'OK',
|
|
737
|
+
issues: [...stepErrors, ...stepWarnings],
|
|
738
|
+
});
|
|
634
739
|
}
|
|
635
740
|
// 5. Check edges with missing mappers for non-first target steps.
|
|
636
741
|
// Missing mappers cause empty/red fields in the UI.
|
|
@@ -646,77 +751,13 @@ export async function healthCheckFlow(flow, client) {
|
|
|
646
751
|
}
|
|
647
752
|
}
|
|
648
753
|
}
|
|
649
|
-
|
|
650
|
-
// If a field is defined as type:"array" in the schema but the mapper value is a plain
|
|
651
|
-
// string passthrough (not a JSONata array expression), warn that it needs to be an array expression.
|
|
652
|
-
for (const edge of edges) {
|
|
653
|
-
const edgeId = edge.id || `${edge.source}:${edge.target}`;
|
|
654
|
-
const mapper = edge.config?.mapper;
|
|
655
|
-
if (!mapper || typeof mapper !== 'object' || Array.isArray(mapper))
|
|
656
|
-
continue;
|
|
657
|
-
const targetNode = nodes.find((n) => n.id === edge.target);
|
|
658
|
-
if (!targetNode?.component_id)
|
|
659
|
-
continue;
|
|
660
|
-
try {
|
|
661
|
-
const compRes = await client.get(`/components/${targetNode.component_id}`);
|
|
662
|
-
const comp = compRes.data?.data?.attributes || compRes.data?.data;
|
|
663
|
-
if (!comp)
|
|
664
|
-
continue;
|
|
665
|
-
const actionOrTrigger = findActionOrTrigger(comp, targetNode.command);
|
|
666
|
-
const inputSchema = actionOrTrigger?.metadata?.in?.properties;
|
|
667
|
-
// For components with dynamicMetadata and no static input schema,
|
|
668
|
-
// we can't verify field types statically. Warn if any mapper field name
|
|
669
|
-
// contains common array-indicating patterns (e.g. "urls", "items", "files", "messages")
|
|
670
|
-
// and the value is not a JSONata array expression.
|
|
671
|
-
if (!inputSchema && actionOrTrigger?.dynamicMetadata === true) {
|
|
672
|
-
const arrayFieldPatterns = /^(file_urls|urls|items|files|messages|recipients|attachments|records|entries|rows|contacts|emails)$/i;
|
|
673
|
-
for (const [fieldKey, fieldValue] of Object.entries(mapper)) {
|
|
674
|
-
if (typeof fieldValue === 'string' && arrayFieldPatterns.test(fieldKey)) {
|
|
675
|
-
const trimmed = fieldValue.trim();
|
|
676
|
-
if (!trimmed.startsWith('[')) {
|
|
677
|
-
warnings.push(`Edge '${edgeId}' mapper field '${fieldKey}' looks like an array-typed field (dynamicMetadata component) ` +
|
|
678
|
-
`but is mapped as a plain string "${trimmed}". The UI visual mapper cannot render array fields mapped ` +
|
|
679
|
-
`as plain passthroughs — it shows "impossible to map properties not inside the array property". ` +
|
|
680
|
-
`Use a JSONata array expression starting with "[" to construct the array inline, e.g.: ` +
|
|
681
|
-
`"[{\\"file_url\\": $getPassthrough().step_X.body.file_url}]". ` +
|
|
682
|
-
`The mapping works at runtime but causes red indicators in the UI.`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
if (!inputSchema)
|
|
689
|
-
continue;
|
|
690
|
-
for (const [fieldKey, fieldValue] of Object.entries(mapper)) {
|
|
691
|
-
const schemaDef = inputSchema[fieldKey];
|
|
692
|
-
// Check array-typed fields mapped as plain strings
|
|
693
|
-
if (schemaDef?.type === 'array' && typeof fieldValue === 'string') {
|
|
694
|
-
const trimmed = fieldValue.trim();
|
|
695
|
-
// A valid array mapper should start with "[" (JSONata array expression)
|
|
696
|
-
if (!trimmed.startsWith('[')) {
|
|
697
|
-
const itemProps = schemaDef.items?.properties || schemaDef.items?.type;
|
|
698
|
-
const hint = typeof itemProps === 'object'
|
|
699
|
-
? `Expected format: [{"${Object.keys(itemProps)[0]}": ...}]`
|
|
700
|
-
: `Expected a JSONata expression that evaluates to an array`;
|
|
701
|
-
errors.push(`Edge '${edgeId}' mapper field '${fieldKey}' is mapped as a plain string, but the component schema ` +
|
|
702
|
-
`defines it as type "array". A plain string passthrough will cause a type mismatch (red field in the UI). ` +
|
|
703
|
-
`${hint}. Wrap the value in a JSONata array expression, e.g.: ` +
|
|
704
|
-
`"[{\\"field\\": $getPassthrough().step_X.body.value}]".`);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
// Component fetch may have already been done above; skip on error
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return { errors, warnings };
|
|
754
|
+
return { errors, warnings, stepStatuses };
|
|
714
755
|
}
|
|
715
756
|
/** Find action or trigger definition by parsing the command string */
|
|
716
757
|
function findActionOrTrigger(component, command) {
|
|
717
758
|
if (!command)
|
|
718
759
|
return null;
|
|
719
|
-
const match =
|
|
760
|
+
const match = /^[^:]+:([^@]+)@/.exec(command);
|
|
720
761
|
if (!match)
|
|
721
762
|
return null;
|
|
722
763
|
const methodId = match[1];
|
|
@@ -724,10 +765,18 @@ function findActionOrTrigger(component, command) {
|
|
|
724
765
|
}
|
|
725
766
|
/** Format health check results as a readable string to append to tool response */
|
|
726
767
|
export function formatHealthCheck(result) {
|
|
727
|
-
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
728
|
-
return '\n\nHealth check passed — no issues found.';
|
|
729
|
-
}
|
|
730
768
|
const parts = ['\n\n--- Flow Health Check ---'];
|
|
769
|
+
// Per-step status summary
|
|
770
|
+
if (result.stepStatuses && result.stepStatuses.length > 0) {
|
|
771
|
+
parts.push('\nStep Validation:');
|
|
772
|
+
for (const step of result.stepStatuses) {
|
|
773
|
+
const icon = step.status === 'OK' ? '[OK]' : step.status === 'WARNING' ? '[WARN]' : '[FAIL]';
|
|
774
|
+
parts.push(` ${icon} ${step.stepId} (${step.name}) — ${step.component}`);
|
|
775
|
+
for (const issue of step.issues) {
|
|
776
|
+
parts.push(` ${issue}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
731
780
|
if (result.errors.length > 0) {
|
|
732
781
|
parts.push(`\nERRORS (${result.errors.length}):`);
|
|
733
782
|
for (const err of result.errors) {
|
|
@@ -740,7 +789,10 @@ export function formatHealthCheck(result) {
|
|
|
740
789
|
parts.push(` - ${warn}`);
|
|
741
790
|
}
|
|
742
791
|
}
|
|
743
|
-
if (result.errors.length
|
|
792
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
793
|
+
parts.push('\nAll steps validated successfully.');
|
|
794
|
+
}
|
|
795
|
+
else if (result.errors.length > 0) {
|
|
744
796
|
parts.push('\nFix the errors above before starting the flow.');
|
|
745
797
|
}
|
|
746
798
|
return parts.join('\n');
|