@ema.co/mcp-toolkit 1.5.1 → 1.6.0

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.

@@ -0,0 +1,11 @@
1
+ /**
2
+ * Auto-generated TypeScript types from Ema Platform OpenAPI spec.
3
+ * Generated from: Ema Platform API v1.0.0
4
+ * Generated at: 2026-01-16T13:46:50.729Z
5
+ *
6
+ * DO NOT EDIT MANUALLY - regenerate with: npm run generate:types
7
+ */
8
+ export {};
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ // API Operation Types
11
+ // ─────────────────────────────────────────────────────────────────────────────
package/dist/sdk/index.js CHANGED
@@ -33,8 +33,14 @@ export {
33
33
  compileWorkflow,
34
34
  // Settings builders
35
35
  buildVoiceConfig, buildChatConfig, } from "./workflow-generator.js";
36
+ // Action Registry (API-driven action/template definitions)
37
+ export { ActionRegistry, ensureActionRegistry, parseActionDefinition, parseTemplateDefinition, } from "./action-registry.js";
36
38
  // Workflow Intent (Normalization layer)
37
- export { parseInput, validateIntent, intentToSpec, detectInputType, parseNaturalLanguage, parsePartialSpec, } from "./workflow-intent.js";
39
+ export { parseInput, validateIntent, intentToSpec, detectInputType, parseNaturalLanguage, parsePartialSpec,
40
+ // LLM-driven generation
41
+ needsLLMGeneration, generateWorkflow, parseWorkflowSpecFromLLM, } from "./workflow-intent.js";
42
+ // Workflow Validator (API-driven validation)
43
+ export { APISchemaRegistry, ensureSchemaRegistry, getSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM, generateTemplateCatalogForLLM, } from "./workflow-validator.js";
38
44
  // Generation Schema (Compact format for LLM-based generation)
39
45
  export { generateSchema, generateSchemaMarkdown, buildCompactAgents, buildTypeRules, buildConstraints, getAgentSchema, isTypeCompatible, getRecommendedInput, } from "./generation-schema.js";
40
46
  // Version Tracking (Persona version history management)
@@ -49,3 +55,11 @@ export { VersionPolicyEngine, createVersionPolicyEngine, } from "./version-polic
49
55
  export { analyzeExecutionFlow, detectLoops, detectMultipleResponders, detectRedundantClassifiers, analyzeDataFlow, findDeadCodePaths, generateASCIIFlow, } from "./workflow-execution-analyzer.js";
50
56
  // Workflow Fixer (Auto-fix including multiple responder issues)
51
57
  export { autoFixWorkflow, suggestFixes, } from "./workflow-fixer.js";
58
+ // Workflow Optimizer (Health scoring & optimization recommendations)
59
+ export { analyzeOptimizations, summarizeOptimizationReport, } from "./workflow-optimizer.js";
60
+ // Workflow Tracer (Flow visualization & path analysis)
61
+ export { traceWorkflow, generateDetailedTrace, formatFlowTrace, formatDetailedTrace, } from "./workflow-tracer.js";
62
+ // Quality Gates (Pre-deploy validation)
63
+ export { runQualityGates, canDeploy, getQualityGates, formatQualityReport, } from "./quality-gates.js";
64
+ // Structural Rules (LLM validation context)
65
+ export { STRUCTURAL_RULES_FOR_LLM, STRUCTURAL_INVARIANTS, EXECUTION_RULES, COMMON_STRUCTURAL_MISTAKES, getAllStructuralRules, getInvariantById, getCriticalInvariants, } from "./structural-rules.js";
@@ -1019,6 +1019,7 @@ export const COMMON_MISTAKES = [
1019
1019
  { mistake: "Missing Fallback category", problem: "Workflow validation fails, unhandled intents", solution: "ALWAYS include Fallback category in every categorizer" },
1020
1020
  { mistake: "Type mismatches in connections", problem: "Validation errors, runtime failures", solution: "Check type compatibility: use conversation_to_search_query when needed" },
1021
1021
  { mistake: "Not mapping to WORKFLOW_OUTPUT", problem: "Responses don't reach user", solution: "Ensure ALL paths terminate at WORKFLOW_OUTPUT" },
1022
+ { mistake: "Using workflow_def instead of workflow in updates", problem: "API accepts request but changes silently don't persist", solution: "Use 'workflow' field (not 'workflow_def') in updateAiEmployee(). GET returns workflow_def, UPDATE expects workflow." },
1022
1023
  ];
