@ema.co/mcp-toolkit 2026.2.5 → 2026.2.13
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.
- package/.context/public/guides/dashboard-operations.md +63 -0
- package/.context/public/guides/workflow-builder-patterns.md +708 -0
- package/LICENSE +29 -21
- package/README.md +58 -35
- package/dist/mcp/domain/proto-constraints.js +284 -0
- package/dist/mcp/domain/structural-rules.js +8 -0
- package/dist/mcp/domain/validation-rules.js +102 -15
- package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
- package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
- package/dist/mcp/domain/workflow-graph.js +376 -0
- package/dist/mcp/domain/workflow-optimizer.js +10 -4
- package/dist/mcp/guidance.js +45 -2
- package/dist/mcp/handlers/feedback/index.js +139 -0
- package/dist/mcp/handlers/feedback/store.js +262 -0
- package/dist/mcp/handlers/workflow/index.js +12 -11
- package/dist/mcp/handlers/workflow/optimize.js +73 -33
- package/dist/mcp/knowledge.js +87 -36
- package/dist/mcp/resources.js +393 -17
- package/dist/mcp/server.js +38 -4
- package/dist/mcp/tools.js +89 -2
- package/dist/sdk/generated/deprecated-actions.js +182 -96
- package/dist/sdk/generated/proto-fields.js +2 -1
- package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
- package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
- package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
- package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
- package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
- package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
- package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
- package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
- package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
- package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
- package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
- package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
- package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
- package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
- package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
- package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
- package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
- package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Workflow Graph Representation
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for parsing workflow_def JSON into a typed graph.
|
|
5
|
+
* Replaces duplicated graph-building across workflow-execution-analyzer,
|
|
6
|
+
* workflow-optimizer (deprecated), and workflow-tracer.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: `workflow-execution-analyzer.ts` still has its own `buildGraphMaps()`
|
|
9
|
+
* which serves validation (runtime checks). This graph serves optimization
|
|
10
|
+
* (static analysis). Future work: consolidate both into this module.
|
|
11
|
+
*/
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Constants
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
const TRIGGER_TYPES = new Set([
|
|
16
|
+
"chat_trigger",
|
|
17
|
+
"voice_trigger",
|
|
18
|
+
"document_trigger",
|
|
19
|
+
]);
|
|
20
|
+
// WARNING: These lists are hardcoded and may become stale as new action types
|
|
21
|
+
// are added to the platform. Future work: load side-effect classification from
|
|
22
|
+
// ActionRegistry metadata instead of maintaining a static list here.
|
|
23
|
+
// Unknown action types default to hasSideEffects=true (safe default).
|
|
24
|
+
const SIDE_EFFECT_FREE = new Set([
|
|
25
|
+
"search",
|
|
26
|
+
"call_llm",
|
|
27
|
+
"entity_extraction",
|
|
28
|
+
"respond_with_sources",
|
|
29
|
+
"respond_for_external_actions",
|
|
30
|
+
"conversation_to_search_query",
|
|
31
|
+
"combine_search_results",
|
|
32
|
+
"text_categorizer",
|
|
33
|
+
"chat_categorizer",
|
|
34
|
+
"response_validator",
|
|
35
|
+
"fixed_response",
|
|
36
|
+
"live_web_search",
|
|
37
|
+
"ai_web_search",
|
|
38
|
+
"json_mapper",
|
|
39
|
+
"custom_agent",
|
|
40
|
+
]);
|
|
41
|
+
const SIDE_EFFECT_ACTIONS = new Set([
|
|
42
|
+
"send_email_agent",
|
|
43
|
+
"external_action_caller",
|
|
44
|
+
"general_hitl",
|
|
45
|
+
]);
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
// Main Entry Point
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Parse a workflow_def JSON into a typed WorkflowGraph.
|
|
51
|
+
*
|
|
52
|
+
* Handles the Ema platform action format:
|
|
53
|
+
* ```
|
|
54
|
+
* { actions: [{ name, action: { name: { namespaces, name }, version }, inputs, runIf, categories, displaySettings }] }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function buildWorkflowGraph(workflowDef) {
|
|
58
|
+
const emptyGraph = {
|
|
59
|
+
nodes: new Map(),
|
|
60
|
+
edges: [],
|
|
61
|
+
forward: new Map(),
|
|
62
|
+
reverse: new Map(),
|
|
63
|
+
trigger: null,
|
|
64
|
+
outputMappings: [],
|
|
65
|
+
enumTypes: [],
|
|
66
|
+
};
|
|
67
|
+
if (!workflowDef || typeof workflowDef !== "object") {
|
|
68
|
+
return emptyGraph;
|
|
69
|
+
}
|
|
70
|
+
const actions = workflowDef.actions ?? [];
|
|
71
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
72
|
+
return emptyGraph;
|
|
73
|
+
}
|
|
74
|
+
const nodes = new Map();
|
|
75
|
+
const edges = [];
|
|
76
|
+
const forward = new Map();
|
|
77
|
+
const reverse = new Map();
|
|
78
|
+
let trigger = null;
|
|
79
|
+
// ── Pass 1: Build nodes ──────────────────────────────────────────────────
|
|
80
|
+
for (const action of actions) {
|
|
81
|
+
if (!action || typeof action !== "object")
|
|
82
|
+
continue;
|
|
83
|
+
const nodeId = action.name;
|
|
84
|
+
if (!nodeId)
|
|
85
|
+
continue;
|
|
86
|
+
const actionType = extractActionType(action);
|
|
87
|
+
const actionVersion = extractActionVersion(action);
|
|
88
|
+
const displayName = extractDisplayName(action);
|
|
89
|
+
const isTrigger = TRIGGER_TYPES.has(actionType);
|
|
90
|
+
const hasSideEffects = classifySideEffects(actionType);
|
|
91
|
+
// Categories can be string[] or { name: string, description?: string }[]
|
|
92
|
+
let categories;
|
|
93
|
+
const rawCategories = action.categories;
|
|
94
|
+
if (Array.isArray(rawCategories)) {
|
|
95
|
+
categories = rawCategories
|
|
96
|
+
.map((c) => typeof c === "string" ? c : c?.name)
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
if (categories.length === 0)
|
|
99
|
+
categories = undefined;
|
|
100
|
+
}
|
|
101
|
+
if (isTrigger && trigger === null) {
|
|
102
|
+
trigger = nodeId;
|
|
103
|
+
}
|
|
104
|
+
// Parse inputs
|
|
105
|
+
const inputsObj = action.inputs ?? {};
|
|
106
|
+
const inputRefs = new Map();
|
|
107
|
+
for (const [inputName, inputValue] of Object.entries(inputsObj)) {
|
|
108
|
+
if (!inputValue || typeof inputValue !== "object")
|
|
109
|
+
continue;
|
|
110
|
+
const binding = inputValue;
|
|
111
|
+
if (binding.actionOutput) {
|
|
112
|
+
const ao = binding.actionOutput;
|
|
113
|
+
inputRefs.set(inputName, {
|
|
114
|
+
type: "action_output",
|
|
115
|
+
sourceNode: ao.actionName,
|
|
116
|
+
sourceOutput: ao.output,
|
|
117
|
+
});
|
|
118
|
+
addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
|
|
119
|
+
}
|
|
120
|
+
else if (binding.multiBinding) {
|
|
121
|
+
inputRefs.set(inputName, { type: "multi_binding" });
|
|
122
|
+
// Parse elements for nested actionOutput refs
|
|
123
|
+
const mb = binding.multiBinding;
|
|
124
|
+
const elements = mb.elements ?? [];
|
|
125
|
+
for (const elem of elements) {
|
|
126
|
+
if (elem.namedBinding) {
|
|
127
|
+
const nb = elem.namedBinding;
|
|
128
|
+
const value = nb.value;
|
|
129
|
+
if (value?.actionOutput) {
|
|
130
|
+
const ao = value.actionOutput;
|
|
131
|
+
addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (elem.actionOutput) {
|
|
135
|
+
const ao = elem.actionOutput;
|
|
136
|
+
addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (binding.widgetConfig) {
|
|
141
|
+
inputRefs.set(inputName, { type: "widget" });
|
|
142
|
+
}
|
|
143
|
+
else if (binding.inline) {
|
|
144
|
+
inputRefs.set(inputName, { type: "inline" });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Parse runIf
|
|
148
|
+
const runIf = parseRunIf(action.runIf);
|
|
149
|
+
if (runIf) {
|
|
150
|
+
// runIf creates a dependency edge from the source node
|
|
151
|
+
addEdge(edges, forward, reverse, runIf.sourceNode, nodeId, runIf.sourceOutput, "__runIf__");
|
|
152
|
+
}
|
|
153
|
+
forward.set(nodeId, forward.get(nodeId) ?? new Set());
|
|
154
|
+
reverse.set(nodeId, reverse.get(nodeId) ?? new Set());
|
|
155
|
+
nodes.set(nodeId, {
|
|
156
|
+
id: nodeId,
|
|
157
|
+
actionType,
|
|
158
|
+
actionVersion,
|
|
159
|
+
displayName,
|
|
160
|
+
depth: 0,
|
|
161
|
+
inputs: inputRefs,
|
|
162
|
+
rawAction: action,
|
|
163
|
+
runIf,
|
|
164
|
+
categories: Array.isArray(categories) ? categories : undefined,
|
|
165
|
+
isTrigger,
|
|
166
|
+
hasSideEffects,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// ── Pass 2: BFS depth from trigger ───────────────────────────────────────
|
|
170
|
+
if (trigger) {
|
|
171
|
+
const visited = new Set();
|
|
172
|
+
const queue = [{ node: trigger, depth: 0 }];
|
|
173
|
+
while (queue.length > 0) {
|
|
174
|
+
const { node, depth } = queue.shift();
|
|
175
|
+
if (visited.has(node))
|
|
176
|
+
continue;
|
|
177
|
+
visited.add(node);
|
|
178
|
+
const graphNode = nodes.get(node);
|
|
179
|
+
if (graphNode) {
|
|
180
|
+
graphNode.depth = depth;
|
|
181
|
+
}
|
|
182
|
+
for (const child of forward.get(node) ?? []) {
|
|
183
|
+
if (!visited.has(child)) {
|
|
184
|
+
queue.push({ node: child, depth: depth + 1 });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ── Pass 3: Parse output mappings ────────────────────────────────────────
|
|
190
|
+
const outputMappings = [];
|
|
191
|
+
const results = workflowDef.results ?? {};
|
|
192
|
+
for (const resultDef of Object.values(results)) {
|
|
193
|
+
if (resultDef && typeof resultDef === "object") {
|
|
194
|
+
outputMappings.push({
|
|
195
|
+
nodeId: resultDef.actionName,
|
|
196
|
+
output: resultDef.outputName,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ── Pass 4: Parse enumTypes ──────────────────────────────────────────────
|
|
201
|
+
const enumTypes = [];
|
|
202
|
+
const rawEnumTypes = workflowDef.enumTypes ?? [];
|
|
203
|
+
for (const et of rawEnumTypes) {
|
|
204
|
+
const name = extractEnumTypeName(et);
|
|
205
|
+
const options = (et.options ?? et.values ?? []);
|
|
206
|
+
if (name) {
|
|
207
|
+
enumTypes.push({
|
|
208
|
+
name,
|
|
209
|
+
values: options.filter(o => typeof o.name === "string").map(o => o.name),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
nodes,
|
|
215
|
+
edges,
|
|
216
|
+
forward,
|
|
217
|
+
reverse,
|
|
218
|
+
trigger,
|
|
219
|
+
outputMappings,
|
|
220
|
+
enumTypes,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
// Utility Functions
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
/**
|
|
227
|
+
* BFS from trigger, returning the set of reachable node IDs.
|
|
228
|
+
*/
|
|
229
|
+
export function getReachableNodes(graph) {
|
|
230
|
+
if (!graph.trigger)
|
|
231
|
+
return new Set();
|
|
232
|
+
const visited = new Set();
|
|
233
|
+
const queue = [graph.trigger];
|
|
234
|
+
while (queue.length > 0) {
|
|
235
|
+
const node = queue.shift();
|
|
236
|
+
if (visited.has(node))
|
|
237
|
+
continue;
|
|
238
|
+
visited.add(node);
|
|
239
|
+
for (const child of graph.forward.get(node) ?? []) {
|
|
240
|
+
if (!visited.has(child)) {
|
|
241
|
+
queue.push(child);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return visited;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Topological ordering via Kahn's algorithm.
|
|
249
|
+
* Returns node IDs in dependency order (parents before children).
|
|
250
|
+
*/
|
|
251
|
+
export function getTopologicalOrder(graph) {
|
|
252
|
+
if (graph.nodes.size === 0)
|
|
253
|
+
return [];
|
|
254
|
+
// Calculate in-degrees
|
|
255
|
+
const inDegree = new Map();
|
|
256
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
257
|
+
inDegree.set(nodeId, 0);
|
|
258
|
+
}
|
|
259
|
+
for (const edge of graph.edges) {
|
|
260
|
+
inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1);
|
|
261
|
+
}
|
|
262
|
+
// Seed queue with zero-in-degree nodes
|
|
263
|
+
const queue = [];
|
|
264
|
+
for (const [nodeId, degree] of inDegree) {
|
|
265
|
+
if (degree === 0) {
|
|
266
|
+
queue.push(nodeId);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const result = [];
|
|
270
|
+
while (queue.length > 0) {
|
|
271
|
+
const node = queue.shift();
|
|
272
|
+
result.push(node);
|
|
273
|
+
for (const child of graph.forward.get(node) ?? []) {
|
|
274
|
+
const newDegree = (inDegree.get(child) ?? 1) - 1;
|
|
275
|
+
inDegree.set(child, newDegree);
|
|
276
|
+
if (newDegree === 0) {
|
|
277
|
+
queue.push(child);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Filter graph nodes by one or more action types.
|
|
285
|
+
*/
|
|
286
|
+
export function getNodesByType(graph, ...types) {
|
|
287
|
+
const typeSet = new Set(types);
|
|
288
|
+
const result = [];
|
|
289
|
+
for (const node of graph.nodes.values()) {
|
|
290
|
+
if (typeSet.has(node.actionType)) {
|
|
291
|
+
result.push(node);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get IDs of nodes that consume outputs from the given node.
|
|
298
|
+
*/
|
|
299
|
+
export function getOutputConsumers(graph, nodeId) {
|
|
300
|
+
const children = graph.forward.get(nodeId);
|
|
301
|
+
if (!children)
|
|
302
|
+
return [];
|
|
303
|
+
return [...children];
|
|
304
|
+
}
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
306
|
+
// Internal Helpers
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
308
|
+
function extractActionType(action) {
|
|
309
|
+
const actionDef = action.action;
|
|
310
|
+
const actionName = actionDef?.name;
|
|
311
|
+
if (typeof actionName === "string")
|
|
312
|
+
return actionName;
|
|
313
|
+
if (actionName && typeof actionName === "object") {
|
|
314
|
+
return actionName.name ?? "unknown";
|
|
315
|
+
}
|
|
316
|
+
return "unknown";
|
|
317
|
+
}
|
|
318
|
+
function extractActionVersion(action) {
|
|
319
|
+
const actionDef = action.action;
|
|
320
|
+
return actionDef?.version ?? "";
|
|
321
|
+
}
|
|
322
|
+
function extractDisplayName(action) {
|
|
323
|
+
const ds = action.displaySettings;
|
|
324
|
+
return ds?.displayName ?? action.name ?? "";
|
|
325
|
+
}
|
|
326
|
+
function classifySideEffects(actionType) {
|
|
327
|
+
if (TRIGGER_TYPES.has(actionType))
|
|
328
|
+
return false;
|
|
329
|
+
if (SIDE_EFFECT_FREE.has(actionType))
|
|
330
|
+
return false;
|
|
331
|
+
if (SIDE_EFFECT_ACTIONS.has(actionType))
|
|
332
|
+
return true;
|
|
333
|
+
// Unknown → assume has side effects (safe default)
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
function parseRunIf(runIf) {
|
|
337
|
+
if (!runIf)
|
|
338
|
+
return undefined;
|
|
339
|
+
const lhs = runIf.lhs;
|
|
340
|
+
if (!lhs?.actionOutput)
|
|
341
|
+
return undefined;
|
|
342
|
+
const actionOutput = lhs.actionOutput;
|
|
343
|
+
const rhs = runIf.rhs;
|
|
344
|
+
const inlineRhs = rhs?.inline;
|
|
345
|
+
return {
|
|
346
|
+
sourceNode: actionOutput.actionName,
|
|
347
|
+
sourceOutput: actionOutput.output ?? "",
|
|
348
|
+
operator: runIf.operator ?? 0,
|
|
349
|
+
value: inlineRhs?.enumValue ?? "",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function extractEnumTypeName(et) {
|
|
353
|
+
const rawName = et.name;
|
|
354
|
+
if (typeof rawName === "string")
|
|
355
|
+
return rawName;
|
|
356
|
+
if (rawName && typeof rawName === "object") {
|
|
357
|
+
const nameObj = rawName;
|
|
358
|
+
if (typeof nameObj.name === "string")
|
|
359
|
+
return nameObj.name;
|
|
360
|
+
if (nameObj.name && typeof nameObj.name === "object") {
|
|
361
|
+
const nestedName = nameObj.name;
|
|
362
|
+
if (typeof nestedName.name === "string")
|
|
363
|
+
return nestedName.name;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
function addEdge(edges, forward, reverse, source, target, sourceOutput, targetInput) {
|
|
369
|
+
edges.push({ source, target, sourceOutput, targetInput });
|
|
370
|
+
if (!forward.has(source))
|
|
371
|
+
forward.set(source, new Set());
|
|
372
|
+
forward.get(source).add(target);
|
|
373
|
+
if (!reverse.has(target))
|
|
374
|
+
reverse.set(target, new Set());
|
|
375
|
+
reverse.get(target).add(source);
|
|
376
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @deprecated Use `workflow-graph-optimizer.ts` instead.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
package/dist/mcp/guidance.js
CHANGED
|
@@ -340,6 +340,37 @@ export const TOOL_GUIDANCE = {
|
|
|
340
340
|
],
|
|
341
341
|
applicableRules: ["preview-before-deploy"],
|
|
342
342
|
},
|
|
343
|
+
toolkit_feedback: {
|
|
344
|
+
toolName: "toolkit_feedback",
|
|
345
|
+
quickTip: "Submit feedback to help improve the MCP toolkit. Report gaps, confusion, successes, or suggestions.",
|
|
346
|
+
operations: [
|
|
347
|
+
{
|
|
348
|
+
name: "Submit feedback",
|
|
349
|
+
description: "Report an issue or positive experience",
|
|
350
|
+
example: 'toolkit_feedback(method="submit", category="gap", message="Missing docs on X")',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "List feedback",
|
|
354
|
+
description: "View recent feedback entries",
|
|
355
|
+
example: 'toolkit_feedback(method="list")',
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "Analyze feedback",
|
|
359
|
+
description: "Aggregate feedback into actionable insights",
|
|
360
|
+
example: 'toolkit_feedback(method="analyze")',
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
nextSteps: {
|
|
364
|
+
submit: "Continue with your task. Your feedback will be reviewed to improve the toolkit.",
|
|
365
|
+
list: "Review entries and use analyze for aggregated insights.",
|
|
366
|
+
analyze: "Address actionable_items by updating guidance, resources, or patterns documentation.",
|
|
367
|
+
},
|
|
368
|
+
commonMistakes: [
|
|
369
|
+
"Forgetting to include which tool/operation the feedback is about",
|
|
370
|
+
"Not describing what you were trying to accomplish (use context parameter)",
|
|
371
|
+
],
|
|
372
|
+
applicableRules: [],
|
|
373
|
+
},
|
|
343
374
|
};
|
|
344
375
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
345
376
|
// Contextual Tips (for tool responses)
|
|
@@ -385,10 +416,13 @@ export const CONTEXTUAL_TIPS = [
|
|
|
385
416
|
* Generate server instructions from rules.
|
|
386
417
|
* These are injected into the MCP init response and should be concise.
|
|
387
418
|
*/
|
|
388
|
-
export function generateServerInstructions() {
|
|
419
|
+
export function generateServerInstructions(buildInfo) {
|
|
389
420
|
const critical = GUIDANCE_RULES.filter((r) => r.level === "critical");
|
|
390
421
|
const important = GUIDANCE_RULES.filter((r) => r.level === "important");
|
|
391
|
-
|
|
422
|
+
const versionLine = buildInfo
|
|
423
|
+
? `\n> Build: ${buildInfo.version}${buildInfo.commit ? ` (${buildInfo.commit})` : ""}\n`
|
|
424
|
+
: "";
|
|
425
|
+
return `# Ema MCP Toolkit${versionLine}
|
|
392
426
|
|
|
393
427
|
## Workflow Pattern
|
|
394
428
|
1. **Discover**: persona() to list AI Employees
|
|
@@ -416,6 +450,15 @@ The LLM generates the full workflow_def. MCP provides data and executes.
|
|
|
416
450
|
- \`ema://catalog/agents-summary\` - Action catalog
|
|
417
451
|
- \`ema://rules/anti-patterns\` - Common mistakes
|
|
418
452
|
|
|
453
|
+
## Feedback Welcome
|
|
454
|
+
Help us improve! Use \`toolkit_feedback\` to report issues or successes:
|
|
455
|
+
- **Missing docs**: \`toolkit_feedback(method="submit", category="gap", message="...")\`
|
|
456
|
+
- **Confusing guidance**: \`toolkit_feedback(method="submit", category="confusion", message="...")\`
|
|
457
|
+
- **Something worked well**: \`toolkit_feedback(method="submit", category="success", message="...")\`
|
|
458
|
+
- **Unclear error**: \`toolkit_feedback(method="submit", category="error_unclear", tool="...", message="...")\`
|
|
459
|
+
|
|
460
|
+
Your feedback is automatically collected and analyzed to improve the toolkit for all agents.
|
|
461
|
+
|
|
419
462
|
## Need Help?
|
|
420
463
|
Use the \`toolkit_onboard\` prompt for comprehensive guidance.
|
|
421
464
|
`;
|
|
@@ -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, resources.ts, or workflow-builder-patterns.md 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
|
+
}
|