@ema.co/mcp-toolkit 1.5.2 → 1.7.0
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 +773 -25
- package/dist/mcp/resources.js +124 -0
- package/dist/mcp/server.js +13 -205
- package/dist/mcp/tools-consolidated.js +163 -103
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/action-schema-parser.js +379 -0
- package/dist/sdk/client.js +757 -90
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +59 -2
- package/dist/sdk/intent-architect.js +883 -0
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/sanitizer.js +1121 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +88 -34
- package/dist/sdk/workflow-intent.js +237 -24
- package/dist/sdk/workflow-optimizer.js +665 -0
- package/dist/sdk/workflow-tracer.js +648 -0
- package/dist/sdk/workflow-transformer.js +10 -0
- package/dist/sdk/workflow-validator.js +609 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +196 -204
- package/docs/release-process.md +153 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +8 -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
|
@@ -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
|
+
}
|