@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.
- package/dist/mcp/handlers/action/index.js +17 -20
- package/dist/mcp/handlers/data/index.js +75 -6
- package/dist/mcp/handlers/deprecation.js +50 -0
- package/dist/mcp/handlers/env/index.js +3 -3
- package/dist/mcp/handlers/knowledge/index.js +44 -237
- package/dist/mcp/handlers/persona/create.js +63 -18
- package/dist/mcp/handlers/persona/index.js +9 -10
- package/dist/mcp/handlers/persona/update.js +6 -3
- package/dist/mcp/handlers/reference/index.js +15 -2
- package/dist/mcp/handlers/sync/index.js +3 -18
- package/dist/mcp/handlers/workflow/analyze.js +53 -105
- package/dist/mcp/handlers/workflow/deploy.js +160 -1
- package/dist/mcp/handlers/workflow/generate.js +8 -28
- package/dist/mcp/handlers/workflow/index.js +258 -85
- package/dist/mcp/handlers/workflow/modify.js +69 -26
- package/dist/mcp/handlers/workflow/optimize.js +22 -108
- package/dist/mcp/handlers/workflow/utils.js +0 -102
- package/dist/mcp/handlers-consolidated.js +15 -38
- package/dist/mcp/prompts.js +82 -44
- package/dist/mcp/resources.js +335 -3
- package/dist/mcp/server.js +242 -457
- package/dist/mcp/tools.js +44 -61
- package/dist/sdk/action-schema-parser.js +11 -5
- package/dist/sdk/client.js +81 -25
- package/dist/sdk/ema-client.js +11 -0
- package/dist/sdk/generated/deprecated-actions.js +171 -0
- package/dist/sdk/guidance.js +58 -35
- package/dist/sdk/index.js +8 -7
- package/dist/sdk/knowledge.js +216 -1932
- package/dist/sdk/quality-gates.js +60 -336
- package/dist/sdk/validation-rules.js +33 -0
- package/dist/sdk/workflow-fixer.js +29 -360
- package/dist/sdk/workflow-generator.js +2 -1
- package/dist/sdk/workflow-intent.js +43 -3
- package/dist/sdk/workflow-transformer.js +0 -342
- package/docs/dashboard-operations.md +35 -0
- package/docs/ema-user-guide.md +66 -0
- package/docs/mcp-tools-guide.md +74 -45
- package/package.json +2 -2
- package/dist/mcp/handlers/persona/analyze.js +0 -275
- package/dist/mcp/handlers/persona/compare.js +0 -32
- package/dist/mcp/handlers/workflow/compile.js +0 -39
- package/docs/DEBUG-ANALYSIS-unused-category-type-mismatch.md +0 -481
- package/docs/TODO-fix-analyzer-and-modify.md +0 -182
- 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 ===
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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:
|
|
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:
|
|
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
|
|
2
|
+
* Workflow Handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 = ["
|
|
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 "
|
|
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
|
-
|
|
301
|
+
case "generate":
|
|
302
|
+
case "analyze":
|
|
303
|
+
case "optimize":
|
|
304
|
+
case "compare":
|
|
305
|
+
return handleWorkflow;
|
|
133
306
|
default:
|
|
134
307
|
return undefined;
|
|
135
308
|
}
|