@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
@@ -2,8 +2,61 @@
2
2
  * Workflow Deploy Handler
3
3
  *
4
4
  * Deploys a workflow_def to an existing persona.
5
+ * Includes deprecation warnings (does not block, just warns).
6
+ *
7
+ * BLOCKING VALIDATION:
8
+ * - If workflow has knowledge search nodes, data sources MUST be attached and indexed.
5
9
  */
10
+ import { PROJECT_TYPES } from "../../../sdk/knowledge.js";
6
11
  import { sanitizeWorkflowForDeploy } from "./utils.js";
12
+ import { DEPRECATED_ACTIONS_FALLBACK, checkWorkflowDeprecations } from "./index.js";
13
+ /**
14
+ * Extract action type name from various workflow_def formats.
15
+ * Handles both string and object formats:
16
+ * - String: "search/v2"
17
+ * - Object: { name: { name: "search", namespaces: [...] }, version: "v2" }
18
+ */
19
+ function extractActionTypeName(action) {
20
+ if (typeof action === "string") {
21
+ return action;
22
+ }
23
+ if (typeof action === "object" && action !== null) {
24
+ const actionObj = action;
25
+ // Format: { name: { name: "search", namespaces: [...] } }
26
+ if (actionObj.name && typeof actionObj.name === "object") {
27
+ const nameObj = actionObj.name;
28
+ if (typeof nameObj.name === "string") {
29
+ return nameObj.name;
30
+ }
31
+ }
32
+ // Format: { name: "search" } (direct)
33
+ if (typeof actionObj.name === "string") {
34
+ return actionObj.name;
35
+ }
36
+ }
37
+ return "";
38
+ }
39
+ /**
40
+ * Detect knowledge search nodes in a workflow.
41
+ * Returns true if workflow contains search/v2 or search_datastore nodes (NOT web_search).
42
+ */
43
+ function hasKnowledgeSearchNodes(workflowDef) {
44
+ const actions = (workflowDef.actions ?? []);
45
+ return actions.some(a => {
46
+ // Extract action type from various formats
47
+ const actionTypeName = extractActionTypeName(a.action) || a.actionType || "";
48
+ const nodeName = (typeof a.name === "string" ? a.name : "").toLowerCase();
49
+ // Match: search, search/v2, search_datastore
50
+ // Exclude: web_search, live_web_search
51
+ const isSearchNode = actionTypeName.includes("search") ||
52
+ nodeName.includes("search");
53
+ const isWebSearch = actionTypeName.includes("web_search") ||
54
+ nodeName.includes("web_search");
55
+ return isSearchNode && !isWebSearch;
56
+ });
57
+ }
58
+ // Export for use in modify.ts
59
+ export { hasKnowledgeSearchNodes, extractActionTypeName };
7
60
  /**
8
61
  * Handle workflow deploy mode
9
62
  */
