@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.

Files changed (67) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/LICENSE +29 -21
  4. package/README.md +58 -35
  5. package/dist/mcp/domain/loop-detection.js +97 -0
  6. package/dist/mcp/domain/proto-constraints.js +284 -0
  7. package/dist/mcp/domain/structural-rules.js +12 -5
  8. package/dist/mcp/domain/validation-rules.js +107 -20
  9. package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
  10. package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
  11. package/dist/mcp/domain/workflow-graph.js +374 -0
  12. package/dist/mcp/domain/workflow-optimizer.js +10 -4
  13. package/dist/mcp/guidance.js +54 -31
  14. package/dist/mcp/handlers/feedback/index.js +139 -0
  15. package/dist/mcp/handlers/feedback/store.js +262 -0
  16. package/dist/mcp/handlers/persona/index.js +237 -8
  17. package/dist/mcp/handlers/persona/schema.js +27 -0
  18. package/dist/mcp/handlers/reference/index.js +6 -4
  19. package/dist/mcp/handlers/workflow/index.js +25 -28
  20. package/dist/mcp/handlers/workflow/optimize.js +73 -33
  21. package/dist/mcp/handlers/workflow/validation.js +1 -1
  22. package/dist/mcp/knowledge-types.js +7 -0
  23. package/dist/mcp/knowledge.js +146 -834
  24. package/dist/mcp/resources.js +610 -18
  25. package/dist/mcp/server.js +233 -2156
  26. package/dist/mcp/tools.js +91 -5
  27. package/dist/sdk/generated/agent-catalog.js +615 -0
  28. package/dist/sdk/generated/deprecated-actions.js +182 -96
  29. package/dist/sdk/generated/proto-fields.js +2 -1
  30. package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
  31. package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
  32. package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
  33. package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
  34. package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
  35. package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
  36. package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
  37. package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
  38. package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
  39. package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
  40. package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
  41. package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
  42. package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
  43. package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
  44. package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
  45. package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
  46. package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
  47. package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
  48. package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
  49. package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
  50. package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
  51. package/dist/sdk/generated/widget-catalog.js +60 -0
  52. package/docs/README.md +17 -9
  53. package/package.json +2 -2
  54. package/.context/public/guides/dashboard-operations.md +0 -286
  55. package/.context/public/guides/email-patterns.md +0 -125
  56. package/dist/mcp/domain/intent-architect.js +0 -914
  57. package/dist/mcp/domain/quality-gates.js +0 -110
  58. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  59. package/dist/mcp/domain/workflow-intent.js +0 -1806
  60. package/dist/mcp/domain/workflow-merge.js +0 -449
  61. package/dist/mcp/domain/workflow-tracer.js +0 -648
  62. package/dist/mcp/domain/workflow-transformer.js +0 -742
  63. package/dist/mcp/handlers/persona/intent.js +0 -141
  64. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  65. package/dist/mcp/handlers/workflow/compare.js +0 -70
  66. package/dist/mcp/handlers/workflow/generate.js +0 -384
  67. package/dist/mcp/handlers-consolidated.js +0 -333
package/LICENSE CHANGED
@@ -1,21 +1,29 @@
1
- MIT License
2
-
3
- Copyright (c) 2024-2025 Ema Inc.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 Ema Unlimited, Inc.
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published
8
+ by the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ -----------------------------------------------------------------------
20
+
21
+ This software is offered under a dual-licensing model.
22
+
23
+ You may use this software under the terms of the GNU Affero General
24
+ Public License (AGPL) v3, or you may obtain a commercial license
25
+ from Ema Unlimited, Inc.
26
+
27
+ For commercial licensing inquiries, contact:
28
+
29
+ legal@ema.co or steven.poitras@ema.co
package/README.md CHANGED
@@ -76,7 +76,8 @@ Then reload: `source ~/.zshrc`
76
76
  }
