@ema.co/mcp-toolkit 2026.2.5 → 2026.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/.context/public/guides/ema-user-guide.md +12 -16
- package/.context/public/guides/mcp-tools-guide.md +203 -334
- package/LICENSE +29 -21
- package/README.md +58 -35
- package/dist/mcp/domain/loop-detection.js +97 -0
- package/dist/mcp/domain/proto-constraints.js +284 -0
- package/dist/mcp/domain/structural-rules.js +12 -5
- package/dist/mcp/domain/validation-rules.js +107 -20
- package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
- package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
- package/dist/mcp/domain/workflow-graph.js +374 -0
- package/dist/mcp/domain/workflow-optimizer.js +10 -4
- package/dist/mcp/guidance.js +54 -31
- package/dist/mcp/handlers/feedback/index.js +139 -0
- package/dist/mcp/handlers/feedback/store.js +262 -0
- package/dist/mcp/handlers/persona/index.js +237 -8
- package/dist/mcp/handlers/persona/schema.js +27 -0
- package/dist/mcp/handlers/reference/index.js +6 -4
- package/dist/mcp/handlers/workflow/index.js +25 -28
- package/dist/mcp/handlers/workflow/optimize.js +73 -33
- package/dist/mcp/handlers/workflow/validation.js +1 -1
- package/dist/mcp/knowledge-types.js +7 -0
- package/dist/mcp/knowledge.js +146 -834
- package/dist/mcp/resources.js +610 -18
- package/dist/mcp/server.js +233 -2156
- package/dist/mcp/tools.js +91 -5
- package/dist/sdk/generated/agent-catalog.js +615 -0
- package/dist/sdk/generated/deprecated-actions.js +182 -96
- package/dist/sdk/generated/proto-fields.js +2 -1
- package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
- package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
- package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
- package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
- package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
- package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
- package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
- package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
- package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
- package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
- package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
- package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
- package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
- package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
- package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
- package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
- package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
- package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
- package/dist/sdk/generated/widget-catalog.js +60 -0
- package/docs/README.md +17 -9
- package/package.json +2 -2
- package/.context/public/guides/dashboard-operations.md +0 -286
- package/.context/public/guides/email-patterns.md +0 -125
- package/dist/mcp/domain/intent-architect.js +0 -914
- package/dist/mcp/domain/quality-gates.js +0 -110
- package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
- package/dist/mcp/domain/workflow-intent.js +0 -1806
- package/dist/mcp/domain/workflow-merge.js +0 -449
- package/dist/mcp/domain/workflow-tracer.js +0 -648
- package/dist/mcp/domain/workflow-transformer.js +0 -742
- package/dist/mcp/handlers/persona/intent.js +0 -141
- package/dist/mcp/handlers/workflow/analyze.js +0 -119
- package/dist/mcp/handlers/workflow/compare.js +0 -70
- package/dist/mcp/handlers/workflow/generate.js +0 -384
- package/dist/mcp/handlers-consolidated.js +0 -333
package/dist/mcp/server.js
CHANGED
|
@@ -25,2135 +25,61 @@ import { PromptRegistry, isPromptError } from "./prompts.js";
|
|
|
25
25
|
import { ResourceRegistry, isResourceError } from "./resources.js";
|
|
26
26
|
import { generateServerInstructions, getContextualTip, TOOL_GUIDANCE } from "./guidance.js";
|
|
27
27
|
import { resolveSyncBehavior, loadSyncOptions } from "../sync/sync-options.js";
|
|
28
|
-
import { fingerprintPersona
|
|
28
|
+
import { fingerprintPersona } from "../sync.js";
|
|
29
29
|
// Direct Sync (Config-less) - extracted to handlers/sync/direct.ts
|
|
30
30
|
import { directSyncPersona, directSyncPersonaById, directSyncAll } from "./handlers/sync/direct.js";
|
|
31
31
|
import { createVersionStorage } from "../sync/version-storage.js";
|
|
32
32
|
import { createVersionPolicyEngine } from "../sync/version-policy.js";
|
|
33
|
-
//
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
import {
|
|
41
|
-
// V2 Tools (4 tools: persona, catalog, sync, env) - NEW DEFAULT
|
|
42
|
-
import { generateTools, } from "./tools.js";
|
|
43
|
-
import { handleEnv, handlePersona,
|
|
44
|
-
// handleWorkflow - removed, now using extracted handler from ./handlers/workflow/index.js
|
|
45
|
-
handleAction,
|
|
46
|
-
// handleData - removed, now using extracted handler from ./handlers/data/index.js
|
|
47
|
-
handleTemplate, handleKnowledge, handleReference, } from "./handlers-consolidated.js";
|
|
33
|
+
// V2 Tools
|
|
34
|
+
import { generateTools } from "./tools.js";
|
|
35
|
+
import { handlePersona } from "./handlers/persona/index.js";
|
|
36
|
+
import { handleEnv } from "./handlers/env/index.js";
|
|
37
|
+
import { handleAction } from "./handlers/action/index.js";
|
|
38
|
+
import { handleTemplate } from "./handlers/template/index.js";
|
|
39
|
+
import { handleKnowledge } from "./handlers/knowledge/index.js";
|
|
40
|
+
import { handleReference } from "./handlers/reference/index.js";
|
|
48
41
|
// Import extracted handlers
|
|
49
42
|
import { handleWorkflow } from "./handlers/workflow/index.js";
|
|
50
43
|
import { handleCatalog } from "./handlers/catalog/index.js";
|
|
44
|
+
import { handleFeedback } from "./handlers/feedback/index.js";
|
|
45
|
+
import { recordTelemetry } from "./handlers/feedback/store.js";
|
|
51
46
|
import { handleConsolidateDemoData, handleGenerateDemoDocument, handleValidateDemoDocument, handleGetDemoDataTemplate, } from "./handlers/demo/index.js";
|
|
52
|
-
// Import from handler utilities (normalizeTriggerType uses generated enum labels)
|
|
53
|
-
import { normalizeTriggerType } from "./handlers/utils.js";
|
|
54
|
-
// Import from SDK proto-config (CANONICAL widget validation and merging)
|
|
55
|
-
import { mergeProtoConfig } from "../sdk/proto-config.js";
|
|
56
|
-
// Workflow Auto-Fix Helpers (extracted to handlers/workflow/fix.ts)
|
|
57
|
-
import { summarizeWorkflow } from "./handlers/workflow/fix.js";
|
|
58
47
|
// Start token initialization in background (non-blocking)
|
|
59
48
|
void initializeApiKeyTokens();
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* V2: 5 tools (persona, catalog, workflow, sync, env) - LLM-optimized
|
|
93
|
-
*
|
|
94
|
-
* Why V2:
|
|
95
|
-
* - Minimal tool count optimizes LLM tool selection
|
|
96
|
-
* - Data operations under persona (always persona-scoped)
|
|
97
|
-
* - Catalog consolidates all reference data (actions, templates, etc.)
|
|
98
|
-
* - Clear separation: entity (persona), reference (catalog), operation (sync, workflow)
|
|
99
|
-
*/
|
|
100
|
-
function generateAllTools() {
|
|
101
|
-
const envNames = getAvailableEnvironments().map(e => e.name);
|
|
102
|
-
const defaultEnv = getDefaultEnvName();
|
|
103
|
-
return generateTools(envNames, defaultEnv);
|
|
104
|
-
}
|
|
105
|
-
// Generate tools (called once at module load)
|
|
106
|
-
const TOOLS = generateAllTools();
|
|
107
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
-
// ADDITIONAL TOOLS (special-purpose inline tools)
|
|
109
|
-
// These handle specific operations that don't fit the V2 pattern
|
|
110
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
-
const ADDITIONAL_TOOLS = [
|
|
112
|
-
// NOTE: Tools (persona, catalog, workflow, sync, env) are in ./tools.ts
|
|
113
|
-
//
|
|
114
|
-
// Special-purpose tools here:
|
|
115
|
-
// - compile_workflow - Direct workflow compilation
|
|
116
|
-
// - Demo data tools - RAG document generation/validation
|
|
117
|
-
// - Data source tools - Upload/delete/manage knowledge sources
|
|
118
|
-
// toggle_embedding
|
|
119
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
-
// Workflow Compilation - Template-driven, no biased patterns
|
|
121
|
-
// Read ema://catalog/patterns for pattern references, then construct nodes
|
|
122
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
-
{
|
|
124
|
-
name: "compile_workflow",
|
|
125
|
-
description: `🔧 Compile workflow from node specification. Template-driven - read \`ema://catalog/patterns\` for pattern templates, construct nodes, then compile.
|
|
126
|
-
|
|
127
|
-
**Process**:
|
|
128
|
-
1. Read \`ema://catalog/patterns\` for pattern reference
|
|
129
|
-
2. Read \`ema://catalog/agents\` for available actions
|
|
130
|
-
3. Construct nodes array based on requirements
|
|
131
|
-
4. Call compile_workflow with nodes
|
|
132
|
-
5. Use deploy_workflow to deploy result
|
|
133
|
-
|
|
134
|
-
**Example nodes** (KB search):
|
|
135
|
-
\`\`\`json
|
|
136
|
-
[
|
|
137
|
-
{ "id": "trigger", "action_type": "chat_trigger", "display_name": "Trigger" },
|
|
138
|
-
{ "id": "search", "action_type": "search", "display_name": "Search", "inputs": { "query": { "type": "action_output", "action_name": "trigger", "output": "user_query" } } },
|
|
139
|
-
{ "id": "respond", "action_type": "respond_with_sources", "display_name": "Respond", "inputs": { "search_results": { "type": "action_output", "action_name": "search", "output": "search_results" } } }
|
|
140
|
-
]
|
|
141
|
-
\`\`\``,
|
|
142
|
-
inputSchema: {
|
|
143
|
-
type: "object",
|
|
144
|
-
properties: {
|
|
145
|
-
name: { type: "string", description: "Workflow name" },
|
|
146
|
-
description: { type: "string", description: "Workflow description" },
|
|
147
|
-
persona_type: { type: "string", enum: ["voice", "chat", "dashboard"], description: "AI type" },
|
|
148
|
-
nodes: {
|
|
149
|
-
type: "array",
|
|
150
|
-
description: "Node definitions",
|
|
151
|
-
items: {
|
|
152
|
-
type: "object",
|
|
153
|
-
properties: {
|
|
154
|
-
id: { type: "string", description: "Node ID" },
|
|
155
|
-
action_type: { type: "string", description: "Action type (e.g., chat_trigger, search, respond_with_sources)" },
|
|
156
|
-
display_name: { type: "string", description: "Display name" },
|
|
157
|
-
description: { type: "string", description: "Optional description" },
|
|
158
|
-
inputs: { type: "object", description: "Input bindings (key: input name, value: binding spec)" },
|
|
159
|
-
run_if: {
|
|
160
|
-
type: "object",
|
|
161
|
-
description: "Conditional execution",
|
|
162
|
-
properties: {
|
|
163
|
-
source_action: { type: "string" },
|
|
164
|
-
source_output: { type: "string", description: "Output name to check" },
|
|
165
|
-
operator: { type: "string", enum: ["eq", "neq", "gt", "lt", "gte", "lte"], description: "Comparison operator" },
|
|
166
|
-
value: { type: "string", description: "Value to compare against" },
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
categories: {
|
|
170
|
-
type: "array",
|
|
171
|
-
description: "Categories for categorizer nodes",
|
|
172
|
-
items: {
|
|
173
|
-
type: "object",
|
|
174
|
-
properties: {
|
|
175
|
-
name: { type: "string", description: "Category name (e.g., 'Password Reset', 'Fallback')" },
|
|
176
|
-
description: { type: "string", description: "When this category triggers" },
|
|
177
|
-
examples: { type: "array", items: { type: "string" }, description: "Example phrases" },
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
tools: {
|
|
182
|
-
type: "array",
|
|
183
|
-
description: "External tools for external_action_caller nodes",
|
|
184
|
-
items: {
|
|
185
|
-
type: "object",
|
|
186
|
-
properties: {
|
|
187
|
-
name: { type: "string", description: "Tool name" },
|
|
188
|
-
namespace: { type: "string", description: "Tool namespace" },
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
disable_human_interaction: { type: "boolean", description: "If true, disable HITL for this node" },
|
|
193
|
-
},
|
|
194
|
-
required: ["id", "action_type", "display_name"],
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
result_mappings: {
|
|
198
|
-
type: "array",
|
|
199
|
-
description: "Which node outputs map to WORKFLOW_OUTPUT",
|
|
200
|
-
items: {
|
|
201
|
-
type: "object",
|
|
202
|
-
properties: {
|
|
203
|
-
node_id: { type: "string", description: "Node ID" },
|
|
204
|
-
output: { type: "string", description: "Output name from the node" },
|
|
205
|
-
},
|
|
206
|
-
required: ["node_id", "output"],
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
required: ["name", "description", "persona_type", "nodes", "result_mappings"],
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
214
|
-
// Data Source Management
|
|
215
|
-
// TODO: Add support for 3rd party data sources (Google Drive, SharePoint, Confluence, etc.)
|
|
216
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
217
|
-
{
|
|
218
|
-
name: "upload_data_source",
|
|
219
|
-
description: "Upload a file from the local filesystem to an AI Employee's knowledge base. The file will be available for RAG/search if embedding is enabled. IMPORTANT: Upload data sources BEFORE deploying workflows that reference them.",
|
|
220
|
-
inputSchema: {
|
|
221
|
-
type: "object",
|
|
222
|
-
properties: {
|
|
223
|
-
persona_id: {
|
|
224
|
-
type: "string",
|
|
225
|
-
description: "The AI Employee ID to upload the file to",
|
|
226
|
-
},
|
|
227
|
-
file_path: {
|
|
228
|
-
type: "string",
|
|
229
|
-
description: "Absolute path to the file on the local filesystem",
|
|
230
|
-
},
|
|
231
|
-
tags: {
|
|
232
|
-
type: "string",
|
|
233
|
-
description: "Optional tags for categorizing the file (default: 'fileUpload')",
|
|
234
|
-
},
|
|
235
|
-
env: {
|
|
236
|
-
type: "string",
|
|
237
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
238
|
-
},
|
|
239
|
-
},
|
|
240
|
-
required: ["persona_id", "file_path"],
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
name: "delete_data_source",
|
|
245
|
-
description: "Delete a file from an AI Employee's knowledge base.",
|
|
246
|
-
inputSchema: {
|
|
247
|
-
type: "object",
|
|
248
|
-
properties: {
|
|
249
|
-
persona_id: {
|
|
250
|
-
type: "string",
|
|
251
|
-
description: "The AI Employee ID",
|
|
252
|
-
},
|
|
253
|
-
file_id: {
|
|
254
|
-
type: "string",
|
|
255
|
-
description: "The file ID to delete (from list_data_sources)",
|
|
256
|
-
},
|
|
257
|
-
env: {
|
|
258
|
-
type: "string",
|
|
259
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
required: ["persona_id", "file_id"],
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
name: "list_data_sources",
|
|
267
|
-
description: "List data sources (knowledge base files/documents) configured for an AI Employee, including upload status and file count.",
|
|
268
|
-
inputSchema: {
|
|
269
|
-
type: "object",
|
|
270
|
-
properties: {
|
|
271
|
-
persona_id: {
|
|
272
|
-
type: "string",
|
|
273
|
-
description: "The AI Employee ID to list data sources for",
|
|
274
|
-
},
|
|
275
|
-
env: {
|
|
276
|
-
type: "string",
|
|
277
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
required: ["persona_id"],
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: "get_embedding_status",
|
|
285
|
-
description: "Get the embedding/RAG status for an AI Employee's knowledge base.",
|
|
286
|
-
inputSchema: {
|
|
287
|
-
type: "object",
|
|
288
|
-
properties: {
|
|
289
|
-
persona_id: {
|
|
290
|
-
type: "string",
|
|
291
|
-
description: "The AI Employee ID",
|
|
292
|
-
},
|
|
293
|
-
env: {
|
|
294
|
-
type: "string",
|
|
295
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
required: ["persona_id"],
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
name: "toggle_embedding",
|
|
303
|
-
description: "Enable or disable embedding/RAG for an AI Employee's knowledge base. When enabled, uploaded documents are indexed for semantic search.",
|
|
304
|
-
inputSchema: {
|
|
305
|
-
type: "object",
|
|
306
|
-
properties: {
|
|
307
|
-
persona_id: {
|
|
308
|
-
type: "string",
|
|
309
|
-
description: "The AI Employee ID",
|
|
310
|
-
},
|
|
311
|
-
enabled: {
|
|
312
|
-
type: "boolean",
|
|
313
|
-
description: "Whether to enable (true) or disable (false) embedding",
|
|
314
|
-
},
|
|
315
|
-
env: {
|
|
316
|
-
type: "string",
|
|
317
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
required: ["persona_id", "enabled"],
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
324
|
-
// Unified Workflow Tool
|
|
325
|
-
// Accepts any input: natural language, partial spec, full spec, or persona_id
|
|
326
|
-
// Normalizes to WorkflowIntent → validates → generates → deploys
|
|
327
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
328
|
-
{
|
|
329
|
-
name: "workflow",
|
|
330
|
-
description: `🔧 UNIFIED workflow tool. Accepts ANY input and normalizes it.
|
|
331
|
-
|
|
332
|
-
**Input types** (auto-detected):
|
|
333
|
-
- Natural language: "IT helpdesk that creates ServiceNow tickets"
|
|
334
|
-
- Partial spec: { intents: [...], tools: [...] }
|
|
335
|
-
- Full nodes spec: { nodes: [...], result_mappings: [...] }
|
|
336
|
-
- Existing persona: persona_id to analyze/improve
|
|
337
|
-
|
|
338
|
-
**Process**:
|
|
339
|
-
1. Parse input → WorkflowIntent (normalized representation)
|
|
340
|
-
2. Validate completeness → return questions if incomplete
|
|
341
|
-
3. Generate workflow (local compile or Auto Builder)
|
|
342
|
-
4. Validate output → auto-fix if enabled
|
|
343
|
-
5. Deploy if persona_id provided
|
|
344
|
-
|
|
345
|
-
**Examples**:
|
|
346
|
-
\`\`\`
|
|
347
|
-
workflow("IT helpdesk bot with KB search")
|
|
348
|
-
workflow({ intents: [{name: "Billing", handler: "search"}], tools: [{namespace: "service_now", action: "Create_Ticket"}] })
|
|
349
|
-
workflow(persona_id, mode="improve")
|
|
350
|
-
\`\`\``,
|
|
351
|
-
inputSchema: withEnvParam({
|
|
352
|
-
input: {
|
|
353
|
-
description: "Natural language description, partial spec object, or full nodes spec",
|
|
354
|
-
},
|
|
355
|
-
persona_id: {
|
|
356
|
-
type: "string",
|
|
357
|
-
description: "For deployment OR to analyze/improve existing workflow",
|
|
358
|
-
},
|
|
359
|
-
mode: {
|
|
360
|
-
type: "string",
|
|
361
|
-
enum: ["generate", "improve", "analyze"],
|
|
362
|
-
description: "generate (default): Create new workflow. improve: Fix existing. analyze: Validate only.",
|
|
363
|
-
},
|
|
364
|
-
persona_type: {
|
|
365
|
-
type: "string",
|
|
366
|
-
enum: ["voice", "chat", "dashboard"],
|
|
367
|
-
description: "AI type (default: chat, auto-detected from input)",
|
|
368
|
-
},
|
|
369
|
-
use_autobuilder: {
|
|
370
|
-
type: "boolean",
|
|
371
|
-
description: "Force Auto Builder for generation (default: auto-decide based on complexity)",
|
|
372
|
-
},
|
|
373
|
-
auto_deploy: {
|
|
374
|
-
type: "boolean",
|
|
375
|
-
description: "Deploy immediately (default: false - returns preview)",
|
|
376
|
-
},
|
|
377
|
-
auto_fix: {
|
|
378
|
-
type: "boolean",
|
|
379
|
-
description: "Auto-fix detected issues (default: true)",
|
|
380
|
-
},
|
|
381
|
-
}, []),
|
|
382
|
-
},
|
|
383
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
384
|
-
// Demo Data Management
|
|
385
|
-
// Tools for consolidating, transforming, and preparing mock data for RAG
|
|
386
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
387
|
-
{
|
|
388
|
-
name: "consolidate_demo_data",
|
|
389
|
-
description: `Transform normalized JSON source files into RAG-optimized Markdown documents for Ema knowledge bases.
|
|
390
|
-
|
|
391
|
-
This tool pre-joins related data (like customers + orders + tickets) into denormalized entity documents that work well with semantic search.
|
|
392
|
-
|
|
393
|
-
**Why this matters**: RAG systems can't do SQL-style joins at query time. Data relationships must be explicit in the document content.
|
|
394
|
-
|
|
395
|
-
**Supported patterns**:
|
|
396
|
-
- Entity consolidation (customer with all their orders, tickets, contacts)
|
|
397
|
-
- Product catalogs with cross-references
|
|
398
|
-
- Scenario documents for demos
|
|
399
|
-
|
|
400
|
-
**Output format**: Markdown files with embedded tables, metadata comments, and narrative summaries optimized for Ema's search agents.`,
|
|
401
|
-
inputSchema: {
|
|
402
|
-
type: "object",
|
|
403
|
-
properties: {
|
|
404
|
-
source_dir: {
|
|
405
|
-
type: "string",
|
|
406
|
-
description: "Path to directory containing source JSON files (e.g., './data/source')",
|
|
407
|
-
},
|
|
408
|
-
output_dir: {
|
|
409
|
-
type: "string",
|
|
410
|
-
description: "Path to output directory for generated Markdown files (e.g., './data/knowledge-base')",
|
|
411
|
-
},
|
|
412
|
-
entity_type: {
|
|
413
|
-
type: "string",
|
|
414
|
-
enum: ["customer", "product", "employee", "scenario", "custom"],
|
|
415
|
-
description: "Type of entity being consolidated. Determines document structure.",
|
|
416
|
-
},
|
|
417
|
-
primary_file: {
|
|
418
|
-
type: "string",
|
|
419
|
-
description: "Name of the primary JSON file (e.g., 'customers.json')",
|
|
420
|
-
},
|
|
421
|
-
joins: {
|
|
422
|
-
type: "array",
|
|
423
|
-
items: {
|
|
424
|
-
type: "object",
|
|
425
|
-
properties: {
|
|
426
|
-
file: { type: "string", description: "JSON file to join (e.g., 'orders.json')" },
|
|
427
|
-
on: { type: "string", description: "Foreign key field (e.g., 'customerId')" },
|
|
428
|
-
as: { type: "string", description: "Name for the joined data (e.g., 'orders')" },
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
description: "Array of files to join with the primary file",
|
|
432
|
-
},
|
|
433
|
-
id_field: {
|
|
434
|
-
type: "string",
|
|
435
|
-
description: "Field name for the entity ID (default: 'id')",
|
|
436
|
-
},
|
|
437
|
-
name_field: {
|
|
438
|
-
type: "string",
|
|
439
|
-
description: "Field name for the entity name (default: 'name')",
|
|
440
|
-
},
|
|
441
|
-
tags: {
|
|
442
|
-
type: "string",
|
|
443
|
-
description: "Comma-separated tags to include in document metadata",
|
|
444
|
-
},
|
|
445
|
-
},
|
|
446
|
-
required: ["source_dir", "output_dir", "entity_type", "primary_file"],
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
name: "generate_demo_document",
|
|
451
|
-
description: `Generate a single RAG-optimized Markdown document from provided JSON data.
|
|
452
|
-
|
|
453
|
-
Use this for:
|
|
454
|
-
- Creating individual entity documents programmatically
|
|
455
|
-
- Testing document formats before batch consolidation
|
|
456
|
-
- Custom document generation with specific data
|
|
457
|
-
|
|
458
|
-
The output follows Ema's knowledge base best practices with metadata comments, tables, and narrative context.`,
|
|
459
|
-
inputSchema: {
|
|
460
|
-
type: "object",
|
|
461
|
-
properties: {
|
|
462
|
-
entity_type: {
|
|
463
|
-
type: "string",
|
|
464
|
-
enum: ["customer", "product", "employee", "scenario", "reference"],
|
|
465
|
-
description: "Type of document to generate",
|
|
466
|
-
},
|
|
467
|
-
data: {
|
|
468
|
-
type: "object",
|
|
469
|
-
description: "The entity data as a JSON object",
|
|
470
|
-
},
|
|
471
|
-
related_data: {
|
|
472
|
-
type: "object",
|
|
473
|
-
description: "Related data to include (e.g., { orders: [...], tickets: [...] })",
|
|
474
|
-
},
|
|
475
|
-
output_path: {
|
|
476
|
-
type: "string",
|
|
477
|
-
description: "Optional: Path to save the generated document. If not provided, returns the content.",
|
|
478
|
-
},
|
|
479
|
-
tags: {
|
|
480
|
-
type: "string",
|
|
481
|
-
description: "Comma-separated tags for metadata",
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
|
-
required: ["entity_type", "data"],
|
|
485
|
-
},
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
name: "validate_demo_document",
|
|
489
|
-
description: `Validate a Markdown document for RAG optimization and Ema compatibility.
|
|
490
|
-
|
|
491
|
-
Checks for:
|
|
492
|
-
- Required metadata comments (ema_entity, ema_id, ema_tags)
|
|
493
|
-
- Table formatting
|
|
494
|
-
- Narrative context presence
|
|
495
|
-
- Cross-reference consistency
|
|
496
|
-
- Filename conventions`,
|
|
497
|
-
inputSchema: {
|
|
498
|
-
type: "object",
|
|
499
|
-
properties: {
|
|
500
|
-
file_path: {
|
|
501
|
-
type: "string",
|
|
502
|
-
description: "Path to the Markdown file to validate",
|
|
503
|
-
},
|
|
504
|
-
content: {
|
|
505
|
-
type: "string",
|
|
506
|
-
description: "Alternatively, provide the document content directly",
|
|
507
|
-
},
|
|
508
|
-
},
|
|
509
|
-
required: [],
|
|
510
|
-
},
|
|
511
|
-
},
|
|
512
|
-
{
|
|
513
|
-
name: "get_demo_data_template",
|
|
514
|
-
description: `Get a template for demo data documents based on entity type.
|
|
515
|
-
|
|
516
|
-
Returns:
|
|
517
|
-
- Source JSON schema (what fields to include)
|
|
518
|
-
- Output Markdown template
|
|
519
|
-
- Best practices for the entity type
|
|
520
|
-
- Example data`,
|
|
521
|
-
inputSchema: {
|
|
522
|
-
type: "object",
|
|
523
|
-
properties: {
|
|
524
|
-
entity_type: {
|
|
525
|
-
type: "string",
|
|
526
|
-
enum: ["customer", "product", "employee", "scenario", "reference"],
|
|
527
|
-
description: "Type of template to get",
|
|
528
|
-
},
|
|
529
|
-
include_example: {
|
|
530
|
-
type: "boolean",
|
|
531
|
-
description: "Include example data (default: true)",
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
required: ["entity_type"],
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
];
|
|
538
|
-
const toolHandlers = {
|
|
539
|
-
// Environment Management
|
|
540
|
-
list_environments: async () => {
|
|
541
|
-
const envs = getAvailableEnvironments();
|
|
542
|
-
const defaultEnv = getDefaultEnvName();
|
|
543
|
-
return {
|
|
544
|
-
default_environment: defaultEnv,
|
|
545
|
-
environments: envs.map((e) => ({
|
|
546
|
-
name: e.name,
|
|
547
|
-
url: e.baseUrl,
|
|
548
|
-
is_default: e.name === defaultEnv,
|
|
549
|
-
})),
|
|
550
|
-
};
|
|
551
|
-
},
|
|
552
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
553
|
-
// AI Employee Handlers (Consolidated)
|
|
554
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
555
|
-
get_persona: async (args) => {
|
|
556
|
-
const client = createClient(args.env);
|
|
557
|
-
const identifier = String(args.identifier);
|
|
558
|
-
const includeWorkflow = args.include_workflow === true;
|
|
559
|
-
const includeFingerprint = args.include_fingerprint === true;
|
|
560
|
-
// Auto-detect: UUIDs are 36 chars with dashes, otherwise it's a name
|
|
561
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
562
|
-
let persona;
|
|
563
|
-
if (isUUID) {
|
|
564
|
-
// Fetch by ID - use full fetch if workflow needed, otherwise list
|
|
565
|
-
if (includeWorkflow || includeFingerprint) {
|
|
566
|
-
persona = (await client.getPersonaById(identifier)) ?? undefined;
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
const personas = await client.getPersonasForTenant();
|
|
570
|
-
persona = personas.find((p) => p.id === identifier);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
// Fetch by name - always need to list first to find ID
|
|
575
|
-
const personas = await client.getPersonasForTenant();
|
|
576
|
-
persona = personas.find((p) => p.name === identifier);
|
|
577
|
-
// If found and need workflow, fetch full details
|
|
578
|
-
if (persona && (includeWorkflow || includeFingerprint)) {
|
|
579
|
-
persona = (await client.getPersonaById(persona.id)) ?? undefined;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
if (!persona) {
|
|
583
|
-
throw new Error(`AI Employee not found: ${identifier} (searched by ${isUUID ? "ID" : "name"})`);
|
|
584
|
-
}
|
|
585
|
-
// Build response
|
|
586
|
-
const result = {
|
|
587
|
-
environment: client["env"].name,
|
|
588
|
-
ai_employee: persona,
|
|
589
|
-
};
|
|
590
|
-
// Add fingerprint if requested
|
|
591
|
-
if (includeFingerprint) {
|
|
592
|
-
result.fingerprint = fingerprintPersona(persona);
|
|
593
|
-
}
|
|
594
|
-
return result;
|
|
595
|
-
},
|
|
596
|
-
find_personas: async (args) => {
|
|
597
|
-
const client = createClient(args.env);
|
|
598
|
-
let personas = await client.getPersonasForTenant();
|
|
599
|
-
// Apply filters
|
|
600
|
-
if (args.query) {
|
|
601
|
-
const q = String(args.query).toLowerCase();
|
|
602
|
-
personas = personas.filter((p) => p.name?.toLowerCase().includes(q));
|
|
603
|
-
}
|
|
604
|
-
if (args.status) {
|
|
605
|
-
const f = String(args.status).toLowerCase();
|
|
606
|
-
personas = personas.filter((p) => p.status?.toLowerCase() === f);
|
|
607
|
-
}
|
|
608
|
-
if (args.trigger_type) {
|
|
609
|
-
const f = String(args.trigger_type).toLowerCase();
|
|
610
|
-
personas = personas.filter((p) => p.trigger_type?.toLowerCase() === f);
|
|
611
|
-
}
|
|
612
|
-
if (args.access_level) {
|
|
613
|
-
const f = String(args.access_level).toLowerCase();
|
|
614
|
-
personas = personas.filter((p) => p.access_level?.toLowerCase() === f);
|
|
615
|
-
}
|
|
616
|
-
if (typeof args.has_workflow === "boolean") {
|
|
617
|
-
personas = personas.filter((p) => args.has_workflow ? !!p.workflow_id : !p.workflow_id);
|
|
618
|
-
}
|
|
619
|
-
if (typeof args.embedding_enabled === "boolean") {
|
|
620
|
-
personas = personas.filter((p) => p.embedding_enabled === args.embedding_enabled);
|
|
621
|
-
}
|
|
622
|
-
const limit = typeof args.limit === "number" ? args.limit : 50;
|
|
623
|
-
personas = personas.slice(0, limit);
|
|
624
|
-
return {
|
|
625
|
-
environment: client["env"].name,
|
|
626
|
-
count: personas.length,
|
|
627
|
-
ai_employees: personas.map((p) => ({
|
|
628
|
-
id: p.id,
|
|
629
|
-
name: p.name,
|
|
630
|
-
description: p.description,
|
|
631
|
-
status: p.status,
|
|
632
|
-
template_id: p.template_id ?? p.templateId,
|
|
633
|
-
workflow_id: p.workflow_id,
|
|
634
|
-
trigger_type: p.trigger_type,
|
|
635
|
-
access_level: p.access_level,
|
|
636
|
-
embedding_enabled: p.embedding_enabled,
|
|
637
|
-
})),
|
|
638
|
-
};
|
|
639
|
-
},
|
|
640
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
641
|
-
// AI Employee CRUD Handlers
|
|
642
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
643
|
-
create_ai_employee: async (args) => {
|
|
644
|
-
const client = createClient(args.env);
|
|
645
|
-
// Determine template_id: explicit > dynamic lookup based on persona_type
|
|
646
|
-
let templateId = args.template_id ? String(args.template_id) : undefined;
|
|
647
|
-
const sourcePersonaId = args.source_persona_id ? String(args.source_persona_id) : undefined;
|
|
648
|
-
// If no template_id or source_persona_id, use dynamic template lookup
|
|
649
|
-
if (!templateId && !sourcePersonaId) {
|
|
650
|
-
const personaType = args.persona_type ? String(args.persona_type).toLowerCase() : null;
|
|
651
|
-
if (personaType) {
|
|
652
|
-
// Dynamic template lookup - templates are tenant-specific
|
|
653
|
-
const templates = await client.getPersonaTemplates();
|
|
654
|
-
const matchingTemplate = templates.find(t => normalizeTriggerType(t.trigger_type) === personaType);
|
|
655
|
-
if (matchingTemplate) {
|
|
656
|
-
templateId = matchingTemplate.id;
|
|
657
|
-
}
|
|
658
|
-
else {
|
|
659
|
-
const availableTypes = [...new Set(templates.map(t => normalizeTriggerType(t.trigger_type)).filter(Boolean))];
|
|
660
|
-
throw new Error(`No template found for type "${personaType}". Available types: ${availableTypes.join(", ") || "none"}`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
throw new Error("Must provide template_id, source_persona_id, or persona_type");
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
const req = {
|
|
668
|
-
name: String(args.name),
|
|
669
|
-
description: args.description ? String(args.description) : undefined,
|
|
670
|
-
template_id: templateId,
|
|
671
|
-
source_persona_id: sourcePersonaId,
|
|
672
|
-
// Note: trigger_type is determined by template, not passed separately
|
|
673
|
-
};
|
|
674
|
-
const result = await client.createAiEmployee(req);
|
|
675
|
-
return {
|
|
676
|
-
environment: client["env"].name,
|
|
677
|
-
success: true,
|
|
678
|
-
persona_id: result.persona_id ?? result.id,
|
|
679
|
-
status: result.status ?? "created",
|
|
680
|
-
template_used: templateId,
|
|
681
|
-
};
|
|
682
|
-
},
|
|
683
|
-
update_ai_employee: async (args) => {
|
|
684
|
-
const client = createClient(args.env);
|
|
685
|
-
const personaId = String(args.persona_id);
|
|
686
|
-
// Use getPersonaById for complete data (including full proto_config)
|
|
687
|
-
const existing = await client.getPersonaById(personaId);
|
|
688
|
-
if (!existing)
|
|
689
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
690
|
-
// Merge proto_config using SDK function (deep merges widgets by name)
|
|
691
|
-
const mergedProtoConfig = mergeProtoConfig(existing.proto_config, args.proto_config);
|
|
692
|
-
// Check if user is trying to pass workflow - redirect them to deploy_workflow
|
|
693
|
-
if (args.workflow) {
|
|
694
|
-
return {
|
|
695
|
-
environment: client["env"].name,
|
|
696
|
-
success: false,
|
|
697
|
-
error: "workflow_parameter_deprecated",
|
|
698
|
-
message: "The 'workflow' parameter has been removed from update_ai_employee. " +
|
|
699
|
-
"Use workflow(mode='get' → mode='deploy') instead (it provides validation and strict stale-state protection).",
|
|
700
|
-
suggestion: {
|
|
701
|
-
tool: "workflow",
|
|
702
|
-
parameters: {
|
|
703
|
-
mode: "deploy",
|
|
704
|
-
persona_id: personaId,
|
|
705
|
-
base_fingerprint: "<fingerprint_from_workflow_get>",
|
|
706
|
-
workflow_def: "your_workflow_here",
|
|
707
|
-
},
|
|
708
|
-
},
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
// CRITICAL: The Ema API requires workflow to be sent along with proto_config
|
|
712
|
-
// for proto_config changes to persist. Without workflow, updates silently fail.
|
|
713
|
-
// GET returns 'workflow_def', UPDATE expects 'workflow' field.
|
|
714
|
-
const existingWorkflow = (existing.workflow_def ?? existing.workflow);
|
|
715
|
-
const req = {
|
|
716
|
-
persona_id: personaId,
|
|
717
|
-
name: args.name ? String(args.name) : undefined,
|
|
718
|
-
description: args.description ? String(args.description) : undefined,
|
|
719
|
-
proto_config: mergedProtoConfig,
|
|
720
|
-
workflow: existingWorkflow, // Required for proto_config to persist
|
|
721
|
-
embedding_enabled: typeof args.embedding_enabled === "boolean" ? args.embedding_enabled : undefined,
|
|
722
|
-
enabled_by_user: typeof args.enabled_by_user === "boolean" ? args.enabled_by_user : undefined,
|
|
723
|
-
};
|
|
724
|
-
await client.updateAiEmployee(req);
|
|
725
|
-
return {
|
|
726
|
-
environment: client["env"].name,
|
|
727
|
-
success: true,
|
|
728
|
-
persona_id: personaId,
|
|
729
|
-
persona_name: existing.name,
|
|
730
|
-
updated_fields: {
|
|
731
|
-
name: !!args.name,
|
|
732
|
-
description: !!args.description,
|
|
733
|
-
proto_config: !!args.proto_config,
|
|
734
|
-
embedding_enabled: typeof args.embedding_enabled === "boolean",
|
|
735
|
-
enabled_by_user: typeof args.enabled_by_user === "boolean",
|
|
736
|
-
},
|
|
737
|
-
note: "For workflow changes, use deploy_workflow which provides validation and auto-fix.",
|
|
738
|
-
};
|
|
739
|
-
},
|
|
740
|
-
deploy_workflow: async (args) => {
|
|
741
|
-
const client = createClient(args.env);
|
|
742
|
-
const personaId = String(args.persona_id);
|
|
743
|
-
const validateFirst = args.validate_first !== false; // default true
|
|
744
|
-
const autoFix = args.auto_fix === true; // default false
|
|
745
|
-
const force = args.force === true;
|
|
746
|
-
const baseFingerprint = args.base_fingerprint;
|
|
747
|
-
// Get existing persona with full details
|
|
748
|
-
const persona = await client.getPersonaById(personaId);
|
|
749
|
-
if (!persona)
|
|
750
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
751
|
-
// STRICT: stale-state protection + required snapshot (same safety as workflow tool)
|
|
752
|
-
const currentFp = fingerprintPersona(persona);
|
|
753
|
-
if (!force && !baseFingerprint) {
|
|
754
|
-
return {
|
|
755
|
-
environment: client["env"].name,
|
|
756
|
-
success: false,
|
|
757
|
-
error: "base_fingerprint_required",
|
|
758
|
-
persona_id: personaId,
|
|
759
|
-
current_fingerprint: currentFp,
|
|
760
|
-
message: "base_fingerprint is required for deploy_workflow (stale-state protection). " +
|
|
761
|
-
"Re-fetch the latest state and retry.",
|
|
762
|
-
hint: "Prefer: workflow(mode='get') → workflow(mode='deploy', base_fingerprint='<fingerprint>', ...). " +
|
|
763
|
-
"Use force=true only for emergency overrides.",
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
if (!force && baseFingerprint && baseFingerprint !== currentFp) {
|
|
767
|
-
return {
|
|
768
|
-
environment: client["env"].name,
|
|
769
|
-
success: false,
|
|
770
|
-
error: "fingerprint_mismatch",
|
|
771
|
-
persona_id: personaId,
|
|
772
|
-
base_fingerprint: baseFingerprint,
|
|
773
|
-
current_fingerprint: currentFp,
|
|
774
|
-
message: "Persona changed since you last fetched it (fingerprint mismatch).",
|
|
775
|
-
hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. " +
|
|
776
|
-
"Use force=true only if you intend to overwrite out-of-band changes.",
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
// Required pre-deploy snapshot (local)
|
|
780
|
-
try {
|
|
781
|
-
const storage = createVersionStorage(process.cwd());
|
|
782
|
-
const engine = createVersionPolicyEngine(storage);
|
|
783
|
-
const snap = engine.forceCreateVersion(persona, {
|
|
784
|
-
environment: client["env"].name,
|
|
785
|
-
tenant_id: client["env"].name,
|
|
786
|
-
message: "Pre-deploy snapshot (deploy_workflow)",
|
|
787
|
-
created_by: "mcp-toolkit",
|
|
788
|
-
});
|
|
789
|
-
if (!snap.created || !snap.version) {
|
|
790
|
-
if (!force) {
|
|
791
|
-
return {
|
|
792
|
-
environment: client["env"].name,
|
|
793
|
-
success: false,
|
|
794
|
-
error: "pre_deploy_snapshot_failed",
|
|
795
|
-
persona_id: personaId,
|
|
796
|
-
details: snap.reason,
|
|
797
|
-
hint: "Fix local snapshot storage or retry with force=true for emergency override.",
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
catch (e) {
|
|
803
|
-
if (!force) {
|
|
804
|
-
return {
|
|
805
|
-
environment: client["env"].name,
|
|
806
|
-
success: false,
|
|
807
|
-
error: "pre_deploy_snapshot_failed",
|
|
808
|
-
persona_id: personaId,
|
|
809
|
-
details: e instanceof Error ? e.message : String(e),
|
|
810
|
-
hint: "Fix local snapshot storage or retry with force=true for emergency override.",
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
let workflowDef = args.workflow_def;
|
|
815
|
-
const protoConfig = args.proto_config;
|
|
816
|
-
if (!workflowDef && !protoConfig) {
|
|
817
|
-
throw new Error("At least one of workflow_def or proto_config must be provided");
|
|
818
|
-
}
|
|
819
|
-
// Sanitize workflow_def to prevent server-side crashes
|
|
820
|
-
if (workflowDef) {
|
|
821
|
-
// Fix enumTypes - remove entries with empty/missing names (causes server panic)
|
|
822
|
-
// EnumType structure in proto: { name: { name: { name: "string", namespaces: [] } }, options: [...] }
|
|
823
|
-
// The backend calls FlattenNamespacedName(enum.Name.Name) which panics if Name.Name is nil
|
|
824
|
-
const enumTypes = workflowDef.enumTypes;
|
|
825
|
-
if (Array.isArray(enumTypes)) {
|
|
826
|
-
const validEnumTypes = enumTypes.filter(e => {
|
|
827
|
-
// Navigate the nested structure: e.name.name.name
|
|
828
|
-
const outerName = e.name;
|
|
829
|
-
const innerName = outerName?.name;
|
|
830
|
-
const actualName = innerName?.name;
|
|
831
|
-
// Must have the full structure with a non-empty string name
|
|
832
|
-
return typeof actualName === "string" && actualName.trim().length > 0;
|
|
833
|
-
});
|
|
834
|
-
if (validEnumTypes.length > 0) {
|
|
835
|
-
workflowDef.enumTypes = validEnumTypes;
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
// Remove empty enumTypes entirely
|
|
839
|
-
delete workflowDef.enumTypes;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
// Ensure all actions have a 'name' field (node identifier)
|
|
843
|
-
const actions = workflowDef.actions;
|
|
844
|
-
if (Array.isArray(actions)) {
|
|
845
|
-
for (const action of actions) {
|
|
846
|
-
// If action has 'actionName' but not 'name', fix it
|
|
847
|
-
if (!action.name && action.actionName) {
|
|
848
|
-
action.name = action.actionName;
|
|
849
|
-
}
|
|
850
|
-
// Ensure name is a non-empty string
|
|
851
|
-
if (!action.name || (typeof action.name === "string" && action.name.trim().length === 0)) {
|
|
852
|
-
// Try to derive from action type
|
|
853
|
-
const actionType = action.action;
|
|
854
|
-
if (actionType?.name?.name) {
|
|
855
|
-
action.name = `${actionType.name.name}_${actions.indexOf(action)}`;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
// CRITICAL: Normalize action structure to include required empty fields
|
|
859
|
-
// The backend expects these fields to exist (even if empty) or it may 500
|
|
860
|
-
if (action.typeArguments === undefined) {
|
|
861
|
-
action.typeArguments = {};
|
|
862
|
-
}
|
|
863
|
-
if (action.tools === undefined) {
|
|
864
|
-
action.tools = [];
|
|
865
|
-
}
|
|
866
|
-
if (action.disableHumanInteraction === undefined) {
|
|
867
|
-
action.disableHumanInteraction = false;
|
|
868
|
-
}
|
|
869
|
-
// Ensure displaySettings exists and has required structure
|
|
870
|
-
if (!action.displaySettings) {
|
|
871
|
-
action.displaySettings = {
|
|
872
|
-
displayName: String(action.name || ""),
|
|
873
|
-
coordinates: { x: 0, y: 0 },
|
|
874
|
-
description: "",
|
|
875
|
-
showConfig: 0,
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
const ds = action.displaySettings;
|
|
880
|
-
if (ds.description === undefined)
|
|
881
|
-
ds.description = "";
|
|
882
|
-
if (ds.showConfig === undefined)
|
|
883
|
-
ds.showConfig = 0;
|
|
884
|
-
}
|
|
885
|
-
// Ensure inputs exists
|
|
886
|
-
if (action.inputs === undefined) {
|
|
887
|
-
action.inputs = {};
|
|
888
|
-
}
|
|
889
|
-
// Normalize runIf operator enum: backend proto expects numeric values (e.g. 1),
|
|
890
|
-
// but some JSON payloads use string enums (e.g. "OPERATOR_EQ") which can 500.
|
|
891
|
-
const runIf = action.runIf;
|
|
892
|
-
if (runIf && typeof runIf === "object") {
|
|
893
|
-
const op = runIf.operator;
|
|
894
|
-
if (typeof op === "string") {
|
|
895
|
-
const opMap = {
|
|
896
|
-
OPERATOR_EQ: 1,
|
|
897
|
-
OPERATOR_NEQ: 2,
|
|
898
|
-
OPERATOR_GT: 3,
|
|
899
|
-
OPERATOR_GTE: 4,
|
|
900
|
-
OPERATOR_LT: 5,
|
|
901
|
-
OPERATOR_LTE: 6,
|
|
902
|
-
OPERATOR_IN: 7,
|
|
903
|
-
OPERATOR_NOT_IN: 8,
|
|
904
|
-
};
|
|
905
|
-
if (opMap[op] !== undefined)
|
|
906
|
-
runIf.operator = opMap[op];
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
// Get existing workflow info
|
|
913
|
-
const existingWorkflow = persona.workflow_def;
|
|
914
|
-
const existingWorkflowId = persona.workflow_id;
|
|
915
|
-
// Copy missing top-level workflow fields from existing workflow.
|
|
916
|
-
// Some backends are strict about presence of these keys.
|
|
917
|
-
if (workflowDef && existingWorkflow) {
|
|
918
|
-
const copyIfMissing = (k) => {
|
|
919
|
-
if (workflowDef[k] === undefined && existingWorkflow[k] !== undefined) {
|
|
920
|
-
workflowDef[k] = JSON.parse(JSON.stringify(existingWorkflow[k]));
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
copyIfMissing("workflowInputs");
|
|
924
|
-
copyIfMissing("namedResults");
|
|
925
|
-
copyIfMissing("displayName");
|
|
926
|
-
copyIfMissing("description");
|
|
927
|
-
copyIfMissing("namedResultsEditable");
|
|
928
|
-
copyIfMissing("namedResultsEnabled");
|
|
929
|
-
copyIfMissing("edges");
|
|
930
|
-
}
|
|
931
|
-
// Determine deployment strategy
|
|
932
|
-
const hasExistingWorkflow = !!existingWorkflowId;
|
|
933
|
-
let deploymentMethod = hasExistingWorkflow ? "direct_api" : "autobuilder";
|
|
934
|
-
// === WORKFLOW ID TRANSFORMATION ===
|
|
935
|
-
// If workflow was generated for a different persona, transform IDs to target persona
|
|
936
|
-
// (No brownfield merging - we do full workflow replacement)
|
|
937
|
-
if (workflowDef && hasExistingWorkflow && existingWorkflow) {
|
|
938
|
-
// Extract source persona ID from incoming workflow (if any)
|
|
939
|
-
const incomingWfName = workflowDef.workflowName;
|
|
940
|
-
const sourcePersonaId = incomingWfName?.name?.namespaces?.[2]; // Usually at index 2: ["ema", "templates", "<persona_id>"]
|
|
941
|
-
if (sourcePersonaId && sourcePersonaId !== personaId) {
|
|
942
|
-
// Workflow was generated for a different persona - transform IDs
|
|
943
|
-
workflowDef = transformWorkflowForTarget(workflowDef, sourcePersonaId, personaId);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
else if (workflowDef && hasExistingWorkflow && !existingWorkflow) {
|
|
947
|
-
// Has workflow_id but no workflow_def (edge case) - construct workflowName from workflow_id
|
|
948
|
-
// workflow_id format: "ema.templates.<persona_id>.default" or similar
|
|
949
|
-
const parts = existingWorkflowId.split(".");
|
|
950
|
-
if (parts.length >= 3) {
|
|
951
|
-
workflowDef.workflowName = {
|
|
952
|
-
name: {
|
|
953
|
-
namespaces: parts.slice(0, -1),
|
|
954
|
-
name: parts[parts.length - 1],
|
|
955
|
-
},
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
// Track fixes applied
|
|
960
|
-
const appliedFixes = [];
|
|
961
|
-
let fixAttempted = false;
|
|
962
|
-
// Validation is now done by backend when workflow is deployed
|
|
963
|
-
// LLM should use ema://rules/* for pre-validation guidance
|
|
964
|
-
const validationResults = { valid: true, issues: [] };
|
|
965
|
-
if (validateFirst && workflowDef) {
|
|
966
|
-
// DEPRECATED: MCP no longer pre-validates workflows
|
|
967
|
-
// Backend validation happens on deploy
|
|
968
|
-
// LLM uses ema://rules/anti-patterns for analysis
|
|
969
|
-
console.warn("[DEPRECATED] validateFirst is deprecated - backend validates on deploy");
|
|
970
|
-
if (autoFix) {
|
|
971
|
-
// Auto-fix is removed - return guidance for LLM
|
|
972
|
-
return {
|
|
973
|
-
environment: client["env"].name,
|
|
974
|
-
success: false,
|
|
975
|
-
persona_id: personaId,
|
|
976
|
-
persona_name: persona.name,
|
|
977
|
-
_deprecation_notice: "autoFix is deprecated. Use LLM analysis with ema://rules/* instead.",
|
|
978
|
-
_guidance: [
|
|
979
|
-
"1. Fetch ema://rules/anti-patterns",
|
|
980
|
-
"2. Analyze workflow against rules",
|
|
981
|
-
"3. Make structured modifications",
|
|
982
|
-
"4. Deploy via workflow(mode='deploy')",
|
|
983
|
-
],
|
|
984
|
-
applied_fixes: [],
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
// If validation failed and not forcing, return the issues
|
|
989
|
-
if (!validationResults.valid) {
|
|
990
|
-
return {
|
|
991
|
-
environment: client["env"].name,
|
|
992
|
-
success: false,
|
|
993
|
-
persona_id: personaId,
|
|
994
|
-
persona_name: persona.name,
|
|
995
|
-
validation_failed: true,
|
|
996
|
-
issues: validationResults.issues,
|
|
997
|
-
auto_fix_attempted: fixAttempted,
|
|
998
|
-
fixes_applied: appliedFixes.filter(f => f.applied),
|
|
999
|
-
fixes_failed: appliedFixes.filter(f => !f.applied),
|
|
1000
|
-
remaining_issues: validationResults.issues.length,
|
|
1001
|
-
hint: autoFix
|
|
1002
|
-
? "Some issues could not be auto-fixed. Review the remaining issues and fix manually, or set validate_first=false to skip validation (not recommended)"
|
|
1003
|
-
: "Fix the issues above, enable auto_fix=true for automatic fixes, or set validate_first=false to skip validation (not recommended)",
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
// Merge proto_config using SDK function (deep merges widgets by name)
|
|
1007
|
-
const mergedProtoConfig = mergeProtoConfig(persona.proto_config, protoConfig);
|
|
1008
|
-
// Auto-fix malformed HITL runIf patterns BEFORE deploy (H1: production-grade fix)
|
|
1009
|
-
// Pattern: "hitl_status_HITL Success" should be output="hitl_status", enumValue="HITL Success"
|
|
1010
|
-
let hitlFixCount = 0;
|
|
1011
|
-
if (workflowDef) {
|
|
1012
|
-
const wfActions = workflowDef.actions ?? [];
|
|
1013
|
-
for (const action of wfActions) {
|
|
1014
|
-
const runIfAction = action.runIf;
|
|
1015
|
-
if (!runIfAction)
|
|
1016
|
-
continue;
|
|
1017
|
-
const lhsAction = runIfAction.lhs;
|
|
1018
|
-
const rhsAction = runIfAction.rhs;
|
|
1019
|
-
if (lhsAction?.actionOutput && rhsAction?.inline) {
|
|
1020
|
-
const actionOutputField = lhsAction.actionOutput;
|
|
1021
|
-
const inlineRhsField = rhsAction.inline;
|
|
1022
|
-
const outputStr = String(actionOutputField.output ?? "");
|
|
1023
|
-
// Detect malformed HITL patterns: "hitl_status_HITL Success" or "hitl_status HITL Success"
|
|
1024
|
-
const hitlPatternMatch = outputStr.match(/^hitl_status[_\s]?(HITL[ _]?(?:Success|Failure))$/i);
|
|
1025
|
-
if (hitlPatternMatch) {
|
|
1026
|
-
// Extract and normalize the enum value
|
|
1027
|
-
let correctedEnumVal = hitlPatternMatch[1].replace(/_/g, " ");
|
|
1028
|
-
if (correctedEnumVal.toLowerCase().includes("success")) {
|
|
1029
|
-
correctedEnumVal = "HITL Success";
|
|
1030
|
-
}
|
|
1031
|
-
else if (correctedEnumVal.toLowerCase().includes("failure")) {
|
|
1032
|
-
correctedEnumVal = "HITL Failure";
|
|
1033
|
-
}
|
|
1034
|
-
// Apply the fix
|
|
1035
|
-
actionOutputField.output = "hitl_status";
|
|
1036
|
-
inlineRhsField.enumValue = correctedEnumVal;
|
|
1037
|
-
hitlFixCount++;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
// Build update request
|
|
1043
|
-
const req = {
|
|
1044
|
-
persona_id: personaId,
|
|
1045
|
-
proto_config: mergedProtoConfig,
|
|
1046
|
-
workflow: workflowDef,
|
|
1047
|
-
};
|
|
1048
|
-
// Deployment attempt with automatic fallback
|
|
1049
|
-
let deployedVia = "direct_api";
|
|
1050
|
-
let autobuilderResult;
|
|
1051
|
-
if (deploymentMethod === "direct_api") {
|
|
1052
|
-
try {
|
|
1053
|
-
await client.updateAiEmployee(req);
|
|
1054
|
-
}
|
|
1055
|
-
catch (err) {
|
|
1056
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1057
|
-
// If direct API fails due to "no existing workflow", try Auto Builder
|
|
1058
|
-
if (errorMessage.includes("Cannot set persona workflow without existing workflow") && workflowDef) {
|
|
1059
|
-
deploymentMethod = "autobuilder";
|
|
1060
|
-
}
|
|
1061
|
-
else if (errorMessage.includes("Workflow name does not match")) {
|
|
1062
|
-
// This shouldn't happen with our name sync, but handle gracefully
|
|
1063
|
-
throw new Error(`Workflow deployment failed: The workflow structure may be incompatible. ` +
|
|
1064
|
-
`Please use the Ema UI Auto Builder to make changes to this persona's workflow. ` +
|
|
1065
|
-
`(Technical: ${errorMessage})`);
|
|
1066
|
-
}
|
|
1067
|
-
else if ((errorMessage.toLowerCase().includes("internal server error") || errorMessage.includes("500")) && workflowDef) {
|
|
1068
|
-
// 500 error - attempt Autobuilder fallback
|
|
1069
|
-
deploymentMethod = "autobuilder";
|
|
1070
|
-
}
|
|
1071
|
-
else {
|
|
1072
|
-
// Other API errors - surface clearly
|
|
1073
|
-
throw new Error(`Workflow deployment failed: ${errorMessage}`);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
// Auto Builder fallback for personas without existing workflows
|
|
1078
|
-
if (deploymentMethod === "autobuilder" && workflowDef) {
|
|
1079
|
-
try {
|
|
1080
|
-
// Generate a prompt that asks the Auto Builder to deploy this specific workflow
|
|
1081
|
-
const workflowSummary = summarizeWorkflow(workflowDef);
|
|
1082
|
-
const prompt = `Deploy this workflow to the persona. The workflow has the following structure:\n\n${workflowSummary}\n\nPlease create and save this workflow.`;
|
|
1083
|
-
// Use the iterate workflow method which handles Auto Builder discovery
|
|
1084
|
-
autobuilderResult = await client.iterateWorkflow(personaId, prompt, { newConversation: true });
|
|
1085
|
-
deployedVia = "autobuilder";
|
|
1086
|
-
// Also update proto_config if provided (Auto Builder may not handle this)
|
|
1087
|
-
if (protoConfig) {
|
|
1088
|
-
await client.updateAiEmployee({
|
|
1089
|
-
persona_id: personaId,
|
|
1090
|
-
proto_config: mergedProtoConfig,
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
catch (autoErr) {
|
|
1095
|
-
const autoErrorMessage = autoErr instanceof Error ? autoErr.message : String(autoErr);
|
|
1096
|
-
// If Auto Builder also fails, provide clear guidance
|
|
1097
|
-
if (autoErrorMessage.includes("No Autobuilder persona found")) {
|
|
1098
|
-
throw new Error(`Cannot deploy workflow: This persona has no existing workflow, and the Ema Auto Builder is not available in this tenant. ` +
|
|
1099
|
-
`Please contact your Ema administrator to enable the Auto Builder, or create a new persona from a workflow template.`);
|
|
1100
|
-
}
|
|
1101
|
-
throw new Error(`Workflow deployment via Auto Builder failed: ${autoErrorMessage}`);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
// Build success note
|
|
1105
|
-
let successNote = "Workflow deployed successfully. Test in the Ema simulator to verify behavior.";
|
|
1106
|
-
if (deployedVia === "autobuilder") {
|
|
1107
|
-
successNote = "Workflow deployed via Ema Auto Builder. Test in the Ema simulator to verify behavior.";
|
|
1108
|
-
}
|
|
1109
|
-
else if (fixAttempted && appliedFixes.some(f => f.applied)) {
|
|
1110
|
-
successNote = `Workflow deployed successfully with ${appliedFixes.filter(f => f.applied).length} auto-fix(es) applied. Test in the Ema simulator to verify behavior.`;
|
|
1111
|
-
}
|
|
1112
|
-
else if (!workflowDef) {
|
|
1113
|
-
successNote = "Proto config updated successfully.";
|
|
1114
|
-
}
|
|
1115
|
-
return {
|
|
1116
|
-
environment: client["env"].name,
|
|
1117
|
-
success: true,
|
|
1118
|
-
persona_id: personaId,
|
|
1119
|
-
persona_name: persona.name,
|
|
1120
|
-
deployed: {
|
|
1121
|
-
workflow_def: !!workflowDef,
|
|
1122
|
-
proto_config: !!protoConfig,
|
|
1123
|
-
},
|
|
1124
|
-
deployment_method: deployedVia,
|
|
1125
|
-
validation_passed: validationResults.valid,
|
|
1126
|
-
auto_fix_applied: fixAttempted && appliedFixes.some(f => f.applied),
|
|
1127
|
-
fixes_applied: appliedFixes.filter(f => f.applied),
|
|
1128
|
-
autobuilder_response: autobuilderResult?.response,
|
|
1129
|
-
note: successNote,
|
|
1130
|
-
};
|
|
1131
|
-
},
|
|
1132
|
-
optimize_workflow: async (args) => {
|
|
1133
|
-
const client = createClient(args.env);
|
|
1134
|
-
const identifier = args.identifier ? String(args.identifier) : undefined;
|
|
1135
|
-
const targetPersonaId = args.persona_id ? String(args.persona_id) : undefined;
|
|
1136
|
-
const prompt = args.prompt ? String(args.prompt) : undefined;
|
|
1137
|
-
const personaType = args.type ?? "chat";
|
|
1138
|
-
const preview = args.preview === true;
|
|
1139
|
-
// Validate inputs
|
|
1140
|
-
if (!identifier && !targetPersonaId && !prompt) {
|
|
1141
|
-
throw new Error('Provide either: identifier (to fix existing), or persona_id + prompt (to enhance existing)');
|
|
1142
|
-
}
|
|
1143
|
-
let persona = null;
|
|
1144
|
-
let workflowDef;
|
|
1145
|
-
let personaId;
|
|
1146
|
-
let enhancementPrompt = prompt; // Store prompt for enhancement logging
|
|
1147
|
-
// === ALWAYS START BY FETCHING EXISTING WORKFLOW ===
|
|
1148
|
-
// Brownfield: fix existing + apply enhancements from prompt
|
|
1149
|
-
// The prompt describes what to ADD or CHANGE, not a complete replacement
|
|
1150
|
-
const lookupId = identifier ?? targetPersonaId;
|
|
1151
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(lookupId);
|
|
1152
|
-
if (isUUID) {
|
|
1153
|
-
persona = await client.getPersonaById(lookupId);
|
|
1154
|
-
}
|
|
1155
|
-
else {
|
|
1156
|
-
// Search by name
|
|
1157
|
-
const personas = await client.getPersonasForTenant();
|
|
1158
|
-
const match = personas.find((p) => p.name?.toLowerCase() === lookupId.toLowerCase() ||
|
|
1159
|
-
p.name?.toLowerCase().includes(lookupId.toLowerCase()));
|
|
1160
|
-
if (match) {
|
|
1161
|
-
persona = await client.getPersonaById(match.id);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
if (!persona) {
|
|
1165
|
-
throw new Error(`AI Employee "${lookupId}" not found. Check the name or ID.`);
|
|
1166
|
-
}
|
|
1167
|
-
personaId = persona.id;
|
|
1168
|
-
workflowDef = persona.workflow_def;
|
|
1169
|
-
if (!workflowDef) {
|
|
1170
|
-
return {
|
|
1171
|
-
success: false,
|
|
1172
|
-
persona: persona.name,
|
|
1173
|
-
status: "⚠️ No Workflow",
|
|
1174
|
-
message: "This AI Employee has no workflow. Use prompt parameter to generate one: optimize_workflow(persona_id=\"...\", prompt=\"description of what it should do\")",
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
// DEPRECATED: MCP no longer pre-analyzes workflows
|
|
1178
|
-
// LLM should use ema://rules/* for analysis
|
|
1179
|
-
console.warn("[DEPRECATED] optimize_workflow tool is deprecated - use LLM analysis with ema://rules/*");
|
|
1180
|
-
const nodes = parseWorkflowDef(workflowDef);
|
|
1181
|
-
return {
|
|
1182
|
-
success: true,
|
|
1183
|
-
persona: persona?.name ?? "Unknown",
|
|
1184
|
-
persona_id: personaId,
|
|
1185
|
-
status: "DEPRECATED - use LLM analysis",
|
|
1186
|
-
node_count: nodes.length,
|
|
1187
|
-
workflow_def: workflowDef,
|
|
1188
|
-
_deprecation_notice: {
|
|
1189
|
-
message: "optimize_workflow is deprecated. MCP does not pre-compute issues.",
|
|
1190
|
-
new_workflow: [
|
|
1191
|
-
"1. Fetch rules: ema://rules/anti-patterns, ema://rules/optimizations",
|
|
1192
|
-
"2. Apply rules to find issues (LLM does this, not MCP)",
|
|
1193
|
-
"3. Make structured modifications based on your analysis",
|
|
1194
|
-
"4. Deploy via workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
1195
|
-
],
|
|
1196
|
-
},
|
|
1197
|
-
};
|
|
1198
|
-
},
|
|
1199
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1200
|
-
// Action Handlers (Consolidated)
|
|
1201
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1202
|
-
get_workflow_action: async (args) => {
|
|
1203
|
-
const client = createClient(args.env);
|
|
1204
|
-
const identifier = String(args.identifier);
|
|
1205
|
-
const actions = await client.listActions();
|
|
1206
|
-
// Try ID first, then name
|
|
1207
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
1208
|
-
let action = isUUID
|
|
1209
|
-
? actions.find((a) => a.id === identifier)
|
|
1210
|
-
: actions.find((a) => a.name?.toLowerCase() === identifier.toLowerCase());
|
|
1211
|
-
if (!action) {
|
|
1212
|
-
throw new Error(`Action not found: ${identifier} (searched by ${isUUID ? "ID" : "name"})`);
|
|
1213
|
-
}
|
|
1214
|
-
return { environment: client["env"].name, action };
|
|
1215
|
-
},
|
|
1216
|
-
find_workflow_actions: async (args) => {
|
|
1217
|
-
const client = createClient(args.env);
|
|
1218
|
-
// Handle list_categories request
|
|
1219
|
-
if (args.list_categories === true) {
|
|
1220
|
-
const actions = await client.listActions();
|
|
1221
|
-
const categoryMap = new Map();
|
|
1222
|
-
for (const a of actions) {
|
|
1223
|
-
const cat = a.category ?? "uncategorized";
|
|
1224
|
-
categoryMap.set(cat, (categoryMap.get(cat) ?? 0) + 1);
|
|
1225
|
-
}
|
|
1226
|
-
return {
|
|
1227
|
-
environment: client["env"].name,
|
|
1228
|
-
categories: Array.from(categoryMap.entries()).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count),
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
// Handle persona/workflow scope
|
|
1232
|
-
if (args.persona_id) {
|
|
1233
|
-
const personaId = String(args.persona_id);
|
|
1234
|
-
const personas = await client.getPersonasForTenant();
|
|
1235
|
-
const persona = personas.find((p) => p.id === personaId);
|
|
1236
|
-
if (!persona)
|
|
1237
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1238
|
-
if (!persona.workflow_id) {
|
|
1239
|
-
return {
|
|
1240
|
-
environment: client["env"].name,
|
|
1241
|
-
persona_id: personaId,
|
|
1242
|
-
persona_name: persona.name,
|
|
1243
|
-
error: "AI Employee has no workflow",
|
|
1244
|
-
actions: [],
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
const actionIds = await client.listActionsFromWorkflow(persona.workflow_id);
|
|
1248
|
-
const allActions = await client.listActions();
|
|
1249
|
-
const actionIdSet = new Set(actionIds);
|
|
1250
|
-
const actions = allActions.filter(a => actionIdSet.has(a.id));
|
|
1251
|
-
return {
|
|
1252
|
-
environment: client["env"].name,
|
|
1253
|
-
persona_id: personaId,
|
|
1254
|
-
persona_name: persona.name,
|
|
1255
|
-
workflow_id: persona.workflow_id,
|
|
1256
|
-
count: actions.length,
|
|
1257
|
-
actions: actions.map((a) => ({
|
|
1258
|
-
id: a.id, name: a.name, description: a.description,
|
|
1259
|
-
category: a.category, inputs: a.inputs, outputs: a.outputs,
|
|
1260
|
-
})),
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
if (args.workflow_id) {
|
|
1264
|
-
const workflowId = String(args.workflow_id);
|
|
1265
|
-
const actionIds = await client.listActionsFromWorkflow(workflowId);
|
|
1266
|
-
const allActions = await client.listActions();
|
|
1267
|
-
const actionIdSet = new Set(actionIds);
|
|
1268
|
-
const actions = allActions.filter(a => actionIdSet.has(a.id));
|
|
1269
|
-
return {
|
|
1270
|
-
environment: client["env"].name,
|
|
1271
|
-
workflow_id: workflowId,
|
|
1272
|
-
count: actions.length,
|
|
1273
|
-
actions: actions.map((a) => ({
|
|
1274
|
-
id: a.id, name: a.name, description: a.description,
|
|
1275
|
-
category: a.category, inputs: a.inputs, outputs: a.outputs,
|
|
1276
|
-
})),
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
// Default: search all actions
|
|
1280
|
-
let actions = await client.listActions();
|
|
1281
|
-
if (args.query) {
|
|
1282
|
-
const q = String(args.query).toLowerCase();
|
|
1283
|
-
actions = actions.filter((a) => a.name?.toLowerCase().includes(q));
|
|
1284
|
-
}
|
|
1285
|
-
if (args.category) {
|
|
1286
|
-
const f = String(args.category).toLowerCase();
|
|
1287
|
-
actions = actions.filter((a) => a.category?.toLowerCase() === f);
|
|
1288
|
-
}
|
|
1289
|
-
if (typeof args.enabled === "boolean") {
|
|
1290
|
-
actions = actions.filter((a) => a.enabled === args.enabled);
|
|
1291
|
-
}
|
|
1292
|
-
const limit = typeof args.limit === "number" ? args.limit : 100;
|
|
1293
|
-
actions = actions.slice(0, limit);
|
|
1294
|
-
return {
|
|
1295
|
-
environment: client["env"].name,
|
|
1296
|
-
count: actions.length,
|
|
1297
|
-
actions: actions.map((a) => ({
|
|
1298
|
-
id: a.id, name: a.name, description: a.description,
|
|
1299
|
-
category: a.category, enabled: a.enabled, tags: a.tags,
|
|
1300
|
-
})),
|
|
1301
|
-
};
|
|
1302
|
-
},
|
|
1303
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1304
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1305
|
-
// Diagnostics & Comparison
|
|
1306
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1307
|
-
compare_ai_employees: async (args) => {
|
|
1308
|
-
const env1 = args.env_1 ?? getDefaultEnvName();
|
|
1309
|
-
const env2 = args.env_2 ?? env1;
|
|
1310
|
-
const id1 = String(args.persona_id_1);
|
|
1311
|
-
const id2 = String(args.persona_id_2);
|
|
1312
|
-
const client1 = createClient(env1);
|
|
1313
|
-
const client2 = env1 === env2 ? client1 : createClient(env2);
|
|
1314
|
-
const [p1, p2] = await Promise.all([
|
|
1315
|
-
client1.getPersonaById(id1),
|
|
1316
|
-
client2.getPersonaById(id2),
|
|
1317
|
-
]);
|
|
1318
|
-
if (!p1)
|
|
1319
|
-
throw new Error(`AI Employee not found: ${id1} in ${env1}`);
|
|
1320
|
-
if (!p2)
|
|
1321
|
-
throw new Error(`AI Employee not found: ${id2} in ${env2}`);
|
|
1322
|
-
const fp1 = fingerprintPersona(p1);
|
|
1323
|
-
const fp2 = fingerprintPersona(p2);
|
|
1324
|
-
const compareFields = ["name", "description", "status", "trigger_type", "access_level", "embedding_enabled", "template_id", "workflow_id"];
|
|
1325
|
-
const differences = [];
|
|
1326
|
-
for (const field of compareFields) {
|
|
1327
|
-
const val1 = p1[field] ?? p1[field === "template_id" ? "templateId" : field];
|
|
1328
|
-
const val2 = p2[field] ?? p2[field === "template_id" ? "templateId" : field];
|
|
1329
|
-
if (JSON.stringify(val1) !== JSON.stringify(val2)) {
|
|
1330
|
-
differences.push({ field, value_1: val1, value_2: val2 });
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
if (JSON.stringify(p1.proto_config ?? {}) !== JSON.stringify(p2.proto_config ?? {})) {
|
|
1334
|
-
differences.push({ field: "proto_config", value_1: "(differs)", value_2: "(differs)" });
|
|
1335
|
-
}
|
|
1336
|
-
if (JSON.stringify(p1.welcome_messages ?? {}) !== JSON.stringify(p2.welcome_messages ?? {})) {
|
|
1337
|
-
differences.push({ field: "welcome_messages", value_1: "(differs)", value_2: "(differs)" });
|
|
1338
|
-
}
|
|
1339
|
-
return {
|
|
1340
|
-
persona_1: { id: id1, env: env1, name: p1.name, fingerprint: fp1 },
|
|
1341
|
-
persona_2: { id: id2, env: env2, name: p2.name, fingerprint: fp2 },
|
|
1342
|
-
fingerprints_match: fp1 === fp2,
|
|
1343
|
-
difference_count: differences.length,
|
|
1344
|
-
differences,
|
|
1345
|
-
};
|
|
1346
|
-
},
|
|
1347
|
-
list_ai_employee_templates: async (args) => {
|
|
1348
|
-
const client = createClient(args.env);
|
|
1349
|
-
const personas = await client.getPersonasForTenant();
|
|
1350
|
-
const templateMap = new Map();
|
|
1351
|
-
for (const p of personas) {
|
|
1352
|
-
const templateId = p.template_id ?? p.templateId ?? "unknown";
|
|
1353
|
-
const existing = templateMap.get(templateId) ?? { count: 0, names: [] };
|
|
1354
|
-
existing.count++;
|
|
1355
|
-
if (p.name && existing.names.length < 3)
|
|
1356
|
-
existing.names.push(p.name);
|
|
1357
|
-
templateMap.set(templateId, existing);
|
|
1358
|
-
}
|
|
1359
|
-
const templates = Array.from(templateMap.entries())
|
|
1360
|
-
.map(([template_id, data]) => ({ template_id, usage_count: data.count, examples: data.names }))
|
|
1361
|
-
.sort((a, b) => b.usage_count - a.usage_count);
|
|
1362
|
-
return {
|
|
1363
|
-
environment: client["env"].name,
|
|
1364
|
-
total_ai_employees: personas.length,
|
|
1365
|
-
template_count: templates.length,
|
|
1366
|
-
templates,
|
|
1367
|
-
};
|
|
1368
|
-
},
|
|
1369
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1370
|
-
// CONSOLIDATED SYNC HANDLERS
|
|
1371
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1372
|
-
sync: async (args) => {
|
|
1373
|
-
const targetEnv = String(args.target_env);
|
|
1374
|
-
const sourceEnv = args.source_env ? String(args.source_env) : getDefaultEnvName();
|
|
1375
|
-
const dryRun = args.dry_run === true;
|
|
1376
|
-
const includeStatus = args.include_status === true;
|
|
1377
|
-
const scope = args.scope === "all" ? "all" : "one";
|
|
1378
|
-
const identifier = args.identifier ? String(args.identifier) : undefined;
|
|
1379
|
-
// Sync all tagged personas
|
|
1380
|
-
if (scope === "all" || !identifier) {
|
|
1381
|
-
const sdk = getSyncSDK();
|
|
1382
|
-
if (sdk) {
|
|
1383
|
-
try {
|
|
1384
|
-
const result = await sdk.runSync();
|
|
1385
|
-
return { success: true, mode: "config", ...result };
|
|
1386
|
-
}
|
|
1387
|
-
finally {
|
|
1388
|
-
sdk.close();
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
// Config-less mode
|
|
1392
|
-
try {
|
|
1393
|
-
const result = await directSyncAll({ targetEnv, dryRun });
|
|
1394
|
-
return { success: true, mode: "tags", ...result };
|
|
1395
|
-
}
|
|
1396
|
-
catch (e) {
|
|
1397
|
-
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
// Sync single persona
|
|
1401
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
1402
|
-
const behavior = resolveSyncBehavior({
|
|
1403
|
-
personaName: isUUID ? undefined : identifier,
|
|
1404
|
-
targetEnv,
|
|
1405
|
-
overrides: {
|
|
1406
|
-
dry_run: dryRun ? true : undefined,
|
|
1407
|
-
sync_status: includeStatus ? true : undefined,
|
|
1408
|
-
},
|
|
1409
|
-
});
|
|
1410
|
-
try {
|
|
1411
|
-
const result = isUUID
|
|
1412
|
-
? await directSyncPersonaById({
|
|
1413
|
-
personaId: identifier,
|
|
1414
|
-
sourceEnv,
|
|
1415
|
-
targetEnv,
|
|
1416
|
-
dryRun: behavior.dry_run,
|
|
1417
|
-
syncStatus: behavior.sync_status,
|
|
1418
|
-
})
|
|
1419
|
-
: await directSyncPersona({
|
|
1420
|
-
name: identifier,
|
|
1421
|
-
sourceEnv,
|
|
1422
|
-
targetEnv,
|
|
1423
|
-
dryRun: behavior.dry_run,
|
|
1424
|
-
syncStatus: behavior.sync_status,
|
|
1425
|
-
});
|
|
1426
|
-
return { ...result, resolved_behavior: behavior };
|
|
1427
|
-
}
|
|
1428
|
-
catch (e) {
|
|
1429
|
-
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
1430
|
-
}
|
|
1431
|
-
},
|
|
1432
|
-
sync_info: async (args) => {
|
|
1433
|
-
const client = args.env ? createClient(args.env) : undefined;
|
|
1434
|
-
// Check if persona is synced
|
|
1435
|
-
if (args.persona_id) {
|
|
1436
|
-
if (!client)
|
|
1437
|
-
throw new Error("env required when checking persona sync status");
|
|
1438
|
-
const personaId = String(args.persona_id);
|
|
1439
|
-
const personas = await client.getPersonasForTenant();
|
|
1440
|
-
const persona = personas.find((p) => p.id === personaId);
|
|
1441
|
-
if (!persona)
|
|
1442
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1443
|
-
const meta = client.getSyncMetadata(persona);
|
|
1444
|
-
return {
|
|
1445
|
-
environment: client["env"].name,
|
|
1446
|
-
persona_id: personaId,
|
|
1447
|
-
persona_name: persona.name,
|
|
1448
|
-
is_synced: !!meta,
|
|
1449
|
-
sync_metadata: meta,
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
// Check by persona name
|
|
1453
|
-
if (args.persona_name) {
|
|
1454
|
-
const sdk = getSyncSDK();
|
|
1455
|
-
if (!sdk)
|
|
1456
|
-
return { error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG." };
|
|
1457
|
-
try {
|
|
1458
|
-
const persona = await sdk.getMasterPersonaByName(String(args.persona_name));
|
|
1459
|
-
if (!persona)
|
|
1460
|
-
return { error: `Persona not found: ${args.persona_name}` };
|
|
1461
|
-
return await sdk.getPersonaSyncStatus(persona.id);
|
|
1462
|
-
}
|
|
1463
|
-
finally {
|
|
1464
|
-
sdk.close();
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
// List all synced personas
|
|
1468
|
-
if (args.list_synced === true) {
|
|
1469
|
-
if (!client)
|
|
1470
|
-
throw new Error("env required when listing synced personas");
|
|
1471
|
-
const personas = await client.getPersonasForTenant();
|
|
1472
|
-
const masterEnvFilter = args.master_env ? String(args.master_env).toLowerCase() : undefined;
|
|
1473
|
-
const synced = [];
|
|
1474
|
-
for (const p of personas) {
|
|
1475
|
-
const meta = client.getSyncMetadata(p);
|
|
1476
|
-
if (meta) {
|
|
1477
|
-
if (masterEnvFilter && meta.master_env.toLowerCase() !== masterEnvFilter)
|
|
1478
|
-
continue;
|
|
1479
|
-
synced.push({ persona_id: p.id, persona_name: p.name, sync_metadata: meta });
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
return { environment: client["env"].name, count: synced.length, synced_personas: synced };
|
|
1483
|
-
}
|
|
1484
|
-
// Default: return overall sync config/status
|
|
1485
|
-
const sdk = getSyncSDK();
|
|
1486
|
-
const options = args.include_options === true ? loadSyncOptions() : undefined;
|
|
1487
|
-
if (!sdk) {
|
|
1488
|
-
return {
|
|
1489
|
-
configured: false,
|
|
1490
|
-
error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG.",
|
|
1491
|
-
options,
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
try {
|
|
1495
|
-
const master = sdk.getMasterEnvironment();
|
|
1496
|
-
const envs = sdk.getEnvironments();
|
|
1497
|
-
const personas = await sdk.listMasterPersonas();
|
|
1498
|
-
return {
|
|
1499
|
-
configured: true,
|
|
1500
|
-
master_environment: { name: master.name, url: master.baseUrl },
|
|
1501
|
-
target_environments: envs.filter((e) => !e.isMaster).map((e) => ({ name: e.name, url: e.baseUrl })),
|
|
1502
|
-
total_personas: personas.length,
|
|
1503
|
-
options,
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
finally {
|
|
1507
|
-
sdk.close();
|
|
1508
|
-
}
|
|
1509
|
-
},
|
|
1510
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1511
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1512
|
-
// Auto Builder Knowledge Handlers
|
|
1513
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1514
|
-
list_auto_builder_agents: async (args) => {
|
|
1515
|
-
const category = args.category;
|
|
1516
|
-
const agents = category ? getAgentsByCategory(category) : AGENT_CATALOG;
|
|
1517
|
-
return {
|
|
1518
|
-
count: agents.length,
|
|
1519
|
-
category: category ?? "all",
|
|
1520
|
-
agents: agents.map(a => ({
|
|
1521
|
-
action_name: a.actionName,
|
|
1522
|
-
display_name: a.displayName,
|
|
1523
|
-
category: a.category,
|
|
1524
|
-
description: a.description,
|
|
1525
|
-
when_to_use: a.whenToUse,
|
|
1526
|
-
inputs: a.inputs.map(i => i.name),
|
|
1527
|
-
outputs: a.outputs.map(o => o.name),
|
|
1528
|
-
has_critical_rules: !!a.criticalRules?.length,
|
|
1529
|
-
})),
|
|
1530
|
-
};
|
|
1531
|
-
},
|
|
1532
|
-
get_auto_builder_agent: async (args) => {
|
|
1533
|
-
const actionName = String(args.action_name);
|
|
1534
|
-
const agent = getAgentByName(actionName);
|
|
1535
|
-
if (!agent) {
|
|
1536
|
-
const available = AGENT_CATALOG.slice(0, 15).map(a => a.actionName);
|
|
1537
|
-
return {
|
|
1538
|
-
error: `Agent not found: ${actionName}`,
|
|
1539
|
-
available_examples: available,
|
|
1540
|
-
hint: "Use list_auto_builder_agents to see all available agents",
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
return {
|
|
1544
|
-
action_name: agent.actionName,
|
|
1545
|
-
display_name: agent.displayName,
|
|
1546
|
-
category: agent.category,
|
|
1547
|
-
description: agent.description,
|
|
1548
|
-
inputs: agent.inputs,
|
|
1549
|
-
outputs: agent.outputs,
|
|
1550
|
-
critical_rules: agent.criticalRules ?? [],
|
|
1551
|
-
when_to_use: agent.whenToUse,
|
|
1552
|
-
when_not_to_use: agent.whenNotToUse,
|
|
1553
|
-
example: agent.example,
|
|
1554
|
-
};
|
|
1555
|
-
},
|
|
1556
|
-
suggest_agents_for_use_case: async (args) => {
|
|
1557
|
-
const useCase = String(args.use_case);
|
|
1558
|
-
const suggestions = suggestAgentsForUseCase(useCase);
|
|
1559
|
-
return {
|
|
1560
|
-
use_case: useCase,
|
|
1561
|
-
suggested_agent_count: suggestions.length,
|
|
1562
|
-
suggested_agents: suggestions.map(a => ({
|
|
1563
|
-
action_name: a.actionName,
|
|
1564
|
-
display_name: a.displayName,
|
|
1565
|
-
category: a.category,
|
|
1566
|
-
why: a.whenToUse,
|
|
1567
|
-
inputs: a.inputs.map(i => `${i.name} (${i.type})`),
|
|
1568
|
-
outputs: a.outputs.map(o => `${o.name} (${o.type})`),
|
|
1569
|
-
})),
|
|
1570
|
-
suggested_flow: suggestions.map(a => a.actionName).join(" → "),
|
|
1571
|
-
next_steps: [
|
|
1572
|
-
"Use get_auto_builder_agent to get detailed info on each agent",
|
|
1573
|
-
"Use get_workflow_pattern for a complete template if a pattern matches",
|
|
1574
|
-
"Use get_qualifying_questions to ensure you have all required information",
|
|
1575
|
-
],
|
|
1576
|
-
};
|
|
1577
|
-
},
|
|
1578
|
-
get_workflow_pattern: async (args) => {
|
|
1579
|
-
const patternName = String(args.pattern_name);
|
|
1580
|
-
const pattern = WORKFLOW_PATTERNS.find(p => p.name === patternName);
|
|
1581
|
-
if (!pattern) {
|
|
1582
|
-
return {
|
|
1583
|
-
error: `Pattern not found: ${patternName}`,
|
|
1584
|
-
available: WORKFLOW_PATTERNS.map(p => ({ name: p.name, description: p.description })),
|
|
1585
|
-
};
|
|
1586
|
-
}
|
|
1587
|
-
return {
|
|
1588
|
-
name: pattern.name,
|
|
1589
|
-
persona_type: pattern.personaType,
|
|
1590
|
-
description: pattern.description,
|
|
1591
|
-
use_case: pattern.useCase,
|
|
1592
|
-
nodes: pattern.nodes,
|
|
1593
|
-
connections: pattern.connections,
|
|
1594
|
-
anti_patterns: pattern.antiPatterns ?? [],
|
|
1595
|
-
implementation_notes: [
|
|
1596
|
-
"Replace * with actual category/handler names",
|
|
1597
|
-
"All paths must lead to WORKFLOW_OUTPUT",
|
|
1598
|
-
"Include Fallback category for categorizers",
|
|
1599
|
-
"Check type compatibility for all connections",
|
|
1600
|
-
],
|
|
1601
|
-
};
|
|
1602
|
-
},
|
|
1603
|
-
list_workflow_patterns: async (args) => {
|
|
1604
|
-
const personaType = args.persona_type;
|
|
1605
|
-
const patterns = personaType
|
|
1606
|
-
? WORKFLOW_PATTERNS.filter(p => p.personaType === personaType)
|
|
1607
|
-
: WORKFLOW_PATTERNS;
|
|
1608
|
-
return {
|
|
1609
|
-
count: patterns.length,
|
|
1610
|
-
persona_type_filter: personaType ?? "all",
|
|
1611
|
-
patterns: patterns.map(p => ({
|
|
1612
|
-
name: p.name,
|
|
1613
|
-
persona_type: p.personaType,
|
|
1614
|
-
description: p.description,
|
|
1615
|
-
use_case: p.useCase,
|
|
1616
|
-
node_count: p.nodes.length,
|
|
1617
|
-
})),
|
|
1618
|
-
};
|
|
1619
|
-
},
|
|
1620
|
-
check_type_compatibility: async (args) => {
|
|
1621
|
-
const sourceType = String(args.source_type);
|
|
1622
|
-
const targetType = String(args.target_type);
|
|
1623
|
-
const compat = checkTypeCompatibility(sourceType, targetType);
|
|
1624
|
-
if (!compat) {
|
|
1625
|
-
return {
|
|
1626
|
-
source_type: sourceType,
|
|
1627
|
-
target_type: targetType,
|
|
1628
|
-
compatible: false,
|
|
1629
|
-
note: "No explicit compatibility rule found - likely incompatible",
|
|
1630
|
-
recommendation: "Use an intermediate node to convert types, or check if target accepts WELL_KNOWN_TYPE_ANY",
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
|
-
return {
|
|
1634
|
-
source_type: sourceType,
|
|
1635
|
-
target_type: targetType,
|
|
1636
|
-
compatible: compat.compatible,
|
|
1637
|
-
note: compat.note,
|
|
1638
|
-
recommendation: compat.compatible
|
|
1639
|
-
? "These types are compatible for direct connection"
|
|
1640
|
-
: `Incompatible. ${compat.note || "Use an intermediate node to convert types."}`,
|
|
1641
|
-
};
|
|
1642
|
-
},
|
|
1643
|
-
get_widget_reference: async (args) => {
|
|
1644
|
-
const personaType = String(args.persona_type);
|
|
1645
|
-
const widgets = getWidgetsForPersonaType(personaType);
|
|
1646
|
-
const projectType = PROJECT_TYPES[personaType];
|
|
1647
|
-
return {
|
|
1648
|
-
persona_type: personaType,
|
|
1649
|
-
project_type: projectType,
|
|
1650
|
-
widget_count: widgets.length,
|
|
1651
|
-
widgets: widgets.map(w => ({
|
|
1652
|
-
id: w.id,
|
|
1653
|
-
name: w.name,
|
|
1654
|
-
description: w.description,
|
|
1655
|
-
fields: w.fields,
|
|
1656
|
-
})),
|
|
1657
|
-
note: `Project type ${projectType} is used in proto_config for ${personaType} AI Employees`,
|
|
1658
|
-
};
|
|
1659
|
-
},
|
|
1660
|
-
get_qualifying_questions: async (args) => {
|
|
1661
|
-
const category = args.category;
|
|
1662
|
-
const requiredOnly = args.required_only === true;
|
|
1663
|
-
let questions = category
|
|
1664
|
-
? getQualifyingQuestionsByCategory(category)
|
|
1665
|
-
: QUALIFYING_QUESTIONS;
|
|
1666
|
-
if (requiredOnly) {
|
|
1667
|
-
questions = questions.filter(q => q.required);
|
|
1668
|
-
}
|
|
1669
|
-
const grouped = questions.reduce((acc, q) => {
|
|
1670
|
-
if (!acc[q.category])
|
|
1671
|
-
acc[q.category] = [];
|
|
1672
|
-
acc[q.category].push({ question: q.question, why_it_matters: q.whyItMatters, required: q.required });
|
|
1673
|
-
return acc;
|
|
1674
|
-
}, {});
|
|
1675
|
-
return {
|
|
1676
|
-
total_questions: questions.length,
|
|
1677
|
-
categories: Object.keys(grouped),
|
|
1678
|
-
questions_by_category: grouped,
|
|
1679
|
-
minimum_required: [
|
|
1680
|
-
"AI Type (Voice/Chat/Dashboard)",
|
|
1681
|
-
"2-3 intent categories + Fallback",
|
|
1682
|
-
"1 primary data source or action",
|
|
1683
|
-
"Success output format",
|
|
1684
|
-
],
|
|
1685
|
-
questioning_rounds: {
|
|
1686
|
-
round_1: "Core Context: AI type, trigger, main intents, data sources",
|
|
1687
|
-
round_2: "Workflow Details: Actions, validations, outputs, approvals",
|
|
1688
|
-
round_3: "Voice/Chat Specifics: Welcome message, hangup conditions (if applicable)",
|
|
1689
|
-
},
|
|
1690
|
-
};
|
|
1691
|
-
},
|
|
1692
|
-
get_voice_persona_template: async () => {
|
|
1693
|
-
// Use generated fallback from proto definitions
|
|
1694
|
-
// In production, prefer API templates via client.getPersonaTemplates()
|
|
1695
|
-
const template = getTemplateFallback("voice");
|
|
1696
|
-
return {
|
|
1697
|
-
template: template || VOICE_TEMPLATE_FALLBACK,
|
|
1698
|
-
field_docs: getTemplateFieldDocs("voice"),
|
|
1699
|
-
required_fields: ["conversationSettings.welcomeMessage", "conversationSettings.identityAndPurpose", "conversationSettings.takeActionInstructions", "conversationSettings.hangupInstructions"],
|
|
1700
|
-
optional_fields: ["conversationSettings.transferCallInstructions", "conversationSettings.speechCharacteristics", "conversationSettings.systemPrompt", "conversationSettings.formFillingInstructions", "conversationSettings.waitMessage"],
|
|
1701
|
-
project_type: PROJECT_TYPES.voice,
|
|
1702
|
-
widget_ids: {
|
|
1703
|
-
voiceSettings: 38,
|
|
1704
|
-
conversationSettings: 39,
|
|
1705
|
-
vadSettings: 43,
|
|
1706
|
-
dataStorageSettings: 42,
|
|
1707
|
-
},
|
|
1708
|
-
_source: "generated_fallback",
|
|
1709
|
-
_note: "This template is auto-generated from proto definitions. For live templates, use client.getPersonaTemplates().",
|
|
1710
|
-
};
|
|
1711
|
-
},
|
|
1712
|
-
validate_workflow_prompt: async (args) => {
|
|
1713
|
-
const prompt = String(args.prompt);
|
|
1714
|
-
const result = validateWorkflowPrompt(prompt);
|
|
1715
|
-
return {
|
|
1716
|
-
valid: result.valid,
|
|
1717
|
-
issue_count: result.issues.length,
|
|
1718
|
-
warning_count: result.warnings.length,
|
|
1719
|
-
issues: result.issues,
|
|
1720
|
-
warnings: result.warnings,
|
|
1721
|
-
recommendations: result.issues.length > 0 ? [
|
|
1722
|
-
"Add Fallback category to all categorizers",
|
|
1723
|
-
"Ensure HITL nodes have both success and failure paths",
|
|
1724
|
-
"Map all response nodes to WORKFLOW_OUTPUT",
|
|
1725
|
-
"Specify persona type (Voice AI, Chat AI, Dashboard AI)",
|
|
1726
|
-
"Check type compatibility for all connections",
|
|
1727
|
-
] : ["Prompt structure looks valid - verify type compatibility after generation"],
|
|
1728
|
-
};
|
|
1729
|
-
},
|
|
1730
|
-
get_auto_builder_guidance: async (args) => {
|
|
1731
|
-
const topic = String(args.topic);
|
|
1732
|
-
const guidance = GUIDANCE_TOPICS[topic];
|
|
1733
|
-
if (!guidance) {
|
|
1734
|
-
return {
|
|
1735
|
-
error: `Topic not found: ${topic}`,
|
|
1736
|
-
available_topics: Object.keys(GUIDANCE_TOPICS).map(k => ({
|
|
1737
|
-
topic: k,
|
|
1738
|
-
title: GUIDANCE_TOPICS[k].title,
|
|
1739
|
-
})),
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
return guidance;
|
|
1743
|
-
},
|
|
1744
|
-
get_platform_concept: async (args) => {
|
|
1745
|
-
const term = String(args.term);
|
|
1746
|
-
const concept = getConceptByTerm(term);
|
|
1747
|
-
if (!concept) {
|
|
1748
|
-
return {
|
|
1749
|
-
error: `Concept not found: ${term}`,
|
|
1750
|
-
available_concepts: PLATFORM_CONCEPTS.map(c => c.term),
|
|
1751
|
-
hint: "Try searching for aliases like 'Persona' (AI Employee) or 'Action' (Agent)",
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
return {
|
|
1755
|
-
term: concept.term,
|
|
1756
|
-
definition: concept.definition,
|
|
1757
|
-
aliases: concept.aliases ?? [],
|
|
1758
|
-
related_terms: concept.relatedTerms ?? [],
|
|
1759
|
-
examples: concept.examples ?? [],
|
|
1760
|
-
common_confusions: concept.commonConfusions,
|
|
1761
|
-
};
|
|
1762
|
-
},
|
|
1763
|
-
list_platform_concepts: async () => {
|
|
1764
|
-
return {
|
|
1765
|
-
count: PLATFORM_CONCEPTS.length,
|
|
1766
|
-
concepts: PLATFORM_CONCEPTS.map(c => ({
|
|
1767
|
-
term: c.term,
|
|
1768
|
-
definition: c.definition,
|
|
1769
|
-
aliases: c.aliases ?? [],
|
|
1770
|
-
})),
|
|
1771
|
-
key_relationships: [
|
|
1772
|
-
"AI Employee CONTAINS Workflow (processing logic) + Persona (conversational behavior)",
|
|
1773
|
-
"Workflow is made up of Agents/Actions connected by Edges",
|
|
1774
|
-
"Agents use Connectors to interact with external systems",
|
|
1775
|
-
"HITL = Human-in-the-Loop approval/verification step",
|
|
1776
|
-
],
|
|
1777
|
-
};
|
|
1778
|
-
},
|
|
1779
|
-
get_common_mistakes: async () => {
|
|
1780
|
-
return {
|
|
1781
|
-
count: COMMON_MISTAKES.length,
|
|
1782
|
-
mistakes: COMMON_MISTAKES,
|
|
1783
|
-
top_3_critical: [
|
|
1784
|
-
COMMON_MISTAKES.find(m => m.mistake.includes("Fallback")),
|
|
1785
|
-
COMMON_MISTAKES.find(m => m.mistake.includes("HITL")),
|
|
1786
|
-
COMMON_MISTAKES.find(m => m.mistake.includes("duplicate")),
|
|
1787
|
-
].filter(Boolean),
|
|
1788
|
-
};
|
|
1789
|
-
},
|
|
1790
|
-
get_debug_checklist: async () => {
|
|
1791
|
-
return {
|
|
1792
|
-
total_steps: DEBUG_CHECKLIST.length,
|
|
1793
|
-
checklist: DEBUG_CHECKLIST,
|
|
1794
|
-
quick_checks: [
|
|
1795
|
-
"Is the AI Employee status 'active' or 'ready'?",
|
|
1796
|
-
"Does the categorizer have all category edges including Fallback?",
|
|
1797
|
-
"Do all paths lead to WORKFLOW_OUTPUT?",
|
|
1798
|
-
"Are all connections type-compatible?",
|
|
1799
|
-
],
|
|
1800
|
-
};
|
|
1801
|
-
},
|
|
1802
|
-
get_workflow_execution_model: async () => {
|
|
1803
|
-
return {
|
|
1804
|
-
...WORKFLOW_EXECUTION_MODEL,
|
|
1805
|
-
summary: "Each user message triggers a NEW workflow execution. Use chat_conversation to detect previous actions and avoid duplicates.",
|
|
1806
|
-
};
|
|
1807
|
-
},
|
|
1808
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1809
|
-
// Workflow Review & Audit Handlers
|
|
1810
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1811
|
-
analyze_workflow: async (args) => {
|
|
1812
|
-
// DEPRECATED: MCP no longer pre-analyzes workflows
|
|
1813
|
-
// LLM should use ema://rules/* for analysis
|
|
1814
|
-
const client = createClient(args.env);
|
|
1815
|
-
const personaId = String(args.persona_id);
|
|
1816
|
-
const persona = await client.getPersonaById(personaId);
|
|
1817
|
-
if (!persona)
|
|
1818
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1819
|
-
const nodes = persona.workflow_def ? parseWorkflowDef(persona.workflow_def) : [];
|
|
1820
|
-
const connections = persona.workflow_def ? validateWorkflowConnections(persona.workflow_def) : [];
|
|
1821
|
-
return {
|
|
1822
|
-
environment: client["env"].name,
|
|
1823
|
-
persona_id: personaId,
|
|
1824
|
-
persona_name: persona.name,
|
|
1825
|
-
status: "DEPRECATED - use LLM analysis",
|
|
1826
|
-
node_count: nodes.length,
|
|
1827
|
-
workflow_def: persona.workflow_def,
|
|
1828
|
-
connections: connections.map(c => ({
|
|
1829
|
-
edge: c.edge_id,
|
|
1830
|
-
source_type: c.source_type,
|
|
1831
|
-
target_type: c.target_type,
|
|
1832
|
-
compatible: c.compatible,
|
|
1833
|
-
})),
|
|
1834
|
-
_deprecation_notice: {
|
|
1835
|
-
message: "analyze_workflow is deprecated. MCP does not pre-compute issues.",
|
|
1836
|
-
new_workflow: [
|
|
1837
|
-
"1. Fetch rules: ema://rules/anti-patterns",
|
|
1838
|
-
"2. Apply rules to find issues (LLM does this, not MCP)",
|
|
1839
|
-
"3. Report your findings",
|
|
1840
|
-
],
|
|
1841
|
-
},
|
|
1842
|
-
};
|
|
1843
|
-
},
|
|
1844
|
-
detect_workflow_issues: async (args) => {
|
|
1845
|
-
// DEPRECATED: MCP no longer detects workflow issues
|
|
1846
|
-
// LLM should use ema://rules/* for analysis
|
|
1847
|
-
const workflowDef = args.workflow_def;
|
|
1848
|
-
if (!workflowDef || typeof workflowDef !== "object") {
|
|
1849
|
-
return {
|
|
1850
|
-
error: "Invalid workflow_def - must be an object",
|
|
1851
|
-
hint: "Get workflow_def from get_ai_employee_full(persona_id).ai_employee.workflow_def",
|
|
1852
|
-
};
|
|
1853
|
-
}
|
|
1854
|
-
const nodes = parseWorkflowDef(workflowDef);
|
|
1855
|
-
return {
|
|
1856
|
-
status: "DEPRECATED - use LLM analysis",
|
|
1857
|
-
node_count: nodes.length,
|
|
1858
|
-
nodes: nodes.map(n => ({ id: n.id, action: n.action_name })),
|
|
1859
|
-
_deprecation_notice: {
|
|
1860
|
-
message: "detect_workflow_issues is deprecated. MCP does not pre-compute issues.",
|
|
1861
|
-
new_workflow: [
|
|
1862
|
-
"1. Fetch rules: ema://rules/anti-patterns",
|
|
1863
|
-
"2. Apply rules to this workflow (LLM does this)",
|
|
1864
|
-
"3. Report issues YOU find",
|
|
1865
|
-
],
|
|
1866
|
-
},
|
|
1867
|
-
};
|
|
1868
|
-
},
|
|
1869
|
-
validate_workflow_connections: async (args) => {
|
|
1870
|
-
const workflowDef = args.workflow_def;
|
|
1871
|
-
if (!workflowDef || typeof workflowDef !== "object") {
|
|
1872
|
-
return {
|
|
1873
|
-
error: "Invalid workflow_def - must be an object",
|
|
1874
|
-
hint: "Get workflow_def from get_ai_employee_full(persona_id).ai_employee.workflow_def",
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1877
|
-
const validations = validateWorkflowConnections(workflowDef);
|
|
1878
|
-
const compatible = validations.filter(v => v.compatible);
|
|
1879
|
-
const incompatible = validations.filter(v => !v.compatible);
|
|
1880
|
-
return {
|
|
1881
|
-
total_edges: validations.length,
|
|
1882
|
-
compatible_count: compatible.length,
|
|
1883
|
-
incompatible_count: incompatible.length,
|
|
1884
|
-
all_valid: incompatible.length === 0,
|
|
1885
|
-
validations: validations.map(v => ({
|
|
1886
|
-
edge: v.edge_id,
|
|
1887
|
-
source_type: v.source_type,
|
|
1888
|
-
target_type: v.target_type,
|
|
1889
|
-
compatible: v.compatible,
|
|
1890
|
-
note: v.note,
|
|
1891
|
-
})),
|
|
1892
|
-
incompatible_edges: incompatible.map(v => ({
|
|
1893
|
-
edge: v.edge_id,
|
|
1894
|
-
source_type: v.source_type,
|
|
1895
|
-
target_type: v.target_type,
|
|
1896
|
-
note: v.note,
|
|
1897
|
-
fix_hint: v.source_type === "WELL_KNOWN_TYPE_CHAT_CONVERSATION" && v.target_type === "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES"
|
|
1898
|
-
? "Insert conversation_to_search_query between source and target"
|
|
1899
|
-
: "Use named_inputs (accepts ANY type) or add intermediate conversion node",
|
|
1900
|
-
})),
|
|
1901
|
-
};
|
|
1902
|
-
},
|
|
1903
|
-
suggest_workflow_fixes: async (_args) => {
|
|
1904
|
-
// DEPRECATED: MCP no longer suggests fixes
|
|
1905
|
-
// LLM should reason about fixes using ema://rules/*
|
|
1906
|
-
return {
|
|
1907
|
-
status: "DEPRECATED - use LLM reasoning",
|
|
1908
|
-
_deprecation_notice: {
|
|
1909
|
-
message: "suggest_workflow_fixes is deprecated. LLM should propose fixes.",
|
|
1910
|
-
new_workflow: [
|
|
1911
|
-
"1. Get workflow: workflow(mode='get', persona_id='...')",
|
|
1912
|
-
"2. Analyze with ema://rules/anti-patterns",
|
|
1913
|
-
"3. LLM modifies workflow_def to fix issues",
|
|
1914
|
-
"4. Deploy: workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
1915
|
-
],
|
|
1916
|
-
},
|
|
1917
|
-
};
|
|
1918
|
-
},
|
|
1919
|
-
compare_workflow_versions: async (args) => {
|
|
1920
|
-
// DEPRECATED: MCP no longer pre-analyzes for comparison
|
|
1921
|
-
// LLM should compare workflows directly
|
|
1922
|
-
const client = createClient(args.env);
|
|
1923
|
-
const idBefore = String(args.persona_id_before);
|
|
1924
|
-
const idAfter = String(args.persona_id_after);
|
|
1925
|
-
const [personaBefore, personaAfter] = await Promise.all([
|
|
1926
|
-
client.getPersonaById(idBefore),
|
|
1927
|
-
client.getPersonaById(idAfter),
|
|
1928
|
-
]);
|
|
1929
|
-
if (!personaBefore)
|
|
1930
|
-
throw new Error(`AI Employee not found (before): ${idBefore}`);
|
|
1931
|
-
if (!personaAfter)
|
|
1932
|
-
throw new Error(`AI Employee not found (after): ${idAfter}`);
|
|
1933
|
-
const nodesBefore = personaBefore.workflow_def ? parseWorkflowDef(personaBefore.workflow_def) : [];
|
|
1934
|
-
const nodesAfter = personaAfter.workflow_def ? parseWorkflowDef(personaAfter.workflow_def) : [];
|
|
1935
|
-
// Compare fingerprints
|
|
1936
|
-
const fpBefore = personaBefore.workflow_def ? fingerprintPersona(personaBefore) : null;
|
|
1937
|
-
const fpAfter = personaAfter.workflow_def ? fingerprintPersona(personaAfter) : null;
|
|
1938
|
-
return {
|
|
1939
|
-
environment: client["env"].name,
|
|
1940
|
-
status: "DEPRECATED - LLM should compare",
|
|
1941
|
-
before: {
|
|
1942
|
-
persona_id: idBefore,
|
|
1943
|
-
name: personaBefore.name,
|
|
1944
|
-
fingerprint: fpBefore,
|
|
1945
|
-
has_workflow: !!personaBefore.workflow_def,
|
|
1946
|
-
node_count: nodesBefore.length,
|
|
1947
|
-
workflow_def: personaBefore.workflow_def,
|
|
1948
|
-
},
|
|
1949
|
-
after: {
|
|
1950
|
-
persona_id: idAfter,
|
|
1951
|
-
name: personaAfter.name,
|
|
1952
|
-
fingerprint: fpAfter,
|
|
1953
|
-
has_workflow: !!personaAfter.workflow_def,
|
|
1954
|
-
node_count: nodesAfter.length,
|
|
1955
|
-
workflow_def: personaAfter.workflow_def,
|
|
1956
|
-
},
|
|
1957
|
-
comparison: {
|
|
1958
|
-
fingerprints_match: fpBefore === fpAfter,
|
|
1959
|
-
node_count_change: nodesAfter.length - nodesBefore.length,
|
|
1960
|
-
},
|
|
1961
|
-
_deprecation_notice: {
|
|
1962
|
-
message: "compare_workflow_versions is deprecated. LLM should compare workflows directly.",
|
|
1963
|
-
guidance: "Compare the workflow_def objects returned above. Use ema://rules/anti-patterns to check each.",
|
|
1964
|
-
},
|
|
1965
|
-
};
|
|
1966
|
-
},
|
|
1967
|
-
get_workflow_metrics: async (args) => {
|
|
1968
|
-
const client = createClient(args.env);
|
|
1969
|
-
const personaId = String(args.persona_id);
|
|
1970
|
-
const persona = await client.getPersonaById(personaId);
|
|
1971
|
-
if (!persona)
|
|
1972
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1973
|
-
if (!persona.workflow_def) {
|
|
1974
|
-
return {
|
|
1975
|
-
environment: client["env"].name,
|
|
1976
|
-
persona_id: personaId,
|
|
1977
|
-
persona_name: persona.name,
|
|
1978
|
-
error: "AI Employee has no workflow_def",
|
|
1979
|
-
};
|
|
1980
|
-
}
|
|
1981
|
-
const nodes = parseWorkflowDef(persona.workflow_def);
|
|
1982
|
-
const connections = validateWorkflowConnections(persona.workflow_def);
|
|
1983
|
-
// Calculate basic metrics
|
|
1984
|
-
const totalEdges = nodes.reduce((sum, n) => sum + (n.incoming_edges?.length ?? 0), 0);
|
|
1985
|
-
const avgEdgesPerNode = nodes.length > 0
|
|
1986
|
-
? (totalEdges / nodes.length).toFixed(2)
|
|
1987
|
-
: 0;
|
|
1988
|
-
// Check for categorizers and HITL
|
|
1989
|
-
const categorizerCount = nodes.filter(n => n.action_name?.includes("categorizer")).length;
|
|
1990
|
-
const hitlCount = nodes.filter(n => n.action_name?.includes("hitl") || n.id?.includes("hitl")).length;
|
|
1991
|
-
const hasTrigger = nodes.some(n => n.action_name === "trigger" || n.id === "trigger");
|
|
1992
|
-
return {
|
|
1993
|
-
environment: client["env"].name,
|
|
1994
|
-
persona_id: personaId,
|
|
1995
|
-
persona_name: persona.name,
|
|
1996
|
-
structure: {
|
|
1997
|
-
total_nodes: nodes.length,
|
|
1998
|
-
total_edges: totalEdges,
|
|
1999
|
-
has_trigger: hasTrigger,
|
|
2000
|
-
connection_count: connections.length,
|
|
2001
|
-
},
|
|
2002
|
-
routing: {
|
|
2003
|
-
categorizers_count: categorizerCount,
|
|
2004
|
-
hitl_nodes_count: hitlCount,
|
|
2005
|
-
has_parallel_branches: categorizerCount > 0,
|
|
2006
|
-
},
|
|
2007
|
-
complexity: {
|
|
2008
|
-
avg_edges_per_node: avgEdgesPerNode,
|
|
2009
|
-
complexity_rating: nodes.length <= 5 ? "simple"
|
|
2010
|
-
: nodes.length <= 15 ? "moderate"
|
|
2011
|
-
: "complex",
|
|
2012
|
-
},
|
|
2013
|
-
_note: "Use ema://rules/anti-patterns for quality analysis. MCP no longer pre-computes issues.",
|
|
2014
|
-
};
|
|
2015
|
-
},
|
|
2016
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2017
|
-
// Workflow Compilation (Template-driven)
|
|
2018
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2019
|
-
compile_workflow: async (args) => {
|
|
2020
|
-
const name = String(args.name);
|
|
2021
|
-
const description = String(args.description);
|
|
2022
|
-
const personaType = String(args.persona_type);
|
|
2023
|
-
const rawNodes = args.nodes;
|
|
2024
|
-
const rawResultMappings = args.result_mappings;
|
|
2025
|
-
if (!["voice", "chat", "dashboard"].includes(personaType)) {
|
|
2026
|
-
throw new Error(`Invalid persona_type: ${personaType}. Must be one of: voice, chat, dashboard`);
|
|
2027
|
-
}
|
|
2028
|
-
if (!rawNodes || rawNodes.length === 0) {
|
|
2029
|
-
throw new Error("At least one node is required");
|
|
2030
|
-
}
|
|
2031
|
-
if (!rawResultMappings || rawResultMappings.length === 0) {
|
|
2032
|
-
throw new Error("At least one result_mapping is required to connect outputs to WORKFLOW_OUTPUT");
|
|
2033
|
-
}
|
|
2034
|
-
// Convert raw input spec to internal Node format
|
|
2035
|
-
const nodes = rawNodes.map((rawNode) => {
|
|
2036
|
-
const node = {
|
|
2037
|
-
id: rawNode.id,
|
|
2038
|
-
actionType: rawNode.action_type,
|
|
2039
|
-
displayName: rawNode.display_name,
|
|
2040
|
-
description: rawNode.description,
|
|
2041
|
-
disableHitl: rawNode.disable_hitl,
|
|
2042
|
-
};
|
|
2043
|
-
// Convert inputs
|
|
2044
|
-
if (rawNode.inputs) {
|
|
2045
|
-
node.inputs = {};
|
|
2046
|
-
for (const [key, rawBinding] of Object.entries(rawNode.inputs)) {
|
|
2047
|
-
const binding = {
|
|
2048
|
-
type: rawBinding.type,
|
|
2049
|
-
actionName: rawBinding.action_name,
|
|
2050
|
-
output: rawBinding.output,
|
|
2051
|
-
value: rawBinding.value,
|
|
2052
|
-
widgetName: rawBinding.widget_name,
|
|
2053
|
-
};
|
|
2054
|
-
node.inputs[key] = binding;
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
// Convert run_if condition
|
|
2058
|
-
if (rawNode.run_if) {
|
|
2059
|
-
node.runIf = {
|
|
2060
|
-
sourceAction: rawNode.run_if.source_action,
|
|
2061
|
-
sourceOutput: rawNode.run_if.source_output,
|
|
2062
|
-
operator: rawNode.run_if.operator,
|
|
2063
|
-
value: rawNode.run_if.value,
|
|
2064
|
-
};
|
|
2065
|
-
}
|
|
2066
|
-
// Convert categories
|
|
2067
|
-
if (rawNode.categories) {
|
|
2068
|
-
node.categories = rawNode.categories.map((cat) => ({
|
|
2069
|
-
name: cat.name,
|
|
2070
|
-
description: cat.description,
|
|
2071
|
-
examples: cat.examples,
|
|
2072
|
-
}));
|
|
2073
|
-
}
|
|
2074
|
-
// Convert tools
|
|
2075
|
-
if (rawNode.tools) {
|
|
2076
|
-
node.tools = rawNode.tools.map((tool) => ({
|
|
2077
|
-
name: tool.name,
|
|
2078
|
-
namespace: tool.namespace,
|
|
2079
|
-
}));
|
|
2080
|
-
}
|
|
2081
|
-
return node;
|
|
2082
|
-
});
|
|
2083
|
-
// Convert result mappings
|
|
2084
|
-
const resultMappings = rawResultMappings.map((rm) => ({
|
|
2085
|
-
nodeId: rm.node_id,
|
|
2086
|
-
output: rm.output,
|
|
2087
|
-
}));
|
|
2088
|
-
// Build the WorkflowSpec
|
|
2089
|
-
const spec = {
|
|
2090
|
-
name,
|
|
2091
|
-
description,
|
|
2092
|
-
personaType,
|
|
2093
|
-
nodes,
|
|
2094
|
-
resultMappings,
|
|
2095
|
-
};
|
|
2096
|
-
// Compile the workflow
|
|
2097
|
-
const result = compileWorkflow(spec);
|
|
2098
|
-
return {
|
|
2099
|
-
success: true,
|
|
2100
|
-
message: `Compiled workflow for ${personaType} AI Employee "${name}" with ${nodes.length} nodes`,
|
|
2101
|
-
workflow_def: result.workflow_def,
|
|
2102
|
-
proto_config: result.proto_config,
|
|
2103
|
-
nodes_compiled: nodes.map((n) => ({ id: n.id, action: n.actionType, display_name: n.displayName })),
|
|
2104
|
-
result_mappings: resultMappings,
|
|
2105
|
-
usage: 'Deploy with: workflow(mode="deploy", persona_id="<persona_id>", workflow_def=<workflow_def>, proto_config=<proto_config>)',
|
|
2106
|
-
_tip: 'Stale-state protection is strict: call workflow(mode="get") immediately before deploy and pass base_fingerprint from the response. For large payloads, use workflow_def_path.',
|
|
2107
|
-
};
|
|
2108
|
-
},
|
|
2109
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2110
|
-
// Data Source / Embedding Management
|
|
2111
|
-
// Delegates to extracted handler in ./handlers/data/index.js
|
|
2112
|
-
// TODO: Add support for 3rd party data sources (Google Drive, SharePoint, Confluence, etc.)
|
|
2113
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2114
|
-
upload_data_source: async (args) => {
|
|
2115
|
-
const client = createClient(args.env);
|
|
2116
|
-
const fs = await import("fs/promises");
|
|
2117
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2118
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "upload", path: String(args.file_path) } }, client, (path) => fs.readFile(path));
|
|
2119
|
-
},
|
|
2120
|
-
delete_data_source: async (args) => {
|
|
2121
|
-
const client = createClient(args.env);
|
|
2122
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2123
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "delete", file_id: String(args.file_id) } }, client);
|
|
2124
|
-
},
|
|
2125
|
-
list_data_sources: async (args) => {
|
|
2126
|
-
const client = createClient(args.env);
|
|
2127
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2128
|
-
return handleData({ persona_id: String(args.persona_id), method: "list" }, client);
|
|
2129
|
-
},
|
|
2130
|
-
get_embedding_status: async (args) => {
|
|
2131
|
-
const client = createClient(args.env);
|
|
2132
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2133
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "embedding" } }, client);
|
|
2134
|
-
},
|
|
2135
|
-
toggle_embedding: async (args) => {
|
|
2136
|
-
const client = createClient(args.env);
|
|
2137
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2138
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "embedding", enabled: Boolean(args.enabled) } }, client);
|
|
2139
|
-
},
|
|
2140
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2141
|
-
// Demo Data Management Handlers
|
|
2142
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2143
|
-
consolidate_demo_data: async (args) => {
|
|
2144
|
-
return handleConsolidateDemoData(args);
|
|
2145
|
-
},
|
|
2146
|
-
generate_demo_document: async (args) => {
|
|
2147
|
-
return handleGenerateDemoDocument(args);
|
|
2148
|
-
},
|
|
2149
|
-
validate_demo_document: async (args) => {
|
|
2150
|
-
return handleValidateDemoDocument(args);
|
|
2151
|
-
},
|
|
2152
|
-
get_demo_data_template: async (args) => {
|
|
2153
|
-
return handleGetDemoDataTemplate(args);
|
|
2154
|
-
},
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// Tool Definitions
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
//
|
|
53
|
+
// V2 TOOLS (5 tools) - LLM-optimized minimal interface
|
|
54
|
+
// - env, persona, catalog, workflow, sync
|
|
55
|
+
// - Defined in: ./tools.ts
|
|
56
|
+
//
|
|
57
|
+
// NAMING CONVENTION:
|
|
58
|
+
// - Tool names are defined as BASE NAMES (e.g., "persona")
|
|
59
|
+
// - MCP clients prefix with "mcp_{server}_" (e.g., "mcp_ema_persona")
|
|
60
|
+
//
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Generate all available tools
|
|
64
|
+
*
|
|
65
|
+
* V2: 5 tools (persona, catalog, workflow, sync, env) - LLM-optimized
|
|
66
|
+
*
|
|
67
|
+
* Why V2:
|
|
68
|
+
* - Minimal tool count optimizes LLM tool selection
|
|
69
|
+
* - Data operations under persona (always persona-scoped)
|
|
70
|
+
* - Catalog consolidates all reference data (actions, templates, etc.)
|
|
71
|
+
* - Clear separation: entity (persona), reference (catalog), operation (sync, workflow)
|
|
72
|
+
*/
|
|
73
|
+
function generateAllTools() {
|
|
74
|
+
const envNames = getAvailableEnvironments().map(e => e.name);
|
|
75
|
+
const defaultEnv = getDefaultEnvName();
|
|
76
|
+
return generateTools(envNames, defaultEnv);
|
|
77
|
+
}
|
|
78
|
+
// Generate tools (called once at module load)
|
|
79
|
+
const TOOLS = generateAllTools();
|
|
80
|
+
const toolHandlers = {
|
|
2155
81
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2156
|
-
//
|
|
82
|
+
// V2 TOOLS — LLM-optimized interface
|
|
2157
83
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2158
84
|
env: async () => {
|
|
2159
85
|
return handleEnv({}, () => getAvailableEnvironments().map(e => ({
|
|
@@ -2417,41 +343,165 @@ const toolHandlers = {
|
|
|
2417
343
|
client,
|
|
2418
344
|
});
|
|
2419
345
|
},
|
|
2420
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2421
|
-
// V2 TOOLS (4 tools: persona, catalog, sync, env)
|
|
2422
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2423
346
|
// catalog: Consolidated reference data (actions, templates, widgets, voices, patterns, concepts)
|
|
2424
347
|
catalog: async (args) => {
|
|
2425
348
|
const client = createClient(args.env);
|
|
2426
349
|
return handleCatalog(args, client);
|
|
2427
350
|
},
|
|
2428
|
-
//
|
|
2429
|
-
|
|
351
|
+
// toolkit_feedback: Agent feedback collection and analysis
|
|
352
|
+
toolkit_feedback: async (args) => {
|
|
353
|
+
return handleFeedback(args);
|
|
354
|
+
},
|
|
2430
355
|
};
|
|
2431
356
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
|
+
// Standalone sync implementation functions
|
|
358
|
+
// (Used by the V2 sync adapter below)
|
|
359
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
360
|
+
async function syncRunImpl(args) {
|
|
361
|
+
const targetEnv = String(args.target_env);
|
|
362
|
+
const sourceEnv = args.source_env ? String(args.source_env) : getDefaultEnvName();
|
|
363
|
+
const dryRun = args.dry_run === true;
|
|
364
|
+
const includeStatus = args.include_status === true;
|
|
365
|
+
const scope = args.scope === "all" ? "all" : "one";
|
|
366
|
+
const identifier = args.identifier ? String(args.identifier) : undefined;
|
|
367
|
+
// Sync all tagged personas
|
|
368
|
+
if (scope === "all" || !identifier) {
|
|
369
|
+
const sdk = getSyncSDK();
|
|
370
|
+
if (sdk) {
|
|
371
|
+
try {
|
|
372
|
+
const result = await sdk.runSync();
|
|
373
|
+
return { success: true, mode: "config", ...result };
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
sdk.close();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Config-less mode
|
|
380
|
+
try {
|
|
381
|
+
const result = await directSyncAll({ targetEnv, dryRun });
|
|
382
|
+
return { success: true, mode: "tags", ...result };
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Sync single persona
|
|
389
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
390
|
+
const behavior = resolveSyncBehavior({
|
|
391
|
+
personaName: isUUID ? undefined : identifier,
|
|
392
|
+
targetEnv,
|
|
393
|
+
overrides: {
|
|
394
|
+
dry_run: dryRun ? true : undefined,
|
|
395
|
+
sync_status: includeStatus ? true : undefined,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
try {
|
|
399
|
+
const result = isUUID
|
|
400
|
+
? await directSyncPersonaById({
|
|
401
|
+
personaId: identifier,
|
|
402
|
+
sourceEnv,
|
|
403
|
+
targetEnv,
|
|
404
|
+
dryRun: behavior.dry_run,
|
|
405
|
+
syncStatus: behavior.sync_status,
|
|
406
|
+
})
|
|
407
|
+
: await directSyncPersona({
|
|
408
|
+
name: identifier,
|
|
409
|
+
sourceEnv,
|
|
410
|
+
targetEnv,
|
|
411
|
+
dryRun: behavior.dry_run,
|
|
412
|
+
syncStatus: behavior.sync_status,
|
|
413
|
+
});
|
|
414
|
+
return { ...result, resolved_behavior: behavior };
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function syncInfoImpl(args) {
|
|
421
|
+
const client = args.env ? createClient(args.env) : undefined;
|
|
422
|
+
// Check if persona is synced
|
|
423
|
+
if (args.persona_id) {
|
|
424
|
+
if (!client)
|
|
425
|
+
throw new Error("env required when checking persona sync status");
|
|
426
|
+
const personaId = String(args.persona_id);
|
|
427
|
+
const personas = await client.getPersonasForTenant();
|
|
428
|
+
const persona = personas.find((p) => p.id === personaId);
|
|
429
|
+
if (!persona)
|
|
430
|
+
throw new Error(`AI Employee not found: ${personaId}`);
|
|
431
|
+
const meta = client.getSyncMetadata(persona);
|
|
432
|
+
return {
|
|
433
|
+
environment: client["env"].name,
|
|
434
|
+
persona_id: personaId,
|
|
435
|
+
persona_name: persona.name,
|
|
436
|
+
is_synced: !!meta,
|
|
437
|
+
sync_metadata: meta,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
// Check by persona name
|
|
441
|
+
if (args.persona_name) {
|
|
442
|
+
const sdk = getSyncSDK();
|
|
443
|
+
if (!sdk)
|
|
444
|
+
return { error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG." };
|
|
445
|
+
try {
|
|
446
|
+
const persona = await sdk.getMasterPersonaByName(String(args.persona_name));
|
|
447
|
+
if (!persona)
|
|
448
|
+
return { error: `Persona not found: ${args.persona_name}` };
|
|
449
|
+
return await sdk.getPersonaSyncStatus(persona.id);
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
sdk.close();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// List all synced personas
|
|
456
|
+
if (args.list_synced === true) {
|
|
457
|
+
if (!client)
|
|
458
|
+
throw new Error("env required when listing synced personas");
|
|
459
|
+
const personas = await client.getPersonasForTenant();
|
|
460
|
+
const masterEnvFilter = args.master_env ? String(args.master_env).toLowerCase() : undefined;
|
|
461
|
+
const synced = [];
|
|
462
|
+
for (const p of personas) {
|
|
463
|
+
const meta = client.getSyncMetadata(p);
|
|
464
|
+
if (meta) {
|
|
465
|
+
if (masterEnvFilter && meta.master_env.toLowerCase() !== masterEnvFilter)
|
|
466
|
+
continue;
|
|
467
|
+
synced.push({ persona_id: p.id, persona_name: p.name, sync_metadata: meta });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return { environment: client["env"].name, count: synced.length, synced_personas: synced };
|
|
471
|
+
}
|
|
472
|
+
// Default: return overall sync config/status
|
|
473
|
+
const sdk = getSyncSDK();
|
|
474
|
+
const options = args.include_options === true ? loadSyncOptions() : undefined;
|
|
475
|
+
if (!sdk) {
|
|
476
|
+
return {
|
|
477
|
+
configured: false,
|
|
478
|
+
error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG.",
|
|
479
|
+
options,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const master = sdk.getMasterEnvironment();
|
|
484
|
+
const envs = sdk.getEnvironments();
|
|
485
|
+
const personas = await sdk.listMasterPersonas();
|
|
486
|
+
return {
|
|
487
|
+
configured: true,
|
|
488
|
+
master_environment: { name: master.name, url: master.baseUrl },
|
|
489
|
+
target_environments: envs.filter((e) => !e.isMaster).map((e) => ({ name: e.name, url: e.baseUrl })),
|
|
490
|
+
total_personas: personas.length,
|
|
491
|
+
options,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
sdk.close();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2432
499
|
// V2 Tool Adapters (contract ↔ implementation)
|
|
2433
500
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2434
501
|
//
|
|
2435
502
|
// The tool schemas in tools.ts are the public MCP contract.
|
|
2436
|
-
// This file still contains proven handlers that we reuse
|
|
2437
|
-
// (e.g. deploy_workflow, optimize_workflow, compile_workflow, etc.).
|
|
2438
|
-
//
|
|
2439
503
|
// These adapters ensure the V2 tool surface behaves as documented,
|
|
2440
|
-
// while
|
|
2441
|
-
const legacyWorkflowTool = toolHandlers.workflow;
|
|
2442
|
-
const legacyDeployWorkflow = toolHandlers.deploy_workflow;
|
|
2443
|
-
const legacyOptimizeWorkflow = toolHandlers.optimize_workflow;
|
|
2444
|
-
const legacyCompareWorkflowVersions = toolHandlers.compare_workflow_versions;
|
|
2445
|
-
const legacyCompileWorkflow = toolHandlers.compile_workflow;
|
|
2446
|
-
const legacyDetectWorkflowIssues = toolHandlers.detect_workflow_issues;
|
|
2447
|
-
const legacyValidateWorkflowConnections = toolHandlers.validate_workflow_connections;
|
|
2448
|
-
const legacySuggestWorkflowFixes = toolHandlers.suggest_workflow_fixes;
|
|
2449
|
-
const legacySyncRun = toolHandlers.sync;
|
|
2450
|
-
const legacySyncInfo = toolHandlers.sync_info;
|
|
2451
|
-
const legacyConsolidateDemoData = toolHandlers.consolidate_demo_data;
|
|
2452
|
-
const legacyGenerateDemoDocument = toolHandlers.generate_demo_document;
|
|
2453
|
-
const legacyValidateDemoDocument = toolHandlers.validate_demo_document;
|
|
2454
|
-
const legacyGetDemoDataTemplate = toolHandlers.get_demo_data_template;
|
|
504
|
+
// while routing to the extracted handler implementations.
|
|
2455
505
|
// Workflow tool: MCP provides data (get) and executes (deploy). LLM does all thinking.
|
|
2456
506
|
toolHandlers.workflow = async (args) => {
|
|
2457
507
|
const normalizedArgs = { ...(args ?? {}) };
|
|
@@ -2644,14 +694,14 @@ toolHandlers.sync = async (args) => {
|
|
|
2644
694
|
const identifier = normalizedArgs.identifier; // deprecated alias
|
|
2645
695
|
const idOrIdentifier = id ?? identifier;
|
|
2646
696
|
if (mode === "config") {
|
|
2647
|
-
return
|
|
697
|
+
return syncInfoImpl({ include_options: true });
|
|
2648
698
|
}
|
|
2649
699
|
if (mode === "status") {
|
|
2650
700
|
const env = normalizedArgs.env;
|
|
2651
701
|
if (normalizedArgs.list_synced === true) {
|
|
2652
702
|
if (!env)
|
|
2653
703
|
throw new Error('env is required for sync(mode="status", list_synced=true)');
|
|
2654
|
-
return
|
|
704
|
+
return syncInfoImpl({ list_synced: true, master_env: normalizedArgs.master_env, env });
|
|
2655
705
|
}
|
|
2656
706
|
if (idOrIdentifier) {
|
|
2657
707
|
if (!env)
|
|
@@ -2659,7 +709,7 @@ toolHandlers.sync = async (args) => {
|
|
|
2659
709
|
const identifierToResolve = String(idOrIdentifier);
|
|
2660
710
|
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifierToResolve);
|
|
2661
711
|
if (isUUID) {
|
|
2662
|
-
return
|
|
712
|
+
return syncInfoImpl({ persona_id: identifierToResolve, env });
|
|
2663
713
|
}
|
|
2664
714
|
// Name lookup: resolve to ID in env, then reuse persona_id path
|
|
2665
715
|
const client = createClient(env);
|
|
@@ -2667,16 +717,16 @@ toolHandlers.sync = async (args) => {
|
|
|
2667
717
|
const match = personas.find((p) => p.name === identifierToResolve);
|
|
2668
718
|
if (!match)
|
|
2669
719
|
throw new Error(`AI Employee not found by name in ${env}: ${identifierToResolve}`);
|
|
2670
|
-
return
|
|
720
|
+
return syncInfoImpl({ persona_id: match.id, env });
|
|
2671
721
|
}
|
|
2672
722
|
// Default: overall sync status/config summary
|
|
2673
|
-
return
|
|
723
|
+
return syncInfoImpl({ include_options: normalizedArgs.include_options === true });
|
|
2674
724
|
}
|
|
2675
725
|
// mode === "run" (default)
|
|
2676
726
|
if (!target) {
|
|
2677
727
|
throw new Error('target (or target_env) is required for sync(mode="run")');
|
|
2678
728
|
}
|
|
2679
|
-
return
|
|
729
|
+
return syncRunImpl({
|
|
2680
730
|
identifier: idOrIdentifier,
|
|
2681
731
|
target_env: target,
|
|
2682
732
|
source_env: source,
|
|
@@ -2829,7 +879,7 @@ toolHandlers.demo = async (args) => {
|
|
|
2829
879
|
if (!source || !output || !entity) {
|
|
2830
880
|
throw new Error('demo(mode="consolidate") requires: source, output, entity');
|
|
2831
881
|
}
|
|
2832
|
-
return
|
|
882
|
+
return handleConsolidateDemoData({
|
|
2833
883
|
source_dir: source,
|
|
2834
884
|
output_dir: output,
|
|
2835
885
|
entity_type: entity,
|
|
@@ -2842,7 +892,7 @@ toolHandlers.demo = async (args) => {
|
|
|
2842
892
|
const entity = String(normalizedArgs.entity ?? "");
|
|
2843
893
|
if (!entity)
|
|
2844
894
|
throw new Error('demo(mode="generate") requires: entity');
|
|
2845
|
-
return
|
|
895
|
+
return handleGenerateDemoDocument({
|
|
2846
896
|
entity_type: entity,
|
|
2847
897
|
data: normalizedArgs.data ?? {},
|
|
2848
898
|
related_data: normalizedArgs.related ?? {},
|
|
@@ -2851,7 +901,7 @@ toolHandlers.demo = async (args) => {
|
|
|
2851
901
|
});
|
|
2852
902
|
}
|
|
2853
903
|
case "validate": {
|
|
2854
|
-
return
|
|
904
|
+
return handleValidateDemoDocument({
|
|
2855
905
|
file_path: normalizedArgs.file,
|
|
2856
906
|
content: normalizedArgs.content,
|
|
2857
907
|
});
|
|
@@ -2860,7 +910,7 @@ toolHandlers.demo = async (args) => {
|
|
|
2860
910
|
const entity = String(normalizedArgs.entity ?? "");
|
|
2861
911
|
if (!entity)
|
|
2862
912
|
throw new Error('demo(mode="template") requires: entity');
|
|
2863
|
-
return
|
|
913
|
+
return handleGetDemoDataTemplate({
|
|
2864
914
|
entity_type: entity,
|
|
2865
915
|
include_example: normalizedArgs.include_example,
|
|
2866
916
|
});
|
|
@@ -2869,7 +919,6 @@ toolHandlers.demo = async (args) => {
|
|
|
2869
919
|
throw new Error(`Unknown demo mode: ${mode}`);
|
|
2870
920
|
}
|
|
2871
921
|
};
|
|
2872
|
-
// generateEntityDocument moved to handlers/demo/index.js
|
|
2873
922
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2874
923
|
// Helpers
|
|
2875
924
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2919,6 +968,9 @@ function determineOperation(toolName, args) {
|
|
|
2919
968
|
return "status";
|
|
2920
969
|
return "preview";
|
|
2921
970
|
}
|
|
971
|
+
if (toolName === "toolkit_feedback") {
|
|
972
|
+
return args.method ? String(args.method) : "submit";
|
|
973
|
+
}
|
|
2922
974
|
return toolName;
|
|
2923
975
|
}
|
|
2924
976
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2936,7 +988,10 @@ export async function startMcpServer() {
|
|
|
2936
988
|
},
|
|
2937
989
|
// Server instructions - injected into client's system prompt
|
|
2938
990
|
// Single source of truth from guidance module
|
|
2939
|
-
instructions: generateServerInstructions(
|
|
991
|
+
instructions: generateServerInstructions({
|
|
992
|
+
version: TOOLKIT_VERSION,
|
|
993
|
+
commit: TOOLKIT_COMMIT,
|
|
994
|
+
}),
|
|
2940
995
|
});
|
|
2941
996
|
// ─────────────────────────────────────────────────────────────────────────
|
|
2942
997
|
// Tool Handlers
|
|
@@ -2951,11 +1006,17 @@ export async function startMcpServer() {
|
|
|
2951
1006
|
isError: true,
|
|
2952
1007
|
};
|
|
2953
1008
|
}
|
|
1009
|
+
const startMs = Date.now();
|
|
2954
1010
|
try {
|
|
2955
1011
|
const argsObj = (args ?? {});
|
|
2956
1012
|
const result = await handler(argsObj);
|
|
1013
|
+
const elapsedMs = Date.now() - startMs;
|
|
2957
1014
|
// Determine operation type for contextual tips
|
|
2958
1015
|
const operation = determineOperation(name, argsObj);
|
|
1016
|
+
// Passive telemetry: record successful tool call (fire-and-forget)
|
|
1017
|
+
if (name !== "toolkit_feedback") {
|
|
1018
|
+
recordTelemetry({ type: "tool_call", tool: name, op: operation, ok: true, ms: elapsedMs }).catch(() => { });
|
|
1019
|
+
}
|
|
2959
1020
|
// Get contextual tip based on operation and result
|
|
2960
1021
|
const tip = getContextualTip({
|
|
2961
1022
|
operation,
|
|
@@ -2981,8 +1042,21 @@ export async function startMcpServer() {
|
|
|
2981
1042
|
};
|
|
2982
1043
|
}
|
|
2983
1044
|
catch (error) {
|
|
1045
|
+
const elapsedMs = Date.now() - startMs;
|
|
1046
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1047
|
+
const argsObj = (args ?? {});
|
|
1048
|
+
const operation = determineOperation(name, argsObj);
|
|
1049
|
+
// Passive telemetry: record failed tool call (fire-and-forget)
|
|
1050
|
+
if (name !== "toolkit_feedback") {
|
|
1051
|
+
recordTelemetry({ type: "error", tool: name, op: operation, ok: false, error_message: errorMessage, ms: elapsedMs }).catch(() => { });
|
|
1052
|
+
}
|
|
1053
|
+
// Solicit feedback on errors
|
|
1054
|
+
const errorResponse = {
|
|
1055
|
+
error: errorMessage,
|
|
1056
|
+
_feedback_welcome: `If this error was unclear or unhelpful, please report it: toolkit_feedback(method="submit", category="error_unclear", tool="${name}", operation="${operation}", message="<describe what was confusing>")`,
|
|
1057
|
+
};
|
|
2984
1058
|
return {
|
|
2985
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
1059
|
+
content: [{ type: "text", text: JSON.stringify(errorResponse) }],
|
|
2986
1060
|
isError: true,
|
|
2987
1061
|
};
|
|
2988
1062
|
}
|
|
@@ -3034,6 +1108,8 @@ export async function startMcpServer() {
|
|
|
3034
1108
|
});
|
|
3035
1109
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
3036
1110
|
const { uri } = request.params;
|
|
1111
|
+
// Passive telemetry: record resource fetch (fire-and-forget)
|
|
1112
|
+
recordTelemetry({ type: "resource_fetch", resource_uri: uri, ok: true }).catch(() => { });
|
|
3037
1113
|
const result = await resourceRegistry.read(uri);
|
|
3038
1114
|
if (isResourceError(result)) {
|
|
3039
1115
|
throw new Error(`${result.code}: ${result.message}`);
|
|
@@ -3050,11 +1126,12 @@ export async function startMcpServer() {
|
|
|
3050
1126
|
});
|
|
3051
1127
|
const transport = new StdioServerTransport();
|
|
3052
1128
|
await server.connect(transport);
|
|
3053
|
-
// Log startup with version and
|
|
1129
|
+
// Log startup with version, commit, and tool count
|
|
3054
1130
|
const buildInfo = TOOLKIT_COMMIT
|
|
3055
1131
|
? `${TOOLKIT_VERSION} (${TOOLKIT_COMMIT})`
|
|
3056
1132
|
: TOOLKIT_VERSION;
|
|
3057
|
-
|
|
1133
|
+
const toolCount = TOOLS.length;
|
|
1134
|
+
console.error(`Ema MCP Server started: ${TOOLKIT_NAME}@${buildInfo} | ${toolCount} tools`);
|
|
3058
1135
|
}
|
|
3059
1136
|
// CLI support: allow --help to exit (used by build:verify)
|
|
3060
1137
|
const argv = process.argv.slice(2);
|