@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.
- package/.context/public/guides/ema-user-guide.md +12 -16
- package/.context/public/guides/mcp-tools-guide.md +203 -334
- package/LICENSE +29 -21
- package/README.md +58 -35
- package/dist/mcp/domain/loop-detection.js +97 -0
- package/dist/mcp/domain/proto-constraints.js +284 -0
- package/dist/mcp/domain/structural-rules.js +12 -5
- package/dist/mcp/domain/validation-rules.js +107 -20
- 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 +374 -0
- package/dist/mcp/domain/workflow-optimizer.js +10 -4
- package/dist/mcp/guidance.js +54 -31
- package/dist/mcp/handlers/feedback/index.js +139 -0
- package/dist/mcp/handlers/feedback/store.js +262 -0
- package/dist/mcp/handlers/persona/index.js +237 -8
- package/dist/mcp/handlers/persona/schema.js +27 -0
- package/dist/mcp/handlers/reference/index.js +6 -4
- package/dist/mcp/handlers/workflow/index.js +25 -28
- package/dist/mcp/handlers/workflow/optimize.js +73 -33
- package/dist/mcp/handlers/workflow/validation.js +1 -1
- package/dist/mcp/knowledge-types.js +7 -0
- package/dist/mcp/knowledge.js +146 -834
- package/dist/mcp/resources.js +610 -18
- package/dist/mcp/server.js +233 -2156
- package/dist/mcp/tools.js +91 -5
- package/dist/sdk/generated/agent-catalog.js +615 -0
- 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/dist/sdk/generated/widget-catalog.js +60 -0
- package/docs/README.md +17 -9
- package/package.json +2 -2
- package/.context/public/guides/dashboard-operations.md +0 -286
- package/.context/public/guides/email-patterns.md +0 -125
- package/dist/mcp/domain/intent-architect.js +0 -914
- package/dist/mcp/domain/quality-gates.js +0 -110
- package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
- package/dist/mcp/domain/workflow-intent.js +0 -1806
- package/dist/mcp/domain/workflow-merge.js +0 -449
- package/dist/mcp/domain/workflow-tracer.js +0 -648
- package/dist/mcp/domain/workflow-transformer.js +0 -742
- package/dist/mcp/handlers/persona/intent.js +0 -141
- package/dist/mcp/handlers/workflow/analyze.js +0 -119
- package/dist/mcp/handlers/workflow/compare.js +0 -70
- package/dist/mcp/handlers/workflow/generate.js +0 -384
- 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
|
-
*
|
|
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
|
@@ -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
|
|
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
|
|
381
|
+
// After getting persona — suggest workflow get for understanding
|
|
356
382
|
{
|
|
357
|
-
condition: "operation === 'get'
|
|
358
|
-
message: "
|
|
359
|
-
suggestedAction: "
|
|
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
|
-
|
|
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**:
|
|
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"
|
|
591
|
+
if (operation === "get") {
|
|
560
592
|
return {
|
|
561
|
-
message: "
|
|
562
|
-
suggestedAction: "
|
|
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
|
+
}
|