@ema.co/mcp-toolkit 1.6.0 → 1.7.1
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/README.md +2 -2
- package/dist/mcp/handlers-consolidated.js +608 -9
- package/dist/mcp/resources.js +124 -0
- package/dist/mcp/server.js +12 -2
- package/dist/mcp/tools-consolidated.js +18 -4
- package/dist/sdk/action-schema-parser.js +379 -0
- package/dist/sdk/client.js +735 -0
- package/dist/sdk/index.js +45 -2
- package/dist/sdk/intent-architect.js +883 -0
- package/dist/sdk/sanitizer.js +1121 -0
- package/dist/sdk/workflow-validator.js +221 -3
- package/docs/mcp-tools-guide.md +40 -2
- package/docs/tool-consolidation-v2.md +215 -0
- package/package.json +6 -2
- package/resources/action-schema.json +5678 -0
- package/resources/config/gates.json +88 -0
- package/resources/config/gates.schema.json +77 -0
- package/resources/templates/auto-builder-rules.md +222 -0
- package/resources/templates/demo-scenarios/test-published-package.md +116 -0
- package/docs/.temp/datasource-attach.har +0 -198369
- package/docs/.temp/grpcweb.gar +0 -1
- package/docs/openapi.json +0 -8000
package/dist/mcp/resources.js
CHANGED
|
@@ -25,6 +25,7 @@ import * as path from "path";
|
|
|
25
25
|
import { AGENT_CATALOG, WORKFLOW_PATTERNS, WIDGET_CATALOG } from "../sdk/knowledge.js";
|
|
26
26
|
import { INPUT_SOURCE_RULES, ANTI_PATTERNS, OPTIMIZATION_RULES } from "../sdk/validation-rules.js";
|
|
27
27
|
import { EmaClient } from "../sdk/client.js";
|
|
28
|
+
import { APISchemaRegistry } from "../sdk/workflow-validator.js";
|
|
28
29
|
import { loadConfigFromJsonEnv, loadConfigOptional, resolveBearerToken, getEnvByName, getMasterEnv, } from "../sdk/config.js";
|
|
29
30
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
31
|
// Security Utilities
|
|
@@ -269,6 +270,62 @@ const DYNAMIC_RESOURCES = [
|
|
|
269
270
|
return md;
|
|
270
271
|
},
|
|
271
272
|
},
|
|
273
|
+
// Action Schema - Complete action definitions from ema repo
|
|
274
|
+
{
|
|
275
|
+
uri: "ema://schema/actions",
|
|
276
|
+
name: "schema/actions",
|
|
277
|
+
description: "Complete action schema: all workflow actions with inputs, outputs, types, and documentation from ema_backend/grpc",
|
|
278
|
+
mimeType: "application/json",
|
|
279
|
+
generate: async (ctx) => {
|
|
280
|
+
const schema = await getDynamicActionSchema({ env: ctx.env });
|
|
281
|
+
return JSON.stringify(schema, null, 2);
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
uri: "ema://schema/actions-summary",
|
|
286
|
+
name: "schema/actions-summary",
|
|
287
|
+
description: "Action schema summary: action names, versions, and descriptions for quick reference",
|
|
288
|
+
mimeType: "text/markdown",
|
|
289
|
+
generate: async (ctx) => {
|
|
290
|
+
const schema = await getDynamicActionSchema({ env: ctx.env });
|
|
291
|
+
if (schema.actions.length === 0) {
|
|
292
|
+
return "# Action Schema\n\n> No actions loaded. Check API connectivity or bundled schema.\n";
|
|
293
|
+
}
|
|
294
|
+
let md = "# Action Schema\n\n";
|
|
295
|
+
md += `> ${schema.actions.length} actions loaded (source: ${schema.source})\n`;
|
|
296
|
+
md += `> Version: ${schema.version}\n\n`;
|
|
297
|
+
const byCategory = new Map();
|
|
298
|
+
for (const action of schema.actions) {
|
|
299
|
+
const cat = action.category || "OTHER";
|
|
300
|
+
if (!byCategory.has(cat))
|
|
301
|
+
byCategory.set(cat, []);
|
|
302
|
+
byCategory.get(cat).push(action);
|
|
303
|
+
}
|
|
304
|
+
for (const [category, actions] of byCategory) {
|
|
305
|
+
md += `## ${category.replace("ACTION_CATEGORY_", "")}\n\n`;
|
|
306
|
+
md += "| Action | Version | Description | Inputs | Outputs |\n";
|
|
307
|
+
md += "|--------|---------|-------------|--------|--------|\n";
|
|
308
|
+
for (const action of actions) {
|
|
309
|
+
const inputs = action.inputs?.size || 0;
|
|
310
|
+
const outputs = action.outputs?.size || 0;
|
|
311
|
+
const desc = action.description?.slice(0, 50) || "-";
|
|
312
|
+
md += `| \`${action.name}\` | ${action.version || "v0"} | ${desc}... | ${inputs} | ${outputs} |\n`;
|
|
313
|
+
}
|
|
314
|
+
md += "\n";
|
|
315
|
+
}
|
|
316
|
+
return md;
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
uri: "ema://schema/action/:name",
|
|
321
|
+
name: "schema/action",
|
|
322
|
+
description: "Individual action schema by name (e.g., ema://schema/action/call_llm)",
|
|
323
|
+
mimeType: "application/json",
|
|
324
|
+
generate: async (ctx) => {
|
|
325
|
+
// This is a template - actual resolution happens in getResource
|
|
326
|
+
return JSON.stringify({ error: "Use specific action name in URI" }, null, 2);
|
|
327
|
+
},
|
|
328
|
+
},
|
|
272
329
|
];
|
|
273
330
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
274
331
|
// Dynamic fetching helpers (Ema API as source of truth when available)
|
|
@@ -374,6 +431,56 @@ async function getDynamicPersonaTemplates(opts) {
|
|
|
374
431
|
return [];
|
|
375
432
|
}
|
|
376
433
|
}
|
|
434
|
+
const actionSchemaCache = new Map();
|
|
435
|
+
/**
|
|
436
|
+
* Get action schema with layered loading:
|
|
437
|
+
* 1. Try bundled schema (resources/action-schema.json)
|
|
438
|
+
* 2. Augment with live API data if available
|
|
439
|
+
*/
|
|
440
|
+
async function getDynamicActionSchema(opts) {
|
|
441
|
+
const cacheKey = opts.env ?? "";
|
|
442
|
+
const now = Date.now();
|
|
443
|
+
const cached = actionSchemaCache.get(cacheKey);
|
|
444
|
+
if (cached && now - cached.ts < 60_000)
|
|
445
|
+
return cached.data;
|
|
446
|
+
const registry = new APISchemaRegistry();
|
|
447
|
+
// Try to load from bundled schema first (code source of truth)
|
|
448
|
+
const bundlePath = path.resolve(__dirname, "../../resources/action-schema.json");
|
|
449
|
+
try {
|
|
450
|
+
registry.loadFromBundle(bundlePath);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// Bundle not available - try API
|
|
454
|
+
const client = getClientForEnvName(opts.env);
|
|
455
|
+
if (client) {
|
|
456
|
+
try {
|
|
457
|
+
await registry.load(client);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Neither bundle nor API available
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const response = {
|
|
465
|
+
version: registry.metadata.version ?? "unknown",
|
|
466
|
+
source: registry.metadata.source ?? "bundle",
|
|
467
|
+
actions: registry.getAllActions(),
|
|
468
|
+
};
|
|
469
|
+
actionSchemaCache.set(cacheKey, { ts: now, data: response });
|
|
470
|
+
return response;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get a specific action by name from the schema.
|
|
474
|
+
*/
|
|
475
|
+
async function getActionByName(name, env) {
|
|
476
|
+
const schema = await getDynamicActionSchema({ env });
|
|
477
|
+
// Find latest version of action
|
|
478
|
+
const matching = schema.actions.filter(a => a.name === name);
|
|
479
|
+
if (matching.length === 0)
|
|
480
|
+
return null;
|
|
481
|
+
// Return latest version (sort by version desc)
|
|
482
|
+
return matching.sort((a, b) => (b.version || "v0").localeCompare(a.version || "v0"))[0];
|
|
483
|
+
}
|
|
377
484
|
/**
|
|
378
485
|
* Convert PersonaTemplateDTO to a simplified format for MCP resources.
|
|
379
486
|
*/
|
|
@@ -483,6 +590,23 @@ export class ResourceRegistry {
|
|
|
483
590
|
text: await dynamicResource.generate({ env: envFromQuery }),
|
|
484
591
|
};
|
|
485
592
|
}
|
|
593
|
+
// Handle pattern URIs (e.g., ema://schema/action/:name)
|
|
594
|
+
const actionMatch = baseUri.match(/^ema:\/\/schema\/action\/(.+)$/);
|
|
595
|
+
if (actionMatch) {
|
|
596
|
+
const actionName = actionMatch[1];
|
|
597
|
+
const action = await getActionByName(actionName, envFromQuery);
|
|
598
|
+
if (!action) {
|
|
599
|
+
return {
|
|
600
|
+
code: "NOT_FOUND",
|
|
601
|
+
message: `Action not found: ${actionName}. Use ema://schema/actions-summary to list available actions.`,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
uri: baseUri,
|
|
606
|
+
mimeType: "application/json",
|
|
607
|
+
text: JSON.stringify(action, null, 2),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
486
610
|
// Look up in file-backed allowlist
|
|
487
611
|
const resourceConfig = RESOURCE_MAP[baseUri];
|
|
488
612
|
if (!resourceConfig) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -14,9 +14,19 @@
|
|
|
14
14
|
* - Default environment is set via EMA_ENV_NAME or first in config
|
|
15
15
|
* - Available environments come from sync config (EMA_AGENT_SYNC_CONFIG)
|
|
16
16
|
*/
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
17
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
22
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
// Read package.json for version info
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = dirname(__filename);
|
|
26
|
+
const packageJsonPath = join(__dirname, "..", "..", "package.json");
|
|
27
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
28
|
+
export const TOOLKIT_NAME = packageJson.name;
|
|
29
|
+
export const TOOLKIT_VERSION = packageJson.version;
|
|
20
30
|
// Prompts and Resources
|
|
21
31
|
import { PromptRegistry, isPromptError } from "./prompts.js";
|
|
22
32
|
import { ResourceRegistry, isResourceError } from "./resources.js";
|
|
@@ -4692,7 +4702,7 @@ const toolHandlers = {
|
|
|
4692
4702
|
return handleEnv({}, () => getAvailableEnvironments().map(e => ({
|
|
4693
4703
|
name: e.name,
|
|
4694
4704
|
isDefault: e.name === getDefaultEnvName(),
|
|
4695
|
-
})));
|
|
4705
|
+
})), { name: TOOLKIT_NAME, version: TOOLKIT_VERSION });
|
|
4696
4706
|
},
|
|
4697
4707
|
persona: async (args) => {
|
|
4698
4708
|
const targetEnv = args.env ?? getDefaultEnvName();
|
|
@@ -5278,7 +5288,7 @@ function generateEntityDocument(entityType, entity, related, tags) {
|
|
|
5278
5288
|
const promptRegistry = new PromptRegistry();
|
|
5279
5289
|
const resourceRegistry = new ResourceRegistry();
|
|
5280
5290
|
export async function startMcpServer() {
|
|
5281
|
-
const server = new Server({ name:
|
|
5291
|
+
const server = new Server({ name: TOOLKIT_NAME, version: TOOLKIT_VERSION }, {
|
|
5282
5292
|
capabilities: {
|
|
5283
5293
|
tools: {},
|
|
5284
5294
|
prompts: {},
|
|
@@ -42,7 +42,7 @@ export function generateConsolidatedTools(envNames, defaultEnv) {
|
|
|
42
42
|
// ═══════════════════════════════════════════════════════════════════════
|
|
43
43
|
{
|
|
44
44
|
name: "env",
|
|
45
|
-
description: "List available Ema environments.
|
|
45
|
+
description: "List available Ema environments and toolkit info. Returns environments (with default marker) and toolkit name/version.",
|
|
46
46
|
inputSchema: { type: "object", properties: {}, required: [] },
|
|
47
47
|
},
|
|
48
48
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -185,7 +185,9 @@ For complex workflows, send the prompt to an LLM and deploy:
|
|
|
185
185
|
},
|
|
186
186
|
template_id: { type: "string", description: "Specific template ID (usually auto-selected)" },
|
|
187
187
|
clone_from: { type: "string", description: "Clone from existing persona ID" },
|
|
188
|
-
clone_data: { type: "boolean", description: "Also clone knowledge base files" },
|
|
188
|
+
clone_data: { type: "boolean", description: "Also clone knowledge base files and dashboard rows (auto-enables persona for dashboard cloning)" },
|
|
189
|
+
sanitize: { type: "boolean", description: "Sanitize/obfuscate PII and sensitive data (for demo environments)" },
|
|
190
|
+
sanitize_examples: { type: "array", items: { type: "string" }, description: "Additional items to treat as sensitive (e.g., company names)" },
|
|
189
191
|
enabled: { type: "boolean", description: "Enable/disable persona" },
|
|
190
192
|
// === TEMPLATES ===
|
|
191
193
|
templates: { type: "boolean", description: "List available templates" },
|
|
@@ -427,12 +429,20 @@ Ask the user these questions, then put answers into ONE workflow() call.
|
|
|
427
429
|
|
|
428
430
|
**Attach data source to workflow node**:
|
|
429
431
|
knowledge(persona_id="abc", mode="attach", node_name="knowledge_search_1")
|
|
430
|
-
knowledge(persona_id="abc", mode="attach", node_name="knowledge_search_1", widget_name="fileUpload")
|
|
432
|
+
knowledge(persona_id="abc", mode="attach", node_name="knowledge_search_1", widget_name="fileUpload")
|
|
433
|
+
|
|
434
|
+
**Dashboard rows** (for Dashboard personas):
|
|
435
|
+
knowledge(persona_id="abc", mode="dashboard_rows")
|
|
436
|
+
knowledge(persona_id="abc", mode="dashboard_rows", limit=10)
|
|
437
|
+
|
|
438
|
+
**Clone dashboard data** (copy rows from source to target):
|
|
439
|
+
knowledge(persona_id="target", mode="dashboard_clone", source_persona_id="source")
|
|
440
|
+
knowledge(persona_id="target", mode="dashboard_clone", source_persona_id="source", sanitize=true)`,
|
|
431
441
|
inputSchema: withEnv({
|
|
432
442
|
persona_id: { type: "string", description: "AI Employee ID (required)" },
|
|
433
443
|
mode: {
|
|
434
444
|
type: "string",
|
|
435
|
-
enum: ["list", "aggregates", "upload", "delete", "status", "toggle", "attach"],
|
|
445
|
+
enum: ["list", "aggregates", "upload", "delete", "status", "toggle", "attach", "dashboard_rows", "dashboard_clone"],
|
|
436
446
|
description: "Operation. Default: 'list'"
|
|
437
447
|
},
|
|
438
448
|
// List flags
|
|
@@ -448,6 +458,10 @@ Ask the user these questions, then put answers into ONE workflow() call.
|
|
|
448
458
|
enabled: { type: "boolean", description: "Enable/disable embedding" },
|
|
449
459
|
// Attach flags
|
|
450
460
|
node_name: { type: "string", description: "Workflow node name to attach data source to (e.g., 'knowledge_search_1')" },
|
|
461
|
+
// Dashboard clone flags
|
|
462
|
+
source_persona_id: { type: "string", description: "Source persona ID for dashboard clone" },
|
|
463
|
+
sanitize: { type: "boolean", description: "Sanitize/obfuscate data during clone" },
|
|
464
|
+
sanitize_examples: { type: "array", items: { type: "string" }, description: "Additional sensitive items to sanitize" },
|
|
451
465
|
}, ["persona_id"]),
|
|
452
466
|
},
|
|
453
467
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for Ema action definitions from .txtpb files
|
|
3
|
+
*
|
|
4
|
+
* This parses the Protocol Buffer text format used in ema_backend/grpc/workflow_actions/
|
|
5
|
+
* to extract action schemas for validation.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Parser
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Parse a .txtpb file content into a ParsedAction
|
|
14
|
+
*/
|
|
15
|
+
export function parseTextproto(content, filePath) {
|
|
16
|
+
try {
|
|
17
|
+
// Extract type_name block using brace matching
|
|
18
|
+
const typeNameBlock = extractBlock(content, "type_name");
|
|
19
|
+
// Get the innermost name (action name, not the outer "name" block)
|
|
20
|
+
const nameMatches = [...typeNameBlock.matchAll(/name:\s*"([^"]+)"/g)];
|
|
21
|
+
const name = nameMatches.length > 0 ? nameMatches[nameMatches.length - 1][1] : undefined;
|
|
22
|
+
const version = extractField(typeNameBlock, /version:\s*"([^"]+)"/) || "v0";
|
|
23
|
+
const displayName = extractField(content, /display_name:\s*"([^"]+)"/);
|
|
24
|
+
const description = extractField(content, /description:\s*"([^"]+)"/) ||
|
|
25
|
+
extractMultilineField(content, /description:\s*\n\s*"([^"]+)"/);
|
|
26
|
+
// Extract directly from source - no hardcoded validation
|
|
27
|
+
const category = extractField(content, /category:\s*(\w+)/);
|
|
28
|
+
const lifecycle = extractField(content, /lifecycle:\s*(\w+)/);
|
|
29
|
+
if (!name) {
|
|
30
|
+
console.warn(`Could not parse action name from ${filePath}`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// Parse inputs
|
|
34
|
+
const inputs = parseInputs(content);
|
|
35
|
+
// Parse outputs
|
|
36
|
+
const outputs = parseOutputs(content);
|
|
37
|
+
// Parse type parameters
|
|
38
|
+
const typeParameters = parseTypeParameters(content);
|
|
39
|
+
// Parse required model features
|
|
40
|
+
const requiredModelFeatures = parseRequiredModelFeatures(content);
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
version,
|
|
44
|
+
displayName: displayName || name,
|
|
45
|
+
description: description || "",
|
|
46
|
+
category,
|
|
47
|
+
lifecycle,
|
|
48
|
+
inputs,
|
|
49
|
+
outputs,
|
|
50
|
+
typeParameters,
|
|
51
|
+
requiredModelFeatures,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`Error parsing ${filePath}:`, error);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function extractField(content, regex) {
|
|
60
|
+
const match = content.match(regex);
|
|
61
|
+
return match?.[1];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract a block by keyword using brace matching
|
|
65
|
+
*/
|
|
66
|
+
function extractBlock(content, keyword) {
|
|
67
|
+
// Handle both "keyword {" and "keyword: {"
|
|
68
|
+
const regex = new RegExp(`${keyword}:?\\s*\\{`);
|
|
69
|
+
const match = content.match(regex);
|
|
70
|
+
if (!match || match.index === undefined)
|
|
71
|
+
return "";
|
|
72
|
+
const start = match.index;
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let inBlock = false;
|
|
75
|
+
let blockEnd = start;
|
|
76
|
+
for (let i = start; i < content.length; i++) {
|
|
77
|
+
if (content[i] === "{") {
|
|
78
|
+
depth++;
|
|
79
|
+
inBlock = true;
|
|
80
|
+
}
|
|
81
|
+
if (content[i] === "}") {
|
|
82
|
+
depth--;
|
|
83
|
+
}
|
|
84
|
+
if (inBlock && depth === 0) {
|
|
85
|
+
blockEnd = i + 1;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return content.slice(start, blockEnd);
|
|
90
|
+
}
|
|
91
|
+
function extractMultilineField(content, regex) {
|
|
92
|
+
const match = content.match(regex);
|
|
93
|
+
return match?.[1];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse required_model_features from content
|
|
97
|
+
*/
|
|
98
|
+
function parseRequiredModelFeatures(content) {
|
|
99
|
+
const features = [];
|
|
100
|
+
const regex = /required_model_features:\s*(\w+)/g;
|
|
101
|
+
let match;
|
|
102
|
+
while ((match = regex.exec(content)) !== null) {
|
|
103
|
+
features.push(match[1]);
|
|
104
|
+
}
|
|
105
|
+
return features;
|
|
106
|
+
}
|
|
107
|
+
function parseInputs(content) {
|
|
108
|
+
const inputs = [];
|
|
109
|
+
// Find the inputs block
|
|
110
|
+
const inputsStart = content.indexOf("\ninputs {");
|
|
111
|
+
if (inputsStart === -1)
|
|
112
|
+
return inputs;
|
|
113
|
+
// Find the end of inputs block (next top-level section)
|
|
114
|
+
const outputsStart = content.indexOf("\noutputs {", inputsStart);
|
|
115
|
+
const inputsEnd = outputsStart !== -1 ? outputsStart : content.length;
|
|
116
|
+
const inputsBlock = content.slice(inputsStart, inputsEnd);
|
|
117
|
+
// Find each input definition by key
|
|
118
|
+
const keyMatches = [...inputsBlock.matchAll(/key:\s*"([^"]+)"/g)];
|
|
119
|
+
for (const keyMatch of keyMatches) {
|
|
120
|
+
const key = keyMatch[1];
|
|
121
|
+
const keyIndex = keyMatch.index;
|
|
122
|
+
// Find the value block after this key
|
|
123
|
+
const valueStart = inputsBlock.indexOf("value {", keyIndex);
|
|
124
|
+
if (valueStart === -1)
|
|
125
|
+
continue;
|
|
126
|
+
// Find matching closing brace for the value block
|
|
127
|
+
let valueDepth = 0;
|
|
128
|
+
let valueEnd = valueStart;
|
|
129
|
+
for (let i = valueStart; i < inputsBlock.length; i++) {
|
|
130
|
+
if (inputsBlock[i] === "{")
|
|
131
|
+
valueDepth++;
|
|
132
|
+
else if (inputsBlock[i] === "}") {
|
|
133
|
+
valueDepth--;
|
|
134
|
+
if (valueDepth === 0) {
|
|
135
|
+
valueEnd = i + 1;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const valueBlock = inputsBlock.slice(valueStart, valueEnd);
|
|
141
|
+
const input = {
|
|
142
|
+
name: key,
|
|
143
|
+
displayName: extractField(valueBlock, /display_name:\s*"([^"]+)"/) || key,
|
|
144
|
+
argType: parseArgType(valueBlock),
|
|
145
|
+
isOptional: /is_optional:\s*true/.test(valueBlock),
|
|
146
|
+
isAdvanced: /is_advanced:\s*true/.test(valueBlock),
|
|
147
|
+
isConfigDriven: /is_likely_config_driven:\s*true/.test(valueBlock),
|
|
148
|
+
isActionOutput: /is_likely_action_output:\s*true/.test(valueBlock),
|
|
149
|
+
description: extractField(valueBlock, /description:\s*"([^"]+)"/),
|
|
150
|
+
placeholderDisplayValue: extractField(valueBlock, /placeholder_display_value:\s*"([^"]+)"/),
|
|
151
|
+
};
|
|
152
|
+
inputs.push(input);
|
|
153
|
+
}
|
|
154
|
+
return inputs;
|
|
155
|
+
}
|
|
156
|
+
function parseOutputs(content) {
|
|
157
|
+
const outputs = [];
|
|
158
|
+
// Find the outputs block - more permissive matching
|
|
159
|
+
const outputsStart = content.indexOf("\noutputs {");
|
|
160
|
+
if (outputsStart === -1)
|
|
161
|
+
return outputs;
|
|
162
|
+
// Find the end of outputs block
|
|
163
|
+
let depth = 0;
|
|
164
|
+
let outputsEnd = outputsStart;
|
|
165
|
+
let started = false;
|
|
166
|
+
for (let i = outputsStart; i < content.length; i++) {
|
|
167
|
+
if (content[i] === "{") {
|
|
168
|
+
depth++;
|
|
169
|
+
started = true;
|
|
170
|
+
}
|
|
171
|
+
else if (content[i] === "}") {
|
|
172
|
+
depth--;
|
|
173
|
+
if (started && depth === 0) {
|
|
174
|
+
outputsEnd = i + 1;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const outputsBlock = content.slice(outputsStart, outputsEnd);
|
|
180
|
+
// Find each output definition - handle nested braces
|
|
181
|
+
const keyMatches = [...outputsBlock.matchAll(/key:\s*"([^"]+)"/g)];
|
|
182
|
+
for (const keyMatch of keyMatches) {
|
|
183
|
+
const key = keyMatch[1];
|
|
184
|
+
const keyIndex = keyMatch.index;
|
|
185
|
+
// Find the value block after this key
|
|
186
|
+
const valueStart = outputsBlock.indexOf("value {", keyIndex);
|
|
187
|
+
if (valueStart === -1)
|
|
188
|
+
continue;
|
|
189
|
+
// Find matching closing brace
|
|
190
|
+
let valueDepth = 0;
|
|
191
|
+
let valueEnd = valueStart;
|
|
192
|
+
for (let i = valueStart; i < outputsBlock.length; i++) {
|
|
193
|
+
if (outputsBlock[i] === "{")
|
|
194
|
+
valueDepth++;
|
|
195
|
+
else if (outputsBlock[i] === "}") {
|
|
196
|
+
valueDepth--;
|
|
197
|
+
if (valueDepth === 0) {
|
|
198
|
+
valueEnd = i + 1;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const valueBlock = outputsBlock.slice(valueStart, valueEnd);
|
|
204
|
+
const output = {
|
|
205
|
+
name: key,
|
|
206
|
+
displayName: extractField(valueBlock, /display_name:\s*"([^"]+)"/) || key,
|
|
207
|
+
argType: parseArgType(valueBlock, true),
|
|
208
|
+
};
|
|
209
|
+
outputs.push(output);
|
|
210
|
+
}
|
|
211
|
+
return outputs;
|
|
212
|
+
}
|
|
213
|
+
function parseArgType(block, isOutput = false) {
|
|
214
|
+
const argType = {};
|
|
215
|
+
// Check for type_parameter (enum types)
|
|
216
|
+
const typeParam = extractField(block, /type_parameter:\s*"([^"]+)"/);
|
|
217
|
+
if (typeParam) {
|
|
218
|
+
argType.typeParameter = typeParam;
|
|
219
|
+
return argType;
|
|
220
|
+
}
|
|
221
|
+
// Check for well_known_type directly
|
|
222
|
+
const wellKnownDirect = extractField(block, /well_known_type:\s*(\w+)/);
|
|
223
|
+
// Check for array_type { well_known_type: ... }
|
|
224
|
+
const arrayMatch = block.match(/array_type\s*\{[\s\S]*?well_known_type:\s*(\w+)/);
|
|
225
|
+
// Check for array_type { named_type { well_known_type: ... } }
|
|
226
|
+
const namedArrayMatch = block.match(/array_type\s*\{[\s\S]*?named_type\s*\{[\s\S]*?well_known_type:\s*(\w+)/);
|
|
227
|
+
if (namedArrayMatch) {
|
|
228
|
+
argType.wellKnownType = namedArrayMatch[1];
|
|
229
|
+
argType.isArray = true;
|
|
230
|
+
argType.isNamed = true;
|
|
231
|
+
}
|
|
232
|
+
else if (arrayMatch) {
|
|
233
|
+
argType.wellKnownType = arrayMatch[1];
|
|
234
|
+
argType.isArray = true;
|
|
235
|
+
}
|
|
236
|
+
else if (wellKnownDirect) {
|
|
237
|
+
argType.wellKnownType = wellKnownDirect;
|
|
238
|
+
}
|
|
239
|
+
return argType;
|
|
240
|
+
}
|
|
241
|
+
function parseTypeParameters(content) {
|
|
242
|
+
const params = [];
|
|
243
|
+
const typeParamsMatch = content.match(/type_parameters\s*\{([\s\S]*?)\n\}/);
|
|
244
|
+
if (!typeParamsMatch)
|
|
245
|
+
return params;
|
|
246
|
+
const keyRegex = /key:\s*"([^"]+)"/g;
|
|
247
|
+
let match;
|
|
248
|
+
while ((match = keyRegex.exec(typeParamsMatch[1])) !== null) {
|
|
249
|
+
params.push(match[1]);
|
|
250
|
+
}
|
|
251
|
+
return params;
|
|
252
|
+
}
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// File System Helpers
|
|
255
|
+
// ============================================================================
|
|
256
|
+
/**
|
|
257
|
+
* Parse all .txtpb files from a directory
|
|
258
|
+
*/
|
|
259
|
+
export function parseActionDirectory(dirPath) {
|
|
260
|
+
const actions = [];
|
|
261
|
+
if (!fs.existsSync(dirPath)) {
|
|
262
|
+
console.warn(`Directory not found: ${dirPath}`);
|
|
263
|
+
return actions;
|
|
264
|
+
}
|
|
265
|
+
const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".txtpb"));
|
|
266
|
+
for (const file of files) {
|
|
267
|
+
const filePath = path.join(dirPath, file);
|
|
268
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
269
|
+
const parsed = parseTextproto(content, filePath);
|
|
270
|
+
if (parsed) {
|
|
271
|
+
actions.push(parsed);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return actions;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Load documentation for an action from .md files
|
|
278
|
+
*/
|
|
279
|
+
export function loadDocumentation(docDir, actionName) {
|
|
280
|
+
if (!fs.existsSync(docDir))
|
|
281
|
+
return undefined;
|
|
282
|
+
// Try common naming patterns
|
|
283
|
+
const patterns = [
|
|
284
|
+
`${actionName}.md`,
|
|
285
|
+
`${actionName.replace(/_/g, " ")}.md`,
|
|
286
|
+
`${toTitleCase(actionName.replace(/_/g, " "))}.md`,
|
|
287
|
+
`${toTitleCase(actionName.replace(/_/g, " "))}_Agent.md`,
|
|
288
|
+
];
|
|
289
|
+
for (const pattern of patterns) {
|
|
290
|
+
const filePath = path.join(docDir, pattern);
|
|
291
|
+
if (fs.existsSync(filePath)) {
|
|
292
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Fallback: search for files containing the action name
|
|
296
|
+
const files = fs.readdirSync(docDir);
|
|
297
|
+
const searchName = actionName.replace(/_/g, "").toLowerCase();
|
|
298
|
+
for (const file of files) {
|
|
299
|
+
if (file.toLowerCase().replace(/_/g, "").includes(searchName)) {
|
|
300
|
+
return fs.readFileSync(path.join(docDir, file), "utf-8");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
function toTitleCase(str) {
|
|
306
|
+
return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Generate a complete action schema bundle from the ema repo
|
|
310
|
+
*/
|
|
311
|
+
export function generateSchemaBundle(config) {
|
|
312
|
+
const prodDir = path.join(config.basePath, config.prodDir || "system_agents/prod");
|
|
313
|
+
const devDir = path.join(config.basePath, config.devDir || "system_agents/dev");
|
|
314
|
+
const docsDir = path.join(config.basePath, config.docsDir || "documentation");
|
|
315
|
+
// Parse all actions
|
|
316
|
+
const prodActions = parseActionDirectory(prodDir);
|
|
317
|
+
const devActions = parseActionDirectory(devDir);
|
|
318
|
+
// Group by action name
|
|
319
|
+
const actionMap = {};
|
|
320
|
+
for (const action of [...prodActions, ...devActions]) {
|
|
321
|
+
if (!actionMap[action.name]) {
|
|
322
|
+
actionMap[action.name] = [];
|
|
323
|
+
}
|
|
324
|
+
// Add raw documentation - LLM will interpret at generation time
|
|
325
|
+
action.documentation = loadDocumentation(docsDir, action.name);
|
|
326
|
+
// Note: capabilities field is intentionally left undefined
|
|
327
|
+
// The LLM should analyze documentation in context, not pre-computed extraction
|
|
328
|
+
// Avoid duplicates (same name + version)
|
|
329
|
+
const exists = actionMap[action.name].some((a) => a.version === action.version);
|
|
330
|
+
if (!exists) {
|
|
331
|
+
actionMap[action.name].push(action);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
version: new Date().toISOString().split("T")[0], // YYYY-MM-DD
|
|
336
|
+
generatedAt: new Date().toISOString(),
|
|
337
|
+
source: "code",
|
|
338
|
+
sourcePath: config.basePath,
|
|
339
|
+
actions: actionMap,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Type Compatibility Matrix
|
|
344
|
+
// ============================================================================
|
|
345
|
+
/**
|
|
346
|
+
* Type compatibility rules for workflow validation
|
|
347
|
+
*/
|
|
348
|
+
export const TYPE_COMPATIBILITY = {
|
|
349
|
+
// What inputs can accept each type
|
|
350
|
+
WELL_KNOWN_TYPE_CHAT_CONVERSATION: ["conversation", "chat_conversation"],
|
|
351
|
+
WELL_KNOWN_TYPE_TEXT_WITH_SOURCES: ["query", "text", "user_query", "input"],
|
|
352
|
+
WELL_KNOWN_TYPE_SEARCH_RESULT: ["search_results", "results"],
|
|
353
|
+
WELL_KNOWN_TYPE_ANY: ["*"], // Accepts anything
|
|
354
|
+
WELL_KNOWN_TYPE_STRING: ["text", "query", "instructions", "input"],
|
|
355
|
+
WELL_KNOWN_TYPE_STRUCT: ["custom_data", "extracted_variables", "json"],
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Check if an output type is compatible with an input type
|
|
359
|
+
*/
|
|
360
|
+
export function isTypeCompatible(sourceType, targetType, targetInputName) {
|
|
361
|
+
// ANY accepts everything
|
|
362
|
+
if (targetType === "WELL_KNOWN_TYPE_ANY")
|
|
363
|
+
return true;
|
|
364
|
+
// Same type is always compatible
|
|
365
|
+
if (sourceType === targetType)
|
|
366
|
+
return true;
|
|
367
|
+
// named_inputs accepts anything
|
|
368
|
+
if (targetInputName?.startsWith("named_inputs"))
|
|
369
|
+
return true;
|
|
370
|
+
// Check explicit compatibility
|
|
371
|
+
if (sourceType && TYPE_COMPATIBILITY[sourceType]) {
|
|
372
|
+
const compatible = TYPE_COMPATIBILITY[sourceType];
|
|
373
|
+
if (compatible.includes("*"))
|
|
374
|
+
return true;
|
|
375
|
+
if (targetInputName && compatible.some((c) => targetInputName.includes(c)))
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|