@ema.co/mcp-toolkit 2026.2.23-2 → 2026.2.24

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.

@@ -301,6 +301,134 @@ export const DEMO_SCENARIOS = {
301
301
  },
302
302
  ],
303
303
  },
304
+ "finance-dunning": {
305
+ id: "finance-dunning",
306
+ name: "Finance Automated Dunning",
307
+ description: "AR dunning assistant: balance inquiries, payment links, disputes, payment plans, and escalation",
308
+ persona_types: ["chat", "voice"],
309
+ tags: ["finance", "ar", "dunning", "collections", "receivables", "billing"],
310
+ intents: [
311
+ {
312
+ name: "balance_inquiry",
313
+ description: "What do I owe? Current balance and aging",
314
+ example_questions: ["What's my balance?", "Do I have any overdue invoices?", "What do I owe?"],
315
+ },
316
+ {
317
+ name: "payment_made",
318
+ description: "Customer says they already paid",
319
+ example_questions: ["I paid last week", "I already sent the wire", "Check your records"],
320
+ },
321
+ {
322
+ name: "dispute",
323
+ description: "Dispute invoice or amount",
324
+ example_questions: ["This invoice is wrong", "We never received that order", "I'm disputing this charge"],
325
+ },
326
+ {
327
+ name: "payment_plan",
328
+ description: "Request installments or extension",
329
+ example_questions: ["Can I pay in two installments?", "I need 30 more days", "Can we set up a payment plan?"],
330
+ },
331
+ {
332
+ name: "how_to_pay",
333
+ description: "How to pay or payment link",
334
+ example_questions: ["How do I pay?", "Where's the payment link?", "What payment methods do you accept?"],
335
+ },
336
+ {
337
+ name: "escalate",
338
+ description: "Request to speak to a person",
339
+ example_questions: ["I want to talk to someone", "Transfer me to collections", "I need to speak to AR"],
340
+ },
341
+ {
342
+ name: "Fallback",
343
+ description: "Anything else",
344
+ example_questions: ["What's the weather?", "Tell me about your company"],
345
+ },
346
+ ],
347
+ entities: [
348
+ {
349
+ type: "customer",
350
+ count: 3,
351
+ template: {
352
+ id: "CUST-AR-{{id}}",
353
+ name: "{{name}}",
354
+ email: "{{email}}",
355
+ payment_terms_days: 30,
356
+ payment_plan_active: false,
357
+ },
358
+ },
359
+ {
360
+ type: "invoice",
361
+ count: 3,
362
+ template: {
363
+ invoice_id: "INV-{{id}}",
364
+ customer_id: "CUST-AR-{{cust}}",
365
+ amount_due: "{{amount}}",
366
+ due_date: "{{due_date}}",
367
+ days_past_due: "{{days}}",
368
+ dunning_level: "{{level}}",
369
+ payment_link: "{{payment_link}}",
370
+ },
371
+ },
372
+ {
373
+ type: "dunning_policy",
374
+ count: 1,
375
+ template: {
376
+ reminder_1_days: "1-7",
377
+ reminder_2_days: "8-14",
378
+ final_notice_days: "15-30",
379
+ escalation_days: "31+",
380
+ payment_plan_note: "Payment plan requests are reviewed by AR within 1 business day.",
381
+ },
382
+ },
383
+ ],
384
+ qa_pairs: [
385
+ {
386
+ phase: "intro",
387
+ question: "Hi, I have a question about my invoice.",
388
+ answer_template: "Hi! I'm here to help with your invoice and payment questions. I can look up your balance, share the payment link, or help with a dispute or payment plan request. What would you like to do?",
389
+ intent: "balance_inquiry",
390
+ },
391
+ {
392
+ phase: "main",
393
+ question: "What do I owe?",
394
+ answer_template: "Your current balance is {{amount_due}} (invoice {{invoice_id}}, due {{due_date}}). You can pay here: {{payment_link}}. If you need a payment plan or have already paid, tell me and I'll help.",
395
+ intent: "balance_inquiry",
396
+ required_entity_type: "invoice",
397
+ },
398
+ {
399
+ phase: "main",
400
+ question: "How do I pay?",
401
+ answer_template: "You can pay online using this link: {{payment_link}}. We accept major credit cards and ACH. If you've already paid, please have your payment reference or date ready and I can help verify.",
402
+ intent: "how_to_pay",
403
+ required_entity_type: "invoice",
404
+ },
405
+ {
406
+ phase: "main",
407
+ question: "I already paid last week.",
408
+ answer_template: "Thanks for letting us know. Our AR team will verify the payment. Can you share the payment reference number or the date you paid? We'll follow up if anything doesn't match.",
409
+ intent: "payment_made",
410
+ },
411
+ {
412
+ phase: "advanced",
413
+ question: "This invoice is wrong.",
414
+ answer_template: "I'm sorry to hear that. I'm not able to adjust invoices here. I've escalated this to our AR team—they'll contact you to resolve the dispute. You should hear back within 1 business day.",
415
+ intent: "dispute",
416
+ },
417
+ {
418
+ phase: "advanced",
419
+ question: "Can I pay in two installments?",
420
+ answer_template: "Payment plan requests are reviewed by our AR team. I've submitted your request; someone will get back to you within 1 business day with options.",
421
+ intent: "payment_plan",
422
+ required_entity_type: "dunning_policy",
423
+ },
424
+ {
425
+ phase: "closing",
426
+ question: "I want to talk to someone.",
427
+ answer_template: "No problem. I've requested that our AR team contact you. You should hear from someone within 1 business day. Is there anything else I can help with in the meantime?",
428
+ intent: "escalate",
429
+ },
430
+ ],
431
+ },
304
432
  };
