@ema.co/mcp-toolkit 2026.1.26 → 2026.1.27-10

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 (45) hide show
  1. package/dist/mcp/handlers/action/index.js +17 -20
  2. package/dist/mcp/handlers/data/index.js +75 -6
  3. package/dist/mcp/handlers/deprecation.js +50 -0
  4. package/dist/mcp/handlers/env/index.js +3 -3
  5. package/dist/mcp/handlers/knowledge/index.js +44 -237
  6. package/dist/mcp/handlers/persona/create.js +63 -18
  7. package/dist/mcp/handlers/persona/index.js +9 -10
  8. package/dist/mcp/handlers/persona/update.js +6 -3
  9. package/dist/mcp/handlers/reference/index.js +15 -2
  10. package/dist/mcp/handlers/sync/index.js +3 -18
  11. package/dist/mcp/handlers/workflow/analyze.js +53 -105
  12. package/dist/mcp/handlers/workflow/deploy.js +160 -1
  13. package/dist/mcp/handlers/workflow/generate.js +8 -28
  14. package/dist/mcp/handlers/workflow/index.js +258 -85
  15. package/dist/mcp/handlers/workflow/modify.js +69 -26
  16. package/dist/mcp/handlers/workflow/optimize.js +22 -108
  17. package/dist/mcp/handlers/workflow/utils.js +0 -102
  18. package/dist/mcp/handlers-consolidated.js +15 -38
  19. package/dist/mcp/prompts.js +82 -44
  20. package/dist/mcp/resources.js +335 -3
  21. package/dist/mcp/server.js +242 -457
  22. package/dist/mcp/tools.js +44 -61
  23. package/dist/sdk/action-schema-parser.js +11 -5
  24. package/dist/sdk/client.js +81 -25
  25. package/dist/sdk/ema-client.js +11 -0
  26. package/dist/sdk/generated/deprecated-actions.js +171 -0
  27. package/dist/sdk/guidance.js +58 -35
  28. package/dist/sdk/index.js +8 -7
  29. package/dist/sdk/knowledge.js +216 -1932
  30. package/dist/sdk/quality-gates.js +60 -336
  31. package/dist/sdk/validation-rules.js +33 -0
  32. package/dist/sdk/workflow-fixer.js +29 -360
  33. package/dist/sdk/workflow-generator.js +2 -1
  34. package/dist/sdk/workflow-intent.js +43 -3
  35. package/dist/sdk/workflow-transformer.js +0 -342
  36. package/docs/dashboard-operations.md +35 -0
  37. package/docs/ema-user-guide.md +66 -0
  38. package/docs/mcp-tools-guide.md +74 -45
  39. package/package.json +2 -2
  40. package/dist/mcp/handlers/persona/analyze.js +0 -275
  41. package/dist/mcp/handlers/persona/compare.js +0 -32
  42. package/dist/mcp/handlers/workflow/compile.js +0 -39
  43. package/docs/DEBUG-ANALYSIS-unused-category-type-mismatch.md +0 -481
  44. package/docs/TODO-fix-analyzer-and-modify.md +0 -182
  45. package/resources/action-schema.json +0 -5678
@@ -1,379 +1,48 @@
1
1
  /**
2
2
  * Workflow Fixer (SDK)
3
3
  *
4
- * Self-contained auto-detection and fixing of workflow issues.
5
- * Call autoFixWorkflow() with just a workflow_def for one-call fixing.
4
+ * DEPRECATED: This module used detectWorkflowIssues which has been removed.
6
5
  *
7
- * Automatically fixes:
8
- * - Orphan node removal
9
- * - Email recipient rewiring to entity_extraction
10
- * - Missing fallback addition
11
- * - Multiple responder detection
12
- * - Ungated responder detection
6
+ * The LLM should analyze workflows using rules from:
7
+ * - ema://rules/anti-patterns
8
+ * - ema://rules/input-sources
9
+ * - ema://rules/optimizations
13
10
  *
14
- * NOTE: This is DIFFERENT from server.ts applyWorkflowFixes():
11
+ * And then make structured modifications via:
12
+ * - workflow(mode="deploy", workflow_def={...})
13
+ * - persona(mode="update", operations=[...])
15
14
  *
16
- * - autoFixWorkflow() (HERE): Self-contained, auto-detects issues, no persona
17
- * context needed. Best for SDK consumers who want autonomous fixing.
18
- *
19
- * - applyWorkflowFixes() (MCP): Takes pre-detected issues AND persona for
20
- * Voice AI specific fixes (results mapping, HITL paths). Used by MCP handlers.
21
- *
22
- * These are COMPLEMENTARY, not duplicates. Use autoFixWorkflow() for SDK usage,
23
- * use applyWorkflowFixes() when you have persona context and pre-analyzed issues.
15
+ * This file is kept for backwards compatibility with minimal functionality.
24
16
  */