77
77
  ```
78
78
 
79
- > **Important**:
79
+ > **Important**:
80
+ >
80
81
  > - Use `@latest` to always get the newest version (npx caches aggressively without it)
81
82
  > - The `-y` flag auto-confirms the npx install prompt
82
83
  > - Use `${env:VAR_NAME}` syntax to reference shell environment variables
@@ -119,6 +120,7 @@ Then reload: `source ~/.zshrc`
119
120
  ### 4. Verify It Works
120
121
 
121
122
  Ask your AI assistant:
123
+
122
124
  - "List ema environments" - should show your configured environments
123
125
  - "List personas in demo" - should show AI Employees
124
126
 
@@ -172,15 +174,16 @@ export EMA_DEV_BEARER_TOKEN="..." # → creates "dev" environment
172
174
  ```
173
175
 
174
176
  **Default Environment**:
177
+
175
178
  - Set explicitly: `export EMA_ENV_NAME="demo"`
176
179
  - Or auto-detected from available credentials
177
180
 
178
181
  **Well-known URLs** (automatic):
179
182
 
180
- | Environment | URL |
181
- |-------------|-----|
182
- | `prod` | `https://api.ema.co` |
183
- | `<env>` | `https://api.<env>.ema.co` |
183
+ | Environment | URL |
184
+ | ----------- | -------------------------- |
185
+ | `prod` | `https://api.ema.co` |
186
+ | `<env>` | `https://api.<env>.ema.co` |
184
187
 
185
188
  ### Config File (Advanced)
186
189
 
@@ -202,26 +205,33 @@ environments:
202
205
 
203
206
  ## MCP Tools
204
207
 
205
- | Tool | Purpose |
206
- |------|---------|
207
- | `persona` | **Primary**: AI Employee management (create/clone/modify/analyze/sanitize/optimize) |
208
- | `data` | **v2**: Simplified data management (list/upload/delete/sanitize/embedding) |
208
+ | Tool | Purpose |
209
+ | ----------- | ----------------------------------------------------------------------------------- |
210
+ | `persona` | **Primary**: AI Employee management (create/clone/modify/analyze/sanitize/optimize) |
211
+ | `data` | **v2**: Simplified data management (list/upload/delete/sanitize/embedding) |
209
212
  | `reference` | **v2**: All reference info (envs, actions, templates, patterns, concepts, guidance) |
210
- | `sync` | Sync across environments |
211
- | `knowledge` | Data sources (legacy, use `data`) |
212
- | `action` | Agent lookup (legacy, use `reference(type="actions")`) |
213
- | `template` | Patterns (legacy, use `reference`) |
214
- | `env` | Environments (legacy, use `reference(type="envs")`) |
215
- | `demo` | Demo/RAG document utilities |
213
+ | `sync` | Sync across environments |
214
+ | `knowledge` | Data sources (legacy, use `data`) |
215
+ | `action` | Agent lookup (legacy, use `reference(type="actions")`) |
216
+ | `template` | Patterns (legacy, use `reference`) |
217
+ | `env` | Environments (legacy, use `reference(type="envs")`) |
218
+ | `demo` | Demo/RAG document utilities |
216
219
 
217
220
  ### Common Workflows
218
221
 
219
222
  **Create new AI Employee:**
223
+
220
224
  ```typescript
221
- persona(input="IT helpdesk with KB search", type="chat", name="IT Support", preview=false)
225
+ persona(
226
+ (input = "IT helpdesk with KB search"),
227
+ (type = "chat"),
228
+ (name = "IT Support"),
229
+ (preview = false),
230
+ );
222
231
  ```
223
232
 
224
233
  **Modify existing AI Employee (LLM-Driven):**
234
+
225
235
  ```typescript
226
236
  // Step 1: Get current workflow
227
237
  workflow(mode="get", persona_id="abc-123")
@@ -239,22 +249,23 @@ workflow(mode="deploy", persona_id="abc-123", base_fingerprint="<fingerprint>",
239
249
  ```
240
250
 
241
251
  **Get reference info:**
