@ema.co/mcp-toolkit 2026.2.13 → 2026.2.23-1

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/dist/cli/index.js +2 -2
  4. package/dist/mcp/domain/loop-detection.js +89 -0
  5. package/dist/mcp/domain/sanitizer.js +1 -1
  6. package/dist/mcp/domain/structural-rules.js +4 -5
  7. package/dist/mcp/domain/validation-rules.js +5 -5
  8. package/dist/mcp/domain/workflow-graph.js +3 -5
  9. package/dist/mcp/domain/workflow-path-enumerator.js +7 -4
  10. package/dist/mcp/guidance.js +62 -29
  11. package/dist/mcp/handlers/debug/adapter.js +15 -0
  12. package/dist/mcp/handlers/debug/formatters.js +282 -0
  13. package/dist/mcp/handlers/debug/index.js +133 -0
  14. package/dist/mcp/handlers/demo/adapter.js +180 -0
  15. package/dist/mcp/handlers/env/config.js +2 -2
  16. package/dist/mcp/handlers/feedback/index.js +1 -1
  17. package/dist/mcp/handlers/index.js +0 -1
  18. package/dist/mcp/handlers/persona/adapter.js +135 -0
  19. package/dist/mcp/handlers/persona/index.js +237 -8
  20. package/dist/mcp/handlers/persona/schema.js +27 -0
  21. package/dist/mcp/handlers/reference/index.js +6 -4
  22. package/dist/mcp/handlers/sync/adapter.js +200 -0
  23. package/dist/mcp/handlers/workflow/adapter.js +174 -0
  24. package/dist/mcp/handlers/workflow/fix.js +11 -12
  25. package/dist/mcp/handlers/workflow/index.js +12 -40
  26. package/dist/mcp/handlers/workflow/validation.js +1 -1
  27. package/dist/mcp/knowledge-guidance-topics.js +615 -0
  28. package/dist/mcp/knowledge-types.js +7 -0
  29. package/dist/mcp/knowledge.js +75 -1403
  30. package/dist/mcp/resources-dynamic.js +2395 -0
  31. package/dist/mcp/resources-validation.js +408 -0
  32. package/dist/mcp/resources.js +72 -2508
  33. package/dist/mcp/server.js +69 -2825
  34. package/dist/mcp/tools.js +106 -5
  35. package/dist/sdk/client-adapter.js +265 -24
  36. package/dist/sdk/ema-client.js +100 -9
  37. package/dist/sdk/generated/agent-catalog.js +615 -0
  38. package/dist/sdk/generated/api-client/client/client.gen.js +3 -3
  39. package/dist/sdk/generated/api-client/client/index.js +5 -5
  40. package/dist/sdk/generated/api-client/client/utils.gen.js +4 -4
  41. package/dist/sdk/generated/api-client/client.gen.js +1 -1
  42. package/dist/sdk/generated/api-client/core/utils.gen.js +1 -1
  43. package/dist/sdk/generated/api-client/index.js +1 -1
  44. package/dist/sdk/generated/api-client/sdk.gen.js +2 -2
  45. package/dist/sdk/generated/well-known-types.js +99 -0
  46. package/dist/sdk/generated/widget-catalog.js +60 -0
  47. package/dist/sdk/grpc-client.js +115 -1
  48. package/dist/sync/sdk.js +2 -2
  49. package/dist/sync.js +4 -3
  50. package/docs/README.md +17 -9
  51. package/package.json +4 -3
  52. package/.context/public/guides/dashboard-operations.md +0 -349
  53. package/.context/public/guides/email-patterns.md +0 -125
  54. package/.context/public/guides/workflow-builder-patterns.md +0 -708
  55. package/dist/mcp/domain/intent-architect.js +0 -914
  56. package/dist/mcp/domain/quality-gates.js +0 -110
  57. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  58. package/dist/mcp/domain/workflow-intent.js +0 -1806
  59. package/dist/mcp/domain/workflow-merge.js +0 -449
  60. package/dist/mcp/domain/workflow-tracer.js +0 -648
  61. package/dist/mcp/domain/workflow-transformer.js +0 -742
  62. package/dist/mcp/handlers/knowledge/index.js +0 -54
  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
@@ -24,2138 +24,58 @@ export { TOOLKIT_NAME, TOOLKIT_VERSION, TOOLKIT_COMMIT };
24
24
  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
- import { resolveSyncBehavior, loadSyncOptions } from "../sync/sync-options.js";
28
- import { fingerprintPersona, transformWorkflowForTarget } from "../sync.js";
29
- // Direct Sync (Config-less) - extracted to handlers/sync/direct.ts
30
- import { directSyncPersona, directSyncPersonaById, directSyncAll } from "./handlers/sync/direct.js";
31
- import { createVersionStorage } from "../sync/version-storage.js";
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";
48
- // Import extracted handlers
49
- import { handleWorkflow } from "./handlers/workflow/index.js";
27
+ // V2 Tools
28
+ import { generateTools } from "./tools.js";
29
+ // Handler imports (simple 1-2 line handlers stay inline)
30
+ import { handleEnv } from "./handlers/env/index.js";
31
+ import { handleAction } from "./handlers/action/index.js";
32
+ import { handleTemplate } from "./handlers/template/index.js";
33
+ import { handleReference } from "./handlers/reference/index.js";
50
34
  import { handleCatalog } from "./handlers/catalog/index.js";
51
35
  import { handleFeedback } from "./handlers/feedback/index.js";
52
36
  import { recordTelemetry } from "./handlers/feedback/store.js";
53
- import { handleConsolidateDemoData, handleGenerateDemoDocument, handleValidateDemoDocument, handleGetDemoDataTemplate, } from "./handlers/demo/index.js";
54
- // Import from handler utilities (normalizeTriggerType uses generated enum labels)
55
- import { normalizeTriggerType } from "./handlers/utils.js";
56
- // Import from SDK proto-config (CANONICAL widget validation and merging)
57
- import { mergeProtoConfig } from "../sdk/proto-config.js";
58
- // Workflow Auto-Fix Helpers (extracted to handlers/workflow/fix.ts)
59
- import { summarizeWorkflow } from "./handlers/workflow/fix.js";
37
+ // V2 adapters (extracted from server.ts each owns its routing/transformation logic)
38
+ import { handlePersonaAdapter } from "./handlers/persona/adapter.js";
39
+ import { handleWorkflowAdapter } from "./handlers/workflow/adapter.js";
40
+ import { handleSyncAdapter } from "./handlers/sync/adapter.js";
41
+ import { handleDemoAdapter } from "./handlers/demo/adapter.js";
42
+ import { handleDebugAdapter } from "./handlers/debug/adapter.js";
60
43
  // Start token initialization in background (non-blocking)
61
44
  void initializeApiKeyTokens();
