@ema.co/mcp-toolkit 1.4.2 → 1.4.3

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.

@@ -229,33 +229,46 @@ export class EmaClient {
229
229
  return this.getPersonasForTenant();
230
230
  }
231
231
  /**
232
- * Get a single persona by ID with full details including workflow_def.
232
+ * Get a single persona by ID with full details including workflow_def and proto_config.
233
233
  * Uses /api/personas/{id} endpoint which returns the complete persona including workflow_def.
234
+ * Falls back to /api/ai_employee/get_ai_employee for additional data if needed.
234
235
  */
235
236
  async getPersonaById(personaId) {
237
+ let persona = null;
236
238
  // Primary: /api/personas/{id} - returns full persona with workflow_def
237
239
  // Note: get_minimal_persona=false is REQUIRED to get workflow_def in response
238
240
  try {
239
241
  const resp = await this.requestWithRetries("GET", `/api/personas/${personaId}?get_minimal_persona=false`, {});
240
242
  if (resp.ok) {
241
- return (await resp.json());
243
+ persona = (await resp.json());
242
244
  }
243
245
  }
244
246
  catch {
245
247
  // Fall through to fallback
246
248
  }
247
249
  // Fallback: /api/ai_employee/get_ai_employee endpoint
250
+ // Also used to supplement proto_config if missing from primary endpoint
248
251
  try {
249
252
  const resp = await this.requestWithRetries("GET", `/api/ai_employee/get_ai_employee?persona_id=${personaId}`, {});
250
253
  if (resp.ok) {
251
254
  const data = (await resp.json());
252
- return data.persona ?? data.ai_employee ?? data.config ?? null;
255
+ const fallbackPersona = data.persona ?? data.ai_employee ?? data.config ?? null;
256
+ if (!persona) {
257
+ // Primary failed, use fallback entirely
258
+ persona = fallbackPersona;
259
+ }
260
+ else if (fallbackPersona) {
261
+ // Merge proto_config from fallback if missing in primary
262
+ if (!persona.proto_config && fallbackPersona.proto_config) {
263
+ persona.proto_config = fallbackPersona.proto_config;
264
+ }
265
+ }
253
266
  }
254
267
  }
255
268
  catch {
256
- // Fall through to return null
269
+ // Fallback failed, continue with what we have
257
270
  }
258
- return null;
271
+ return persona;
259
272
  }
260
273
  /**
261
274
  * Get workflow definition by workflow_id.
@@ -492,33 +505,164 @@ export class EmaClient {
492
505
  clearTimeout(timeoutId);
493
506
  }
494
507
  }
508
+ /**
509
+ * Make a Connect-RPC style request with persona context.
510
+ * DataIngestService requires x-persona-id header.
511
+ */
512
+ async connectRequest(path, body, personaId) {
513
+ const fullUrl = `${this.env.baseUrl.replace(/\/$/, "")}${path}`;
514
+ return fetch(fullUrl, {
515
+ method: "POST",
516
+ headers: {
517
+ "Authorization": `Bearer ${this.env.bearerToken}`,
518
+ "Content-Type": "application/json",
519
+ "x-persona-id": personaId,
520
+ "Connect-Protocol-Version": "1",
521
+ },
522
+ body: JSON.stringify(body),
523
+ });
524
+ }
495
525
  /**
496
526
  * List data source files for an AI Employee.
497
- * Uses the DataIngestService gRPC endpoint.
527
+ * Uses the DataIngestService GetRootContentNodes gRPC endpoint.
528
+ *
529
+ * @param personaId - The persona ID
530
+ * @param opts - Optional parameters for pagination and filtering
498
531
  */
