@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.
Files changed (118) hide show
  1. package/README.md +128 -126
  2. package/README.npm.md +173 -0
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +8 -3
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/api/jsonapi.d.ts +2 -9
  7. package/dist/api/jsonapi.d.ts.map +1 -1
  8. package/dist/api/jsonapi.js +1 -14
  9. package/dist/api/jsonapi.js.map +1 -1
  10. package/dist/api/logger.d.ts +0 -1
  11. package/dist/api/logger.d.ts.map +1 -1
  12. package/dist/api/logger.js +2 -5
  13. package/dist/api/logger.js.map +1 -1
  14. package/dist/api/samples.d.ts +0 -6
  15. package/dist/api/samples.d.ts.map +1 -1
  16. package/dist/api/samples.js +8 -8
  17. package/dist/api/samples.js.map +1 -1
  18. package/dist/api/verification.d.ts +6 -0
  19. package/dist/api/verification.d.ts.map +1 -0
  20. package/dist/api/verification.js +71 -0
  21. package/dist/api/verification.js.map +1 -0
  22. package/dist/cli.d.ts +3 -0
  23. package/dist/cli.d.ts.map +1 -0
  24. package/dist/cli.js +9 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/config.d.ts +0 -1
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +0 -5
  29. package/dist/config.js.map +1 -1
  30. package/dist/index.js +11 -10
  31. package/dist/index.js.map +1 -1
  32. package/dist/paths.d.ts +0 -1
  33. package/dist/paths.d.ts.map +1 -1
  34. package/dist/paths.js +1 -1
  35. package/dist/paths.js.map +1 -1
  36. package/dist/server.d.ts.map +1 -1
  37. package/dist/server.js +24 -0
  38. package/dist/server.js.map +1 -1
  39. package/dist/setup.d.ts +3 -0
  40. package/dist/setup.d.ts.map +1 -0
  41. package/dist/setup.js +338 -0
  42. package/dist/setup.js.map +1 -0
  43. package/dist/tools/confirmation.d.ts +0 -4
  44. package/dist/tools/confirmation.d.ts.map +1 -1
  45. package/dist/tools/confirmation.js +1 -1
  46. package/dist/tools/confirmation.js.map +1 -1
  47. package/dist/tools/constants.d.ts +6 -9
  48. package/dist/tools/constants.d.ts.map +1 -1
  49. package/dist/tools/constants.js +48 -41
  50. package/dist/tools/constants.js.map +1 -1
  51. package/dist/tools/definitions.d.ts.map +1 -1
  52. package/dist/tools/definitions.js +37 -5
  53. package/dist/tools/definitions.js.map +1 -1
  54. package/dist/tools/flow-build.d.ts +18 -0
  55. package/dist/tools/flow-build.d.ts.map +1 -0
  56. package/dist/tools/flow-build.js +295 -0
  57. package/dist/tools/flow-build.js.map +1 -0
  58. package/dist/tools/flow-evidence.d.ts +62 -0
  59. package/dist/tools/flow-evidence.d.ts.map +1 -0
  60. package/dist/tools/flow-evidence.js +128 -0
  61. package/dist/tools/flow-evidence.js.map +1 -0
  62. package/dist/tools/flow-state-machine.d.ts +69 -0
  63. package/dist/tools/flow-state-machine.d.ts.map +1 -0
  64. package/dist/tools/flow-state-machine.js +292 -0
  65. package/dist/tools/flow-state-machine.js.map +1 -0
  66. package/dist/tools/handlers/auth.d.ts.map +1 -1
  67. package/dist/tools/handlers/auth.js +18 -148
  68. package/dist/tools/handlers/auth.js.map +1 -1
  69. package/dist/tools/handlers/component.d.ts.map +1 -1
  70. package/dist/tools/handlers/component.js +28 -8
  71. package/dist/tools/handlers/component.js.map +1 -1
  72. package/dist/tools/handlers/data-sample.d.ts.map +1 -1
  73. package/dist/tools/handlers/data-sample.js +37 -5
  74. package/dist/tools/handlers/data-sample.js.map +1 -1
  75. package/dist/tools/handlers/dynamic.d.ts.map +1 -1
  76. package/dist/tools/handlers/dynamic.js +43 -14
  77. package/dist/tools/handlers/dynamic.js.map +1 -1
  78. package/dist/tools/handlers/flow.d.ts.map +1 -1
  79. package/dist/tools/handlers/flow.js +147 -619
  80. package/dist/tools/handlers/flow.js.map +1 -1
  81. package/dist/tools/handlers/log.d.ts.map +1 -1
  82. package/dist/tools/handlers/log.js +31 -0
  83. package/dist/tools/handlers/log.js.map +1 -1
  84. package/dist/tools/handlers/raw.d.ts.map +1 -1
  85. package/dist/tools/handlers/raw.js +5 -2
  86. package/dist/tools/handlers/raw.js.map +1 -1
  87. package/dist/tools/handlers/search.d.ts.map +1 -1
  88. package/dist/tools/handlers/search.js +67 -44
  89. package/dist/tools/handlers/search.js.map +1 -1
  90. package/dist/tools/handlers/session.d.ts.map +1 -1
  91. package/dist/tools/handlers/session.js +8 -1
  92. package/dist/tools/handlers/session.js.map +1 -1
  93. package/dist/tools/handlers.d.ts.map +1 -1
  94. package/dist/tools/handlers.js +7 -2
  95. package/dist/tools/handlers.js.map +1 -1
  96. package/dist/tools/helpers.d.ts +0 -1
  97. package/dist/tools/helpers.d.ts.map +1 -1
  98. package/dist/tools/helpers.js +2 -2
  99. package/dist/tools/helpers.js.map +1 -1
  100. package/dist/tools/types.d.ts +8 -1
  101. package/dist/tools/types.d.ts.map +1 -1
  102. package/dist/tools/validation.d.ts +16 -5
  103. package/dist/tools/validation.d.ts.map +1 -1
  104. package/dist/tools/validation.js +220 -168
  105. package/dist/tools/validation.js.map +1 -1
  106. package/dist/vectorstore/qdrant.js +1 -1
  107. package/dist/vectorstore/qdrant.js.map +1 -1
  108. package/package.json +22 -7
  109. package/resources/docs/flow-building-guide.md +18 -2
  110. package/resources/docs/rest-api-pagination.md +58 -0
  111. package/resources/docs/system-instructions.md +35 -5
  112. package/resources/docs/component-test-scenarios.md +0 -2468
  113. package/resources/docs/credential-validation-gaps.md +0 -35
  114. package/resources/docs/debug-issues.md +0 -71
  115. package/resources/docs/deployment-and-scaling.md +0 -136
  116. package/resources/docs/packaging-plan.md +0 -84
  117. package/resources/docs/unique-components.md +0 -60
  118. package/resources/docs/verified-creds-flow-ids.md +0 -193