1023
1024
  export const DEBUG_CHECKLIST = [
1024
1025
  { step: 1, action: "Check Status", description: "Is the AI Employee active/ready?", apiField: "status" },
@@ -2072,17 +2073,34 @@ function detectCategorizerIssues(nodes) {
2072
2073
  reason: `Categorizer "${categorizer.id}" has no outgoing category edges - routing won't work`,
2073
2074
  });
2074
2075
  }
2075
- // Check for Fallback - look for edges or enumType options
2076
+ // Check for Fallback - multiple ways to configure:
2077
+ // 1. default_category input set to a fallback-like value
2078
+ // 2. Outgoing edges with "fallback" in the source_output
2079
+ // 3. Category handlers wired to fallback output
2076
2080
  let hasFallback = false;
2077
- for (const node of nodes) {
2078
- if (node.incoming_edges) {
2079
- for (const edge of node.incoming_edges) {
2080
- if (edge.source_node_id === categorizer.id &&
2081
- (edge.source_output?.toLowerCase().includes("fallback"))) {
2082
- hasFallback = true;
2083
- break;
2081
+ // Check default_category input (most common way to set fallback)
2082
+ const params = categorizer.parameters;
2083
+ if (params?.default_category) {
2084
+ const defaultCat = params.default_category;
2085
+ const defaultValue = defaultCat?.inline?.enumValue?.toLowerCase() || "";
2086
+ if (defaultValue && ["fallback", "other", "general", "unknown", "default"].includes(defaultValue)) {
2087
+ hasFallback = true;
2088
+ }
2089
+ }
2090
+ // Also check for edges with fallback in the name
2091
+ if (!hasFallback) {
2092
+ for (const node of nodes) {
2093
+ if (node.incoming_edges) {
2094
+ for (const edge of node.incoming_edges) {
2095
+ if (edge.source_node_id === categorizer.id &&
2096
+ (edge.source_output?.toLowerCase().includes("fallback"))) {
2097
+ hasFallback = true;
2098
+ break;
2099
+ }
2084
2100
  }
2085
2101
  }
2102
+ if (hasFallback)
2103
+ break;
2086
2104
  }
2087
2105
  }
2088
2106
  if (!hasFallback && outgoing.size > 0) {
@@ -2191,10 +2209,22 @@ function detectWrongInputSource(nodes) {
2191
2209
  const rule = findInputSourceRule(actionName);
2192
2210
  if (!rule)
2193
2211
  continue;
2212
+ // For send_email nodes, the rule only applies to email_to/recipient inputs
2213
+ // Other inputs (subject, body) can legitimately use LLM/text outputs
2214
+ const isEmailNode = actionName.includes("email") || actionName.includes("send_email");
2194
2215
  if (node.incoming_edges) {
2195
2216
  for (const edge of node.incoming_edges) {
2196
2217
  const sourceOutput = edge.source_output?.toLowerCase() ?? "";
2197
2218
  const targetInput = edge.target_input?.toLowerCase() ?? "";
2219
+ // For email nodes, only check the email_to/recipient input, not all inputs
2220
+ if (isEmailNode) {
2221
+ const isRecipientInput = targetInput.includes("email_to") ||
2222
+ targetInput.includes("to_email") ||
2223
+ targetInput.includes("recipient") ||
2224
+ targetInput.includes("to_address");
2225
+ if (!isRecipientInput)
2226
+ continue; // Skip non-recipient inputs for email nodes
2227
+ }
2198
2228
  // Check if using an avoided input
2199
2229
  for (const avoid of rule.avoid) {
2200
2230
  if (sourceOutput.includes(avoid.toLowerCase()) ||
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Quality Gates - Pre-Deploy Validation
3
+ *
4
+ * Ensures workflows meet minimum quality standards before deployment.
5
+ * These are the "guardrails" that prevent broken workflows from going live.
6
+ */
7
+ import { detectWorkflowIssues } from "./knowledge.js";
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // Quality Gate Definitions
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ const QUALITY_GATES = [
12
+ // ═══════════════════════════════════════════════════════════════════════════
13
+ // CRITICAL GATES (Blocking)
14
+ // ═══════════════════════════════════════════════════════════════════════════
15
+ {
16
+ id: "has_trigger",
17
+ name: "Has Entry Point",
18
+ description: "Workflow must have a trigger node as entry point",
19
+ blocking: true,
20
+ severity: "critical",
21
+ check: (workflow) => {
22
+ const actions = (workflow.actions || []);
23
+ const hasTrigger = actions.some(a => {
24
+ const actionName = a.action?.name;
25
+ const name = (actionName?.name || "").toLowerCase();
26
+ return name.includes("trigger");
27
+ });
28
+ return {
29
+ passed: hasTrigger,
30
+ message: hasTrigger
31
+ ? "Workflow has a valid trigger"
32
+ : "Missing trigger node - workflow has no entry point",
33
+ };
34
+ },
35
+ },
36
+ {
37
+ id: "has_output",
38
+ name: "Has Workflow Output",
39
+ description: "Workflow must have at least one WORKFLOW_OUTPUT defined",
40
+ blocking: true,
41
+ severity: "critical",
42
+ check: (workflow) => {
43
+ const results = workflow.results;
44
+ const hasOutput = results && Object.keys(results).length > 0;
45
+ return {
46
+ passed: !!hasOutput,
47
+ message: hasOutput
48
+ ? `Workflow has ${Object.keys(results).length} output(s) defined`
49
+ : "No WORKFLOW_OUTPUT defined - responses won't reach users",
50
+ };
51
+ },
52
+ },
53
+ {
54
+ id: "no_critical_issues",
55
+ name: "No Critical Issues",
56
+ description: "Workflow must not have any critical structural issues",
57
+ blocking: true,
58
+ severity: "critical",
59
+ check: (workflow) => {
60
+ const issues = detectWorkflowIssues(workflow);
61
+ const criticalIssues = issues.filter((i) => i.severity === "critical");
62
+ return {
63
+ passed: criticalIssues.length === 0,
64
+ message: criticalIssues.length === 0
65
+ ? "No critical issues detected"
66
+ : `${criticalIssues.length} critical issue(s) found`,
67
+ details: criticalIssues.map((i) => `${i.type}: ${i.reason}`),
68
+ };
69
+ },
70
+ },
71
+ {
72
+ id: "all_paths_reach_output",
73
+ name: "All Paths Reach Output",
74
+ description: "Every branch from trigger must eventually reach WORKFLOW_OUTPUT",
75
+ blocking: true,
76
+ severity: "critical",
77
+ check: (workflow) => {
78
+ const actions = (workflow.actions || []);
79
+ const results = workflow.results;
80
+ if (!results || Object.keys(results).length === 0) {
81
+ return { passed: false, message: "No outputs to check" };
82
+ }
83
+ // Get all output nodes
84
+ const outputNodes = new Set(Object.values(results).map((r) => r.actionName));
85
+ // Build reverse adjacency (who feeds into whom)
86
+ const feedsInto = new Map();
87
+ for (const action of actions) {
88
+ const name = action.name;
89
+ feedsInto.set(name, new Set());
90
+ const inputs = action.inputs;
91
+ if (inputs) {
92
+ for (const binding of Object.values(inputs)) {
93
+ const bindingObj = binding;
94
+ const actionOutput = bindingObj?.actionOutput;
95
+ if (actionOutput?.actionName) {
96
+ const source = actionOutput.actionName;
97
+ if (!feedsInto.has(source)) {
98
+ feedsInto.set(source, new Set());
99
+ }
100
+ feedsInto.get(source).add(name);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ // Check if each output node is reachable from trigger
106
+ const reachableFromTrigger = new Set();
107
+ const queue = ["trigger"];
108
+ while (queue.length > 0) {
109
+ const current = queue.shift();
110
+ if (reachableFromTrigger.has(current))
111
+ continue;
112
+ reachableFromTrigger.add(current);
113
+ const children = feedsInto.get(current) || new Set();
114
+ for (const child of children) {
115
+ if (!reachableFromTrigger.has(child)) {
116
+ queue.push(child);
117
+ }
118
+ }
119
+ }
120
+ const unreachableOutputs = Array.from(outputNodes).filter(n => !reachableFromTrigger.has(n));
121
+ return {
122
+ passed: unreachableOutputs.length === 0,
123
+ message: unreachableOutputs.length === 0
124
+ ? "All output nodes are reachable from trigger"
125
+ : `${unreachableOutputs.length} output node(s) unreachable from trigger`,
126
+ details: unreachableOutputs.length > 0
127
+ ? [`Unreachable: ${unreachableOutputs.join(", ")}`]
128
+ : undefined,
129
+ };
130
+ },
131
+ },
132
+ // ═══════════════════════════════════════════════════════════════════════════
133
+ // WARNING GATES (Non-blocking but important)
134
+ // ═══════════════════════════════════════════════════════════════════════════
135
+ {
136
+ id: "hitl_for_side_effects",
137
+ name: "HITL for Side Effects",
138
+ description: "Actions with external side effects should have human approval",
139
+ blocking: false,
140
+ severity: "warning",
141
+ check: (workflow) => {
142
+ const actions = (workflow.actions || []);
143
+ const sideEffectNodes = actions.filter(a => {
144
+ const actionName = a.action?.name;
145
+ const name = (actionName?.name || "").toLowerCase();
146
+ return name.includes("email") ||
147
+ name.includes("external") ||
148
+ name.includes("create_") ||
149
+ name.includes("update_") ||
150
+ name.includes("delete_");
151
+ });
152
+ const hitlNodes = new Set(actions
153
+ .filter(a => {
154
+ const actionName = a.action?.name;
155
+ const name = (actionName?.name || "").toLowerCase();
156
+ return name.includes("hitl");
157
+ })
158
+ .map(a => a.name));
159
+ // Check if side effect nodes have HITL upstream
160
+ const unprotected = sideEffectNodes.filter(a => {
161
+ const inputs = a.inputs;
162
+ if (!inputs)
163
+ return true;
164
+ // Check if any input comes from a HITL node
165
+ for (const binding of Object.values(inputs)) {
166
+ const bindingObj = binding;
167
+ const actionOutput = bindingObj?.actionOutput;
168
+ if (actionOutput?.actionName && hitlNodes.has(actionOutput.actionName)) {
169
+ return false;
170
+ }
171
+ }
172
+ return true;
173
+ });
174
+ return {
175
+ passed: unprotected.length === 0,
176
+ message: unprotected.length === 0
177
+ ? "All side-effect actions have HITL protection"
178
+ : `${unprotected.length} side-effect action(s) without HITL approval`,
179
+ details: unprotected.map(a => `${a.name}: Consider adding human approval before this action`),
180
+ };
181
+ },
182
+ },
183
+ {
184
+ id: "categorizer_has_fallback",
185
+ name: "Categorizer Has Fallback",
186
+ description: "Intent categorizers should have a Fallback category",
187
+ blocking: false,
188
+ severity: "warning",
189
+ check: (workflow) => {
190
+ const enumTypes = (workflow.enumTypes || []);
191
+ const missingFallback = enumTypes.filter(e => {
192
+ if (!e.values)
193
+ return false;
194
+ return !e.values.some(v => v.name.toLowerCase() === "fallback");
195
+ });
196
+ return {
197
+ passed: missingFallback.length === 0,
198
+ message: missingFallback.length === 0
199
+ ? "All categorizers have Fallback category"
200
+ : `${missingFallback.length} categorizer(s) missing Fallback`,
201
+ details: missingFallback.map(e => `${e.name}: Add Fallback category for unmatched intents`),
202
+ };
203
+ },
204
+ },
205
+ {
206
+ id: "no_orphan_nodes",
207
+ name: "No Orphan Nodes",
208
+ description: "All nodes should be connected to the workflow",
209
+ blocking: false,
210
+ severity: "warning",
211
+ check: (workflow) => {
212
+ const issues = detectWorkflowIssues(workflow);
213
+ const orphans = issues.filter((i) => i.type === "orphan");
214
+ return {
215
+ passed: orphans.length === 0,
216
+ message: orphans.length === 0
217
+ ? "No orphan nodes detected"
218
+ : `${orphans.length} orphan node(s) found`,
219
+ details: orphans.map((i) => `${i.node}: ${i.reason}`),
220
+ };
221
+ },
222
+ },
223
+ // ═══════════════════════════════════════════════════════════════════════════
224
+ // INFO GATES (Best practices)
225
+ // ═══════════════════════════════════════════════════════════════════════════
226
+ {
227
+ id: "reasonable_complexity",
228
+ name: "Reasonable Complexity",
229
+ description: "Workflow should not be overly complex",
230
+ blocking: false,
231
+ severity: "info",
232
+ check: (workflow) => {
233
+ const actions = (workflow.actions || []);
234
+ const nodeCount = actions.length;
235
+ // Calculate branching factor
236
+ let branches = 0;
237
+ for (const action of actions) {
238
+ const runIf = action.runIf;
239
+ if (runIf)
240
+ branches++;
241
+ }
242
+ const isComplex = nodeCount > 20 || branches > 10;
243
+ return {
244
+ passed: !isComplex,
245
+ message: isComplex
246
+ ? `High complexity: ${nodeCount} nodes, ${branches} conditional branches`
247
+ : `Reasonable complexity: ${nodeCount} nodes, ${branches} branches`,
248
+ details: isComplex
249
+ ? ["Consider breaking into smaller sub-workflows for maintainability"]
250
+ : undefined,
251
+ };
252
+ },
253
+ },
254
+ ];
255
+ // ─────────────────────────────────────────────────────────────────────────────
256
+ // Main Entry Point
257
+ // ─────────────────────────────────────────────────────────────────────────────
258
+ /**
259
+ * Run all quality gates on a workflow
260
+ */
261
+ export function runQualityGates(workflow) {
262
+ const blockingFailures = [];
263
+ const warnings = [];
264
+ const passed = [];
265
+ for (const gate of QUALITY_GATES) {
266
+ const result = gate.check(workflow);
267
+ const report = {
268
+ gate_id: gate.id,
269
+ gate_name: gate.name,
270
+ result,
271
+ severity: gate.severity,
272
+ };
273
+ if (!result.passed) {
274
+ if (gate.blocking) {
275
+ blockingFailures.push(report);
276
+ }
277
+ else {
278
+ warnings.push(report);
279
+ }
280
+ }
281
+ else {
282
+ passed.push(report);
283
+ }
284
+ }
285
+ const overallStatus = blockingFailures.length > 0
286
+ ? "fail"
287
+ : warnings.length > 0
288
+ ? "warning"
289
+ : "pass";
290
+ const summary = generateSummary(blockingFailures, warnings, passed);
291
+ return {
292
+ overall_status: overallStatus,
293
+ blocking_failures: blockingFailures,
294
+ warnings,
295
+ passed,
296
+ summary,
297
+ deploy_allowed: blockingFailures.length === 0,
298
+ };
299
+ }
300
+ /**
301
+ * Quick check if a workflow passes all blocking gates
302
+ */
303
+ export function canDeploy(workflow) {
304
+ for (const gate of QUALITY_GATES) {
305
+ if (gate.blocking) {
306
+ const result = gate.check(workflow);
307
+ if (!result.passed) {
308
+ return false;
309
+ }
310
+ }
311
+ }
312
+ return true;
313
+ }
314
+ /**
315
+ * Get list of all quality gates
316
+ */
317
+ export function getQualityGates() {
318
+ return [...QUALITY_GATES];
319
+ }
320
+ // ─────────────────────────────────────────────────────────────────────────────
321
+ // Helpers
322
+ // ─────────────────────────────────────────────────────────────────────────────
323
+ function generateSummary(failures, warnings, passed) {
324
+ const parts = [];
325
+ if (failures.length > 0) {
326
+ parts.push(`🔴 ${failures.length} BLOCKING failure(s): ${failures.map(f => f.gate_name).join(", ")}`);
327
+ }
328
+ if (warnings.length > 0) {
329
+ parts.push(`🟡 ${warnings.length} warning(s): ${warnings.map(w => w.gate_name).join(", ")}`);
330
+ }
331
+ if (passed.length > 0) {
332
+ parts.push(`🟢 ${passed.length} check(s) passed`);
333
+ }
334
+ if (failures.length === 0) {
335
+ parts.push("✅ Workflow is ready for deployment");
336
+ }
337
+ else {
338
+ parts.push("❌ Deployment blocked - fix critical issues first");
339
+ }
340
+ return parts.join("\n");
341
+ }
342
+ /**
343
+ * Get a human-readable quality report
344
+ */
345
+ export function formatQualityReport(report) {
346
+ const lines = [];
347
+ lines.push("## Quality Gate Report");
348
+ lines.push("");
349
+ lines.push(`**Status**: ${report.overall_status.toUpperCase()}`);
350
+ lines.push(`**Deploy Allowed**: ${report.deploy_allowed ? "Yes ✅" : "No ❌"}`);
351
+ lines.push("");
352
+ if (report.blocking_failures.length > 0) {
353
+ lines.push("### 🔴 Blocking Failures");
354
+ for (const failure of report.blocking_failures) {
355
+ lines.push(`- **${failure.gate_name}**: ${failure.result.message}`);
356
+ if (failure.result.details) {
357
+ for (const detail of failure.result.details) {
358
+ lines.push(` - ${detail}`);
359
+ }
360
+ }
361
+ }
362
+ lines.push("");
363
+ }
364
+ if (report.warnings.length > 0) {
365
+ lines.push("### 🟡 Warnings");
366
+ for (const warning of report.warnings) {
367
+ lines.push(`- **${warning.gate_name}**: ${warning.result.message}`);
368
+ if (warning.result.details) {
369
+ for (const detail of warning.result.details) {
370
+ lines.push(` - ${detail}`);
371
+ }
372
+ }
373
+ }
374
+ lines.push("");
375
+ }
376
+ if (report.passed.length > 0) {
377
+ lines.push("### 🟢 Passed");
378
+ for (const pass of report.passed) {
379
+ lines.push(`- ${pass.gate_name}`);
380
+ }
381
+ }
382
+ lines.push("");
383
+ lines.push("---");
384
+ lines.push(report.summary);
385
+ return lines.join("\n");
386
+ }