252
+
242
253
  ```typescript
243
- catalog(type="actions", id="send_email") // Get action details
244
- catalog(type="templates") // List templates
254
+ catalog((type = "actions"), (id = "send_email")); // Get action details
255
+ catalog((type = "templates")); // List templates
245
256
  ```
246
257
 
247
258
  ## Dynamic Resources
248
259
 
249
- | Resource | Purpose |
250
- |----------|---------|
251
- | `ema://catalog/agents` | Live action catalog from API |
252
- | `ema://catalog/templates` | Persona templates from API |
253
- | `ema://catalog/patterns` | Workflow patterns |
254
- | `ema://docs/usage-guide` | Complete usage guide (generated) |
255
- | `ema://guidance/rules` | Structured rules as JSON |
256
- | `ema://guidance/cursor-rule` | Export as Cursor .mdc rule |
257
- | `ema://guidance/server-instructions` | Server instructions text |
260
+ | Resource | Purpose |
261
+ | ------------------------------------ | -------------------------------- |
262
+ | `ema://catalog/agents` | Live action catalog from API |
263
+ | `ema://catalog/templates` | Persona templates from API |
264
+ | `ema://catalog/patterns` | Workflow patterns |
265
+ | `ema://docs/usage-guide` | Complete usage guide (generated) |
266
+ | `ema://guidance/rules` | Structured rules as JSON |
267
+ | `ema://guidance/cursor-rule` | Export as Cursor .mdc rule |
268
+ | `ema://guidance/server-instructions` | Server instructions text |
258
269
 
259
270
  ---
260
271
 
@@ -263,6 +274,7 @@ catalog(type="templates") // List templates
263
274
  The MCP is fully self-contained—no external configuration files needed.
264
275
 
265
276
  **How it works:**
277
+
266
278
  - Server instructions are injected on MCP init (system prompt)
267
279
  - Tools include usage tips in their descriptions
268
280
  - Responses include `_tip` and `_next_step` fields with contextual guidance
@@ -270,6 +282,7 @@ The MCP is fully self-contained—no external configuration files needed.
270
282
  - `persona(..., analyze=true)` includes workflow_guidance with state-specific tips
271
283
 
272
284
  **For other services:**
285
+
273
286
  - Read `ema://guidance/rules` (JSON) for programmatic consumption
274
287
  - Read `ema://docs/usage-guide` (markdown) for documentation
275
288
  - Read `ema://guidance/cursor-rule` (.mdc) for IDE integration
@@ -321,14 +334,14 @@ const actions = await client.listActions();
321
334
 
322
335
  ## Environment Variables
323
336
 
324
- | Variable | Description |
325
- |----------|-------------|
326
- | `EMA_BEARER_TOKEN` | Default bearer token |
337
+ | Variable | Description |
338
+ | ------------------------ | --------------------------------------------------------- |
339
+ | `EMA_BEARER_TOKEN` | Default bearer token |
327
340
  | `EMA_<ENV>_BEARER_TOKEN` | Token for specific environment (PROD, DEMO, DEV, STAGING) |
328
- | `EMA_API_KEY` | API key (auto-exchanges for JWT) |
329
- | `EMA_<ENV>_API_KEY` | API key for specific environment |
330
- | `EMA_ENV_NAME` | Default environment name |
331
- | `EMA_AGENT_SYNC_CONFIG` | Path to config file |
341
+ | `EMA_API_KEY` | API key (auto-exchanges for JWT) |
342
+ | `EMA_<ENV>_API_KEY` | API key for specific environment |
343
+ | `EMA_ENV_NAME` | Default environment name |
344
+ | `EMA_AGENT_SYNC_CONFIG` | Path to config file |
332
345
 
333
346
  ---
334
347
 
@@ -346,6 +359,7 @@ The token isn't set or isn't being passed to the MCP server:
346
359
  ### "Invalid API key" error
347
360
 
348
361
  You're using a bearer token (JWT) with an `_API_KEY` variable name:
362
+
349
363
  - Rename `EMA_X_API_KEY` to `EMA_X_BEARER_TOKEN`
350
364
 
351
365
  ### Only some environments detected