25
- import { detectWorkflowIssues } from "./knowledge.js";
26
- import { detectMultipleResponders } from "./workflow-execution-analyzer.js";
27
17
  // ═══════════════════════════════════════════════════════════════════════════
28
- // FIX FUNCTIONS
18
+ // DEPRECATED FUNCTIONS (no-ops)
29
19
  // ═══════════════════════════════════════════════════════════════════════════
30
20
  /**
31
- * Remove orphan nodes (respecting protected nodes used by other fixes)
32
- */
33
- function fixOrphanNodes(workflowDef, issues, protectedNodes) {
34
- const fixes = [];
35
- const orphanIssues = issues.filter(i => i.type === 'orphan');
36
- if (orphanIssues.length === 0)
37
- return { def: workflowDef, fixes };
38
- // Get actions array (Ema format)
39
- const actions = (workflowDef.actions || workflowDef.nodes || []);
40
- const nodesToRemove = new Set();
41
- for (const issue of orphanIssues) {
42
- const nodeName = issue.node;
43
- if (!nodeName)
44
- continue;
45
- if (protectedNodes.has(nodeName)) {
46
- fixes.push({
47
- applied: false,
48
- issueType: 'orphan',
49
- nodeName,
50
- description: `Skipped - node used by another fix`,
51
- });
52
- continue;
53
- }
54
- nodesToRemove.add(nodeName);
55
- fixes.push({
56
- applied: true,
57
- issueType: 'orphan',
58
- nodeName,
59
- description: `Removed orphan node "${nodeName}"`,
60
- change: 'removed',
61
- });
62
- }
63
- const filteredActions = actions.filter((a) => {
64
- const action = a;
65
- return !nodesToRemove.has(action.name || '');
66
- });
67
- const key = workflowDef.actions ? 'actions' : 'nodes';
68
- return {
69
- def: { ...workflowDef, [key]: filteredActions },
70
- fixes,
71
- };
72
- }
73
- /**
74
- * Generate a unique node ID with random suffix
75
- */
76
- function generateNodeId(prefix) {
77
- const suffix = Math.random().toString(36).substring(2, 8);
78
- return `${prefix}_${suffix}`;
79
- }
80
- /**
81
- * Create a convert_to_text action node
82
- * This bridges EXTRACTION_COLUMN → TEXT_WITH_SOURCES
83
- */
84
- function createConvertToTextNode(nodeId, sourceActionName, sourceOutput, displayName) {
85
- return {
86
- name: nodeId,
87
- action: {
88
- name: { namespaces: ['actions', 'emainternal'], name: 'convert_to_text' },
89
- version: 'v0',
90
- },
91
- inputs: {
92
- named_inputs: {
93
- inline: {
94
- array: {
95
- values: [{
96
- named: {
97
- name: 'email_address',
98
- value: {
99
- actionOutput: {
100
- actionName: sourceActionName,
101
- output: sourceOutput,
102
- },
103
- },
104
- description: 'Email address from entity extraction',
105
- },
106
- }],
107
- },
108
- },
109
- },
110
- },
111
- displaySettings: {
112
- displayName,
113
- description: 'Convert extraction output to text for email',
114
- coordinates: { x: 600, y: 300 },
115
- showConfig: 0,
116
- },
117
- typeArguments: {},
118
- tools: [],
119
- disableHumanInteraction: true,
120
- };
121
- }
122
- /**
123
- * Find email column name from entity_extraction node configuration
124
- */
125
- function findEmailColumnName(extractionNode) {
126
- const inputs = extractionNode.inputs;
127
- if (!inputs)
128
- return undefined;
129
- const extractionColumnsInput = inputs.extraction_columns;
130
- const columns = extractionColumnsInput?.inline?.array?.values || [];
131
- for (const col of columns) {
132
- const name = col.wellKnown?.extractionColumn?.name?.toLowerCase() || '';
133
- if (name.includes('email') || name.includes('mail')) {
134
- return col.wellKnown?.extractionColumn?.name;
135
- }
136
- }
137
- return undefined;
138
- }
139
- /**
140
- * Fix wrong email recipient sources by inserting convert_to_text nodes
21
+ * DEPRECATED: Auto-fix workflow issues.
22
+ *
23
+ * This function now returns the workflow unchanged with a deprecation warning.
24
+ * Use LLM analysis with ema://rules/* instead.
141
25
  */