62
- // Helper to add env parameter to schema - returns Tool-compatible inputSchema
63
- function withEnvParam(props, required = []) {
64
- const envs = getAvailableEnvironments();
65
- const envNames = envs.map((e) => e.name);
66
- return {
67
- type: "object",
68
- properties: {
69
- ...props,
70
- env: {
71
- type: "string",
72
- description: `Target environment. Available: ${envNames.join(", ")}. Default: ${getDefaultEnvName()}`,
73
- },
74
- },
75
- required,
76
- };
77
- }
78
- // ─────────────────────────────────────────────────────────────────────────────
79
- // Tool Definitions
80
- // ─────────────────────────────────────────────────────────────────────────────
81
- //
82
- // V2 TOOLS (5 tools) - LLM-optimized minimal interface
83
- // - env, persona, catalog, workflow, sync
84
- // - Defined in: ./tools.ts
85
- //
86
- // NAMING CONVENTION:
87
- // - Tool names are defined as BASE NAMES (e.g., "persona")
88
- // - MCP clients prefix with "mcp_{server}_" (e.g., "mcp_ema_persona")
89
- //
90
- // ─────────────────────────────────────────────────────────────────────────────
91
- /**
92
- * Generate all available tools
93
- *
94
- * V2: 5 tools (persona, catalog, workflow, sync, env) - LLM-optimized
95
- *
96
- * Why V2:
97
- * - Minimal tool count optimizes LLM tool selection
98
- * - Data operations under persona (always persona-scoped)
99
- * - Catalog consolidates all reference data (actions, templates, etc.)
100
- * - Clear separation: entity (persona), reference (catalog), operation (sync, workflow)
101
- */
102
- function generateAllTools() {
103
- const envNames = getAvailableEnvironments().map(e => e.name);
104
- const defaultEnv = getDefaultEnvName();
105
- return generateTools(envNames, defaultEnv);
106
- }
107
- // Generate tools (called once at module load)
108
- const TOOLS = generateAllTools();
109
- // ─────────────────────────────────────────────────────────────────────────────
110
- // ADDITIONAL TOOLS (special-purpose inline tools)
111
- // These handle specific operations that don't fit the V2 pattern
112
- // ─────────────────────────────────────────────────────────────────────────────
113
- const ADDITIONAL_TOOLS = [
114
- // NOTE: Tools (persona, catalog, workflow, sync, env) are in ./tools.ts
115
- //
116
- // Special-purpose tools here:
117
- // - compile_workflow - Direct workflow compilation
118
- // - Demo data tools - RAG document generation/validation
119
- // - Data source tools - Upload/delete/manage knowledge sources
120
- // toggle_embedding
121
- // ─────────────────────────────────────────────────────────────────────────────
122
- // Workflow Compilation - Template-driven, no biased patterns
123
- // Read ema://catalog/patterns for pattern references, then construct nodes
124
- // ─────────────────────────────────────────────────────────────────────────────
125
- {
126
- name: "compile_workflow",
127
- description: `🔧 Compile workflow from node specification. Template-driven - read \`ema://catalog/patterns\` for pattern templates, construct nodes, then compile.
128
-
129
- **Process**:
130
- 1. Read \`ema://catalog/patterns\` for pattern reference
131
- 2. Read \`ema://catalog/agents\` for available actions
132
- 3. Construct nodes array based on requirements
133
- 4. Call compile_workflow with nodes
134
- 5. Use deploy_workflow to deploy result
135
-
136
- **Example nodes** (KB search):
137
- \`\`\`json
138
- [
139
- { "id": "trigger", "action_type": "chat_trigger", "display_name": "Trigger" },
140
- { "id": "search", "action_type": "search", "display_name": "Search", "inputs": { "query": { "type": "action_output", "action_name": "trigger", "output": "user_query" } } },
141
- { "id": "respond", "action_type": "respond_with_sources", "display_name": "Respond", "inputs": { "search_results": { "type": "action_output", "action_name": "search", "output": "search_results" } } }
142
- ]
143
- \`\`\``,
144
- inputSchema: {
145
- type: "object",
146
- properties: {
147
- name: { type: "string", description: "Workflow name" },
148
- description: { type: "string", description: "Workflow description" },
149
- persona_type: { type: "string", enum: ["voice", "chat", "dashboard"], description: "AI type" },
150
- nodes: {
151
- type: "array",
152
- description: "Node definitions",
153
- items: {
154
- type: "object",
155
- properties: {
156
- id: { type: "string", description: "Node ID" },
157
- action_type: { type: "string", description: "Action type (e.g., chat_trigger, search, respond_with_sources)" },
158
- display_name: { type: "string", description: "Display name" },
159
- description: { type: "string", description: "Optional description" },
160
- inputs: { type: "object", description: "Input bindings (key: input name, value: binding spec)" },
161
- run_if: {
162
- type: "object",
163
- description: "Conditional execution",
164
- properties: {
165
- source_action: { type: "string" },
166
- source_output: { type: "string", description: "Output name to check" },
167
- operator: { type: "string", enum: ["eq", "neq", "gt", "lt", "gte", "lte"], description: "Comparison operator" },
168
- value: { type: "string", description: "Value to compare against" },
169
- },
170
- },
171
- categories: {
172
- type: "array",
173
- description: "Categories for categorizer nodes",
174
- items: {
175
- type: "object",
176
- properties: {
177
- name: { type: "string", description: "Category name (e.g., 'Password Reset', 'Fallback')" },
178
- description: { type: "string", description: "When this category triggers" },
179
- examples: { type: "array", items: { type: "string" }, description: "Example phrases" },
180
- },
181
- },
182
- },
183
- tools: {
184
- type: "array",
185
- description: "External tools for external_action_caller nodes",
186
- items: {
187
- type: "object",
188
- properties: {
189
- name: { type: "string", description: "Tool name" },
190
- namespace: { type: "string", description: "Tool namespace" },
191
- },
192
- },
193
- },
194
- disable_human_interaction: { type: "boolean", description: "If true, disable HITL for this node" },
195
- },
196
- required: ["id", "action_type", "display_name"],
197
- },
198
- },
199
- result_mappings: {
200
- type: "array",
201
- description: "Which node outputs map to WORKFLOW_OUTPUT",
202
- items: {
203
- type: "object",
204
- properties: {
205
- node_id: { type: "string", description: "Node ID" },
206
- output: { type: "string", description: "Output name from the node" },
207
- },
208
- required: ["node_id", "output"],
209
- },
210
- },
211
- },
212
- required: ["name", "description", "persona_type", "nodes", "result_mappings"],
213
- },
214
- },
215
- // ─────────────────────────────────────────────────────────────────────────
216
- // Data Source Management
217
- // TODO: Add support for 3rd party data sources (Google Drive, SharePoint, Confluence, etc.)
218
- // ─────────────────────────────────────────────────────────────────────────
219
- {
220
- name: "upload_data_source",
221
- 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.",
222
- inputSchema: {
223
- type: "object",
224
- properties: {
225
- persona_id: {
226
- type: "string",
227
- description: "The AI Employee ID to upload the file to",
228
- },
229
- file_path: {
230
- type: "string",
231
- description: "Absolute path to the file on the local filesystem",
232
- },
233
- tags: {
234
- type: "string",
235
- description: "Optional tags for categorizing the file (default: 'fileUpload')",
236
- },
237
- env: {
238
- type: "string",
239
- description: "Target environment. Available: dev, demo, staging. Default: demo",
240
- },
241
- },
242
- required: ["persona_id", "file_path"],
243
- },
244
- },
245
- {
246
- name: "delete_data_source",
247
- description: "Delete a file from an AI Employee's knowledge base.",
248
- inputSchema: {
249
- type: "object",
250
- properties: {
251
- persona_id: {
252
- type: "string",
253
- description: "The AI Employee ID",
254
- },
255
- file_id: {
256
- type: "string",
257
- description: "The file ID to delete (from list_data_sources)",
258
- },
259
- env: {
260
- type: "string",
261
- description: "Target environment. Available: dev, demo, staging. Default: demo",
262
- },
263
- },
264
- required: ["persona_id", "file_id"],
265
- },
266
- },
267
- {
268
- name: "list_data_sources",
269
- description: "List data sources (knowledge base files/documents) configured for an AI Employee, including upload status and file count.",
270
- inputSchema: {
271
- type: "object",
272
- properties: {
273
- persona_id: {
274
- type: "string",
275
- description: "The AI Employee ID to list data sources for",
276
- },
277
- env: {
278
- type: "string",
279
- description: "Target environment. Available: dev, demo, staging. Default: demo",
280
- },
281
- },
282
- required: ["persona_id"],
283
- },
284
- },
285
- {
286
- name: "get_embedding_status",
287
- description: "Get the embedding/RAG status for an AI Employee's knowledge base.",
288
- inputSchema: {
289
- type: "object",
290
- properties: {
291
- persona_id: {
292
- type: "string",
293
- description: "The AI Employee ID",
294
- },
295
- env: {
296
- type: "string",
297
- description: "Target environment. Available: dev, demo, staging. Default: demo",
298
- },
299
- },
300
- required: ["persona_id"],
301
- },
302
- },
303
- {
304
- name: "toggle_embedding",
305
- description: "Enable or disable embedding/RAG for an AI Employee's knowledge base. When enabled, uploaded documents are indexed for semantic search.",
306
- inputSchema: {
307
- type: "object",
308
- properties: {
309
- persona_id: {
310
- type: "string",
311
- description: "The AI Employee ID",
312
- },
313
- enabled: {
314
- type: "boolean",
315
- description: "Whether to enable (true) or disable (false) embedding",
316
- },
317
- env: {
318
- type: "string",
319
- description: "Target environment. Available: dev, demo, staging. Default: demo",
320
- },
321
- },
322
- required: ["persona_id", "enabled"],
323
- },
324
- },
325
- // ─────────────────────────────────────────────────────────────────────────
326
- // Unified Workflow Tool
327
- // Accepts any input: natural language, partial spec, full spec, or persona_id
328
- // Normalizes to WorkflowIntent → validates → generates → deploys
329
- // ─────────────────────────────────────────────────────────────────────────
330
- {
331
- name: "workflow",
332
- description: `🔧 UNIFIED workflow tool. Accepts ANY input and normalizes it.
333
-
334
- **Input types** (auto-detected):
335
- - Natural language: "IT helpdesk that creates ServiceNow tickets"
336
- - Partial spec: { intents: [...], tools: [...] }
337
- - Full nodes spec: { nodes: [...], result_mappings: [...] }
338
- - Existing persona: persona_id to analyze/improve
339
-
340
- **Process**:
341
- 1. Parse input → WorkflowIntent (normalized representation)
342
- 2. Validate completeness → return questions if incomplete
343
- 3. Generate workflow (local compile or Auto Builder)
344
- 4. Validate output → auto-fix if enabled
345
- 5. Deploy if persona_id provided
346
-
347
- **Examples**:
348
- \`\`\`
349
- workflow("IT helpdesk bot with KB search")
350
- workflow({ intents: [{name: "Billing", handler: "search"}], tools: [{namespace: "service_now", action: "Create_Ticket"}] })
351
- workflow(persona_id, mode="improve")
352
- \`\`\``,
353
- inputSchema: withEnvParam({
354
- input: {
355
- description: "Natural language description, partial spec object, or full nodes spec",
356
- },
357
- persona_id: {
358
- type: "string",
359
- description: "For deployment OR to analyze/improve existing workflow",
360
- },
361
- mode: {
362
- type: "string",
363
- enum: ["generate", "improve", "analyze"],
364
- description: "generate (default): Create new workflow. improve: Fix existing. analyze: Validate only.",
365
- },
366
- persona_type: {
367
- type: "string",
368
- enum: ["voice", "chat", "dashboard"],
369
- description: "AI type (default: chat, auto-detected from input)",
370
- },
371
- use_autobuilder: {
372
- type: "boolean",
373
- description: "Force Auto Builder for generation (default: auto-decide based on complexity)",
374
- },
375
- auto_deploy: {
376
- type: "boolean",
377
- description: "Deploy immediately (default: false - returns preview)",
378
- },
379
- auto_fix: {
380
- type: "boolean",
381
- description: "Auto-fix detected issues (default: true)",
382
- },
383
- }, []),
384
- },
385
- // ─────────────────────────────────────────────────────────────────────────
386
- // Demo Data Management
387
- // Tools for consolidating, transforming, and preparing mock data for RAG
388
- // ─────────────────────────────────────────────────────────────────────────
389
- {
390
- name: "consolidate_demo_data",
391
- description: `Transform normalized JSON source files into RAG-optimized Markdown documents for Ema knowledge bases.
392
-
393
- This tool pre-joins related data (like customers + orders + tickets) into denormalized entity documents that work well with semantic search.
394
-
395
- **Why this matters**: RAG systems can't do SQL-style joins at query time. Data relationships must be explicit in the document content.
396
-
397
- **Supported patterns**:
398
- - Entity consolidation (customer with all their orders, tickets, contacts)
399
- - Product catalogs with cross-references
400
- - Scenario documents for demos
401
-
402
- **Output format**: Markdown files with embedded tables, metadata comments, and narrative summaries optimized for Ema's search agents.`,
403
- inputSchema: {
404
- type: "object",
405
- properties: {
406
- source_dir: {
407
- type: "string",
408
- description: "Path to directory containing source JSON files (e.g., './data/source')",
409
- },
410
- output_dir: {
411
- type: "string",
412
- description: "Path to output directory for generated Markdown files (e.g., './data/knowledge-base')",
413
- },
414
- entity_type: {
415
- type: "string",
416
- enum: ["customer", "product", "employee", "scenario", "custom"],
417
- description: "Type of entity being consolidated. Determines document structure.",
418
- },
419
- primary_file: {
420
- type: "string",
421
- description: "Name of the primary JSON file (e.g., 'customers.json')",
422
- },
423
- joins: {
424
- type: "array",
425
- items: {
426
- type: "object",
427
- properties: {
428
- file: { type: "string", description: "JSON file to join (e.g., 'orders.json')" },
429
- on: { type: "string", description: "Foreign key field (e.g., 'customerId')" },
430
- as: { type: "string", description: "Name for the joined data (e.g., 'orders')" },
431
- },
432
- },
433
- description: "Array of files to join with the primary file",
434
- },
435
- id_field: {
436
- type: "string",
437
- description: "Field name for the entity ID (default: 'id')",
438
- },
439
- name_field: {
440
- type: "string",
441
- description: "Field name for the entity name (default: 'name')",
442
- },
443
- tags: {
444
- type: "string",
445
- description: "Comma-separated tags to include in document metadata",
446
- },
447
- },
448
- required: ["source_dir", "output_dir", "entity_type", "primary_file"],
449
- },
450
- },
451
- {
452
- name: "generate_demo_document",
453
- description: `Generate a single RAG-optimized Markdown document from provided JSON data.
454
-
455
- Use this for:
456
- - Creating individual entity documents programmatically
457
- - Testing document formats before batch consolidation
458
- - Custom document generation with specific data
459
-
460
- The output follows Ema's knowledge base best practices with metadata comments, tables, and narrative context.`,
461
- inputSchema: {
462
- type: "object",
463
- properties: {
464
- entity_type: {
465
- type: "string",
466
- enum: ["customer", "product", "employee", "scenario", "reference"],
467
- description: "Type of document to generate",
468
- },
469
- data: {
470
- type: "object",
471
- description: "The entity data as a JSON object",
472
- },
473
- related_data: {
474
- type: "object",
475
- description: "Related data to include (e.g., { orders: [...], tickets: [...] })",
476
- },
477
- output_path: {
478
- type: "string",
479
- description: "Optional: Path to save the generated document. If not provided, returns the content.",
480
- },
481
- tags: {
482
- type: "string",
483
- description: "Comma-separated tags for metadata",
484
- },
485
- },
486
- required: ["entity_type", "data"],
487
- },
488
- },
489
- {
490
- name: "validate_demo_document",
491
- description: `Validate a Markdown document for RAG optimization and Ema compatibility.
492
-
493
- Checks for:
494
- - Required metadata comments (ema_entity, ema_id, ema_tags)
495
- - Table formatting
496
- - Narrative context presence
497
- - Cross-reference consistency
498
- - Filename conventions`,
499
- inputSchema: {
500
- type: "object",
501
- properties: {
502
- file_path: {
503
- type: "string",
504
- description: "Path to the Markdown file to validate",
505
- },
506
- content: {
507
- type: "string",
508
- description: "Alternatively, provide the document content directly",
509
- },
510
- },
511
- required: [],
512
- },
513
- },
514
- {
515
- name: "get_demo_data_template",
516
- description: `Get a template for demo data documents based on entity type.
517
-
518
- Returns:
519
- - Source JSON schema (what fields to include)
520
- - Output Markdown template
521
- - Best practices for the entity type
522
- - Example data`,
523
- inputSchema: {
524
- type: "object",
525
- properties: {
526
- entity_type: {
527
- type: "string",
528
- enum: ["customer", "product", "employee", "scenario", "reference"],
529
- description: "Type of template to get",
530
- },
531
- include_example: {
532
- type: "boolean",
533
- description: "Include example data (default: true)",
534
- },
535
- },
536
- required: ["entity_type"],
537
- },
538
- },
539
- ];
540
- const toolHandlers = {
541
- // Environment Management
542
- list_environments: async () => {
543
- const envs = getAvailableEnvironments();
544
- const defaultEnv = getDefaultEnvName();
545
- return {
546
- default_environment: defaultEnv,
547
- environments: envs.map((e) => ({
548
- name: e.name,
549
- url: e.baseUrl,
550
- is_default: e.name === defaultEnv,
551
- })),
552
- };
553
- },
554
- // ─────────────────────────────────────────────────────────────────────────
555
- // AI Employee Handlers (Consolidated)
556
- // ─────────────────────────────────────────────────────────────────────────
557
- get_persona: async (args) => {
558
- const client = createClient(args.env);
559
- const identifier = String(args.identifier);
560
- const includeWorkflow = args.include_workflow === true;
561
- const includeFingerprint = args.include_fingerprint === true;
562
- // Auto-detect: UUIDs are 36 chars with dashes, otherwise it's a name
563
- 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);
564
- let persona;
565
- if (isUUID) {
566
- // Fetch by ID - use full fetch if workflow needed, otherwise list
567
- if (includeWorkflow || includeFingerprint) {
568
- persona = (await client.getPersonaById(identifier)) ?? undefined;
569
- }
570
- else {
571
- const personas = await client.getPersonasForTenant();
572
- persona = personas.find((p) => p.id === identifier);
573
- }
574
- }
575
- else {
576
- // Fetch by name - always need to list first to find ID
577
- const personas = await client.getPersonasForTenant();
578
- persona = personas.find((p) => p.name === identifier);
579
- // If found and need workflow, fetch full details
580
- if (persona && (includeWorkflow || includeFingerprint)) {
581
- persona = (await client.getPersonaById(persona.id)) ?? undefined;
582
- }
583
- }
584
- if (!persona) {
585
- throw new Error(`AI Employee not found: ${identifier} (searched by ${isUUID ? "ID" : "name"})`);
586
- }
587
- // Build response
588
- const result = {
589
- environment: client["env"].name,
590
- ai_employee: persona,
591
- };
592
- // Add fingerprint if requested
593
- if (includeFingerprint) {
594
- result.fingerprint = fingerprintPersona(persona);
595
- }
596
- return result;
597
- },
598
- find_personas: async (args) => {
599
- const client = createClient(args.env);
600
- let personas = await client.getPersonasForTenant();
601
- // Apply filters
602
- if (args.query) {
603
- const q = String(args.query).toLowerCase();
604
- personas = personas.filter((p) => p.name?.toLowerCase().includes(q));
605
- }
606
- if (args.status) {
607
- const f = String(args.status).toLowerCase();
608
- personas = personas.filter((p) => p.status?.toLowerCase() === f);
609
- }
610
- if (args.trigger_type) {
611
- const f = String(args.trigger_type).toLowerCase();
612
- personas = personas.filter((p) => p.trigger_type?.toLowerCase() === f);
613
- }
614
- if (args.access_level) {
615
- const f = String(args.access_level).toLowerCase();
616
- personas = personas.filter((p) => p.access_level?.toLowerCase() === f);
617
- }
618
- if (typeof args.has_workflow === "boolean") {
619
- personas = personas.filter((p) => args.has_workflow ? !!p.workflow_id : !p.workflow_id);
620
- }
621
- if (typeof args.embedding_enabled === "boolean") {
622
- personas = personas.filter((p) => p.embedding_enabled === args.embedding_enabled);
623
- }
624
- const limit = typeof args.limit === "number" ? args.limit : 50;
625
- personas = personas.slice(0, limit);
626
- return {
627
- environment: client["env"].name,
628
- count: personas.length,
629
- ai_employees: personas.map((p) => ({
630
- id: p.id,
631
- name: p.name,
632
- description: p.description,
633
- status: p.status,
634
- template_id: p.template_id ?? p.templateId,
635
- workflow_id: p.workflow_id,
636
- trigger_type: p.trigger_type,
637
- access_level: p.access_level,
638
- embedding_enabled: p.embedding_enabled,
639
- })),
640
- };
641
- },
642
- // ─────────────────────────────────────────────────────────────────────────
643
- // AI Employee CRUD Handlers
644
- // ─────────────────────────────────────────────────────────────────────────
645
- create_ai_employee: async (args) => {
646
- const client = createClient(args.env);
647
- // Determine template_id: explicit > dynamic lookup based on persona_type
648
- let templateId = args.template_id ? String(args.template_id) : undefined;
649
- const sourcePersonaId = args.source_persona_id ? String(args.source_persona_id) : undefined;
650
- // If no template_id or source_persona_id, use dynamic template lookup
651
- if (!templateId && !sourcePersonaId) {
652
- const personaType = args.persona_type ? String(args.persona_type).toLowerCase() : null;
653
- if (personaType) {
654
- // Dynamic template lookup - templates are tenant-specific
655
- const templates = await client.getPersonaTemplates();
656
- const matchingTemplate = templates.find(t => normalizeTriggerType(t.trigger_type) === personaType);
657
- if (matchingTemplate) {
658
- templateId = matchingTemplate.id;
659
- }
660
- else {
661
- const availableTypes = [...new Set(templates.map(t => normalizeTriggerType(t.trigger_type)).filter(Boolean))];
662
- throw new Error(`No template found for type "${personaType}". Available types: ${availableTypes.join(", ") || "none"}`);
663
- }
664
- }
665
- else {
666
- throw new Error("Must provide template_id, source_persona_id, or persona_type");
667
- }
668
- }
669
- const req = {
670
- name: String(args.name),
671
- description: args.description ? String(args.description) : undefined,
672
- template_id: templateId,
673
- source_persona_id: sourcePersonaId,
674
- // Note: trigger_type is determined by template, not passed separately
675
- };
676
- const result = await client.createAiEmployee(req);
677
- return {
678
- environment: client["env"].name,
679
- success: true,
680
- persona_id: result.persona_id ?? result.id,
681
- status: result.status ?? "created",
682
- template_used: templateId,
683
- };
684
- },
685
- update_ai_employee: async (args) => {
686
- const client = createClient(args.env);
687
- const personaId = String(args.persona_id);
688
- // Use getPersonaById for complete data (including full proto_config)
689
- const existing = await client.getPersonaById(personaId);
690
- if (!existing)
691
- throw new Error(`AI Employee not found: ${personaId}`);
692
- // Merge proto_config using SDK function (deep merges widgets by name)
693
- const mergedProtoConfig = mergeProtoConfig(existing.proto_config, args.proto_config);
694
- // Check if user is trying to pass workflow - redirect them to deploy_workflow
695
- if (args.workflow) {
696
- return {
697
- environment: client["env"].name,
698
- success: false,
699
- error: "workflow_parameter_deprecated",
700
- message: "The 'workflow' parameter has been removed from update_ai_employee. " +
701
- "Use workflow(mode='get' → mode='deploy') instead (it provides validation and strict stale-state protection).",
702
- suggestion: {
703
- tool: "workflow",
704
- parameters: {
705
- mode: "deploy",
706
- persona_id: personaId,
707
- base_fingerprint: "<fingerprint_from_workflow_get>",
708
- workflow_def: "your_workflow_here",
709
- },
710
- },
711
- };
712
- }
713
- // CRITICAL: The Ema API requires workflow to be sent along with proto_config
714
- // for proto_config changes to persist. Without workflow, updates silently fail.
715
- // GET returns 'workflow_def', UPDATE expects 'workflow' field.
716
- const existingWorkflow = (existing.workflow_def ?? existing.workflow);
717
- const req = {
718
- persona_id: personaId,
719
- name: args.name ? String(args.name) : undefined,
720
- description: args.description ? String(args.description) : undefined,
721
- proto_config: mergedProtoConfig,
722
- workflow: existingWorkflow, // Required for proto_config to persist
723
- embedding_enabled: typeof args.embedding_enabled === "boolean" ? args.embedding_enabled : undefined,
724
- enabled_by_user: typeof args.enabled_by_user === "boolean" ? args.enabled_by_user : undefined,
725
- };
726
- await client.updateAiEmployee(req);
727
- return {
728
- environment: client["env"].name,
729
- success: true,
730
- persona_id: personaId,
731
- persona_name: existing.name,
732
- updated_fields: {
733
- name: !!args.name,
734
- description: !!args.description,
735
- proto_config: !!args.proto_config,
736
- embedding_enabled: typeof args.embedding_enabled === "boolean",
737
- enabled_by_user: typeof args.enabled_by_user === "boolean",
738
- },
739
- note: "For workflow changes, use deploy_workflow which provides validation and auto-fix.",
740
- };
741
- },
742
- deploy_workflow: async (args) => {
743
- const client = createClient(args.env);
744
- const personaId = String(args.persona_id);
745
- const validateFirst = args.validate_first !== false; // default true
746
- const autoFix = args.auto_fix === true; // default false
747
- const force = args.force === true;
748
- const baseFingerprint = args.base_fingerprint;
749
- // Get existing persona with full details
750
- const persona = await client.getPersonaById(personaId);
751
- if (!persona)
752
- throw new Error(`AI Employee not found: ${personaId}`);
753
- // STRICT: stale-state protection + required snapshot (same safety as workflow tool)
754
- const currentFp = fingerprintPersona(persona);
755
- if (!force && !baseFingerprint) {
756
- return {
757
- environment: client["env"].name,
758
- success: false,
759
- error: "base_fingerprint_required",
760
- persona_id: personaId,
761
- current_fingerprint: currentFp,
762
- message: "base_fingerprint is required for deploy_workflow (stale-state protection). " +
763
- "Re-fetch the latest state and retry.",
764
- hint: "Prefer: workflow(mode='get') → workflow(mode='deploy', base_fingerprint='<fingerprint>', ...). " +
765
- "Use force=true only for emergency overrides.",
766
- };
767
- }
768
- if (!force && baseFingerprint && baseFingerprint !== currentFp) {
769
- return {
770
- environment: client["env"].name,
771
- success: false,
772
- error: "fingerprint_mismatch",
773
- persona_id: personaId,
774
- base_fingerprint: baseFingerprint,
775
- current_fingerprint: currentFp,
776
- message: "Persona changed since you last fetched it (fingerprint mismatch).",
777
- hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. " +
778
- "Use force=true only if you intend to overwrite out-of-band changes.",
779
- };
780
- }
781
- // Required pre-deploy snapshot (local)
782
- try {
783
- const storage = createVersionStorage(process.cwd());
784
- const engine = createVersionPolicyEngine(storage);
785
- const snap = engine.forceCreateVersion(persona, {
786
- environment: client["env"].name,
787
- tenant_id: client["env"].name,
788
- message: "Pre-deploy snapshot (deploy_workflow)",
789
- created_by: "mcp-toolkit",
790
- });
791
- if (!snap.created || !snap.version) {
792
- if (!force) {
793
- return {
794
- environment: client["env"].name,
795
- success: false,
796
- error: "pre_deploy_snapshot_failed",
797
- persona_id: personaId,
798
- details: snap.reason,
799
- hint: "Fix local snapshot storage or retry with force=true for emergency override.",
800
- };
801
- }
802
- }
803
- }
804
- catch (e) {
805
- if (!force) {
806
- return {
807
- environment: client["env"].name,
808
- success: false,
809
- error: "pre_deploy_snapshot_failed",
810
- persona_id: personaId,
811
- details: e instanceof Error ? e.message : String(e),
812
- hint: "Fix local snapshot storage or retry with force=true for emergency override.",
813
- };
814
- }
815
- }
816
- let workflowDef = args.workflow_def;
817
- const protoConfig = args.proto_config;
818
- if (!workflowDef && !protoConfig) {
819
- throw new Error("At least one of workflow_def or proto_config must be provided");
820
- }
821
- // Sanitize workflow_def to prevent server-side crashes
822
- if (workflowDef) {
823
- // Fix enumTypes - remove entries with empty/missing names (causes server panic)
824
- // EnumType structure in proto: { name: { name: { name: "string", namespaces: [] } }, options: [...] }
825
- // The backend calls FlattenNamespacedName(enum.Name.Name) which panics if Name.Name is nil
826
- const enumTypes = workflowDef.enumTypes;
827
- if (Array.isArray(enumTypes)) {
828
- const validEnumTypes = enumTypes.filter(e => {
829
- // Navigate the nested structure: e.name.name.name
830
- const outerName = e.name;
831
- const innerName = outerName?.name;
832
- const actualName = innerName?.name;
833
- // Must have the full structure with a non-empty string name
834
- return typeof actualName === "string" && actualName.trim().length > 0;
835
- });
836
- if (validEnumTypes.length > 0) {
837
- workflowDef.enumTypes = validEnumTypes;
838
- }
839
- else {
840
- // Remove empty enumTypes entirely
841
- delete workflowDef.enumTypes;
842
- }
843
- }
844
- // Ensure all actions have a 'name' field (node identifier)
845
- const actions = workflowDef.actions;
846
- if (Array.isArray(actions)) {
847
- for (const action of actions) {
848
- // If action has 'actionName' but not 'name', fix it
849
- if (!action.name && action.actionName) {
850
- action.name = action.actionName;
851
- }
852
- // Ensure name is a non-empty string
853
- if (!action.name || (typeof action.name === "string" && action.name.trim().length === 0)) {
854
- // Try to derive from action type
855
- const actionType = action.action;
856
- if (actionType?.name?.name) {
857
- action.name = `${actionType.name.name}_${actions.indexOf(action)}`;
858
- }
859
- }
860
- // CRITICAL: Normalize action structure to include required empty fields
861
- // The backend expects these fields to exist (even if empty) or it may 500
862
- if (action.typeArguments === undefined) {
863
- action.typeArguments = {};
864
- }
865
- if (action.tools === undefined) {
866
- action.tools = [];
867
- }
868
- if (action.disableHumanInteraction === undefined) {
869
- action.disableHumanInteraction = false;
870
- }
871
- // Ensure displaySettings exists and has required structure
872
- if (!action.displaySettings) {
873
- action.displaySettings = {
874
- displayName: String(action.name || ""),
875
- coordinates: { x: 0, y: 0 },
876
- description: "",
877
- showConfig: 0,
878
- };
879
- }
880
- else {
881
- const ds = action.displaySettings;
882
- if (ds.description === undefined)
883
- ds.description = "";
884
- if (ds.showConfig === undefined)
885
- ds.showConfig = 0;
886
- }
887
- // Ensure inputs exists
888
- if (action.inputs === undefined) {
889
- action.inputs = {};
890
- }
891
- // Normalize runIf operator enum: backend proto expects numeric values (e.g. 1),
892
- // but some JSON payloads use string enums (e.g. "OPERATOR_EQ") which can 500.
893
- const runIf = action.runIf;
894
- if (runIf && typeof runIf === "object") {
895
- const op = runIf.operator;
896
- if (typeof op === "string") {
897
- const opMap = {
898
- OPERATOR_EQ: 1,
899
- OPERATOR_NEQ: 2,
900
- OPERATOR_GT: 3,
901
- OPERATOR_GTE: 4,
902
- OPERATOR_LT: 5,
903
- OPERATOR_LTE: 6,
904
- OPERATOR_IN: 7,
905
- OPERATOR_NOT_IN: 8,
906
- };
907
- if (opMap[op] !== undefined)
908
- runIf.operator = opMap[op];
909
- }
910
- }
911
- }
912
- }
913
- }
914
- // Get existing workflow info
915
- const existingWorkflow = persona.workflow_def;
916
- const existingWorkflowId = persona.workflow_id;
917
- // Copy missing top-level workflow fields from existing workflow.
918
- // Some backends are strict about presence of these keys.
919
- if (workflowDef && existingWorkflow) {
920
- const copyIfMissing = (k) => {
921
- if (workflowDef[k] === undefined && existingWorkflow[k] !== undefined) {
922
- workflowDef[k] = JSON.parse(JSON.stringify(existingWorkflow[k]));
923
- }
924
- };
925
- copyIfMissing("workflowInputs");
926
- copyIfMissing("namedResults");
927
- copyIfMissing("displayName");
928
- copyIfMissing("description");
929
- copyIfMissing("namedResultsEditable");
930
- copyIfMissing("namedResultsEnabled");
931
- copyIfMissing("edges");
932
- }
933
- // Determine deployment strategy
934
- const hasExistingWorkflow = !!existingWorkflowId;
935
- let deploymentMethod = hasExistingWorkflow ? "direct_api" : "autobuilder";
936
- // === WORKFLOW ID TRANSFORMATION ===
937
- // If workflow was generated for a different persona, transform IDs to target persona
938
- // (No brownfield merging - we do full workflow replacement)
939
- if (workflowDef && hasExistingWorkflow && existingWorkflow) {
940
- // Extract source persona ID from incoming workflow (if any)
941
- const incomingWfName = workflowDef.workflowName;
942
- const sourcePersonaId = incomingWfName?.name?.namespaces?.[2]; // Usually at index 2: ["ema", "templates", "<persona_id>"]
943
- if (sourcePersonaId && sourcePersonaId !== personaId) {
944
- // Workflow was generated for a different persona - transform IDs
945
- workflowDef = transformWorkflowForTarget(workflowDef, sourcePersonaId, personaId);
946
- }
947
- }
948
- else if (workflowDef && hasExistingWorkflow && !existingWorkflow) {
949
- // Has workflow_id but no workflow_def (edge case) - construct workflowName from workflow_id
950
- // workflow_id format: "ema.templates.<persona_id>.default" or similar
951
- const parts = existingWorkflowId.split(".");
952
- if (parts.length >= 3) {
953
- workflowDef.workflowName = {
954
- name: {
955
- namespaces: parts.slice(0, -1),
956
- name: parts[parts.length - 1],
957
- },
958
- };
959
- }
960
- }
961
- // Track fixes applied
962
- const appliedFixes = [];
963
- let fixAttempted = false;
964
- // Validation is now done by backend when workflow is deployed
965
- // LLM should use ema://rules/* for pre-validation guidance
966
- const validationResults = { valid: true, issues: [] };
967
- if (validateFirst && workflowDef) {
968
- // DEPRECATED: MCP no longer pre-validates workflows
969
- // Backend validation happens on deploy
970
- // LLM uses ema://rules/anti-patterns for analysis
971
- console.warn("[DEPRECATED] validateFirst is deprecated - backend validates on deploy");
972
- if (autoFix) {
973
- // Auto-fix is removed - return guidance for LLM
974
- return {
975
- environment: client["env"].name,
976
- success: false,
977
- persona_id: personaId,
978
- persona_name: persona.name,
979
- _deprecation_notice: "autoFix is deprecated. Use LLM analysis with ema://rules/* instead.",
980
- _guidance: [
981
- "1. Fetch ema://rules/anti-patterns",
982
- "2. Analyze workflow against rules",
983
- "3. Make structured modifications",
984
- "4. Deploy via workflow(mode='deploy')",
985
- ],
986
- applied_fixes: [],
987
- };
988
- }
989
- }
990
- // If validation failed and not forcing, return the issues
991
- if (!validationResults.valid) {
992
- return {
993
- environment: client["env"].name,
994
- success: false,
995
- persona_id: personaId,
996
- persona_name: persona.name,
997
- validation_failed: true,
998
- issues: validationResults.issues,
999
- auto_fix_attempted: fixAttempted,
1000
- fixes_applied: appliedFixes.filter(f => f.applied),
1001
- fixes_failed: appliedFixes.filter(f => !f.applied),
1002
- remaining_issues: validationResults.issues.length,
1003
- hint: autoFix
1004
- ? "Some issues could not be auto-fixed. Review the remaining issues and fix manually, or set validate_first=false to skip validation (not recommended)"
1005
- : "Fix the issues above, enable auto_fix=true for automatic fixes, or set validate_first=false to skip validation (not recommended)",
1006
- };
1007
- }
1008
- // Merge proto_config using SDK function (deep merges widgets by name)
1009
- const mergedProtoConfig = mergeProtoConfig(persona.proto_config, protoConfig);
1010
- // Auto-fix malformed HITL runIf patterns BEFORE deploy (H1: production-grade fix)
1011
- // Pattern: "hitl_status_HITL Success" should be output="hitl_status", enumValue="HITL Success"
1012
- let hitlFixCount = 0;
1013
- if (workflowDef) {
1014
- const wfActions = workflowDef.actions ?? [];
1015
- for (const action of wfActions) {
1016
- const runIfAction = action.runIf;
1017
- if (!runIfAction)
1018
- continue;
1019
- const lhsAction = runIfAction.lhs;
1020
- const rhsAction = runIfAction.rhs;
1021
- if (lhsAction?.actionOutput && rhsAction?.inline) {
1022
- const actionOutputField = lhsAction.actionOutput;
1023
- const inlineRhsField = rhsAction.inline;
1024
- const outputStr = String(actionOutputField.output ?? "");
1025
- // Detect malformed HITL patterns: "hitl_status_HITL Success" or "hitl_status HITL Success"
1026
- const hitlPatternMatch = outputStr.match(/^hitl_status[_\s]?(HITL[ _]?(?:Success|Failure))$/i);
1027
- if (hitlPatternMatch) {
1028
- // Extract and normalize the enum value
1029
- let correctedEnumVal = hitlPatternMatch[1].replace(/_/g, " ");
1030
- if (correctedEnumVal.toLowerCase().includes("success")) {
1031
- correctedEnumVal = "HITL Success";
1032
- }
1033
- else if (correctedEnumVal.toLowerCase().includes("failure")) {
1034
- correctedEnumVal = "HITL Failure";
1035
- }
1036
- // Apply the fix
1037
- actionOutputField.output = "hitl_status";
1038
- inlineRhsField.enumValue = correctedEnumVal;
1039
- hitlFixCount++;
1040
- }
1041
- }
1042
- }
1043
- }
1044
- // Build update request
1045
- const req = {
1046
- persona_id: personaId,
1047
- proto_config: mergedProtoConfig,
1048
- workflow: workflowDef,
1049
- };
1050
- // Deployment attempt with automatic fallback
1051
- let deployedVia = "direct_api";
1052
- let autobuilderResult;
1053
- if (deploymentMethod === "direct_api") {
1054
- try {
1055
- await client.updateAiEmployee(req);
1056
- }
1057
- catch (err) {
1058
- const errorMessage = err instanceof Error ? err.message : String(err);
1059
- // If direct API fails due to "no existing workflow", try Auto Builder
1060
- if (errorMessage.includes("Cannot set persona workflow without existing workflow") && workflowDef) {
1061
- deploymentMethod = "autobuilder";
1062
- }
1063
- else if (errorMessage.includes("Workflow name does not match")) {
1064
- // This shouldn't happen with our name sync, but handle gracefully
1065
- throw new Error(`Workflow deployment failed: The workflow structure may be incompatible. ` +
1066
- `Please use the Ema UI Auto Builder to make changes to this persona's workflow. ` +
1067
- `(Technical: ${errorMessage})`);
1068
- }
1069
- else if ((errorMessage.toLowerCase().includes("internal server error") || errorMessage.includes("500")) && workflowDef) {
1070
- // 500 error - attempt Autobuilder fallback
1071
- deploymentMethod = "autobuilder";
1072
- }
1073
- else {
1074
- // Other API errors - surface clearly
1075
- throw new Error(`Workflow deployment failed: ${errorMessage}`);
1076
- }
1077
- }
1078
- }
1079
- // Auto Builder fallback for personas without existing workflows
1080
- if (deploymentMethod === "autobuilder" && workflowDef) {
1081
- try {
1082
- // Generate a prompt that asks the Auto Builder to deploy this specific workflow
1083
- const workflowSummary = summarizeWorkflow(workflowDef);
1084
- const prompt = `Deploy this workflow to the persona. The workflow has the following structure:\n\n${workflowSummary}\n\nPlease create and save this workflow.`;
1085
- // Use the iterate workflow method which handles Auto Builder discovery
1086
- autobuilderResult = await client.iterateWorkflow(personaId, prompt, { newConversation: true });
1087
- deployedVia = "autobuilder";
1088
- // Also update proto_config if provided (Auto Builder may not handle this)
1089
- if (protoConfig) {
1090
- await client.updateAiEmployee({
1091
- persona_id: personaId,
1092
- proto_config: mergedProtoConfig,
1093
- });
1094
- }
1095
- }
1096
- catch (autoErr) {
1097
- const autoErrorMessage = autoErr instanceof Error ? autoErr.message : String(autoErr);
1098
- // If Auto Builder also fails, provide clear guidance
1099
- if (autoErrorMessage.includes("No Autobuilder persona found")) {
1100
- throw new Error(`Cannot deploy workflow: This persona has no existing workflow, and the Ema Auto Builder is not available in this tenant. ` +
1101
- `Please contact your Ema administrator to enable the Auto Builder, or create a new persona from a workflow template.`);
1102
- }
1103
- throw new Error(`Workflow deployment via Auto Builder failed: ${autoErrorMessage}`);
1104
- }
1105
- }
1106
- // Build success note
1107
- let successNote = "Workflow deployed successfully. Test in the Ema simulator to verify behavior.";
1108
- if (deployedVia === "autobuilder") {
1109
- successNote = "Workflow deployed via Ema Auto Builder. Test in the Ema simulator to verify behavior.";
1110
- }
1111
- else if (fixAttempted && appliedFixes.some(f => f.applied)) {
1112
- successNote = `Workflow deployed successfully with ${appliedFixes.filter(f => f.applied).length} auto-fix(es) applied. Test in the Ema simulator to verify behavior.`;
1113
- }
1114
- else if (!workflowDef) {
1115
- successNote = "Proto config updated successfully.";
1116
- }
1117
- return {
1118
- environment: client["env"].name,
1119
- success: true,
1120
- persona_id: personaId,
1121
- persona_name: persona.name,
1122
- deployed: {
1123
- workflow_def: !!workflowDef,
1124
- proto_config: !!protoConfig,
1125
- },
1126
- deployment_method: deployedVia,
1127
- validation_passed: validationResults.valid,
1128
- auto_fix_applied: fixAttempted && appliedFixes.some(f => f.applied),
1129
- fixes_applied: appliedFixes.filter(f => f.applied),
1130
- autobuilder_response: autobuilderResult?.response,
1131
- note: successNote,
1132
- };
1133
- },
1134
- optimize_workflow: async (args) => {
1135
- const client = createClient(args.env);
1136
- const identifier = args.identifier ? String(args.identifier) : undefined;
1137
- const targetPersonaId = args.persona_id ? String(args.persona_id) : undefined;
1138
- const prompt = args.prompt ? String(args.prompt) : undefined;
1139
- const personaType = args.type ?? "chat";
1140
- const preview = args.preview === true;
1141
- // Validate inputs
1142
- if (!identifier && !targetPersonaId && !prompt) {
1143
- throw new Error('Provide either: identifier (to fix existing), or persona_id + prompt (to enhance existing)');
1144
- }
1145
- let persona = null;
1146
- let workflowDef;
1147
- let personaId;
1148
- let enhancementPrompt = prompt; // Store prompt for enhancement logging
1149
- // === ALWAYS START BY FETCHING EXISTING WORKFLOW ===
1150
- // Brownfield: fix existing + apply enhancements from prompt
1151
- // The prompt describes what to ADD or CHANGE, not a complete replacement
1152
- const lookupId = identifier ?? targetPersonaId;
1153
- 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);
1154
- if (isUUID) {
1155
- persona = await client.getPersonaById(lookupId);
1156
- }
1157
- else {
1158
- // Search by name
1159
- const personas = await client.getPersonasForTenant();
1160
- const match = personas.find((p) => p.name?.toLowerCase() === lookupId.toLowerCase() ||
1161
- p.name?.toLowerCase().includes(lookupId.toLowerCase()));
1162
- if (match) {
1163
- persona = await client.getPersonaById(match.id);
1164
- }
1165
- }
1166
- if (!persona) {
1167
- throw new Error(`AI Employee "${lookupId}" not found. Check the name or ID.`);
1168
- }
1169
- personaId = persona.id;
1170
- workflowDef = persona.workflow_def;
1171
- if (!workflowDef) {
1172
- return {
1173
- success: false,
1174
- persona: persona.name,
1175
- status: "⚠️ No Workflow",
1176
- message: "This AI Employee has no workflow. Use prompt parameter to generate one: optimize_workflow(persona_id=\"...\", prompt=\"description of what it should do\")",
1177
- };
1178
- }
1179
- // DEPRECATED: MCP no longer pre-analyzes workflows
1180
- // LLM should use ema://rules/* for analysis
1181
- console.warn("[DEPRECATED] optimize_workflow tool is deprecated - use LLM analysis with ema://rules/*");
1182
- const nodes = parseWorkflowDef(workflowDef);
1183
- return {
1184
- success: true,
1185
- persona: persona?.name ?? "Unknown",
1186
- persona_id: personaId,
1187
- status: "DEPRECATED - use LLM analysis",
1188
- node_count: nodes.length,
1189
- workflow_def: workflowDef,
1190
- _deprecation_notice: {
1191
- message: "optimize_workflow is deprecated. MCP does not pre-compute issues.",
1192
- new_workflow: [
1193
- "1. Fetch rules: ema://rules/anti-patterns, ema://rules/optimizations",
1194
- "2. Apply rules to find issues (LLM does this, not MCP)",
1195
- "3. Make structured modifications based on your analysis",
1196
- "4. Deploy via workflow(mode='deploy', persona_id='...', workflow_def={...})",
1197
- ],
1198
- },
1199
- };
1200
- },
1201
- // ─────────────────────────────────────────────────────────────────────────
1202
- // Action Handlers (Consolidated)
1203
- // ─────────────────────────────────────────────────────────────────────────
1204
- get_workflow_action: async (args) => {
1205
- const client = createClient(args.env);
1206
- const identifier = String(args.identifier);
1207
- const actions = await client.listActions();
1208
- // Try ID first, then name
1209
- 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);
1210
- let action = isUUID
1211
- ? actions.find((a) => a.id === identifier)
1212
- : actions.find((a) => a.name?.toLowerCase() === identifier.toLowerCase());
1213
- if (!action) {
1214
- throw new Error(`Action not found: ${identifier} (searched by ${isUUID ? "ID" : "name"})`);
1215
- }
1216
- return { environment: client["env"].name, action };
1217
- },
1218
- find_workflow_actions: async (args) => {
1219
- const client = createClient(args.env);
1220
- // Handle list_categories request
1221
- if (args.list_categories === true) {
1222
- const actions = await client.listActions();
1223
- const categoryMap = new Map();
1224
- for (const a of actions) {
1225
- const cat = a.category ?? "uncategorized";
1226
- categoryMap.set(cat, (categoryMap.get(cat) ?? 0) + 1);
1227
- }
1228
- return {
1229
- environment: client["env"].name,
1230
- categories: Array.from(categoryMap.entries()).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count),
1231
- };
1232
- }
1233
- // Handle persona/workflow scope
1234
- if (args.persona_id) {
1235
- const personaId = String(args.persona_id);
1236
- const personas = await client.getPersonasForTenant();
1237
- const persona = personas.find((p) => p.id === personaId);
1238
- if (!persona)
1239
- throw new Error(`AI Employee not found: ${personaId}`);
1240
- if (!persona.workflow_id) {
1241
- return {
1242
- environment: client["env"].name,
1243
- persona_id: personaId,
1244
- persona_name: persona.name,
1245
- error: "AI Employee has no workflow",
1246
- actions: [],
1247
- };
1248
- }
1249
- const actionIds = await client.listActionsFromWorkflow(persona.workflow_id);
1250
- const allActions = await client.listActions();
1251
- const actionIdSet = new Set(actionIds);
1252
- const actions = allActions.filter(a => actionIdSet.has(a.id));
1253
- return {
1254
- environment: client["env"].name,
1255
- persona_id: personaId,
1256
- persona_name: persona.name,
1257
- workflow_id: persona.workflow_id,
1258
- count: actions.length,
1259
- actions: actions.map((a) => ({
1260
- id: a.id, name: a.name, description: a.description,
1261
- category: a.category, inputs: a.inputs, outputs: a.outputs,
1262
- })),
1263
- };
1264
- }
1265
- if (args.workflow_id) {
1266
- const workflowId = String(args.workflow_id);
1267
- const actionIds = await client.listActionsFromWorkflow(workflowId);
1268
- const allActions = await client.listActions();
1269
- const actionIdSet = new Set(actionIds);
1270
- const actions = allActions.filter(a => actionIdSet.has(a.id));
1271
- return {
1272
- environment: client["env"].name,
1273
- workflow_id: workflowId,
1274
- count: actions.length,
1275
- actions: actions.map((a) => ({
1276
- id: a.id, name: a.name, description: a.description,
1277
- category: a.category, inputs: a.inputs, outputs: a.outputs,
1278
- })),
1279
- };
1280
- }
1281
- // Default: search all actions
1282
- let actions = await client.listActions();
1283
- if (args.query) {
1284
- const q = String(args.query).toLowerCase();
1285
- actions = actions.filter((a) => a.name?.toLowerCase().includes(q));
1286
- }
1287
- if (args.category) {
1288
- const f = String(args.category).toLowerCase();
1289
- actions = actions.filter((a) => a.category?.toLowerCase() === f);
1290
- }
1291
- if (typeof args.enabled === "boolean") {
1292
- actions = actions.filter((a) => a.enabled === args.enabled);
1293
- }
1294
- const limit = typeof args.limit === "number" ? args.limit : 100;
1295
- actions = actions.slice(0, limit);
1296
- return {
1297
- environment: client["env"].name,
1298
- count: actions.length,
1299
- actions: actions.map((a) => ({
1300
- id: a.id, name: a.name, description: a.description,
1301
- category: a.category, enabled: a.enabled, tags: a.tags,
1302
- })),
1303
- };
1304
- },
1305
- // ─────────────────────────────────────────────────────────────────────────
1306
- // ─────────────────────────────────────────────────────────────────────────
1307
- // Diagnostics & Comparison
1308
- // ─────────────────────────────────────────────────────────────────────────
1309
- compare_ai_employees: async (args) => {
1310
- const env1 = args.env_1 ?? getDefaultEnvName();
1311
- const env2 = args.env_2 ?? env1;
1312
- const id1 = String(args.persona_id_1);
1313
- const id2 = String(args.persona_id_2);
1314
- const client1 = createClient(env1);
1315
- const client2 = env1 === env2 ? client1 : createClient(env2);
1316
- const [p1, p2] = await Promise.all([
1317
- client1.getPersonaById(id1),
1318
- client2.getPersonaById(id2),
1319
- ]);
1320
- if (!p1)
1321
- throw new Error(`AI Employee not found: ${id1} in ${env1}`);
1322
- if (!p2)
1323
- throw new Error(`AI Employee not found: ${id2} in ${env2}`);
1324
- const fp1 = fingerprintPersona(p1);
1325
- const fp2 = fingerprintPersona(p2);
1326
- const compareFields = ["name", "description", "status", "trigger_type", "access_level", "embedding_enabled", "template_id", "workflow_id"];
1327
- const differences = [];
1328
- for (const field of compareFields) {
1329
- const val1 = p1[field] ?? p1[field === "template_id" ? "templateId" : field];
1330
- const val2 = p2[field] ?? p2[field === "template_id" ? "templateId" : field];
1331
- if (JSON.stringify(val1) !== JSON.stringify(val2)) {
1332
- differences.push({ field, value_1: val1, value_2: val2 });
1333
- }
1334
- }
1335
- if (JSON.stringify(p1.proto_config ?? {}) !== JSON.stringify(p2.proto_config ?? {})) {
1336
- differences.push({ field: "proto_config", value_1: "(differs)", value_2: "(differs)" });
1337
- }
1338
- if (JSON.stringify(p1.welcome_messages ?? {}) !== JSON.stringify(p2.welcome_messages ?? {})) {
1339
- differences.push({ field: "welcome_messages", value_1: "(differs)", value_2: "(differs)" });
1340
- }
1341
- return {
1342
- persona_1: { id: id1, env: env1, name: p1.name, fingerprint: fp1 },
1343
- persona_2: { id: id2, env: env2, name: p2.name, fingerprint: fp2 },
1344
- fingerprints_match: fp1 === fp2,
1345
- difference_count: differences.length,
1346
- differences,
1347
- };
1348
- },
1349
- list_ai_employee_templates: async (args) => {
1350
- const client = createClient(args.env);
1351
- const personas = await client.getPersonasForTenant();
1352
- const templateMap = new Map();
1353
- for (const p of personas) {
1354
- const templateId = p.template_id ?? p.templateId ?? "unknown";
1355
- const existing = templateMap.get(templateId) ?? { count: 0, names: [] };
1356
- existing.count++;
1357
- if (p.name && existing.names.length < 3)
1358
- existing.names.push(p.name);
1359
- templateMap.set(templateId, existing);
1360
- }
1361
- const templates = Array.from(templateMap.entries())
1362
- .map(([template_id, data]) => ({ template_id, usage_count: data.count, examples: data.names }))
1363
- .sort((a, b) => b.usage_count - a.usage_count);
1364
- return {
1365
- environment: client["env"].name,
1366
- total_ai_employees: personas.length,
1367
- template_count: templates.length,
1368
- templates,
1369
- };
1370
- },
1371
- // ─────────────────────────────────────────────────────────────────────────
1372
- // CONSOLIDATED SYNC HANDLERS
1373
- // ─────────────────────────────────────────────────────────────────────────
1374
- sync: async (args) => {
1375
- const targetEnv = String(args.target_env);
1376
- const sourceEnv = args.source_env ? String(args.source_env) : getDefaultEnvName();
1377
- const dryRun = args.dry_run === true;
1378
- const includeStatus = args.include_status === true;
1379
- const scope = args.scope === "all" ? "all" : "one";
1380
- const identifier = args.identifier ? String(args.identifier) : undefined;
1381
- // Sync all tagged personas
1382
- if (scope === "all" || !identifier) {
1383
- const sdk = getSyncSDK();
1384
- if (sdk) {
1385
- try {
1386
- const result = await sdk.runSync();
1387
- return { success: true, mode: "config", ...result };
1388
- }
1389
- finally {
1390
- sdk.close();
1391
- }
1392
- }
1393
- // Config-less mode
1394
- try {
1395
- const result = await directSyncAll({ targetEnv, dryRun });
1396
- return { success: true, mode: "tags", ...result };
1397
- }
1398
- catch (e) {
1399
- return { success: false, error: e instanceof Error ? e.message : String(e) };
1400
- }
1401
- }
1402
- // Sync single persona
1403
- 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);
1404
- const behavior = resolveSyncBehavior({
1405
- personaName: isUUID ? undefined : identifier,
1406
- targetEnv,
1407
- overrides: {
1408
- dry_run: dryRun ? true : undefined,
1409
- sync_status: includeStatus ? true : undefined,
1410
- },
1411
- });
1412
- try {
1413
- const result = isUUID
1414
- ? await directSyncPersonaById({
1415
- personaId: identifier,
1416
- sourceEnv,
1417
- targetEnv,
1418
- dryRun: behavior.dry_run,
1419
- syncStatus: behavior.sync_status,
1420
- })
1421
- : await directSyncPersona({
1422
- name: identifier,
1423
- sourceEnv,
1424
- targetEnv,
1425
- dryRun: behavior.dry_run,
1426
- syncStatus: behavior.sync_status,
1427
- });
1428
- return { ...result, resolved_behavior: behavior };
1429
- }
1430
- catch (e) {
1431
- return { success: false, error: e instanceof Error ? e.message : String(e) };
1432
- }
1433
- },
1434
- sync_info: async (args) => {
1435
- const client = args.env ? createClient(args.env) : undefined;
1436
- // Check if persona is synced
1437
- if (args.persona_id) {
1438
- if (!client)
1439
- throw new Error("env required when checking persona sync status");
1440
- const personaId = String(args.persona_id);
1441
- const personas = await client.getPersonasForTenant();
1442
- const persona = personas.find((p) => p.id === personaId);
1443
- if (!persona)
1444
- throw new Error(`AI Employee not found: ${personaId}`);
1445
- const meta = client.getSyncMetadata(persona);
1446
- return {
1447
- environment: client["env"].name,
1448
- persona_id: personaId,
1449
- persona_name: persona.name,
1450
- is_synced: !!meta,
1451
- sync_metadata: meta,
1452
- };
1453
- }
1454
- // Check by persona name
1455
- if (args.persona_name) {
1456
- const sdk = getSyncSDK();
1457
- if (!sdk)
1458
- return { error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG." };
1459
- try {
1460
- const persona = await sdk.getMasterPersonaByName(String(args.persona_name));
1461
- if (!persona)
1462
- return { error: `Persona not found: ${args.persona_name}` };
1463
- return await sdk.getPersonaSyncStatus(persona.id);
1464
- }
1465
- finally {
1466
- sdk.close();
1467
- }
1468
- }
1469
- // List all synced personas
1470
- if (args.list_synced === true) {
1471
- if (!client)
1472
- throw new Error("env required when listing synced personas");
1473
- const personas = await client.getPersonasForTenant();
1474
- const masterEnvFilter = args.master_env ? String(args.master_env).toLowerCase() : undefined;
1475
- const synced = [];
1476
- for (const p of personas) {
1477
- const meta = client.getSyncMetadata(p);
1478
- if (meta) {
1479
- if (masterEnvFilter && meta.master_env.toLowerCase() !== masterEnvFilter)
1480
- continue;
1481
- synced.push({ persona_id: p.id, persona_name: p.name, sync_metadata: meta });
1482
- }
1483
- }
1484
- return { environment: client["env"].name, count: synced.length, synced_personas: synced };
1485
- }
1486
- // Default: return overall sync config/status
1487
- const sdk = getSyncSDK();
1488
- const options = args.include_options === true ? loadSyncOptions() : undefined;
1489
- if (!sdk) {
1490
- return {
1491
- configured: false,
1492
- error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG.",
1493
- options,
1494
- };
1495
- }
1496
- try {
1497
- const master = sdk.getMasterEnvironment();
1498
- const envs = sdk.getEnvironments();
1499
- const personas = await sdk.listMasterPersonas();
1500
- return {
1501
- configured: true,
1502
- master_environment: { name: master.name, url: master.baseUrl },
1503
- target_environments: envs.filter((e) => !e.isMaster).map((e) => ({ name: e.name, url: e.baseUrl })),
1504
- total_personas: personas.length,
1505
- options,
1506
- };
1507
- }
1508
- finally {
1509
- sdk.close();
1510
- }
1511
- },
1512
- // ─────────────────────────────────────────────────────────────────────────
1513
- // ─────────────────────────────────────────────────────────────────────────────
1514
- // Auto Builder Knowledge Handlers
1515
- // ─────────────────────────────────────────────────────────────────────────────
1516
- list_auto_builder_agents: async (args) => {
1517
- const category = args.category;
1518
- const agents = category ? getAgentsByCategory(category) : AGENT_CATALOG;
1519
- return {
1520
- count: agents.length,
1521
- category: category ?? "all",
1522
- agents: agents.map(a => ({
1523
- action_name: a.actionName,
1524
- display_name: a.displayName,
1525
- category: a.category,
1526
- description: a.description,
1527
- when_to_use: a.whenToUse,
1528
- inputs: a.inputs.map(i => i.name),
1529
- outputs: a.outputs.map(o => o.name),
1530
- has_critical_rules: !!a.criticalRules?.length,
1531
- })),
1532
- };
1533
- },
1534
- get_auto_builder_agent: async (args) => {
1535
- const actionName = String(args.action_name);
1536
- const agent = getAgentByName(actionName);
1537
- if (!agent) {
1538
- const available = AGENT_CATALOG.slice(0, 15).map(a => a.actionName);
1539
- return {
1540
- error: `Agent not found: ${actionName}`,
1541
- available_examples: available,
1542
- hint: "Use list_auto_builder_agents to see all available agents",
1543
- };
1544
- }
1545
- return {
1546
- action_name: agent.actionName,
1547
- display_name: agent.displayName,
1548
- category: agent.category,
1549
- description: agent.description,
1550
- inputs: agent.inputs,
1551
- outputs: agent.outputs,
1552
- critical_rules: agent.criticalRules ?? [],
1553
- when_to_use: agent.whenToUse,
1554
- when_not_to_use: agent.whenNotToUse,
1555
- example: agent.example,
1556
- };
1557
- },
1558
- suggest_agents_for_use_case: async (args) => {
1559
- const useCase = String(args.use_case);
1560
- const suggestions = suggestAgentsForUseCase(useCase);
1561
- return {
1562
- use_case: useCase,
1563
- suggested_agent_count: suggestions.length,
1564
- suggested_agents: suggestions.map(a => ({
1565
- action_name: a.actionName,
1566
- display_name: a.displayName,
1567
- category: a.category,
1568
- why: a.whenToUse,
1569
- inputs: a.inputs.map(i => `${i.name} (${i.type})`),
1570
- outputs: a.outputs.map(o => `${o.name} (${o.type})`),
1571
- })),
1572
- suggested_flow: suggestions.map(a => a.actionName).join(" → "),
1573
- next_steps: [
1574
- "Use get_auto_builder_agent to get detailed info on each agent",
1575
- "Use get_workflow_pattern for a complete template if a pattern matches",
1576
- "Use get_qualifying_questions to ensure you have all required information",
1577
- ],
1578
- };
1579
- },
1580
- get_workflow_pattern: async (args) => {
1581
- const patternName = String(args.pattern_name);
1582
- const pattern = WORKFLOW_PATTERNS.find(p => p.name === patternName);
1583
- if (!pattern) {
1584
- return {
1585
- error: `Pattern not found: ${patternName}`,
1586
- available: WORKFLOW_PATTERNS.map(p => ({ name: p.name, description: p.description })),
1587
- };
1588
- }
1589
- return {
1590
- name: pattern.name,
1591
- persona_type: pattern.personaType,
1592
- description: pattern.description,
1593
- use_case: pattern.useCase,
1594
- nodes: pattern.nodes,
1595
- connections: pattern.connections,
1596
- anti_patterns: pattern.antiPatterns ?? [],
1597
- implementation_notes: [
1598
- "Replace * with actual category/handler names",
1599
- "All paths must lead to WORKFLOW_OUTPUT",
1600
- "Include Fallback category for categorizers",
1601
- "Check type compatibility for all connections",
1602
- ],
1603
- };
1604
- },
1605
- list_workflow_patterns: async (args) => {
1606
- const personaType = args.persona_type;
1607
- const patterns = personaType
1608
- ? WORKFLOW_PATTERNS.filter(p => p.personaType === personaType)
1609
- : WORKFLOW_PATTERNS;
1610
- return {
1611
- count: patterns.length,
1612
- persona_type_filter: personaType ?? "all",
1613
- patterns: patterns.map(p => ({
1614
- name: p.name,
1615
- persona_type: p.personaType,
1616
- description: p.description,
1617
- use_case: p.useCase,
1618
- node_count: p.nodes.length,
1619
- })),
1620
- };
1621
- },
1622
- check_type_compatibility: async (args) => {
1623
- const sourceType = String(args.source_type);
1624
- const targetType = String(args.target_type);
1625
- const compat = checkTypeCompatibility(sourceType, targetType);
1626
- if (!compat) {
1627
- return {
1628
- source_type: sourceType,
1629
- target_type: targetType,
1630
- compatible: false,
1631
- note: "No explicit compatibility rule found - likely incompatible",
1632
- recommendation: "Use an intermediate node to convert types, or check if target accepts WELL_KNOWN_TYPE_ANY",
1633
- };
1634
- }
1635
- return {
1636
- source_type: sourceType,
1637
- target_type: targetType,
1638
- compatible: compat.compatible,
1639
- note: compat.note,
1640
- recommendation: compat.compatible
1641
- ? "These types are compatible for direct connection"
1642
- : `Incompatible. ${compat.note || "Use an intermediate node to convert types."}`,
1643
- };
1644
- },
1645
- get_widget_reference: async (args) => {
1646
- const personaType = String(args.persona_type);
1647
- const widgets = getWidgetsForPersonaType(personaType);
1648
- const projectType = PROJECT_TYPES[personaType];
1649
- return {
1650
- persona_type: personaType,
1651
- project_type: projectType,
1652
- widget_count: widgets.length,
1653
- widgets: widgets.map(w => ({
1654
- id: w.id,
1655
- name: w.name,
1656
- description: w.description,
1657
- fields: w.fields,
1658
- })),
1659
- note: `Project type ${projectType} is used in proto_config for ${personaType} AI Employees`,
1660
- };
1661
- },
1662
- get_qualifying_questions: async (args) => {
1663
- const category = args.category;
1664
- const requiredOnly = args.required_only === true;
1665
- let questions = category
1666
- ? getQualifyingQuestionsByCategory(category)
1667
- : QUALIFYING_QUESTIONS;
1668
- if (requiredOnly) {
1669
- questions = questions.filter(q => q.required);
1670
- }
1671
- const grouped = questions.reduce((acc, q) => {
1672
- if (!acc[q.category])
1673
- acc[q.category] = [];
1674
- acc[q.category].push({ question: q.question, why_it_matters: q.whyItMatters, required: q.required });
1675
- return acc;
1676
- }, {});
1677
- return {
1678
- total_questions: questions.length,
1679
- categories: Object.keys(grouped),
1680
- questions_by_category: grouped,
1681
- minimum_required: [
1682
- "AI Type (Voice/Chat/Dashboard)",
1683
- "2-3 intent categories + Fallback",
1684
- "1 primary data source or action",
1685
- "Success output format",
1686
- ],
1687
- questioning_rounds: {
1688
- round_1: "Core Context: AI type, trigger, main intents, data sources",
1689
- round_2: "Workflow Details: Actions, validations, outputs, approvals",
1690
- round_3: "Voice/Chat Specifics: Welcome message, hangup conditions (if applicable)",
1691
- },
1692
- };
1693
- },
1694
- get_voice_persona_template: async () => {
1695
- // Use generated fallback from proto definitions
1696
- // In production, prefer API templates via client.getPersonaTemplates()
1697
- const template = getTemplateFallback("voice");
1698
- return {
1699
- template: template || VOICE_TEMPLATE_FALLBACK,
1700
- field_docs: getTemplateFieldDocs("voice"),
1701
- required_fields: ["conversationSettings.welcomeMessage", "conversationSettings.identityAndPurpose", "conversationSettings.takeActionInstructions", "conversationSettings.hangupInstructions"],
1702
- optional_fields: ["conversationSettings.transferCallInstructions", "conversationSettings.speechCharacteristics", "conversationSettings.systemPrompt", "conversationSettings.formFillingInstructions", "conversationSettings.waitMessage"],
1703
- project_type: PROJECT_TYPES.voice,
1704
- widget_ids: {
1705
- voiceSettings: 38,
1706
- conversationSettings: 39,
1707
- vadSettings: 43,
1708
- dataStorageSettings: 42,
1709
- },
1710
- _source: "generated_fallback",
1711
- _note: "This template is auto-generated from proto definitions. For live templates, use client.getPersonaTemplates().",
1712
- };
1713
- },
1714
- validate_workflow_prompt: async (args) => {
1715
- const prompt = String(args.prompt);
1716
- const result = validateWorkflowPrompt(prompt);
1717
- return {
1718
- valid: result.valid,
1719
- issue_count: result.issues.length,
1720
- warning_count: result.warnings.length,
1721
- issues: result.issues,
1722
- warnings: result.warnings,
1723
- recommendations: result.issues.length > 0 ? [
1724
- "Add Fallback category to all categorizers",
1725
- "Ensure HITL nodes have both success and failure paths",
1726
- "Map all response nodes to WORKFLOW_OUTPUT",
1727
- "Specify persona type (Voice AI, Chat AI, Dashboard AI)",
1728
- "Check type compatibility for all connections",
1729
- ] : ["Prompt structure looks valid - verify type compatibility after generation"],
1730
- };
1731
- },
1732
- get_auto_builder_guidance: async (args) => {
1733
- const topic = String(args.topic);
1734
- const guidance = GUIDANCE_TOPICS[topic];
1735
- if (!guidance) {
1736
- return {
1737
- error: `Topic not found: ${topic}`,
1738
- available_topics: Object.keys(GUIDANCE_TOPICS).map(k => ({
1739
- topic: k,
1740
- title: GUIDANCE_TOPICS[k].title,
1741
- })),
1742
- };
1743
- }
1744
- return guidance;
1745
- },
1746
- get_platform_concept: async (args) => {
1747
- const term = String(args.term);
1748
- const concept = getConceptByTerm(term);
1749
- if (!concept) {
1750
- return {
1751
- error: `Concept not found: ${term}`,
1752
- available_concepts: PLATFORM_CONCEPTS.map(c => c.term),
1753
- hint: "Try searching for aliases like 'Persona' (AI Employee) or 'Action' (Agent)",
1754
- };
1755
- }
1756
- return {
1757
- term: concept.term,
1758
- definition: concept.definition,
1759
- aliases: concept.aliases ?? [],
1760
- related_terms: concept.relatedTerms ?? [],
1761
- examples: concept.examples ?? [],
1762
- common_confusions: concept.commonConfusions,
1763
- };
1764
- },
1765
- list_platform_concepts: async () => {
1766
- return {
1767
- count: PLATFORM_CONCEPTS.length,
1768
- concepts: PLATFORM_CONCEPTS.map(c => ({
1769
- term: c.term,
1770
- definition: c.definition,
1771
- aliases: c.aliases ?? [],
1772
- })),
1773
- key_relationships: [
1774
- "AI Employee CONTAINS Workflow (processing logic) + Persona (conversational behavior)",
1775
- "Workflow is made up of Agents/Actions connected by Edges",
1776
- "Agents use Connectors to interact with external systems",
1777
- "HITL = Human-in-the-Loop approval/verification step",
1778
- ],
1779
- };
1780
- },
1781
- get_common_mistakes: async () => {
1782
- return {
1783
- count: COMMON_MISTAKES.length,
1784
- mistakes: COMMON_MISTAKES,
1785
- top_3_critical: [
1786
- COMMON_MISTAKES.find(m => m.mistake.includes("Fallback")),
1787
- COMMON_MISTAKES.find(m => m.mistake.includes("HITL")),
1788
- COMMON_MISTAKES.find(m => m.mistake.includes("duplicate")),
1789
- ].filter(Boolean),
1790
- };
1791
- },
1792
- get_debug_checklist: async () => {
1793
- return {
1794
- total_steps: DEBUG_CHECKLIST.length,
1795
- checklist: DEBUG_CHECKLIST,
1796
- quick_checks: [
1797
- "Is the AI Employee status 'active' or 'ready'?",
1798
- "Does the categorizer have all category edges including Fallback?",
1799
- "Do all paths lead to WORKFLOW_OUTPUT?",
1800
- "Are all connections type-compatible?",
1801
- ],
1802
- };
1803
- },
1804
- get_workflow_execution_model: async () => {
1805
- return {
1806
- ...WORKFLOW_EXECUTION_MODEL,
1807
- summary: "Each user message triggers a NEW workflow execution. Use chat_conversation to detect previous actions and avoid duplicates.",
1808
- };
1809
- },
1810
- // ─────────────────────────────────────────────────────────────────────────────
1811
- // Workflow Review & Audit Handlers
1812
- // ─────────────────────────────────────────────────────────────────────────────
1813
- analyze_workflow: async (args) => {
1814
- // DEPRECATED: MCP no longer pre-analyzes workflows
1815
- // LLM should use ema://rules/* for analysis
1816
- const client = createClient(args.env);
1817
- const personaId = String(args.persona_id);
1818
- const persona = await client.getPersonaById(personaId);
1819
- if (!persona)
1820
- throw new Error(`AI Employee not found: ${personaId}`);
1821
- const nodes = persona.workflow_def ? parseWorkflowDef(persona.workflow_def) : [];
1822
- const connections = persona.workflow_def ? validateWorkflowConnections(persona.workflow_def) : [];
1823
- return {
1824
- environment: client["env"].name,
1825
- persona_id: personaId,
1826
- persona_name: persona.name,
1827
- status: "DEPRECATED - use LLM analysis",
1828
- node_count: nodes.length,
1829
- workflow_def: persona.workflow_def,
1830
- connections: connections.map(c => ({
1831
- edge: c.edge_id,
1832
- source_type: c.source_type,
1833
- target_type: c.target_type,
1834
- compatible: c.compatible,
1835
- })),
1836
- _deprecation_notice: {
1837
- message: "analyze_workflow is deprecated. MCP does not pre-compute issues.",
1838
- new_workflow: [
1839
- "1. Fetch rules: ema://rules/anti-patterns",
1840
- "2. Apply rules to find issues (LLM does this, not MCP)",
1841
- "3. Report your findings",
1842
- ],
1843
- },
1844
- };
1845
- },
1846
- detect_workflow_issues: async (args) => {
1847
- // DEPRECATED: MCP no longer detects workflow issues
1848
- // LLM should use ema://rules/* for analysis
1849
- const workflowDef = args.workflow_def;
1850
- if (!workflowDef || typeof workflowDef !== "object") {
1851
- return {
1852
- error: "Invalid workflow_def - must be an object",
1853
- hint: "Get workflow_def from get_ai_employee_full(persona_id).ai_employee.workflow_def",
1854
- };
1855
- }
1856
- const nodes = parseWorkflowDef(workflowDef);
1857
- return {
1858
- status: "DEPRECATED - use LLM analysis",
1859
- node_count: nodes.length,
1860
- nodes: nodes.map(n => ({ id: n.id, action: n.action_name })),
1861
- _deprecation_notice: {
1862
- message: "detect_workflow_issues is deprecated. MCP does not pre-compute issues.",
1863
- new_workflow: [
1864
- "1. Fetch rules: ema://rules/anti-patterns",
1865
- "2. Apply rules to this workflow (LLM does this)",
1866
- "3. Report issues YOU find",
1867
- ],
1868
- },
1869
- };
1870
- },
1871
- validate_workflow_connections: async (args) => {
1872
- const workflowDef = args.workflow_def;
1873
- if (!workflowDef || typeof workflowDef !== "object") {
1874
- return {
1875
- error: "Invalid workflow_def - must be an object",
1876
- hint: "Get workflow_def from get_ai_employee_full(persona_id).ai_employee.workflow_def",
1877
- };
1878
- }
1879
- const validations = validateWorkflowConnections(workflowDef);
1880
- const compatible = validations.filter(v => v.compatible);
1881
- const incompatible = validations.filter(v => !v.compatible);
1882
- return {
1883
- total_edges: validations.length,
1884
- compatible_count: compatible.length,
1885
- incompatible_count: incompatible.length,
1886
- all_valid: incompatible.length === 0,
1887
- validations: validations.map(v => ({
1888
- edge: v.edge_id,
1889
- source_type: v.source_type,
1890
- target_type: v.target_type,
1891
- compatible: v.compatible,
1892
- note: v.note,
1893
- })),
1894
- incompatible_edges: incompatible.map(v => ({
1895
- edge: v.edge_id,
1896
- source_type: v.source_type,
1897
- target_type: v.target_type,
1898
- note: v.note,
1899
- fix_hint: v.source_type === "WELL_KNOWN_TYPE_CHAT_CONVERSATION" && v.target_type === "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES"
1900
- ? "Insert conversation_to_search_query between source and target"
1901
- : "Use named_inputs (accepts ANY type) or add intermediate conversion node",
1902
- })),
1903
- };
1904
- },
1905
- suggest_workflow_fixes: async (_args) => {
1906
- // DEPRECATED: MCP no longer suggests fixes
1907
- // LLM should reason about fixes using ema://rules/*
1908
- return {
1909
- status: "DEPRECATED - use LLM reasoning",
1910
- _deprecation_notice: {
1911
- message: "suggest_workflow_fixes is deprecated. LLM should propose fixes.",
1912
- new_workflow: [
1913
- "1. Get workflow: workflow(mode='get', persona_id='...')",
1914
- "2. Analyze with ema://rules/anti-patterns",
1915
- "3. LLM modifies workflow_def to fix issues",
1916
- "4. Deploy: workflow(mode='deploy', persona_id='...', workflow_def={...})",
1917
- ],
1918
- },
1919
- };
1920
- },
1921
- compare_workflow_versions: async (args) => {
1922
- // DEPRECATED: MCP no longer pre-analyzes for comparison
1923
- // LLM should compare workflows directly
1924
- const client = createClient(args.env);
1925
- const idBefore = String(args.persona_id_before);
1926
- const idAfter = String(args.persona_id_after);
1927
- const [personaBefore, personaAfter] = await Promise.all([
1928
- client.getPersonaById(idBefore),
1929
- client.getPersonaById(idAfter),
1930
- ]);
1931
- if (!personaBefore)
1932
- throw new Error(`AI Employee not found (before): ${idBefore}`);
1933
- if (!personaAfter)
1934
- throw new Error(`AI Employee not found (after): ${idAfter}`);
1935
- const nodesBefore = personaBefore.workflow_def ? parseWorkflowDef(personaBefore.workflow_def) : [];
1936
- const nodesAfter = personaAfter.workflow_def ? parseWorkflowDef(personaAfter.workflow_def) : [];
1937
- // Compare fingerprints
1938
- const fpBefore = personaBefore.workflow_def ? fingerprintPersona(personaBefore) : null;
1939
- const fpAfter = personaAfter.workflow_def ? fingerprintPersona(personaAfter) : null;
1940
- return {
1941
- environment: client["env"].name,
1942
- status: "DEPRECATED - LLM should compare",
1943
- before: {
1944
- persona_id: idBefore,
1945
- name: personaBefore.name,
1946
- fingerprint: fpBefore,
1947
- has_workflow: !!personaBefore.workflow_def,
1948
- node_count: nodesBefore.length,
1949
- workflow_def: personaBefore.workflow_def,
1950
- },
1951
- after: {
1952
- persona_id: idAfter,
1953
- name: personaAfter.name,
1954
- fingerprint: fpAfter,
1955
- has_workflow: !!personaAfter.workflow_def,
1956
- node_count: nodesAfter.length,
1957
- workflow_def: personaAfter.workflow_def,
1958
- },
1959
- comparison: {
1960
- fingerprints_match: fpBefore === fpAfter,
1961
- node_count_change: nodesAfter.length - nodesBefore.length,
1962
- },
1963
- _deprecation_notice: {
1964
- message: "compare_workflow_versions is deprecated. LLM should compare workflows directly.",
1965
- guidance: "Compare the workflow_def objects returned above. Use ema://rules/anti-patterns to check each.",
1966
- },
1967
- };
1968
- },
1969
- get_workflow_metrics: async (args) => {
1970
- const client = createClient(args.env);
1971
- const personaId = String(args.persona_id);
1972
- const persona = await client.getPersonaById(personaId);
1973
- if (!persona)
1974
- throw new Error(`AI Employee not found: ${personaId}`);
1975
- if (!persona.workflow_def) {
1976
- return {
1977
- environment: client["env"].name,
1978
- persona_id: personaId,
1979
- persona_name: persona.name,
1980
- error: "AI Employee has no workflow_def",
1981
- };
1982
- }
1983
- const nodes = parseWorkflowDef(persona.workflow_def);
1984
- const connections = validateWorkflowConnections(persona.workflow_def);
1985
- // Calculate basic metrics
1986
- const totalEdges = nodes.reduce((sum, n) => sum + (n.incoming_edges?.length ?? 0), 0);
1987
- const avgEdgesPerNode = nodes.length > 0
1988
- ? (totalEdges / nodes.length).toFixed(2)
1989
- : 0;
1990
- // Check for categorizers and HITL
1991
- const categorizerCount = nodes.filter(n => n.action_name?.includes("categorizer")).length;
1992
- const hitlCount = nodes.filter(n => n.action_name?.includes("hitl") || n.id?.includes("hitl")).length;
1993
- const hasTrigger = nodes.some(n => n.action_name === "trigger" || n.id === "trigger");
1994
- return {
1995
- environment: client["env"].name,
1996
- persona_id: personaId,
1997
- persona_name: persona.name,
1998
- structure: {
1999
- total_nodes: nodes.length,
2000
- total_edges: totalEdges,
2001
- has_trigger: hasTrigger,
2002
- connection_count: connections.length,
2003
- },
2004
- routing: {
2005
- categorizers_count: categorizerCount,
2006
- hitl_nodes_count: hitlCount,
2007
- has_parallel_branches: categorizerCount > 0,
2008
- },
2009
- complexity: {
2010
- avg_edges_per_node: avgEdgesPerNode,
2011
- complexity_rating: nodes.length <= 5 ? "simple"
2012
- : nodes.length <= 15 ? "moderate"
2013
- : "complex",
2014
- },
2015
- _note: "Use ema://rules/anti-patterns for quality analysis. MCP no longer pre-computes issues.",
2016
- };
2017
- },
2018
- // ─────────────────────────────────────────────────────────────────────────
2019
- // Workflow Compilation (Template-driven)
2020
- // ─────────────────────────────────────────────────────────────────────────
2021
- compile_workflow: async (args) => {
2022
- const name = String(args.name);
2023
- const description = String(args.description);
2024
- const personaType = String(args.persona_type);
2025
- const rawNodes = args.nodes;
2026
- const rawResultMappings = args.result_mappings;
2027
- if (!["voice", "chat", "dashboard"].includes(personaType)) {
2028
- throw new Error(`Invalid persona_type: ${personaType}. Must be one of: voice, chat, dashboard`);
2029
- }
2030
- if (!rawNodes || rawNodes.length === 0) {
2031
- throw new Error("At least one node is required");
2032
- }
2033
- if (!rawResultMappings || rawResultMappings.length === 0) {
2034
- throw new Error("At least one result_mapping is required to connect outputs to WORKFLOW_OUTPUT");
2035
- }
2036
- // Convert raw input spec to internal Node format
2037
- const nodes = rawNodes.map((rawNode) => {
2038
- const node = {
2039
- id: rawNode.id,
2040
- actionType: rawNode.action_type,
2041
- displayName: rawNode.display_name,
2042
- description: rawNode.description,
2043
- disableHitl: rawNode.disable_hitl,
2044
- };
2045
- // Convert inputs
2046
- if (rawNode.inputs) {
2047
- node.inputs = {};
2048
- for (const [key, rawBinding] of Object.entries(rawNode.inputs)) {
2049
- const binding = {
2050
- type: rawBinding.type,
2051
- actionName: rawBinding.action_name,
2052
- output: rawBinding.output,
2053
- value: rawBinding.value,
2054
- widgetName: rawBinding.widget_name,
2055
- };
2056
- node.inputs[key] = binding;
2057
- }
2058
- }
2059
- // Convert run_if condition
2060
- if (rawNode.run_if) {
2061
- node.runIf = {
2062
- sourceAction: rawNode.run_if.source_action,
2063
- sourceOutput: rawNode.run_if.source_output,
2064
- operator: rawNode.run_if.operator,
2065
- value: rawNode.run_if.value,
2066
- };
2067
- }
2068
- // Convert categories
2069
- if (rawNode.categories) {
2070
- node.categories = rawNode.categories.map((cat) => ({
2071
- name: cat.name,
2072
- description: cat.description,
2073
- examples: cat.examples,
2074
- }));
2075
- }
2076
- // Convert tools
2077
- if (rawNode.tools) {
2078
- node.tools = rawNode.tools.map((tool) => ({
2079
- name: tool.name,
2080
- namespace: tool.namespace,
2081
- }));
2082
- }
2083
- return node;
2084
- });
2085
- // Convert result mappings
2086
- const resultMappings = rawResultMappings.map((rm) => ({
2087
- nodeId: rm.node_id,
2088
- output: rm.output,
2089
- }));
2090
- // Build the WorkflowSpec
2091
- const spec = {
2092
- name,
2093
- description,
2094
- personaType,
2095
- nodes,
2096
- resultMappings,
2097
- };
2098
- // Compile the workflow
2099
- const result = compileWorkflow(spec);
2100
- return {
2101
- success: true,
2102
- message: `Compiled workflow for ${personaType} AI Employee "${name}" with ${nodes.length} nodes`,
2103
- workflow_def: result.workflow_def,
2104
- proto_config: result.proto_config,
2105
- nodes_compiled: nodes.map((n) => ({ id: n.id, action: n.actionType, display_name: n.displayName })),
2106
- result_mappings: resultMappings,
2107
- usage: 'Deploy with: workflow(mode="deploy", persona_id="<persona_id>", workflow_def=<workflow_def>, proto_config=<proto_config>)',
2108
- _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.',
2109
- };
2110
- },
2111
- // ─────────────────────────────────────────────────────────────────────────
2112
- // Data Source / Embedding Management
2113
- // Delegates to extracted handler in ./handlers/data/index.js
2114
- // TODO: Add support for 3rd party data sources (Google Drive, SharePoint, Confluence, etc.)
2115
- // ─────────────────────────────────────────────────────────────────────────
2116
- upload_data_source: async (args) => {
2117
- const client = createClient(args.env);
2118
- const fs = await import("fs/promises");
2119
- const { handleData } = await import("./handlers/data/index.js");
2120
- return handleData({ persona_id: String(args.persona_id), data: { method: "upload", path: String(args.file_path) } }, client, (path) => fs.readFile(path));
2121
- },
2122
- delete_data_source: async (args) => {
2123
- const client = createClient(args.env);
2124
- const { handleData } = await import("./handlers/data/index.js");
2125
- return handleData({ persona_id: String(args.persona_id), data: { method: "delete", file_id: String(args.file_id) } }, client);
2126
- },
2127
- list_data_sources: async (args) => {
2128
- const client = createClient(args.env);
2129
- const { handleData } = await import("./handlers/data/index.js");
2130
- return handleData({ persona_id: String(args.persona_id), method: "list" }, client);
2131
- },
2132
- get_embedding_status: async (args) => {
2133
- const client = createClient(args.env);
2134
- const { handleData } = await import("./handlers/data/index.js");
2135
- return handleData({ persona_id: String(args.persona_id), data: { method: "embedding" } }, client);
2136
- },
2137
- toggle_embedding: async (args) => {
2138
- const client = createClient(args.env);
2139
- const { handleData } = await import("./handlers/data/index.js");
2140
- return handleData({ persona_id: String(args.persona_id), data: { method: "embedding", enabled: Boolean(args.enabled) } }, client);
2141
- },
2142
- // ─────────────────────────────────────────────────────────────────────────
2143
- // Demo Data Management Handlers
2144
- // ─────────────────────────────────────────────────────────────────────────
2145
- consolidate_demo_data: async (args) => {
2146
- return handleConsolidateDemoData(args);
2147
- },
2148
- generate_demo_document: async (args) => {
2149
- return handleGenerateDemoDocument(args);
2150
- },
2151
- validate_demo_document: async (args) => {
2152
- return handleValidateDemoDocument(args);
2153
- },
2154
- get_demo_data_template: async (args) => {
2155
- return handleGetDemoDataTemplate(args);
2156
- },
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ // Tool Definitions
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+ //
49
+ // V2 TOOLS (7 tools) - LLM-optimized minimal interface
50
+ // - env, persona, catalog, workflow, sync, toolkit_feedback, debug
51
+ // - Defined in: ./tools.ts
52
+ //
53
+ // NAMING CONVENTION:
54
+ // - Tool names are defined as BASE NAMES (e.g., "persona")
55
+ // - MCP clients prefix with "mcp_{server}_" (e.g., "mcp_ema_persona")
56
+ //
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+ /**
59
+ * Generate all available tools
60
+ *
61
+ * V2: 7 tools (persona, catalog, workflow, sync, env, toolkit_feedback, debug) - LLM-optimized
62
+ *
63
+ * Why V2:
64
+ * - Minimal tool count optimizes LLM tool selection
65
+ * - Data operations under persona (always persona-scoped)
66
+ * - Catalog consolidates all reference data (actions, templates, etc.)
67
+ * - Clear separation: entity (persona), reference (catalog), operation (sync, workflow)
68
+ */
69
+ function generateAllTools() {
70
+ const envNames = getAvailableEnvironments().map(e => e.name);
71
+ const defaultEnv = getDefaultEnvName();
72
+ return generateTools(envNames, defaultEnv);
73
+ }
74
+ // Generate tools (called once at module load)
75
+ const TOOLS = generateAllTools();
76
+ const toolHandlers = {
2157
77
  // ═══════════════════════════════════════════════════════════════════════════
2158
- // CONSOLIDATED TOOLS (Unix CLI pattern) - 9 tools replace 45
78
+ // V2 TOOLS LLM-optimized interface
2159
79
  // ═══════════════════════════════════════════════════════════════════════════
2160
80
  env: async () => {
2161
81
  return handleEnv({}, () => getAvailableEnvironments().map(e => ({
@@ -2164,224 +84,7 @@ const toolHandlers = {
2164
84
  })), { name: TOOLKIT_NAME, version: TOOLKIT_VERSION, commit: TOOLKIT_COMMIT });
2165
85
  },
2166
86
  persona: async (args) => {
2167
- const targetEnv = args.env ?? getDefaultEnvName();
2168
- const client = createClient(targetEnv);
2169
- // Build version context for version management modes
2170
- const versionContext = {
2171
- // Store versions in the caller's workspace, not the toolkit install dir.
2172
- // This must be writable under typical MCP usage (e.g. Cursor workspace).
2173
- workspaceRoot: process.cwd(),
2174
- environment: targetEnv,
2175
- tenant_id: targetEnv, // Use env name as tenant identifier
2176
- };
2177
- // ─────────────────────────────────────────────────────────────────────────
2178
- // V2 Parameter Transformation
2179
- // Convert v2 structure to v1 mode-based structure for handler compatibility
2180
- // ─────────────────────────────────────────────────────────────────────────
2181
- const transformedArgs = { ...args };
2182
- // ─────────────────────────────────────────────────────────────────────────
2183
- // Explicit Method API (takes priority over flag-based args)
2184
- // persona(method="create|get|list|update|delete|sanitize|analyze|compare|clone")
2185
- // ─────────────────────────────────────────────────────────────────────────
2186
- if (args.method) {
2187
- const method = String(args.method);
2188
- // Map explicit methods to internal modes
2189
- const methodToMode = {
2190
- create: "create",
2191
- get: "get",
2192
- list: "list",
2193
- update: "update",
2194
- delete: "delete",
2195
- sanitize: "sanitize",
2196
- analyze: "analyze",
2197
- compare: "compare",
2198
- schema: "schema", // get persona input schema (dashboard columns)
2199
- clone: "create", // clone is create with from
2200
- snapshot: "version_create",
2201
- history: "version_list",
2202
- restore: "version_restore",
2203
- };
2204
- const mode = methodToMode[method];
2205
- if (!mode) {
2206
- return { error: `Unknown method: ${method}`, valid_methods: Object.keys(methodToMode) };
2207
- }
2208
- transformedArgs.mode = mode;
2209
- delete transformedArgs.method;
2210
- // Handle action composition if present
2211
- if (args.actions && Array.isArray(args.actions)) {
2212
- // Store actions for post-processing after main operation
2213
- transformedArgs._actions = args.actions;
2214
- delete transformedArgs.actions;
2215
- }
2216
- }
2217
- // ─────────────────────────────────────────────────────────────────────────
2218
- // Flag-based API (legacy compatibility)
2219
- // ONLY applies when explicit `method` parameter was NOT provided
2220
- // When method is explicit, flags like sanitize=true are passed through
2221
- // ─────────────────────────────────────────────────────────────────────────
2222
- // Guard: Skip flag-based API if explicit method was provided
2223
- const skipFlagBasedApi = !!args.method;
2224
- // Create: persona(create={name, type, from, input})
2225
- if (!skipFlagBasedApi && args.create && typeof args.create === "object") {
2226
- const create = args.create;
2227
- transformedArgs.mode = "create";
2228
- transformedArgs.name = create.name;
2229
- transformedArgs.type = create.type;
2230
- transformedArgs.from = create.from;
2231
- transformedArgs.input = create.input;
2232
- transformedArgs.preview = create.preview ?? true;
2233
- delete transformedArgs.create;
2234
- }
2235
- // Update: persona(id, update={config, input, workflow_spec})
2236
- else if (!skipFlagBasedApi && args.update && typeof args.update === "object") {
2237
- const update = args.update;
2238
- transformedArgs.mode = "update";
2239
- transformedArgs.proto_config = update.config;
2240
- transformedArgs.input = update.input;
2241
- transformedArgs.workflow_spec = update.workflow_spec;
2242
- transformedArgs.preview = update.preview ?? false; // Default to deploy, not preview
2243
- delete transformedArgs.update;
2244
- }
2245
- // Delete: persona(id, delete=true)
2246
- else if (!skipFlagBasedApi && args.delete === true) {
2247
- transformedArgs.mode = "delete";
2248
- delete transformedArgs.delete;
2249
- }
2250
- // Analyze: persona(id, analyze=true, fix=true)
2251
- else if (!skipFlagBasedApi && args.analyze === true) {
2252
- transformedArgs.mode = "analyze";
2253
- // fix is already in args
2254
- delete transformedArgs.analyze;
2255
- }
2256
- // Sanitize: persona(id, sanitize=true)
2257
- // ONLY converts to mode="sanitize" when method is NOT explicit
2258
- // When method="create" + sanitize=true, sanitize is a FLAG for post-creation sanitization
2259
- else if (!skipFlagBasedApi && args.sanitize === true) {
2260
- transformedArgs.mode = "sanitize";
2261
- delete transformedArgs.sanitize;
2262
- }
2263
- // Snapshot: persona(id, snapshot="message")
2264
- else if (!skipFlagBasedApi && typeof args.snapshot === "string") {
2265
- transformedArgs.mode = "version_create";
2266
- transformedArgs.message = args.snapshot;
2267
- delete transformedArgs.snapshot;
2268
- }
2269
- // History: persona(id, history=true)
2270
- else if (!skipFlagBasedApi && args.history === true) {
2271
- transformedArgs.mode = "version_list";
2272
- delete transformedArgs.history;
2273
- }
2274
- // Restore: persona(id, restore="v3")
2275
- else if (!skipFlagBasedApi && typeof args.restore === "string") {
2276
- transformedArgs.mode = "version_restore";
2277
- transformedArgs.version = args.restore;
2278
- delete transformedArgs.restore;
2279
- }
2280
- // Compare: persona(id, compare="other-id")
2281
- else if (!skipFlagBasedApi && typeof args.compare === "string") {
2282
- transformedArgs.mode = "compare";
2283
- transformedArgs.compare_to = args.compare;
2284
- delete transformedArgs.compare;
2285
- }
2286
- // Data operations: persona(id, data={method:"...", ...})
2287
- // Supports both explicit method format and legacy flag format
2288
- else if (args.data && typeof args.data === "object") {
2289
- const data = args.data;
2290
- const personaId = args.id;
2291
- const fs = await import("fs/promises");
2292
- // EXPLICIT METHOD FORMAT (preferred): data={method:"list/stats/upload/copy/replicate/delete/search/refresh/regenerate/replace"}
2293
- if (typeof data.method === "string") {
2294
- // Import the new data handler - pass data object as part of args
2295
- const { handleData: handleDataNew } = await import("./handlers/data/index.js");
2296
- return handleDataNew({ persona_id: personaId, env: args.env, data }, client);
2297
- }
2298
- // LEGACY FLAG FORMAT (backwards compatibility) - use extracted handler
2299
- const { handleData: handleDataExtracted } = await import("./handlers/data/index.js");
2300
- const readFileFn = (path) => fs.readFile(path);
2301
- if (data.list === true) {
2302
- return handleDataExtracted({ method: "list", persona_id: personaId, env: args.env }, client, readFileFn);
2303
- }
2304
- if (typeof data.upload === "string") {
2305
- return handleDataExtracted({ method: "upload", persona_id: personaId, data: { path: data.upload }, env: args.env }, client, readFileFn);
2306
- }
2307
- if (typeof data.delete === "string") {
2308
- return handleDataExtracted({ method: "delete", persona_id: personaId, data: { file_id: data.delete }, env: args.env }, client, readFileFn);
2309
- }
2310
- if (typeof data.generate === "string" || data.template) {
2311
- return handleDataExtracted({
2312
- method: "generate",
2313
- persona_id: personaId,
2314
- data: {
2315
- input: data.generate,
2316
- from: data.template,
2317
- count: data.count,
2318
- },
2319
- env: args.env
2320
- }, client, readFileFn);
2321
- }
2322
- if (typeof data.embed === "boolean") {
2323
- return handleDataExtracted({ method: "embedding", persona_id: personaId, data: { enabled: data.embed }, env: args.env }, client, readFileFn);
2324
- }
2325
- if (typeof data.search === "string") {
2326
- return handleKnowledge({ mode: "search", persona_id: personaId, query: data.search, env: args.env }, client, (path) => fs.readFile(path));
2327
- }
2328
- return {
2329
- error: "Unknown data operation",
2330
- hint: "Use data={method:'list'} format (explicit method)",
2331
- available_methods: ["list", "stats", "upload", "copy", "replicate", "delete", "search", "refresh", "regenerate", "replace"],
2332
- legacy_flags: ["list", "upload", "delete", "generate", "embed", "search"],
2333
- };
2334
- }
2335
- // Get: persona(id) with no mutation flags
2336
- else if (args.id && !transformedArgs.mode) {
2337
- transformedArgs.mode = "get";
2338
- }
2339
- // List: persona() or persona(type, status, query) with no id
2340
- else if (!args.id && !transformedArgs.mode) {
2341
- transformedArgs.mode = "list";
2342
- }
2343
- // Templates are tenant-specific - don't use hardcoded IDs
2344
- // The handler will use dynamic template lookup from API
2345
- const result = await handlePersona(transformedArgs, client, () => undefined, // Dynamic lookup in handler
2346
- (env) => createClient(env), versionContext);
2347
- // ─────────────────────────────────────────────────────────────────────────
2348
- // Action Composition Post-Processing
2349
- // Execute actions array after main operation completes
2350
- // ─────────────────────────────────────────────────────────────────────────
2351
- const actions = transformedArgs._actions;
2352
- if (actions && actions.length > 0) {
2353
- // Get the target persona ID from result
2354
- const resultObj = result;
2355
- const targetId = resultObj.id ??
2356
- resultObj.persona_id;
2357
- const sourceId = args.from;
2358
- if (!targetId) {
2359
- return {
2360
- ...(typeof result === "object" && result !== null ? result : {}),
2361
- _actions_error: "No persona ID available for action execution",
2362
- };
2363
- }
2364
- // Import and execute actions
2365
- const actionExecutor = await import("./handlers/action-executor.js");
2366
- const context = {
2367
- source: sourceId,
2368
- target: targetId,
2369
- env: targetEnv,
2370
- originalArgs: args,
2371
- };
2372
- const actionsResult = await actionExecutor.executeActions(actions, context, client);
2373
- return {
2374
- ...(typeof result === "object" && result !== null ? result : {}),
2375
- _actions: actionsResult,
2376
- };
2377
- }
2378
- return result;
2379
- },
2380
- // Consolidated workflow handler (replaces legacy inline handler)
2381
- workflow: async (args) => {
2382
- const client = createClient(args.env);
2383
- // Templates are tenant-specific - dynamic lookup in handler
2384
- return handleWorkflow(args, client, () => undefined);
87
+ return handlePersonaAdapter(args, createClient, getDefaultEnvName);
2385
88
  },
2386
89
  action: async (args) => {
2387
90
  const client = createClient(args.env);
@@ -2390,12 +93,6 @@ const toolHandlers = {
2390
93
  template: async (args) => {
2391
94
  return handleTemplate(args);
2392
95
  },
2393
- knowledge: async (args) => {
2394
- const client = createClient(args.env);
2395
- const fs = await import("fs/promises");
2396
- return handleKnowledge(args, client, (path) => fs.readFile(path));
2397
- },
2398
- // v2: data is an alias for knowledge with simplified interface
2399
96
  data: async (args) => {
2400
97
  const client = createClient(args.env);
2401
98
  const fs = await import("fs/promises");
@@ -2419,464 +116,30 @@ const toolHandlers = {
2419
116
  client,
2420
117
  });
2421
118
  },
2422
- // ═══════════════════════════════════════════════════════════════════════════
2423
- // V2 TOOLS (4 tools: persona, catalog, sync, env)
2424
- // ═══════════════════════════════════════════════════════════════════════════
2425
119
  // catalog: Consolidated reference data (actions, templates, widgets, voices, patterns, concepts)
2426
120
  catalog: async (args) => {
2427
121
  const client = createClient(args.env);
2428
122
  return handleCatalog(args, client);
2429
123
  },
2430
- // Note: 'sync' handler already exists above - keeping it for full functionality
2431
- // Note: demo is handled via a consolidated adapter below
2432
124
  // toolkit_feedback: Agent feedback collection and analysis
2433
125
  toolkit_feedback: async (args) => {
2434
126
  return handleFeedback(args);
2435
127
  },
128
+ // V2 adapters — routing + transformation extracted to handler files
129
+ workflow: async (args) => {
130
+ return handleWorkflowAdapter(args, createClient, getDefaultEnvName);
131
+ },
132
+ sync: async (args) => {
133
+ return handleSyncAdapter(args, createClient, getDefaultEnvName, getSyncSDK);
134
+ },
135
+ demo: async (args) => {
136
+ return handleDemoAdapter(args, createClient);
137
+ },
138
+ debug: async (args) => {
139
+ return handleDebugAdapter(args, createClient, getDefaultEnvName);
140
+ },
2436
141
  };
2437
142
  // ─────────────────────────────────────────────────────────────────────────────
2438
- // V2 Tool Adapters (contract ↔ implementation)
2439
- // ─────────────────────────────────────────────────────────────────────────────
2440
- //
2441
- // The tool schemas in tools.ts are the public MCP contract.
2442
- // This file still contains proven handlers that we reuse
2443
- // (e.g. deploy_workflow, optimize_workflow, compile_workflow, etc.).
2444
- //
2445
- // These adapters ensure the V2 tool surface behaves as documented,
2446
- // while preserving the existing internal implementations.
2447
- const legacyWorkflowTool = toolHandlers.workflow;
2448
- const legacyDeployWorkflow = toolHandlers.deploy_workflow;
2449
- const legacyOptimizeWorkflow = toolHandlers.optimize_workflow;
2450
- const legacyCompareWorkflowVersions = toolHandlers.compare_workflow_versions;
2451
- const legacyCompileWorkflow = toolHandlers.compile_workflow;
2452
- const legacyDetectWorkflowIssues = toolHandlers.detect_workflow_issues;
2453
- const legacyValidateWorkflowConnections = toolHandlers.validate_workflow_connections;
2454
- const legacySuggestWorkflowFixes = toolHandlers.suggest_workflow_fixes;
2455
- const legacySyncRun = toolHandlers.sync;
2456
- const legacySyncInfo = toolHandlers.sync_info;
2457
- const legacyConsolidateDemoData = toolHandlers.consolidate_demo_data;
2458
- const legacyGenerateDemoDocument = toolHandlers.generate_demo_document;
2459
- const legacyValidateDemoDocument = toolHandlers.validate_demo_document;
2460
- const legacyGetDemoDataTemplate = toolHandlers.get_demo_data_template;
2461
- // Workflow tool: MCP provides data (get) and executes (deploy). LLM does all thinking.
2462
- toolHandlers.workflow = async (args) => {
2463
- const normalizedArgs = { ...(args ?? {}) };
2464
- const personaId = normalizedArgs.persona_id ? String(normalizedArgs.persona_id) : undefined;
2465
- let workflowDef = normalizedArgs.workflow_def;
2466
- const workflowDefPath = normalizedArgs.workflow_def_path;
2467
- const mode = normalizedArgs.mode ? String(normalizedArgs.mode) : undefined;
2468
- const baseFingerprint = normalizedArgs.base_fingerprint;
2469
- const force = normalizedArgs.force;
2470
- // For deploy: resolve workflow_def from file when workflow_def_path is provided (large payloads)
2471
- // Also handle the common agent mistake: passing {"workflow_def_path": "/path"} as workflow_def
2472
- // (happens when the agent's tool schema doesn't expose workflow_def_path as a separate param)
2473
- let effectivePath = workflowDefPath;
2474
- if (mode === "deploy" && workflowDef && !effectivePath) {
2475
- const keys = Object.keys(workflowDef);
2476
- if (keys.length === 1 && keys[0] === "workflow_def_path" && typeof workflowDef.workflow_def_path === "string") {
2477
- effectivePath = workflowDef.workflow_def_path;
2478
- workflowDef = undefined;
2479
- }
2480
- }
2481
- if (mode === "deploy" && !workflowDef && effectivePath) {
2482
- try {
2483
- // Guardrails: reduce risk of accidental secret exfiltration
2484
- const path = await import("path");
2485
- if (!path.isAbsolute(effectivePath)) {
2486
- return { error: "workflow_def_path must be an absolute path on the MCP server host", path: effectivePath };
2487
- }
2488
- if (!effectivePath.toLowerCase().endsWith(".json")) {
2489
- return { error: "workflow_def_path must point to a .json file", path: effectivePath };
2490
- }
2491
- const fs = await import("fs/promises");
2492
- const stat = await fs.stat(effectivePath);
2493
- const MAX_WORKFLOW_DEF_BYTES = 1024 * 1024; // 1MB
2494
- if (stat.size > MAX_WORKFLOW_DEF_BYTES) {
2495
- return { error: `workflow_def_path file too large (${stat.size} bytes)`, max_bytes: MAX_WORKFLOW_DEF_BYTES, path: effectivePath };
2496
- }
2497
- const raw = await fs.readFile(effectivePath, "utf-8");
2498
- const parsed = JSON.parse(raw);
2499
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
2500
- workflowDef = parsed;
2501
- }
2502
- else {
2503
- return { error: "workflow_def_path must point to a JSON object", path: effectivePath };
2504
- }
2505
- }
2506
- catch (err) {
2507
- const msg = err instanceof Error ? err.message : String(err);
2508
- return { error: `Failed to read workflow_def from path: ${msg}`, path: effectivePath };
2509
- }
2510
- }
2511
- // Route to handleWorkflow for get/deploy (the only public modes)
2512
- const client = createClient(normalizedArgs.env);
2513
- switch (mode) {
2514
- case "get": {
2515
- // Return workflow data for LLM to analyze/modify
2516
- return handleWorkflow({
2517
- mode: "get",
2518
- persona_id: personaId,
2519
- env: normalizedArgs.env,
2520
- }, client, () => undefined);
2521
- }
2522
- case "validate": {
2523
- // Static validation with path enumeration
2524
- return handleWorkflow({
2525
- mode: "validate",
2526
- persona_id: personaId,
2527
- workflow_def: normalizedArgs.workflow_def,
2528
- workflow_spec: normalizedArgs.workflow_spec,
2529
- validation_type: normalizedArgs.validation_type,
2530
- max_paths: normalizedArgs.max_paths,
2531
- timeout_ms: normalizedArgs.timeout_ms,
2532
- env: normalizedArgs.env,
2533
- }, client, () => undefined);
2534
- }
2535
- case "deploy": {
2536
- if (!personaId) {
2537
- return { error: 'persona_id is required for workflow(mode="deploy")' };
2538
- }
2539
- // Pre-deploy snapshot + stale-state protection (out-of-band changes)
2540
- const targetEnv = normalizedArgs.env ?? getDefaultEnvName();
2541
- let versionCreated;
2542
- try {
2543
- const personaBefore = await client.getPersonaById(personaId);
2544
- if (!personaBefore) {
2545
- return { error: `Persona not found: ${personaId}` };
2546
- }
2547
- const currentFp = fingerprintPersona(personaBefore);
2548
- if (!force && !baseFingerprint) {
2549
- return {
2550
- error: "base_fingerprint is required for workflow deploy (stale-state protection)",
2551
- persona_id: personaId,
2552
- current_fingerprint: currentFp,
2553
- hint: "Run workflow(mode='get', persona_id='...') immediately before deploying and pass fingerprint as base_fingerprint. Use force=true only for emergency overrides.",
2554
- };
2555
- }
2556
- if (!force && baseFingerprint && baseFingerprint !== currentFp) {
2557
- return {
2558
- error: "Persona changed since you last fetched it (fingerprint mismatch)",
2559
- persona_id: personaId,
2560
- base_fingerprint: baseFingerprint,
2561
- current_fingerprint: currentFp,
2562
- hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. Use force=true only if you intend to overwrite out-of-band changes.",
2563
- };
2564
- }
2565
- const storage = createVersionStorage(process.cwd());
2566
- const engine = createVersionPolicyEngine(storage);
2567
- const snap = engine.forceCreateVersion(personaBefore, {
2568
- environment: targetEnv,
2569
- tenant_id: targetEnv,
2570
- message: "Pre-deploy snapshot",
2571
- created_by: "mcp-toolkit",
2572
- });
2573
- if (!snap.created || !snap.version) {
2574
- if (!force) {
2575
- return {
2576
- error: "Failed to create pre-deploy snapshot (required before deploy)",
2577
- persona_id: personaId,
2578
- details: snap.reason,
2579
- hint: "Fix snapshotting (workspace storage) or retry with force=true for emergency override.",
2580
- };
2581
- }
2582
- }
2583
- else {
2584
- versionCreated = { id: snap.version.id, version_name: snap.version.version_name };
2585
- }
2586
- }
2587
- catch {
2588
- // Snapshotting is required unless force=true
2589
- if (!force) {
2590
- return {
2591
- error: "Failed to create pre-deploy snapshot (required before deploy)",
2592
- persona_id: personaId,
2593
- hint: "Retry after fixing local workspace write access, or use force=true for emergency override.",
2594
- };
2595
- }
2596
- }
2597
- // Route to handleWorkflow deploy
2598
- const deployResult = await handleWorkflow({
2599
- mode: "deploy",
2600
- persona_id: personaId,
2601
- workflow_def: workflowDef,
2602
- proto_config: normalizedArgs.proto_config,
2603
- env: normalizedArgs.env,
2604
- force: force,
2605
- strict_validation: normalizedArgs.strict_validation,
2606
- }, client, () => undefined);
2607
- // Add version info to result if created
2608
- if (versionCreated && deployResult && typeof deployResult === "object") {
2609
- deployResult.version_snapshot = versionCreated;
2610
- }
2611
- return deployResult;
2612
- }
2613
- // REMOVED modes - LLM does these
2614
- case "analyze":
2615
- case "compare":
2616
- case "compile":
2617
- case "optimize":
2618
- case "generate": {
2619
- return {
2620
- error: `Mode "${mode}" removed - LLM does this thinking`,
2621
- hint: "Use workflow(mode='get') to fetch data, then analyze/generate in your reasoning. Deploy with workflow(mode='deploy').",
2622
- valid_modes: ["get", "validate", "deploy"],
2623
- };
2624
- }
2625
- default: {
2626
- // No mode or unknown mode - require explicit mode
2627
- return {
2628
- error: `Mode required. Valid modes: get, validate, deploy`,
2629
- hint: "workflow(mode='get') returns data for LLM. workflow(mode='validate') validates specs. workflow(mode='deploy') executes LLM's workflow_def.",
2630
- example: `workflow(mode="get", persona_id="...")`,
2631
- };
2632
- }
2633
- }
2634
- };
2635
- // Unify sync modes: run | status | config
2636
- toolHandlers.sync = async (args) => {
2637
- const normalizedArgs = { ...(args ?? {}) };
2638
- // Tool definition uses "method" (preview/execute/status), but legacy callers use "mode" (run/status/config)
2639
- const rawMethod = normalizedArgs.method ? String(normalizedArgs.method) : normalizedArgs.mode ? String(normalizedArgs.mode) : "run";
2640
- // Map tool method values to internal mode values
2641
- const mode = rawMethod === "preview" ? "run" : rawMethod === "execute" ? "run" : rawMethod;
2642
- // "preview" implies dry_run
2643
- if (rawMethod === "preview") {
2644
- normalizedArgs.dry_run = true;
2645
- }
2646
- // Support both old and new arg names
2647
- const target = (normalizedArgs.target ?? normalizedArgs.target_env);
2648
- const source = (normalizedArgs.source ?? normalizedArgs.source_env);
2649
- const id = normalizedArgs.id;
2650
- const identifier = normalizedArgs.identifier; // deprecated alias
2651
- const idOrIdentifier = id ?? identifier;
2652
- if (mode === "config") {
2653
- return legacySyncInfo({ include_options: true });
2654
- }
2655
- if (mode === "status") {
2656
- const env = normalizedArgs.env;
2657
- if (normalizedArgs.list_synced === true) {
2658
- if (!env)
2659
- throw new Error('env is required for sync(mode="status", list_synced=true)');
2660
- return legacySyncInfo({ list_synced: true, master_env: normalizedArgs.master_env, env });
2661
- }
2662
- if (idOrIdentifier) {
2663
- if (!env)
2664
- throw new Error('env is required for sync(mode="status", id="...")');
2665
- const identifierToResolve = String(idOrIdentifier);
2666
- 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);
2667
- if (isUUID) {
2668
- return legacySyncInfo({ persona_id: identifierToResolve, env });
2669
- }
2670
- // Name lookup: resolve to ID in env, then reuse persona_id path
2671
- const client = createClient(env);
2672
- const personas = await client.getPersonasForTenant();
2673
- const match = personas.find((p) => p.name === identifierToResolve);
2674
- if (!match)
2675
- throw new Error(`AI Employee not found by name in ${env}: ${identifierToResolve}`);
2676
- return legacySyncInfo({ persona_id: match.id, env });
2677
- }
2678
- // Default: overall sync status/config summary
2679
- return legacySyncInfo({ include_options: normalizedArgs.include_options === true });
2680
- }
2681
- // mode === "run" (default)
2682
- if (!target) {
2683
- throw new Error('target (or target_env) is required for sync(mode="run")');
2684
- }
2685
- return legacySyncRun({
2686
- identifier: idOrIdentifier,
2687
- target_env: target,
2688
- source_env: source,
2689
- scope: normalizedArgs.scope,
2690
- dry_run: normalizedArgs.dry_run,
2691
- include_status: normalizedArgs.include_status,
2692
- });
2693
- };
2694
- // Consolidated demo tool: kit | validate_kit | scenarios | consolidate | generate | validate | template
2695
- toolHandlers.demo = async (args) => {
2696
- const normalizedArgs = { ...(args ?? {}) };
2697
- const mode = normalizedArgs.mode ? String(normalizedArgs.mode) : "template";
2698
- // Deprecation warning - added to all responses
2699
- const deprecationWarning = {
2700
- _deprecation: {
2701
- message: "The 'demo' tool is deprecated. Please use 'data' and 'persona' tools instead.",
2702
- migration: {
2703
- "demo(mode='kit')": "persona(from='demo-sales-sdr', include_data=true)",
2704
- "demo(mode='generate')": "data(mode='generate', from='customer', count=5)",
2705
- "demo(mode='scenarios')": "data(mode='templates')",
2706
- "demo(mode='template')": "data(mode='templates', template='customer')",
2707
- },
2708
- },
2709
- };
2710
- switch (mode) {
2711
- case "kit": {
2712
- // Generate complete demo kit for a persona
2713
- const personaId = String(normalizedArgs.persona_id ?? "");
2714
- const scenarioId = String(normalizedArgs.scenario ?? "sales-sdr");
2715
- if (!personaId) {
2716
- throw new Error('demo(mode="kit") requires: persona_id');
2717
- }
2718
- // Import demo generator
2719
- const { generateDemoKit, DEMO_SCENARIOS, generateDemoScriptMarkdown, validateDemoKit } = await import("./demo-generator.js");
2720
- // Get scenario
2721
- const scenario = DEMO_SCENARIOS[scenarioId];
2722
- if (!scenario) {
2723
- throw new Error(`Unknown scenario: ${scenarioId}. Available: ${Object.keys(DEMO_SCENARIOS).join(", ")}`);
2724
- }
2725
- // Get persona and workflow
2726
- const client = await createClient(normalizedArgs.env);
2727
- const persona = await client.getPersonaById(personaId);
2728
- if (!persona) {
2729
- throw new Error(`Persona not found: ${personaId}`);
2730
- }
2731
- const workflowDef = persona.workflow_def || {};
2732
- const customQA = normalizedArgs.custom_qa;
2733
- // Generate kit
2734
- const kit = generateDemoKit(personaId, persona.name || personaId, workflowDef, scenario, customQA);
2735
- // Generate markdown script
2736
- const demoScript = generateDemoScriptMarkdown(kit);
2737
- // Validate
2738
- const validation = validateDemoKit(kit);
2739
- return {
2740
- success: true,
2741
- persona_id: personaId,
2742
- persona_name: persona.name,
2743
- scenario: scenarioId,
2744
- kit_summary: {
2745
- kb_documents: kit.kb_documents.length,
2746
- demo_questions: kit.demo_script.length,
2747
- fixed_responses: kit.fixed_responses.length,
2748
- validation_queries: kit.validation_queries.length,
2749
- },
2750
- validation,
2751
- demo_script_preview: demoScript.slice(0, 2000) + (demoScript.length > 2000 ? "\n\n... (truncated)" : ""),
2752
- kit,
2753
- instructions: [
2754
- "1. Upload KB documents to the persona's knowledge base",
2755
- "2. Review the demo script and practice the questions",
2756
- "3. Optionally apply fixed_responses for guaranteed fallbacks",
2757
- "4. Run validation queries to verify demo readiness",
2758
- "5. Conduct the demo with confidence!",
2759
- ],
2760
- };
2761
- }
2762
- case "validate_kit": {
2763
- // Validate a persona's demo readiness
2764
- const personaId = String(normalizedArgs.persona_id ?? "");
2765
- if (!personaId) {
2766
- throw new Error('demo(mode="validate_kit") requires: persona_id');
2767
- }
2768
- const { analyzeWorkflowForDemo, DEMO_SCENARIOS } = await import("./demo-generator.js");
2769
- const client = await createClient(normalizedArgs.env);
2770
- const persona = await client.getPersonaById(personaId);
2771
- if (!persona) {
2772
- throw new Error(`Persona not found: ${personaId}`);
2773
- }
2774
- const analysis = analyzeWorkflowForDemo(persona.workflow_def || {});
2775
- // Check data sources
2776
- const dataSourcesResult = await client.listDataSourceFiles(personaId);
2777
- const dataSources = dataSourcesResult.files || [];
2778
- const hasKnowledgeBase = dataSources.length > 0;
2779
- const issues = [];
2780
- if (!hasKnowledgeBase) {
2781
- issues.push("No knowledge base documents uploaded - RAG search will fail");
2782
- }
2783
- if (analysis.intents.length === 0) {
2784
- issues.push("No categorizer intents detected - workflow may not route correctly");
2785
- }
2786
- if (!analysis.has_search) {
2787
- issues.push("No search nodes detected - cannot retrieve KB data");
2788
- }
2789
- // Suggest best scenario
2790
- let suggestedScenario = "sales-sdr";
2791
- for (const [id, scenario] of Object.entries(DEMO_SCENARIOS)) {
2792
- const intentOverlap = scenario.intents.filter(i => analysis.intents.some(ai => ai.toLowerCase().includes(i.name.toLowerCase()))).length;
2793
- if (intentOverlap > 0) {
2794
- suggestedScenario = id;
2795
- break;
2796
- }
2797
- }
2798
- return {
2799
- persona_id: personaId,
2800
- persona_name: persona.name,
2801
- ready: issues.length === 0,
2802
- issues,
2803
- workflow_analysis: analysis,
2804
- knowledge_base: {
2805
- has_documents: hasKnowledgeBase,
2806
- document_count: dataSources.length,
2807
- },
2808
- suggested_scenario: suggestedScenario,
2809
- next_steps: issues.length > 0
2810
- ? issues.map((issue, i) => `${i + 1}. Fix: ${issue}`)
2811
- : [`Generate demo kit: demo(mode="kit", persona_id="${personaId}", scenario="${suggestedScenario}")`],
2812
- };
2813
- }
2814
- case "scenarios": {
2815
- // List available demo scenarios
2816
- const { DEMO_SCENARIOS } = await import("./demo-generator.js");
2817
- return {
2818
- scenarios: Object.entries(DEMO_SCENARIOS).map(([id, scenario]) => ({
2819
- id,
2820
- name: scenario.name,
2821
- description: scenario.description,
2822
- persona_types: scenario.persona_types,
2823
- tags: scenario.tags,
2824
- intent_count: scenario.intents.length,
2825
- qa_count: scenario.qa_pairs.length,
2826
- entity_types: scenario.entities.map(e => e.type),
2827
- })),
2828
- usage: 'demo(mode="kit", persona_id="...", scenario="<scenario_id>")',
2829
- };
2830
- }
2831
- case "consolidate": {
2832
- const source = String(normalizedArgs.source ?? "");
2833
- const output = String(normalizedArgs.output ?? "");
2834
- const entity = String(normalizedArgs.entity ?? "");
2835
- if (!source || !output || !entity) {
2836
- throw new Error('demo(mode="consolidate") requires: source, output, entity');
2837
- }
2838
- return legacyConsolidateDemoData({
2839
- source_dir: source,
2840
- output_dir: output,
2841
- entity_type: entity,
2842
- primary_file: normalizedArgs.primary ?? `${entity}s.json`,
2843
- joins: normalizedArgs.joins ?? [],
2844
- tags: normalizedArgs.tags,
2845
- });
2846
- }
2847
- case "generate": {
2848
- const entity = String(normalizedArgs.entity ?? "");
2849
- if (!entity)
2850
- throw new Error('demo(mode="generate") requires: entity');
2851
- return legacyGenerateDemoDocument({
2852
- entity_type: entity,
2853
- data: normalizedArgs.data ?? {},
2854
- related_data: normalizedArgs.related ?? {},
2855
- output_path: normalizedArgs.output,
2856
- tags: normalizedArgs.tags,
2857
- });
2858
- }
2859
- case "validate": {
2860
- return legacyValidateDemoDocument({
2861
- file_path: normalizedArgs.file,
2862
- content: normalizedArgs.content,
2863
- });
2864
- }
2865
- case "template": {
2866
- const entity = String(normalizedArgs.entity ?? "");
2867
- if (!entity)
2868
- throw new Error('demo(mode="template") requires: entity');
2869
- return legacyGetDemoDataTemplate({
2870
- entity_type: entity,
2871
- include_example: normalizedArgs.include_example,
2872
- });
2873
- }
2874
- default:
2875
- throw new Error(`Unknown demo mode: ${mode}`);
2876
- }
2877
- };
2878
- // generateEntityDocument moved to handlers/demo/index.js
2879
- // ─────────────────────────────────────────────────────────────────────────────
2880
143
  // Helpers
2881
144
  // ─────────────────────────────────────────────────────────────────────────────
2882
145
  /**
@@ -2885,24 +148,8 @@ toolHandlers.demo = async (args) => {
2885
148
  */
2886
149
  function determineOperation(toolName, args) {
2887
150
  if (toolName === "persona") {
2888
- if (args.analyze)
2889
- return "analyze";
2890
- if (args.update)
2891
- return "update";
2892
- if (args.create)
2893
- return "create";
2894
- if (args.delete)
2895
- return "delete";
2896
- if (args.snapshot)
2897
- return "snapshot";
2898
- if (args.history)
2899
- return "history";
2900
- if (args.restore)
2901
- return "restore";
2902
- if (args.compare)
2903
- return "compare";
2904
- if (args.sanitize)
2905
- return "sanitize";
151
+ if (args.method)
152
+ return String(args.method);
2906
153
  if (args.data)
2907
154
  return "data";
2908
155
  if (args.id)
@@ -2910,24 +157,21 @@ function determineOperation(toolName, args) {
2910
157
  return "list";
2911
158
  }
2912
159
  if (toolName === "catalog") {
2913
- if (args.id)
2914
- return "get";
2915
- if (args.for)
2916
- return "recommend"; // V2 schema uses 'for' parameter
2917
- if (args.query)
2918
- return "search";
160
+ if (args.method)
161
+ return String(args.method);
2919
162
  return "list";
2920
163
  }
2921
164
  if (toolName === "sync") {
2922
- if (args.execute)
2923
- return "execute";
2924
- if (args.status)
2925
- return "status";
165
+ if (args.method)
166
+ return String(args.method);
2926
167
  return "preview";
2927
168
  }
2928
169
  if (toolName === "toolkit_feedback") {
2929
170
  return args.method ? String(args.method) : "submit";
2930
171
  }
172
+ if (toolName === "debug") {
173
+ return args.method ? String(args.method) : "conversations";
174
+ }
2931
175
  return toolName;
2932
176
  }
2933
177
  // ─────────────────────────────────────────────────────────────────────────────