@ema.co/mcp-toolkit 2026.3.24 → 2026.3.25-2

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.
@@ -33,7 +33,10 @@ const VALID_BINDING_CASES = new Set([
33
33
  export function validateWorkflowDefStructure(workflowDef) {
34
34
  const issues = [];
35
35
  validateWorkflowName(workflowDef, issues);
36
+ validateEnumTypes(workflowDef, issues);
36
37
  validateActions(workflowDef, issues);
38
+ validateTypeArguments(workflowDef, issues);
39
+ validateRunIfBindings(workflowDef, issues);
37
40
  validateResults(workflowDef, issues);
38
41
  validateNamedResults(workflowDef, issues);
39
42
  return {
@@ -98,6 +101,205 @@ function validateWorkflowName(wf, issues) {
98
101
  });
99
102
  }
100
103
  }
104
+ // ─────────────────────────────────────────────────────────────────────────────
105
+ // enumTypes validation — empty namespaces here cause opaque 500 from the backend
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+ function validateEnumTypes(wf, issues) {
108
+ const enumTypes = wf.enumTypes;
109
+ if (!Array.isArray(enumTypes) || enumTypes.length === 0)
110
+ return;
111
+ const enumNames = new Set();
112
+ for (let i = 0; i < enumTypes.length; i++) {
113
+ const et = enumTypes[i];
114
+ const prefix = `enumTypes[${i}]`;
115
+ const name = et.name;
116
+ if (!name || typeof name !== "object") {
117
+ issues.push({
118
+ path: `${prefix}.name`,
119
+ severity: "error",
120
+ message: "enumType missing 'name' (NamespacedName — requires { namespaces, name })",
121
+ proto_ref: "workflows.v1.EnumTypeDef.name",
122
+ });
123
+ continue;
124
+ }
125
+ if (!Array.isArray(name.namespaces) || name.namespaces.length === 0) {
126
+ issues.push({
127
+ path: `${prefix}.name.namespaces`,
128
+ severity: "error",
129
+ message: "enumType namespaces must be a non-empty array. Empty [] causes HTTP 500. " +
130
+ "Use the namespaces from the persona's workflowName, or leave namespaces from the template unchanged.",
131
+ proto_ref: "workflows.v1.NamespacedName.namespaces",
132
+ });
133
+ }
134
+ if (typeof name.name !== "string" || name.name.length === 0) {
135
+ issues.push({
136
+ path: `${prefix}.name.name`,
137
+ severity: "error",
138
+ message: "enumType name.name must be a non-empty string (e.g. 'intent_categories')",
139
+ proto_ref: "workflows.v1.NamespacedName.name",
140
+ });
141
+ }
142
+ else {
143
+ if (enumNames.has(name.name)) {
144
+ issues.push({
145
+ path: `${prefix}.name.name`,
146
+ severity: "error",
147
+ message: `Duplicate enumType name '${name.name}'`,
148
+ });
149
+ }
150
+ enumNames.add(name.name);
151
+ }
152
+ // Validate options array
153
+ const options = et.options;
154
+ if (!Array.isArray(options) || options.length === 0) {
155
+ issues.push({
156
+ path: `${prefix}.options`,
157
+ severity: "warning",
158
+ message: "enumType has no options — categorizer will have nothing to route to",
159
+ proto_ref: "workflows.v1.EnumTypeDef.options",
160
+ });
161
+ }
162
+ else {
163
+ const hasFallback = options.some((o) => typeof o.name === "string" && o.name.toLowerCase() === "fallback");
164
+ if (!hasFallback) {
165
+ issues.push({
166
+ path: `${prefix}.options`,
167
+ severity: "warning",
168
+ message: "enumType has no 'Fallback' option — categorizer should always include a Fallback category",
169
+ });
170
+ }
171
+ }
172
+ }
173
+ }
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+ // typeArguments validation — categorizers need typeArguments.categories pointing
176
+ // to a valid enumType defined in enumTypes[]
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+ function validateTypeArguments(wf, issues) {
179
+ const actions = wf.actions;
180
+ if (!Array.isArray(actions))
181
+ return;
182
+ // Collect defined enum type names
183
+ const enumTypes = wf.enumTypes;
184
+ const definedEnums = new Set();
185
+ if (Array.isArray(enumTypes)) {
186
+ for (const et of enumTypes) {
187
+ const name = et.name;
188
+ if (name && typeof name.name === "string") {
189
+ definedEnums.add(name.name);
190
+ }
191
+ }
192
+ }
193
+ for (let i = 0; i < actions.length; i++) {
194
+ const action = actions[i];
195
+ const prefix = `actions[${i}]`;
196
+ const actionDef = action.action;
197
+ const actionName = actionDef?.name?.name;
198
+ // Check if this is a categorizer (needs typeArguments)
199
+ const isCategorizer = actionName === "chat_categorizer" || actionName === "text_categorizer";
200
+ const typeArgs = action.typeArguments;
201
+ if (isCategorizer && (!typeArgs || !typeArgs.categories)) {
202
+ issues.push({
203
+ path: `${prefix}.typeArguments`,
204
+ severity: "error",
205
+ message: `Categorizer '${action.name}' missing typeArguments.categories. ` +
206
+ "Must point to an enumType: { categories: { enumType: { name: { name: '<enum_name>', namespaces: [...] } } } }",
207
+ proto_ref: "workflows.v1.ActionInstance.type_arguments",
208
+ });
209
+ continue;
210
+ }
211
+ if (!typeArgs?.categories)
212
+ continue;
213
+ // Validate the enumType reference
214
+ const categories = typeArgs.categories;
215
+ const enumType = categories.enumType;
216
+ if (!enumType) {
217
+ issues.push({
218
+ path: `${prefix}.typeArguments.categories.enumType`,
219
+ severity: "error",
220
+ message: "typeArguments.categories missing enumType reference",
221
+ });
222
+ continue;
223
+ }
224
+ const enumName = enumType.name;
225
+ if (!enumName || typeof enumName !== "object") {
226
+ issues.push({
227
+ path: `${prefix}.typeArguments.categories.enumType.name`,
228
+ severity: "error",
229
+ message: "enumType.name must be a NamespacedName object { name, namespaces }",
230
+ });
231
+ continue;
232
+ }
233
+ // Check namespaces match the enumType definition
234
+ if (!Array.isArray(enumName.namespaces) || enumName.namespaces.length === 0) {
235
+ issues.push({
236
+ path: `${prefix}.typeArguments.categories.enumType.name.namespaces`,
237
+ severity: "error",
238
+ message: "typeArguments enumType namespaces must be non-empty. Must match the namespaces in the enumTypes[] definition.",
239
+ proto_ref: "workflows.v1.NamespacedName.namespaces",
240
+ });
241
+ }
242
+ // Check the referenced enum exists
243
+ if (typeof enumName.name === "string" && definedEnums.size > 0 && !definedEnums.has(enumName.name)) {
244
+ issues.push({
245
+ path: `${prefix}.typeArguments.categories.enumType.name.name`,
246
+ severity: "error",
247
+ message: `typeArguments references enumType '${enumName.name}' but it's not defined in enumTypes[]. ` +
248
+ `Available: ${[...definedEnums].join(", ")}`,
249
+ });
250
+ }
251
+ }
252
+ }
253
+ // ─────────────────────────────────────────────────────────────────────────────
254
+ // runIf validation — conditional execution bindings must be well-formed
255
+ // ─────────────────────────────────────────────────────────────────────────────
256
+ function validateRunIfBindings(wf, issues) {
257
+ const actions = wf.actions;
258
+ if (!Array.isArray(actions))
259
+ return;
260
+ for (let i = 0; i < actions.length; i++) {
261
+ const action = actions[i];
262
+ const runIf = action.runIf;
263
+ if (!runIf)
264
+ continue;
265
+ const prefix = `actions[${i}].runIf`;
266
+ // lhs must be a valid binding
267
+ const lhs = runIf.lhs;
268
+ if (!lhs || typeof lhs !== "object") {
269
+ issues.push({
270
+ path: `${prefix}.lhs`,
271
+ severity: "error",
272
+ message: "runIf.lhs must be a valid InputBinding (e.g., actionOutput referencing categorizer output)",
273
+ proto_ref: "workflows.v1.ConditionalInputBinding",
274
+ });
275
+ }
276
+ else {
277
+ validateSingleBinding(lhs, `${prefix}.lhs`, issues);
278
+ }
279
+ // operator must be present
280
+ if (runIf.operator === undefined || runIf.operator === null) {
281
+ issues.push({
282
+ path: `${prefix}.operator`,
283
+ severity: "error",
284
+ message: "runIf.operator is required (1 = EQUAL, 2 = NOT_EQUAL)",
285
+ proto_ref: "workflows.v1.ConditionalInputBinding.operator",
286
+ });
287
+ }
288
+ // rhs must be a valid binding
289
+ const rhs = runIf.rhs;
290
+ if (!rhs || typeof rhs !== "object") {
291
+ issues.push({
292
+ path: `${prefix}.rhs`,
293
+ severity: "error",
294
+ message: "runIf.rhs must be a valid InputBinding (e.g., inline enumValue)",
295
+ proto_ref: "workflows.v1.ConditionalInputBinding",
296
+ });
297
+ }
298
+ else {
299
+ validateSingleBinding(rhs, `${prefix}.rhs`, issues);
300
+ }
301
+ }
302
+ }
101
303
  function validateActions(wf, issues) {
102
304
  const actions = wf.actions;
103
305
  if (!Array.isArray(actions) || actions.length === 0) {
@@ -188,7 +390,43 @@ function validateInputBindings(action, prefix, _knownActions, issues) {
188
390
  for (const [inputName, binding] of Object.entries(inputs)) {
189
391
  if (!binding || typeof binding !== "object")
190
392
  continue;
191
- validateSingleBinding(binding, `${prefix}.inputs.${inputName}`, issues);
393
+ const bindingObj = binding;
394
+ validateSingleBinding(bindingObj, `${prefix}.inputs.${inputName}`, issues);
395
+ // named_inputs_* MUST use multiBinding with namedBinding elements
396
+ if (inputName.startsWith("named_inputs_")) {
397
+ if (!bindingObj.multiBinding) {
398
+ issues.push({
399
+ path: `${prefix}.inputs.${inputName}`,
400
+ severity: "error",
401
+ message: `Input '${inputName}' must use multiBinding format: ` +
402
+ `{ multiBinding: { elements: [{ namedBinding: { name: "...", binding: {...} } }] } }. ` +
403
+ "Search knowledge('named inputs format') for correct structure",
404
+ proto_ref: "workflows.v1.InputBinding.MultiBinding",
405
+ });
406
+ }
407
+ else {
408
+ // Check each element uses namedBinding
409
+ const mb = bindingObj.multiBinding;
410
+ const elements = mb.elements;
411
+ if (Array.isArray(elements)) {
412
+ for (let j = 0; j < elements.length; j++) {
413
+ const el = elements[j];
414
+ if (el && typeof el === "object" && !el.namedBinding) {
415
+ issues.push({
416
+ path: `${prefix}.inputs.${inputName}.multiBinding.elements[${j}]`,
417
+ severity: "error",
418
+ message: "named_inputs elements must use namedBinding: { namedBinding: { name: '...', binding: {...} } }. " +
419
+ "Plain actionOutput or inline bindings inside multiBinding are invalid for named_inputs.",
420
+ });
421
+ }
422
+ }
423
+ }
424
+ }
425
+ }
426
+ // extraction_columns MUST use correct array format
427
+ if (inputName === "extraction_columns") {
428
+ validateExtractionColumnsFormat(bindingObj, `${prefix}.inputs.${inputName}`, issues);
429
+ }
192
430
  }
193
431
  }
194
432
  function validateSingleBinding(binding, path, issues) {
@@ -462,3 +700,85 @@ function validateResultDef(value, path, issues) {
462
700
  }
463
701
  }
464
702
  }
703
+ // ─────────────────────────────────────────────────────────────────────────────
704
+ // extraction_columns format validation
705
+ // Must use: inline.array.values[{ wellKnown: { extractionColumn: { id, name, dataType } } }]
706
+ // ─────────────────────────────────────────────────────────────────────────────
707
+ function validateExtractionColumnsFormat(binding, path, issues) {
708
+ const inline = binding.inline;
709
+ if (!inline)
710
+ return; // actionOutput wiring is valid too
711
+ const array = inline.array;
712
+ if (!array) {
713
+ issues.push({
714
+ path: `${path}.inline`,
715
+ severity: "error",
716
+ message: "extraction_columns must use inline.array format: { inline: { array: { values: [{ wellKnown: { extractionColumn: { id, name, dataType } } }] } } }. " +
717
+ "Search knowledge('extraction columns format') for correct structure",
718
+ });
719
+ return;
720
+ }
721
+ const values = array.values;
722
+ if (!Array.isArray(values) || values.length === 0) {
723
+ issues.push({
724
+ path: `${path}.inline.array.values`,
725
+ severity: "warning",
726
+ message: "extraction_columns array is empty — no columns defined",
727
+ });
728
+ return;
729
+ }
730
+ const seenIds = new Set();
731
+ for (let i = 0; i < values.length; i++) {
732
+ const v = values[i];
733
+ const vPath = `${path}.inline.array.values[${i}]`;
734
+ const wellKnown = v?.wellKnown;
735
+ if (!wellKnown) {
736
+ issues.push({
737
+ path: vPath,
738
+ severity: "error",
739
+ message: "extraction_columns values must be wrapped in wellKnown: { extractionColumn: { id, name, dataType } }",
740
+ });
741
+ continue;
742
+ }
743
+ const col = wellKnown.extractionColumn;
744
+ if (!col) {
745
+ issues.push({
746
+ path: `${vPath}.wellKnown`,
747
+ severity: "error",
748
+ message: "Missing extractionColumn inside wellKnown wrapper",
749
+ });
750
+ continue;
751
+ }
752
+ if (typeof col.id !== "string" || col.id.length === 0) {
753
+ issues.push({
754
+ path: `${vPath}.wellKnown.extractionColumn.id`,
755
+ severity: "error",
756
+ message: "extractionColumn requires 'id' (unique string identifier)",
757
+ });
758
+ }
759
+ else {
760
+ if (seenIds.has(col.id)) {
761
+ issues.push({
762
+ path: `${vPath}.wellKnown.extractionColumn.id`,
763
+ severity: "error",
764
+ message: `Duplicate extraction column id '${col.id}'`,
765
+ });
766
+ }
767
+ seenIds.add(col.id);
768
+ }
769
+ if (typeof col.name !== "string" || col.name.length === 0) {
770
+ issues.push({
771
+ path: `${vPath}.wellKnown.extractionColumn.name`,
772
+ severity: "error",
773
+ message: "extractionColumn requires 'name' (display name)",
774
+ });
775
+ }
776
+ if (col.dataType === undefined || col.dataType === null) {
777
+ issues.push({
778
+ path: `${vPath}.wellKnown.extractionColumn.dataType`,
779
+ severity: "error",
780
+ message: "extractionColumn requires 'dataType' (1=STRING, 2=INT, 3=FLOAT, 4=BOOL, 5=DATE)",
781
+ });
782
+ }
783
+ }
784
+ }
@@ -327,7 +327,7 @@ function formatPathName(path) {
327
327
  // ─────────────────────────────────────────────────────────────────────────────
328
328
  // Global Categorizer Validation (not path-dependent)
329
329
  // ─────────────────────────────────────────────────────────────────────────────
330
- function validateAllCategorizers(workflow) {
330
+ export function validateAllCategorizers(workflow) {
331
331
  const errors = [];
332
332
  // Find all categorizer nodes in workflow
333
333
  const categorizers = workflow.nodes.filter(node => node.actionType === "chat_categorizer" ||
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Result Shape Classifier
3
+ *
4
+ * Inspects an MCP tool response and classifies it into a ResultShape.
5
+ * The shape drives which guidance atoms are resolved.
6
+ */
7
+ /**
8
+ * Classify an MCP tool response into a ResultShape.
9
+ *
10
+ * Handlers can override by setting `result._result_shape` directly.
11
+ */
12
+ export function classifyResult(result, unfilteredCount) {
13
+ // Handler override
14
+ if (typeof result._result_shape === "string") {
15
+ return result._result_shape;
16
+ }
17
+ // Error shapes — check error field or status indicators
18
+ const error = result.error;
19
+ const status = result.status;
20
+ const apiStatus = result.api_status;
21
+ if (error || status === "failed") {
22
+ // Validation blocked by toolkit (not API)
23
+ if (typeof result.validation_failed === "string") {
24
+ return "error_validation";
25
+ }
26
+ // Deploy-specific failure — check before generic HTTP codes
27
+ // because deploy 400/500 needs deploy-specific guidance
28
+ if (result.mode === "deploy" || result.deployed === false) {
29
+ return "deploy_failed";
30
+ }
31
+ const errorStr = String(error ?? "").toLowerCase();
32
+ const code = apiStatus ?? extractStatusCode(errorStr);
33
+ if (code === 401 || code === 403)
34
+ return "error_401";
35
+ if (code === 404 || code === 422)
36
+ return "error_not_found";
37
+ if (code === 400)
38
+ return "error_400";
39
+ if (code === 500 || code === 502 || code === 503)
40
+ return "error_500";
41
+ return "error";
42
+ }
43
+ // Success shapes
44
+ if (result.success === true || result.persona_id) {
45
+ // Created entity
46
+ if (result.persona_id && !result.workflow_def) {
47
+ return "created";
48
+ }
49
+ }
50
+ if (result.deployed === true || (result.mode === "deploy" && !error)) {
51
+ return "deployed";
52
+ }
53
+ // List shapes — check count
54
+ const count = typeof result.count === "number" ? result.count : undefined;
55
+ if (count !== undefined) {
56
+ if (count === 0) {
57
+ const unfiltered = unfilteredCount
58
+ ?? (typeof result._unfiltered_count === "number" ? result._unfiltered_count : undefined);
59
+ return unfiltered && unfiltered > 0 ? "empty_filtered" : "empty_source";
60
+ }
61
+ return "success";
62
+ }
63
+ // Partial — success with warnings
64
+ if (result._warning && !error) {
65
+ return "partial";
66
+ }
67
+ return "success";
68
+ }
69
+ /** Extract HTTP status code from error message string. */
70
+ function extractStatusCode(errorStr) {
71
+ // Match patterns like "API error (400)" or "HTTP 500" or just "500"
72
+ const match = errorStr.match(/\b(4\d{2}|5\d{2})\b/);
73
+ return match ? parseInt(match[1], 10) : undefined;
74
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Universal Default Guidance — Fallback When DE Has No Match
3
+ *
4
+ * These are the last resort. DE-served atoms and agent-published solutions
5
+ * take priority. Defaults exist so no response ever has zero guidance.
6
+ */
7
+ /**
8
+ * Get default guidance hints for a result shape.
9
+ * Template variables ({tool}, {method}, etc.) are NOT resolved here —
10
+ * the middleware handles resolution after merging all sources.
11
+ */
12
+ export function getDefaultGuidance(shape, ctx) {
13
+ switch (shape) {
14
+ case "empty_filtered":
15
+ return {
16
+ _warning: "Results exist but none match your filters.",
17
+ _tip: "Try without filters to see all, or use knowledge('{tool}') to search DE.",
18
+ _next_step: "{tool}(method='{method}')",
19
+ };
20
+ case "empty_source":
21
+ return {
22
+ _warning: "No results found.",
23
+ _tip: "Try knowledge('{tool}') to search DE, or check your profile/environment.",
24
+ _next_step: "knowledge('{tool}')",
25
+ };
26
+ case "created":
27
+ return {
28
+ _warning: "Entity created but may need additional configuration.",
29
+ _next_step: "workflow(mode='get', persona_id='{persona_id}') to get starter workflow, then build and deploy a complete workflow_def.",
30
+ };
31
+ case "deployed":
32
+ return {
33
+ _next_step: "Verify: workflow(mode='get', persona_id='{persona_id}') — confirm workflow is active.",
34
+ };
35
+ case "deploy_failed":
36
+ return {
37
+ _warning: "Deployment failed. Do NOT retry with the same workflow_def — fix the issue first. If the error is structural (not just a field fix), backtrack to design before rebuilding JSON.",
38
+ _likely_causes: [
39
+ "Type mismatch: input binding has incompatible type",
40
+ "Missing or invalid named_inputs format — see knowledge('named inputs format')",
41
+ "Empty namespaces on action names or enumTypes",
42
+ "namedResults producer missing actionName or outputName",
43
+ ],
44
+ _debug_steps: [
45
+ "Read the error message — it names the specific field/action that failed",
46
+ "Search for the error: knowledge('{tool} {method} <error_keywords>') — other agents may have solved this",
47
+ "Compare your workflow_def against a working one: workflow(mode='get', persona_id='<similar_persona>')",
48
+ "Run workflow(mode='validate') to catch remaining issues before re-deploying",
49
+ "If the error is structural (wrong shape, not just a wrong value), go back to design — don't patch JSON",
50
+ ],
51
+ _next_step: "knowledge('{tool} deploy <error_keywords>') to find solutions, then fix and workflow(mode='validate') before retrying.",
52
+ };
53
+ case "error_400":
54
+ return {
55
+ _warning: "Bad request — the API rejected the input.",
56
+ _likely_causes: [
57
+ "Invalid field format or missing required field",
58
+ "Input type mismatch (e.g., string where number expected)",
59
+ "Constraint violation (e.g., duplicate name, invalid enum value)",
60
+ ],
61
+ _debug_steps: [
62
+ "Read the error message — it usually names the problematic field",
63
+ "Search for the error: knowledge('{tool} {method} <error_keywords>') — may find known solutions",
64
+ "Check field constraints: knowledge('field constraints')",
65
+ ],
66
+ _next_step: "knowledge('{tool} {method} <error_keywords>') to find known solutions for this error.",
67
+ };
68
+ case "error_500":
69
+ return {
70
+ _warning: "Server error — the API gave no details. Do NOT guess — search knowledge and compare against a working workflow.",
71
+ _likely_causes: [
72
+ "Type mismatch in input bindings (API doesn't say which input)",
73
+ "Missing typeArguments on categorizer nodes",
74
+ "Invalid named_inputs or extraction_columns format",
75
+ "Empty namespaces array on action names or enumTypes",
76
+ "Deprecated action version",
77
+ ],
78
+ _debug_steps: [
79
+ "Search for the error: knowledge('{tool} deploy 500 <error_keywords>') — other agents may have solved this",
80
+ "Compare against a WORKING workflow: workflow(mode='get', persona_id='<similar_persona>')",
81
+ "Check EACH input binding type — TEXT_WITH_SOURCES ≠ STRING ≠ JSON_VALUE",
82
+ "Verify enumTypes and action namespaces are non-empty arrays, not []",
83
+ "Check namedResults producers have both actionName AND outputName",
84
+ ],
85
+ _next_step: "knowledge('{tool} deploy 500') to find known solutions, then compare against a working workflow_def.",
86
+ };
87
+ case "error_401":
88
+ return {
89
+ _warning: "Authentication failed.",
90
+ _next_step: "config(method='login') to re-authenticate.",
91
+ };
92
+ case "error_not_found":
93
+ return {
94
+ _warning: "Entity not found.",
95
+ _tip: "Check the ID is correct and you're using the right profile/environment.",
96
+ };
97
+ case "error_validation":
98
+ return {
99
+ _warning: "Validation blocked the operation before it reached the API.",
100
+ _debug_steps: [
101
+ "Read the validation errors — they describe exactly what to fix",
102
+ "Fix the issues and retry",
103
+ ],
104
+ };
105
+ case "error":
106
+ return {
107
+ _tip: "If this error is unclear, use feedback(method='submit', category='error_unclear', message='...') to report it.",
108
+ };
109
+ case "partial":
110
+ case "success":
111
+ default:
112
+ return {};
113
+ }
114
+ }