@ema.co/mcp-toolkit 2026.3.25-1 → 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.
@@ -32,6 +32,7 @@ export class GuidanceCache {
32
32
  patterns = [];
33
33
  questions = [];
34
34
  invariants = [];
35
+ contextualGuidanceMap = new Map();
35
36
  warmedAt = null;
36
37
  constructor(searchFn, config) {
37
38
  this.searchFn = searchFn;
@@ -122,6 +123,32 @@ export class GuidanceCache {
122
123
  this.maybeRefresh();
123
124
  return this.invariants;
124
125
  }
126
+ /**
127
+ * Get contextual guidance for a specific key.
128
+ * Keys follow the pattern: "{tool}.{method}.{result_shape}"
129
+ * Also checks wildcards: "*.{method}.{shape}", "{tool}.*.{shape}", "*.*.{shape}"
130
+ */
131
+ getContextualGuidance(tool, method, shape) {
132
+ this.maybeRefresh();
133
+ // Try exact match first, then progressively broader wildcards
134
+ const candidates = [
135
+ `${tool}.${method}.${shape}`,
136
+ `${tool}.*.${shape}`,
137
+ `*.${method}.${shape}`,
138
+ `*.*.${shape}`,
139
+ ];
140
+ for (const key of candidates) {
141
+ const atom = this.contextualGuidanceMap.get(key);
142
+ if (atom)
143
+ return atom;
144
+ }
145
+ return undefined;
146
+ }
147
+ /** All contextual guidance atoms (for debugging/inspection). */
148
+ getAllContextualGuidance() {
149
+ this.maybeRefresh();
150
+ return Array.from(this.contextualGuidanceMap.values());
151
+ }
125
152
  // ── Internals ───────────────────────────────────────────────────────────
126
153
  clear() {
127
154
  this.rules = [];
@@ -131,6 +158,7 @@ export class GuidanceCache {
131
158
  this.patterns = [];
132
159
  this.questions = [];
133
160
  this.invariants = [];
161
+ this.contextualGuidanceMap = new Map();
134
162
  }
135
163
  /**
136
164
  * Index artifacts by reading structured data from structData.data.
@@ -184,6 +212,13 @@ export class GuidanceCache {
184
212
  case "structural-invariant":
185
213
  this.invariants.push(data);
186
214
  break;
215
+ case "contextual-guidance": {
216
+ const atom = data;
217
+ const key = atom.key
218
+ ?? `${atom.tool ?? "*"}.${atom.method ?? "*"}.${atom.result_shape ?? "*"}`;
219
+ this.contextualGuidanceMap.set(key, { ...atom, key });
220
+ break;
221
+ }
187
222
  default:
188
223
  break;
189
224
  }
@@ -209,10 +244,16 @@ export class GuidanceCache {
209
244
  return "qualifying-question";
210
245
  if (tags.includes("structural-invariant"))
211
246
  return "structural-invariant";
247
+ if (tags.includes("structural-invariant"))
248
+ return "structural-invariant";
249
+ if (tags.includes("contextual-guidance"))
250
+ return "contextual-guidance";
212
251
  if (name.includes("tool-guidance"))
213
252
  return "tool-guidance";
214
253
  if (name.startsWith("invariant-"))
215
254
  return "structural-invariant";
255
+ if (name.includes("contextual-guidance"))
256
+ return "contextual-guidance";
216
257
  return undefined;
217
258
  }
218
259
  }
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Generation Schema
3
3
  *
4
- * Transforms AGENT_CATALOG into a compact format for workflow generation.
5
- * This avoids the token waste of including full documentation in prompts.
4
+ * API-first schema generation: fetches live action definitions from the
5
+ * backend, enriches with catalog metadata, falls back to static catalog
6
+ * when the API is unavailable.
7
+ *
8
+ * This ensures agents always see the FULL set of required inputs/outputs
9
+ * as defined by the backend, not a hand-curated subset from documentation.
6
10
  *
7
11
  * The auto-builder sends ~70K tokens of documentation per request.
8
12
  * This schema reduces that to ~5K tokens of actionable constraints.
@@ -11,42 +15,213 @@ import { AGENT_CATALOG } from "../knowledge.js";
11
15
  import { INPUT_SOURCE_RULES } from "./validation-rules.js";
12
16
  import { STRUCTURAL_INVARIANTS } from "./structural-rules.js";
13
17
  import { LLM_ACTIONS, LLM_WIDGET_INPUTS, widgetBinding } from "../../config/widget-bindings.js";
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- // Schema Generation
16
- // ─────────────────────────────────────────────────────────────────────────────
18
+ import { searchDocuments } from "../../knowledge/search-client.js";
19
+ export async function buildCompactAgents(client) {
20
+ // Build catalog index for enrichment
21
+ const catalogIndex = new Map();
22
+ for (const agent of AGENT_CATALOG) {
23
+ catalogIndex.set(agent.actionName, agent);
24
+ }
25
+ // Try API-first for full input/output definitions
26
+ if (client) {
27
+ try {
28
+ const apiAgents = await buildFromApi(client, catalogIndex);
29
+ if (Object.keys(apiAgents).length > 0) {
30
+ return { agents: apiAgents, source: "api" };
31
+ }
32
+ }
33
+ catch (e) {
34
+ console.error(`[generation-schema] API listActions failed: ${e instanceof Error ? e.message : String(e)}`);
35
+ }
36
+ }
37
+ // DE middle layer: search for action definitions in Discovery Engine
38
+ try {
39
+ const deAgents = await buildFromDE(catalogIndex);
40
+ if (deAgents && Object.keys(deAgents).length > 0) {
41
+ return { agents: deAgents, source: "api" };
42
+ }
43
+ }
44
+ catch (e) {
45
+ console.error(`[generation-schema] DE search failed, using static catalog: ${e instanceof Error ? e.message : String(e)}`);
46
+ }
47
+ // Final fallback: static catalog (may have incomplete inputs)
48
+ return { agents: buildFromCatalog(catalogIndex), source: "catalog" };
49
+ }
50
+ /** Synchronous version for backwards compatibility (uses catalog only). */
51
+ export function buildCompactAgentsSync() {
52
+ const catalogIndex = new Map();
53
+ for (const agent of AGENT_CATALOG) {
54
+ catalogIndex.set(agent.actionName, agent);
55
+ }
56
+ return { agents: buildFromCatalog(catalogIndex), source: "catalog" };
57
+ }
17
58
  /**
18
- * Build a compact agent lookup from the full catalog
59
+ * Build agents from live API full input/output definitions.
60
+ * Enriches with catalog metadata (description, whenToUse, etc.).
19
61
  */
20
- export function buildCompactAgents() {
62
+ async function buildFromApi(client, catalogIndex) {
63
+ const actionDTOs = await client.listActions();
21
64
  const agents = {};
22
- for (const agent of AGENT_CATALOG) {
65
+ for (const dto of actionDTOs) {
66
+ const typeName = dto.typeName;
67
+ const name = typeName?.name?.name;
68
+ if (!name)
69
+ continue;
23
70
  const inputs = {};
24
- for (const input of agent.inputs ?? []) {
25
- inputs[input.name] = { type: input.type, required: input.required };
71
+ const outputs = {};
72
+ // Parse inputs from API response (FULL definition)
73
+ const inputsObj = dto.inputs?.inputs;
74
+ if (inputsObj && typeof inputsObj === "object") {
75
+ for (const [inputName, inputDef] of Object.entries(inputsObj)) {
76
+ const def = inputDef;
77
+ inputs[inputName] = {
78
+ type: (def.type?.wellKnownType ?? "WELL_KNOWN_TYPE_ANY"),
79
+ required: !def.isOptional,
80
+ };
81
+ }
26
82
  }
83
+ // Parse outputs from API response
84
+ const outputsObj = dto.outputs?.outputs;
85
+ if (outputsObj && typeof outputsObj === "object") {
86
+ for (const [outputName, outputDef] of Object.entries(outputsObj)) {
87
+ const def = outputDef;
88
+ outputs[outputName] = (def.type?.wellKnownType ?? "WELL_KNOWN_TYPE_ANY");
89
+ }
90
+ }
91
+ // Inject widget inputs for LLM actions (may already be in API response)
92
+ if (LLM_ACTIONS.has(name)) {
93
+ for (const binding of LLM_WIDGET_INPUTS) {
94
+ if (!inputs[binding.inputName]) {
95
+ inputs[binding.inputName] = {
96
+ type: "WELL_KNOWN_TYPE_ANY",
97
+ required: binding.required,
98
+ };
99
+ }
100
+ }
101
+ }
102
+ // Enrich with catalog metadata (doesn't override inputs/outputs)
103
+ const catalogEntry = catalogIndex.get(name);
104
+ agents[name] = {
105
+ name: catalogEntry?.displayName ?? name,
106
+ category: catalogEntry?.category ?? "unknown",
107
+ description: catalogEntry?.description,
108
+ whenToUse: catalogEntry?.whenToUse,
109
+ aliases: catalogEntry?.aliases,
110
+ tier: catalogEntry?.tier,
111
+ inputs,
112
+ outputs,
113
+ constraints: catalogEntry?.criticalRules,
114
+ };
115
+ }
116
+ return agents;
117
+ }
118
+ /**
119
+ * Build agents from DE search — middle layer between API and static catalog.
120
+ * Queries DE for entity-type action docs, matches to catalog by action name
121
+ * extracted from the document ID (e.g., "entity:search_v2" → "search_v2").
122
+ * Returns null if DE search yields no action entities.
123
+ */
124
+ async function buildFromDE(catalogIndex) {
125
+ const response = await searchDocuments("action definitions inputs outputs workflow actions", {
126
+ topK: 15,
127
+ filters: { type: ["entity"] },
128
+ });
129
+ if (!response.results || response.results.length === 0)
130
+ return null;
131
+ const deAgents = {};
132
+ const deActionNames = new Set();
133
+ for (const result of response.results) {
134
+ const doc = result.document;
135
+ if (!doc?.id)
136
+ continue;
137
+ // Extract action name from entity ID (e.g., "entity:search_v2" → "search_v2")
138
+ const actionName = doc.id.startsWith("entity:") ? doc.id.slice("entity:".length) : "";
139
+ if (!actionName)
140
+ continue;
141
+ deActionNames.add(actionName);
142
+ const catalogEntry = catalogIndex.get(actionName);
143
+ const inputs = {};
27
144
  const outputs = {};
28
- for (const output of agent.outputs ?? []) {
29
- outputs[output.name] = output.type;
145
+ // Use catalog I/O specs (DE entities don't carry structured I/O)
146
+ if (catalogEntry) {
147
+ for (const input of catalogEntry.inputs ?? []) {
148
+ inputs[input.name] = { type: input.type, required: input.required };
149
+ }
150
+ for (const output of catalogEntry.outputs ?? []) {
151
+ outputs[output.name] = output.type;
152
+ }
30
153
  }
31
- if (LLM_ACTIONS.has(agent.actionName)) {
154
+ if (LLM_ACTIONS.has(actionName)) {
32
155
  for (const binding of LLM_WIDGET_INPUTS) {
156
+ if (!inputs[binding.inputName]) {
157
+ inputs[binding.inputName] = {
158
+ type: "WELL_KNOWN_TYPE_ANY",
159
+ required: binding.required,
160
+ };
161
+ }
162
+ }
163
+ }
164
+ deAgents[actionName] = {
165
+ name: catalogEntry?.displayName ?? doc.name ?? actionName,
166
+ category: catalogEntry?.category ?? "unknown",
167
+ description: catalogEntry?.description ?? doc.description ?? doc.summary,
168
+ whenToUse: catalogEntry?.whenToUse,
169
+ aliases: catalogEntry?.aliases,
170
+ tier: catalogEntry?.tier,
171
+ inputs,
172
+ outputs,
173
+ constraints: catalogEntry?.criticalRules,
174
+ };
175
+ }
176
+ if (deActionNames.size === 0)
177
+ return null;
178
+ // Supplement with catalog entries not found in DE
179
+ for (const [actionName, catalogEntry] of catalogIndex) {
180
+ if (deAgents[actionName])
181
+ continue;
182
+ deAgents[actionName] = buildCatalogAgent(actionName, catalogEntry);
183
+ }
184
+ return deAgents;
185
+ }
186
+ /** Build a CompactAgent from a single catalog entry. */
187
+ function buildCatalogAgent(actionName, entry) {
188
+ const inputs = {};
189
+ for (const input of entry.inputs ?? []) {
190
+ inputs[input.name] = { type: input.type, required: input.required };
191
+ }
192
+ const outputs = {};
193
+ for (const output of entry.outputs ?? []) {
194
+ outputs[output.name] = output.type;
195
+ }
196
+ if (LLM_ACTIONS.has(actionName)) {
197
+ for (const binding of LLM_WIDGET_INPUTS) {
198
+ if (!inputs[binding.inputName]) {
33
199
  inputs[binding.inputName] = {
34
200
  type: "WELL_KNOWN_TYPE_ANY",
35
201
  required: binding.required,
36
202
  };
37
203
  }
38
204
  }
39
- agents[agent.actionName] = {
40
- name: agent.displayName,
41
- category: agent.category,
42
- description: agent.description,
43
- whenToUse: agent.whenToUse,
44
- aliases: agent.aliases,
45
- tier: agent.tier,
46
- inputs,
47
- outputs,
48
- constraints: agent.criticalRules,
49
- };
205
+ }
206
+ return {
207
+ name: entry.displayName,
208
+ category: entry.category,
209
+ description: entry.description,
210
+ whenToUse: entry.whenToUse,
211
+ aliases: entry.aliases,
212
+ tier: entry.tier,
213
+ inputs,
214
+ outputs,
215
+ constraints: entry.criticalRules,
216
+ };
217
+ }
218
+ /**
219
+ * Build agents from static AGENT_CATALOG (fallback when API unavailable).
220
+ */
221
+ function buildFromCatalog(catalogIndex) {
222
+ const agents = {};
223
+ for (const [actionName, entry] of catalogIndex) {
224
+ agents[actionName] = buildCatalogAgent(actionName, entry);
50
225
  }
51
226
  return agents;
52
227
  }
@@ -119,15 +294,37 @@ export function buildConstraints() {
119
294
  };
120
295
  }
121
296
  /**
122
- * Generate the complete compact schema for workflow generation
297
+ * Generate the complete compact schema for workflow generation.
298
+ * API-first: fetches live action definitions when client is provided.
123
299
  */
124
- export function generateSchema(cache) {
125
- // DE-first for structural invariants: use cache if populated, fall back to local constants
300
+ export async function generateSchema(client, cache) {
301
+ const invariants = cache?.getInvariants()?.length
302
+ ? cache.getInvariants()
303
+ : STRUCTURAL_INVARIANTS;
304
+ const { agents, source } = await buildCompactAgents(client);
305
+ return {
306
+ agents,
307
+ schema_source: source,
308
+ typeRules: buildTypeRules(),
309
+ inputRules: INPUT_SOURCE_RULES.map(rule => ({
310
+ action: rule.actionPattern,
311
+ recommended: rule.recommended,
312
+ avoid: rule.avoid,
313
+ severity: rule.severity,
314
+ })),
315
+ constraints: buildConstraints(),
316
+ structuralInvariants: invariants.map(({ id, rule, violation, fix, severity }) => ({ id, rule, violation, fix, severity })),
317
+ };
318
+ }
319
+ /** Synchronous version (catalog-only, no API). For callers that can't await. */
320
+ export function generateSchemaSync(cache) {
126
321
  const invariants = cache?.getInvariants()?.length
127
322
  ? cache.getInvariants()
128
323
  : STRUCTURAL_INVARIANTS;
324
+ const { agents, source } = buildCompactAgentsSync();
129
325
  return {
130
- agents: buildCompactAgents(),
326
+ agents,
327
+ schema_source: source,
131
328
  typeRules: buildTypeRules(),
132
329
  inputRules: INPUT_SOURCE_RULES.map(rule => ({
133
330
  action: rule.actionPattern,
@@ -144,7 +341,7 @@ export function generateSchema(cache) {
144
341
  * This is ~5K tokens vs ~70K for full documentation
145
342
  */
146
343
  export function generateSchemaMarkdown(cache) {
147
- const schema = generateSchema(cache);
344
+ const schema = generateSchemaSync(cache);
148
345
  let md = `# Workflow Generation Schema\n\n`;
149
346
  // Agent summary table with tier and description
150
347
  md += `## Available Agents (${Object.keys(schema.agents).length})\n\n`;
@@ -217,7 +414,7 @@ export function generateSchemaMarkdown(cache) {
217
414
  * Get compact agent info for a specific action
218
415
  */
219
416
  export function getAgentSchema(actionName) {
220
- const schema = generateSchema();
417
+ const schema = generateSchemaSync();
221
418
  return schema.agents[actionName];
222
419
  }
223
420
  /**
@@ -231,7 +428,7 @@ export function isTypeCompatible(sourceType, targetType) {
231
428
  if (sourceType === targetType)
232
429
  return true;
233
430
  // Check explicit rules
234
- const schema = generateSchema();
431
+ const schema = generateSchemaSync();
235
432
  const rule = schema.typeRules.find(r => r.sourceType === sourceType);
236
433
  return rule?.targetTypes.includes(targetType) ?? false;
237
434
  }