@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.
- package/.context/public/guides/ema-user-guide.md +12 -16
- package/.context/public/guides/mcp-tools-guide.md +203 -334
- package/LICENSE +29 -21
- package/README.md +58 -35
- package/dist/mcp/domain/loop-detection.js +97 -0
- package/dist/mcp/domain/proto-constraints.js +284 -0
- package/dist/mcp/domain/structural-rules.js +12 -5
- package/dist/mcp/domain/validation-rules.js +107 -20
- package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
- package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
- package/dist/mcp/domain/workflow-graph.js +374 -0
- package/dist/mcp/domain/workflow-optimizer.js +10 -4
- package/dist/mcp/guidance.js +54 -31
- package/dist/mcp/handlers/feedback/index.js +139 -0
- package/dist/mcp/handlers/feedback/store.js +262 -0
- package/dist/mcp/handlers/persona/index.js +237 -8
- package/dist/mcp/handlers/persona/schema.js +27 -0
- package/dist/mcp/handlers/reference/index.js +6 -4
- package/dist/mcp/handlers/workflow/index.js +25 -28
- package/dist/mcp/handlers/workflow/optimize.js +73 -33
- package/dist/mcp/handlers/workflow/validation.js +1 -1
- package/dist/mcp/knowledge-types.js +7 -0
- package/dist/mcp/knowledge.js +146 -834
- package/dist/mcp/resources.js +610 -18
- package/dist/mcp/server.js +233 -2156
- package/dist/mcp/tools.js +91 -5
- package/dist/sdk/generated/agent-catalog.js +615 -0
- package/dist/sdk/generated/deprecated-actions.js +182 -96
- package/dist/sdk/generated/proto-fields.js +2 -1
- package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
- package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
- package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
- package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
- package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
- package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
- package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
- package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
- package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
- package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
- package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
- package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
- package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
- package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
- package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
- package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
- package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
- package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
- package/dist/sdk/generated/widget-catalog.js +60 -0
- package/docs/README.md +17 -9
- package/package.json +2 -2
- package/.context/public/guides/dashboard-operations.md +0 -286
- package/.context/public/guides/email-patterns.md +0 -125
- package/dist/mcp/domain/intent-architect.js +0 -914
- package/dist/mcp/domain/quality-gates.js +0 -110
- package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
- package/dist/mcp/domain/workflow-intent.js +0 -1806
- package/dist/mcp/domain/workflow-merge.js +0 -449
- package/dist/mcp/domain/workflow-tracer.js +0 -648
- package/dist/mcp/domain/workflow-transformer.js +0 -742
- package/dist/mcp/handlers/persona/intent.js +0 -141
- package/dist/mcp/handlers/workflow/analyze.js +0 -119
- package/dist/mcp/handlers/workflow/compare.js +0 -70
- package/dist/mcp/handlers/workflow/generate.js +0 -384
- package/dist/mcp/handlers-consolidated.js +0 -333
package/LICENSE
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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`
|
|
183
|
-
| `<env>`
|
|
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
|
|
206
|
-
|
|
207
|
-
| `persona`
|
|
208
|
-
| `data`
|
|
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`
|
|
211
|
-
| `knowledge` | Data sources (legacy, use `data`)
|
|
212
|
-
| `action`
|
|
213
|
-
| `template`
|
|
214
|
-
| `env`
|
|
215
|
-
| `demo`
|
|
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(
|
|
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")
|
|
244
|
-
catalog(type="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
|
|
250
|
-
|
|
251
|
-
| `ema://catalog/agents`
|
|
252
|
-
| `ema://catalog/templates`
|
|
253
|
-
| `ema://catalog/patterns`
|
|
254
|
-
| `ema://docs/usage-guide`
|
|
255
|
-
| `ema://guidance/rules`
|
|
256
|
-
| `ema://guidance/cursor-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
|
|
325
|
-
|
|
326
|
-
| `EMA_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`
|
|
329
|
-
| `EMA_<ENV>_API_KEY`
|
|
330
|
-
| `EMA_ENV_NAME`
|
|
331
|
-
| `EMA_AGENT_SYNC_CONFIG`
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|