499
- async listDataSourceFiles(personaId) {
500
- // Try the content aggregates endpoint (may return limited info)
532
+ async listDataSourceFiles(personaId, opts) {
533
+ const widgetName = opts?.widgetName ?? "fileUpload";
534
+ const page = opts?.page ?? 1;
535
+ const limit = opts?.limit ?? 50;
501
536
  try {
502
- const resp = await this.requestWithRetries("POST", "/dataingest.v1.DataIngestService/GetContentNodeAggregates", {
503
- json: {
504
- widget_name: "fileUpload",
505
- include_files: true,
506
- persona_id: personaId,
507
- },
508
- });
537
+ // Use Connect-RPC JSON encoding
538
+ const resp = await this.connectRequest("/dataingest.v1.DataIngestService/GetRootContentNodes", {
539
+ widgetName: widgetName,
540
+ includeFiles: true,
541
+ groupId: personaId,
542
+ groupType: "FILE_PICKER_GROUP_TYPE_PERSONA", // 2 in proto enum
543
+ page: page,
544
+ limit: limit,
545
+ }, personaId);
509
546
  if (resp.ok) {
510
547
  const data = await resp.json();
511
- return (data.files ?? []).map((f) => ({
512
- id: f.id,
513
- filename: f.name,
514
- status: f.status ?? "unknown",
548
+ const files = (data.data ?? []).map((f) => ({
549
+ id: f.contentNodeId ?? "",
550
+ filename: f.nodeName ?? "",
551
+ status: this.normalizeContentNodeStatus(f.status),
552
+ createdAt: f.createdAt,
553
+ updatedAt: f.updatedAt,
554
+ sourceType: f.sourceType,
515
555
  }));
556
+ return {
557
+ files,
558
+ pagination: {
559
+ page: data.pagination?.page ?? page,
560
+ limit: data.pagination?.limit ?? limit,
561
+ total: data.pagination?.total ?? files.length,
562
+ totalPages: data.pagination?.totalPages ?? 1,
563
+ },
564
+ widgetName: data.meta?.widgetName ?? widgetName,
565
+ };
566
+ }
567
+ // If Connect-JSON fails, try to get files from persona's status_log
568
+ const persona = await this.getPersonaById(personaId);
569
+ if (persona) {
570
+ const statusLogFiles = this.extractFilesFromStatusLog(persona, widgetName);
571
+ if (statusLogFiles.length > 0) {
572
+ return {
573
+ files: statusLogFiles,
574
+ pagination: {
575
+ page: 1,
576
+ limit: statusLogFiles.length,
577
+ total: statusLogFiles.length,
578
+ totalPages: 1
579
+ },
580
+ widgetName,
581
+ source: "status_log",
582
+ };
583
+ }
516
584
  }
517
585
  }
518
586
  catch {
519
587
  // Fall through to return empty
520
588
  }
521
- return [];
589
+ return {
590
+ files: [],
591
+ pagination: { page: 1, limit: 0, total: 0, totalPages: 0 },
592
+ widgetName,
593
+ };
594
+ }
595
+ /**
596
+ * Extract file list from persona's status_log.
597
+ * This is available when persona is fetched with get_minimal_persona=false.
598
+ *
599
+ * @param persona - The persona DTO with status_log
600
+ * @param widgetName - Widget name to extract files for (default: "fileUpload")
601
+ */
602
+ extractFilesFromStatusLog(persona, widgetName = "fileUpload") {
603
+ const statusLog = persona.status_log;
604
+ if (!statusLog) {
605
+ return [];
606
+ }
607
+ const files = statusLog[widgetName];
608
+ if (!Array.isArray(files)) {
609
+ return [];
610
+ }
611
+ return files
612
+ .filter((f) => typeof f === "object" && f !== null)
613
+ .map((f) => ({
614
+ id: f.id ?? f.filename ?? "",
615
+ filename: f.filename ?? f.id ?? "",
616
+ status: f.status ?? "unknown",
617
+ tags: Array.isArray(f.tags) ? f.tags : undefined,
618
+ }))
619
+ .filter((f) => f.id !== "");
620
+ }
621
+ /**
622
+ * Get data source aggregates (file counts by status).
623
+ */
624
+ async getDataSourceAggregates(personaId, widgetName = "fileUpload") {
625
+ try {
626
+ const resp = await this.connectRequest("/dataingest.v1.DataIngestService/GetContentNodeAggregates", {
627
+ widgetName: widgetName,
628
+ includeFiles: true,
629
+ groupId: personaId,
630
+ groupType: "FILE_PICKER_GROUP_TYPE_PERSONA",
631
+ }, personaId);
632
+ if (resp.ok) {
633
+ const data = await resp.json();
634
+ const agg = data.widgetAggregates?.find(w => w.widgetName === widgetName);
635
+ const counts = agg?.totalCounts ?? {};
636
+ return {
637
+ total: (counts.pending ?? 0) + (counts.success ?? 0) + (counts.failed ?? 0),
638
+ pending: counts.pending ?? 0,
639
+ success: counts.success ?? 0,
640
+ failed: counts.failed ?? 0,
641
+ widgetName,
642
+ };
643
+ }
644
+ }
645
+ catch {
646
+ // Fall through
647
+ }
648
+ return { total: 0, pending: 0, success: 0, failed: 0, widgetName };
649
+ }
650
+ /**
651
+ * Normalize content node status from proto enum to readable string.
652
+ */
653
+ normalizeContentNodeStatus(status) {
654
+ if (!status)
655
+ return "unknown";
656
+ // Handle both enum name and human-readable formats
657
+ const statusMap = {
658
+ "CONTENT_NODE_STATUS_UNSPECIFIED": "unknown",
659
+ "CONTENT_NODE_STATUS_PENDING": "pending",
660
+ "CONTENT_NODE_STATUS_INDEXING": "indexing",
661
+ "CONTENT_NODE_STATUS_INDEXED": "indexed",
662
+ "CONTENT_NODE_STATUS_FAILED": "failed",
663
+ "CONTENT_NODE_STATUS_SUCCESS": "success",
664
+ };
665
+ return statusMap[status] ?? status.toLowerCase().replace("content_node_status_", "");
522
666
  }
523
667
  /**
524
668
  * Detect MIME type from filename extension.
@@ -153,6 +153,7 @@ export const CreatePersonaRequestSchema = z.object({
153
153
  description: z.string().optional(),
154
154
  template_id: z.string().optional(),
155
155
  source_persona_id: z.string().optional(),
156
+ clone_data: z.boolean().optional(), // Clone knowledge base files when cloning from source_persona_id
156
157
  proto_config: z.record(z.unknown()).optional(),
157
158
  welcome_messages: WelcomeMessageSchema.optional(),
158
159
  trigger_type: z.string().optional(),
@@ -28,24 +28,40 @@ Some clients show a **prefixed name** (for example Cursor: `mcp_ema_workflow`).
28
28
 
29
29
  ## Start-here flows (LLM-friendly)
30
30
 
31
- ### Create a new AI Employee (recommended sequence)
31
+ ### Create a new AI Employee (greenfield)
32
32
 
33
33
  1. **Requirements**: `template(questions=true)` (and `template(questions=true, category="Voice")` for voice)
34
34
  2. **Pick agents/pattern**: `action(suggest="<use case>")`
35
35
  3. **Get the pattern**: `template(pattern="<pattern>")`
36
- 4. **Generate configs**:
37
- - `workflow(input="<requirements>", type="chat|voice|dashboard")`
38
- - If you want deterministic local generation: add `use_autobuilder=false`
36
+ 4. **Generate workflow** (preview by default):
37
+ ```typescript
38
+ workflow(input="<requirements>", type="chat|voice|dashboard")
39
+ ```
39
40
  5. **Create persona** (if needed): `persona(mode="create", name="...", type="chat|voice|dashboard")`
40
- 6. **Deploy**: `workflow(mode="deploy", persona_id="...", workflow_def=<...>, proto_config=<...>)`
41
+ 6. **Deploy** (preview=false):
42
+ ```typescript
43
+ workflow(input="<requirements>", persona_id="<id>", preview=false)
44
+ ```
41
45
  7. **Review**: `workflow(mode="analyze", persona_id="...")`
42
46
 
47
+ ### Extend an existing AI Employee (brownfield)
48
+
49
+ **Combine multiple changes in one command!**
50
+
51
+ ```typescript
52
+ // Preview changes (safe default)
53
+ workflow(mode="extend", persona_id="abc-123", input="add caller_type categorizer, add HITL before email")
54
+
55
+ // Deploy changes
56
+ workflow(mode="extend", persona_id="abc-123", input="...", preview=false)
57
+ ```
58
+
43
59
  ### Review or debug an existing AI Employee
44
60
 
45
61
  1. Fetch full workflow: `persona(id="<id-or-exact-name>", include_workflow=true)`
46
62
  2. Analyze: `workflow(mode="analyze", persona_id="<persona_id>")`
47
- 3. Preview fixes: `workflow(mode="optimize", persona_id="<persona_id>", preview=true)`
48
- 4. Apply fixes: `workflow(mode="optimize", persona_id="<persona_id>")`
63
+ 3. Preview fixes: `workflow(mode="optimize", persona_id="<persona_id>")`
64
+ 4. Apply fixes: `workflow(mode="optimize", persona_id="<persona_id>", preview=false)`
49
65
 
50
66
  ### Upload / manage knowledge base documents
51
67
 
@@ -61,6 +77,28 @@ Some clients show a **prefixed name** (for example Cursor: `mcp_ema_workflow`).
61
77
  - List all synced in env: `sync(mode="status", list_synced=true, env="dev")`
62
78
  - Config: `sync(mode="config")`
63
79
 
80
+ ### Version / snapshot AI Employees
81
+
82
+ Take snapshots before making changes, restore if something breaks.
83
+
84
+ ```typescript
85
+ // Before a risky change - create a snapshot
86
+ persona(id="My Bot", mode="version_create", message="Before adding HITL")
87
+
88
+ // Make changes...
89
+ workflow(mode="extend", persona_id="abc", input="add HITL before email", preview=false)
90
+
91
+ // If something breaks - list versions and restore
92
+ persona(id="My Bot", mode="version_list")
93
+ persona(id="My Bot", mode="version_restore", version="v3")
94
+ ```
95
+
96
+ **Auto-snapshot on deploy:**
97
+ ```typescript
98
+ persona(id="My Bot", mode="version_policy", auto_on_deploy=true)
99
+ // Now every deploy automatically creates a snapshot first
100
+ ```
101
+
64
102
  ### Demo data → RAG-ready Markdown → upload
65
103
 
66
104
  1. Get a template: `demo(mode="template", entity="customer")`
@@ -77,18 +115,55 @@ Some clients show a **prefixed name** (for example Cursor: `mcp_ema_workflow`).
77
115
  - **Update**: `persona(id="<id>", mode="update", name="...", description="...", enabled=true|false)`
78
116
  - **Compare**: `persona(id="<id>", mode="compare", compare_to="<id>", compare_env="dev")`
79
117
  - **Templates**: `persona(templates=true)`
118
+ - **Version management**:
119
+ ```typescript
120
+ persona(id="abc", mode="version_create", message="Before major update") // Create snapshot
121
+ persona(id="abc", mode="version_list") // List all versions
122
+ persona(id="abc", mode="version_get", version="v3") // Get specific version
123
+ persona(id="abc", mode="version_compare", v1="v2", v2="v3") // Compare versions
124
+ persona(id="abc", mode="version_restore", version="v2") // Restore to version
125
+ persona(id="abc", mode="version_policy", auto_on_deploy=true) // Auto-snapshot settings
126
+ ```
80
127
 
81
128
  ### `workflow`
82
129
 
83
- - **Generate**: `workflow(input="<requirements>", type="chat|voice|dashboard")`
84
- - **Analyze**:
85
- - By persona: `workflow(mode="analyze", persona_id="<id>")`
86
- - By JSON: `workflow(mode="analyze", workflow_def={<...>})`
87
- - Limit output: `workflow(mode="analyze", persona_id="<id>", include=["issues"|"connections"|"fixes"|"metrics"])`
88
- - **Deploy**: `workflow(mode="deploy", persona_id="<id>", workflow_def={<...>}, proto_config={<...>}, auto_fix=true)`
89
- - **Optimize** (analyze + fixes + deploy): `workflow(mode="optimize", persona_id="<id>", preview=true|false)`
90
- - **Compare**: `workflow(mode="compare", persona_id="<before>", compare_to="<after>")`
91
- - **Compile** (from explicit nodes): `workflow(mode="compile", name="...", description="...", type="chat|voice|dashboard", nodes=[...], result_mappings=[...])`
130
+ All modification modes default to `preview=true` for safety. Use `preview=false` to deploy.
131
+
132
+ - **Generate (greenfield)**: Create new workflow
133
+ ```typescript
134
+ workflow(input="IT helpdesk with KB search") // Preview
135
+ workflow(input="...", persona_id="abc", preview=false) // Generate AND deploy
136
+ ```
137
+
138
+ - **Extend (brownfield)**: Modify existing workflow with natural language
139
+ ```typescript
140
+ workflow(mode="extend", persona_id="abc", input="add caller_type categorizer")
141
+ workflow(mode="extend", persona_id="abc", input="add X, add Y, add Z") // Multiple changes!
142
+ workflow(mode="extend", persona_id="abc", input="...", preview=false) // Extend AND deploy
143
+ ```
144
+
145
+ - **Optimize**: Fix issues in existing workflow
146
+ ```typescript
147
+ workflow(mode="optimize", persona_id="abc") // Preview fixes
148
+ workflow(mode="optimize", persona_id="abc", preview=false) // Apply fixes
149
+ ```
150
+
151
+ - **Analyze** (always read-only):
152
+ ```typescript
153
+ workflow(mode="analyze", persona_id="abc")
154
+ workflow(mode="analyze", workflow_def={...})
155
+ workflow(mode="analyze", persona_id="abc", include=["issues", "fixes"])
156
+ ```
157
+
158
+ - **Compare** (always read-only):
159
+ ```typescript
160
+ workflow(mode="compare", persona_id="abc", compare_to="def")
161
+ ```
162
+
163
+ - **Compile** (returns workflow_def from explicit node spec):
164
+ ```typescript
165
+ workflow(mode="compile", name="...", description="...", nodes=[...])
166
+ ```
92
167
 
93
168
  ### `action`
94
169
 
@@ -150,7 +225,49 @@ The `workflow(mode="optimize")` can automatically fix common issues:
150
225
  | External API call | ❌ No HITL | Request explicitly: "require human approval" |
151
226
  | Create/update records | ❌ No HITL | Request explicitly in prompt |
152
227
 
153
- To add HITL after deployment: `workflow(mode="extend", persona_id="...", description="add human approval before email")`
228
+ To add HITL after deployment: `workflow(persona_id="...", input="add human approval before email")`
229
+
230
+
231
+ ## Brownfield Workflow Extension
232
+
233
+ Extend existing workflows with new capabilities while preserving existing logic.
234
+
235
+ ### Usage
236
+
237
+ ```typescript
238
+ // Add new nodes/intents
239
+ workflow(persona_id="abc", input="add caller_type categorizer with Advisor and Client roles")
240
+
241
+ // Add enum options to existing categorizer
242
+ workflow(persona_id="abc", input="add Market Analysis intent to the categorizer")
243
+
244
+ // Add custom wiring
245
+ workflow(persona_id="abc", input="add send_email node connected to entity_extractor output")
246
+
247
+ // Merge a complete workflow_def (extend mode - preserves existing)
248
+ workflow(persona_id="abc", workflow_def={...}, merge_mode="extend")
249
+
250
+ // Replace with a new workflow (destructive)
251
+ workflow(persona_id="abc", workflow_def={...}, merge_mode="replace", force=true)
252
+ ```
253
+
254
+ ### Merge Modes
255
+
256
+ | Mode | Behavior |
257
+ |------|----------|
258
+ | `extend` (default) | Add new nodes, preserve existing, merge enums |
259
+ | `replace` | Replace nodes not in incoming (destructive) |
260
+
261
+ ### Capabilities
262
+
263
+ | Capability | Example |
264
+ |------------|---------|
265
+ | ✅ Add new nodes | `input="add password reset intent"` |
266
+ | ✅ Add enum options | `input="add Advisor/Client to caller_type"` |
267
+ | ✅ Add HITL gates | `input="add approval before sending emails"` |
268
+ | ✅ Modify node wiring | `input="wire entity_extraction to email handler"` |
269
+ | ✅ Preserve existing logic | Existing nodes/enums kept unless modified |
270
+
154
271
 
155
272
  ## Auto Builder Prompt Length Limits
156
273
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Ema AI Employee toolkit - MCP server, CLI, and SDK for managing AI Employees across environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",