142
- function fixWrongInputSource(workflowDef, issues) {
143
- const fixes = [];
144
- const usedNodes = new Set();
145
- const wrongInputIssues = issues.filter(i => i.type === 'wrong_input_source');
146
- if (wrongInputIssues.length === 0)
147
- return { def: workflowDef, fixes, usedNodes };
148
- const actionsSource = workflowDef.actions || workflowDef.nodes || [];
149
- const actions = (Array.isArray(actionsSource) ? [...actionsSource] : []);
150
- const newNodes = [];
151
- // Find entity extraction nodes
152
- const entityExtractionNodes = actions.filter((a) => {
153
- const action = a;
154
- return action.action?.name?.name === 'entity_extraction' ||
155
- action.action?.name?.name === 'entity_extraction_with_documents';
156
- });
157
- const entityExtractionNodeNames = entityExtractionNodes
158
- .map((a) => a.name)
159
- .filter(Boolean);
160
- if (entityExtractionNodeNames.length === 0) {
161
- fixes.push({
162
- applied: false,
163
- issueType: 'wrong_input_source',
164
- nodeName: 'N/A',
165
- description: `No entity_extraction node found to rewire email to`,
166
- });
167
- return { def: workflowDef, fixes, usedNodes };
168
- }
169
- // Track which convert_to_text nodes we've created (to reuse for same source)
170
- const converterNodes = new Map(); // "source.output" -> converterId
171
- const updatedActions = actions.map((a) => {
172
- const action = a;
173
- const nodeIssue = wrongInputIssues.find(i => i.node === action.name);
174
- if (!nodeIssue)
175
- return a;
176
- const isEmailNode = ['send_email_agent', 'send_communications_handler'].includes(action.action?.name?.name || '');
177
- if (!isEmailNode)
178
- return a;
179
- const inputs = { ...action.inputs };
180
- let anyFixed = false;
181
- for (const [inputName, binding] of Object.entries(inputs)) {
182
- if (!['to_email', 'email_to', 'recipient'].includes(inputName.toLowerCase()))
183
- continue;
184
- const b = binding;
185
- if (!b.actionOutput?.actionName)
186
- continue;
187
- const targetExtractionName = entityExtractionNodeNames[0];
188
- const targetExtractionNode = entityExtractionNodes.find((n) => n.name === targetExtractionName);
189
- // Find the email column name from the extraction node config
190
- const emailColumnName = findEmailColumnName(targetExtractionNode) || 'Email';
191
- // Create or reuse a convert_to_text node
192
- const converterKey = `${targetExtractionName}.${emailColumnName}`;
193
- let converterId = converterNodes.get(converterKey);
194
- if (!converterId) {
195
- converterId = generateNodeId('convert_email');
196
- converterNodes.set(converterKey, converterId);
197
- // Create the new convert_to_text node
198
- const converterNode = createConvertToTextNode(converterId, targetExtractionName, emailColumnName, `Convert ${emailColumnName} to Text`);
199
- newNodes.push(converterNode);
200
- }
201
- // Rewire the email input to use the converter's output
202
- inputs[inputName] = {
203
- actionOutput: {
204
- actionName: converterId,
205
- output: 'text_with_sources',
206
- },
207
- };
208
- usedNodes.add(targetExtractionName);
209
- anyFixed = true;
210
- fixes.push({
211
- applied: true,
212
- issueType: 'wrong_input_source',
213
- nodeName: action.name || '',
214
- description: `Inserted convert_to_text node and rewired ${inputName}`,
215
- change: `${targetExtractionName}.${emailColumnName} → ${converterId} → ${action.name}.${inputName}`,
216
- });
217
- }
218
- if (!anyFixed && Object.keys(inputs).length > 0) {
219
- // Only report if we couldn't find any email inputs to fix
220
- const emailInputs = Object.keys(inputs).filter(k => ['to_email', 'email_to', 'recipient'].includes(k.toLowerCase()));
221
- if (emailInputs.length === 0) {
222
- fixes.push({
223
- applied: false,
224
- issueType: 'wrong_input_source',
225
- nodeName: action.name || '',
226
- description: `No standard email input field found to rewire`,
227
- });
228
- }
229
- }
230
- return { ...action, inputs };
231
- });
232
- // Add the new converter nodes to the workflow
233
- const allActions = [...updatedActions, ...newNodes];
234
- const key = workflowDef.actions ? 'actions' : 'nodes';
26
+ export function autoFixWorkflow(workflowDef) {
235
27
  return {
236
- def: { ...workflowDef, [key]: allActions },
237
- fixes,
238
- usedNodes,
28
+ workflowDef: workflowDef,
29
+ fixesApplied: [],
30
+ success: true,
31
+ warnings: [
32
+ "DEPRECATED: autoFixWorkflow() no longer auto-fixes issues.",
33
+ "Use LLM analysis with ema://rules/anti-patterns instead.",
34
+ ],
239
35
  };
240
36
  }
241
37
  /**
242
- * Add fallback category to categorizers missing one
243
- */
244
- function fixMissingFallback(workflowDef, issues) {
245
- const fixes = [];
246
- const fallbackIssues = issues.filter(i => i.type === 'missing_fallback');
247
- if (fallbackIssues.length === 0)
248
- return { def: workflowDef, fixes };
249
- const actions = (workflowDef.actions || workflowDef.nodes || []);
250
- for (const issue of fallbackIssues) {
251
- const categorizer = actions.find((a) => a.name === issue.node);
252
- if (!categorizer?.action?.typeArguments?.categories)
253
- continue;
254
- const categories = categorizer.action.typeArguments.categories;
255
- if (categories.some(c => c.toLowerCase() === 'fallback' || c.toLowerCase() === 'other')) {
256
- continue;
257
- }
258
- categorizer.action.typeArguments.categories = [...categories, 'Fallback'];
259
- fixes.push({
260
- applied: true,
261
- issueType: 'missing_fallback',
262
- nodeName: issue.node || '',
263
- description: `Added "Fallback" category`,
264
- change: `categories += "Fallback"`,
265
- });
266
- }
267
- return { def: workflowDef, fixes };
268
- }
269
- /**
270
- * NEW: Fix ungated responders by adding runIf conditions
38
+ * DEPRECATED: Suggest fixes for workflow issues.
39
+ *
40
+ * This function now returns empty results with a deprecation warning.
41
+ * Use LLM analysis with ema://rules/* instead.
271
42
  */
272
- function fixUngatedResponders(workflowDef, multiResponderIssues) {
273
- const fixes = [];
274
- const ungatedIssues = multiResponderIssues.filter(i => i.type === 'ungated_responder');
275
- if (ungatedIssues.length === 0)
276
- return { def: workflowDef, fixes };
277
- const actions = (workflowDef.actions || workflowDef.nodes || []);
278
- // Find categorizer to use for gating
279
- const categorizer = actions.find((a) => {
280
- const action = a;
281
- return action.action?.name?.name === 'chat_categorizer';
282
- });
283
- if (!categorizer) {
284
- fixes.push({
285
- applied: false,
286
- issueType: 'ungated_responder',
287
- nodeName: 'N/A',
288
- description: `No categorizer found to add gating condition from`,
289
- });
290
- return { def: workflowDef, fixes };
291
- }
292
- // For now, just report what SHOULD be done
293
- for (const issue of ungatedIssues) {
294
- for (const nodeId of issue.nodes) {
295
- fixes.push({
296
- applied: false, // Manual fix recommended
297
- issueType: 'ungated_responder',
298
- nodeName: nodeId,
299
- description: `Should add runIf condition from "${categorizer.name}"`,
300
- change: `Add: runIf: { enum: { enumType: "category", enumValue: "<specific_category>" } }`,
301
- });
302
- }
303
- }
304
- return { def: workflowDef, fixes };
305
- }
306
- // ═══════════════════════════════════════════════════════════════════════════
307
- // MAIN AUTO-FIX FUNCTION
308
- // ═══════════════════════════════════════════════════════════════════════════
309
- export function autoFixWorkflow(workflowDef) {
310
- const def = workflowDef;
311
- const originalIssues = detectWorkflowIssues(workflowDef);
312
- const multiResponderIssues = detectMultipleResponders(workflowDef);
313
- const originalIssueCount = originalIssues.length + multiResponderIssues.length;
314
- const allFixes = [];
315
- const warnings = [];
316
- let currentDef = { ...def };
317
- // 1. Fix wrong input sources first (protects nodes)
318
- const inputSourceResult = fixWrongInputSource(currentDef, originalIssues);
319
- currentDef = inputSourceResult.def;
320
- allFixes.push(...inputSourceResult.fixes);
321
- const protectedNodes = inputSourceResult.usedNodes;
322
- // 2. Fix missing fallback
323
- const fallbackResult = fixMissingFallback(currentDef, originalIssues);
324
- currentDef = fallbackResult.def;
325
- allFixes.push(...fallbackResult.fixes);
326
- // 3. Fix orphan nodes (respecting protected)
327
- const orphanResult = fixOrphanNodes(currentDef, originalIssues, protectedNodes);
328
- currentDef = orphanResult.def;
329
- allFixes.push(...orphanResult.fixes);
330
- // 4. NEW: Report ungated responder fixes needed
331
- const ungatedResult = fixUngatedResponders(currentDef, multiResponderIssues);
332
- const multipleResponderFixes = ungatedResult.fixes;
333
- // Re-detect remaining issues
334
- const remainingIssues = detectWorkflowIssues(currentDef);
335
- const remainingMultiResponder = detectMultipleResponders(currentDef);
336
- const remainingIssueCount = remainingIssues.length + remainingMultiResponder.length;
337
- const appliedCount = allFixes.filter(f => f.applied).length;
338
- if (multipleResponderFixes.length > 0) {
339
- warnings.push(`${multipleResponderFixes.length} ungated responder(s) need manual gating to prevent duplicate responses`);
340
- }
43
+ export function suggestFixes(_workflowDef) {
341
44
  return {
342
- originalIssueCount,
343
- fixesApplied: allFixes,
344
- remainingIssueCount,
345
- workflowDef: currentDef,
346
- success: appliedCount > 0,
347
- warnings,
348
- multipleResponderFixes,
45
+ standardFixes: [],
46
+ multiResponderFixes: [],
349
47
  };
350
48
  }
351
- /**
352
- * Suggest fixes without applying (dry run)
353
- */
354
- export function suggestFixes(workflowDef) {
355
- const issues = detectWorkflowIssues(workflowDef);
356
- const multiResponderIssues = detectMultipleResponders(workflowDef);
357
- const standardFixes = [];
358
- const multiResponderFixes = [];
359
- for (const issue of issues) {
360
- if (!issue.auto_fixable)
361
- continue;
362
- standardFixes.push({
363
- applied: false,
364
- issueType: issue.type,
365
- nodeName: issue.node || 'N/A',
366
- description: issue.reason || `Fix ${issue.type}`,
367
- });
368
- }
369
- for (const issue of multiResponderIssues) {
370
- multiResponderFixes.push({
371
- applied: false,
372
- issueType: issue.type,
373
- nodeName: issue.nodes.join(', '),
374
- description: issue.description,
375
- change: issue.fixSuggestion,
376
- });
377
- }
378
- return { standardFixes, multiResponderFixes };
379
- }
@@ -9,6 +9,7 @@
9
9
  * 2. Construct nodes based on user requirements
10
10
  * 3. Pass to compileWorkflow() to generate deployment-ready JSON
11
11
  */
12
+ import { PROJECT_TYPES } from "./knowledge.js";
12
13
  /**
13
14
  * FALLBACK VALUES - Used only when ActionRegistry is not available.
14
15
  * These are based on API data as of 2026-01-16.
@@ -329,7 +330,7 @@ function buildInputBinding(binding) {
329
330
  }
330
331
  }
331
332
  function buildProtoConfig(spec) {
332
- const projectType = spec.personaType === "voice" ? 5 : spec.personaType === "chat" ? 4 : 2;
333
+ const projectType = PROJECT_TYPES[spec.personaType] ?? PROJECT_TYPES.dashboard;
333
334
  // Widget format expected by Ema API:
334
335
  // { name: "widgetName", type: <type_id>, widgetName: { ...config... } }
335
336
  // The config is stored under a key matching the widget's name
@@ -445,9 +445,14 @@ const ACTION_CHAIN_PATTERNS = [
445
445
  },
446
446
  ];
447
447
  /**
448
- * Type compatibility rules - what outputs can connect to what inputs
448
+ * Intent routing type compatibility - determines how outputs wire to inputs during intent processing.
449
+ *
450
+ * NOTE: This is SEPARATE from knowledge.ts TYPE_COMPATIBILITY which is user-facing documentation.
451
+ * This focuses on the "can_connect_to" and "use_named_inputs_for" routing logic.
452
+ *
453
+ * Canonical type documentation: src/sdk/knowledge.ts → TYPE_COMPATIBILITY
449
454
  */
450
- export const TYPE_COMPATIBILITY = {
455
+ export const INTENT_TYPE_ROUTING = {
451
456
  WELL_KNOWN_TYPE_CHAT_CONVERSATION: {
452
457
  can_connect_to: ["conversation"],
453
458
  use_named_inputs_for: [],
@@ -1312,6 +1317,37 @@ export function summarizeIntentConfidence(confidence) {
1312
1317
  // ─────────────────────────────────────────────────────────────────────────────
1313
1318
  // Intent to Spec Conversion
1314
1319
  // ─────────────────────────────────────────────────────────────────────────────
1320
+ /**
1321
+ * Generate takeActionInstructions from tools for Voice AI.
1322
+ *
1323
+ * This populates the "Define what actions your AI Employee can perform" section
1324
+ * in the Voice persona config. Format follows the Ema Voice API spec.
1325
+ */
1326
+ function generateTakeActionInstructions(tools) {
1327
+ if (!tools || tools.length === 0) {
1328
+ return "";
1329
+ }
1330
+ const cases = tools.map((tool, index) => {
1331
+ const caseNum = index + 1;
1332
+ const toolName = tool.display_name ?? tool.action ?? `Action ${caseNum}`;
1333
+ const triggerCondition = tool.trigger_condition ?? tool.description ?? "Perform this action when requested";
1334
+ // Extract required parameters from tool schema if available
1335
+ const requiredParams = tool.required_inputs ?? [];
1336
+ const paramsObj = requiredParams.length > 0
1337
+ ? `{ ${requiredParams.map(p => `"${p}": ""`).join(", ")} }`
1338
+ : "{ }";
1339
+ return `</Case ${caseNum}>
1340
+ ${toolName}
1341
+
1342
+ Trigger When: ${triggerCondition}
1343
+
1344
+ Intent for tool call: "User requests ${toolName.toLowerCase()}"
1345
+
1346
+ Required parameters: ${paramsObj}
1347
+ </Case ${caseNum}>`;
1348
+ });
1349
+ return cases.join("\n\n");
1350
+ }
1315
1351
  export function intentToSpec(intent) {
1316
1352
  const nodes = [];
1317
1353
  const resultMappings = [];
@@ -1463,7 +1499,11 @@ export function intentToSpec(intent) {
1463
1499
  welcomeMessage: intent.voice_config?.welcome_message,
1464
1500
  identityAndPurpose: intent.voice_config?.identity ?? intent.description,
1465
1501
  hangupInstructions: intent.voice_config?.hangup_instructions,
1466
- // Additional fields can be extracted from intent description in the future
1502
+ transferInstructions: intent.voice_config?.transfer_instructions,
1503
+ waitMessage: intent.voice_config?.wait_message,
1504
+ // Generate takeActionInstructions from tools if not explicitly provided
1505
+ takeActionInstructions: intent.voice_config?.take_action_instructions ??
1506
+ generateTakeActionInstructions(intent.tools),
1467
1507
  } : undefined;
1468
1508
  return {
1469
1509
  name: intent.name,