@ema.co/mcp-toolkit 1.7.0 → 1.7.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.
@@ -194,10 +194,18 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
194
194
  try {
195
195
  sourcePersona = await resolvePersona(client, cloneFrom);
196
196
  if (sourcePersona) {
197
- const protoConfig = sourcePersona.proto_config;
198
- const projectSettings = protoConfig?.projectSettings;
199
- const projectType = projectSettings?.projectType;
200
- sourcePersonaType = projectType === 5 ? "voice" : projectType === 4 ? "chat" : projectType === 2 ? "dashboard" : undefined;
197
+ // Use trigger_type (root level) for reliable persona type detection
198
+ // trigger_type: 0=chat, 1=voice, 2=dashboard
199
+ const triggerType = sourcePersona.trigger_type;
200
+ if (triggerType === 1) {
201
+ sourcePersonaType = "voice";
202
+ }
203
+ else if (triggerType === 2) {
204
+ sourcePersonaType = "dashboard";
205
+ }
206
+ else if (triggerType === 0) {
207
+ sourcePersonaType = "chat";
208
+ }
201
209
  sourceDashboardId = sourcePersona.workflow_dashboard_id;
202
210
  }
203
211
  }
@@ -241,17 +249,35 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
241
249
  if (shouldSanitize) {
242
250
  sanitizationSession = new SanitizationSession();
243
251
  }
252
+ // Track document inputs that couldn't be cloned (require manual re-upload)
253
+ const allSkippedDocuments = [];
244
254
  for (const row of sourceRows.rows) {
245
255
  try {
246
256
  // Build inputs from row's input column values
247
257
  const inputs = [];
258
+ const rowSkippedDocs = [];
248
259
  for (const inputCol of inputColumns) {
249
260
  const colValue = row.columnValues.find(cv => cv.columnId === inputCol.columnId);
250
261
  if (!colValue)
251
262
  continue;
252
263
  // Handle different column types
253
264
  if (inputCol.columnType === "COLUMN_TYPE_DOCUMENT") {
254
- // Skip document columns for now
265
+ // Document inputs can't be auto-cloned (no download API for original files)
266
+ // Track them for manual re-upload notification
267
+ const docValues = colValue.value.documentCellValue?.documentValues ?? [];
268
+ for (const doc of docValues) {
269
+ if (!doc.deleted) {
270
+ rowSkippedDocs.push({
271
+ name: inputCol.name,
272
+ original_filename: doc.name
273
+ });
274
+ allSkippedDocuments.push({
275
+ column: inputCol.name,
276
+ filename: doc.name,
277
+ contentNodeId: doc.contentNodeId,
278
+ });
279
+ }
280
+ }
255
281
  continue;
256
282
  }
257
283
  else if (inputCol.columnType === "COLUMN_TYPE_STRING") {
@@ -288,27 +314,74 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
288
314
  inputs.push({ name: inputCol.name, string_value: value });
289
315
  }
290
316
  }
317
+ // Note: COLUMN_TYPE_NUMBER and COLUMN_TYPE_BOOLEAN are not currently
318
+ // parsed from the dashboard rows protobuf response, so they're skipped
291
319
  }
320
+ // If we have inputs (even if some docs were skipped), try to clone
292
321
  if (inputs.length === 0) {
293
- results.push({ source_row_id: row.id, status: "skipped", error: "No clonable input values" });
322
+ // Check if we had document-only inputs
323
+ if (rowSkippedDocs.length > 0) {
324
+ results.push({
325
+ source_row_id: row.id,
326
+ status: "skipped",
327
+ error: "Document-only inputs require manual re-upload",
328
+ skipped_documents: rowSkippedDocs,
329
+ });
330
+ }
331
+ else {
332
+ results.push({ source_row_id: row.id, status: "skipped", error: "No clonable input values" });
333
+ }
294
334
  continue;
295
335
  }
296
336
  // Upload the row to target dashboard
297
337
  const uploadResult = await client.uploadAndRunDashboardRow(newPersonaId, inputs);
298
- results.push({ source_row_id: row.id, target_row_id: uploadResult.row_id, status: "cloned" });
338
+ const rowResult = {
339
+ source_row_id: row.id,
340
+ target_row_id: uploadResult.row_id,
341
+ status: rowSkippedDocs.length > 0 ? "partial" : "cloned",
342
+ };
343
+ if (rowSkippedDocs.length > 0) {
344
+ rowResult.skipped_documents = rowSkippedDocs;
345
+ }
346
+ results.push(rowResult);
299
347
  }
