@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.
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { generateSchema } from "../../domain/generation-schema.js";
20
20
  import { fingerprintPersona } from "../../../sync.js";
21
+ import { LLM_ACTIONS, LLM_WIDGET_INPUTS, widgetBinding } from "../../../config/widget-bindings.js";
21
22
  // Re-export types
22
23
  export * from "./types.js";
23
24
  // Re-export utilities
@@ -172,7 +173,7 @@ function slimWorkflowDef(wf) {
172
173
  *
173
174
  * LLM uses this to generate or modify workflow_def
174
175
  */
175
- async function handleWorkflowGet(args, client) {
176
+ async function handleWorkflowGet(args, client, cache) {
176
177
  const personaId = args.persona_id;
177
178
  if (!personaId) {
178
179
  return { error: "persona_id required" };
@@ -191,8 +192,8 @@ async function handleWorkflowGet(args, client) {
191
192
  name: w.name,
192
193
  type: w.type,
193
194
  }));
194
- // Get generation schema for LLM
195
- const schema = generateSchema();
195
+ // Get generation schema for LLM — API-first + DE-first for structural invariants
196
+ const schema = await generateSchema(client, cache);
196
197
  // Get deprecated actions (API-first, with fallback)
197
198
  const { deprecated: deprecatedActions, source: deprecationSource } = await getDeprecatedActions(client);
198
199
  // Check if current workflow uses deprecated actions
@@ -217,6 +218,17 @@ async function handleWorkflowGet(args, client) {
217
218
  agents: schema.agents,
218
219
  constraints: schema.constraints,
219
220
  input_rules: schema.inputRules,
221
+ widget_bindings: LLM_WIDGET_INPUTS.map(b => ({
222
+ input: b.inputName,
223
+ widget: b.widgetName,
224
+ required: b.required,
225
+ binding: widgetBinding(b),
226
+ })),
227
+ llm_actions: [...LLM_ACTIONS],
228
+ _source: schema.schema_source,
229
+ ...(schema.schema_source === "catalog" ? {
230
+ _warning: "Using static catalog (API unavailable) — input lists may be incomplete. Search knowledge('<action_name> inputs') for full definitions.",
231
+ } : {}),
220
232
  },
221
233
  // HARD REQUIREMENTS (must be satisfied)
222
234
  hard_requirements: {
@@ -255,8 +267,8 @@ async function handleWorkflowGet(args, client) {
255
267
  "",
256
268
  "IMPORTANT: Before deploying changes, re-run workflow(mode='get') and pass fingerprint as base_fingerprint to workflow(mode='deploy').",
257
269
  "To analyze workflow health:",
258
- "1. Fetch ema://rules/anti-patterns",
259
- "2. Fetch ema://rules/structural-invariants",
270
+ "1. Search knowledge('workflow anti-patterns')",
271
+ "2. Search knowledge('structural invariants')",
260
272
  "3. Check workflow_def nodes against the rules",
261
273
  "4. Report issues YOU find (MCP does not pre-compute)",
262
274
  "",
@@ -264,8 +276,8 @@ async function handleWorkflowGet(args, client) {
264
276
  ]
265
277
  : [
266
278
  "To analyze workflow health:",
267
- "1. Fetch ema://rules/anti-patterns",
268
- "2. Fetch ema://rules/structural-invariants",
279
+ "1. Search knowledge('workflow anti-patterns')",
280
+ "2. Search knowledge('structural invariants')",
269
281
  "3. Check workflow_def nodes against the rules",
270
282
  "4. Report issues YOU find (MCP does not pre-compute)",
271
283
  "",
@@ -310,14 +322,14 @@ async function handleWorkflowGet(args, client) {
310
322
  * PUBLIC modes: get, deploy, validate, optimize
311
323
  * INTERNAL modes: modify, generate, analyze, compare (called from persona tool)
312
324
  */
313
- export async function handleWorkflow(args, client, _getTemplateId) {
325
+ export async function handleWorkflow(args, client, _getTemplateId, cache) {
314
326
  const personaId = args.persona_id;
315
327
  const workflowDef = args.workflow_def;
316
328
  // Explicit mode takes priority
317
329
  const mode = args.mode;
318
330
  // PUBLIC MODES (from workflow tool)
319
331
  if (mode === "get") {
320
- return handleWorkflowGet(args, client);
332
+ return handleWorkflowGet(args, client, cache);
321
333
  }
322
334
  if (mode === "deploy") {
323
335
  return handleWorkflowDeploy(args, client);
@@ -347,10 +359,10 @@ export async function handleWorkflow(args, client, _getTemplateId) {
347
359
  if (mode === "analyze" || mode === "compare") {
348
360
  return {
349
361
  error: `Mode "${mode}" has been removed.`,
350
- reason: "LLM can do analysis/comparison by fetching ema://rules/* and reasoning about workflow_def.",
362
+ reason: "LLM can do analysis/comparison by fetching knowledge('anti-patterns') and reasoning about workflow_def.",
351
363
  correct_flow: [
352
364
  "1. workflow(mode='get', persona_id='...') - get current workflow_def",
353
- "2. Fetch ema://rules/anti-patterns and ema://rules/structural-invariants",
365
+ "2. Fetch knowledge('anti-patterns') and knowledge('structural invariants')",
354
366
  "3. LLM analyzes/compares the data",
355
367
  ],
356
368
  };
@@ -5,7 +5,8 @@
5
5
  * Validates: wiring, deprecated actions, circular refs, categorizer fallbacks, etc.
6
6
  */
7
7
  import { validateWorkflowStatic } from "../../domain/workflow-static-validator.js";
8
- import { validateWorkflowSchema, validateNodeInputsWired, validateNoDeprecatedActions, validateExternalActionCallerTools, validateNoCircularReferences, validateCategorizersFallback, checkBestPractices, checkRunIfGatedDependencies, } from "./validation.js";
8
+ import { validateWorkflowDefStructure } from "../../domain/workflow-def-validator.js";
9
+ import { validateWorkflowSchema, validateNodeInputsWired, validateNoDeprecatedActions, validateExternalActionCallerTools, validateNoCircularReferences, validateCategorizersFallback, checkBestPractices, checkRunIfGatedDependencies, detectTypeMismatches, } from "./validation.js";
9
10
  import { DEPRECATED_ACTIONS_MAP } from "../../../sdk/generated/deprecated-actions.js";
10
11
  /**
11
12
  * Handle workflow(mode="validate") - pre-deployment validation
@@ -85,6 +86,26 @@ export async function handleWorkflowValidate(args, client) {
85
86
  _tip: "Fix structural errors first. Use workflow(mode='get') to see valid structure.",
86
87
  };
87
88
  }
89
+ // 0b. Proto structural validation — catches empty namespaces, wrong named_inputs
90
+ // format, invalid extraction_columns, missing typeArguments, etc. that cause
91
+ // opaque API 500s. MUST match the deploy handler's validation (deploy.ts:106).
92
+ const protoValidation = validateWorkflowDefStructure(workflowToValidate);
93
+ const protoErrors = protoValidation.issues.filter(i => i.severity === "error");
94
+ if (protoErrors.length > 0) {
95
+ for (const issue of protoErrors) {
96
+ errors.push({
97
+ check: "proto_structural",
98
+ message: `${issue.path}: ${issue.message}`,
99
+ });
100
+ }
101
+ }
102
+ const protoWarnings = protoValidation.issues.filter(i => i.severity === "warning");
103
+ for (const issue of protoWarnings) {
104
+ warnings.push({
105
+ check: "proto_structural",
106
+ message: `${issue.path}: ${issue.message}`,
107
+ });
108
+ }
88
109
  // 1. Node inputs must be wired
89
110
  const inputsValidation = validateNodeInputsWired(workflowToValidate);
90
111
  if (!inputsValidation.valid) {
@@ -155,6 +176,14 @@ export async function handleWorkflowValidate(args, client) {
155
176
  message: gd.issue,
156
177
  });
157
178
  }
179
+ // 8. Type mismatch detection (warnings — uses generation schema type info)
180
+ const typeMismatches = detectTypeMismatches(workflowToValidate);
181
+ for (const tm of typeMismatches) {
182
+ warnings.push({
183
+ check: "type_mismatch",
184
+ message: `Type mismatch: "${tm.action}.${tm.input}" expects ${tm.expectedType} but receives ${tm.sourceType} from "${tm.sourceAction}.${tm.sourceOutput}"`,
185
+ });
186
+ }
158
187
  const valid = errors.length === 0;
159
188
  return {
160
189
  valid,
@@ -17,7 +17,7 @@
17
17
  import { ConnectError, Code } from "@connectrpc/connect";
18
18
  import { validateWorkflowDefSchema } from "../../domain/workflow-def-schema.js";
19
19
  import { LLM_ACTIONS, MODEL_CONFIG, widgetBinding } from "../../../config/widget-bindings.js";
20
- import { generateSchema, isTypeCompatible } from "../../domain/generation-schema.js";
20
+ import { generateSchemaSync, isTypeCompatible } from "../../domain/generation-schema.js";
21
21
  // Re-export schema validation for use by other handlers
22
22
  export { validateWorkflowDefSchema };
23
23
  /**
@@ -585,7 +585,7 @@ export function validateNodeInputsWired(workflowDef) {
585
585
  fixSteps.push("To find required inputs for an action:");
586
586
  fixSteps.push(" knowledge(\"id=<action_type>\")");
587
587
  fixSteps.push("For full input format reference:");
588
- fixSteps.push(" Fetch ema://schema/workflow-def 'Raw workflow_def Input Binding Formats' section");
588
+ fixSteps.push(" Search knowledge(\"input binding formats\") for correct wiring structure");
589
589
  return {
590
590
  valid: false,
591
591
  error: `DEPLOYMENT BLOCKED: ${unwiredNodes.length} node(s) have empty inputs and are not wired to any data source.`,
@@ -1017,7 +1017,7 @@ export function checkBestPractices(workflowDef) {
1017
1017
  issue: "custom_agent output consumed by json_mapper but no output_fields defined",
1018
1018
  recommendation: "Add output_fields with extraction columns matching your JSON keys, OR " +
1019
1019
  "ensure task_instructions prompt says 'output ONLY JSON, no markdown'. " +
1020
- "See ema://rules/json-output-patterns for the full pattern.",
1020
+ "Search knowledge(\"json output patterns\") for the full pattern.",
1021
1021
  severity: "warning",
1022
1022
  });
1023
1023
  }
@@ -1163,7 +1163,7 @@ export function checkRunIfGatedDependencies(workflowDef) {
1163
1163
  * the schema may not cover all actions (custom actions, unknown versions).
1164
1164
  */
1165
1165
  export function detectTypeMismatches(workflowDef) {
1166
- const schema = generateSchema();
1166
+ const schema = generateSchemaSync();
1167
1167
  const actions = (workflowDef.actions ?? []);
1168
1168
  const mismatches = [];
1169
1169
  // Build action name → schema agent lookup
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
- import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
19
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, SubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
20
20
  // Environment configuration, client factories, and toolkit metadata
21
21
  import { TOOLKIT_NAME, TOOLKIT_VERSION, TOOLKIT_COMMIT, createClient, getSyncSDK, getAvailableEnvironments, getDefaultEnvName, initializeApiKeyTokens, } from "./handlers/env/config.js";
22
22
  export { TOOLKIT_NAME, TOOLKIT_VERSION, TOOLKIT_COMMIT };
@@ -28,6 +28,27 @@ import { generateServerInstructions, getContextualTip, TOOL_GUIDANCE } from "./g
28
28
  // Set EMA_GUIDANCE_CACHE=false to disable and use local constants only.
29
29
  import { GuidanceCache } from "../knowledge/guidance-cache.js";
30
30
  import { createDeSearchAdapter } from "../knowledge/search-adapter.js";
31
+ // Dynamic contextual guidance middleware — DE-first, defaults fallback
32
+ import { enrichWithGuidance } from "./guidance/middleware.js";
33
+ import { searchDocuments } from "../knowledge/search-client.js";
34
+ // Live DE search for contextual error guidance.
35
+ // On errors, the middleware queries DE with the error context to find
36
+ // relevant solutions — including agent-published fixes for similar errors.
37
+ const guidanceSearchFn = async (query, options) => {
38
+ const response = await searchDocuments(query, {
39
+ topK: options?.limit ?? 3,
40
+ filters: { type: ["contextual-guidance", "guidance", "entity", "pattern"] },
41
+ });
42
+ return response.results.map((r) => {
43
+ const doc = r.document;
44
+ return {
45
+ id: r.id,
46
+ score: r.score,
47
+ content: doc.description,
48
+ structData: doc.structData,
49
+ };
50
+ });
51
+ };
31
52
  const cacheEnabled = process.env.EMA_GUIDANCE_CACHE?.trim().toLowerCase() !== "false";
32
53
  const guidanceCache = cacheEnabled ? new GuidanceCache(createDeSearchAdapter()) : undefined;
33
54
  // Cache is warmed in startMcpServer() before generating server instructions.
@@ -162,7 +183,7 @@ const toolHandlers = {
162
183
  },
163
184
  // V2 adapters — routing + transformation extracted to handler files
164
185
  workflow: async (args) => {
165
- return handleWorkflowAdapter(args, createClient, getDefaultEnvName);
186
+ return handleWorkflowAdapter(args, createClient, getDefaultEnvName, guidanceCache);
166
187
  },
167
188
  sync: async (args) => {
168
189
  return handleSyncAdapter(args, createClient, getDefaultEnvName, getSyncSDK);
@@ -355,15 +376,44 @@ export async function startMcpServer() {
355
376
  if (name !== "feedback" && name !== "toolkit_feedback") {
356
377
  recordTelemetry({ type: "tool_call", tool: name, op: operation, ok: true, ms: elapsedMs }).catch(() => { });
357
378
  }
379
+ // ── Dynamic Contextual Guidance (DE-first, defaults fallback) ──────
380
+ // Classifies result shape and injects error-aware hints BEFORE
381
+ // the legacy hint injection layers below. Only sets fields the
382
+ // handler didn't already set.
383
+ const response = result;
384
+ try {
385
+ const method = (argsObj.method ?? argsObj.mode ?? operation);
386
+ let profileName;
387
+ let envName;
388
+ try {
389
+ const p = getActiveProfile();
390
+ profileName = p?.name;
391
+ envName = p?.env;
392
+ }
393
+ catch { /* config not available */ }
394
+ await enrichWithGuidance(response, {
395
+ tool: name,
396
+ method,
397
+ args: argsObj,
398
+ result: response,
399
+ env: envName,
400
+ profile: profileName,
401
+ }, guidanceCache, guidanceSearchFn);
402
+ }
403
+ catch {
404
+ // Guidance middleware must never break tool dispatch
405
+ }
406
+ // ── Legacy hint injection (kept for backwards compatibility) ────────
407
+ // These layers run AFTER the middleware. They use ??= so they won't
408
+ // overwrite hints the middleware already set.
358
409
  // Get contextual tip based on operation and result
359
410
  const tip = getContextualTip({
360
411
  operation,
361
412
  args: argsObj,
362
- result: result,
413
+ result: response,
363
414
  }, guidanceCache);
364
415
  // Add tip and next_step to response if available
365
- const response = result;
366
- if (tip) {
416
+ if (tip && !response._tip) {
367
417
  response._tip = {
368
418
  message: tip.message,
369
419
  level: tip.level,
@@ -521,6 +571,8 @@ export async function startMcpServer() {
521
571
  ],
522
572
  };
523
573
  });
574
+ // Subscribe handler — no-op (resources are generated on-read, no push notifications yet)
575
+ server.setRequestHandler(SubscribeRequestSchema, async () => ({}));
524
576
  const transport = new StdioServerTransport();
525
577
  await server.connect(transport);
526
578
  // Log startup with version, commit, and tool count
package/dist/mcp/tools.js CHANGED
@@ -289,10 +289,10 @@ Sync a persona between environments (dev, staging, prod). Always preview first.
289
289
  properties: {
290
290
  method: {
291
291
  type: "string",
292
- enum: ["conversations", "conversation_detail", "show_work", "action_detail", "search"],
292
+ enum: ["conversations", "conversation_detail", "show_work", "action_detail", "workflow_runs", "search"],
293
293
  description: "Debug operation to perform (required)"
294
294
  },
295
- conversation_id: { type: "string", description: "Conversation ID (for conversation_detail)" },
295
+ conversation_id: { type: "string", description: "Conversation ID (for conversation_detail). Also accepted by show_work." },
296
296
  workflow_run_id: { type: "string", description: "Workflow run ID (for show_work, action_detail)" },
297
297
  action_name: { type: "string", description: "Action name (for action_detail)" },
298
298
  query: { type: "string", description: "Search query (for search)" },
@@ -777,6 +777,11 @@ Include \`knowledge_ref\` to correlate feedback with specific knowledge document
777
777
  - \`feedback(method="analyze")\` - aggregate local insights and actionable items
778
778
  - \`feedback(method="analyze", scope="global")\` - [Ema maintainers only] aggregate across ALL clients from cloud storage (requires EMA_INTERNAL=1 + gcloud auth with @ema.co account)
779
779
 
780
+ ## Inter-Agent Messaging
781
+ - \`feedback(method="local")\` - read scoped messages from the in-memory session buffer
782
+ - \`feedback(method="local", scope="role:compliance")\` - filter by message scope
783
+ Messages are emitted by MCP response actions (search, publish, deploy) and stay in-memory for the session. Use for coordination between agents working on the same task.
784
+
780
785
  ## Maintenance
781
786
  - \`feedback(method="flush")\` - force immediate upload of pending outbox entries to remote storage
782
787
 
@@ -791,8 +796,8 @@ Include \`knowledge_ref\` to correlate feedback with specific knowledge document
791
796
  properties: {
792
797
  method: {
793
798
  type: "string",
794
- enum: ["submit", "list", "analyze", "flush"],
795
- description: "Operation to perform (required)",
799
+ enum: ["submit", "list", "local", "analyze", "flush"],
800
+ description: "Operation to perform (required). 'local' reads scoped messages from the in-memory session buffer for inter-agent coordination.",
796
801
  },
797
802
  category: {
798
803
  type: "string",
@@ -858,12 +863,14 @@ Include \`knowledge_ref\` to correlate feedback with specific knowledge document
858
863
  3. \`debug(method="show_work", persona_id="abc", workflow_run_id="...")\` - see all actions' execution traces
859
864
  4. \`debug(method="action_detail", persona_id="abc", workflow_run_id="...", action_name="...")\` - deep trace
860
865
 
861
- ## Dashboard personas (no conversations)
862
- Dashboard personas do NOT create conversations. Skip steps 1-2:
863
- 1. Get \`workflow_run_id\` from the dashboard UI debug view
866
+ ## Dashboard personas
867
+ 1. \`debug(method="workflow_runs", persona_id="abc")\` - list recent workflow runs (dashboard rows)
864
868
  2. \`debug(method="show_work", persona_id="abc", workflow_run_id="...")\` - see execution traces
865
869
  3. \`debug(method="action_detail", persona_id="abc", workflow_run_id="...", action_name="...")\` - deep trace
866
870
 
871
+ ## Auto-routing
872
+ If you pass a workflow_run_id to \`conversation_detail\`, it auto-routes to \`show_work\`. \`show_work\` also accepts \`conversation_id\` as an alias for \`workflow_run_id\`.
873
+
867
874
  ## Also available as persona sub-resource
868
875
  - \`persona(id="abc", debug={method:"conversations"})\` - same as debug tool but scoped to persona`,
869
876
  inputSchema: {
@@ -871,20 +878,20 @@ Dashboard personas do NOT create conversations. Skip steps 1-2:
871
878
  properties: {
872
879
  method: {
873
880
  type: "string",
874
- enum: ["conversations", "conversation_detail", "show_work", "action_detail"],
881
+ enum: ["conversations", "conversation_detail", "show_work", "action_detail", "workflow_runs", "search"],
875
882
  description: "Operation to perform (required)",
876
883
  },
877
884
  persona_id: {
878
885
  type: "string",
879
- description: "Persona ID (required for conversations, show_work, action_detail, search)",
886
+ description: "Persona ID (required for conversations, workflow_runs, show_work, action_detail, search)",
880
887
  },
881
888
  conversation_id: {
882
889
  type: "string",
883
- description: "Conversation ID (for method=conversation_detail, or to scope search)",
890
+ description: "Conversation ID (for method=conversation_detail, or to scope search). Also accepted by show_work as alias for workflow_run_id.",
884
891
  },
885
892
  workflow_run_id: {
886
893
  type: "string",
887
- description: "Workflow run ID (for method=show_work, action_detail)",
894
+ description: "Workflow run ID (for method=show_work, action_detail). Auto-discovered via workflow_runs for dashboard personas.",
888
895
  },
889
896
  action_name: {
890
897
  type: "string",
@@ -1028,13 +1028,13 @@ export class EmaClient {
1028
1028
  * that define pre-configured AI Employee configurations.
1029
1029
  */
1030
1030
  async getPersonaTemplates() {
1031
- // Try GET first (newer API), fall back to POST
1032
- let resp = await this.requestWithRetries("GET", "/api/personas/get_persona_templates", {});
1033
- if (resp.status === 405) {
1034
- // Fall back to POST
1035
- resp = await this.requestWithRetries("POST", "/api/personas/get_persona_templates", {
1036
- json: {},
1037
- });
1031
+ // POST first (original, frontend still uses), GET fallback (planned replacement).
1032
+ // Once backend drops POST, GET takes over automatically.
1033
+ let resp = await this.requestWithRetries("POST", "/api/personas/get_persona_templates", {
1034
+ json: {},
1035
+ });
1036
+ if (!resp.ok || resp.status === 405) {
1037
+ resp = await this.requestWithRetries("GET", "/api/personas/get_persona_templates", {});
1038
1038
  }
1039
1039
  if (!resp.ok) {
1040
1040
  throw new EmaApiError({
@@ -293,9 +293,24 @@ export class EmaClientV2 {
293
293
  // Persona Templates - REST API
294
294
  // ─────────────────────────────────────────────────────────────────────────────
295
295
  /**
296
- * List all persona templates available to the tenant
296
+ * List all persona templates available to the tenant.
297
+ *
298
+ * Backend has both POST (original, frontend still uses) and GET (planned
299
+ * replacement — TODO in backend to drop POST once frontend migrates).
300
+ * POST first since GET is not yet deployed everywhere; GET as fallback
301
+ * so we're ready when POST is eventually removed.
297
302
  */
298
303
  async listPersonaTemplates() {
304
+ try {
305
+ const result = await api.listPersonaTemplatesPost({ client: this.restClient });
306
+ const response = result.data;
307
+ const templates = response?.templates ?? [];
308
+ if (templates.length > 0)
309
+ return templates;
310
+ }
311
+ catch {
312
+ // POST failed (removed or 405) — fall through to GET
313
+ }
299
314
  const result = await api.listPersonaTemplates({ client: this.restClient });
300
315
  const response = result.data;
301
316
  return response?.templates ?? [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "2026.3.24",
3
+ "version": "2026.3.25-2",
4
4
  "description": "Ema AI Employee toolkit - MCP server, CLI, and SDK for managing AI Employees across environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",