@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.

Files changed (67) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/LICENSE +29 -21
  4. package/README.md +58 -35
  5. package/dist/mcp/domain/loop-detection.js +97 -0
  6. package/dist/mcp/domain/proto-constraints.js +284 -0
  7. package/dist/mcp/domain/structural-rules.js +12 -5
  8. package/dist/mcp/domain/validation-rules.js +107 -20
  9. package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
  10. package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
  11. package/dist/mcp/domain/workflow-graph.js +374 -0
  12. package/dist/mcp/domain/workflow-optimizer.js +10 -4
  13. package/dist/mcp/guidance.js +54 -31
  14. package/dist/mcp/handlers/feedback/index.js +139 -0
  15. package/dist/mcp/handlers/feedback/store.js +262 -0
  16. package/dist/mcp/handlers/persona/index.js +237 -8
  17. package/dist/mcp/handlers/persona/schema.js +27 -0
  18. package/dist/mcp/handlers/reference/index.js +6 -4
  19. package/dist/mcp/handlers/workflow/index.js +25 -28
  20. package/dist/mcp/handlers/workflow/optimize.js +73 -33
  21. package/dist/mcp/handlers/workflow/validation.js +1 -1
  22. package/dist/mcp/knowledge-types.js +7 -0
  23. package/dist/mcp/knowledge.js +146 -834
  24. package/dist/mcp/resources.js +610 -18
  25. package/dist/mcp/server.js +233 -2156
  26. package/dist/mcp/tools.js +91 -5
  27. package/dist/sdk/generated/agent-catalog.js +615 -0
  28. package/dist/sdk/generated/deprecated-actions.js +182 -96
  29. package/dist/sdk/generated/proto-fields.js +2 -1
  30. package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
  31. package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
  32. package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
  33. package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
  34. package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
  35. package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
  36. package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
  37. package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
  38. package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
  39. package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
  40. package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
  41. package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
  42. package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
  43. package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
  44. package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
  45. package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
  46. package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
  47. package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
  48. package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
  49. package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
  50. package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
  51. package/dist/sdk/generated/widget-catalog.js +60 -0
  52. package/docs/README.md +17 -9
  53. package/package.json +2 -2
  54. package/.context/public/guides/dashboard-operations.md +0 -286
  55. package/.context/public/guides/email-patterns.md +0 -125
  56. package/dist/mcp/domain/intent-architect.js +0 -914
  57. package/dist/mcp/domain/quality-gates.js +0 -110
  58. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  59. package/dist/mcp/domain/workflow-intent.js +0 -1806
  60. package/dist/mcp/domain/workflow-merge.js +0 -449
  61. package/dist/mcp/domain/workflow-tracer.js +0 -648
  62. package/dist/mcp/domain/workflow-transformer.js +0 -742
  63. package/dist/mcp/handlers/persona/intent.js +0 -141
  64. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  65. package/dist/mcp/handlers/workflow/compare.js +0 -70
  66. package/dist/mcp/handlers/workflow/generate.js +0 -384
  67. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -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, transformWorkflowForTarget } from "../sync.js";
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
- // Auto Builder Knowledge
34
- import { AGENT_CATALOG, WORKFLOW_PATTERNS, QUALIFYING_QUESTIONS, PLATFORM_CONCEPTS, WORKFLOW_EXECUTION_MODEL, COMMON_MISTAKES, DEBUG_CHECKLIST, GUIDANCE_TOPICS, PROJECT_TYPES, getAgentsByCategory, getAgentByName, getWidgetsForPersonaType, checkTypeCompatibility, getQualifyingQuestionsByCategory, getConceptByTerm, suggestAgentsForUseCase, validateWorkflowPrompt,
35
- // Workflow Data (LLM does analysis with rules from ema://rules/*)
36
- validateWorkflowConnections, parseWorkflowDef, } from "./knowledge.js";
37
- // Template fallbacks (generated from protos, used when API unavailable)
38
- import { getTemplateFallback, getTemplateFieldDocs, VOICE_TEMPLATE_FALLBACK } from "../sdk/generated/template-fallbacks.js";
39
- // Workflow Compiler (Template-driven)
40
- import { compileWorkflow, } from "./domain/workflow-generator.js";
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
- // Helper to add env parameter to schema - returns Tool-compatible inputSchema
61
- function withEnvParam(props, required = []) {
62
- const envs = getAvailableEnvironments();
63
- const envNames = envs.map((e) => e.name);
64
- return {
65
- type: "object",
66
- properties: {
67
- ...props,
68
- env: {
69
- type: "string",
70
- description: `Target environment. Available: ${envNames.join(", ")}. Default: ${getDefaultEnvName()}`,
71
- },
72
- },
73
- required,
74
- };
75
- }
76
- // ─────────────────────────────────────────────────────────────────────────────
77
- // Tool Definitions
78
- // ─────────────────────────────────────────────────────────────────────────────
79
- //
80
- // V2 TOOLS (5 tools) - LLM-optimized minimal interface
81
- // - env, persona, catalog, workflow, sync
82
- // - Defined in: ./tools.ts
83
- //
84
- // NAMING CONVENTION:
85
- // - Tool names are defined as BASE NAMES (e.g., "persona")
86
- // - MCP clients prefix with "mcp_{server}_" (e.g., "mcp_ema_persona")
87
- //
88
- // ─────────────────────────────────────────────────────────────────────────────
89
- /**
90
- * Generate all available tools
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
- // CONSOLIDATED TOOLS (Unix CLI pattern) - 9 tools replace 45
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
- // Note: 'sync' handler already exists above - keeping it for full functionality
2429
- // Note: demo is handled via a consolidated adapter below
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 preserving the existing internal implementations.
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 legacySyncInfo({ include_options: true });
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 legacySyncInfo({ list_synced: true, master_env: normalizedArgs.master_env, env });
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 legacySyncInfo({ persona_id: identifierToResolve, env });
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 legacySyncInfo({ persona_id: match.id, env });
720
+ return syncInfoImpl({ persona_id: match.id, env });
2671
721
  }
2672
722
  // Default: overall sync status/config summary
2673
- return legacySyncInfo({ include_options: normalizedArgs.include_options === true });
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 legacySyncRun({
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 legacyConsolidateDemoData({
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 legacyGenerateDemoDocument({
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 legacyValidateDemoDocument({
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 legacyGetDemoDataTemplate({
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({ error: error instanceof Error ? error.message : String(error) }) }],
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 commit info
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
- console.error(`Ema MCP Server started: ${TOOLKIT_NAME}@${buildInfo}`);
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);