@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +321 -0
  3. package/config.example.yaml +32 -0
  4. package/dist/cli/index.js +333 -0
  5. package/dist/config.js +136 -0
  6. package/dist/emaClient.js +398 -0
  7. package/dist/index.js +109 -0
  8. package/dist/mcp/handlers-consolidated.js +851 -0
  9. package/dist/mcp/index.js +15 -0
  10. package/dist/mcp/prompts.js +1753 -0
  11. package/dist/mcp/resources.js +624 -0
  12. package/dist/mcp/server.js +4723 -0
  13. package/dist/mcp/tools-consolidated.js +590 -0
  14. package/dist/mcp/tools-legacy.js +736 -0
  15. package/dist/models.js +8 -0
  16. package/dist/scheduler.js +21 -0
  17. package/dist/sdk/client.js +788 -0
  18. package/dist/sdk/config.js +136 -0
  19. package/dist/sdk/contracts.js +429 -0
  20. package/dist/sdk/generation-schema.js +189 -0
  21. package/dist/sdk/index.js +39 -0
  22. package/dist/sdk/knowledge.js +2780 -0
  23. package/dist/sdk/models.js +8 -0
  24. package/dist/sdk/state.js +88 -0
  25. package/dist/sdk/sync-options.js +216 -0
  26. package/dist/sdk/sync.js +220 -0
  27. package/dist/sdk/validation-rules.js +355 -0
  28. package/dist/sdk/workflow-generator.js +291 -0
  29. package/dist/sdk/workflow-intent.js +1585 -0
  30. package/dist/state.js +88 -0
  31. package/dist/sync.js +416 -0
  32. package/dist/syncOptions.js +216 -0
  33. package/dist/ui.js +334 -0
  34. package/docs/advisor-comms-assistant-fixes.md +175 -0
  35. package/docs/api-contracts.md +216 -0
  36. package/docs/auto-builder-analysis.md +271 -0
  37. package/docs/data-architecture.md +166 -0
  38. package/docs/ema-auto-builder-guide.html +394 -0
  39. package/docs/ema-user-guide.md +1121 -0
  40. package/docs/mcp-tools-guide.md +149 -0
  41. package/docs/naming-conventions.md +218 -0
  42. package/docs/tool-consolidation-proposal.md +427 -0
  43. package/package.json +98 -0
  44. package/resources/templates/chat-ai/README.md +119 -0
  45. package/resources/templates/chat-ai/persona-config.json +111 -0
  46. package/resources/templates/dashboard-ai/README.md +156 -0
  47. package/resources/templates/dashboard-ai/persona-config.json +180 -0
  48. package/resources/templates/voice-ai/README.md +123 -0
  49. package/resources/templates/voice-ai/persona-config.json +74 -0
  50. 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
+ }