@ema.co/mcp-toolkit 2026.2.5 → 2026.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

Files changed (67) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/LICENSE +29 -21
  4. package/README.md +58 -35
  5. package/dist/mcp/domain/loop-detection.js +97 -0
  6. package/dist/mcp/domain/proto-constraints.js +284 -0
  7. package/dist/mcp/domain/structural-rules.js +12 -5
  8. package/dist/mcp/domain/validation-rules.js +107 -20
  9. package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
  10. package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
  11. package/dist/mcp/domain/workflow-graph.js +374 -0
  12. package/dist/mcp/domain/workflow-optimizer.js +10 -4
  13. package/dist/mcp/guidance.js +54 -31
  14. package/dist/mcp/handlers/feedback/index.js +139 -0
  15. package/dist/mcp/handlers/feedback/store.js +262 -0
  16. package/dist/mcp/handlers/persona/index.js +237 -8
  17. package/dist/mcp/handlers/persona/schema.js +27 -0
  18. package/dist/mcp/handlers/reference/index.js +6 -4
  19. package/dist/mcp/handlers/workflow/index.js +25 -28
  20. package/dist/mcp/handlers/workflow/optimize.js +73 -33
  21. package/dist/mcp/handlers/workflow/validation.js +1 -1
  22. package/dist/mcp/knowledge-types.js +7 -0
  23. package/dist/mcp/knowledge.js +146 -834
  24. package/dist/mcp/resources.js +610 -18
  25. package/dist/mcp/server.js +233 -2156
  26. package/dist/mcp/tools.js +91 -5
  27. package/dist/sdk/generated/agent-catalog.js +615 -0
  28. package/dist/sdk/generated/deprecated-actions.js +182 -96
  29. package/dist/sdk/generated/proto-fields.js +2 -1
  30. package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
  31. package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
  32. package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
  33. package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
  34. package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
  35. package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
  36. package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
  37. package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
  38. package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
  39. package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
  40. package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
  41. package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
  42. package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
  43. package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
  44. package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
  45. package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
  46. package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
  47. package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
  48. package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
  49. package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
  50. package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
  51. package/dist/sdk/generated/widget-catalog.js +60 -0
  52. package/docs/README.md +17 -9
  53. package/package.json +2 -2
  54. package/.context/public/guides/dashboard-operations.md +0 -286
  55. package/.context/public/guides/email-patterns.md +0 -125
  56. package/dist/mcp/domain/intent-architect.js +0 -914
  57. package/dist/mcp/domain/quality-gates.js +0 -110
  58. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  59. package/dist/mcp/domain/workflow-intent.js +0 -1806
  60. package/dist/mcp/domain/workflow-merge.js +0 -449
  61. package/dist/mcp/domain/workflow-tracer.js +0 -648
  62. package/dist/mcp/domain/workflow-transformer.js +0 -742
  63. package/dist/mcp/handlers/persona/intent.js +0 -141
  64. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  65. package/dist/mcp/handlers/workflow/compare.js +0 -70
  66. package/dist/mcp/handlers/workflow/generate.js +0 -384
  67. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Shared Workflow Graph Representation
