@ema.co/mcp-toolkit 2026.2.13 → 2026.2.19

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.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

Files changed (37) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/dist/mcp/domain/loop-detection.js +97 -0
  4. package/dist/mcp/domain/structural-rules.js +4 -5
  5. package/dist/mcp/domain/validation-rules.js +5 -5
  6. package/dist/mcp/domain/workflow-graph.js +3 -5
  7. package/dist/mcp/guidance.js +9 -29
  8. package/dist/mcp/handlers/feedback/index.js +1 -1
  9. package/dist/mcp/handlers/persona/index.js +237 -8
  10. package/dist/mcp/handlers/persona/schema.js +27 -0
  11. package/dist/mcp/handlers/reference/index.js +6 -4
  12. package/dist/mcp/handlers/workflow/index.js +15 -19
  13. package/dist/mcp/handlers/workflow/validation.js +1 -1
  14. package/dist/mcp/knowledge-types.js +7 -0
  15. package/dist/mcp/knowledge.js +61 -800
  16. package/dist/mcp/resources.js +217 -1
  17. package/dist/mcp/server.js +195 -2152
  18. package/dist/mcp/tools.js +2 -3
  19. package/dist/sdk/generated/agent-catalog.js +615 -0
  20. package/dist/sdk/generated/widget-catalog.js +60 -0
  21. package/docs/README.md +17 -9
  22. package/package.json +1 -1
  23. package/.context/public/guides/dashboard-operations.md +0 -349
  24. package/.context/public/guides/email-patterns.md +0 -125
  25. package/.context/public/guides/workflow-builder-patterns.md +0 -708
  26. package/dist/mcp/domain/intent-architect.js +0 -914
  27. package/dist/mcp/domain/quality-gates.js +0 -110
  28. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  29. package/dist/mcp/domain/workflow-intent.js +0 -1806
  30. package/dist/mcp/domain/workflow-merge.js +0 -449
  31. package/dist/mcp/domain/workflow-tracer.js +0 -648
  32. package/dist/mcp/domain/workflow-transformer.js +0 -742
  33. package/dist/mcp/handlers/persona/intent.js +0 -141
  34. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  35. package/dist/mcp/handlers/workflow/compare.js +0 -70
  36. package/dist/mcp/handlers/workflow/generate.js +0 -384
  37. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -1,449 +0,0 @@