300
348
  catch (err) {
301
349
  results.push({ source_row_id: row.id, status: "error", error: err instanceof Error ? err.message : String(err) });
302
350
  }
303
351
  }
304
- const clonedCount = results.filter(r => r.status === "cloned").length;
352
+ const clonedCount = results.filter(r => r.status === "cloned" || r.status === "partial").length;
305
353
  dashboardCloneResult = {
306
354
  source_rows: sourceRows.rows.length,
307
355
  cloned_rows: clonedCount,
356
+ partial_rows: results.filter(r => r.status === "partial").length,
308
357
  skipped_rows: results.filter(r => r.status === "skipped").length,
309
358
  error_rows: results.filter(r => r.status === "error").length,
310
359
  sanitization_applied: !!shouldSanitize,
360
+ // Debug info for troubleshooting
361
+ _debug: {
362
+ total_count_from_api: sourceRows.totalCount,
363
+ schema_columns_count: sourceRows.schema.columns.length,
364
+ input_columns_count: inputColumns.length,
365
+ schema_columns: sourceRows.schema.columns.map(c => ({
366
+ id: c.columnId,
367
+ name: c.name,
368
+ type: c.columnType,
369
+ isInput: c.isInput,
370
+ })),
371
+ first_row_id: sourceRows.rows[0]?.id?.substring(0, 100),
372
+ first_row_state: sourceRows.rows[0]?.state,
373
+ first_row_column_values_count: sourceRows.rows[0]?.columnValues?.length,
374
+ results_detail: results,
375
+ },
311
376
  };
377
+ // Include document upload notice if any documents were skipped
378
+ if (allSkippedDocuments.length > 0) {
379
+ dashboardCloneResult.documents_require_manual_upload = {
380
+ count: allSkippedDocuments.length,
381
+ files: allSkippedDocuments.slice(0, 10), // Limit to first 10 for readability
382
+ note: "Document inputs cannot be auto-cloned. Re-upload original files to the cloned dashboard.",
383
+ };
384
+ }
312
385
  }
313
386
  else {
314
387
  dashboardCloneResult = { source_rows: 0, cloned_rows: 0, message: "Source dashboard was empty" };
@@ -318,18 +391,27 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
318
391
  dashboardCloneResult = { error: `Dashboard clone failed: ${err instanceof Error ? err.message : String(err)}` };
319
392
  }
320
393
  }
321
- // Handle sanitization for non-dashboard personas
394
+ // Handle workflow sanitization for ALL persona types (including dashboard)
395
+ // Dashboard rows are sanitized during clone loop above, but workflow still needs sanitization
322
396
  const shouldSanitize = args.sanitize;
