@ema.co/mcp-toolkit 0.2.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.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/config.example.yaml +32 -0
- package/dist/cli/index.js +333 -0
- package/dist/config.js +136 -0
- package/dist/emaClient.js +398 -0
- package/dist/index.js +109 -0
- package/dist/mcp/handlers-consolidated.js +851 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/prompts.js +1753 -0
- package/dist/mcp/resources.js +624 -0
- package/dist/mcp/server.js +4723 -0
- package/dist/mcp/tools-consolidated.js +590 -0
- package/dist/mcp/tools-legacy.js +736 -0
- package/dist/models.js +8 -0
- package/dist/scheduler.js +21 -0
- package/dist/sdk/client.js +788 -0
- package/dist/sdk/config.js +136 -0
- package/dist/sdk/contracts.js +429 -0
- package/dist/sdk/generation-schema.js +189 -0
- package/dist/sdk/index.js +39 -0
- package/dist/sdk/knowledge.js +2780 -0
- package/dist/sdk/models.js +8 -0
- package/dist/sdk/state.js +88 -0
- package/dist/sdk/sync-options.js +216 -0
- package/dist/sdk/sync.js +220 -0
- package/dist/sdk/validation-rules.js +355 -0
- package/dist/sdk/workflow-generator.js +291 -0
- package/dist/sdk/workflow-intent.js +1585 -0
- package/dist/state.js +88 -0
- package/dist/sync.js +416 -0
- package/dist/syncOptions.js +216 -0
- package/dist/ui.js +334 -0
- package/docs/advisor-comms-assistant-fixes.md +175 -0
- package/docs/api-contracts.md +216 -0
- package/docs/auto-builder-analysis.md +271 -0
- package/docs/data-architecture.md +166 -0
- package/docs/ema-auto-builder-guide.html +394 -0
- package/docs/ema-user-guide.md +1121 -0
- package/docs/mcp-tools-guide.md +149 -0
- package/docs/naming-conventions.md +218 -0
- package/docs/tool-consolidation-proposal.md +427 -0
- package/package.json +98 -0
- package/resources/templates/chat-ai/README.md +119 -0
- package/resources/templates/chat-ai/persona-config.json +111 -0
- package/resources/templates/dashboard-ai/README.md +156 -0
- package/resources/templates/dashboard-ai/persona-config.json +180 -0
- package/resources/templates/voice-ai/README.md +123 -0
- package/resources/templates/voice-ai/persona-config.json +74 -0
- package/resources/templates/voice-ai/workflow-prompt.md +120 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Resources Registry
|
|
3
|
+
*
|
|
4
|
+
* Provides static and file-backed resources for the Ema MCP Server.
|
|
5
|
+
*
|
|
6
|
+
* Resources are the canonical source of truth for:
|
|
7
|
+
* - Templates: Persona configuration templates (voice, chat, dashboard)
|
|
8
|
+
* - Agent Catalog: Dynamic list of available workflow agents
|
|
9
|
+
* - Validation Rules: Input/output compatibility rules
|
|
10
|
+
* - Documentation: User guides and references
|
|
11
|
+
*
|
|
12
|
+
* Why resources vs tools:
|
|
13
|
+
* - Resources: Static/reference content the AI assistant reads to understand context
|
|
14
|
+
* - Tools: Actions that query, compute, or mutate data
|
|
15
|
+
*
|
|
16
|
+
* Security:
|
|
17
|
+
* - Only allowlisted paths can be read
|
|
18
|
+
* - Path traversal is blocked
|
|
19
|
+
* - Secret patterns are detected and blocked
|
|
20
|
+
* - Only ema:// URI scheme is supported
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
// Import knowledge catalogs for dynamic resources
|
|
25
|
+
import { AGENT_CATALOG, WORKFLOW_PATTERNS, WIDGET_CATALOG } from "../sdk/knowledge.js";
|
|
26
|
+
import { INPUT_SOURCE_RULES, ANTI_PATTERNS, OPTIMIZATION_RULES } from "../sdk/validation-rules.js";
|
|
27
|
+
import { EmaClient } from "../sdk/client.js";
|
|
28
|
+
import { loadConfigFromJsonEnv, loadConfigOptional, resolveBearerToken, getEnvByName, getMasterEnv, } from "../sdk/config.js";
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Security Utilities
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Patterns that indicate ACTUAL secret content - never expose
|
|
33
|
+
// These patterns are designed to catch real secrets while avoiding false positives
|
|
34
|
+
// on documentation examples (e.g., "your-token-here", "...", etc.)
|
|
35
|
+
const SECRET_PATTERNS = [
|
|
36
|
+
// Private keys - definitive indicator of secrets
|
|
37
|
+
/-----BEGIN\s+(RSA|DSA|EC|OPENSSH|PGP)\s+PRIVATE\s+KEY-----/i,
|
|
38
|
+
// AWS Access Key - specific format
|
|
39
|
+
/AKIA[0-9A-Z]{16}/i,
|
|
40
|
+
// OpenAI API Key - specific format (sk- followed by 48+ chars)
|
|
41
|
+
/sk-[a-zA-Z0-9]{48,}/i,
|
|
42
|
+
// GitHub tokens - specific formats
|
|
43
|
+
/ghp_[a-zA-Z0-9]{36}/i,
|
|
44
|
+
/gho_[a-zA-Z0-9]{36}/i,
|
|
45
|
+
// JWT tokens (3 base64 segments separated by dots, min length)
|
|
46
|
+
/eyJ[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}/,
|
|
47
|
+
// Actual Bearer tokens (not just "Bearer" keyword)
|
|
48
|
+
// Exclude placeholder patterns like "...", "your-token", etc.
|
|
49
|
+
/Bearer\s+(?!\.{3}|your|example|placeholder|<)[a-zA-Z0-9_\-.]{20,}/i,
|
|
50
|
+
// Password with actual value (not placeholders)
|
|
51
|
+
// Exclude: password: "...", password: "your-password", password: ""
|
|
52
|
+
/password\s*[:=]\s*["'](?!\.{3}|your|example|placeholder|<|$)[a-zA-Z0-9!@#$%^&*()_+\-=]{8,}["']/i,
|
|
53
|
+
];
|
|
54
|
+
// Files that should never be exposed
|
|
55
|
+
const BLOCKED_FILES = [
|
|
56
|
+
".env",
|
|
57
|
+
".env.local",
|
|
58
|
+
".env.development",
|
|
59
|
+
".env.production",
|
|
60
|
+
".env.test",
|
|
61
|
+
"secrets.yaml",
|
|
62
|
+
"secrets.json",
|
|
63
|
+
".npmrc",
|
|
64
|
+
".pypirc",
|
|
65
|
+
"id_rsa",
|
|
66
|
+
"id_ed25519",
|
|
67
|
+
"*.pem",
|
|
68
|
+
"*.key",
|
|
69
|
+
];
|
|
70
|
+
function containsSecrets(content) {
|
|
71
|
+
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
|
|
72
|
+
}
|
|
73
|
+
function isBlockedFile(filename) {
|
|
74
|
+
const basename = path.basename(filename);
|
|
75
|
+
return BLOCKED_FILES.some((blocked) => {
|
|
76
|
+
if (blocked.startsWith("*")) {
|
|
77
|
+
return basename.endsWith(blocked.slice(1));
|
|
78
|
+
}
|
|
79
|
+
return basename === blocked;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function hasPathTraversal(uriPath) {
|
|
83
|
+
return uriPath.includes("..") || uriPath.includes("//");
|
|
84
|
+
}
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
// Resource Registry
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
// Allowlisted resource mappings: uri -> relative file path
|
|
89
|
+
// Only includes files that ACTUALLY exist in this repo (not symlinked)
|
|
90
|
+
// and are useful for MCP consumers
|
|
91
|
+
const RESOURCE_MAP = {
|
|
92
|
+
// Core Documentation (files that exist in this repo)
|
|
93
|
+
"ema://docs/readme": {
|
|
94
|
+
path: "README.md",
|
|
95
|
+
description: "Ema Toolkit overview: MCP tools, prompts, resources, CLI usage, SDK API, configuration",
|
|
96
|
+
mimeType: "text/markdown",
|
|
97
|
+
},
|
|
98
|
+
"ema://docs/ema-user-guide": {
|
|
99
|
+
path: "docs/ema-user-guide.md",
|
|
100
|
+
description: "Ema Platform guide: concepts (AI Employees, Workflows, Agents), glossary, architecture, examples, troubleshooting",
|
|
101
|
+
mimeType: "text/markdown",
|
|
102
|
+
},
|
|
103
|
+
"ema://docs/mcp-tools-guide": {
|
|
104
|
+
path: "docs/mcp-tools-guide.md",
|
|
105
|
+
description: "MCP tools usage guide: tool categories, best practices, example call sequences, workflow review patterns",
|
|
106
|
+
mimeType: "text/markdown",
|
|
107
|
+
},
|
|
108
|
+
// Templates - Persona configuration templates
|
|
109
|
+
"ema://templates/voice-ai/config": {
|
|
110
|
+
path: "resources/templates/voice-ai/persona-config.json",
|
|
111
|
+
description: "Voice AI persona configuration template: voiceSettings, conversationSettings, vadSettings, dataProtection",
|
|
112
|
+
mimeType: "application/json",
|
|
113
|
+
},
|
|
114
|
+
"ema://templates/voice-ai/workflow-prompt": {
|
|
115
|
+
path: "resources/templates/voice-ai/workflow-prompt.md",
|
|
116
|
+
description: "Voice AI Auto Builder prompt template with node definitions, connections, and validation assertions",
|
|
117
|
+
mimeType: "text/markdown",
|
|
118
|
+
},
|
|
119
|
+
"ema://templates/voice-ai/readme": {
|
|
120
|
+
path: "resources/templates/voice-ai/README.md",
|
|
121
|
+
description: "Voice AI deployment guide: setup steps, testing checklist, maintenance procedures",
|
|
122
|
+
mimeType: "text/markdown",
|
|
123
|
+
},
|
|
124
|
+
"ema://templates/chat-ai/config": {
|
|
125
|
+
path: "resources/templates/chat-ai/persona-config.json",
|
|
126
|
+
description: "Chat AI persona configuration template: chatbotSdkConfig, feedbackMessage, fileUpload settings",
|
|
127
|
+
mimeType: "application/json",
|
|
128
|
+
},
|
|
129
|
+
"ema://templates/chat-ai/readme": {
|
|
130
|
+
path: "resources/templates/chat-ai/README.md",
|
|
131
|
+
description: "Chat AI deployment guide: workflow generation, widget configuration, knowledge base setup",
|
|
132
|
+
mimeType: "text/markdown",
|
|
133
|
+
},
|
|
134
|
+
"ema://templates/dashboard-ai/config": {
|
|
135
|
+
path: "resources/templates/dashboard-ai/persona-config.json",
|
|
136
|
+
description: "Dashboard AI persona configuration template: inputSchema, batchSettings, timeout configuration",
|
|
137
|
+
mimeType: "application/json",
|
|
138
|
+
},
|
|
139
|
+
"ema://templates/dashboard-ai/readme": {
|
|
140
|
+
path: "resources/templates/dashboard-ai/README.md",
|
|
141
|
+
description: "Dashboard AI deployment guide: batch processing setup, input configuration",
|
|
142
|
+
mimeType: "text/markdown",
|
|
143
|
+
},
|
|
144
|
+
// Cursor Rules - Generation guidance
|
|
145
|
+
"ema://rules/generation": {
|
|
146
|
+
path: ".cursor/rules/platforms/ema/generation/LOCAL-GENERATION.md",
|
|
147
|
+
description: "Local workflow generation rules: how to generate workflows without Auto Builder",
|
|
148
|
+
mimeType: "text/markdown",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const DYNAMIC_RESOURCES = [
|
|
152
|
+
// Agent Catalog - Dynamic list of all workflow agents
|
|
153
|
+
{
|
|
154
|
+
uri: "ema://catalog/agents",
|
|
155
|
+
name: "catalog/agents",
|
|
156
|
+
description: "Complete agent catalog: all available workflow agents with inputs, outputs, critical rules, and usage guidance",
|
|
157
|
+
mimeType: "application/json",
|
|
158
|
+
generate: async (ctx) => JSON.stringify(await getDynamicAgentCatalog({ env: ctx.env }), null, 2),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
uri: "ema://catalog/agents-summary",
|
|
162
|
+
name: "catalog/agents-summary",
|
|
163
|
+
description: "Agent catalog summary: agent names, categories, and brief descriptions for quick reference",
|
|
164
|
+
mimeType: "text/markdown",
|
|
165
|
+
generate: async (ctx) => {
|
|
166
|
+
const agents = await getDynamicAgentCatalog({ env: ctx.env });
|
|
167
|
+
const byCategory = new Map();
|
|
168
|
+
for (const agent of agents) {
|
|
169
|
+
const cat = agent.category || "other";
|
|
170
|
+
if (!byCategory.has(cat))
|
|
171
|
+
byCategory.set(cat, []);
|
|
172
|
+
byCategory.get(cat).push(agent);
|
|
173
|
+
}
|
|
174
|
+
let md = "# Ema Agent Catalog\n\n";
|
|
175
|
+
md += `> ${agents.length} agents available for workflow composition\n\n`;
|
|
176
|
+
for (const [category, agents] of byCategory) {
|
|
177
|
+
md += `## ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
|
|
178
|
+
md += "| Agent | Description | Key Inputs | Key Outputs |\n";
|
|
179
|
+
md += "|-------|-------------|------------|-------------|\n";
|
|
180
|
+
for (const agent of agents) {
|
|
181
|
+
const inputs = agent.inputs?.slice(0, 2).map((i) => i.name).join(", ") || "-";
|
|
182
|
+
const outputs = agent.outputs?.slice(0, 2).map((o) => o.name).join(", ") || "-";
|
|
183
|
+
md += `| \`${agent.actionName}\` | ${agent.description?.slice(0, 60) || "-"}... | ${inputs} | ${outputs} |\n`;
|
|
184
|
+
}
|
|
185
|
+
md += "\n";
|
|
186
|
+
}
|
|
187
|
+
return md;
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
// Workflow Patterns - Common workflow templates
|
|
191
|
+
{
|
|
192
|
+
uri: "ema://catalog/patterns",
|
|
193
|
+
name: "catalog/patterns",
|
|
194
|
+
description: "Workflow patterns: common workflow structures (kb-search, intent-routing, tool-calling) with examples",
|
|
195
|
+
mimeType: "application/json",
|
|
196
|
+
generate: async () => JSON.stringify(WORKFLOW_PATTERNS, null, 2),
|
|
197
|
+
},
|
|
198
|
+
// Widget Catalog - UI configuration widgets
|
|
199
|
+
{
|
|
200
|
+
uri: "ema://catalog/widgets",
|
|
201
|
+
name: "catalog/widgets",
|
|
202
|
+
description: "Widget catalog: proto_config widget types for voice, chat, and dashboard personas",
|
|
203
|
+
mimeType: "application/json",
|
|
204
|
+
generate: async () => JSON.stringify(WIDGET_CATALOG, null, 2),
|
|
205
|
+
},
|
|
206
|
+
// Validation Rules - Input source and anti-pattern rules
|
|
207
|
+
{
|
|
208
|
+
uri: "ema://rules/input-sources",
|
|
209
|
+
name: "rules/input-sources",
|
|
210
|
+
description: "Input source validation rules: which agent inputs accept which data types (user_query vs chat_conversation)",
|
|
211
|
+
mimeType: "application/json",
|
|
212
|
+
generate: async () => JSON.stringify(INPUT_SOURCE_RULES, null, 2),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
uri: "ema://rules/anti-patterns",
|
|
216
|
+
name: "rules/anti-patterns",
|
|
217
|
+
description: "Anti-patterns to avoid: common workflow mistakes and how to prevent them",
|
|
218
|
+
mimeType: "application/json",
|
|
219
|
+
generate: async () => JSON.stringify(ANTI_PATTERNS, null, 2),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
uri: "ema://rules/optimizations",
|
|
223
|
+
name: "rules/optimizations",
|
|
224
|
+
description: "Optimization rules: workflow improvements for better performance and reliability",
|
|
225
|
+
mimeType: "application/json",
|
|
226
|
+
generate: async () => JSON.stringify(OPTIMIZATION_RULES, null, 2),
|
|
227
|
+
},
|
|
228
|
+
// Persona Templates - Dynamic from API
|
|
229
|
+
{
|
|
230
|
+
uri: "ema://catalog/templates",
|
|
231
|
+
name: "catalog/templates",
|
|
232
|
+
description: "Persona templates from Ema API: pre-configured AI Employee templates (chatbot_starter, voicebot, dashboard, etc.)",
|
|
233
|
+
mimeType: "application/json",
|
|
234
|
+
generate: async (ctx) => {
|
|
235
|
+
const templates = await getDynamicPersonaTemplates({ env: ctx.env });
|
|
236
|
+
return JSON.stringify(templates.map(templateDtoToResource), null, 2);
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
uri: "ema://catalog/templates-summary",
|
|
241
|
+
name: "catalog/templates-summary",
|
|
242
|
+
description: "Persona templates summary: template names, categories, and descriptions for quick reference",
|
|
243
|
+
mimeType: "text/markdown",
|
|
244
|
+
generate: async (ctx) => {
|
|
245
|
+
const templates = await getDynamicPersonaTemplates({ env: ctx.env });
|
|
246
|
+
if (templates.length === 0) {
|
|
247
|
+
return "# Persona Templates\n\n> No templates available from API. Check configuration or use file-backed templates at `ema://templates/*`.\n";
|
|
248
|
+
}
|
|
249
|
+
const byCategory = new Map();
|
|
250
|
+
for (const t of templates) {
|
|
251
|
+
const cat = t.category || "GENERAL";
|
|
252
|
+
if (!byCategory.has(cat))
|
|
253
|
+
byCategory.set(cat, []);
|
|
254
|
+
byCategory.get(cat).push(t);
|
|
255
|
+
}
|
|
256
|
+
let md = "# Persona Templates (from API)\n\n";
|
|
257
|
+
md += `> ${templates.length} templates available for creating AI Employees\n\n`;
|
|
258
|
+
for (const [category, tpls] of byCategory) {
|
|
259
|
+
md += `## ${category}\n\n`;
|
|
260
|
+
md += "| Template | Type | Description |\n";
|
|
261
|
+
md += "|----------|------|-------------|\n";
|
|
262
|
+
for (const t of tpls) {
|
|
263
|
+
const triggerType = t.trigger_type || "-";
|
|
264
|
+
const desc = t.description?.slice(0, 80) || "-";
|
|
265
|
+
md += `| \`${t.name}\` | ${triggerType} | ${desc}... |\n`;
|
|
266
|
+
}
|
|
267
|
+
md += "\n";
|
|
268
|
+
}
|
|
269
|
+
return md;
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
274
|
+
// Dynamic fetching helpers (Ema API as source of truth when available)
|
|
275
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
276
|
+
function loadAnyConfig() {
|
|
277
|
+
return (loadConfigFromJsonEnv() ??
|
|
278
|
+
loadConfigOptional(process.env.EMA_AGENT_SYNC_CONFIG ?? "./config.yaml"));
|
|
279
|
+
}
|
|
280
|
+
function getDefaultEnvNameFromConfig(cfg) {
|
|
281
|
+
const fromEnv = process.env.EMA_ENV_NAME?.trim();
|
|
282
|
+
if (fromEnv)
|
|
283
|
+
return fromEnv;
|
|
284
|
+
const master = getMasterEnv(cfg);
|
|
285
|
+
return master?.name ?? cfg.environments[0]?.name ?? "demo";
|
|
286
|
+
}
|
|
287
|
+
const WK_ANY = "WELL_KNOWN_TYPE_ANY";
|
|
288
|
+
function actionDtoToAgentDefinition(a) {
|
|
289
|
+
return {
|
|
290
|
+
actionName: a.name ?? a.id,
|
|
291
|
+
displayName: a.name ?? a.id,
|
|
292
|
+
category: (a.category ?? "other"),
|
|
293
|
+
description: a.description ?? "",
|
|
294
|
+
inputs: (a.inputs ?? []).map((p) => ({
|
|
295
|
+
name: p.name ?? "input",
|
|
296
|
+
type: WK_ANY,
|
|
297
|
+
required: Boolean(p.required),
|
|
298
|
+
description: p.description ?? "",
|
|
299
|
+
})),
|
|
300
|
+
outputs: (a.outputs ?? []).map((p) => ({
|
|
301
|
+
name: p.name ?? "output",
|
|
302
|
+
type: WK_ANY,
|
|
303
|
+
description: p.description ?? "",
|
|
304
|
+
})),
|
|
305
|
+
whenToUse: "",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const clientCache = new Map();
|
|
309
|
+
const agentCatalogCache = new Map();
|
|
310
|
+
function getClientForEnvName(envName) {
|
|
311
|
+
const cfg = loadAnyConfig();
|
|
312
|
+
if (!cfg)
|
|
313
|
+
return null;
|
|
314
|
+
const effectiveEnv = envName ?? getDefaultEnvNameFromConfig(cfg);
|
|
315
|
+
const cached = clientCache.get(effectiveEnv);
|
|
316
|
+
if (cached)
|
|
317
|
+
return cached;
|
|
318
|
+
const envCfg = getEnvByName(cfg, effectiveEnv) ?? getEnvByName(cfg, getDefaultEnvNameFromConfig(cfg));
|
|
319
|
+
if (!envCfg)
|
|
320
|
+
return null;
|
|
321
|
+
const env = {
|
|
322
|
+
name: envCfg.name,
|
|
323
|
+
baseUrl: envCfg.baseUrl,
|
|
324
|
+
bearerToken: resolveBearerToken(envCfg.bearerTokenEnv),
|
|
325
|
+
};
|
|
326
|
+
const client = new EmaClient(env);
|
|
327
|
+
clientCache.set(effectiveEnv, client);
|
|
328
|
+
return client;
|
|
329
|
+
}
|
|
330
|
+
async function getDynamicAgentCatalog(opts) {
|
|
331
|
+
const cacheKey = opts.env ?? "";
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
const cached = agentCatalogCache.get(cacheKey);
|
|
334
|
+
if (cached && now - cached.ts < 60_000)
|
|
335
|
+
return cached.agents;
|
|
336
|
+
const client = getClientForEnvName(opts.env);
|
|
337
|
+
if (!client)
|
|
338
|
+
return AGENT_CATALOG;
|
|
339
|
+
try {
|
|
340
|
+
const actions = await client.listActions();
|
|
341
|
+
const agents = actions
|
|
342
|
+
.filter((a) => typeof a.name === "string" && a.name.trim().length > 0)
|
|
343
|
+
.map(actionDtoToAgentDefinition);
|
|
344
|
+
agentCatalogCache.set(cacheKey, { ts: now, agents });
|
|
345
|
+
return agents.length > 0 ? agents : AGENT_CATALOG;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return AGENT_CATALOG;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Dynamic Persona Templates (API-first)
|
|
353
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
354
|
+
const templateCatalogCache = new Map();
|
|
355
|
+
/**
|
|
356
|
+
* Fetch persona templates from API with caching.
|
|
357
|
+
* Falls back to empty array if API unavailable (file-backed templates still available via RESOURCE_MAP).
|
|
358
|
+
*/
|
|
359
|
+
async function getDynamicPersonaTemplates(opts) {
|
|
360
|
+
const cacheKey = opts.env ?? "";
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
const cached = templateCatalogCache.get(cacheKey);
|
|
363
|
+
if (cached && now - cached.ts < 60_000)
|
|
364
|
+
return cached.templates;
|
|
365
|
+
const client = getClientForEnvName(opts.env);
|
|
366
|
+
if (!client)
|
|
367
|
+
return [];
|
|
368
|
+
try {
|
|
369
|
+
const templates = await client.getPersonaTemplates();
|
|
370
|
+
templateCatalogCache.set(cacheKey, { ts: now, templates });
|
|
371
|
+
return templates;
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Convert PersonaTemplateDTO to a simplified format for MCP resources.
|
|
379
|
+
*/
|
|
380
|
+
function templateDtoToResource(t) {
|
|
381
|
+
return {
|
|
382
|
+
id: t.id,
|
|
383
|
+
name: t.name,
|
|
384
|
+
description: t.description,
|
|
385
|
+
category: t.category,
|
|
386
|
+
trigger_type: t.trigger_type,
|
|
387
|
+
has_project_template: t.has_project_template,
|
|
388
|
+
how_to_use: t.how_to_use,
|
|
389
|
+
value_prop: t.value_prop,
|
|
390
|
+
// Include proto_config for full template details
|
|
391
|
+
proto_config: t.proto_config,
|
|
392
|
+
// Include workflow_definition if available
|
|
393
|
+
workflow_definition: t.workflow_definition,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// Get workspace root from environment or default
|
|
397
|
+
function getWorkspaceRoot() {
|
|
398
|
+
// Try EMA_WORKSPACE_ROOT first (for testing)
|
|
399
|
+
if (process.env.EMA_WORKSPACE_ROOT) {
|
|
400
|
+
return process.env.EMA_WORKSPACE_ROOT;
|
|
401
|
+
}
|
|
402
|
+
// Otherwise use current working directory
|
|
403
|
+
return process.cwd();
|
|
404
|
+
}
|
|
405
|
+
export class ResourceRegistry {
|
|
406
|
+
workspaceRoot;
|
|
407
|
+
constructor(workspaceRoot) {
|
|
408
|
+
this.workspaceRoot = workspaceRoot ?? getWorkspaceRoot();
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* List all available resources
|
|
412
|
+
*/
|
|
413
|
+
list() {
|
|
414
|
+
const resources = [];
|
|
415
|
+
// Add file-backed resources
|
|
416
|
+
for (const [uri, config] of Object.entries(RESOURCE_MAP)) {
|
|
417
|
+
resources.push({
|
|
418
|
+
uri,
|
|
419
|
+
name: uri.replace("ema://", ""),
|
|
420
|
+
description: config.description,
|
|
421
|
+
mimeType: config.mimeType,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// Add dynamic resources
|
|
425
|
+
for (const resource of DYNAMIC_RESOURCES) {
|
|
426
|
+
resources.push({
|
|
427
|
+
uri: resource.uri,
|
|
428
|
+
name: resource.name,
|
|
429
|
+
description: resource.description,
|
|
430
|
+
mimeType: resource.mimeType,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// Add virtual index resource
|
|
434
|
+
resources.push({
|
|
435
|
+
uri: "ema://index/all-resources",
|
|
436
|
+
name: "index/all-resources",
|
|
437
|
+
description: "Index of all available Ema MCP resources with descriptions",
|
|
438
|
+
mimeType: "text/markdown",
|
|
439
|
+
});
|
|
440
|
+
return resources;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Read a specific resource by URI
|
|
444
|
+
*/
|
|
445
|
+
async read(uri) {
|
|
446
|
+
// Validate URI scheme
|
|
447
|
+
if (!uri.startsWith("ema://")) {
|
|
448
|
+
return {
|
|
449
|
+
code: "INVALID_URI",
|
|
450
|
+
message: `Invalid URI scheme. Expected 'ema://', got '${uri.split("://")[0] ?? "unknown"}://'`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const rawUriPath = uri.replace("ema://", "");
|
|
454
|
+
// Check for path traversal
|
|
455
|
+
// IMPORTANT: check the raw, non-normalized path so `..` can't be
|
|
456
|
+
// normalized away by URL parsing.
|
|
457
|
+
if (hasPathTraversal(rawUriPath)) {
|
|
458
|
+
return {
|
|
459
|
+
code: "UNAUTHORIZED",
|
|
460
|
+
message: "Path traversal detected and blocked",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
let baseUri = uri;
|
|
464
|
+
let envFromQuery;
|
|
465
|
+
try {
|
|
466
|
+
const u = new URL(uri);
|
|
467
|
+
envFromQuery = u.searchParams.get("env") ?? undefined;
|
|
468
|
+
baseUri = `ema://${u.host}${u.pathname}`;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// ignore parse errors; treat as raw URI
|
|
472
|
+
}
|
|
473
|
+
// Handle virtual index resource
|
|
474
|
+
if (baseUri === "ema://index/all-resources") {
|
|
475
|
+
return this.buildResourceIndex();
|
|
476
|
+
}
|
|
477
|
+
// Check for dynamic resources first
|
|
478
|
+
const dynamicResource = DYNAMIC_RESOURCES.find((r) => r.uri === baseUri);
|
|
479
|
+
if (dynamicResource) {
|
|
480
|
+
return {
|
|
481
|
+
uri: baseUri,
|
|
482
|
+
mimeType: dynamicResource.mimeType,
|
|
483
|
+
text: await dynamicResource.generate({ env: envFromQuery }),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// Look up in file-backed allowlist
|
|
487
|
+
const resourceConfig = RESOURCE_MAP[baseUri];
|
|
488
|
+
if (!resourceConfig) {
|
|
489
|
+
return {
|
|
490
|
+
code: "NOT_FOUND",
|
|
491
|
+
message: `Resource not found: ${uri}. Use resources/list to see available resources.`,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// Check if file is blocked
|
|
495
|
+
if (isBlockedFile(resourceConfig.path)) {
|
|
496
|
+
return {
|
|
497
|
+
code: "UNAUTHORIZED",
|
|
498
|
+
message: "Access to this file type is not permitted",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// Read the file
|
|
502
|
+
const fullPath = path.resolve(this.workspaceRoot, resourceConfig.path);
|
|
503
|
+
// Verify path is still within workspace (defense in depth)
|
|
504
|
+
if (!fullPath.startsWith(this.workspaceRoot)) {
|
|
505
|
+
return {
|
|
506
|
+
code: "UNAUTHORIZED",
|
|
507
|
+
message: "Path traversal detected and blocked",
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
512
|
+
// Check for secrets
|
|
513
|
+
if (containsSecrets(content)) {
|
|
514
|
+
return {
|
|
515
|
+
code: "SECRET_DETECTED",
|
|
516
|
+
message: "This resource contains sensitive data and cannot be exposed",
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
uri: baseUri,
|
|
521
|
+
mimeType: resourceConfig.mimeType,
|
|
522
|
+
text: content,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (error.code === "ENOENT") {
|
|
527
|
+
return {
|
|
528
|
+
code: "NOT_FOUND",
|
|
529
|
+
message: `Resource file not found: ${resourceConfig.path}`,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Build the virtual resource index
|
|
537
|
+
*/
|
|
538
|
+
buildResourceIndex() {
|
|
539
|
+
const resources = this.list().filter((r) => r.uri !== "ema://index/all-resources");
|
|
540
|
+
// Group resources by category
|
|
541
|
+
const docs = resources.filter((r) => r.uri.startsWith("ema://docs/"));
|
|
542
|
+
const templates = resources.filter((r) => r.uri.startsWith("ema://templates/"));
|
|
543
|
+
const catalog = resources.filter((r) => r.uri.startsWith("ema://catalog/"));
|
|
544
|
+
const rules = resources.filter((r) => r.uri.startsWith("ema://rules/"));
|
|
545
|
+
const markdown = `# Ema MCP Resources Index
|
|
546
|
+
|
|
547
|
+
> Available resources exposed by the Ema MCP Server
|
|
548
|
+
>
|
|
549
|
+
> **Use resources for reference data. Use tools for actions.**
|
|
550
|
+
|
|
551
|
+
## Summary
|
|
552
|
+
|
|
553
|
+
| Category | Count | Description |
|
|
554
|
+
|----------|-------|-------------|
|
|
555
|
+
| Documentation | ${docs.length} | User guides, README, tool references |
|
|
556
|
+
| Templates | ${templates.length} | Persona configuration templates (voice/chat/dashboard) |
|
|
557
|
+
| Catalog | ${catalog.length} | Agent catalog, workflow patterns, widgets |
|
|
558
|
+
| Rules | ${rules.length} | Validation rules, anti-patterns, optimizations |
|
|
559
|
+
|
|
560
|
+
## Documentation (${docs.length})
|
|
561
|
+
|
|
562
|
+
| URI | Description |
|
|
563
|
+
|-----|-------------|
|
|
564
|
+
${docs.map((r) => `| \`${r.uri}\` | ${r.description} |`).join("\n")}
|
|
565
|
+
|
|
566
|
+
## Templates (${templates.length})
|
|
567
|
+
|
|
568
|
+
> Use templates as starting points for new AI Employees
|
|
569
|
+
|
|
570
|
+
| URI | Description |
|
|
571
|
+
|-----|-------------|
|
|
572
|
+
${templates.map((r) => `| \`${r.uri}\` | ${r.description} |`).join("\n")}
|
|
573
|
+
|
|
574
|
+
## Catalog (${catalog.length})
|
|
575
|
+
|
|
576
|
+
> Dynamic catalogs generated from SDK knowledge
|
|
577
|
+
|
|
578
|
+
| URI | Description |
|
|
579
|
+
|-----|-------------|
|
|
580
|
+
${catalog.map((r) => `| \`${r.uri}\` | ${r.description} |`).join("\n")}
|
|
581
|
+
|
|
582
|
+
## Rules (${rules.length})
|
|
583
|
+
|
|
584
|
+
> Validation and best-practice rules
|
|
585
|
+
|
|
586
|
+
| URI | Description |
|
|
587
|
+
|-----|-------------|
|
|
588
|
+
${rules.map((r) => `| \`${r.uri}\` | ${r.description} |`).join("\n")}
|
|
589
|
+
|
|
590
|
+
## Usage
|
|
591
|
+
|
|
592
|
+
To read a resource, use the \`resources/read\` endpoint:
|
|
593
|
+
|
|
594
|
+
\`\`\`json
|
|
595
|
+
{
|
|
596
|
+
"method": "resources/read",
|
|
597
|
+
"params": {
|
|
598
|
+
"uri": "ema://catalog/agents-summary"
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
\`\`\`
|
|
602
|
+
|
|
603
|
+
## When to Use Resources vs Tools
|
|
604
|
+
|
|
605
|
+
| Need | Use |
|
|
606
|
+
|------|-----|
|
|
607
|
+
| Get agent definitions | Resource: \`ema://catalog/agents\` |
|
|
608
|
+
| Get input/output compatibility rules | Resource: \`ema://rules/input-sources\` |
|
|
609
|
+
| Get persona template | Resource: \`ema://templates/voice-ai/config\` |
|
|
610
|
+
| Query live persona data | Tool: \`persona\` |
|
|
611
|
+
| Generate a workflow | Tool: \`workflow\` |
|
|
612
|
+
| Analyze an existing workflow | Tool: \`workflow(mode="analyze")\` |
|
|
613
|
+
`;
|
|
614
|
+
return {
|
|
615
|
+
uri: "ema://index/all-resources",
|
|
616
|
+
mimeType: "text/markdown",
|
|
617
|
+
text: markdown,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Helper to check if result is an error
|
|
622
|
+
export function isResourceError(result) {
|
|
623
|
+
return "code" in result;
|
|
624
|
+
}
|