@ema.co/mcp-toolkit 1.5.1 → 1.6.0
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/dist/mcp/handlers-consolidated.js +400 -14
- package/dist/mcp/prompts.js +80 -123
- package/dist/mcp/server.js +134 -209
- package/dist/mcp/tools-consolidated.js +212 -150
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/client.js +58 -90
- package/dist/sdk/demo-generator.js +978 -0
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +15 -1
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +187 -39
- package/dist/sdk/workflow-intent.js +246 -24
- package/dist/sdk/workflow-optimizer.js +665 -0
- package/dist/sdk/workflow-tracer.js +648 -0
- package/dist/sdk/workflow-transformer.js +10 -0
- package/dist/sdk/workflow-validator.js +391 -0
- package/docs/.temp/datasource-attach.har +198369 -0
- package/docs/.temp/grpcweb.gar +1 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +163 -197
- package/docs/openapi.json +8000 -0
- package/docs/release-process.md +153 -0
- package/docs/test-persona-creation.md +196 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +3 -1
- package/resources/templates/demo-scenarios/README.md +63 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-Driven Workflow Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates workflow specs against actual API definitions:
|
|
5
|
+
* - ListActions: Available actions with input/output schemas
|
|
6
|
+
* - GetPersonaTemplates: Available templates with configurations
|
|
7
|
+
* - OpenAPI spec: Endpoint contracts (optional, for payload validation)
|
|
8
|
+
*
|
|
9
|
+
* This ensures workflows are valid BEFORE deployment, catching errors early.
|
|
10
|
+
*/
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// Schema Registry - Caches API definitions
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
export class APISchemaRegistry {
|
|
15
|
+
actions = new Map();
|
|
16
|
+
templates = new Map();
|
|
17
|
+
loaded = false;
|
|
18
|
+
/**
|
|
19
|
+
* Load schemas from API
|
|
20
|
+
*/
|
|
21
|
+
async load(client) {
|
|
22
|
+
const [actionDTOs, templateDTOs] = await Promise.all([
|
|
23
|
+
client.listActions().catch(() => []),
|
|
24
|
+
client.getPersonaTemplates().catch(() => []),
|
|
25
|
+
]);
|
|
26
|
+
// Parse actions
|
|
27
|
+
for (const dto of actionDTOs) {
|
|
28
|
+
const schema = this.parseActionDTO(dto);
|
|
29
|
+
if (schema) {
|
|
30
|
+
this.actions.set(schema.name, schema);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Parse templates
|
|
34
|
+
for (const dto of templateDTOs) {
|
|
35
|
+
const schema = this.parseTemplateDTO(dto);
|
|
36
|
+
if (schema) {
|
|
37
|
+
this.templates.set(schema.id, schema);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.loaded = true;
|
|
41
|
+
}
|
|
42
|
+
isLoaded() {
|
|
43
|
+
return this.loaded;
|
|
44
|
+
}
|
|
45
|
+
getAction(name) {
|
|
46
|
+
return this.actions.get(name);
|
|
47
|
+
}
|
|
48
|
+
getTemplate(id) {
|
|
49
|
+
return this.templates.get(id);
|
|
50
|
+
}
|
|
51
|
+
getAllActions() {
|
|
52
|
+
return Array.from(this.actions.values());
|
|
53
|
+
}
|
|
54
|
+
getAllTemplates() {
|
|
55
|
+
return Array.from(this.templates.values());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse ActionDTO from ListActions API into ActionSchema
|
|
59
|
+
*/
|
|
60
|
+
parseActionDTO(dto) {
|
|
61
|
+
try {
|
|
62
|
+
const typeName = dto.typeName;
|
|
63
|
+
const name = typeName?.name?.name;
|
|
64
|
+
if (!name)
|
|
65
|
+
return null;
|
|
66
|
+
const inputs = new Map();
|
|
67
|
+
const outputs = new Map();
|
|
68
|
+
// Parse inputs from dto.inputs.inputs
|
|
69
|
+
const inputsObj = dto.inputs?.inputs;
|
|
70
|
+
if (inputsObj && typeof inputsObj === "object") {
|
|
71
|
+
for (const [inputName, inputDef] of Object.entries(inputsObj)) {
|
|
72
|
+
const def = inputDef;
|
|
73
|
+
inputs.set(inputName, {
|
|
74
|
+
name: inputName,
|
|
75
|
+
type: def.type?.wellKnownType ?? "unknown",
|
|
76
|
+
required: !def.isOptional,
|
|
77
|
+
description: def.description ?? def.displayName,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Parse outputs from dto.outputs.outputs
|
|
82
|
+
const outputsObj = dto.outputs?.outputs;
|
|
83
|
+
if (outputsObj && typeof outputsObj === "object") {
|
|
84
|
+
for (const [outputName, outputDef] of Object.entries(outputsObj)) {
|
|
85
|
+
const def = outputDef;
|
|
86
|
+
outputs.set(outputName, {
|
|
87
|
+
name: outputName,
|
|
88
|
+
type: def.type?.wellKnownType ?? "unknown",
|
|
89
|
+
description: def.description,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Get displayName - might be string or object with nested value
|
|
94
|
+
const displayName = typeof dto.displayName === "string"
|
|
95
|
+
? dto.displayName
|
|
96
|
+
: dto.name ?? name;
|
|
97
|
+
// Get description - might be string or object
|
|
98
|
+
const description = typeof dto.description === "string"
|
|
99
|
+
? dto.description
|
|
100
|
+
: "";
|
|
101
|
+
return {
|
|
102
|
+
name,
|
|
103
|
+
displayName: displayName,
|
|
104
|
+
description,
|
|
105
|
+
category: dto.category ?? "unknown",
|
|
106
|
+
version: typeName?.version ?? "v1",
|
|
107
|
+
inputs,
|
|
108
|
+
outputs,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse PersonaTemplateDTO from GetPersonaTemplates API
|
|
117
|
+
*/
|
|
118
|
+
parseTemplateDTO(dto) {
|
|
119
|
+
try {
|
|
120
|
+
if (!dto.id)
|
|
121
|
+
return null;
|
|
122
|
+
// Determine type from template name or ID
|
|
123
|
+
let type = "chat";
|
|
124
|
+
const nameLower = (dto.name ?? "").toLowerCase();
|
|
125
|
+
if (nameLower.includes("voice"))
|
|
126
|
+
type = "voice";
|
|
127
|
+
else if (nameLower.includes("dashboard"))
|
|
128
|
+
type = "dashboard";
|
|
129
|
+
// Extract widget names from proto_config
|
|
130
|
+
const defaultWidgets = [];
|
|
131
|
+
const protoConfig = dto.proto_config;
|
|
132
|
+
if (protoConfig?.widgets) {
|
|
133
|
+
for (const widget of protoConfig.widgets) {
|
|
134
|
+
if (widget.name)
|
|
135
|
+
defaultWidgets.push(widget.name);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
id: dto.id,
|
|
140
|
+
name: dto.name ?? dto.id,
|
|
141
|
+
type,
|
|
142
|
+
description: dto.description,
|
|
143
|
+
defaultWidgets,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
// Workflow Validator
|
|
153
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
154
|
+
/**
|
|
155
|
+
* Validate a WorkflowSpec against API schemas
|
|
156
|
+
*/
|
|
157
|
+
export function validateWorkflowSpec(spec, registry) {
|
|
158
|
+
const errors = [];
|
|
159
|
+
const warnings = [];
|
|
160
|
+
const usedActions = [];
|
|
161
|
+
const unknownActions = [];
|
|
162
|
+
// Build node map for reference checking
|
|
163
|
+
const nodeMap = new Map();
|
|
164
|
+
for (const node of spec.nodes) {
|
|
165
|
+
nodeMap.set(node.id, node);
|
|
166
|
+
}
|
|
167
|
+
// Check if registry actually has data - if not, skip action-specific validation
|
|
168
|
+
// (API may be unavailable or mocked)
|
|
169
|
+
const registryHasData = registry.isLoaded() && registry.getAllActions().length > 0;
|
|
170
|
+
// 1. Check for trigger node
|
|
171
|
+
const triggerTypes = ["chat_trigger", "document_trigger", "voice_trigger"];
|
|
172
|
+
const hasTrigger = spec.nodes.some(n => triggerTypes.includes(n.actionType));
|
|
173
|
+
if (!hasTrigger) {
|
|
174
|
+
errors.push({
|
|
175
|
+
type: "missing_trigger",
|
|
176
|
+
message: "Workflow must have a trigger node (chat_trigger, document_trigger)",
|
|
177
|
+
suggestion: "Add a trigger node as the first node in your workflow",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// 2. Validate each node
|
|
181
|
+
for (const node of spec.nodes) {
|
|
182
|
+
const actionSchema = registry.getAction(node.actionType);
|
|
183
|
+
// Schema-dependent validation (only when registry has data)
|
|
184
|
+
if (!actionSchema && registryHasData) {
|
|
185
|
+
// Action not found in API (only flag if we have data to check against)
|
|
186
|
+
unknownActions.push(node.actionType);
|
|
187
|
+
errors.push({
|
|
188
|
+
type: "missing_action",
|
|
189
|
+
node_id: node.id,
|
|
190
|
+
message: `Action "${node.actionType}" not found in API`,
|
|
191
|
+
suggestion: `Check ListActions for available actions. Similar: ${findSimilarActions(node.actionType, registry)}`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else if (actionSchema) {
|
|
195
|
+
usedActions.push(node.actionType);
|
|
196
|
+
// Validate required inputs
|
|
197
|
+
for (const [inputName, inputSchema] of actionSchema.inputs) {
|
|
198
|
+
if (inputSchema.required && !node.inputs?.[inputName]) {
|
|
199
|
+
errors.push({
|
|
200
|
+
type: "missing_input",
|
|
201
|
+
node_id: node.id,
|
|
202
|
+
field: inputName,
|
|
203
|
+
message: `Required input "${inputName}" missing on node "${node.id}" (${node.actionType})`,
|
|
204
|
+
suggestion: `Add input "${inputName}" of type ${inputSchema.type}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Schema-aware input validation
|
|
209
|
+
if (node.inputs) {
|
|
210
|
+
for (const [inputName, binding] of Object.entries(node.inputs)) {
|
|
211
|
+
const inputSchema = actionSchema.inputs.get(inputName);
|
|
212
|
+
// Check if input exists on action
|
|
213
|
+
if (!inputSchema && actionSchema.inputs.size > 0) {
|
|
214
|
+
warnings.push({
|
|
215
|
+
type: "suboptimal_wiring",
|
|
216
|
+
node_id: node.id,
|
|
217
|
+
message: `Input "${inputName}" may not exist on action "${node.actionType}"`,
|
|
218
|
+
suggestion: `Valid inputs: ${Array.from(actionSchema.inputs.keys()).join(", ")}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Schema-INDEPENDENT validation (ALWAYS runs - checks node references)
|
|
225
|
+
if (node.inputs) {
|
|
226
|
+
for (const [inputName, binding] of Object.entries(node.inputs)) {
|
|
227
|
+
// Validate action_output references - doesn't need schema
|
|
228
|
+
if (binding.type === "action_output" && binding.actionName) {
|
|
229
|
+
const sourceNode = nodeMap.get(binding.actionName);
|
|
230
|
+
if (!sourceNode) {
|
|
231
|
+
errors.push({
|
|
232
|
+
type: "invalid_wiring",
|
|
233
|
+
node_id: node.id,
|
|
234
|
+
field: inputName,
|
|
235
|
+
message: `Input "${inputName}" references non-existent node "${binding.actionName}"`,
|
|
236
|
+
suggestion: `Valid nodes: ${Array.from(nodeMap.keys()).join(", ")}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else if (actionSchema) {
|
|
240
|
+
// Output validation only if we have schema
|
|
241
|
+
const sourceSchema = registry.getAction(sourceNode.actionType);
|
|
242
|
+
if (sourceSchema && binding.output && !sourceSchema.outputs.has(binding.output)) {
|
|
243
|
+
warnings.push({
|
|
244
|
+
type: "suboptimal_wiring",
|
|
245
|
+
node_id: node.id,
|
|
246
|
+
message: `Output "${binding.output}" may not exist on action "${sourceNode.actionType}"`,
|
|
247
|
+
suggestion: `Valid outputs: ${Array.from(sourceSchema.outputs.keys()).join(", ")}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Validate runIf references - also schema-independent
|
|
255
|
+
if (node.runIf && "actionName" in node.runIf) {
|
|
256
|
+
const runIfNode = nodeMap.get(node.runIf.actionName);
|
|
257
|
+
if (!runIfNode) {
|
|
258
|
+
errors.push({
|
|
259
|
+
type: "invalid_wiring",
|
|
260
|
+
node_id: node.id,
|
|
261
|
+
message: `runIf references non-existent node "${node.runIf.actionName}"`,
|
|
262
|
+
suggestion: `Valid nodes: ${Array.from(nodeMap.keys()).join(", ")}`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// 3. Validate result mappings
|
|
268
|
+
for (const mapping of spec.resultMappings) {
|
|
269
|
+
if (!nodeMap.has(mapping.nodeId)) {
|
|
270
|
+
errors.push({
|
|
271
|
+
type: "invalid_structure",
|
|
272
|
+
message: `Result mapping references non-existent node "${mapping.nodeId}"`,
|
|
273
|
+
suggestion: `Valid nodes: ${Array.from(nodeMap.keys()).join(", ")}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
valid: errors.length === 0,
|
|
279
|
+
errors,
|
|
280
|
+
warnings,
|
|
281
|
+
action_coverage: {
|
|
282
|
+
used: [...new Set(usedActions)],
|
|
283
|
+
available: registry.getAllActions().map(a => a.name),
|
|
284
|
+
unknown: unknownActions,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Find similar action names for suggestions
|
|
290
|
+
*/
|
|
291
|
+
function findSimilarActions(name, registry) {
|
|
292
|
+
const allActions = registry.getAllActions();
|
|
293
|
+
const similar = allActions
|
|
294
|
+
.filter(a => {
|
|
295
|
+
const aLower = a.name.toLowerCase();
|
|
296
|
+
const nLower = name.toLowerCase();
|
|
297
|
+
return aLower.includes(nLower) || nLower.includes(aLower) ||
|
|
298
|
+
levenshteinDistance(aLower, nLower) <= 3;
|
|
299
|
+
})
|
|
300
|
+
.slice(0, 3)
|
|
301
|
+
.map(a => a.name);
|
|
302
|
+
return similar.length > 0 ? similar.join(", ") : "none found";
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Simple Levenshtein distance for fuzzy matching
|
|
306
|
+
*/
|
|
307
|
+
function levenshteinDistance(a, b) {
|
|
308
|
+
if (a.length === 0)
|
|
309
|
+
return b.length;
|
|
310
|
+
if (b.length === 0)
|
|
311
|
+
return a.length;
|
|
312
|
+
const matrix = [];
|
|
313
|
+
for (let i = 0; i <= b.length; i++) {
|
|
314
|
+
matrix[i] = [i];
|
|
315
|
+
}
|
|
316
|
+
for (let j = 0; j <= a.length; j++) {
|
|
317
|
+
matrix[0][j] = j;
|
|
318
|
+
}
|
|
319
|
+
for (let i = 1; i <= b.length; i++) {
|
|
320
|
+
for (let j = 1; j <= a.length; j++) {
|
|
321
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
322
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return matrix[b.length][a.length];
|
|
330
|
+
}
|
|
331
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
332
|
+
// LLM Context Generation
|
|
333
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
334
|
+
/**
|
|
335
|
+
* Generate action catalog for LLM context
|
|
336
|
+
*/
|
|
337
|
+
export function generateActionCatalogForLLM(registry) {
|
|
338
|
+
const sections = [];
|
|
339
|
+
sections.push("# Available Actions (from API)\n");
|
|
340
|
+
// Group by category
|
|
341
|
+
const byCategory = new Map();
|
|
342
|
+
for (const action of registry.getAllActions()) {
|
|
343
|
+
const cat = action.category || "other";
|
|
344
|
+
if (!byCategory.has(cat))
|
|
345
|
+
byCategory.set(cat, []);
|
|
346
|
+
byCategory.get(cat).push(action);
|
|
347
|
+
}
|
|
348
|
+
for (const [category, actions] of byCategory) {
|
|
349
|
+
sections.push(`## ${category}\n`);
|
|
350
|
+
for (const action of actions) {
|
|
351
|
+
const inputs = Array.from(action.inputs.values())
|
|
352
|
+
.map(i => `${i.name}${i.required ? "*" : ""}: ${i.type}`)
|
|
353
|
+
.join(", ");
|
|
354
|
+
const outputs = Array.from(action.outputs.keys()).join(", ");
|
|
355
|
+
sections.push(`### ${action.name} (${action.version})`);
|
|
356
|
+
sections.push(`${action.description}`);
|
|
357
|
+
sections.push(`- Inputs: ${inputs || "none"}`);
|
|
358
|
+
sections.push(`- Outputs: ${outputs || "none"}\n`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return sections.join("\n");
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Generate template catalog for LLM context
|
|
365
|
+
*/
|
|
366
|
+
export function generateTemplateCatalogForLLM(registry) {
|
|
367
|
+
const sections = [];
|
|
368
|
+
sections.push("# Available Templates (from API)\n");
|
|
369
|
+
for (const template of registry.getAllTemplates()) {
|
|
370
|
+
sections.push(`## ${template.name} (${template.type})`);
|
|
371
|
+
sections.push(`ID: ${template.id}`);
|
|
372
|
+
if (template.description)
|
|
373
|
+
sections.push(template.description);
|
|
374
|
+
sections.push(`Default widgets: ${template.defaultWidgets.join(", ") || "none"}\n`);
|
|
375
|
+
}
|
|
376
|
+
return sections.join("\n");
|
|
377
|
+
}
|
|
378
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
// Singleton Registry
|
|
380
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
381
|
+
let _schemaRegistry = null;
|
|
382
|
+
export async function ensureSchemaRegistry(client) {
|
|
383
|
+
if (!_schemaRegistry) {
|
|
384
|
+
_schemaRegistry = new APISchemaRegistry();
|
|
385
|
+
await _schemaRegistry.load(client);
|
|
386
|
+
}
|
|
387
|
+
return _schemaRegistry;
|
|
388
|
+
}
|
|
389
|
+
export function getSchemaRegistry() {
|
|
390
|
+
return _schemaRegistry;
|
|
391
|
+
}
|