323
- if (shouldSanitize && newPersonaId && sourcePersonaType !== "dashboard") {
397
+ if (shouldSanitize && newPersonaId) {
324
398
  const sanitizeResult = await sanitizePersonaById(client, newPersonaId, {
325
399
  examples: args.sanitize_examples,
400
+ preview: args.preview ?? false, // Use preview arg, default to false (apply)
326
401
  });
327
- return {
402
+ const createResult = {
328
403
  success: true,
329
404
  persona_id: newPersonaId,
330
405
  name,
331
406
  sanitization: sanitizeResult,
332
407
  };
408
+ if (sourcePersonaType) {
409
+ createResult.source_persona_type = sourcePersonaType;
410
+ }
411
+ if (dashboardCloneResult) {
412
+ createResult.dashboard_data_clone = dashboardCloneResult;
413
+ }
414
+ return createResult;
333
415
  }
334
416
  const createResult = {
335
417
  success: true,
@@ -883,10 +883,6 @@ export class EmaClient {
883
883
  */
884
884
  async getDashboardRows(dashboardId, personaId, opts) {
885
885
  // Build protobuf request manually (GetDashboardRowsRequest)
886
- // Field 1: dashboard_id (string)
887
- // Field 2: limit (int32)
888
- // Field 3: offset (int32)
889
- // Field 9: get_minimal_row_values (bool)
890
886
  const limit = opts?.limit ?? 100;
891
887
  const offset = opts?.offset ?? 0;
892
888
  const getMinimal = false;
@@ -1003,6 +999,9 @@ export class EmaClient {
1003
999
  }
1004
1000
  /**
1005
1001
  * Parse the protobuf GetDashboardRowsResponse message.
1002
+ *
1003
+ * Note: The response format varies - sometimes columns are in field 1 (as repeated Column),
1004
+ * sometimes in field 2 as Schema { columns }. We detect and handle both cases.
1006
1005
  */
1007
1006
  parseGetDashboardRowsResponse(data) {
1008
1007
  const result = {
@@ -1011,6 +1010,11 @@ export class EmaClient {
1011
1010
  schema: { columns: [] },
1012
1011
  rows: [],
1013
1012
  };
1013
+ // Field mapping (from protobuf analysis):
1014
+ // Field 1 = Schema message (contains nested Column messages at subfield 1)
1015
+ // Field 2 = Row messages (repeated)
1016
+ // Field 3 = totalCount (varint)
1017
+ // Field 4 = dashboardName (string)
1014
1018
  let pos = 0;
1015
1019
  const dec = new TextDecoder();
1016
1020
  while (pos < data.length) {
@@ -1024,14 +1028,15 @@ export class EmaClient {
1024
1028
  const fieldData = data.slice(pos, pos + length);
1025
1029
  pos += length;
1026
1030
  if (fieldNumber === 1) {
1027
- // rows (repeated Row message)
1028
- const row = this.parseProtoRow(fieldData, dec);
1029
- if (row)
1030
- result.rows.push(row);
1031
+ // Schema message - parse columns from it
1032
+ result.schema = this.parseProtoSchemaFromField1(fieldData, dec);
1031
1033
  }
1032
1034
  else if (fieldNumber === 2) {
1033
- // schema (Schema message)
1034
- result.schema = this.parseProtoSchema(fieldData, dec);
1035
+ // Row message
1036
+ const row = this.parseProtoRow(fieldData, dec);
1037
+ if (row && row.id) {
1038
+ result.rows.push(row);
1039
+ }
1035
1040
  }
1036
1041
  else if (fieldNumber === 4) {
1037
1042
  // dashboard_name (string)
@@ -1116,9 +1121,18 @@ export class EmaClient {
1116
1121
  return row.id ? row : null;
1117
1122
  }
1118
1123
  /**
1119
- * Parse Schema message from protobuf.
1124
+ * Parse Schema message from protobuf field 1.
1125
+ * Schema contains repeated Column messages at subfield 1.
1126
+ *
1127
+ * Column fields (from protobuf analysis):
1128
+ * - Field 1 (0x0a): name (string) e.g. "Document"
1129
+ * - Field 2 (0x10): columnType (varint) e.g. 6 = DOCUMENT
1130
+ * - Field 4 (0x20): isInput (bool)
1131
+ * - Field 5 (0x28): isRequired (bool)
1132
+ * - Field 11 (0x5a): actionId (string) e.g. "workflow_input"
1133
+ * - Field 13 (0x6a): columnId (string) e.g. "document-jghm"
1120
1134
  */
1121
- parseProtoSchema(data, dec) {
1135
+ parseProtoSchemaFromField1(data, dec) {
1122
1136
  const schema = { columns: [] };
1123
1137
  let pos = 0;
1124
1138
  while (pos < data.length) {
@@ -1131,10 +1145,11 @@ export class EmaClient {
1131
1145
  const fieldData = data.slice(pos, pos + length);
1132
1146
  pos += length;
1133
1147
  if (fieldNumber === 1) {
1134
- // columns (repeated Column)
1135
- const col = this.parseProtoColumn(fieldData, dec);
1136
- if (col)
1148
+ // Column message
1149
+ const col = this.parseProtoColumnV2(fieldData, dec);
1150
+ if (col) {
1137
1151
  schema.columns.push(col);
1152
+ }
1138
1153
  }
1139
1154
  }
1140
1155
  else if (wireType === 0) {
@@ -1148,24 +1163,35 @@ export class EmaClient {
1148
1163
  return schema;
1149
1164
  }
1150
1165
  /**
1151
- * Parse Column message from protobuf.
1166
+ * Parse Column message from protobuf (v2 - correct field mapping).
1167
+ * Based on actual protobuf analysis:
1168
+ * - Field 1: name (string)
1169
+ * - Field 2: columnType (varint)
1170
+ * - Field 4: isInput (bool)
1171
+ * - Field 5: isRequired (bool)
1172
+ * - Field 11: actionId (string)
1173
+ * - Field 12: actionName (string)
1174
+ * - Field 13: columnId (string)
1152
1175
  */
1153
- parseProtoColumn(data, dec) {
1154
- // Column type enum mapping
1176
+ parseProtoColumnV2(data, dec) {
1177
+ const col = {
1178
+ columnId: "",
1179
+ name: "",
1180
+ columnType: "COLUMN_TYPE_UNSPECIFIED",
1181
+ isInput: false,
1182
+ actionId: "",
1183
+ actionName: "",
1184
+ outputFieldName: "",
1185
+ };
1155
1186
  const columnTypeMap = {
1156
1187
  0: "COLUMN_TYPE_UNSPECIFIED",
1157
1188
  1: "COLUMN_TYPE_STRING",
1158
1189
  2: "COLUMN_TYPE_NUMBER",
1159
1190
  3: "COLUMN_TYPE_BOOLEAN",
1160
1191
  4: "COLUMN_TYPE_ARRAY",
1161
- 5: "COLUMN_TYPE_DOCUMENT",
1162
- };
1163
- const col = {
1164
- columnId: "",
1165
- columnType: "COLUMN_TYPE_UNSPECIFIED",
1166
- name: "",
1167
- actionId: "",
1168
- isInput: false,
1192
+ 5: "COLUMN_TYPE_OBJECT",
1193
+ 6: "COLUMN_TYPE_DOCUMENT",
1194
+ 7: "COLUMN_TYPE_ENUM",
1169
1195
  };
1170
1196
  let pos = 0;
1171
1197
  while (pos < data.length) {
@@ -1177,34 +1203,44 @@ export class EmaClient {
1177
1203
  pos += bytesRead;
1178
1204
  const fieldData = data.slice(pos, pos + length);
1179
1205
  pos += length;
1206
+ const strValue = dec.decode(fieldData);
1180
1207
  if (fieldNumber === 1) {
1181
- col.columnId = dec.decode(fieldData);
1208
+ col.name = strValue;
1182
1209
  }
1183
- else if (fieldNumber === 2) {
1184
- col.name = dec.decode(fieldData);
1210
+ else if (fieldNumber === 11) {
1211
+ col.actionId = strValue;
1185
1212
  }
1186
- else if (fieldNumber === 5) {
1187
- col.actionId = dec.decode(fieldData);
1213
+ else if (fieldNumber === 12) {
1214
+ col.actionName = strValue;
1188
1215
  }
1189
- else if (fieldNumber === 6) {
1190
- col.actionName = dec.decode(fieldData);
1216
+ else if (fieldNumber === 13) {
1217
+ col.columnId = strValue;
1218
+ }
1219
+ else if (fieldNumber === 14) {
1220
+ col.outputFieldName = strValue;
1191
1221
  }
1192
1222
  }
1193
1223
  else if (wireType === 0) {
1194
1224
  const { value, bytesRead } = this.decodeVarintAt(data, pos);
1195
1225
  pos += bytesRead;
1196
- if (fieldNumber === 3) {
1197
- col.columnType = columnTypeMap[value] ?? `COLUMN_TYPE_${value}`;
1226
+ if (fieldNumber === 2) {
1227
+ col.columnType = columnTypeMap[value] ?? "COLUMN_TYPE_UNSPECIFIED";
1198
1228
  }
1199
1229
  else if (fieldNumber === 4) {
1200
1230
  col.isInput = value === 1;
1201
1231
  }
1202
1232
  }
1203
1233
  else {
1204
- break;
1234
+ // Skip unknown wire types (like fixed32/64)
1235
+ if (wireType === 5)
1236
+ pos += 4;
1237
+ else if (wireType === 1)
1238
+ pos += 8;
1239
+ else
1240
+ break;
1205
1241
  }
1206
1242
  }
1207
- return col.columnId ? col : null;
1243
+ return (col.name || col.columnId) ? col : null;
1208
1244
  }
1209
1245
  /**
1210
1246
  * Parse ColumnValue message from protobuf.
@@ -0,0 +1,215 @@
1
+ # MCP Tool Consolidation v2
2
+
3
+ ## Overview
4
+
5
+ Consolidate from 9 tools to **5 core tools** with cleaner separation of concerns.
6
+
7
+ ## Final Tool Design
8
+
9
+ ### 1. `persona` - Manage AI Employees
10
+
11
+ ```typescript
12
+ persona(
13
+ id?: string, // get specific persona
14
+ query?: string, // search by name
15
+ all?: boolean, // list all
16
+
17
+ // Create/Clone
18
+ name?: string, // for create
19
+ type?: "voice" | "chat" | "dashboard",
20
+ input?: string, // natural language description
21
+ clone_from?: string, // clone source persona ID
22
+ clone_data?: boolean, // also clone files
23
+ sanitize?: boolean, // sanitize PII during clone
24
+
25
+ // Update
26
+ enabled?: boolean, // enable/disable
27
+
28
+ // Compare
29
+ compare_to?: string, // compare two personas
30
+
31
+ // Output control
32
+ include_workflow?: boolean, // include workflow_def in response
33
+ )
34
+ ```
35
+
36
+ **Examples:**
37
+ ```
38
+ persona(all=true) # list all
39
+ persona(id="abc-123") # get specific
40
+ persona(query="support") # search
41
+ persona(name="My Bot", type="voice", input="...") # create
42
+ persona(clone_from="abc", name="Clone", clone_data=true, sanitize=true)
43
+ ```
44
+
45
+ ---
46
+
47
+ ### 2. `data` - Manage Persona Data
48
+
49
+ ```typescript
50
+ data(
51
+ persona_id: string, // REQUIRED - data belongs to persona
52
+ mode?: "list" | "get" | "upload" | "delete" | "sanitize", // default: list
53
+ file_id?: string, // for get/delete specific
54
+ include_results?: boolean, // for dashboard: show extracted values
55
+ )
56
+ ```
57
+
58
+ **Examples:**
59
+ ```
60
+ data(persona_id="abc") # list files
61
+ data(persona_id="abc", file_id="x") # get file details
62
+ data(persona_id="abc", mode="upload") # upload (file from context)
63
+ data(persona_id="abc", mode="delete", file_id="x")
64
+ data(persona_id="abc", mode="sanitize") # sanitize all data
65
+ data(persona_id="abc", include_results=true) # list with extraction results
66
+ ```
67
+
68
+ **For dashboard personas, file listing includes:**
69
+ ```json
70
+ {
71
+ "files": [{
72
+ "id": "file-123",
73
+ "name": "contract.pdf",
74
+ "processing": {
75
+ "status": "success",
76
+ "extracted": { "vendor": "Acme", "amount": "$50k" }
77
+ }
78
+ }]
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ### 3. `workflow` - Workflow Operations
85
+
86
+ ```typescript
87
+ workflow(
88
+ persona_id?: string, // target persona
89
+
90
+ // Generate/Modify
91
+ input?: string, // natural language description
92
+
93
+ // Analyze
94
+ analyze?: boolean, // analyze current workflow
95
+ optimize?: boolean, // suggest optimizations
96
+
97
+ // Deploy
98
+ workflow_def?: object, // direct workflow deployment
99
+ preview?: boolean, // preview without deploying (default: true)
100
+ )
101
+ ```
102
+
103
+ **Examples:**
104
+ ```
105
+ workflow(persona_id="abc", input="Add email notification")
106
+ workflow(persona_id="abc", analyze=true)
107
+ workflow(persona_id="abc", optimize=true, preview=false)
108
+ ```
109
+
110
+ ---
111
+
112
+ ### 4. `reference` - All Reference Information
113
+
114
+ ```typescript
115
+ reference(
116
+ type?: "actions" | "templates" | "patterns" | "envs" | "concepts" | "guidance",
117
+
118
+ // For actions
119
+ action_id?: string, // get specific action
120
+ query?: string, // search
121
+ suggest?: string, // suggest actions for use case
122
+
123
+ // For guidance
124
+ topic?: string, // guidance topic
125
+
126
+ // For concepts
127
+ concept?: string, // specific concept
128
+
129
+ // Validation helpers
130
+ check_types?: { source: string, target: string },
131
+ validate_prompt?: string,
132
+ )
133
+ ```
134
+
135
+ **Examples:**
136
+ ```
137
+ reference(type="actions") # list all actions
138
+ reference(type="actions", query="email") # search actions
139
+ reference(type="actions", suggest="IT helpdesk")
140
+ reference(action_id="send_email") # get specific action
141
+ reference(type="templates") # list workflow templates
142
+ reference(type="patterns") # list patterns
143
+ reference(type="envs") # list environments
144
+ reference(type="concepts") # list concepts
145
+ reference(concept="HITL") # get specific concept
146
+ reference(type="guidance", topic="categorizer-routing")
147
+ ```
148
+
149
+ ---
150
+
151
+ ### 5. `sync` - Environment Sync
152
+
153
+ ```typescript
154
+ sync(
155
+ id?: string, // persona ID or name
156
+ target?: string, // target environment
157
+ dry_run?: boolean, // simulate without changes
158
+
159
+ // Status
160
+ mode?: "run" | "status" | "config",
161
+ list_synced?: boolean, // list all synced personas
162
+ )
163
+ ```
164
+
165
+ **Examples:**
166
+ ```
167
+ sync(id="IT Support", target="dev")
168
+ sync(id="abc-123", target="staging", dry_run=true)
169
+ sync(mode="status", id="abc-123")
170
+ sync(mode="config")
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Migration Path
176
+
177
+ | Old Tool | New Location |
178
+ |----------|--------------|
179
+ | `env` | `reference(type="envs")` |
180
+ | `knowledge` | `data` |
181
+ | `demo` | Removed (generate separately or future addition) |
182
+ | `action` | `reference(type="actions")` |
183
+ | `template` | `reference(type="templates")` / `reference(type="patterns")` |
184
+
185
+ ## Mental Model
186
+
187
+ ```
188
+ ┌─────────────────────────────────────────┐
189
+ │ PERSONA │
190
+ │ ┌─────────┐ ┌──────────┐ │
191
+ │ │ data │ │ workflow │ │
192
+ │ └─────────┘ └──────────┘ │
193
+ └─────────────────────────────────────────┘
194
+ │ │
195
+ └──────┬───────┘
196
+
197
+ ┌───────────▼───────────┐
198
+ │ reference │ (actions, templates, guidance)
199
+ └───────────────────────┘
200
+
201
+ ┌───────────▼───────────┐
202
+ │ sync │ (copy between envs)
203
+ └───────────────────────┘
204
+ ```
205
+
206
+ ## Implementation Checklist
207
+
208
+ - [ ] Update `tools-consolidated.ts` with new tool definitions
209
+ - [ ] Update `handlers-consolidated.ts` with routing logic
210
+ - [ ] Migrate `knowledge` handlers to `data`
211
+ - [ ] Migrate `action`, `template`, `env` to `reference`
212
+ - [ ] Remove deprecated tools
213
+ - [ ] Update tests
214
+ - [ ] Update documentation
215
+ - [ ] Add deprecation warnings for old tool names (backwards compat)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
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",