305
433
  // ─────────────────────────────────────────────────────────────────────────────
306
434
  // Sample Entity Data
@@ -473,6 +601,55 @@ export const SAMPLE_ENTITIES = {
473
601
  manager: "Bob Director",
474
602
  },
475
603
  ],
604
+ invoice: [
605
+ {
606
+ id: "INV-2025-001234",
607
+ invoice_id: "INV-2025-001234",
608
+ customer_id: "CUST-AR-001",
609
+ amount_due: 12500.0,
610
+ currency: "USD",
611
+ due_date: "2025-01-20",
612
+ days_past_due: 34,
613
+ dunning_level: 3,
614
+ payment_link: "https://pay.example.com/INV-2025-001234",
615
+ line_items_summary: "Subscription Q1 2025",
616
+ },
617
+ {
618
+ id: "INV-2025-005678",
619
+ invoice_id: "INV-2025-005678",
620
+ customer_id: "CUST-AR-002",
621
+ amount_due: 4850.0,
622
+ currency: "USD",
623
+ due_date: "2025-02-01",
624
+ days_past_due: 22,
625
+ dunning_level: 3,
626
+ payment_link: "https://pay.example.com/INV-2025-005678",
627
+ line_items_summary: "Professional services February 2025",
628
+ },
629
+ {
630
+ id: "INV-2025-009999",
631
+ invoice_id: "INV-2025-009999",
632
+ customer_id: "CUST-AR-003",
633
+ amount_due: 899.0,
634
+ currency: "USD",
635
+ due_date: "2025-02-15",
636
+ days_past_due: 8,
637
+ dunning_level: 2,
638
+ payment_link: "https://pay.example.com/INV-2025-009999",
639
+ line_items_summary: "Monthly license March 2025",
640
+ },
641
+ ],
642
+ dunning_policy: [
643
+ {
644
+ id: "dunning-policy",
645
+ name: "Dunning Policy",
646
+ reminder_1_days: "1-7",
647
+ reminder_2_days: "8-14",
648
+ final_notice_days: "15-30",
649
+ escalation_days: "31+",
650
+ payment_plan_note: "Payment plan requests are reviewed by AR within 1 business day. Do not send reminders to customers on active payment plans.",
651
+ },
652
+ ],
476
653
  benefit: [
477
654
  {
478
655
  id: "BEN-001",
@@ -68,6 +68,19 @@ export const INPUT_SOURCE_RULES = [
68
68
  severity: "warning",
69
69
  fix: "Use user_query for query, pass chat_conversation via named_inputs if needed",
70
70
  },
71
+ // Rule validation inputs
72
+ {
73
+ actionPattern: "rule_validation_with_documents",
74
+ recommended: "entity_extraction.extraction_columns for map_of_extracted_columns, workflowInput.document for primary_docs",
75
+ avoid: ["omitting primary_docs", "wiring phases in parallel instead of sequentially"],
76
+ reason: "rule_validation_with_documents requires both extracted columns and original documents. " +
77
+ "primary_docs gives the validator context from the original document. " +
78
+ "When chaining multiple validation phases, wire them sequentially (phase1.ruleset_output → phase2.map_of_extracted_columns) " +
79
+ "so each phase builds on prior validation results.",
80
+ severity: "critical",
81
+ fix: "Wire entity_extraction.extraction_columns → rule_validation.map_of_extracted_columns AND " +
82
+ "workflowInput.document → rule_validation.primary_docs. For multi-phase: chain sequentially, not in parallel.",
83
+ },
71
84
  // Email-specific rules - GUIDANCE ONLY (not hard backend constraints)
72
85
  // NOTE: Backend accepts TEXT_WITH_SOURCES for email_to, so this is soft guidance
73
86
  // The correct pattern is: entity_extraction → fixed_response with {{email}} template → send_email
@@ -191,8 +191,8 @@ export async function handleData(args, client, readFile) {
191
191
  }
192
192
  if (filePath) {
193
193
  // File upload - use provided readFile or fall back to fs
194
+ let fileContent;
194
195
  try {
195
- let fileContent;
196
196
  if (readFile) {
197
197
  fileContent = await readFile(filePath);
198
198
  }
@@ -200,25 +200,84 @@ export async function handleData(args, client, readFile) {
200
200
  const fs = await import("fs/promises");
201
201
  fileContent = await fs.readFile(filePath);
202
202
  }
203
- const path = await import("path");
204
- const filename = path.basename(filePath);
205
- const result = await client.uploadDataSource(personaId, fileContent, filename, {
206
- widgetName: widgetName, // Pass through widget_name for doc gen personas
203
+ }
204
+ catch (error) {
205
+ return { error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}` };
206
+ }
207
+ const path = await import("path");
208
+ const filename = path.basename(filePath);
209
+ const effectiveWidgetName = widgetName ?? "fileUpload";
210
+ let result;
211
+ try {
212
+ result = await client.uploadDataSource(personaId, fileContent, filename, {
213
+ widgetName: effectiveWidgetName,
207
214
  });
208
- return {
209
- method: "upload",
210
- persona_id: personaId,
211
- path: filePath,
212
- widget_name: widgetName ?? "fileUpload",
213
- ...result,
214
- _warning: "IMPORTANT: Uploaded documents will NOT be used unless your workflow has a search node (search/v2).",
215
- _next_step: `Verify workflow has search: workflow(mode='get', persona_id='${personaId}') → check for search node. If missing, add one.`,
216
- _validation: "Deploy will BLOCK if you have documents but no search node in your workflow.",
217
- };
218
215
  }
219
216
  catch (error) {
220
217
  return { error: `Failed to upload file: ${error instanceof Error ? error.message : String(error)}` };
221
218
  }
219
+ // Auto-create the widget in proto_config if it doesn't exist yet.
220
+ // The upload API stores the file under the given widget_name/tags, but the
221
+ // widget entry in proto_config is what makes it searchable from a workflow.
222
+ // Flow from HAR: POST /api/v2/upload/files → update_persona adds widget entry.
223
+ const existingWidgets = (protoConfig?.widgets ?? []);
224
+ const widgetAlreadyExists = existingWidgets.some(w => w.name === effectiveWidgetName);
225
+ let widgetCreated = false;
226
+ if (!widgetAlreadyExists) {
227
+ const newWidget = {
228
+ name: effectiveWidgetName,
229
+ type: 3,
230
+ title: effectiveWidgetName,
231
+ editable: true,
232
+ fileUpload: {
233
+ localFiles: [],
234
+ tags: [effectiveWidgetName],
235
+ useChunking: true,
236
+ mergeFiles: [],
237
+ transforms: [],
238
+ fileTagMappings: [],
239
+ acceptedMimeTypes: [],
240
+ },
241
+ subtitle: "",
242
+ required: false,
243
+ subProjectType: 0,
244
+ };
245
+ const updatedProtoConfig = {
246
+ ...(protoConfig ?? {}),
247
+ widgets: [...existingWidgets, newWidget],
248
+ };
249
+ try {
250
+ await client.updateAiEmployee({
251
+ persona_id: personaId,
252
+ proto_config: updatedProtoConfig,
253
+ workflow: persona?.workflow_def,
254
+ });
255
+ widgetCreated = true;
256
+ }
257
+ catch (widgetError) {
258
+ return {
259
+ method: "upload",
260
+ persona_id: personaId,
261
+ path: filePath,
262
+ uploaded: true,
263
+ widget_created: false,
264
+ ...result,
265
+ error: `File uploaded successfully but widget registration failed: ${widgetError instanceof Error ? widgetError.message : String(widgetError)}`,
266
+ _tip: "The file is stored. Retry the upload with the same path — it will re-attempt widget creation.",
267
+ };
268
+ }
269
+ }
270
+ return {
271
+ method: "upload",
272
+ persona_id: personaId,
273
+ path: filePath,
274
+ widget_name: effectiveWidgetName,
275
+ widget_created: widgetCreated,
276
+ ...result,
277
+ _warning: "IMPORTANT: Uploaded documents will NOT be used unless your workflow has a search node (search/v2).",
278
+ _next_step: `Verify workflow has search: workflow(mode='get', persona_id='${personaId}') → check for search node wired to widget '${effectiveWidgetName}'. If missing, add one.`,
279
+ _validation: "Deploy will BLOCK if you have documents but no search node in your workflow.",
280
+ };
222
281
  }
223
282
  else if (items && items.length > 0) {
224
283
  // Dashboard row upload (LLM-generated content or file attachments)
@@ -187,24 +187,23 @@ export async function handleReference(args, context) {
187
187
  // type="patterns" - Workflow patterns
188
188
  // ─────────────────────────────────────────────────────────────────────────
189
189
  if (type === "patterns") {
190
- // Get specific pattern
190
+ // Get specific pattern by name
191
191
  if (args.pattern) {
192
- const patternName = args.pattern;
193
- const pattern = WORKFLOW_PATTERNS[patternName];
192
+ const pattern = WORKFLOW_PATTERNS.find(p => p.name === args.pattern);
194
193
  if (!pattern) {
195
- return { error: `Pattern not found: ${args.pattern}` };
194
+ return { error: `Pattern not found: ${args.pattern}`, available: WORKFLOW_PATTERNS.map(p => p.name) };
196
195
  }
197
196
  return pattern;
198
197
  }
199
- // List patterns
200
- let patterns = Object.entries(WORKFLOW_PATTERNS);
198
+ // List patterns (optionally filtered by persona_type)
199
+ let patterns = [...WORKFLOW_PATTERNS];
201
200
  if (args.persona_type) {
202
- patterns = patterns.filter(([_, p]) => !p.personaType || p.personaType === args.persona_type);
201
+ patterns = patterns.filter(p => !p.personaType || p.personaType === args.persona_type);
203
202
  }
204
203
  return {
205
204
  count: patterns.length,
206
- patterns: patterns.map(([name, p]) => ({
207
- name,
205
+ patterns: patterns.map(p => ({
206
+ name: p.name,
208
207
  description: p.description,
209
208
  use_case: p.useCase,
210
209
  persona_type: p.personaType,
@@ -321,8 +320,16 @@ export async function handleReference(args, context) {
321
320
  tags: ["support", "voice", "chat"],
322
321
  description: "Tier 1 customer support automation",
323
322
  },
323
+ {
324
+ id: "finance-dunning",
325
+ name: "Finance - Automated Dunning",
326
+ domain: "finance",
327
+ personas: 2,
328
+ tags: ["finance", "ar", "dunning", "collections", "receivables"],
329
+ description: "AR dunning assistant: balance inquiries, payment links, disputes, payment plans, escalation.",
330
+ },
324
331
  ],
325
- count: 3,
332
+ count: 4,
326
333
  _tip: "Use reference(demo_kit=\"finance-ap\") to get full kit details",
327
334
  };
328
335
  }
@@ -370,7 +377,23 @@ export async function handleReference(args, context) {
370
377
  _tip: "Use persona(method=\"analyze\", id=\"...\") to analyze each persona's workflow",
371
378
  };
372
379
  }
373
- return { error: `Demo kit not found: ${kitId}`, available: ["finance-ap", "sales-sdr", "support-tier1"] };
380
+ if (kitId === "finance-dunning") {
381
+ return {
382
+ id: "finance-dunning",
383
+ name: "Finance - Automated Dunning",
384
+ version: "1.0.0",
385
+ domain: "finance",
386
+ tags: ["finance", "ar", "dunning", "collections", "receivables", "billing"],
387
+ description: "AR dunning assistant: balance inquiries, payment links, disputes, payment plans, escalation. Chat or voice.",
388
+ intents: ["balance_inquiry", "payment_made", "dispute", "payment_plan", "how_to_pay", "escalate", "Fallback"],
389
+ persona_types: ["chat", "voice"],
390
+ demo_script: "docs/demos/finance-dunning.md",
391
+ design_doc: ".context/core/designs/2026-02-23-finance-automated-dunning.md",
392
+ scenario_id: "finance-dunning",
393
+ _tip: "Use demo(mode=\"kit\", persona_id=\"<id>\", scenario=\"finance-dunning\") to generate KB docs and demo script for a dunning persona",
394
+ };
395
+ }
396
+ return { error: `Demo kit not found: ${kitId}`, available: ["finance-ap", "finance-dunning", "sales-sdr", "support-tier1"] };
374
397
  }
375
398
  // ─────────────────────────────────────────────────────────────────────────
376
399
  // tags=true - Get tagging taxonomy
@@ -183,6 +183,11 @@ export const WORKFLOW_PATTERNS = [
183
183
  "respond_for_external_actions.response → WORKFLOW_OUTPUT",
184
184
  ],
185
185
  useCase: "FAQ bot, documentation assistant, policy lookup",
186
+ antiPatterns: [
187
+ "Using call_llm instead of respond_for_external_actions (loses citation and conversation awareness)",
188
+ "Connecting chat_conversation directly to search.query (type mismatch — use conversation_to_search_query)",
189
+ "Forgetting to upload data sources to the persona (search returns empty results)",
190
+ ],
186
191
  },
187
192
  {
188
193
  name: "intent-routing",
@@ -223,6 +228,11 @@ export const WORKFLOW_PATTERNS = [
223
228
  "respond_for_external_actions.response → WORKFLOW_OUTPUT",
224
229
  ],
225
230
  useCase: "Research assistant needing both internal docs and current web info",
231
+ antiPatterns: [
232
+ "Using web search as the primary/only data source (slower, less reliable, uncontrolled content)",
233
+ "Not wiring combine_search_results.combined_results to response node (combined results go unused)",
234
+ "Forgetting to upload internal KB documents (search returns empty, only web results used)",
235
+ ],
226
236
  },
227
237
  {
228
238
  name: "tool-calling",
@@ -241,17 +251,20 @@ export const WORKFLOW_PATTERNS = [
241
251
  antiPatterns: [
242
252
  "Creating duplicate records on follow-up questions",
243
253
  "Not checking conversation history before actions",
254
+ "Forgetting voice-specific widgets (conversationSettings, voiceSettings, callSettings, vadSettings)",
255
+ "external_action_caller does NOT support HITL — cannot gate tool calls with human approval",
244
256
  ],
245
257
  },
246
258
  {
247
259
  name: "hitl-approval",
248
260
  personaType: "chat",
249
261
  description: "Human-in-the-loop approval — enable HITL flag on send_email_agent or entity_extraction_with_documents (only nodes that support HITL)",
250
- nodes: ["chat_trigger", "send_email_agent (with HITL flag)", "respond_for_external_actions"],
262
+ nodes: ["chat_trigger", "entity_extraction_with_documents", "send_email_agent", "respond_for_external_actions"],
251
263
  connections: [
252
- "chat_trigger.user_query → external_action_caller.query",
253
- "chat_trigger.chat_conversation → external_action_caller.conversation",
254
- "external_action_caller.tool_execution_resultrespond_for_external_actions.external_action_result",
264
+ "chat_trigger.user_query → entity_extraction_with_documents.query",
265
+ "chat_trigger.chat_conversation → entity_extraction_with_documents.conversation",
266
+ "entity_extraction_with_documents.extraction_columnssend_email_agent (HITL flag enabled: disable_human_interaction: false)",
267
+ "send_email_agent.confirmation → respond_for_external_actions.external_action_result",
255
268
  "chat_trigger.user_query → respond_for_external_actions.query",
256
269
  "chat_trigger.chat_conversation → respond_for_external_actions.conversation",
257
270
  "respond_for_external_actions.response → WORKFLOW_OUTPUT",
@@ -260,6 +273,8 @@ export const WORKFLOW_PATTERNS = [
260
273
  antiPatterns: [
261
274
  "Adding general_hitl as a standalone node (it is NOT deployable)",
262
275
  "Not wiring conversation context to response node",
276
+ "Using external_action_caller for HITL — it does NOT support the HITL flag",
277
+ "Only send_email_agent and entity_extraction_with_documents support HITL (disable_human_interaction: false)",
263
278
  ],
264
279
  },
265
280
  {
@@ -272,31 +287,38 @@ export const WORKFLOW_PATTERNS = [
272
287
  "entity_extraction_with_documents.extraction_columns → rule_validation_with_documents.map_of_extracted_columns",
273
288
  "workflowInput.document-mmf2 → rule_validation_with_documents.primary_docs",
274
289
  "rule_validation_with_documents.ruleset_output → call_llm.named_inputs_Validation_Results",
275
- "entity_extraction_node.extraction_columns → results (dot-notation key)",
276
- "rule_validation_node.ruleset_output → results (dot-notation key)",
277
- "call_llm.llm_output → results (dot-notation key)",
290
+ "entity_extraction_with_documents.extraction_columns → results (dot-notation: '<nodeId>.extraction_columns')",
291
+ "rule_validation_with_documents.ruleset_output → results (dot-notation: '<nodeId>.ruleset_output')",
292
+ "call_llm.llm_output → results (dot-notation: '<nodeId>.llm_output')",
278
293
  ],
279
294
  useCase: "Invoice processing, contract analysis, compliance checking",
295
+ antiPatterns: [
296
+ "Not mapping extraction/validation outputs to results (dashboard columns won't appear)",
297
+ "Missing primary_docs on rule_validation_with_documents (validator needs original documents for context)",
298
+ "Using a single call_llm without passing validation results (analysis lacks validation context)",
299
+ ],
280
300
  },
281
301
  {
282
302
  name: "dashboard-email-notification",
283
303
  personaType: "dashboard",
284
- description: "Document upload → extraction → email notification (with intermediary for type conversion)",
285
- nodes: ["document_trigger", "entity_extraction_with_documents", "json_mapper", "fixed_response", "send_email_agent", "call_llm"],
304
+ description: "Document upload → extraction → email notification (with intermediary for type conversion). Production workflows often add a body generator (call_llm/custom_agent) and dual send paths (auto + HITL) with CC config.",
305
+ nodes: ["document_trigger", "entity_extraction_with_documents", "json_mapper", "fixed_response", "call_llm (body generator)", "send_email_agent"],
286
306
  connections: [
287
307
  "workflowInput.document-mmf2 → entity_extraction_with_documents.documents",
288
308
  "entity_extraction_with_documents.extraction_columns → json_mapper.input_json",
289
309
  "json_mapper.output_json → fixed_response.named_inputs_Extracted_Data (template: '{{to}}', '{{subject}}', etc.)",
290
310
  "fixed_response.response → send_email_agent.email_to (one fixed_response per email field)",
291
- "send_email_agent.confirmation → call_llm.named_inputs_Email_Result",
292
- "entity_extraction_node.extraction_columnsresults (dot-notation key)",
293
- "call_llm.llm_output → results (dot-notation key)",
311
+ "entity_extraction_with_documents.extraction_columns → call_llm.named_inputs_Extracted_Data (for body generation)",
312
+ "call_llm.llm_outputsend_email_agent.email_body (LLM-generated body)",
313
+ "send_email_agent.confirmation → results (dot-notation: '<nodeId>.confirmation')",
314
+ "entity_extraction_with_documents.extraction_columns → results (dot-notation: '<nodeId>.extraction_columns')",
294
315
  ],
295
- useCase: "Invoice receipt notification, contract alerts, document-triggered emails",
316
+ useCase: "Invoice receipt notification, contract alerts, document-triggered emails, payment confirmations",
296
317
  antiPatterns: [
297
318
  "DO NOT wire entity_extraction directly to send_email — type mismatch (ANY vs TEXT_WITH_SOURCES)",
298
319
  "Use json_mapper + fixed_response as intermediary for type conversion",
299
320
  "Enable HITL flag on send_email_agent (disable_human_interaction: false) if approval needed",
321
+ "For CC/BCC: extract additional recipients via entity_extraction columns, route through separate fixed_response nodes",
300
322
  ],
301
323
  },
302
324
  {
@@ -314,6 +336,11 @@ export const WORKFLOW_PATTERNS = [
314
336
  "response_validator.abstain_reason → [conditional: if invalid] → abstain_action → WORKFLOW_OUTPUT",
315
337
  ],
316
338
  useCase: "Regulated industries, compliance-sensitive responses",
339
+ antiPatterns: [
340
+ "Not connecting both response and abstain paths to WORKFLOW_OUTPUT",
341
+ "Using guardrails without search results (validator has nothing to check against)",
342
+ "Skipping the abstain_action fallback (invalid responses return nothing to user)",
343
+ ],
317
344
  },
318
345
  {
319
346
  name: "externalized-instructions",
@@ -357,6 +384,140 @@ export const WORKFLOW_PATTERNS = [
357
384
  "Not gating fallback separately (fixed_response should handle fallback, not the LLM)",
358
385
  ],
359
386
  },
387
+ // ─── Composite Dashboard Patterns (from FX2 production analysis) ──────────
388
+ {
389
+ name: "multi-phase-validation",
390
+ personaType: "dashboard",
391
+ description: "Document upload → extraction → N sequential rule_validation_with_documents phases. Each phase checks a different concern (format → compliance → cross-reference) and feeds its output to the next. All phase outputs mapped to dashboard columns for per-phase visibility.",
392
+ nodes: ["document_trigger", "entity_extraction_with_documents", "rule_validation_phase_1", "rule_validation_phase_2", "rule_validation_phase_3", "call_llm"],
393
+ connections: [
394
+ "workflowInput.document-mmf2 → entity_extraction_with_documents.documents",
395
+ "entity_extraction_with_documents.extraction_columns → rule_validation_phase_1.map_of_extracted_columns",
396
+ "workflowInput.document-mmf2 → rule_validation_phase_1.primary_docs",
397
+ "rule_validation_phase_1.ruleset_output → rule_validation_phase_2.map_of_extracted_columns",
398
+ "workflowInput.document-mmf2 → rule_validation_phase_2.primary_docs",
399
+ "rule_validation_phase_2.ruleset_output → rule_validation_phase_3.map_of_extracted_columns",
400
+ "workflowInput.document-mmf2 → rule_validation_phase_3.primary_docs",
401
+ "rule_validation_phase_3.ruleset_output → call_llm.named_inputs_Final_Validation",
402
+ "entity_extraction_with_documents.extraction_columns → results (dot-notation: '<nodeId>.extraction_columns')",
403
+ "rule_validation_phase_1.ruleset_output → results (dot-notation: '<nodeId>.ruleset_output')",
404
+ "rule_validation_phase_2.ruleset_output → results (dot-notation: '<nodeId>.ruleset_output')",
405
+ "rule_validation_phase_3.ruleset_output → results (dot-notation: '<nodeId>.ruleset_output')",
406
+ "call_llm.llm_output → results (dot-notation: '<nodeId>.llm_output')",
407
+ ],
408
+ useCase: "Invoice processing with multi-step validation (format → compliance → PO matching), regulatory document review, dunning letter compliance checks",
409
+ antiPatterns: [
410
+ "Running all validation rules in a single phase (loses granularity, hard to debug which phase failed)",
411
+ "Not passing primary_docs to each validation phase (validator needs original documents for context)",
412
+ "Forgetting to map intermediate phase outputs to results (dashboard won't show per-phase status)",
413
+ ],
414
+ },
415
+ {
416
+ name: "confidence-dual-path",
417
+ personaType: "dashboard",
418
+ description: "Dashboard: after extraction and validation, fork into AUTO path (send_email_agent without HITL) and ESCALATE path (send_email_agent with HITL enabled). A confidence/risk score from validation determines which path fires via runIf conditions.",
419
+ nodes: ["document_trigger", "entity_extraction_with_documents", "rule_validation_with_documents", "call_llm (confidence scorer)", "send_email_auto (no HITL)", "send_email_escalate (HITL enabled)"],
420
+ connections: [
421
+ "workflowInput.document-mmf2 → entity_extraction_with_documents.documents",
422
+ "entity_extraction_with_documents.extraction_columns → rule_validation_with_documents.map_of_extracted_columns",
423
+ "workflowInput.document-mmf2 → rule_validation_with_documents.primary_docs",
424
+ "rule_validation_with_documents.ruleset_output → call_llm.named_inputs_Validation_Results",
425
+ "call_llm.llm_output → send_email_auto (runIf: validation_status == PASS)",
426
+ "call_llm.llm_output → send_email_escalate (runIf: validation_status != PASS)",
427
+ "send_email_auto.confirmation → results (dot-notation: '<nodeId>.confirmation')",
428
+ "send_email_escalate.confirmation → results (dot-notation: '<nodeId>.confirmation')",
429
+ "entity_extraction_with_documents.extraction_columns → results (dot-notation: '<nodeId>.extraction_columns')",
430
+ "rule_validation_with_documents.ruleset_output → results (dot-notation: '<nodeId>.ruleset_output')",
431
+ ],
432
+ useCase: "AP invoice processing (clean invoices auto-process, exceptions need human review), dunning workflows (high-confidence auto-send, ambiguous escalate), contract approvals",
433
+ antiPatterns: [
434
+ "Using a single send_email_agent for both paths (loses ability to gate high-risk sends separately)",
435
+ "Not having the ESCALATE path (all documents auto-process with no human oversight for edge cases)",
436
+ "Hardcoding threshold in node config — use validation rules output to drive the routing decision",
437
+ "Forgetting to use intermediary (json_mapper + fixed_response) between extraction and send_email inputs",
438
+ ],
439
+ },
440
+ {
441
+ name: "document-intake-resolution",
442
+ personaType: "dashboard",
443
+ description: "Dashboard: full document intake pipeline — extract entities → convert/normalize → search knowledge base for matching records → LLM resolves/reconciles against master data. The resolution chain ensures extracted entities are validated against existing records before downstream processing.",
444
+ nodes: ["document_trigger", "entity_extraction_with_documents", "json_mapper", "search", "call_llm (resolver)"],
445
+ connections: [
446
+ "workflowInput.document-mmf2 → entity_extraction_with_documents.documents",
447
+ "entity_extraction_with_documents.extraction_columns → json_mapper.input_json",
448
+ "json_mapper.output_json → search.query (lookup extracted entity in KB)",
449
+ "search.search_results → call_llm.named_inputs_Matching_Records",
450
+ "entity_extraction_with_documents.extraction_columns → call_llm.named_inputs_Extracted_Data",
451
+ "call_llm.llm_output → results (dot-notation: '<nodeId>.llm_output' — resolution status + matched record)",
452
+ "entity_extraction_with_documents.extraction_columns → results (dot-notation: '<nodeId>.extraction_columns')",
453
+ ],
454
+ useCase: "Invoice vendor matching against vendor master, contract party resolution, employee onboarding verification against HR records, PO line-item matching",
455
+ antiPatterns: [
456
+ "Skipping the search/resolution step (extracted data goes unvalidated against master data)",
457
+ "Using entity_extraction output directly as the resolved entity (extraction ≠ resolution)",
458
+ "Not handling 'no match found' case in the resolver LLM (must surface unresolved items)",
459
+ ],
460
+ },
461
+ {
462
+ name: "hitl-decision-form",
463
+ personaType: "dashboard",
464
+ description: "Dashboard: use entity_extraction_with_documents with HITL flag as a human review/decision form — presenting processed data for human verification or correction before downstream actions. The extraction columns define the form fields the reviewer sees and can modify.",
465
+ nodes: ["document_trigger", "entity_extraction_with_documents (processing)", "call_llm (prepare review)", "entity_extraction_with_documents (HITL review form)", "send_email_agent"],
466
+ connections: [
467
+ "workflowInput.document-mmf2 → entity_extraction_processing.documents",
468
+ "entity_extraction_processing.extraction_columns → call_llm.named_inputs_Extracted_Data",
469
+ "call_llm.llm_output → entity_extraction_review.named_inputs_Summary (HITL enabled)",
470
+ "entity_extraction_review.extraction_columns → send_email_agent (human-verified data)",
471
+ "entity_extraction_review.extraction_columns → results (dot-notation: '<nodeId>.extraction_columns')",
472
+ ],
473
+ useCase: "Invoice approval where reviewer corrects extracted amounts before payment, contract review where legal team verifies extracted terms, compliance review with sign-off",
474
+ antiPatterns: [
475
+ "Using general_hitl (NOT deployable) — use HITL flag on entity_extraction_with_documents",
476
+ "Confusing extraction-for-processing with extraction-as-review-form (different roles, different column configs)",
477
+ "Not passing processed data to the review form's named_inputs (reviewer sees empty form)",
478
+ ],
479
+ },
480
+ {
481
+ name: "document-generation-pipeline",
482
+ personaType: "dashboard",
483
+ description: "Dashboard: generate formatted documents from processed data — extraction → LLM drafts content → generate_document creates formatted output (PDF) → send_email_agent delivers as attachment.",
484
+ nodes: ["document_trigger", "entity_extraction_with_documents", "call_llm (content drafter)", "generate_document", "send_email_agent"],
485
+ connections: [
486
+ "workflowInput.document-mmf2 → entity_extraction_with_documents.documents",
487
+ "entity_extraction_with_documents.extraction_columns → call_llm.named_inputs_Extracted_Data",
488
+ "call_llm.llm_output → generate_document.markdown_file_contents",
489
+ "generate_document.document_link → send_email_agent.named_inputs_Attachment",
490
+ "send_email_agent.confirmation → results (dot-notation: '<nodeId>.confirmation')",
491
+ "entity_extraction_with_documents.extraction_columns → results (dot-notation: '<nodeId>.extraction_columns')",
492
+ ],
493
+ useCase: "Dunning letter generation, invoice creation from PO data, compliance report generation, customer correspondence, welcome packets",
494
+ antiPatterns: [
495
+ "Putting the full template in call_llm instructions (use data source templates for strict regulatory formats)",
496
+ "Skipping generate_document and sending raw LLM text as attachment (no formatting, no PDF)",
497
+ "Not including extracted entity data in the LLM's named_inputs (generated document lacks specifics)",
498
+ ],
499
+ },
500
+ // ─── Voice Patterns ──────────────────────────────────────────────────────
501
+ {
502
+ name: "voice-kb-search",
503
+ personaType: "voice",
504
+ description: "Voice AI with knowledge base search only — no external actions or side effects. Clean 4-node pattern for informational help desks. Requires voice-specific widgets.",
505
+ nodes: ["chat_trigger", "conversation_to_search_query", "search", "respond_for_external_actions"],
506
+ connections: [
507
+ "chat_trigger.chat_conversation → conversation_to_search_query.conversation",
508
+ "conversation_to_search_query.summarized_conversation → search.query",
509
+ "search.search_results → respond_for_external_actions.external_action_result",
510
+ "chat_trigger.user_query → respond_for_external_actions.query",
511
+ "chat_trigger.chat_conversation → respond_for_external_actions.conversation",
512
+ "respond_for_external_actions.response → WORKFLOW_OUTPUT",
513
+ ],
514
+ useCase: "FX rate inquiries, policy Q&A hotline, product information line, internal help desk for common questions",
515
+ antiPatterns: [
516
+ "Adding external_action_caller when no side effects are needed (over-engineering)",
517
+ "Using call_llm instead of respond_for_external_actions (loses citation and conversation awareness)",
518
+ "Forgetting voice-specific widgets (conversationSettings, voiceSettings, callSettings, vadSettings)",
519
+ ],
520
+ },
360
521
  ];
361
522
  // ─────────────────────────────────────────────────────────────────────────────
362
523
  // Qualifying Questions
@@ -1523,8 +1523,13 @@ persona(method="update", id="<ID>", config={widgets: [...]})
1523
1523
 
1524
1524
  ### 6. Upload Knowledge
1525
1525
  \`\`\`
1526
+ # Default widget ('fileUpload') — exists on every chat/voice persona
1526
1527
  persona(id="<ID>", data={method:"upload", path:"your-data.txt"})
1528
+
1529
+ # Custom widget — auto-created in proto_config on first upload (widget_created: true in response)
1530
+ persona(id="<ID>", data={method:"upload", path:"policies.pdf", widget_name:"policies"})
1527
1531
  \`\`\`
1532
+ Wire any custom widget to a search node: \`search/v2\` → \`datastore_configs\` → \`widgetConfig: { widgetName: "<widget_name>" }\`.
1528
1533
 
1529
1534
  ## Hard Requirements
1530
1535
 
package/dist/mcp/tools.js CHANGED
@@ -278,7 +278,7 @@ persona(
278
278
  // Especially important for Document Generation personas with multiple upload widgets
279
279
  widget_name: {
280
280
  type: "string",
281
- description: "Target widget for upload OR filter for stats. For Document Proposal Manager: 'upload' (Content Repository), 'upload1' (Service Line Docs), 'upload2' (Style Guide). Default: 'fileUpload'. See catalog(type='widgets') for reference."
281
+ description: "Target widget for upload OR filter for stats. Default: 'fileUpload'. If the named widget doesn't exist in the persona's proto_config, it is auto-created (type 3, fileUpload). Use a custom name to create a second knowledge base alongside the default one. For Document Proposal Manager: 'upload' (Content Repository), 'upload1' (Service Line Docs), 'upload2' (Style Guide). See catalog(type='widgets') for reference."
282
282
  },
283
283
  // delete params
284
284
  file_id: { type: "string", description: "File/item ID to delete (for method=delete)" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "2026.2.23-2",
3
+ "version": "2026.2.24",
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",