@@ -1,8 +1,10 @@
1
+ import { inspectFlow, readFlow, saveDraft, publishDraft, validateSample, resampleStaleSteps } from '../flow-build.js';
2
+ import { getFlowBuildStatus, configurationRevision, requireInactive, requireRevision, markRunRequested, schemaErrors } from '../flow-state-machine.js';
3
+ import { fingerprint, operation, schemaFor } from '../flow-evidence.js';
1
4
  import { deserializeJsonApi, deserializeWithMeta } from '../../api/jsonapi.js';
2
- import { logger } from '../../api/logger.js';
3
- import { textResult, paginatedResult, formatResult } from '../helpers.js';
4
- import { validateFlowGraph, checkDiscoveryState, checkFlowLifecycle, checkFlowBuildingGuideRead, healthCheckFlow, formatHealthCheck } from '../validation.js';
5
- import { PLACEHOLDER_VALUES, PLACEHOLDER_DEFAULT, COMPONENT_SAMPLE_SCHEMAS, PAGE_SIZE } from '../constants.js';
5
+ import { textResult, paginatedResult } from '../helpers.js';
6
+ import { validateFlowGraph, checkFlowBuildingGuideRead, healthCheckFlow } from '../validation.js';
7
+ import { PAGE_SIZE } from '../constants.js';
6
8
  /**
7
9
  * Full domino-effect impact analysis for flow graph changes.
8
10
  * Compares existing flow/draft graph with new body and returns a formatted
@@ -14,8 +16,8 @@ export function analyzeFlowImpact(existingFlow, body, newNodes) {
14
16
  const impactLines = [];
15
17
  const prevNodes = existingFlow.graph.nodes || [];
16
18
  const prevEdges = existingFlow.graph.edges || [];
17
- const updatedNodes = newNodes || body?.data?.attributes?.graph?.nodes || [];
18
- const newEdges = body?.data?.attributes?.graph?.edges || [];
19
+ const updatedNodes = newNodes || (body)?.data?.attributes?.graph?.nodes || [];
20
+ const newEdges = (body)?.data?.attributes?.graph?.edges || [];
19
21
  if (updatedNodes.length === 0 && newEdges.length === 0)
20
22
  return null;
21
23
  const newNodeIds = new Set(updatedNodes.map((n) => n.id));
@@ -209,362 +211,36 @@ export function analyzeFlowImpact(existingFlow, body, newNodes) {
209
211
  return null;
210
212
  return `--- Impact Analysis ---\n${impactLines.join('\n')}`;
211
213
  }
212
- /**
213
- * Infer a plausible placeholder value for a field name based on naming conventions.
214
- */
215
- function inferPlaceholderValue(fieldName) {
216
- const lower = fieldName.toLowerCase();
217
- for (const entry of PLACEHOLDER_VALUES) {
218
- if (entry.test(lower)) {
219
- return typeof entry.value === 'function' ? entry.value() : entry.value;
220
- }
221
- }
222
- return PLACEHOLDER_DEFAULT;
223
- }
224
- /**
225
- * Generate a meaningful sample body for a node by analyzing downstream mapper
226
- * references and falling back to known component output patterns.
227
- */
228
- function generateSampleBody(node, edges, nodesConfig) {
229
- const nodeId = node.id;
230
- const command = node.command || '';
231
- // Check if nodes_config has explicit sample data for this node
232
- const nodeConfig = nodesConfig[nodeId];
233
- if (nodeConfig?.sample?.body && Object.keys(nodeConfig.sample.body).length > 0) {
234
- return nodeConfig.sample.body;
235
- }
236
- // Scan all downstream mappers and conditions for references to this node's output.
237
- // Track both deep field refs ($getPassthrough().nodeId.body.field) and whole body refs
238
- // ($getPassthrough().nodeId.body). For whole body refs, also track the mapper KEY
239
- // so we can infer what fields the downstream component expects.
240
- const referencedFields = new Set();
241
- const wholeBodyMapperKeys = new Set();
242
- let hasWholeBodyRef = false;
243
- for (const edge of edges) {
244
- const mapper = edge.config?.mapper;
245
- if (mapper && typeof mapper === 'object' && !Array.isArray(mapper)) {
246
- for (const [mapperKey, val] of Object.entries(mapper)) {
247
- if (typeof val !== 'string')
248
- continue;
249
- // Match $getPassthrough().nodeId.body.field (deep path)
250
- const fieldRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body\\.([a-zA-Z_][a-zA-Z0-9_.]*)`, 'g');
251
- let match;
252
- while ((match = fieldRegex.exec(val)) !== null) {
253
- referencedFields.add(match[1].split('.')[0]);
254
- }
255
- // Match whole body reference: $getPassthrough().nodeId.body (not followed by .)
256
- const wholeBodyRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body(?![.a-zA-Z_])`, 'g');
257
- if (wholeBodyRegex.test(val)) {
258
- hasWholeBodyRef = true;
259
- wholeBodyMapperKeys.add(mapperKey);
260
- }
261
- }
262
- }
263
- else if (typeof mapper === 'string') {
264
- const fieldRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body\\.([a-zA-Z_][a-zA-Z0-9_.]*)`, 'g');
265
- let match;
266
- while ((match = fieldRegex.exec(mapper)) !== null) {
267
- referencedFields.add(match[1].split('.')[0]);
268
- }
269
- const wholeBodyRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body(?![.a-zA-Z_])`, 'g');
270
- if (wholeBodyRegex.test(mapper)) {
271
- hasWholeBodyRef = true;
272
- }
273
- }
274
- if (edge.config?.condition && typeof edge.config.condition === 'string') {
275
- const cond = edge.config.condition;
276
- const fieldRegex = new RegExp(`\\$getPassthrough\\(\\)\\.${nodeId}\\.body\\.([a-zA-Z_][a-zA-Z0-9_.]*)`, 'g');
277
- let match;
278
- while ((match = fieldRegex.exec(cond)) !== null) {
279
- referencedFields.add(match[1].split('.')[0]);
280
- }
281
- }
282
- }
283
- // Build sample body from referenced fields
284
- const body = {};
285
- for (const field of referencedFields) {
286
- body[field] = inferPlaceholderValue(field);
287
- }
288
- // If downstream references the whole body, use the mapper keys as field names
289
- // so the UI can render meaningful passthrough pills.
290
- // E.g. mapper {"idValue": "$getPassthrough().step_1.body"} → sample {idValue: "sample_value"}
291
- if (hasWholeBodyRef) {
292
- for (const key of wholeBodyMapperKeys) {
293
- if (!body[key]) {
294
- body[key] = inferPlaceholderValue(key);
295
- }
296
- }
297
- // Fallback if no keys were captured
298
- if (Object.keys(body).length === 0) {
299
- body.message = 'Sample data';
300
- body.id = 'sample-id-123';
301
- }
302
- }
303
- // If we found referenced fields, return them
304
- if (Object.keys(body).length > 0)
305
- return body;
306
- // Fall back to known component output patterns
307
- for (const schema of COMPONENT_SAMPLE_SCHEMAS) {
308
- if (schema.pattern.test(command)) {
309
- return schema.sample();
310
- }
311
- }
312
- // Generic fallback
313
- return { sample: 'auto-generated' };
314
- }
315
- const DEBUG_SYNC_TIMEOUT_MS = 30_000;
316
- /**
317
- * Execute a single node via /flows/debug-sync and return the output body.
318
- * Returns null on any failure (timeout, bad creds, component error, etc.).
319
- */
320
- async function executeDebugSync(client, workspaceId, node, sampleData) {
321
- try {
322
- const nodePayload = {
323
- id: node.id,
324
- command: node.command,
325
- fields: node.fields || {}
326
- };
327
- if (node.credentials_id)
328
- nodePayload.credentials_id = node.credentials_id;
329
- if (node.secret_id)
330
- nodePayload.secret_id = node.secret_id;
331
- const payload = {
332
- data: {
333
- type: 'flow',
334
- attributes: {
335
- name: `Debug ${node.command}`,
336
- type: 'debug',
337
- graph: { nodes: [nodePayload], edges: [] }
338
- },
339
- meta: { sampleData },
340
- relationships: {
341
- workspace: { data: { id: workspaceId, type: 'workspace' } }
342
- }
343
- }
344
- };
345
- const response = await client.post('/flows/debug-sync', payload, {
346
- timeout: DEBUG_SYNC_TIMEOUT_MS,
347
- __retryCount: 3 // skip retries — debug-sync 500s are component failures, not transient
348
- });
349
- const result = response.data?.data?.data || response.data?.data;
350
- const body = result?.body;
351
- if (body && typeof body === 'object' && Object.keys(body).length > 0) {
352
- return body;
353
- }
354
- logger.warn(`[executeDebugSync] No usable body for ${node.id}, keys=${JSON.stringify(Object.keys(response.data?.data || {}))}`);
355
- return null;
356
- }
357
- catch (err) {
358
- logger.warn(`[executeDebugSync] Failed for node ${node.id}: ${err instanceof Error ? err.message : String(err)}`);
359
- return null;
360
- }
361
- }
362
- /**
363
- * After creating or updating a flow, automatically create data samples
364
- * for nodes that don't already have selected_data_samples.
365
- * Attempts debug-sync retrieval only for new nodes (without existing samples),
366
- * falls back to synthetic samples on failure.
367
- */
368
- async function autoConfirmSamples(flowId, client, discoveryState) {
369
- try {
370
- // Get the published flow for workspace info
371
- const flowRes = await client.get(`/flows/${flowId}`);
372
- const flow = deserializeJsonApi(flowRes.data);
373
- const workspaceId = flow?.workspace?.id
374
- || flow?.workspace?.data?.id;
375
- if (!workspaceId)
376
- return;
377
- // Get or create the draft
378
- let draft;
379
- try {
380
- const draftRes = await client.get(`/flows/${flowId}/draft`);
381
- draft = deserializeJsonApi(draftRes.data);
382
- }
383
- catch {
384
- // Draft doesn't exist yet — create it from the flow's graph
385
- const draftAttributes = {
386
- name: flow.name || flow.attributes?.name || 'Untitled',
387
- graph: flow.graph
388
- };
389
- if (flow.cron || flow.attributes?.cron) {
390
- draftAttributes.cron = flow.cron || flow.attributes.cron;
391
- }
392
- if (flow.type || flow.attributes?.type) {
393
- draftAttributes.type = flow.type || flow.attributes.type;
394
- }
395
- const createDraftRes = await client.put(`/flows/${flowId}/draft`, {
396
- data: {
397
- type: 'flow-draft',
398
- attributes: draftAttributes
399
- }
400
- });
401
- draft = deserializeJsonApi(createDraftRes.data);
402
- }
403
- // The draft is always the source of truth for graph structure.
404
- // autoConfirmSamples only adds missing samples — it never overwrites
405
- // the draft's nodes, fields, commands, or edges.
406
- const draftNodes = draft?.graph?.nodes || [];
407
- if (draftNodes.length === 0)
408
- return;
409
- let draftUpdated = false;
410
- for (const node of draftNodes) {
411
- // Skip nodes that already have confirmed samples
412
- if (node.selected_data_samples && node.selected_data_samples.length > 0) {
413
- continue;
414
- }
415
- const componentId = node.component_id;
416
- if (!componentId)
417
- continue;
418
- const commandMatch = node.command?.match(/^[^:]+:([^@]+)@/);
419
- const method = commandMatch?.[1] || '';
420
- if (!method)
421
- continue;
422
- let versionId = discoveryState.componentVersions.get(componentId);
423
- if (!versionId) {
424
- try {
425
- const compRes = await client.get(`/components/${componentId}`);
426
- const comp = deserializeJsonApi(compRes.data);
427
- versionId = comp?.latest_version?.id || comp?.latest_version?.data?.id;
428
- if (versionId) {
429
- discoveryState.componentVersions.set(componentId, versionId);
430
- }
431
- }
432
- catch (err) {
433
- logger.warn(`[autoConfirmSamples] Failed to fetch component ${componentId}: ${err instanceof Error ? err.message : String(err)}`);
434
- continue;
435
- }
436
- }
437
- if (!versionId)
438
- continue;
439
- // Attempt live retrieval via debug-sync — no synthetic fallback
440
- const sampleBody = await executeDebugSync(client, workspaceId, node, {});
441
- if (sampleBody) {
442
- logger.info(`[autoConfirmSamples] Live sample retrieved for node ${node.id}`);
443
- }
444
- else {
445
- logger.warn(`[autoConfirmSamples] No live sample for node ${node.id} — skipping (use debug_flow_step to retrieve manually)`);
446
- continue;
447
- }
448
- try {
449
- const sampleRes = await client.post('/data-samples', {
450
- data: {
451
- type: 'data-sample',
452
- attributes: {
453
- result: { body: sampleBody },
454
- method
455
- },
456
- relationships: {
457
- component: { data: { type: 'component', id: componentId } },
458
- component_version: { data: { type: 'component-version', id: versionId } },
459
- workspace: { data: { type: 'workspace', id: workspaceId } }
460
- }
461
- }
462
- });
463
- const sample = deserializeJsonApi(sampleRes.data);
464
- if (sample?.id) {
465
- node.selected_data_samples = [sample.id];
466
- draftUpdated = true;
467
- }
468
- }
469
- catch (err) {
470
- logger.warn(`[autoConfirmSamples] Failed to create sample for node ${node.id}: ${err instanceof Error ? err.message : String(err)}`);
471
- }
472
- }
473
- if (draftUpdated) {
474
- // Always use draft as source of truth — never overwrite draft with published flow data
475
- const source = draft;
476
- const updateDraftAttributes = {
477
- name: source.name || source.attributes?.name || 'Untitled',
478
- graph: draft.graph
479
- };
480
- const cron = source.cron || source.attributes?.cron;
481
- if (cron) {
482
- updateDraftAttributes.cron = cron;
483
- }
484
- const type = source.type || source.attributes?.type;
485
- if (type) {
486
- updateDraftAttributes.type = type;
487
- }
488
- if (source.description !== undefined) {
489
- updateDraftAttributes.description = source.description;
490
- }
491
- if (source.nodes_config !== undefined) {
492
- updateDraftAttributes.nodes_config = source.nodes_config;
493
- }
494
- await client.put(`/flows/${flowId}/draft`, {
495
- data: {
496
- id: draft.id,
497
- type: 'flow-draft',
498
- attributes: updateDraftAttributes
499
- }
500
- });
501
- logger.info(`[autoConfirmSamples] Auto-confirmed synthetic samples for flow ${flowId}`);
502
- }
503
- }
504
- catch (err) {
505
- // Non-fatal: flow was created/updated successfully, sample confirmation is best-effort
506
- logger.warn(`[autoConfirmSamples] Failed to auto-confirm samples for flow ${flowId}: ${err instanceof Error ? err.message : String(err)}`);
507
- }
508
- }
509
214
  export const flowHandlers = {
510
215
  create_flow: async (args, { client, discoveryState }) => {
511
216
  const { body } = args;
512
217
  const guideErrors = checkFlowBuildingGuideRead(discoveryState);
513
- if (guideErrors.length > 0) {
514
- throw new Error(`Playbook State Machine Check Failed:\n- ${guideErrors.join('\n- ')}`);
515
- }
516
- const { errors: valErrors, warnings: valWarnings } = validateFlowGraph(body);
517
- if (valErrors.length > 0) {
518
- throw new Error(`Flow Graph Validation Failed:\n- ${valErrors.join('\n- ')}`);
519
- }
520
- const stateErrors = checkDiscoveryState(body, discoveryState);
521
- if (stateErrors.length > 0) {
522
- throw new Error(`Playbook State Machine Check Failed:\n- ${stateErrors.join('\n- ')}`);
523
- }
524
- // Default cron to every 10 minutes if not specified and flow type is ordinary
218
+ if (guideErrors.length)
219
+ throw new Error(guideErrors.join('\n'));
525
220
  const attrs = body?.data?.attributes;
526
- if (attrs && !attrs.cron && (!attrs.type || attrs.type === 'ordinary')) {
527
- attrs.cron = '*/10 * * * *';
221
+ if (!attrs || 'graph' in attrs || 'nodes' in attrs || 'edges' in attrs) {
222
+ throw new Error('create_flow creates an empty flow. Supply minimal attributes without graph; build the draft with update_flow_draft.');
528
223
  }
224
+ if (!attrs.cron && (!attrs.type || attrs.type === 'ordinary'))
225
+ attrs.cron = '*/10 * * * *';
529
226
  const response = await client.post('/flows', body);
530
227
  const data = deserializeJsonApi(response.data);
531
- if (data?.id) {
532
- await autoConfirmSamples(data.id, client, discoveryState);
533
- }
534
- // Run health check after creation, merging graph validation warnings
535
- const healthCheck = await healthCheckFlow(data, client);
536
- healthCheck.warnings.push(...valWarnings);
537
- const healthSuffix = formatHealthCheck(healthCheck);
538
- const resultText = formatResult(data) + healthSuffix;
539
- return { content: [{ type: 'text', text: resultText }] };
228
+ if (!data?.id)
229
+ throw new Error('Create returned no flow ID. Inspect existing flows before retrying to avoid a duplicate.');
230
+ const current = await inspectFlow(client, discoveryState, data.id);
231
+ return textResult({ ...current.flow, build: await getFlowBuildStatus(discoveryState, data.id) });
540
232
  },
541
233
  get_flow: async (args, { client, discoveryState }) => {
542
- const { flowId, include } = args;
543
- let url = `/flows/${flowId}`;
544
- if (include !== undefined) {
545
- url += `?include=${include}`;
546
- }
547
- const response = await client.get(url);
548
- const data = deserializeJsonApi(response.data);
549
- discoveryState.inspectedFlows.add(flowId);
550
- // Auto-fetch draft if one exists
551
- let draft = null;
552
- if (data?.draft?.data?.id) {
553
- try {
554
- const draftRes = await client.get(`/flows/${flowId}/draft`);
555
- draft = deserializeJsonApi(draftRes.data);
556
- }
557
- catch (err) {
558
- // Draft link exists but fetch failed — skip silently
559
- }
560
- }
561
- return textResult(draft ? { ...data, draft_content: draft } : data);
234
+ const current = await inspectFlow(client, discoveryState, args.flowId);
235
+ return textResult({ ...current.flow, draft_content: current.draft, build: await getFlowBuildStatus(discoveryState, args.flowId) });
562
236
  },
563
- get_flow_draft: async (args, { client }) => {
564
- const { flowId } = args;
565
- const response = await client.get(`/flows/${flowId}/draft`);
566
- const data = deserializeJsonApi(response.data);
567
- return textResult(data);
237
+ get_flow_draft: async (args, { client, discoveryState }) => {
238
+ const current = await inspectFlow(client, discoveryState, args.flowId);
239
+ return textResult({ draft: current.draft, build: await getFlowBuildStatus(discoveryState, args.flowId) });
240
+ },
241
+ get_flow_build_status: async (args, { client, discoveryState }) => {
242
+ await inspectFlow(client, discoveryState, args.flowId);
243
+ return textResult(await getFlowBuildStatus(discoveryState, args.flowId));
568
244
  },
569
245
  list_flows: async (args, { client }) => {
570
246
  const { workspaceId, pageSize, pageNumber, search, status, type, componentId, hasDraft, userId, include, sort } = args;
@@ -595,7 +271,7 @@ export const flowHandlers = {
595
271
  }
596
272
  const response = await client.get(url);
597
273
  const { data: rawData, meta } = deserializeWithMeta(response.data);
598
- let flowList = Array.isArray(rawData) ? rawData : (rawData ? [rawData] : []);
274
+ const flowList = Array.isArray(rawData) ? rawData : (rawData ? [rawData] : []);
599
275
  const summary = flowList.map((flow) => ({
600
276
  id: flow.id,
601
277
  name: flow.name,
@@ -605,295 +281,147 @@ export const flowHandlers = {
605
281
  }));
606
282
  return paginatedResult(summary, meta, limit);
607
283
  },
608
- run_flow_now: async (args, { client }) => {
609
- const { flowId } = args;
610
- const response = await client.post(`/flows/${flowId}/run-now`);
284
+ run_flow_now: async (args, { client, discoveryState }) => {
285
+ const current = await inspectFlow(client, discoveryState, args.flowId);
286
+ if (current.runtime !== 'ACTIVE')
287
+ throw new Error('Flow must be ACTIVE before run_flow_now.');
288
+ const response = await client.post(`/flows/${args.flowId}/run-now`);
611
289
  const data = deserializeJsonApi(response.data);
612
- return textResult(data);
290
+ markRunRequested(discoveryState, args.flowId, data?.execution_id || data?.execution?.id);
291
+ return textResult({ result: data, verification: 'PENDING', message: 'Execution requested. Successful completion has not been verified.' });
613
292
  },
614
- start_flow: async (args, { client }) => {
615
- const { flowId } = args;
616
- // Pre-start health check
617
- const flowRes = await client.get(`/flows/${flowId}`);
618
- const flow = deserializeJsonApi(flowRes.data);
619
- const healthCheck = await healthCheckFlow(flow, client);
620
- if (healthCheck.errors.length > 0) {
621
- const healthSuffix = formatHealthCheck(healthCheck);
622
- throw new Error(`Flow cannot be started due to configuration issues:${healthSuffix}`);
623
- }
624
- const response = await client.post(`/flows/${flowId}/start`);
625
- const data = deserializeJsonApi(response.data);
626
- if (healthCheck.warnings.length > 0) {
627
- const healthSuffix = formatHealthCheck(healthCheck);
628
- const resultText = formatResult(data) + healthSuffix;
629
- return { content: [{ type: 'text', text: resultText }] };
630
- }
631
- return textResult(data);
293
+ start_flow: async (args, { client, discoveryState }) => {
294
+ const current = await inspectFlow(client, discoveryState, args.flowId);
295
+ if (current.runtime === 'ACTIVE')
296
+ return textResult({ ...current.flow, alreadyActive: true });
297
+ requireInactive(current);
298
+ if (current.draft) {
299
+ const draftGraph = configurationRevision({ graph: current.draft.graph }, false);
300
+ const flowGraph = configurationRevision({ graph: current.flow.graph }, false);
301
+ if (draftGraph !== flowGraph) {
302
+ throw new Error('Unpublished draft changes exist. Publish the validated draft first.');
303
+ }
304
+ }
305
+ const structural = validateFlowGraph({ graph: current.flow.graph });
306
+ const health = await healthCheckFlow(current.flow, client, discoveryState);
307
+ const errors = [...structural.errors, ...health.errors];
308
+ if (errors.length)
309
+ throw new Error(`Flow cannot be started:\n- ${errors.join('\n- ')}`);
310
+ const latest = await readFlow(client, args.flowId);
311
+ requireRevision(latest, current.revision);
312
+ requireInactive(latest);
313
+ await client.post(`/flows/${args.flowId}/start`);
314
+ const started = await inspectFlow(client, discoveryState, args.flowId);
315
+ return textResult({ ...started.flow, operation: 'start_requested', runtime: started.runtime });
632
316
  },
633
- stop_flow: async (args, { client }) => {
634
- const { flowId } = args;
635
- const response = await client.post(`/flows/${flowId}/stop`);
636
- const data = deserializeJsonApi(response.data);
637
- return textResult(data);
317
+ stop_flow: async (args, { client, discoveryState }) => {
318
+ const current = await inspectFlow(client, discoveryState, args.flowId);
319
+ if (current.runtime === 'INACTIVE')
320
+ return textResult({ ...current.flow, alreadyInactive: true });
321
+ if (current.runtime === 'UNKNOWN')
322
+ throw new Error('Cannot stop flow with unknown runtime status. Inspect again.');
323
+ if (current.runtime !== 'STOPPING')
324
+ await client.post(`/flows/${args.flowId}/stop`);
325
+ const stopped = await inspectFlow(client, discoveryState, args.flowId);
326
+ return textResult({ ...stopped.flow, operation: 'stop_requested', runtime: stopped.runtime });
638
327
  },
639
328
  update_flow: async (args, { client, discoveryState }) => {
640
- const { flowId, body } = args;
641
329
  const guideErrors = checkFlowBuildingGuideRead(discoveryState);
642
- if (guideErrors.length > 0) {
643
- throw new Error(`Playbook State Machine Check Failed:\n- ${guideErrors.join('\n- ')}`);
644
- }
645
- let existingFlow = null;
646
- try {
647
- const flowRes = await client.get(`/flows/${flowId}`);
648
- existingFlow = deserializeJsonApi(flowRes.data);
649
- }
650
- catch (err) {
651
- logger.warn(`Failed to fetch existing flow for validation: ${err instanceof Error ? err.message : String(err)}`);
652
- }
653
- const lifecycleErrors = checkFlowLifecycle(flowId, discoveryState, existingFlow, 'update_flow');
654
- if (lifecycleErrors.length > 0) {
655
- throw new Error(`Flow Lifecycle Check Failed:\n- ${lifecycleErrors.join('\n- ')}`);
656
- }
657
- const { errors: valErrors, warnings: valWarnings } = validateFlowGraph(body);
658
- if (valErrors.length > 0) {
659
- throw new Error(`Flow Graph Validation Failed:\n- ${valErrors.join('\n- ')}`);
660
- }
661
- const stateErrors = checkDiscoveryState(body, discoveryState, existingFlow);
662
- if (stateErrors.length > 0) {
663
- throw new Error(`Playbook State Machine Check Failed:\n- ${stateErrors.join('\n- ')}`);
664
- }
665
- // Full domino-effect impact analysis before publish
330
+ if (guideErrors.length)
331
+ throw new Error(guideErrors.join('\n'));
666
332
  if (args.confirmed !== true) {
667
- const impact = analyzeFlowImpact(existingFlow, body);
668
- if (impact) {
669
- return {
670
- isError: true,
671
- content: [{
672
- type: 'text',
673
- text: `⚠️ Confirmation required.\n\nAction: Publish (update) flow ${flowId}.\n\n${impact}\n\nTo proceed, call this tool again with the same parameters and set confirmed: true.`
674
- }]
675
- };
676
- }
333
+ const current = await inspectFlow(client, discoveryState, args.flowId);
334
+ requireRevision(current, args.expectedRevision);
335
+ return { ...textResult({ preview: analyzeFlowImpact(current.flow, args.body), revision: current.revision,
336
+ message: 'Publish the saved draft with confirmed: true and this expectedRevision.' }), isError: true };
677
337
  }
678
- const response = await client.patch(`/flows/${flowId}`, body);
679
- const data = deserializeJsonApi(response.data);
680
- await autoConfirmSamples(flowId, client, discoveryState);
681
- // Run health check after update, merging graph validation warnings
682
- const healthCheck = await healthCheckFlow(data, client);
683
- healthCheck.warnings.push(...valWarnings);
684
- const healthSuffix = formatHealthCheck(healthCheck);
685
- const resultText = formatResult(data) + healthSuffix;
686
- return { content: [{ type: 'text', text: resultText }] };
338
+ return textResult(await publishDraft(client, discoveryState, args));
687
339
  },
688
340
  update_flow_draft: async (args, { client, discoveryState }) => {
689
- const { flowId, body } = args;
690
341
  const guideErrors = checkFlowBuildingGuideRead(discoveryState);
691
- if (guideErrors.length > 0) {
692
- throw new Error(`Playbook State Machine Check Failed:\n- ${guideErrors.join('\n- ')}`);
693
- }
694
- let existingFlow = null;
695
- try {
696
- const draftRes = await client.get(`/flows/${flowId}/draft`);
697
- existingFlow = deserializeJsonApi(draftRes.data);
698
- }
699
- catch (err) {
700
- try {
701
- const flowRes = await client.get(`/flows/${flowId}`);
702
- existingFlow = deserializeJsonApi(flowRes.data);
703
- }
704
- catch (e) {
705
- logger.warn(`Failed to fetch existing flow/draft for validation: ${e instanceof Error ? e.message : String(e)}`);
706
- }
707
- }
708
- const lifecycleErrors = checkFlowLifecycle(flowId, discoveryState, existingFlow, 'update_flow_draft');
709
- if (lifecycleErrors.length > 0) {
710
- throw new Error(`Flow Lifecycle Check Failed:\n- ${lifecycleErrors.join('\n- ')}`);
711
- }
712
- const { errors: valErrors, warnings: valWarnings } = validateFlowGraph(body);
713
- if (valErrors.length > 0) {
714
- throw new Error(`Flow Graph Validation Failed:\n- ${valErrors.join('\n- ')}`);
715
- }
716
- const stateErrors = checkDiscoveryState(body, discoveryState, existingFlow);
717
- if (stateErrors.length > 0) {
718
- throw new Error(`Playbook State Machine Check Failed:\n- ${stateErrors.join('\n- ')}`);
719
- }
720
- // Preserve selected_data_samples from existing draft nodes when not explicitly provided
721
- const prevNodes = existingFlow?.graph?.nodes || [];
722
- const updatedNodes = body?.data?.attributes?.graph?.nodes || [];
723
- for (const node of updatedNodes) {
724
- if (!node.selected_data_samples) {
725
- const prev = prevNodes.find((n) => n.id === node.id);
726
- if (prev?.selected_data_samples?.length > 0) {
727
- node.selected_data_samples = prev.selected_data_samples;
728
- }
729
- }
730
- }
731
- // Full domino-effect impact analysis
342
+ if (guideErrors.length)
343
+ throw new Error(guideErrors.join('\n'));
732
344
  if (args.confirmed !== true) {
733
- const impact = analyzeFlowImpact(existingFlow, body, updatedNodes);
734
- if (impact) {
735
- return {
736
- isError: true,
737
- content: [{
738
- type: 'text',
739
- text: `⚠️ Confirmation required.\n\nAction: Update the draft of flow ${flowId}.\n\n${impact}\n\nTo proceed, call this tool again with the same parameters and set confirmed: true.`
740
- }]
741
- };
742
- }
345
+ const current = await inspectFlow(client, discoveryState, args.flowId);
346
+ requireRevision(current, args.expectedRevision);
347
+ return { ...textResult({ preview: analyzeFlowImpact(current.draft || current.flow, args.body), revision: current.revision,
348
+ message: 'Save the draft with confirmed: true and this expectedRevision.' }), isError: true };
349
+ }
350
+ const saved = await saveDraft(client, discoveryState, args);
351
+ // Impact analysis: compare what was submitted against what existed before.
352
+ // saveDraft already reconciled the state, so we can compute impact from the
353
+ // submitted body alone (analyzeFlowImpact only needs the existing graph + body).
354
+ const result = { ...saved };
355
+ // Auto-resample steps whose samples were invalidated by the draft change.
356
+ // Steps with precondition blockers (credentials, metadata) are skipped.
357
+ const resample = await resampleStaleSteps(client, discoveryState, args.flowId);
358
+ if (resample.resampled.length || resample.failed.length) {
359
+ result.resample = resample;
360
+ result.build = await getFlowBuildStatus(discoveryState, args.flowId);
743
361
  }
744
- const response = await client.put(`/flows/${flowId}/draft`, body);
745
- const data = deserializeJsonApi(response.data);
746
- // Auto-create data samples for steps that don't have them.
747
- // Auto-create data samples for steps that don't have them (draft is always source of truth).
748
- await autoConfirmSamples(flowId, client, discoveryState);
749
- // Detect steps with changed configuration or mappings to remind about data sample updates
750
- const changedSteps = [];
751
- const existingNodes = existingFlow?.graph?.nodes || [];
752
- const existingEdges = existingFlow?.graph?.edges || [];
753
- const newNodes = body?.data?.attributes?.graph?.nodes || [];
754
- const newEdges = body?.data?.attributes?.graph?.edges || [];
755
- const existingNodeMap = new Map(existingNodes.map((n) => [n.id, n]));
756
- const existingEdgeMap = new Map(existingEdges.map((e) => [`${e.source}:${e.target}`, e]));
757
- for (let i = 0; i < newNodes.length; i++) {
758
- const node = newNodes[i];
759
- const prev = existingNodeMap.get(node.id);
760
- if (!prev || JSON.stringify(prev.fields) !== JSON.stringify(node.fields)) {
761
- changedSteps.push(node.name || node.id);
762
- }
763
- }
764
- for (let i = 0; i < newEdges.length; i++) {
765
- const edge = newEdges[i];
766
- const key = `${edge.source}:${edge.target}`;
767
- const prev = existingEdgeMap.get(key);
768
- if (edge.config?.mapper && (!prev?.config?.mapper || JSON.stringify(prev.config.mapper) !== JSON.stringify(edge.config.mapper))) {
769
- const targetNode = newNodes.find((n) => n.id === edge.target);
770
- const stepName = targetNode?.name || edge.target;
771
- if (!changedSteps.includes(stepName)) {
772
- changedSteps.push(stepName);
773
- }
774
- }
775
- }
776
- let resultText = formatResult(data);
777
- if (valWarnings.length > 0) {
778
- resultText += '\n\n--- Graph Validation Warnings ---\n' + valWarnings.map(w => ` - ${w}`).join('\n');
779
- }
780
- if (changedSteps.length > 0) {
781
- resultText += '\n\n--- Data Sample Reminder ---\n' +
782
- `The following steps had configuration or mapping changes: ${changedSteps.join(', ')}.\n` +
783
- 'Ask the user if they want to update/regenerate data samples for these steps (using debug_flow_step or create_data_sample).';
784
- }
785
- return { content: [{ type: 'text', text: resultText }] };
362
+ return textResult(result);
786
363
  },
787
364
  debug_flow_step: async (args, { client, discoveryState }) => {
788
- const { workspaceId, command, credentialsId, secretId, fields, sampleData, attachments, stepId: customStepId, flowId, componentId } = args;
789
- const stepId = customStepId || 'step_1';
790
- const node = {
791
- id: stepId,
792
- command,
793
- error: false
794
- };
795
- if (fields && Object.keys(fields).length > 0) {
796
- node.fields = fields;
797
- }
798
- if (credentialsId) {
365
+ const { workspaceId, command, credentialsId, secretId, fields = {}, sampleData, attachments, stepId = 'step_1', flowId, componentId } = args;
366
+ let current;
367
+ if (flowId) {
368
+ current = await inspectFlow(client, discoveryState, flowId);
369
+ requireRevision(current, args.expectedRevision);
370
+ requireInactive(current);
371
+ const node = current.draft?.graph?.nodes?.find((n) => n.id === stepId);
372
+ if (!node || !componentId)
373
+ throw new Error('To attach a debug sample, select an existing draft step and its componentId.');
374
+ const expected = { command: node.command, component_id: node.component_id, fields: node.fields || {},
375
+ credentials_id: node.credentials_id, secret_id: node.secret_id };
376
+ const supplied = { command, component_id: componentId, fields, credentials_id: credentialsId, secret_id: secretId };
377
+ if (current.workspaceId !== workspaceId || fingerprint(expected) !== fingerprint(supplied)) {
378
+ throw new Error('Debug configuration does not match the current draft step.');
379
+ }
380
+ if (current.draft.graph.edges.some((e) => e.target === stepId) && sampleData === undefined) {
381
+ throw new Error('Provide sampleData from the upstream mapper when debugging an action.');
382
+ }
383
+ const inputErrors = schemaErrors(schemaFor(discoveryState, workspaceId, node)?.in, sampleData || {});
384
+ if (inputErrors.length)
385
+ throw new Error(`Invalid debug input: ${inputErrors.join('; ')}`);
386
+ }
387
+ const node = { id: stepId, command, fields, error: false };
388
+ if (credentialsId)
799
389
  node.credentials_id = credentialsId;
800
- }
801
- if (secretId) {
390
+ if (secretId)
802
391
  node.secret_id = secretId;
803
- }
804
- const inputData = sampleData || {};
805
- const msgPayload = { body: inputData };
806
- if (attachments && Object.keys(attachments).length > 0) {
807
- msgPayload.attachments = attachments;
808
- }
809
- const payload = {
810
- data: {
811
- type: 'flow',
812
- attributes: {
813
- name: `Debug ${command}`,
814
- type: 'debug',
815
- graph: {
816
- nodes: [node],
817
- edges: []
818
- },
819
- variables: {}
820
- },
821
- meta: {
822
- sampleData: msgPayload
823
- },
824
- relationships: {
825
- workspace: {
826
- data: {
827
- id: workspaceId,
828
- type: 'workspace'
829
- }
830
- }
831
- }
832
- }
833
- };
834
- const response = await client.post('/flows/debug-sync', payload, {
835
- __retryCount: 3 // skip retries — debug-sync 500s are component failures, not transient
836
- });
392
+ const response = await client.post('/flows/start-reply-debug', { data: {
393
+ type: 'flow', attributes: { name: `Debug ${command}`, type: 'debug', graph: { nodes: [node], edges: [] }, variables: {} },
394
+ meta: { sampleData: { body: sampleData || {}, ...(attachments ? { attachments } : {}) } },
395
+ relationships: { workspace: { data: { id: workspaceId, type: 'workspace' } } },
396
+ } });
837
397
  const result = response.data?.data?.data || response.data?.data || response.data;
838
- const body = result?.body;
839
- // Save as data sample and update flow draft if flowId and componentId provided
840
- if (flowId && componentId && body && typeof body === 'object' && Object.keys(body).length > 0) {
398
+ if (result?.error)
399
+ return { ...textResult(result), isError: true };
400
+ if (current && result?.body !== undefined) {
401
+ const op = operation({ command, component_id: componentId });
402
+ const sampleResponse = await client.post('/data-samples', { data: {
403
+ type: 'data-sample', attributes: { result: { body: result.body }, method: op.method },
404
+ relationships: { component: { data: { type: 'component', id: componentId } },
405
+ component_version: { data: { type: 'component-version', id: op.version } },
406
+ workspace: { data: { type: 'workspace', id: workspaceId } } },
407
+ } });
408
+ const sample = deserializeJsonApi(sampleResponse.data);
409
+ if (!sample?.id)
410
+ throw new Error('Debug succeeded, but creating its sample returned no ID.');
411
+ const graph = structuredClone(current.draft.graph);
412
+ graph.nodes.find((n) => n.id === stepId).selected_data_samples = [sample.id];
841
413
  try {
842
- const commandMatch = command.match(/^[^:]+:([^@]+)@/);
843
- const method = commandMatch?.[1] || '';
844
- let versionId = discoveryState.componentVersions.get(componentId);
845
- if (!versionId) {
846
- const compRes = await client.get(`/components/${componentId}`);
847
- const comp = deserializeJsonApi(compRes.data);
848
- versionId = comp?.latest_version?.id || comp?.latest_version?.data?.id;
849
- if (versionId)
850
- discoveryState.componentVersions.set(componentId, versionId);
851
- }
852
- if (versionId && method) {
853
- const sampleRes = await client.post('/data-samples', {
854
- data: {
855
- type: 'data-sample',
856
- attributes: {
857
- result: { body },
858
- method
859
- },
860
- relationships: {
861
- component: { data: { type: 'component', id: componentId } },
862
- component_version: { data: { type: 'component-version', id: versionId } },
863
- workspace: { data: { type: 'workspace', id: workspaceId } }
864
- }
865
- }
866
- });
867
- const sample = deserializeJsonApi(sampleRes.data);
868
- if (sample?.id) {
869
- // Update draft with the new sample (append to existing)
870
- const draftRes = await client.get(`/flows/${flowId}/draft`);
871
- const draft = deserializeJsonApi(draftRes.data);
872
- const draftNodes = draft?.graph?.nodes || [];
873
- const targetNode = draftNodes.find((n) => n.id === stepId);
874
- if (targetNode) {
875
- targetNode.selected_data_samples = [sample.id];
876
- const updateAttrs = {
877
- name: draft.name || 'Untitled',
878
- graph: draft.graph
879
- };
880
- if (draft.cron)
881
- updateAttrs.cron = draft.cron;
882
- if (draft.type)
883
- updateAttrs.type = draft.type;
884
- await client.put(`/flows/${flowId}/draft`, {
885
- data: { id: draft.id, type: 'flow-draft', attributes: updateAttrs }
886
- });
887
- logger.info(`[debug_flow_step] Saved sample ${sample.id} for node ${stepId} in flow ${flowId}`);
888
- }
889
- }
890
- }
414
+ await saveDraft(client, discoveryState, { flowId, expectedRevision: current.revision,
415
+ body: { data: { type: 'flow-draft', attributes: { graph } } } });
416
+ const saved = await inspectFlow(client, discoveryState, flowId);
417
+ validateSample(discoveryState, flowId, saved, stepId, sample);
418
+ return textResult({ result, sampleId: sample.id, build: await getFlowBuildStatus(discoveryState, flowId) });
891
419
  }
892
- catch (err) {
893
- logger.warn(`[debug_flow_step] Failed to save sample for node ${stepId}: ${err instanceof Error ? err.message : String(err)}`);
420
+ catch (error) {
421
+ throw new Error(`Debug sample ${sample.id} was created, but attachment/validation failed: ${error.message}. Inspect before retrying the debug action.`);
894
422
  }
895
423
  }
896
- return textResult(result);
424
+ return textResult({ result, sampleSaved: false });
897
425
  }
898
426
  };
899
427
  //# sourceMappingURL=flow.js.map