@@ -21,16 +74,116 @@ export async function handleWorkflowDeploy(args, client) {
21
74
  if (!persona) {
22
75
  return { error: `Persona not found: ${personaId}` };
23
76
  }
77
+ // ── BLOCKING VALIDATION: Bidirectional search/data source validation ──
78
+ const hasSearchNodes = hasKnowledgeSearchNodes(workflowDef);
79
+ let dataSourceStats = null;
80
+ // Try to get data source stats (used for both validation directions)
81
+ try {
82
+ dataSourceStats = await client.getDataSourceAggregates(personaId);
83
+ }
84
+ catch (statsError) {
85
+ // If we can't check stats, log warning but don't block
86
+ console.error(`[deploy] Could not verify data sources: ${statsError}`);
87
+ }
88
+ // VALIDATION 1: If workflow has search nodes, data sources MUST exist and be indexed
89
+ if (hasSearchNodes && dataSourceStats) {
90
+ if (dataSourceStats.total === 0) {
91
+ return {
92
+ error: "DEPLOYMENT BLOCKED: Workflow contains knowledge search nodes but persona has no data sources attached.",
93
+ mode: "deploy",
94
+ persona_id: personaId,
95
+ persona_name: persona.name,
96
+ validation_failed: "search_nodes_require_data_sources",
97
+ current_state: {
98
+ search_nodes_detected: true,
99
+ data_sources_total: 0,
100
+ data_sources_indexed: 0,
101
+ },
102
+ _fix: [
103
+ "1. Upload documents: persona(id='...',data={method:'upload',path:'/path/to/doc.pdf'})",
104
+ "2. Check status: persona(id='...',data={method:'stats'}) → wait for 'success' > 0",
105
+ "3. Then retry: workflow(mode='deploy', persona_id='...', workflow_def={...})",
106
+ ],
107
+ };
108
+ }
109
+ if (dataSourceStats.success === 0) {
110
+ return {
111
+ error: "DEPLOYMENT BLOCKED: Workflow contains knowledge search nodes but no data sources have been successfully indexed.",
112
+ mode: "deploy",
113
+ persona_id: personaId,
114
+ persona_name: persona.name,
115
+ validation_failed: "search_nodes_require_indexed_data",
116
+ current_state: {
117
+ search_nodes_detected: true,
118
+ data_sources_total: dataSourceStats.total,
119
+ data_sources_pending: dataSourceStats.pending,
120
+ data_sources_success: dataSourceStats.success,
121
+ data_sources_failed: dataSourceStats.failed,
122
+ },
123
+ _fix: dataSourceStats.pending > 0
124
+ ? [
125
+ "Data sources are still indexing. Wait for indexing to complete:",
126
+ "1. Check status: persona(id='...',data={method:'stats'})",
127
+ "2. Retry when 'success' > 0",
128
+ ]
129
+ : [
130
+ "All data sources failed indexing. Check file formats and re-upload:",
131
+ "1. List files: persona(id='...',data={method:'list'})",
132
+ "2. Delete failed: persona(id='...',data={method:'delete',file_id:'...'})",
133
+ "3. Re-upload: persona(id='...',data={method:'upload',path:'...'})",
134
+ ],
135
+ };
136
+ }
137
+ }
138
+ // VALIDATION 2: If persona has indexed data sources but workflow has NO search nodes → BLOCK
139
+ // This catches the case where user uploads documents but deploys a workflow that won't use them
140
+ if (!hasSearchNodes && dataSourceStats && dataSourceStats.success > 0) {
141
+ return {
142
+ error: "DEPLOYMENT BLOCKED: Persona has indexed knowledge files but workflow has no search nodes. The uploaded documents will NEVER be used.",
143
+ mode: "deploy",
144
+ persona_id: personaId,
145
+ persona_name: persona.name,
146
+ validation_failed: "data_sources_require_search_nodes",
147
+ current_state: {
148
+ search_nodes_detected: false,
149
+ data_sources_total: dataSourceStats.total,
150
+ data_sources_indexed: dataSourceStats.success,
151
+ },
152
+ _fix: [
153
+ "Your workflow needs a search node to query the uploaded documents.",
154
+ "1. Get workflow schema: workflow(mode='get', persona_id='...')",
155
+ "2. Add search node: Update workflow_def to include a search/v2 node that queries the knowledge base",
156
+ "3. Wire it up: Connect chat_trigger → summarizer → search → respond_for_external_actions",
157
+ "Example pattern: trigger → conversation_summarizer → search/v2 → respond_for_external_actions → WORKFLOW_OUTPUT",
158
+ ],
159
+ _example_workflow_pattern: {
160
+ nodes: [
161
+ "chat_trigger (trigger)",
162
+ "conversation_summarizer (extract query from conversation)",
163
+ "search/v2 (query knowledge base with datastore_configs)",
164
+ "respond_for_external_actions (generate response with sources)",
165
+ ],
166
+ flow: "trigger → summarizer → search → respond → output",
167
+ },
168
+ };
169
+ }
24
170
  // Sanitize workflow before deployment
25
171
  const sanitizedWorkflow = sanitizeWorkflowForDeploy(workflowDef);
172
+ // Check for deprecated actions (warn, don't block)
173
+ const deprecationWarnings = checkWorkflowDeprecations(workflowDef, DEPRECATED_ACTIONS_FALLBACK);
26
174
  // Determine proto_config to use: provided > existing
27
175
  const existingProtoConfig = persona.proto_config;
28
176
  const providedProtoConfig = args.proto_config;
29
177
  const protoConfigToUse = providedProtoConfig || existingProtoConfig || {};
30
178
  // Check if this is a voice persona and warn if critical settings are missing
31
179
  const projectSettings = protoConfigToUse.projectSettings;
32
- const isVoice = projectSettings?.projectType === 5;
180
+ const isVoice = projectSettings?.projectType === PROJECT_TYPES.voice;
33
181
  const warnings = [];
182
+ // Add deprecation warning message if deprecated actions found
183
+ if (deprecationWarnings.length > 0) {
184
+ warnings.push(`Workflow uses ${deprecationWarnings.length} deprecated action(s). ` +
185
+ "Consider updating to prevent future breakage when deprecated actions are removed.");
186
+ }
34
187
  if (isVoice && !providedProtoConfig) {
35
188
  // Check if existing proto_config has conversationSettings populated
36
189
  const widgets = (protoConfigToUse.widgets ?? []);
@@ -58,6 +211,12 @@ export async function handleWorkflowDeploy(args, client) {
58
211
  if (warnings.length > 0) {
59
212
  result.warnings = warnings;
60
213
  }
214
+ // Include detailed deprecation info if any deprecated actions found
215
+ if (deprecationWarnings.length > 0) {
216
+ result.deprecation_warnings = deprecationWarnings;
217
+ result._warning = "Workflow deployed but contains deprecated actions. Update before platform removes them.";
218
+ result._next_step = "Use workflow(mode='get') to see current schema, then deploy updated workflow_def with non-deprecated actions.";
219
+ }
61
220
  return result;
62
221
  }
63
222
  catch (err) {
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { parseInput, intentToSpec, generateWorkflow } from "../../../sdk/workflow-intent.js";
12
12
  import { compileWorkflow } from "../../../sdk/workflow-generator.js";
13
- import { detectWorkflowIssues } from "../../../sdk/knowledge.js";
13
+ // Detection removed - LLM analyzes with rules from ema://rules/*
14
14
  import { runIntentArchitect } from "../../../sdk/intent-architect.js";
15
15
  import { ensureActionRegistry } from "../../../sdk/action-registry.js";
16
16
  import { ensureSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM } from "../../../sdk/workflow-validator.js";
@@ -142,30 +142,13 @@ async function deployToNewPersona(args, client, compiled, actionRegistry, getTem
142
142
  widgets: Array.from(widgetMap.values()),
143
143
  };
144
144
  // Step 4: Deploy workflow
145
- const existingWorkflow = newPersona.workflow_def;
146
- const existingWfName = existingWorkflow?.workflowName;
147
- const workflowForDeploy = JSON.parse(JSON.stringify(compiled.workflow_def));
148
- const wfName = workflowForDeploy.workflowName;
149
- if (wfName?.name && existingWfName?.name) {
150
- wfName.name.namespaces = existingWfName.name.namespaces;
151
- wfName.name.name = existingWfName.name.name;
152
- }
153
- // Fix results format
154
- const compiledResults = workflowForDeploy.results;
155
- if (compiledResults) {
156
- const newResults = {};
157
- for (const [, value] of Object.entries(compiledResults)) {
158
- if (value.actionName && value.outputName) {
159
- const key = `${value.actionName}.${value.outputName}`;
160
- newResults[key] = value;
161
- }
162
- }
163
- workflowForDeploy.results = newResults;
164
- }
145
+ // NOTE: The SDK's updateAiEmployee() handles workflowName namespace automatically.
146
+ // It will copy from existing workflow if present, or generate a valid namespace if not.
147
+ // It also fixes the results format. No need to manually manipulate these here.
165
148
  try {
166
149
  await client.updateAiEmployee({
167
150
  persona_id: newPersonaId,
168
- workflow: workflowForDeploy,
151
+ workflow: compiled.workflow_def,
169
152
  proto_config: mergedProtoConfig,
170
153
  }, { verbose: true });
171
154
  return { success: true, personaId: newPersonaId, personaName };
@@ -182,7 +165,7 @@ async function deployToNewPersona(args, client, compiled, actionRegistry, getTem
182
165
  personaId: newPersonaId,
183
166
  personaName,
184
167
  workflowDeployError: errMsg,
185
- workflowAttempted: workflowForDeploy,
168
+ workflowAttempted: compiled.workflow_def,
186
169
  };
187
170
  }
188
171
  }
@@ -288,14 +271,14 @@ export async function handleWorkflowGenerate(args, client, getTemplateId) {
288
271
  }
289
272
  }
290
273
  const compiled = compileWorkflow(spec, { registry: actionRegistry });
291
- // Validate generated workflow
292
- const issues = detectWorkflowIssues(compiled.workflow_def);
293
274
  const result = {
294
275
  mode: "generate",
295
276
  status: preview ? "preview" : "deployed",
296
277
  workflow_def: compiled.workflow_def,
297
278
  proto_config: compiled.proto_config,
298
279
  validation: parseResult.validation,
280
+ // LLM should analyze with ema://rules/anti-patterns
281
+ _analysis_hint: "Use ema://rules/anti-patterns to check for issues",
299
282
  };
300
283
  if (specValidation) {
301
284
  result.api_validation = {
@@ -304,9 +287,6 @@ export async function handleWorkflowGenerate(args, client, getTemplateId) {
304
287
  action_coverage: specValidation.action_coverage,
305
288
  };
306
289
  }
307
- if (issues.length > 0) {
308
- result.issues = issues;
309
- }
310
290
  // Deploy if not preview
311
291
  if (!preview && personaId) {
312
292
  const persona = await client.getPersonaById(personaId);
@@ -1,43 +1,228 @@
1
1
  /**
2
- * Workflow Handler - Unified greenfield/brownfield operations
2
+ * Workflow Handler
3
3
  *
4
- * Modes:
5
- * - generate: Create new workflow from input
6
- * - modify/extend: Transform existing workflow
7
- * - analyze: Return current state + issues
8
- * - deploy: Deploy workflow to persona
9
- * - compare: Compare two workflow versions
10
- * - compile: Compile WorkflowSpec to workflow_def
11
- * - optimize: Auto-fix issues
4
+ * PUBLIC TOOL MODES (workflow tool):
5
+ * - get: Return workflow data + schema for LLM to generate/modify
6
+ * - deploy: Deploy LLM-generated workflow_def
12
7
  *
13
- * Note: During extraction, individual mode handlers are being moved here.
14
- * Until complete, delegates to handlers-consolidated.ts.
8
+ * INTERNAL MODES (called from persona tool, not exposed):
9
+ * - modify: Used internally by persona(method="update", input="...")
10
+ * - generate: Used internally for workflow generation
11
+ * - analyze: Used internally for workflow analysis
12
+ * - optimize: Used internally for auto-fix
13
+ * - compare: Used internally for comparison
14
+ *
15
+ * THE LLM DOES ALL THE THINKING. MCP provides data and executes.
15
16
  */
17
+ import { generateSchema } from "../../../sdk/generation-schema.js";
16
18
  // Re-export types
17
19
  export * from "./types.js";
18
- // Re-export utilities
20
+ // Re-export utilities
19
21
  export * from "./utils.js";
20
- // Import mode handlers
22
+ // Import internal handlers (not exposed as tool modes, but used internally)
21
23
  import { handleWorkflowAnalyze } from "./analyze.js";
22
24
  import { handleWorkflowCompare } from "./compare.js";
23
- import { handleWorkflowCompile } from "./compile.js";
24
25
  import { handleWorkflowOptimize } from "./optimize.js";
25
26
  import { handleWorkflowDeploy } from "./deploy.js";
26
27
  import { handleWorkflowGenerate } from "./generate.js";
27
28
  import { handleWorkflowModify } from "./modify.js";
28
- // Re-export mode handlers
29
+ // Re-export for internal use (persona handler routes to these)
29
30
  export { handleWorkflowAnalyze } from "./analyze.js";
30
31
  export { handleWorkflowCompare } from "./compare.js";
31
- export { handleWorkflowCompile } from "./compile.js";
32
32
  export { handleWorkflowOptimize } from "./optimize.js";
33
33
  export { handleWorkflowDeploy } from "./deploy.js";
34
34
  export { handleWorkflowGenerate } from "./generate.js";
35
35
  export { handleWorkflowModify } from "./modify.js";
36
36
  export { applyWorkflowModifications, buildWorkflowAction, buildInputBinding, } from "./modify.js";
37
+ // Fallback deprecated actions - ONLY used when API unavailable
38
+ // Source: ema/ema_backend/grpc/workflow_actions/registered_actions.py (synced 2026-01-26)
39
+ // Exported for use by deploy handler
40
+ export const DEPRECATED_ACTIONS_FALLBACK = {
41
+ "search/v0": { replacement: "search/v2", notes: "v2 requires datastore_configs input" },
42
+ "web_search/v0": { replacement: "live_web_search or ai_web_search", notes: "Split into two specialized actions" },
43
+ "call_llm/v0": { replacement: "call_llm/v2" },
44
+ "human_collaboration/v0": { replacement: "general_hitl" },
45
+ "fixed_response/v0": { replacement: "fixed_response/v1" },
46
+ "custom_agent/v0": { replacement: "custom_agent/v1" },
47
+ "json_mapper/v0": { replacement: "json_mapper/v1" },
48
+ "text_categorizer/v0": { replacement: "text_categorizer/v1" },
49
+ "respond_with_sources/v0": { replacement: "respond_for_external_actions", notes: "Handles tool results better" },
50
+ };
51
+ /**
52
+ * Get deprecated actions from API (with fallback)
53
+ * Returns map of action/version -> replacement info
54
+ */
55
+ async function getDeprecatedActions(client) {
56
+ try {
57
+ const actions = await client.listActions();
58
+ const deprecated = {};
59
+ for (const action of actions) {
60
+ if (action.deprecated) {
61
+ // Use action name as key (API doesn't include version in deprecated flag)
62
+ deprecated[action.id] = {
63
+ notes: "Deprecated - check catalog for current version",
64
+ };
65
+ }
66
+ }
67
+ // Merge with fallback to get replacement info
68
+ for (const [key, info] of Object.entries(DEPRECATED_ACTIONS_FALLBACK)) {
69
+ const actionName = key.split("/")[0];
70
+ if (deprecated[actionName] || !deprecated[key]) {
71
+ deprecated[key] = { ...info, ...deprecated[key] };
72
+ }
73
+ }
74
+ return { deprecated, source: "api" };
75
+ }
76
+ catch {
77
+ // API unavailable, use fallback
78
+ return { deprecated: DEPRECATED_ACTIONS_FALLBACK, source: "fallback" };
79
+ }
80
+ }
81
+ /**
82
+ * Check if workflow uses deprecated actions
83
+ * Exported for use by deploy handler
84
+ */
85
+ export function checkWorkflowDeprecations(workflowDef, deprecatedActions) {
86
+ if (!workflowDef)
87
+ return [];
88
+ const warnings = [];
89
+ const actions = (workflowDef.actions ?? []);
90
+ for (const action of actions) {
91
+ const actionName = action.action?.name?.name;
92
+ const version = action.action?.name?.version || "v0";
93
+ if (!actionName)
94
+ continue;
95
+ const key = `${actionName}/${version}`;
96
+ const deprecation = deprecatedActions[key] || deprecatedActions[actionName];
97
+ if (deprecation) {
98
+ warnings.push({
99
+ action: action.name,
100
+ version,
101
+ replacement: deprecation.replacement,
102
+ notes: deprecation.notes,
103
+ severity: "warning",
104
+ });
105
+ }
106
+ }
107
+ return warnings;
108
+ }
109
+ /**
110
+ * Handle workflow(mode="get") - return data + schema + guidance for LLM
111
+ *
112
+ * Returns:
113
+ * - Current workflow_def (if exists)
114
+ * - Deprecation warnings for current workflow
115
+ * - Generation schema (agents, types, constraints)
116
+ * - Requirements and guidance (NOT hardcoded templates)
117
+ * - Available widgets
118
+ *
119
+ * LLM uses this to generate or modify workflow_def
120
+ */
121
+ async function handleWorkflowGet(args, client) {
122
+ const personaId = args.persona_id;
123
+ if (!personaId) {
124
+ return { error: "persona_id required" };
125
+ }
126
+ const persona = await client.getPersonaById(personaId);
127
+ if (!persona) {
128
+ return { error: `Persona not found: ${personaId}` };
129
+ }
130
+ const protoConfig = persona.proto_config;
131
+ const widgets = (protoConfig?.widgets ?? []);
132
+ const workflowDef = persona.workflow_def;
133
+ // Extract available widget names for LLM
134
+ const availableWidgets = widgets
135
+ .filter(w => typeof w.name === "string")
136
+ .map(w => ({
137
+ name: w.name,
138
+ type: w.type,
139
+ }));
140
+ // Get generation schema for LLM
141
+ const schema = generateSchema();
142
+ // Get deprecated actions (API-first, with fallback)
143
+ const { deprecated: deprecatedActions, source: deprecationSource } = await getDeprecatedActions(client);
144
+ // Check if current workflow uses deprecated actions
145
+ const deprecationWarnings = checkWorkflowDeprecations(workflowDef, deprecatedActions);
146
+ // Build response
147
+ const result = {
148
+ persona_id: persona.id,
149
+ persona_name: persona.name,
150
+ persona_type: persona.type,
151
+ workflow_def: workflowDef ?? null,
152
+ available_widgets: availableWidgets,
153
+ // Deprecation warnings (severity: warning)
154
+ deprecation_warnings: deprecationWarnings.length > 0 ? deprecationWarnings : undefined,
155
+ deprecated_actions_source: deprecationWarnings.length > 0 ? deprecationSource : undefined,
156
+ // Schema for LLM to generate valid workflow_def
157
+ generation_schema: {
158
+ agents: schema.agents,
159
+ constraints: schema.constraints,
160
+ input_rules: schema.inputRules,
161
+ },
162
+ // HARD REQUIREMENTS (must be satisfied)
163
+ hard_requirements: {
164
+ workflow_output: {
165
+ rule: "Every workflow MUST have results.WORKFLOW_OUTPUT mapped to a final action output",
166
+ format: '{ "results": { "WORKFLOW_OUTPUT": { "actionName": "...", "outputName": "..." } } }',
167
+ failure: "Persona cannot be activated without this",
168
+ },
169
+ workflow_name: {
170
+ rule: "workflowName must be ['ema', 'personas', '<persona_id>']",
171
+ format: '{ "workflowName": ["ema", "personas", "actual-persona-id"] }',
172
+ },
173
+ action_structure: {
174
+ rule: "Each action needs: name (unique ID), action.name (namespaces, name, version), action.inputs",
175
+ namespaces: "Most actions use ['actions', 'emainternal'], triggers use ['triggers', 'emainternal']",
176
+ },
177
+ no_deprecated_actions: deprecationWarnings.length > 0 ? {
178
+ rule: "Update deprecated actions before deploying",
179
+ warnings: deprecationWarnings,
180
+ } : undefined,
181
+ },
182
+ // GUIDANCE (best practices, not hard requirements)
183
+ guidance: {
184
+ flow_pattern: "trigger → categorizer (optional) → processing → response → WORKFLOW_OUTPUT",
185
+ categorizer: "For multi-intent workflows, use chat_categorizer early with a Fallback category",
186
+ fallback: "Every categorizer MUST have a Fallback category for unmatched intents",
187
+ hitl: "For actions with side effects (email, external calls), consider general_hitl for approval",
188
+ search: "Use search/v2 with datastore_configs. Wire query from trigger output.",
189
+ },
190
+ // LLM-driven analysis pattern: MCP provides data, LLM does the thinking
191
+ _next_steps: deprecationWarnings.length > 0
192
+ ? [
193
+ "WARNING: Workflow uses deprecated actions. Update them first:",
194
+ ...deprecationWarnings.map(w => ` - ${w.action}: ${w.replacement || "check catalog for replacement"}`),
195
+ "",
196
+ "To analyze workflow health:",
197
+ "1. Fetch ema://rules/anti-patterns",
198
+ "2. Fetch ema://rules/structural-invariants",
199
+ "3. Check workflow_def nodes against the rules",
200
+ "4. Report issues YOU find (MCP does not pre-compute)",
201
+ "",
202
+ "Then deploy with: workflow(mode='deploy', persona_id='...', workflow_def={...})",
203
+ ]
204
+ : [
205
+ "To analyze workflow health:",
206
+ "1. Fetch ema://rules/anti-patterns",
207
+ "2. Fetch ema://rules/structural-invariants",
208
+ "3. Check workflow_def nodes against the rules",
209
+ "4. Report issues YOU find (MCP does not pre-compute)",
210
+ "",
211
+ "Generate or modify workflow_def based on requirements above",
212
+ "Deploy with: workflow(mode='deploy', persona_id='...', workflow_def={...})",
213
+ ],
214
+ };
215
+ // Include deprecated actions reference if there are warnings or on request
216
+ if (deprecationWarnings.length > 0 || args.include_deprecated) {
217
+ result.deprecated_actions_reference = deprecatedActions;
218
+ }
219
+ return result;
220
+ }
37
221
  /**
38
222
  * Main workflow handler with mode-based dispatch
39
223
  *
40
- * Routes to the appropriate extracted handler based on effective mode.
224
+ * PUBLIC modes: get, deploy
225
+ * INTERNAL modes: modify, generate, analyze, optimize, compare (called from persona tool)
41
226
  */
42
227
  export async function handleWorkflow(args, client, getTemplateId) {
43
228
  const personaId = args.persona_id;
@@ -46,90 +231,78 @@ export async function handleWorkflow(args, client, getTemplateId) {
46
231
  const optimize = args.optimize;
47
232
  const compareTo = args.compare_to;
48
233
  const operations = args.operations;
49
- // Smart mode detection - user doesn't need to specify
50
- let effectiveMode;
51
- if (args.mode === "compile") {
52
- effectiveMode = "compile";
53
- }
54
- else if (compareTo) {
55
- effectiveMode = "compare";
56
- }
57
- else if (optimize && personaId) {
58
- effectiveMode = "optimize";
59
- }
60
- else if (personaId && workflowDef) {
61
- effectiveMode = "deploy";
62
- }
63
- else if (personaId && (input || operations)) {
64
- // Modify mode: either natural language input OR structured operations
65
- effectiveMode = "modify";
66
- }
67
- else if (input && !personaId) {
68
- effectiveMode = "generate";
69
- }
70
- else if (personaId || workflowDef || args.include) {
71
- effectiveMode = "analyze";
72
- }
73
- else {
74
- effectiveMode = "analyze";
75
- }
76
- // Explicit mode support
77
- const legacyMode = args.mode;
78
- if (legacyMode === "extend")
79
- effectiveMode = "modify";
80
- if (legacyMode === "modify")
81
- effectiveMode = "modify";
82
- if (legacyMode === "deploy")
83
- effectiveMode = "deploy";
84
- // Route to extracted handler
85
- switch (effectiveMode) {
86
- case "generate":
87
- return handleWorkflowGenerate(args, client, getTemplateId);
88
- case "modify":
89
- case "extend":
90
- return handleWorkflowModify(args, client);
91
- case "analyze":
92
- return handleWorkflowAnalyze(args, client);
93
- case "deploy":
94
- return handleWorkflowDeploy(args, client);
95
- case "compare":
96
- return handleWorkflowCompare(args, client);
97
- case "compile":
98
- return handleWorkflowCompile(args, client);
99
- case "optimize":
100
- return handleWorkflowOptimize(args, client);
101
- default:
102
- return { error: `Unknown mode: ${effectiveMode}` };
234
+ // Explicit mode takes priority
235
+ const mode = args.mode;
236
+ // PUBLIC MODES (from workflow tool)
237
+ if (mode === "get") {
238
+ return handleWorkflowGet(args, client);
239
+ }
240
+ if (mode === "deploy") {
241
+ return handleWorkflowDeploy(args, client);
242
+ }
243
+ // INTERNAL MODES (called from persona tool, not workflow tool)
244
+ if (mode === "modify" || mode === "extend") {
245
+ return handleWorkflowModify(args, client);
246
+ }
247
+ if (mode === "generate") {
248
+ return handleWorkflowGenerate(args, client, getTemplateId);
249
+ }
250
+ if (mode === "analyze") {
251
+ return handleWorkflowAnalyze(args, client);
252
+ }
253
+ if (mode === "optimize") {
254
+ return handleWorkflowOptimize(args, client);
255
+ }
256
+ if (mode === "compare") {
257
+ return handleWorkflowCompare(args, client);
258
+ }
259
+ // Auto-detect mode (for backwards compatibility with internal routing)
260
+ if (compareTo) {
261
+ return handleWorkflowCompare(args, client);
262
+ }
263
+ if (optimize && personaId) {
264
+ return handleWorkflowOptimize(args, client);
265
+ }
266
+ if (personaId && workflowDef) {
267
+ return handleWorkflowDeploy(args, client);
103
268
  }
269
+ if (personaId && (input || operations)) {
270
+ return handleWorkflowModify(args, client);
271
+ }
272
+ if (input && !personaId) {
273
+ return handleWorkflowGenerate(args, client, getTemplateId);
274
+ }
275
+ if (personaId) {
276
+ return handleWorkflowAnalyze(args, client);
277
+ }
278
+ // Invalid mode
279
+ return {
280
+ error: `Invalid or missing mode: ${mode}`,
281
+ public_modes: ["get", "deploy"],
282
+ hint: "MCP provides data (get) and executes (deploy). LLM does all thinking.",
283
+ };
104
284
  }
105
285
  /**
106
286
  * Check if a workflow mode has been extracted
107
287
  */
108
288
  export function hasExtractedWorkflowHandler(mode) {
109
- const extractedModes = ["analyze", "compare", "compile", "optimize", "deploy", "generate", "modify", "extend"];
289
+ const extractedModes = ["get", "deploy", "modify", "extend", "generate", "analyze", "optimize", "compare"];
110
290
  return extractedModes.includes(mode);
111
291
  }
112
292
  /**
113
293
  * Get handler for a specific workflow mode
114
- * Note: generate handler has a different signature (requires getTemplateId)
115
294
  */
116
295
  export function getWorkflowModeHandler(mode) {
117
296
  switch (mode) {
118
- case "analyze":
119
- return handleWorkflowAnalyze;
120
- case "compare":
121
- return handleWorkflowCompare;
122
- case "compile":
123
- return handleWorkflowCompile;
124
- case "optimize":
125
- return handleWorkflowOptimize;
297
+ case "get":
126
298
  case "deploy":
127
- return handleWorkflowDeploy;
128
- case "generate":
129
- return handleWorkflowGenerate;
130
299
  case "modify":
131
300
  case "extend":
132
- return handleWorkflowModify;
301
+ case "generate":
302
+ case "analyze":
303
+ case "optimize":
304
+ case "compare":
305
+ return handleWorkflow;
133
306
  default:
134
307
  return undefined;
135
308
  }