3
+ *
4
+ * Single source of truth for parsing workflow_def JSON into a typed graph.
5
+ * Used by the workflow optimizer for static analysis.
6
+ *
7
+ * NOTE: `loop-detection.ts` has its own `buildGraphMaps()` for validation
8
+ * (cycle/re-entry detection). Future work: consolidate both into this module.
9
+ */
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Constants
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ const TRIGGER_TYPES = new Set([
14
+ "chat_trigger",
15
+ "voice_trigger",
16
+ "document_trigger",
17
+ ]);
18
+ // WARNING: These lists are hardcoded and may become stale as new action types
19
+ // are added to the platform. Future work: load side-effect classification from
20
+ // ActionRegistry metadata instead of maintaining a static list here.
21
+ // Unknown action types default to hasSideEffects=true (safe default).
22
+ const SIDE_EFFECT_FREE = new Set([
23
+ "search",
24
+ "call_llm",
25
+ "entity_extraction",
26
+ "respond_with_sources",
27
+ "respond_for_external_actions",
28
+ "conversation_to_search_query",
29
+ "combine_search_results",
30
+ "text_categorizer",
31
+ "chat_categorizer",
32
+ "response_validator",
33
+ "fixed_response",
34
+ "live_web_search",
35
+ "ai_web_search",
36
+ "json_mapper",
37
+ "custom_agent",
38
+ ]);
39
+ const SIDE_EFFECT_ACTIONS = new Set([
40
+ "send_email_agent",
41
+ "external_action_caller",
42
+ "general_hitl",
43
+ ]);
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // Main Entry Point
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ /**
48
+ * Parse a workflow_def JSON into a typed WorkflowGraph.
49
+ *
50
+ * Handles the Ema platform action format:
51
+ * ```
52
+ * { actions: [{ name, action: { name: { namespaces, name }, version }, inputs, runIf, categories, displaySettings }] }
53
+ * ```
54
+ */
55
+ export function buildWorkflowGraph(workflowDef) {
56
+ const emptyGraph = {
57
+ nodes: new Map(),
58
+ edges: [],
59
+ forward: new Map(),
60
+ reverse: new Map(),
61
+ trigger: null,
62
+ outputMappings: [],
63
+ enumTypes: [],
64
+ };
65
+ if (!workflowDef || typeof workflowDef !== "object") {
66
+ return emptyGraph;
67
+ }
68
+ const actions = workflowDef.actions ?? [];
69
+ if (!Array.isArray(actions) || actions.length === 0) {
70
+ return emptyGraph;
71
+ }
72
+ const nodes = new Map();
73
+ const edges = [];
74
+ const forward = new Map();
75
+ const reverse = new Map();
76
+ let trigger = null;
77
+ // ── Pass 1: Build nodes ──────────────────────────────────────────────────
78
+ for (const action of actions) {
79
+ if (!action || typeof action !== "object")
80
+ continue;
81
+ const nodeId = action.name;
82
+ if (!nodeId)
83
+ continue;
84
+ const actionType = extractActionType(action);
85
+ const actionVersion = extractActionVersion(action);
86
+ const displayName = extractDisplayName(action);
87
+ const isTrigger = TRIGGER_TYPES.has(actionType);
88
+ const hasSideEffects = classifySideEffects(actionType);
89
+ // Categories can be string[] or { name: string, description?: string }[]
90
+ let categories;
91
+ const rawCategories = action.categories;
92
+ if (Array.isArray(rawCategories)) {
93
+ categories = rawCategories
94
+ .map((c) => typeof c === "string" ? c : c?.name)
95
+ .filter(Boolean);
96
+ if (categories.length === 0)
97
+ categories = undefined;
98
+ }
99
+ if (isTrigger && trigger === null) {
100
+ trigger = nodeId;
101
+ }
102
+ // Parse inputs
103
+ const inputsObj = action.inputs ?? {};
104
+ const inputRefs = new Map();
105
+ for (const [inputName, inputValue] of Object.entries(inputsObj)) {
106
+ if (!inputValue || typeof inputValue !== "object")
107
+ continue;
108
+ const binding = inputValue;
109
+ if (binding.actionOutput) {
110
+ const ao = binding.actionOutput;
111
+ inputRefs.set(inputName, {
112
+ type: "action_output",
113
+ sourceNode: ao.actionName,
114
+ sourceOutput: ao.output,
115
+ });
116
+ addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
117
+ }
118
+ else if (binding.multiBinding) {
119
+ inputRefs.set(inputName, { type: "multi_binding" });
120
+ // Parse elements for nested actionOutput refs
121
+ const mb = binding.multiBinding;
122
+ const elements = mb.elements ?? [];
123
+ for (const elem of elements) {
124
+ if (elem.namedBinding) {
125
+ const nb = elem.namedBinding;
126
+ const value = nb.value;
127
+ if (value?.actionOutput) {
128
+ const ao = value.actionOutput;
129
+ addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
130
+ }
131
+ }
132
+ else if (elem.actionOutput) {
133
+ const ao = elem.actionOutput;
134
+ addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
135
+ }
136
+ }
137
+ }
138
+ else if (binding.widgetConfig) {
139
+ inputRefs.set(inputName, { type: "widget" });
140
+ }
141
+ else if (binding.inline) {
142
+ inputRefs.set(inputName, { type: "inline" });
143
+ }
144
+ }
145
+ // Parse runIf
146
+ const runIf = parseRunIf(action.runIf);
147
+ if (runIf) {
148
+ // runIf creates a dependency edge from the source node
149
+ addEdge(edges, forward, reverse, runIf.sourceNode, nodeId, runIf.sourceOutput, "__runIf__");
150
+ }
151
+ forward.set(nodeId, forward.get(nodeId) ?? new Set());
152
+ reverse.set(nodeId, reverse.get(nodeId) ?? new Set());
153
+ nodes.set(nodeId, {
154
+ id: nodeId,
155
+ actionType,
156
+ actionVersion,
157
+ displayName,
158
+ depth: 0,
159
+ inputs: inputRefs,
160
+ rawAction: action,
161
+ runIf,
162
+ categories: Array.isArray(categories) ? categories : undefined,
163
+ isTrigger,
164
+ hasSideEffects,
165
+ });
166
+ }
167
+ // ── Pass 2: BFS depth from trigger ───────────────────────────────────────
168
+ if (trigger) {
169
+ const visited = new Set();
170
+ const queue = [{ node: trigger, depth: 0 }];
171
+ while (queue.length > 0) {
172
+ const { node, depth } = queue.shift();
173
+ if (visited.has(node))
174
+ continue;
175
+ visited.add(node);
176
+ const graphNode = nodes.get(node);
177
+ if (graphNode) {
178
+ graphNode.depth = depth;
179
+ }
180
+ for (const child of forward.get(node) ?? []) {
181
+ if (!visited.has(child)) {
182
+ queue.push({ node: child, depth: depth + 1 });
183
+ }
184
+ }
185
+ }
186
+ }
187
+ // ── Pass 3: Parse output mappings ────────────────────────────────────────
188
+ const outputMappings = [];
189
+ const results = workflowDef.results ?? {};
190
+ for (const resultDef of Object.values(results)) {
191
+ if (resultDef && typeof resultDef === "object") {
192
+ outputMappings.push({
193
+ nodeId: resultDef.actionName,
194
+ output: resultDef.outputName,
195
+ });
196
+ }
197
+ }
198
+ // ── Pass 4: Parse enumTypes ──────────────────────────────────────────────
199
+ const enumTypes = [];
200
+ const rawEnumTypes = workflowDef.enumTypes ?? [];
201
+ for (const et of rawEnumTypes) {
202
+ const name = extractEnumTypeName(et);
203
+ const options = (et.options ?? et.values ?? []);
204
+ if (name) {
205
+ enumTypes.push({
206
+ name,
207
+ values: options.filter(o => typeof o.name === "string").map(o => o.name),
208
+ });
209
+ }
210
+ }
211
+ return {
212
+ nodes,
213
+ edges,
214
+ forward,
215
+ reverse,
216
+ trigger,
217
+ outputMappings,
218
+ enumTypes,
219
+ };
220
+ }
221
+ // ─────────────────────────────────────────────────────────────────────────────
222
+ // Utility Functions
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ /**
225
+ * BFS from trigger, returning the set of reachable node IDs.
226
+ */
227
+ export function getReachableNodes(graph) {
228
+ if (!graph.trigger)
229
+ return new Set();
230
+ const visited = new Set();
231
+ const queue = [graph.trigger];
232
+ while (queue.length > 0) {
233
+ const node = queue.shift();
234
+ if (visited.has(node))
235
+ continue;
236
+ visited.add(node);
237
+ for (const child of graph.forward.get(node) ?? []) {
238
+ if (!visited.has(child)) {
239
+ queue.push(child);
240
+ }
241
+ }
242
+ }
243
+ return visited;
244
+ }
245
+ /**
246
+ * Topological ordering via Kahn's algorithm.
247
+ * Returns node IDs in dependency order (parents before children).
248
+ */
249
+ export function getTopologicalOrder(graph) {
250
+ if (graph.nodes.size === 0)
251
+ return [];
252
+ // Calculate in-degrees
253
+ const inDegree = new Map();
254
+ for (const nodeId of graph.nodes.keys()) {
255
+ inDegree.set(nodeId, 0);
256
+ }
257
+ for (const edge of graph.edges) {
258
+ inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1);
259
+ }
260
+ // Seed queue with zero-in-degree nodes
261
+ const queue = [];
262
+ for (const [nodeId, degree] of inDegree) {
263
+ if (degree === 0) {
264
+ queue.push(nodeId);
265
+ }
266
+ }
267
+ const result = [];
268
+ while (queue.length > 0) {
269
+ const node = queue.shift();
270
+ result.push(node);
271
+ for (const child of graph.forward.get(node) ?? []) {
272
+ const newDegree = (inDegree.get(child) ?? 1) - 1;
273
+ inDegree.set(child, newDegree);
274
+ if (newDegree === 0) {
275
+ queue.push(child);
276
+ }
277
+ }
278
+ }
279
+ return result;
280
+ }
281
+ /**
282
+ * Filter graph nodes by one or more action types.
283
+ */
284
+ export function getNodesByType(graph, ...types) {
285
+ const typeSet = new Set(types);
286
+ const result = [];
287
+ for (const node of graph.nodes.values()) {
288
+ if (typeSet.has(node.actionType)) {
289
+ result.push(node);
290
+ }
291
+ }
292
+ return result;
293
+ }
294
+ /**
295
+ * Get IDs of nodes that consume outputs from the given node.
296
+ */
297
+ export function getOutputConsumers(graph, nodeId) {
298
+ const children = graph.forward.get(nodeId);
299
+ if (!children)
300
+ return [];
301
+ return [...children];
302
+ }
303
+ // ─────────────────────────────────────────────────────────────────────────────
304
+ // Internal Helpers
305
+ // ─────────────────────────────────────────────────────────────────────────────
306
+ function extractActionType(action) {
307
+ const actionDef = action.action;
308
+ const actionName = actionDef?.name;
309
+ if (typeof actionName === "string")
310
+ return actionName;
311
+ if (actionName && typeof actionName === "object") {
312
+ return actionName.name ?? "unknown";
313
+ }
314
+ return "unknown";
315
+ }
316
+ function extractActionVersion(action) {
317
+ const actionDef = action.action;
318
+ return actionDef?.version ?? "";
319
+ }
320
+ function extractDisplayName(action) {
321
+ const ds = action.displaySettings;
322
+ return ds?.displayName ?? action.name ?? "";
323
+ }
324
+ function classifySideEffects(actionType) {
325
+ if (TRIGGER_TYPES.has(actionType))
326
+ return false;
327
+ if (SIDE_EFFECT_FREE.has(actionType))
328
+ return false;
329
+ if (SIDE_EFFECT_ACTIONS.has(actionType))
330
+ return true;
331
+ // Unknown → assume has side effects (safe default)
332
+ return true;
333
+ }
334
+ function parseRunIf(runIf) {
335
+ if (!runIf)
336
+ return undefined;
337
+ const lhs = runIf.lhs;
338
+ if (!lhs?.actionOutput)
339
+ return undefined;
340
+ const actionOutput = lhs.actionOutput;
341
+ const rhs = runIf.rhs;
342
+ const inlineRhs = rhs?.inline;
343
+ return {
344
+ sourceNode: actionOutput.actionName,
345
+ sourceOutput: actionOutput.output ?? "",
346
+ operator: runIf.operator ?? 0,
347
+ value: inlineRhs?.enumValue ?? "",
348
+ };
349
+ }
350
+ function extractEnumTypeName(et) {
351
+ const rawName = et.name;
352
+ if (typeof rawName === "string")
353
+ return rawName;
354
+ if (rawName && typeof rawName === "object") {
355
+ const nameObj = rawName;
356
+ if (typeof nameObj.name === "string")
357
+ return nameObj.name;
358
+ if (nameObj.name && typeof nameObj.name === "object") {
359
+ const nestedName = nameObj.name;
360
+ if (typeof nestedName.name === "string")
361
+ return nestedName.name;
362
+ }
363
+ }
364
+ return undefined;
365
+ }
366
+ function addEdge(edges, forward, reverse, source, target, sourceOutput, targetInput) {
367
+ edges.push({ source, target, sourceOutput, targetInput });
368
+ if (!forward.has(source))
369
+ forward.set(source, new Set());
370
+ forward.get(source).add(target);
371
+ if (!reverse.has(target))
372
+ reverse.set(target, new Set());
373
+ reverse.get(target).add(source);
374
+ }
@@ -1,9 +1,15 @@
1
1
  /**
2
- * Workflow Optimizer - Proactive Intelligence Engine
2
+ * @deprecated Use `workflow-graph-optimizer.ts` instead.
3
3
  *
4
- * Goes beyond issue detection to find optimization opportunities,
5
- * suggest improvements, and calculate workflow health scores.
6
- * This is the "brains" that makes workflows better.
4
+ * This file contains the legacy heuristic-based optimizer (health scores,
5
+ * recommendations, scoring). It is superseded by the deterministic graph
6
+ * optimizer which provides:
7
+ * - Shared typed graph (`workflow-graph.ts`)
8
+ * - Provably safe auto-fix transforms (`workflow-graph-transforms.ts`)
9
+ * - Convergence-based orchestrator (`workflow-graph-optimizer.ts`)
10
+ *
11
+ * Remaining consumer: `test/intelligence-layer.test.ts` (legacy tests).
12
+ * Remove this file once that test is migrated or deleted.
7
13
  */