@@ -375,4 +389,13 @@ Some Cursor versions have issues with env var expansion. Workaround: paste token
375
389
 
376
390
  ## License
377
391
 
378
- MIT - see [LICENSE](LICENSE)
392
+ This project is dual-licensed:
393
+
394
+ - **AGPLv3** for open-source use
395
+ - **Commercial license** for proprietary, hosted, or closed-source use
396
+
397
+ If you intend to run this software in production, expose it over a
398
+ network, or integrate it into a proprietary system, you must either
399
+ comply with AGPLv3 or obtain a commercial license.
400
+
401
+ See `LICENSE` and `LICENSE-COMMERCIAL.md` for details.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Loop Detection for Workflow Graphs
3
+ *
4
+ * Extracted from workflow-execution-analyzer.ts — only the parts
5
+ * used by live validation code (detectLoops + LoopInfo).
6
+ */
7
+ import { parseWorkflowDef } from "../knowledge.js";
8
+ function buildGraphMaps(nodes) {
9
+ const forward = new Map();
10
+ const nodeMap = new Map();
11
+ for (const node of nodes) {
12
+ nodeMap.set(node.id, node);
13
+ forward.set(node.id, new Set());
14
+ }
15
+ for (const node of nodes) {
16
+ if (node.incoming_edges) {
17
+ for (const edge of node.incoming_edges) {
18
+ const sourceId = edge.source_node_id;
19
+ forward.get(sourceId)?.add(node.id);
20
+ }
21
+ }
22
+ }
23
+ return { forward, nodeMap };
24
+ }
25
+ export function detectLoops(workflowDef) {
26
+ const nodes = parseWorkflowDef(workflowDef);
27
+ const loops = [];
28
+ const { forward, nodeMap } = buildGraphMaps(nodes);
29
+ const visited = new Set();
30
+ const recursionStack = new Set();
31
+ const path = [];
32
+ function dfs(nodeId) {
33
+ visited.add(nodeId);
34
+ recursionStack.add(nodeId);
35
+ path.push(nodeId);
36
+ for (const neighbor of (forward.get(nodeId) || [])) {
37
+ if (!visited.has(neighbor)) {
38
+ if (dfs(neighbor))
39
+ return true;
40
+ }
41
+ else if (recursionStack.has(neighbor)) {
42
+ const cycleStart = path.indexOf(neighbor);
43
+ const cyclePath = path.slice(cycleStart);
44
+ cyclePath.push(neighbor);
45
+ loops.push({
46
+ type: 'circular_dependency',
47
+ nodes: cyclePath,
48
+ description: `Circular dependency: ${cyclePath.join(' → ')}`,
49
+ severity: 'critical',
50
+ fixSuggestion: `Break the cycle by removing one edge.`,
51
+ });
52
+ return true;
53
+ }
54
+ }
55
+ path.pop();
56
+ recursionStack.delete(nodeId);
57
+ return false;
58
+ }
59
+ const trigger = nodes.find(n => n.action_name === 'trigger' || n.id === 'trigger');
60
+ if (trigger)
61
+ dfs(trigger.id);
62
+ for (const node of nodes) {
63
+ if (!visited.has(node.id))
64
+ dfs(node.id);
65
+ }
66
+ // Detect categorizer routing back to upstream (re-entry)
67
+ for (const node of nodes) {
68
+ if (node.action_name === 'chat_categorizer') {
69
+ const downstream = forward.get(node.id) || new Set();
70
+ const upstream = new Set();
71
+ function findUpstream(nId, depth = 0) {
72
+ if (depth > 20)
73
+ return;
74
+ const nodeInfo = nodeMap.get(nId);
75
+ if (!nodeInfo?.incoming_edges)
76
+ return;
77
+ for (const edge of nodeInfo.incoming_edges) {
78
+ upstream.add(edge.source_node_id);
79
+ findUpstream(edge.source_node_id, depth + 1);
80
+ }
81
+ }
82
+ findUpstream(node.id);
83
+ for (const downNode of downstream) {
84
+ if (upstream.has(downNode)) {
85
+ loops.push({
86
+ type: 'reentry_loop',
87
+ nodes: [node.id, downNode],
88
+ description: `Categorizer "${node.display_name || node.id}" routes to "${downNode}" which is upstream - can cause re-processing`,
89
+ severity: 'warning',
90
+ fixSuggestion: `Add state tracking or one-time-execution gate to prevent re-processing.`,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return loops;
97
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Proto Constraint Miner
3
+ *
4
+ * Scans generated proto TypeScript files (`*_pb.ts`) for JSDoc comments
5
+ * containing constraint language (immutable, read-only, creation-only, etc.)
6
+ * and produces a structured report of field-level constraints.
7
+ *
8
+ * Used by:
9
+ * - `ema://docs/field-constraints` MCP resource (runtime)
10
+ * - `scripts/mine-proto-constraints.ts` CLI tool (ad-hoc)
11
+ */
12
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
13
+ import { join, relative, dirname } from "path";
14
+ import { fileURLToPath } from "url";
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ // Default location of generated proto files relative to this module
18
+ const DEFAULT_PROTOS_DIR = join(__dirname, "..", "..", "sdk", "generated", "protos");
19
+ // ---------------------------------------------------------------------------
20
+ // Constraint patterns to search for in JSDoc comments
21
+ // ---------------------------------------------------------------------------
22
+ const CONSTRAINT_PATTERNS = [
23
+ {
24
+ label: "immutable",
25
+ pattern: /\bimmutable\b|cannot be (updated|modified|changed|edited|deleted)\b/i,
26
+ },
27
+ {
28
+ label: "creation-only",
29
+ pattern: /should not be populated in creation|only (set|populated|used) (at|during|on|for) creation\b/i,
30
+ },
31
+ {
32
+ label: "read-only",
33
+ pattern: /\bread[- ]?only\b|not editable\b|cannot edit\b/i,
34
+ },
35
+ {
36
+ label: "inferred",
37
+ pattern: /will be inferred\b|automatically (set|populated|filled|computed)\b/i,
38
+ },
39
+ {
40
+ label: "deprecated",
41
+ pattern: /\[deprecated\s*=\s*true\]|@deprecated\b/i,
42
+ },
43
+ {
44
+ label: "conditional",
45
+ pattern: /only (set|used|populated|valid) (for|when|if|during)\b/i,
46
+ },
47
+ {
48
+ label: "do-not-populate",
49
+ pattern: /should not be populated\b|do not (set|populate|fill)\b/i,
50
+ },
51
+ ];
52
+ // ---------------------------------------------------------------------------
53
+ // Internal helpers
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * Walk a directory recursively and yield file paths matching a predicate.
57
+ */
58
+ function walkFiles(dir, predicate) {
59
+ const results = [];
60
+ for (const entry of readdirSync(dir)) {
61
+ const full = join(dir, entry);
62
+ const stat = statSync(full);
63
+ if (stat.isDirectory()) {
64
+ results.push(...walkFiles(full, predicate));
65
+ }
66
+ else if (predicate(full)) {
67
+ results.push(full);
68
+ }
69
+ }
70
+ return results;
71
+ }
72
+ /**
73
+ * Extract field identity from a `@generated from field:` annotation.
74
+ *
75
+ * The annotation format is:
76
+ * `@generated from field: <type> <field_name> = <field_number>`
77
+ * The message context comes from the enclosing `Message<"...">` declaration.
78
+ */
79
+ function parseGeneratedField(comment) {
80
+ // Field annotations: `@generated from field: <type> <name> = <number>`
81
+ const fieldMatch = comment.match(/@generated from field:\s*(?:optional\s+|repeated\s+)?(?:\S+\s+)?(\S+)\s*=\s*\d+/);
82
+ if (fieldMatch) {
83
+ const fullPath = fieldMatch[1];
84
+ const lastDot = fullPath.lastIndexOf(".");
85
+ if (lastDot > 0) {
86
+ return {
87
+ message: fullPath.substring(0, lastDot),
88
+ field: fullPath.substring(lastDot + 1),
89
+ };
90
+ }
91
+ return { message: "(unknown)", field: fullPath };
92
+ }
93
+ // RPC annotations: `@generated from rpc <service>.<method>`
94
+ const rpcMatch = comment.match(/@generated from rpc\s+(\S+)\.(\w+)/);
95
+ if (rpcMatch) {
96
+ return { message: rpcMatch[1], field: rpcMatch[2] };
97
+ }
98
+ // Message annotations: `@generated from message <name>`
99
+ const msgMatch = comment.match(/@generated from message\s+(\S+)/);
100
+ if (msgMatch) {
101
+ return { message: msgMatch[1], field: "(message)" };
102
+ }
103
+ // Enum value annotations: `@generated from enum value: <name> = <number>`
104
+ const enumMatch = comment.match(/@generated from enum value:\s+(\S+)\s*=\s*\d+/);
105
+ if (enumMatch) {
106
+ const fullPath = enumMatch[1];
107
+ const lastDot = fullPath.lastIndexOf(".");
108
+ if (lastDot > 0) {
109
+ return {
110
+ message: fullPath.substring(0, lastDot),
111
+ field: fullPath.substring(lastDot + 1),
112
+ };
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ /**
118
+ * Scan a single proto TypeScript file for constraint comments.
119
+ *
120
+ * Tracks the enclosing `Message<"...">` declaration to resolve field paths,
121
+ * since `@generated from field:` only contains `<type> <field_name> = <number>`,
122
+ * not the full qualified message path.
123
+ */
124
+ function scanFile(filePath, protosDir) {
125
+ const content = readFileSync(filePath, "utf-8");
126
+ const lines = content.split("\n");
127
+ const constraints = [];
128
+ const relPath = relative(protosDir, filePath);
129
+ // Track the current enclosing message
130
+ let currentMessage = "(unknown)";
131
+ const messageContextRe = /Message<"([^"]+)">|@generated from message\s+(\S+)/;
132
+ let inComment = false;
133
+ let commentLines = [];
134
+ let commentStartLine = 0;
135
+ for (let i = 0; i < lines.length; i++) {
136
+ const line = lines[i];
137
+ const trimmed = line.trim();
138
+ // Update message context from non-comment lines
139
+ if (!inComment) {
140
+ const msgMatch = line.match(messageContextRe);
141
+ if (msgMatch) {
142
+ currentMessage = msgMatch[1] ?? msgMatch[2] ?? currentMessage;
143
+ }
144
+ }
145
+ if (trimmed.startsWith("/**")) {
146
+ inComment = true;
147
+ commentLines = [trimmed];
148
+ commentStartLine = i + 1; // 1-indexed
149
+ }
150
+ else if (inComment) {
151
+ commentLines.push(trimmed);
152
+ if (trimmed.includes("*/")) {
153
+ inComment = false;
154
+ const fullComment = commentLines
155
+ .map((l) => l.replace(/^\/?\*+\/?/g, "").trim())
156
+ .filter((l) => l.length > 0)
157
+ .join(" ");
158
+ // Update message context from inline comments
159
+ const inlineMsg = fullComment.match(/@generated from message\s+(\S+)/);
160
+ if (inlineMsg) {
161
+ currentMessage = inlineMsg[1];
162
+ }
163
+ // Check against constraint patterns
164
+ const matchedLabels = [];
165
+ for (const { label, pattern } of CONSTRAINT_PATTERNS) {
166
+ if (pattern.test(fullComment)) {
167
+ matchedLabels.push(label);
168
+ }
169
+ }
170
+ if (matchedLabels.length > 0) {
171
+ const parsed = parseGeneratedField(fullComment);
172
+ const afterComment = lines.slice(i + 1, i + 5).join(" ");
173
+ const parsedAfter = parsed ?? parseGeneratedField(afterComment);
174
+ if (parsedAfter) {
175
+ const message = parsedAfter.message === "(unknown)"
176
+ ? currentMessage
177
+ : parsedAfter.message;
178
+ constraints.push({
179
+ message,
180
+ field: parsedAfter.field,
181
+ labels: matchedLabels,
182
+ comment: fullComment.substring(0, 500),
183
+ source: relPath,
184
+ line: commentStartLine,
185
+ });
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ return constraints;
192
+ }
193
+ // ---------------------------------------------------------------------------
194
+ // Public API
195
+ // ---------------------------------------------------------------------------
196
+ /**
197
+ * Mine all constraint comments from generated proto TypeScript files.
198
+ *
199
+ * @param protosDir - Override the directory to scan (defaults to `src/sdk/generated/protos/`)
200
+ */
201
+ export function mineConstraints(protosDir) {
202
+ const dir = protosDir ?? DEFAULT_PROTOS_DIR;
203
+ if (!existsSync(dir)) {
204
+ return {
205
+ generated_at: new Date().toISOString(),
206
+ total_constraints: 0,
207
+ by_label: {},
208
+ constraints: [],
209
+ };
210
+ }
211
+ const files = walkFiles(dir, (f) => f.endsWith("_pb.ts"));
212
+ const allConstraints = [];
213
+ for (const file of files) {
214
+ allConstraints.push(...scanFile(file, dir));
215
+ }
216
+ // Sort by message then field for stable output
217
+ allConstraints.sort((a, b) => a.message === b.message
218
+ ? a.field.localeCompare(b.field)
219
+ : a.message.localeCompare(b.message));
220
+ // Count by label
221
+ const byLabel = {};
222
+ for (const c of allConstraints) {
223
+ for (const label of c.labels) {
224
+ byLabel[label] = (byLabel[label] ?? 0) + 1;
225
+ }
226
+ }
227
+ return {
228
+ generated_at: new Date().toISOString(),
229
+ total_constraints: allConstraints.length,
230
+ by_label: byLabel,
231
+ constraints: allConstraints,
232
+ };
233
+ }
234
+ /**
235
+ * Format a constraint report as human-readable markdown for the MCP resource.
236
+ * Filters out deprecated-only entries (those are in `ema://rules/deprecated-actions`).
237
+ */
238
+ export function formatConstraintReport(report) {
239
+ // Filter to non-deprecated constraints (or constraints with labels beyond just deprecated)
240
+ const meaningful = report.constraints.filter((c) => c.labels.length > 1 ||
241
+ !c.labels.includes("deprecated"));
242
+ const sections = [
243
+ `# Proto Field Constraints`,
244
+ ``,
245
+ `> Auto-generated from proto definitions. ${meaningful.length} field constraints found (deprecated-only fields filtered out; see \`ema://rules/deprecated-actions\` for those).`,
246
+ ``,
247
+ `## Summary`,
248
+ ``,
249
+ `| Category | Count | Description |`,
250
+ `|----------|-------|-------------|`,
251
+ `| immutable | ${report.by_label["immutable"] ?? 0} | Fields that cannot be updated after creation |`,
252
+ `| creation-only | ${report.by_label["creation-only"] ?? 0} | Fields that should not be set in creation requests (auto-populated) |`,
253
+ `| read-only | ${report.by_label["read-only"] ?? 0} | Fields that cannot be edited by users |`,
254
+ `| inferred | ${report.by_label["inferred"] ?? 0} | Fields automatically computed from other data |`,
255
+ `| do-not-populate | ${report.by_label["do-not-populate"] ?? 0} | Fields that should not be set by callers |`,
256
+ `| conditional | ${report.by_label["conditional"] ?? 0} | Fields only used in specific contexts |`,
257
+ ``,
258
+ ];
259
+ // Group by category (excluding deprecated-only)
260
+ const byCategory = new Map();
261
+ for (const c of meaningful) {
262
+ for (const label of c.labels) {
263
+ if (label === "deprecated")
264
+ continue;
265
+ const list = byCategory.get(label) ?? [];
266
+ list.push(c);
267
+ byCategory.set(label, list);
268
+ }
269
+ }
270
+ for (const [category, items] of byCategory) {
271
+ sections.push(`## ${category}`);
272
+ sections.push(``);
273
+ for (const item of items) {
274
+ const labels = item.labels.filter((l) => l !== "deprecated").join(", ");
275
+ sections.push(`### \`${item.message}.${item.field}\` [${labels}]`);
276
+ sections.push(``);
277
+ sections.push(`${item.comment.substring(0, 300)}`);
278
+ sections.push(``);
279
+ sections.push(`_Source: ${item.source}:${item.line}_`);
280
+ sections.push(``);
281
+ }
282
+ }
283
+ return sections.join("\n");
284
+ }
@@ -2,9 +2,8 @@
2
2
  * Structural Rules for LLM Context
3
3
  *
4
4
  * These rules encode the validation logic from:
5
- * - workflow-fixer.ts
6
- * - workflow-execution-analyzer.ts
7
5
  * - knowledge.ts (detectWorkflowIssues)
6
+ * - loop-detection.ts (cycle detection, extracted from workflow-execution-analyzer.ts)
8
7
  *
9
8
  * PURPOSE: Feed these to the LLM so it can self-validate during generation/transformation.
10
9
  * This is the "teach the LLM the rules" approach vs "fix after the fact".
@@ -105,9 +104,9 @@ export const STRUCTURAL_INVARIANTS = [
105
104
  {
106
105
  id: "hitl_has_both_paths",
107
106
  name: "HITL Must Have Success AND Failure Paths",
108
- rule: "A general_hitl node has two outcomes: approval and rejection. Both MUST have downstream handlers.",
107
+ rule: "Legacy general_hitl nodes (if present) have two outcomes: approval and rejection. Both MUST have downstream handlers. Note: general_hitl is NOT deployable for new workflows — HITL is a flag on entity_extraction_with_documents and send_email_agent only.",
109
108
  violation: "HITL 'approval' only has success path - rejections hang",
110
- fix: "Add handler for hitl.approval_decision = 'reject' (typically fixed_response with apology)",
109
+ fix: "For legacy workflows: add handler for hitl.approval_decision = 'reject'. For new workflows: use HITL flag on send_email_agent or entity_extraction_with_documents instead.",
111
110
  severity: "critical",
112
111
  },
113
112
  // ═══════════════════════════════════════════════════════════════════════════
@@ -180,6 +179,14 @@ export const STRUCTURAL_INVARIANTS = [
180
179
  fix: "Connect the output to a downstream node, map to WORKFLOW_OUTPUT, or remove the node if unused",
181
180
  severity: "warning",
182
181
  },
182
+ {
183
+ id: "all_gated_responses_wired",
184
+ name: "All Gated Response Nodes Must Reach Output",
185
+ rule: "When a categorizer gates multiple response nodes via runIf/trigger_when, EVERY gated response node must have its output mapped to WORKFLOW_OUTPUT. A gated response node that executes but isn't wired to output silently discards its result.",
186
+ violation: "Categorizer routes to get_respond, schedule_respond, reschedule_respond — but only get_respond.response_with_sources is mapped to WORKFLOW_OUTPUT. Schedule and reschedule responses are silently lost.",
187
+ fix: "Map ALL gated response node outputs to WORKFLOW_OUTPUT, or consolidate into a single response node that receives the category as a named_input",
188
+ severity: "critical",
189
+ },
183
190
  ];
184
191
  export const EXECUTION_RULES = [
185
192
  {
@@ -330,7 +337,7 @@ BEFORE finalizing any workflow modification, verify these rules:
330
337
 
331
338
  ### HITL Rules
332
339
 
333
- 10. **Both Paths Required**: general_hitl needs handlers for both approval AND rejection.
340
+ 10. **Both Paths Required**: Legacy general_hitl nodes need handlers for both approval AND rejection. Note: general_hitl is NOT deployable — HITL is a flag on entity_extraction_with_documents and send_email_agent only.
334
341
 
335
342
  ### Raw workflow_def Format Rules (CRITICAL)
336
343