1
- /**
2
- * Workflow Merge Module
3
- *
4
- * Provides workflow comparison, merging, and validation utilities for
5
- * brownfield workflow updates. Extracted from server.ts for better
6
- * testability and separation of concerns.
7
- *
8
- * @module sdk/workflow-merge
9
- */
10
- // ─────────────────────────────────────────────────────────────────────────────
11
- // Internal Helpers
12
- // ─────────────────────────────────────────────────────────────────────────────
13
- /**
14
- * Extract a clean enum name from the nested structure.
15
- * Handles: {name: {name: {name: "foo"}}} or {name: {name: "foo"}} or "foo"
16
- */
17
- function extractEnumName(enumNameObj) {
18
- if (typeof enumNameObj === "string")
19
- return enumNameObj;
20
- if (!enumNameObj || typeof enumNameObj !== "object")
21
- return "";
22
- const obj = enumNameObj;
23
- if (obj.name)
24
- return extractEnumName(obj.name);
25
- return "";
26
- }
27
- /**
28
- * Extract node action type from the action structure.
29
- * Handles: {name: {namespaces: [...], name: "type"}} or {name: {name: "type"}}
30
- */
31
- function extractActionType(action) {
32
- if (!action || typeof action !== "object")
33
- return "";
34
- const obj = action;
35
- // Try action.name.name (nested structure)
36
- if (obj.name && typeof obj.name === "object") {
37
- const nameObj = obj.name;
38
- if (typeof nameObj.name === "string")
39
- return nameObj.name;
40
- }
41
- // Try direct name
42
- if (typeof obj.name === "string")
43
- return obj.name;
44
- return "";
45
- }
46
- /**
47
- * Parse a workflow into normalized node structures.
48
- * @internal Used by compareWorkflows and mergeWorkflows
49
- */
50
- function parseWorkflowNodes(workflow) {
51
- const nodes = new Map();
52
- if (!workflow)
53
- return nodes;
54
- const actions = workflow.actions;
55
- if (!actions)
56
- return nodes;
57
- for (const action of actions) {
58
- const name = String(action.name ?? "");
59
- if (!name)
60
- continue;
61
- nodes.set(name, {
62
- name,
63
- actionType: extractActionType(action.action ?? action.actionType),
64
- inputs: action.inputs ?? {},
65
- runIf: action.runIf,
66
- typeArguments: action.typeArguments,
67
- });
68
- }
69
- return nodes;
70
- }
71
- /**
72
- * Parse a workflow's enum types into normalized structures.
73
- * @internal Used by compareWorkflows and mergeWorkflows
74
- */
75
- function parseWorkflowEnums(workflow) {
76
- const enums = new Map();
77
- if (!workflow)
78
- return enums;
79
- const enumTypes = workflow.enumTypes;
80
- if (!enumTypes)
81
- return enums;
82
- for (const enumType of enumTypes) {
83
- const fullName = enumType.name;
84
- const name = extractEnumName(fullName);
85
- if (!name)
86
- continue;
87
- const options = enumType.options ?? [];
88
- enums.set(name, {
89
- name,
90
- fullName,
91
- options,
92
- });
93
- }
94
- return enums;
95
- }
96
- // ─────────────────────────────────────────────────────────────────────────────
97
- // Public API
98
- // ─────────────────────────────────────────────────────────────────────────────
99
- /**
100
- * Compare two workflows and produce a diff.
101
- *
102
- * @param existing - The existing workflow to compare against
103
- * @param incoming - The incoming workflow with changes
104
- * @returns A diff object describing all changes between the workflows
105
- */
106
- export function compareWorkflows(existing, incoming) {
107
- // Guard against null/undefined inputs
108
- if (!existing || typeof existing !== "object") {
109
- existing = {};
110
- }
111
- if (!incoming || typeof incoming !== "object") {
112
- incoming = {};
113
- }
114
- const existingNodes = parseWorkflowNodes(existing);
115
- const incomingNodes = parseWorkflowNodes(incoming);
116
- const existingEnums = parseWorkflowEnums(existing);
117
- const incomingEnums = parseWorkflowEnums(incoming);
118
- // Node comparison
119
- const nodesAdded = [];
120
- const nodesRemoved = [];
121
- const nodesModified = [];
122
- const nodesUnchanged = [];
123
- const conflicts = [];
124
- // Check existing nodes
125
- for (const [name, existingNode] of existingNodes) {
126
- const incomingNode = incomingNodes.get(name);
127
- if (!incomingNode) {
128
- nodesRemoved.push(name);
129
- }
130
- else if (existingNode.actionType !== incomingNode.actionType) {
131
- // Type mismatch - this is a conflict
132
- conflicts.push({
133
- type: "node_type_mismatch",
134
- node: name,
135
- message: `Node "${name}" has different types: existing="${existingNode.actionType}", incoming="${incomingNode.actionType}"`,
136
- });
137
- nodesModified.push(name);
138
- }
139
- else if (JSON.stringify(existingNode) !== JSON.stringify(incomingNode)) {
140
- nodesModified.push(name);
141
- }
142
- else {
143
- nodesUnchanged.push(name);
144
- }
145
- }
146
- // Check for new nodes
147
- for (const [name] of incomingNodes) {
148
- if (!existingNodes.has(name)) {
149
- nodesAdded.push(name);
150
- }
151
- }
152
- // Enum comparison
153
- const enumsAdded = [];
154
- const enumsRemoved = [];
155
- const enumsModified = [];
156
- const enumsUnchanged = [];
157
- for (const [name, existingEnum] of existingEnums) {
158
- const incomingEnum = incomingEnums.get(name);
159
- if (!incomingEnum) {
160
- enumsRemoved.push(name);
161
- }
162
- else {
163
- // Compare options
164
- const existingOpts = existingEnum.options.map((o) => o.name).sort().join(",");
165
- const incomingOpts = incomingEnum.options.map((o) => o.name).sort().join(",");
166
- if (existingOpts !== incomingOpts) {
167
- enumsModified.push(name);
168
- }
169
- else {
170
- enumsUnchanged.push(name);
171
- }
172
- }
173
- }
174
- for (const [name] of incomingEnums) {
175
- if (!existingEnums.has(name)) {
176
- enumsAdded.push(name);
177
- }
178
- }
179
- // Results comparison
180
- const existingResults = Object.keys(existing.results ?? {});
181
- const incomingResults = Object.keys(incoming.results ?? {});
182
- const resultsAdded = incomingResults.filter((r) => !existingResults.includes(r));
183
- const resultsRemoved = existingResults.filter((r) => !incomingResults.includes(r));
184
- // Structural analysis
185
- const hasHitlNodes = [...incomingNodes.values()].some((n) => n.actionType.includes("hitl") || n.actionType.includes("general_hitl")) || nodesAdded.some((name) => name.includes("hitl"));
186
- const hasCategorizerChanges = enumsAdded.length > 0 || enumsModified.length > 0;
187
- // Check for broken references (nodes referencing removed nodes)
188
- for (const [name, node] of incomingNodes) {
189
- const inputs = node.inputs;
190
- for (const [inputKey, binding] of Object.entries(inputs)) {
191
- const actionOutput = binding?.actionOutput;
192
- if (actionOutput) {
193
- const refNodeName = String(actionOutput.actionName ?? "");
194
- if (refNodeName && !incomingNodes.has(refNodeName)) {
195
- conflicts.push({
196
- type: "reference_broken",
197
- node: name,
198
- message: `Node "${name}" references non-existent node "${refNodeName}" in input "${inputKey}"`,
199
- });
200
- }
201
- }
202
- }
203
- // Check runIf references
204
- if (node.runIf) {
205
- const lhs = node.runIf.lhs;
206
- const actionOutput = lhs?.actionOutput;
207
- if (actionOutput) {
208
- const refNodeName = String(actionOutput.actionName ?? "");
209
- if (refNodeName && !incomingNodes.has(refNodeName)) {
210
- conflicts.push({
211
- type: "reference_broken",
212
- node: name,
213
- message: `Node "${name}" runIf references non-existent node "${refNodeName}"`,
214
- });
215
- }
216
- }
217
- }
218
- }
219
- return {
220
- nodesAdded,
221
- nodesRemoved,
222
- nodesModified,
223
- nodesUnchanged,
224
- enumsAdded,
225
- enumsRemoved,
226
- enumsModified,
227
- enumsUnchanged,
228
- resultsAdded,
229
- resultsRemoved,
230
- hasHitlNodes,
231
- hasCategorizerChanges,
232
- conflicts,
233
- };
234
- }
235
- /**
236
- * Merge an incoming workflow into an existing one intelligently.
237
- *
238
- * Strategy:
239
- * - Preserve existing workflowName (required by backend)
240
- * - For nodes: add new nodes, update modified nodes, keep unchanged nodes
241
- * - For enums: merge enum options (add new options, keep existing)
242
- * - For results: merge result mappings
243
- * - Detect conflicts and route to Autobuilder if HITL involved
244
- *
245
- * @param existing - The existing workflow
246
- * @param incoming - The incoming workflow with changes
247
- * @param options - Merge options
248
- * @returns The merge result including the merged workflow and any conflicts
249
- */
250
- export function mergeWorkflows(existing, incoming, options = {}) {
251
- // Guard against null/undefined inputs
252
- if (!existing || typeof existing !== "object") {
253
- existing = {};
254
- }
255
- if (!incoming || typeof incoming !== "object") {
256
- incoming = {};
257
- }
258
- const diff = compareWorkflows(existing, incoming);
259
- const warnings = [];
260
- const errors = [];
261
- // Check for fatal conflicts
262
- const fatalConflicts = diff.conflicts.filter((c) => c.type === "node_type_mismatch");
263
- if (fatalConflicts.length > 0 && !options.forceReplace) {
264
- return {
265
- success: false,
266
- mergedWorkflow: existing,
267
- diff,
268
- warnings,
269
- errors: fatalConflicts.map((c) => c.message),
270
- requiresAutobuilder: true,
271
- description: `Cannot merge: ${fatalConflicts.length} node type mismatch(es)`,
272
- };
273
- }
274
- // Start with a deep copy of the incoming workflow
275
- const merged = JSON.parse(JSON.stringify(incoming));
276
- // Preserve existing workflowName (critical for backend validation)
277
- if (existing.workflowName) {
278
- merged.workflowName = JSON.parse(JSON.stringify(existing.workflowName));
279
- }
280
- // Get parsed structures
281
- const existingNodes = parseWorkflowNodes(existing);
282
- const existingEnums = parseWorkflowEnums(existing);
283
- const incomingEnums = parseWorkflowEnums(incoming);
284
- // === ENUM MERGING ===
285
- // Merge enum types: keep existing structure but add new options
286
- const mergedEnumTypes = [];
287
- const processedEnums = new Set();
288
- // Process existing enums first
289
- for (const [name, existingEnum] of existingEnums) {
290
- const incomingEnum = incomingEnums.get(name);
291
- processedEnums.add(name);
292
- if (incomingEnum) {
293
- // Merge options: keep all existing, add any new from incoming
294
- const existingOptNames = new Set(existingEnum.options.map((o) => o.name));
295
- const mergedOptions = [...existingEnum.options];
296
- for (const opt of incomingEnum.options) {
297
- if (!existingOptNames.has(opt.name)) {
298
- mergedOptions.push(opt);
299
- warnings.push(`Added new option "${opt.name}" to enum "${name}"`);
300
- }
301
- }
302
- mergedEnumTypes.push({
303
- name: existingEnum.fullName, // Use existing nested structure
304
- options: mergedOptions,
305
- });
306
- }
307
- else {
308
- // Keep existing enum as-is
309
- mergedEnumTypes.push({
310
- name: existingEnum.fullName,
311
- options: existingEnum.options,
312
- });
313
- warnings.push(`Kept existing enum "${name}" (not in incoming workflow)`);
314
- }
315
- }
316
- // Add new enums from incoming
317
- for (const [name, incomingEnum] of incomingEnums) {
318
- if (!processedEnums.has(name)) {
319
- mergedEnumTypes.push({
320
- name: incomingEnum.fullName,
321
- options: incomingEnum.options,
322
- });
323
- warnings.push(`Added new enum "${name}"`);
324
- }
325
- }
326
- merged.enumTypes = mergedEnumTypes;
327
- // === NODE MERGING ===
328
- // If preserveExistingNodes is true, keep existing nodes that aren't in incoming
329
- if (options.preserveExistingNodes && diff.nodesRemoved.length > 0) {
330
- const existingActions = existing.actions;
331
- const mergedActions = merged.actions;
332
- if (existingActions && mergedActions) {
333
- for (const nodeName of diff.nodesRemoved) {
334
- const existingAction = existingActions.find((a) => String(a.name) === nodeName);
335
- if (existingAction) {
336
- mergedActions.push(JSON.parse(JSON.stringify(existingAction)));
337
- warnings.push(`Preserved existing node "${nodeName}" (not in incoming workflow)`);
338
- }
339
- }
340
- }
341
- }
342
- // === RESULTS MERGING ===
343
- // Merge results: combine both existing and incoming
344
- const existingResults = existing.results ?? {};
345
- const incomingResults = incoming.results ?? {};
346
- const mergedResults = {
347
- ...existingResults,
348
- ...incomingResults, // Incoming takes precedence
349
- };
350
- merged.results = mergedResults;
351
- // Check for broken references after merge
352
- const brokenRefs = diff.conflicts.filter((c) => c.type === "reference_broken");
353
- if (brokenRefs.length > 0) {
354
- errors.push(...brokenRefs.map((c) => c.message));
355
- }
356
- // Generate description
357
- const descParts = [];
358
- if (diff.nodesAdded.length > 0)
359
- descParts.push(`+${diff.nodesAdded.length} nodes`);
360
- if (diff.nodesRemoved.length > 0)
361
- descParts.push(`-${diff.nodesRemoved.length} nodes`);
362
- if (diff.nodesModified.length > 0)
363
- descParts.push(`~${diff.nodesModified.length} modified`);
364
- if (diff.enumsAdded.length > 0)
365
- descParts.push(`+${diff.enumsAdded.length} enums`);
366
- if (diff.enumsModified.length > 0)
367
- descParts.push(`~${diff.enumsModified.length} enums`);
368
- const description = descParts.length > 0
369
- ? `Brownfield merge: ${descParts.join(", ")}`
370
- : "No changes detected";
371
- return {
372
- success: errors.length === 0,
373
- mergedWorkflow: merged,
374
- diff,
375
- warnings,
376
- errors,
377
- requiresAutobuilder: diff.hasHitlNodes,
378
- description,
379
- };
380
- }
381
- /**
382
- * Validate a merged workflow for deployment readiness.
383
- *
384
- * @param merged - The merged workflow to validate
385
- * @returns Validation result with any issues found
386
- */
387
- export function validateMergedWorkflow(merged) {
388
- const issues = [];
389
- // Guard against null/undefined input
390
- if (!merged || typeof merged !== "object") {
391
- return { valid: false, issues: ["Workflow is null or undefined"] };
392
- }
393
- const nodes = parseWorkflowNodes(merged);
394
- const enums = parseWorkflowEnums(merged);
395
- // Check 1: All referenced nodes exist
396
- for (const [name, node] of nodes) {
397
- // Check input references
398
- for (const [inputKey, binding] of Object.entries(node.inputs)) {
399
- const actionOutput = binding?.actionOutput;
400
- if (actionOutput) {
401
- const refNodeName = String(actionOutput.actionName ?? "");
402
- if (refNodeName && !nodes.has(refNodeName) && refNodeName !== "trigger") {
403
- // Special case: "trigger" is always valid
404
- issues.push(`Node "${name}" references non-existent node "${refNodeName}" in input "${inputKey}"`);
405
- }
406
- }
407
- }
408
- // Check runIf references
409
- if (node.runIf) {
410
- const lhs = node.runIf.lhs;
411
- const actionOutput = lhs?.actionOutput;
412
- if (actionOutput) {
413
- const refNodeName = String(actionOutput.actionName ?? "");
414
- if (refNodeName && !nodes.has(refNodeName) && refNodeName !== "trigger") {
415
- issues.push(`Node "${name}" runIf references non-existent node "${refNodeName}"`);
416
- }
417
- }
418
- }
419
- // Check typeArguments enum references
420
- if (node.typeArguments) {
421
- const categories = node.typeArguments.categories;
422
- const enumType = categories?.enumType;
423
- if (enumType) {
424
- const enumName = extractEnumName(enumType.name);
425
- if (enumName && !enums.has(enumName)) {
426
- issues.push(`Node "${name}" references non-existent enum "${enumName}"`);
427
- }
428
- }
429
- }
430
- }
431
- // Check 2: At least one trigger node exists
432
- const hasTrigger = [...nodes.values()].some((n) => n.actionType.includes("trigger") || n.name === "trigger");
433
- if (!hasTrigger && nodes.size > 0) {
434
- issues.push("Workflow has no trigger node");
435
- }
436
- // Check 3: Results reference existing nodes
437
- const results = merged.results;
438
- if (results) {
439
- for (const [resultKey, mapping] of Object.entries(results)) {
440
- if (mapping.actionName && !nodes.has(mapping.actionName)) {
441
- issues.push(`Result "${resultKey}" references non-existent node "${mapping.actionName}"`);
442
- }
443
- }
444
- }
445
- return {
446
- valid: issues.length === 0,
447
- issues,
448
- };
449
- }