8
14
  // ─────────────────────────────────────────────────────────────────────────────
9
15
  // Main Entry Point
@@ -231,7 +231,7 @@ persona({ id: "abc", env: "prod" }) // production`,
231
231
  export const TOOL_GUIDANCE = {
232
232
  persona: {
233
233
  toolName: "persona",
234
- quickTip: "Use analyze=true before modifying. Use preview=true before deploying. Build structured operations for modify.",
234
+ quickTip: "Use workflow(mode='get') to understand the workflow before modifying. Use preview=true before deploying.",
235
235
  operations: [
236
236
  {
237
237
  name: "List all",
@@ -243,11 +243,6 @@ export const TOOL_GUIDANCE = {
243
243
  description: "Get details for a specific persona",
244
244
  example: 'persona(id="abc")',
245
245
  },
246
- {
247
- name: "Analyze",
248
- description: "Get workflow_spec, issues, and fix suggestions",
249
- example: 'persona(id="abc", analyze=true)',
250
- },
251
246
  {
252
247
  name: "Update config",
253
248
  description: "Update persona config (name, description, widgets)",
@@ -340,6 +335,37 @@ export const TOOL_GUIDANCE = {
340
335
  ],
341
336
  applicableRules: ["preview-before-deploy"],
342
337
  },
338
+ toolkit_feedback: {
339
+ toolName: "toolkit_feedback",
340
+ quickTip: "Submit feedback to help improve the MCP toolkit. Report gaps, confusion, successes, or suggestions.",
341
+ operations: [
342
+ {
343
+ name: "Submit feedback",
344
+ description: "Report an issue or positive experience",
345
+ example: 'toolkit_feedback(method="submit", category="gap", message="Missing docs on X")',
346
+ },
347
+ {
348
+ name: "List feedback",
349
+ description: "View recent feedback entries",
350
+ example: 'toolkit_feedback(method="list")',
351
+ },
352
+ {
353
+ name: "Analyze feedback",
354
+ description: "Aggregate feedback into actionable insights",
355
+ example: 'toolkit_feedback(method="analyze")',
356
+ },
357
+ ],
358
+ nextSteps: {
359
+ submit: "Continue with your task. Your feedback will be reviewed to improve the toolkit.",
360
+ list: "Review entries and use analyze for aggregated insights.",
361
+ analyze: "Address actionable_items by updating guidance, resources, or patterns documentation.",
362
+ },
363
+ commonMistakes: [
364
+ "Forgetting to include which tool/operation the feedback is about",
365
+ "Not describing what you were trying to accomplish (use context parameter)",
366
+ ],
367
+ applicableRules: [],
368
+ },
343
369
  };
344
370
  // ─────────────────────────────────────────────────────────────────────────────
345
371
  // Contextual Tips (for tool responses)
@@ -352,19 +378,13 @@ export const CONTEXTUAL_TIPS = [
352
378
  suggestedAction: "persona(id='<id>')",
353
379
  level: "info",
354
380
  },
355
- // After getting persona without analysis
381
+ // After getting persona suggest workflow get for understanding
356
382
  {
357
- condition: "operation === 'get' && !args.analyze",
358
- message: "Run analyze to understand the workflow before making changes",
359
- suggestedAction: "persona(id='...', analyze=true)",
383
+ condition: "operation === 'get'",
384
+ message: "Use workflow(mode='get') to understand the workflow before making changes",
385
+ suggestedAction: "workflow(mode='get', persona_id='...')",
360
386
  level: "info",
361
387
  },
362
- // After analysis shows issues
363
- {
364
- condition: "operation === 'analyze' && result.issues?.length > 0",
365
- message: `Found ${"{result.issues.length}"} issues. Review and build workflow_spec to fix.`,
366
- level: "warning",
367
- },
368
388
  // Update without preview
369
389
  {
370
390
  condition: "operation === 'update' && !args.preview && result.success",
@@ -385,14 +405,17 @@ export const CONTEXTUAL_TIPS = [
385
405
  * Generate server instructions from rules.
386
406
  * These are injected into the MCP init response and should be concise.
387
407
  */
388
- export function generateServerInstructions() {
408
+ export function generateServerInstructions(buildInfo) {
389
409
  const critical = GUIDANCE_RULES.filter((r) => r.level === "critical");
390
410
  const important = GUIDANCE_RULES.filter((r) => r.level === "important");
391
- return `# Ema MCP Toolkit
411
+ const versionLine = buildInfo
412
+ ? `\n> Build: ${buildInfo.version}${buildInfo.commit ? ` (${buildInfo.commit})` : ""}\n`
413
+ : "";
414
+ return `# Ema MCP Toolkit${versionLine}
392
415
 
393
416
  ## Workflow Pattern
394
417
  1. **Discover**: persona() to list AI Employees
395
- 2. **Understand**: persona(id="...", analyze=true) before any changes
418
+ 2. **Understand**: workflow(mode="get", persona_id="...") before any changes
396
419
  3. **Preview**: Always use preview=true before deploying
397
420
  4. **Deploy**: Only after reviewing preview
398
421
 
@@ -416,6 +439,15 @@ The LLM generates the full workflow_def. MCP provides data and executes.
416
439
  - \`ema://catalog/agents-summary\` - Action catalog
417
440
  - \`ema://rules/anti-patterns\` - Common mistakes
418
441
 
442
+ ## Feedback Welcome
443
+ Help us improve! Use \`toolkit_feedback\` to report issues or successes:
444
+ - **Missing docs**: \`toolkit_feedback(method="submit", category="gap", message="...")\`
445
+ - **Confusing guidance**: \`toolkit_feedback(method="submit", category="confusion", message="...")\`
446
+ - **Something worked well**: \`toolkit_feedback(method="submit", category="success", message="...")\`
447
+ - **Unclear error**: \`toolkit_feedback(method="submit", category="error_unclear", tool="...", message="...")\`
448
+
449
+ Your feedback is automatically collected and analyzed to improve the toolkit for all agents.
450
+
419
451
  ## Need Help?
420
452
  Use the \`toolkit_onboard\` prompt for comprehensive guidance.
421
453
  `;
@@ -556,22 +588,13 @@ export function getContextualTip(context) {
556
588
  level: "info",
557
589
  };
558
590
  }
559
- if (operation === "get" && !args.analyze) {
591
+ if (operation === "get") {
560
592
  return {
561
- message: "Run analyze to understand the workflow before making changes",
562
- suggestedAction: "persona(id='...', analyze=true)",
593
+ message: "Use workflow(mode='get') to understand the workflow before making changes",
594
+ suggestedAction: "workflow(mode='get', persona_id='...')",
563
595
  level: "info",
564
596
  };
565
597
  }
566
- if (operation === "analyze" && Array.isArray(result.issues)) {
567
- const issues = result.issues;
568
- if (issues.length > 0) {
569
- return {
570
- message: `Found ${issues.length} issues. Review and build workflow_spec to fix.`,
571
- level: "warning",
572
- };
573
- }
574
- }
575
598
  if (operation === "update" && !args.preview) {
576
599
  return {
577
600
  message: "Changes deployed. Consider using preview=true next time.",
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Feedback Handler - Agent feedback collection and analysis
3
+ *
4
+ * Enables a self-improving loop where agents that USE the MCP toolkit
5
+ * can report gaps, confusion, successes, and suggestions.
6
+ *
7
+ * Methods:
8
+ * - submit - Agent reports feedback
9
+ * - list - View recent feedback entries
10
+ * - analyze - Aggregate feedback + telemetry into actionable insights
11
+ * - rotate - Manually trigger log rotation
12
+ */
13
+ import { errorResult } from "../types.js";
14
+ import { submitFeedback, listFeedback, listTelemetry, analyzeFeedback, rotateLogs, } from "./store.js";
15
+ const VALID_CATEGORIES = [
16
+ "gap",
17
+ "confusion",
18
+ "success",
19
+ "error_unclear",
20
+ "suggestion",
21
+ ];
22
+ const VALID_SEVERITIES = ["low", "medium", "high"];
23
+ /** Max lengths to prevent oversized entries from consuming disk/memory */
24
+ const MAX_MESSAGE_LENGTH = 2000;
25
+ const MAX_CONTEXT_LENGTH = 1000;
26
+ const MAX_FIELD_LENGTH = 100;
27
+ /** Truncate a string to maxLen, appending "..." if truncated */
28
+ function truncate(value, maxLen) {
29
+ return value.length > maxLen ? value.slice(0, maxLen) + "..." : value;
30
+ }
31
+ /**
32
+ * Handle toolkit_feedback tool requests.
33
+ */
34
+ export async function handleFeedback(args) {
35
+ const method = args.method ? String(args.method) : "submit";
36
+ switch (method) {
37
+ case "submit":
38
+ return handleSubmit(args);
39
+ case "list":
40
+ return handleList(args);
41
+ case "analyze":
42
+ return handleAnalyze();
43
+ case "rotate":
44
+ return handleRotate();
45
+ default:
46
+ return errorResult(`Unknown feedback method: ${method}`, {
47
+ hint: "Available methods: submit, list, analyze, rotate",
48
+ });
49
+ }
50
+ }
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+ // Method handlers
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ async function handleSubmit(args) {
55
+ const category = args.category ? String(args.category) : undefined;
56
+ const message = args.message ? String(args.message) : undefined;
57
+ if (!category) {
58
+ return errorResult("Missing required parameter: category", {
59
+ hint: `Valid categories: ${VALID_CATEGORIES.join(", ")}`,
60
+ });
61
+ }
62
+ if (!VALID_CATEGORIES.includes(category)) {
63
+ return errorResult(`Invalid category: ${category}`, {
64
+ hint: `Valid categories: ${VALID_CATEGORIES.join(", ")}`,
65
+ });
66
+ }
67
+ if (!message) {
68
+ return errorResult("Missing required parameter: message", {
69
+ hint: "Describe what you encountered, what was confusing, or what's missing.",
70
+ });
71
+ }
72
+ const severity = args.severity ? String(args.severity) : undefined;
73
+ if (severity && !VALID_SEVERITIES.includes(severity)) {
74
+ return errorResult(`Invalid severity: ${severity}`, {
75
+ hint: `Valid severities: ${VALID_SEVERITIES.join(", ")}`,
76
+ });
77
+ }
78
+ const entry = await submitFeedback({
79
+ category: category,
80
+ message: truncate(message, MAX_MESSAGE_LENGTH),
81
+ tool: args.tool ? truncate(String(args.tool), MAX_FIELD_LENGTH) : undefined,
82
+ operation: args.operation ? truncate(String(args.operation), MAX_FIELD_LENGTH) : undefined,
83
+ context: args.context ? truncate(String(args.context), MAX_CONTEXT_LENGTH) : undefined,
84
+ severity: severity,
85
+ });
86
+ return {
87
+ success: true,
88
+ feedback_id: entry.id,
89
+ message: "Feedback recorded. Thank you for helping improve the toolkit!",
90
+ _tip: "Your feedback helps us identify documentation gaps and improve guidance for all agents.",
91
+ };
92
+ }
93
+ async function handleList(args) {
94
+ const categoryStr = args.category ? String(args.category) : undefined;
95
+ if (categoryStr && !VALID_CATEGORIES.includes(categoryStr)) {
96
+ return errorResult(`Invalid category filter: ${categoryStr}`, {
97
+ hint: `Valid categories: ${VALID_CATEGORIES.join(", ")}`,
98
+ });
99
+ }
100
+ const category = categoryStr;
101
+ const limit = args.limit ? Number(args.limit) : 50;
102
+ const includeTelemetry = args.include_telemetry === true;
103
+ const feedback = await listFeedback({ limit, category });
104
+ const result = {
105
+ count: feedback.length,
106
+ feedback,
107
+ };
108
+ if (includeTelemetry) {
109
+ const telemetry = await listTelemetry({ limit });
110
+ result.telemetry = {
111
+ count: telemetry.length,
112
+ entries: telemetry,
113
+ };
114
+ }
115
+ if (feedback.length === 0) {
116
+ result._tip = "No feedback recorded yet. Agents can submit feedback using toolkit_feedback(method='submit', category='...', message='...').";
117
+ }
118
+ return result;
119
+ }
120
+ async function handleAnalyze() {
121
+ const analysis = await analyzeFeedback();
122
+ return {
123
+ ...analysis,
124
+ _tip: analysis.actionable_items.length > 0
125
+ ? `Found ${analysis.actionable_items.length} actionable item(s). Review and address these to improve agent experience.`
126
+ : "No actionable items found. Feedback data may be limited - encourage agents to submit feedback.",
127
+ _next_step: analysis.actionable_items.length > 0
128
+ ? "Review actionable_items and update guidance.ts or resources.ts accordingly."
129
+ : "Continue collecting feedback. Use toolkit_feedback(method='list') to review individual entries.",
130
+ };
131
+ }
132
+ async function handleRotate() {
133
+ const result = await rotateLogs();
134
+ return {
135
+ success: true,
136
+ ...result,
137
+ message: "Log files rotated successfully.",
138
+ };
139
+ }