@ema.co/mcp-toolkit 2026.2.19 → 2026.2.23

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.

@@ -24,34 +24,30 @@ export { TOOLKIT_NAME, TOOLKIT_VERSION, TOOLKIT_COMMIT };
24
24
  import { PromptRegistry, isPromptError } from "./prompts.js";
25
25
  import { ResourceRegistry, isResourceError } from "./resources.js";
26
26
  import { generateServerInstructions, getContextualTip, TOOL_GUIDANCE } from "./guidance.js";
27
- import { resolveSyncBehavior, loadSyncOptions } from "../sync/sync-options.js";
28
- import { fingerprintPersona } from "../sync.js";
29
- // Direct Sync (Config-less) - extracted to handlers/sync/direct.ts
30
- import { directSyncPersona, directSyncPersonaById, directSyncAll } from "./handlers/sync/direct.js";
31
- import { createVersionStorage } from "../sync/version-storage.js";
32
- import { createVersionPolicyEngine } from "../sync/version-policy.js";
33
27
  // V2 Tools
34
28
  import { generateTools } from "./tools.js";
35
- import { handlePersona } from "./handlers/persona/index.js";
29
+ // Handler imports (simple 1-2 line handlers stay inline)
36
30
  import { handleEnv } from "./handlers/env/index.js";
37
31
  import { handleAction } from "./handlers/action/index.js";
38
32
  import { handleTemplate } from "./handlers/template/index.js";
39
- import { handleKnowledge } from "./handlers/knowledge/index.js";
40
33
  import { handleReference } from "./handlers/reference/index.js";
41
- // Import extracted handlers
42
- import { handleWorkflow } from "./handlers/workflow/index.js";
43
34
  import { handleCatalog } from "./handlers/catalog/index.js";
44
35
  import { handleFeedback } from "./handlers/feedback/index.js";
45
36
  import { recordTelemetry } from "./handlers/feedback/store.js";
46
- import { handleConsolidateDemoData, handleGenerateDemoDocument, handleValidateDemoDocument, handleGetDemoDataTemplate, } from "./handlers/demo/index.js";
37
+ // V2 adapters (extracted from server.ts each owns its routing/transformation logic)
38
+ import { handlePersonaAdapter } from "./handlers/persona/adapter.js";
39
+ import { handleWorkflowAdapter } from "./handlers/workflow/adapter.js";
40
+ import { handleSyncAdapter } from "./handlers/sync/adapter.js";
41
+ import { handleDemoAdapter } from "./handlers/demo/adapter.js";
42
+ import { handleDebugAdapter } from "./handlers/debug/adapter.js";
47
43
  // Start token initialization in background (non-blocking)
48
44
  void initializeApiKeyTokens();
49
45
  // ─────────────────────────────────────────────────────────────────────────────
50
46
  // Tool Definitions
51
47
  // ─────────────────────────────────────────────────────────────────────────────
52
48
  //
53
- // V2 TOOLS (5 tools) - LLM-optimized minimal interface
54
- // - env, persona, catalog, workflow, sync
49
+ // V2 TOOLS (7 tools) - LLM-optimized minimal interface
50
+ // - env, persona, catalog, workflow, sync, toolkit_feedback, debug
55
51
  // - Defined in: ./tools.ts
56
52
  //
57
53
  // NAMING CONVENTION:
@@ -62,7 +58,7 @@ void initializeApiKeyTokens();
62
58
  /**
63
59
  * Generate all available tools
64
60
  *
65
- * V2: 5 tools (persona, catalog, workflow, sync, env) - LLM-optimized
61
+ * V2: 7 tools (persona, catalog, workflow, sync, env, toolkit_feedback, debug) - LLM-optimized
66
62
  *
67
63
  * Why V2:
68
64
  * - Minimal tool count optimizes LLM tool selection
@@ -88,224 +84,7 @@ const toolHandlers = {
88
84
  })), { name: TOOLKIT_NAME, version: TOOLKIT_VERSION, commit: TOOLKIT_COMMIT });
89
85
  },
90
86
  persona: async (args) => {
91
- const targetEnv = args.env ?? getDefaultEnvName();
92
- const client = createClient(targetEnv);
93
- // Build version context for version management modes
94
- const versionContext = {
95
- // Store versions in the caller's workspace, not the toolkit install dir.
96
- // This must be writable under typical MCP usage (e.g. Cursor workspace).
97
- workspaceRoot: process.cwd(),
98
- environment: targetEnv,
99
- tenant_id: targetEnv, // Use env name as tenant identifier
100
- };
101
- // ─────────────────────────────────────────────────────────────────────────
102
- // V2 Parameter Transformation
103
- // Convert v2 structure to v1 mode-based structure for handler compatibility
104
- // ─────────────────────────────────────────────────────────────────────────
105
- const transformedArgs = { ...args };
106
- // ─────────────────────────────────────────────────────────────────────────
107
- // Explicit Method API (takes priority over flag-based args)
108
- // persona(method="create|get|list|update|delete|sanitize|analyze|compare|clone")
109
- // ─────────────────────────────────────────────────────────────────────────
110
- if (args.method) {
111
- const method = String(args.method);
112
- // Map explicit methods to internal modes
113
- const methodToMode = {
114
- create: "create",
115
- get: "get",
116
- list: "list",
117
- update: "update",
118
- delete: "delete",
119
- sanitize: "sanitize",
120
- analyze: "analyze",
121
- compare: "compare",
122
- schema: "schema", // get persona input schema (dashboard columns)
123
- clone: "create", // clone is create with from
124
- snapshot: "version_create",
125
- history: "version_list",
126
- restore: "version_restore",
127
- };
128
- const mode = methodToMode[method];
129
- if (!mode) {
130
- return { error: `Unknown method: ${method}`, valid_methods: Object.keys(methodToMode) };
131
- }
132
- transformedArgs.mode = mode;
133
- delete transformedArgs.method;
134
- // Handle action composition if present
135
- if (args.actions && Array.isArray(args.actions)) {
136
- // Store actions for post-processing after main operation
137
- transformedArgs._actions = args.actions;
138
- delete transformedArgs.actions;
139
- }
140
- }
141
- // ─────────────────────────────────────────────────────────────────────────
142
- // Flag-based API (legacy compatibility)
143
- // ONLY applies when explicit `method` parameter was NOT provided
144
- // When method is explicit, flags like sanitize=true are passed through
145
- // ─────────────────────────────────────────────────────────────────────────
146
- // Guard: Skip flag-based API if explicit method was provided
147
- const skipFlagBasedApi = !!args.method;
148
- // Create: persona(create={name, type, from, input})
149
- if (!skipFlagBasedApi && args.create && typeof args.create === "object") {
150
- const create = args.create;
151
- transformedArgs.mode = "create";
152
- transformedArgs.name = create.name;
153
- transformedArgs.type = create.type;
154
- transformedArgs.from = create.from;
155
- transformedArgs.input = create.input;
156
- transformedArgs.preview = create.preview ?? true;
157
- delete transformedArgs.create;
158
- }
159
- // Update: persona(id, update={config, input, workflow_spec})
160
- else if (!skipFlagBasedApi && args.update && typeof args.update === "object") {
161
- const update = args.update;
162
- transformedArgs.mode = "update";
163
- transformedArgs.proto_config = update.config;
164
- transformedArgs.input = update.input;
165
- transformedArgs.workflow_spec = update.workflow_spec;
166
- transformedArgs.preview = update.preview ?? false; // Default to deploy, not preview
167
- delete transformedArgs.update;
168
- }
169
- // Delete: persona(id, delete=true)
170
- else if (!skipFlagBasedApi && args.delete === true) {
171
- transformedArgs.mode = "delete";
172
- delete transformedArgs.delete;
173
- }
174
- // Analyze: persona(id, analyze=true, fix=true)
175
- else if (!skipFlagBasedApi && args.analyze === true) {
176
- transformedArgs.mode = "analyze";
177
- // fix is already in args
178
- delete transformedArgs.analyze;
179
- }
180
- // Sanitize: persona(id, sanitize=true)
181
- // ONLY converts to mode="sanitize" when method is NOT explicit
182
- // When method="create" + sanitize=true, sanitize is a FLAG for post-creation sanitization
183
- else if (!skipFlagBasedApi && args.sanitize === true) {
184
- transformedArgs.mode = "sanitize";
185
- delete transformedArgs.sanitize;
186
- }
187
- // Snapshot: persona(id, snapshot="message")
188
- else if (!skipFlagBasedApi && typeof args.snapshot === "string") {
189
- transformedArgs.mode = "version_create";
190
- transformedArgs.message = args.snapshot;
191
- delete transformedArgs.snapshot;
192
- }
193
- // History: persona(id, history=true)
194
- else if (!skipFlagBasedApi && args.history === true) {
195
- transformedArgs.mode = "version_list";
196
- delete transformedArgs.history;
197
- }
198
- // Restore: persona(id, restore="v3")
199
- else if (!skipFlagBasedApi && typeof args.restore === "string") {
200
- transformedArgs.mode = "version_restore";
201
- transformedArgs.version = args.restore;
202
- delete transformedArgs.restore;
203
- }
204
- // Compare: persona(id, compare="other-id")
205
- else if (!skipFlagBasedApi && typeof args.compare === "string") {
206
- transformedArgs.mode = "compare";
207
- transformedArgs.compare_to = args.compare;
208
- delete transformedArgs.compare;
209
- }
210
- // Data operations: persona(id, data={method:"...", ...})
211
- // Supports both explicit method format and legacy flag format
212
- else if (args.data && typeof args.data === "object") {
213
- const data = args.data;
214
- const personaId = args.id;
215
- const fs = await import("fs/promises");
216
- // EXPLICIT METHOD FORMAT (preferred): data={method:"list/stats/upload/copy/replicate/delete/search/refresh/regenerate/replace"}
217
- if (typeof data.method === "string") {
218
- // Import the new data handler - pass data object as part of args
219
- const { handleData: handleDataNew } = await import("./handlers/data/index.js");
220
- return handleDataNew({ persona_id: personaId, env: args.env, data }, client);
221
- }
222
- // LEGACY FLAG FORMAT (backwards compatibility) - use extracted handler
223
- const { handleData: handleDataExtracted } = await import("./handlers/data/index.js");
224
- const readFileFn = (path) => fs.readFile(path);
225
- if (data.list === true) {
226
- return handleDataExtracted({ method: "list", persona_id: personaId, env: args.env }, client, readFileFn);
227
- }
228
- if (typeof data.upload === "string") {
229
- return handleDataExtracted({ method: "upload", persona_id: personaId, data: { path: data.upload }, env: args.env }, client, readFileFn);
230
- }
231
- if (typeof data.delete === "string") {
232
- return handleDataExtracted({ method: "delete", persona_id: personaId, data: { file_id: data.delete }, env: args.env }, client, readFileFn);
233
- }
234
- if (typeof data.generate === "string" || data.template) {
235
- return handleDataExtracted({
236
- method: "generate",
237
- persona_id: personaId,
238
- data: {
239
- input: data.generate,
240
- from: data.template,
241
- count: data.count,
242
- },
243
- env: args.env
244
- }, client, readFileFn);
245
- }
246
- if (typeof data.embed === "boolean") {
247
- return handleDataExtracted({ method: "embedding", persona_id: personaId, data: { enabled: data.embed }, env: args.env }, client, readFileFn);
248
- }
249
- if (typeof data.search === "string") {
250
- return handleKnowledge({ mode: "search", persona_id: personaId, query: data.search, env: args.env }, client, (path) => fs.readFile(path));
251
- }
252
- return {
253
- error: "Unknown data operation",
254
- hint: "Use data={method:'list'} format (explicit method)",
255
- available_methods: ["list", "stats", "upload", "copy", "replicate", "delete", "search", "refresh", "regenerate", "replace"],
256
- legacy_flags: ["list", "upload", "delete", "generate", "embed", "search"],
257
- };
258
- }
259
- // Get: persona(id) with no mutation flags
260
- else if (args.id && !transformedArgs.mode) {
261
- transformedArgs.mode = "get";
262
- }
263
- // List: persona() or persona(type, status, query) with no id
264
- else if (!args.id && !transformedArgs.mode) {
265
- transformedArgs.mode = "list";
266
- }
267
- // Templates are tenant-specific - don't use hardcoded IDs
268
- // The handler will use dynamic template lookup from API
269
- const result = await handlePersona(transformedArgs, client, () => undefined, // Dynamic lookup in handler
270
- (env) => createClient(env), versionContext);
271
- // ─────────────────────────────────────────────────────────────────────────
272
- // Action Composition Post-Processing
273
- // Execute actions array after main operation completes
274
- // ─────────────────────────────────────────────────────────────────────────
275
- const actions = transformedArgs._actions;
276
- if (actions && actions.length > 0) {
277
- // Get the target persona ID from result
278
- const resultObj = result;
279
- const targetId = resultObj.id ??
280
- resultObj.persona_id;
281
- const sourceId = args.from;
282
- if (!targetId) {
283
- return {
284
- ...(typeof result === "object" && result !== null ? result : {}),
285
- _actions_error: "No persona ID available for action execution",
286
- };
287
- }
288
- // Import and execute actions
289
- const actionExecutor = await import("./handlers/action-executor.js");
290
- const context = {
291
- source: sourceId,
292
- target: targetId,
293
- env: targetEnv,
294
- originalArgs: args,
295
- };
296
- const actionsResult = await actionExecutor.executeActions(actions, context, client);
297
- return {
298
- ...(typeof result === "object" && result !== null ? result : {}),
299
- _actions: actionsResult,
300
- };
301
- }
302
- return result;
303
- },
304
- // Consolidated workflow handler (replaces legacy inline handler)
305
- workflow: async (args) => {
306
- const client = createClient(args.env);
307
- // Templates are tenant-specific - dynamic lookup in handler
308
- return handleWorkflow(args, client, () => undefined);
87
+ return handlePersonaAdapter(args, createClient, getDefaultEnvName);
309
88
  },
310
89
  action: async (args) => {
311
90
  const client = createClient(args.env);
@@ -314,12 +93,6 @@ const toolHandlers = {
314
93
  template: async (args) => {
315
94
  return handleTemplate(args);
316
95
  },
317
- knowledge: async (args) => {
318
- const client = createClient(args.env);
319
- const fs = await import("fs/promises");
320
- return handleKnowledge(args, client, (path) => fs.readFile(path));
321
- },
322
- // v2: data is an alias for knowledge with simplified interface
323
96
  data: async (args) => {
324
97
  const client = createClient(args.env);
325
98
  const fs = await import("fs/promises");
@@ -352,572 +125,19 @@ const toolHandlers = {
352
125
  toolkit_feedback: async (args) => {
353
126
  return handleFeedback(args);
354
127
  },
355
- };
356
- // ─────────────────────────────────────────────────────────────────────────────
357
- // Standalone sync implementation functions
358
- // (Used by the V2 sync adapter below)
359
- // ─────────────────────────────────────────────────────────────────────────────
360
- async function syncRunImpl(args) {
361
- const targetEnv = String(args.target_env);
362
- const sourceEnv = args.source_env ? String(args.source_env) : getDefaultEnvName();
363
- const dryRun = args.dry_run === true;
364
- const includeStatus = args.include_status === true;
365
- const scope = args.scope === "all" ? "all" : "one";
366
- const identifier = args.identifier ? String(args.identifier) : undefined;
367
- // Sync all tagged personas
368
- if (scope === "all" || !identifier) {
369
- const sdk = getSyncSDK();
370
- if (sdk) {
371
- try {
372
- const result = await sdk.runSync();
373
- return { success: true, mode: "config", ...result };
374
- }
375
- finally {
376
- sdk.close();
377
- }
378
- }
379
- // Config-less mode
380
- try {
381
- const result = await directSyncAll({ targetEnv, dryRun });
382
- return { success: true, mode: "tags", ...result };
383
- }
384
- catch (e) {
385
- return { success: false, error: e instanceof Error ? e.message : String(e) };
386
- }
387
- }
388
- // Sync single persona
389
- const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
390
- const behavior = resolveSyncBehavior({
391
- personaName: isUUID ? undefined : identifier,
392
- targetEnv,
393
- overrides: {
394
- dry_run: dryRun ? true : undefined,
395
- sync_status: includeStatus ? true : undefined,
396
- },
397
- });
398
- try {
399
- const result = isUUID
400
- ? await directSyncPersonaById({
401
- personaId: identifier,
402
- sourceEnv,
403
- targetEnv,
404
- dryRun: behavior.dry_run,
405
- syncStatus: behavior.sync_status,
406
- })
407
- : await directSyncPersona({
408
- name: identifier,
409
- sourceEnv,
410
- targetEnv,
411
- dryRun: behavior.dry_run,
412
- syncStatus: behavior.sync_status,
413
- });
414
- return { ...result, resolved_behavior: behavior };
415
- }
416
- catch (e) {
417
- return { success: false, error: e instanceof Error ? e.message : String(e) };
418
- }
419
- }
420
- async function syncInfoImpl(args) {
421
- const client = args.env ? createClient(args.env) : undefined;
422
- // Check if persona is synced
423
- if (args.persona_id) {
424
- if (!client)
425
- throw new Error("env required when checking persona sync status");
426
- const personaId = String(args.persona_id);
427
- const personas = await client.getPersonasForTenant();
428
- const persona = personas.find((p) => p.id === personaId);
429
- if (!persona)
430
- throw new Error(`AI Employee not found: ${personaId}`);
431
- const meta = client.getSyncMetadata(persona);
432
- return {
433
- environment: client["env"].name,
434
- persona_id: personaId,
435
- persona_name: persona.name,
436
- is_synced: !!meta,
437
- sync_metadata: meta,
438
- };
439
- }
440
- // Check by persona name
441
- if (args.persona_name) {
442
- const sdk = getSyncSDK();
443
- if (!sdk)
444
- return { error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG." };
445
- try {
446
- const persona = await sdk.getMasterPersonaByName(String(args.persona_name));
447
- if (!persona)
448
- return { error: `Persona not found: ${args.persona_name}` };
449
- return await sdk.getPersonaSyncStatus(persona.id);
450
- }
451
- finally {
452
- sdk.close();
453
- }
454
- }
455
- // List all synced personas
456
- if (args.list_synced === true) {
457
- if (!client)
458
- throw new Error("env required when listing synced personas");
459
- const personas = await client.getPersonasForTenant();
460
- const masterEnvFilter = args.master_env ? String(args.master_env).toLowerCase() : undefined;
461
- const synced = [];
462
- for (const p of personas) {
463
- const meta = client.getSyncMetadata(p);
464
- if (meta) {
465
- if (masterEnvFilter && meta.master_env.toLowerCase() !== masterEnvFilter)
466
- continue;
467
- synced.push({ persona_id: p.id, persona_name: p.name, sync_metadata: meta });
468
- }
469
- }
470
- return { environment: client["env"].name, count: synced.length, synced_personas: synced };
471
- }
472
- // Default: return overall sync config/status
473
- const sdk = getSyncSDK();
474
- const options = args.include_options === true ? loadSyncOptions() : undefined;
475
- if (!sdk) {
476
- return {
477
- configured: false,
478
- error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG.",
479
- options,
480
- };
481
- }
482
- try {
483
- const master = sdk.getMasterEnvironment();
484
- const envs = sdk.getEnvironments();
485
- const personas = await sdk.listMasterPersonas();
486
- return {
487
- configured: true,
488
- master_environment: { name: master.name, url: master.baseUrl },
489
- target_environments: envs.filter((e) => !e.isMaster).map((e) => ({ name: e.name, url: e.baseUrl })),
490
- total_personas: personas.length,
491
- options,
492
- };
493
- }
494
- finally {
495
- sdk.close();
496
- }
497
- }
498
- // ─────────────────────────────────────────────────────────────────────────────
499
- // V2 Tool Adapters (contract ↔ implementation)
500
- // ─────────────────────────────────────────────────────────────────────────────
501
- //
502
- // The tool schemas in tools.ts are the public MCP contract.
503
- // These adapters ensure the V2 tool surface behaves as documented,
504
- // while routing to the extracted handler implementations.
505
- // Workflow tool: MCP provides data (get) and executes (deploy). LLM does all thinking.
506
- toolHandlers.workflow = async (args) => {
507
- const normalizedArgs = { ...(args ?? {}) };
508
- const personaId = normalizedArgs.persona_id ? String(normalizedArgs.persona_id) : undefined;
509
- let workflowDef = normalizedArgs.workflow_def;
510
- const workflowDefPath = normalizedArgs.workflow_def_path;
511
- const mode = normalizedArgs.mode ? String(normalizedArgs.mode) : undefined;
512
- const baseFingerprint = normalizedArgs.base_fingerprint;
513
- const force = normalizedArgs.force;
514
- // For deploy: resolve workflow_def from file when workflow_def_path is provided (large payloads)
515
- // Also handle the common agent mistake: passing {"workflow_def_path": "/path"} as workflow_def
516
- // (happens when the agent's tool schema doesn't expose workflow_def_path as a separate param)
517
- let effectivePath = workflowDefPath;
518
- if (mode === "deploy" && workflowDef && !effectivePath) {
519
- const keys = Object.keys(workflowDef);
520
- if (keys.length === 1 && keys[0] === "workflow_def_path" && typeof workflowDef.workflow_def_path === "string") {
521
- effectivePath = workflowDef.workflow_def_path;
522
- workflowDef = undefined;
523
- }
524
- }
525
- if (mode === "deploy" && !workflowDef && effectivePath) {
526
- try {
527
- // Guardrails: reduce risk of accidental secret exfiltration
528
- const path = await import("path");
529
- if (!path.isAbsolute(effectivePath)) {
530
- return { error: "workflow_def_path must be an absolute path on the MCP server host", path: effectivePath };
531
- }
532
- if (!effectivePath.toLowerCase().endsWith(".json")) {
533
- return { error: "workflow_def_path must point to a .json file", path: effectivePath };
534
- }
535
- const fs = await import("fs/promises");
536
- const stat = await fs.stat(effectivePath);
537
- const MAX_WORKFLOW_DEF_BYTES = 1024 * 1024; // 1MB
538
- if (stat.size > MAX_WORKFLOW_DEF_BYTES) {
539
- return { error: `workflow_def_path file too large (${stat.size} bytes)`, max_bytes: MAX_WORKFLOW_DEF_BYTES, path: effectivePath };
540
- }
541
- const raw = await fs.readFile(effectivePath, "utf-8");
542
- const parsed = JSON.parse(raw);
543
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
544
- workflowDef = parsed;
545
- }
546
- else {
547
- return { error: "workflow_def_path must point to a JSON object", path: effectivePath };
548
- }
549
- }
550
- catch (err) {
551
- const msg = err instanceof Error ? err.message : String(err);
552
- return { error: `Failed to read workflow_def from path: ${msg}`, path: effectivePath };
553
- }
554
- }
555
- // Route to handleWorkflow for get/deploy (the only public modes)
556
- const client = createClient(normalizedArgs.env);
557
- switch (mode) {
558
- case "get": {
559
- // Return workflow data for LLM to analyze/modify
560
- return handleWorkflow({
561
- mode: "get",
562
- persona_id: personaId,
563
- env: normalizedArgs.env,
564
- }, client, () => undefined);
565
- }
566
- case "validate": {
567
- // Static validation with path enumeration
568
- return handleWorkflow({
569
- mode: "validate",
570
- persona_id: personaId,
571
- workflow_def: normalizedArgs.workflow_def,
572
- workflow_spec: normalizedArgs.workflow_spec,
573
- validation_type: normalizedArgs.validation_type,
574
- max_paths: normalizedArgs.max_paths,
575
- timeout_ms: normalizedArgs.timeout_ms,
576
- env: normalizedArgs.env,
577
- }, client, () => undefined);
578
- }
579
- case "deploy": {
580
- if (!personaId) {
581
- return { error: 'persona_id is required for workflow(mode="deploy")' };
582
- }
583
- // Pre-deploy snapshot + stale-state protection (out-of-band changes)
584
- const targetEnv = normalizedArgs.env ?? getDefaultEnvName();
585
- let versionCreated;
586
- try {
587
- const personaBefore = await client.getPersonaById(personaId);
588
- if (!personaBefore) {
589
- return { error: `Persona not found: ${personaId}` };
590
- }
591
- const currentFp = fingerprintPersona(personaBefore);
592
- if (!force && !baseFingerprint) {
593
- return {
594
- error: "base_fingerprint is required for workflow deploy (stale-state protection)",
595
- persona_id: personaId,
596
- current_fingerprint: currentFp,
597
- hint: "Run workflow(mode='get', persona_id='...') immediately before deploying and pass fingerprint as base_fingerprint. Use force=true only for emergency overrides.",
598
- };
599
- }
600
- if (!force && baseFingerprint && baseFingerprint !== currentFp) {
601
- return {
602
- error: "Persona changed since you last fetched it (fingerprint mismatch)",
603
- persona_id: personaId,
604
- base_fingerprint: baseFingerprint,
605
- current_fingerprint: currentFp,
606
- hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. Use force=true only if you intend to overwrite out-of-band changes.",
607
- };
608
- }
609
- const storage = createVersionStorage(process.cwd());
610
- const engine = createVersionPolicyEngine(storage);
611
- const snap = engine.forceCreateVersion(personaBefore, {
612
- environment: targetEnv,
613
- tenant_id: targetEnv,
614
- message: "Pre-deploy snapshot",
615
- created_by: "mcp-toolkit",
616
- });
617
- if (!snap.created || !snap.version) {
618
- if (!force) {
619
- return {
620
- error: "Failed to create pre-deploy snapshot (required before deploy)",
621
- persona_id: personaId,
622
- details: snap.reason,
623
- hint: "Fix snapshotting (workspace storage) or retry with force=true for emergency override.",
624
- };
625
- }
626
- }
627
- else {
628
- versionCreated = { id: snap.version.id, version_name: snap.version.version_name };
629
- }
630
- }
631
- catch {
632
- // Snapshotting is required unless force=true
633
- if (!force) {
634
- return {
635
- error: "Failed to create pre-deploy snapshot (required before deploy)",
636
- persona_id: personaId,
637
- hint: "Retry after fixing local workspace write access, or use force=true for emergency override.",
638
- };
639
- }
640
- }
641
- // Route to handleWorkflow deploy
642
- const deployResult = await handleWorkflow({
643
- mode: "deploy",
644
- persona_id: personaId,
645
- workflow_def: workflowDef,
646
- proto_config: normalizedArgs.proto_config,
647
- env: normalizedArgs.env,
648
- force: force,
649
- strict_validation: normalizedArgs.strict_validation,
650
- }, client, () => undefined);
651
- // Add version info to result if created
652
- if (versionCreated && deployResult && typeof deployResult === "object") {
653
- deployResult.version_snapshot = versionCreated;
654
- }
655
- return deployResult;
656
- }
657
- // REMOVED modes - LLM does these
658
- case "analyze":
659
- case "compare":
660
- case "compile":
661
- case "optimize":
662
- case "generate": {
663
- return {
664
- error: `Mode "${mode}" removed - LLM does this thinking`,
665
- hint: "Use workflow(mode='get') to fetch data, then analyze/generate in your reasoning. Deploy with workflow(mode='deploy').",
666
- valid_modes: ["get", "validate", "deploy"],
667
- };
668
- }
669
- default: {
670
- // No mode or unknown mode - require explicit mode
671
- return {
672
- error: `Mode required. Valid modes: get, validate, deploy`,
673
- hint: "workflow(mode='get') returns data for LLM. workflow(mode='validate') validates specs. workflow(mode='deploy') executes LLM's workflow_def.",
674
- example: `workflow(mode="get", persona_id="...")`,
675
- };
676
- }
677
- }
678
- };
679
- // Unify sync modes: run | status | config
680
- toolHandlers.sync = async (args) => {
681
- const normalizedArgs = { ...(args ?? {}) };
682
- // Tool definition uses "method" (preview/execute/status), but legacy callers use "mode" (run/status/config)
683
- const rawMethod = normalizedArgs.method ? String(normalizedArgs.method) : normalizedArgs.mode ? String(normalizedArgs.mode) : "run";
684
- // Map tool method values to internal mode values
685
- const mode = rawMethod === "preview" ? "run" : rawMethod === "execute" ? "run" : rawMethod;
686
- // "preview" implies dry_run
687
- if (rawMethod === "preview") {
688
- normalizedArgs.dry_run = true;
689
- }
690
- // Support both old and new arg names
691
- const target = (normalizedArgs.target ?? normalizedArgs.target_env);
692
- const source = (normalizedArgs.source ?? normalizedArgs.source_env);
693
- const id = normalizedArgs.id;
694
- const identifier = normalizedArgs.identifier; // deprecated alias
695
- const idOrIdentifier = id ?? identifier;
696
- if (mode === "config") {
697
- return syncInfoImpl({ include_options: true });
698
- }
699
- if (mode === "status") {
700
- const env = normalizedArgs.env;
701
- if (normalizedArgs.list_synced === true) {
702
- if (!env)
703
- throw new Error('env is required for sync(mode="status", list_synced=true)');
704
- return syncInfoImpl({ list_synced: true, master_env: normalizedArgs.master_env, env });
705
- }
706
- if (idOrIdentifier) {
707
- if (!env)
708
- throw new Error('env is required for sync(mode="status", id="...")');
709
- const identifierToResolve = String(idOrIdentifier);
710
- const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifierToResolve);
711
- if (isUUID) {
712
- return syncInfoImpl({ persona_id: identifierToResolve, env });
713
- }
714
- // Name lookup: resolve to ID in env, then reuse persona_id path
715
- const client = createClient(env);
716
- const personas = await client.getPersonasForTenant();
717
- const match = personas.find((p) => p.name === identifierToResolve);
718
- if (!match)
719
- throw new Error(`AI Employee not found by name in ${env}: ${identifierToResolve}`);
720
- return syncInfoImpl({ persona_id: match.id, env });
721
- }
722
- // Default: overall sync status/config summary
723
- return syncInfoImpl({ include_options: normalizedArgs.include_options === true });
724
- }
725
- // mode === "run" (default)
726
- if (!target) {
727
- throw new Error('target (or target_env) is required for sync(mode="run")');
728
- }
729
- return syncRunImpl({
730
- identifier: idOrIdentifier,
731
- target_env: target,
732
- source_env: source,
733
- scope: normalizedArgs.scope,
734
- dry_run: normalizedArgs.dry_run,
735
- include_status: normalizedArgs.include_status,
736
- });
737
- };
738
- // Consolidated demo tool: kit | validate_kit | scenarios | consolidate | generate | validate | template
739
- toolHandlers.demo = async (args) => {
740
- const normalizedArgs = { ...(args ?? {}) };
741
- const mode = normalizedArgs.mode ? String(normalizedArgs.mode) : "template";
742
- // Deprecation warning - added to all responses
743
- const deprecationWarning = {
744
- _deprecation: {
745
- message: "The 'demo' tool is deprecated. Please use 'data' and 'persona' tools instead.",
746
- migration: {
747
- "demo(mode='kit')": "persona(from='demo-sales-sdr', include_data=true)",
748
- "demo(mode='generate')": "data(mode='generate', from='customer', count=5)",
749
- "demo(mode='scenarios')": "data(mode='templates')",
750
- "demo(mode='template')": "data(mode='templates', template='customer')",
751
- },
752
- },
753
- };
754
- switch (mode) {
755
- case "kit": {
756
- // Generate complete demo kit for a persona
757
- const personaId = String(normalizedArgs.persona_id ?? "");
758
- const scenarioId = String(normalizedArgs.scenario ?? "sales-sdr");
759
- if (!personaId) {
760
- throw new Error('demo(mode="kit") requires: persona_id');
761
- }
762
- // Import demo generator
763
- const { generateDemoKit, DEMO_SCENARIOS, generateDemoScriptMarkdown, validateDemoKit } = await import("./demo-generator.js");
764
- // Get scenario
765
- const scenario = DEMO_SCENARIOS[scenarioId];
766
- if (!scenario) {
767
- throw new Error(`Unknown scenario: ${scenarioId}. Available: ${Object.keys(DEMO_SCENARIOS).join(", ")}`);
768
- }
769
- // Get persona and workflow
770
- const client = await createClient(normalizedArgs.env);
771
- const persona = await client.getPersonaById(personaId);
772
- if (!persona) {
773
- throw new Error(`Persona not found: ${personaId}`);
774
- }
775
- const workflowDef = persona.workflow_def || {};
776
- const customQA = normalizedArgs.custom_qa;
777
- // Generate kit
778
- const kit = generateDemoKit(personaId, persona.name || personaId, workflowDef, scenario, customQA);
779
- // Generate markdown script
780
- const demoScript = generateDemoScriptMarkdown(kit);
781
- // Validate
782
- const validation = validateDemoKit(kit);
783
- return {
784
- success: true,
785
- persona_id: personaId,
786
- persona_name: persona.name,
787
- scenario: scenarioId,
788
- kit_summary: {
789
- kb_documents: kit.kb_documents.length,
790
- demo_questions: kit.demo_script.length,
791
- fixed_responses: kit.fixed_responses.length,
792
- validation_queries: kit.validation_queries.length,
793
- },
794
- validation,
795
- demo_script_preview: demoScript.slice(0, 2000) + (demoScript.length > 2000 ? "\n\n... (truncated)" : ""),
796
- kit,
797
- instructions: [
798
- "1. Upload KB documents to the persona's knowledge base",
799
- "2. Review the demo script and practice the questions",
800
- "3. Optionally apply fixed_responses for guaranteed fallbacks",
801
- "4. Run validation queries to verify demo readiness",
802
- "5. Conduct the demo with confidence!",
803
- ],
804
- };
805
- }
806
- case "validate_kit": {
807
- // Validate a persona's demo readiness
808
- const personaId = String(normalizedArgs.persona_id ?? "");
809
- if (!personaId) {
810
- throw new Error('demo(mode="validate_kit") requires: persona_id');
811
- }
812
- const { analyzeWorkflowForDemo, DEMO_SCENARIOS } = await import("./demo-generator.js");
813
- const client = await createClient(normalizedArgs.env);
814
- const persona = await client.getPersonaById(personaId);
815
- if (!persona) {
816
- throw new Error(`Persona not found: ${personaId}`);
817
- }
818
- const analysis = analyzeWorkflowForDemo(persona.workflow_def || {});
819
- // Check data sources
820
- const dataSourcesResult = await client.listDataSourceFiles(personaId);
821
- const dataSources = dataSourcesResult.files || [];
822
- const hasKnowledgeBase = dataSources.length > 0;
823
- const issues = [];
824
- if (!hasKnowledgeBase) {
825
- issues.push("No knowledge base documents uploaded - RAG search will fail");
826
- }
827
- if (analysis.intents.length === 0) {
828
- issues.push("No categorizer intents detected - workflow may not route correctly");
829
- }
830
- if (!analysis.has_search) {
831
- issues.push("No search nodes detected - cannot retrieve KB data");
832
- }
833
- // Suggest best scenario
834
- let suggestedScenario = "sales-sdr";
835
- for (const [id, scenario] of Object.entries(DEMO_SCENARIOS)) {
836
- const intentOverlap = scenario.intents.filter(i => analysis.intents.some(ai => ai.toLowerCase().includes(i.name.toLowerCase()))).length;
837
- if (intentOverlap > 0) {
838
- suggestedScenario = id;
839
- break;
840
- }
841
- }
842
- return {
843
- persona_id: personaId,
844
- persona_name: persona.name,
845
- ready: issues.length === 0,
846
- issues,
847
- workflow_analysis: analysis,
848
- knowledge_base: {
849
- has_documents: hasKnowledgeBase,
850
- document_count: dataSources.length,
851
- },
852
- suggested_scenario: suggestedScenario,
853
- next_steps: issues.length > 0
854
- ? issues.map((issue, i) => `${i + 1}. Fix: ${issue}`)
855
- : [`Generate demo kit: demo(mode="kit", persona_id="${personaId}", scenario="${suggestedScenario}")`],
856
- };
857
- }
858
- case "scenarios": {
859
- // List available demo scenarios
860
- const { DEMO_SCENARIOS } = await import("./demo-generator.js");
861
- return {
862
- scenarios: Object.entries(DEMO_SCENARIOS).map(([id, scenario]) => ({
863
- id,
864
- name: scenario.name,
865
- description: scenario.description,
866
- persona_types: scenario.persona_types,
867
- tags: scenario.tags,
868
- intent_count: scenario.intents.length,
869
- qa_count: scenario.qa_pairs.length,
870
- entity_types: scenario.entities.map(e => e.type),
871
- })),
872
- usage: 'demo(mode="kit", persona_id="...", scenario="<scenario_id>")',
873
- };
874
- }
875
- case "consolidate": {
876
- const source = String(normalizedArgs.source ?? "");
877
- const output = String(normalizedArgs.output ?? "");
878
- const entity = String(normalizedArgs.entity ?? "");
879
- if (!source || !output || !entity) {
880
- throw new Error('demo(mode="consolidate") requires: source, output, entity');
881
- }
882
- return handleConsolidateDemoData({
883
- source_dir: source,
884
- output_dir: output,
885
- entity_type: entity,
886
- primary_file: normalizedArgs.primary ?? `${entity}s.json`,
887
- joins: normalizedArgs.joins ?? [],
888
- tags: normalizedArgs.tags,
889
- });
890
- }
891
- case "generate": {
892
- const entity = String(normalizedArgs.entity ?? "");
893
- if (!entity)
894
- throw new Error('demo(mode="generate") requires: entity');
895
- return handleGenerateDemoDocument({
896
- entity_type: entity,
897
- data: normalizedArgs.data ?? {},
898
- related_data: normalizedArgs.related ?? {},
899
- output_path: normalizedArgs.output,
900
- tags: normalizedArgs.tags,
901
- });
902
- }
903
- case "validate": {
904
- return handleValidateDemoDocument({
905
- file_path: normalizedArgs.file,
906
- content: normalizedArgs.content,
907
- });
908
- }
909
- case "template": {
910
- const entity = String(normalizedArgs.entity ?? "");
911
- if (!entity)
912
- throw new Error('demo(mode="template") requires: entity');
913
- return handleGetDemoDataTemplate({
914
- entity_type: entity,
915
- include_example: normalizedArgs.include_example,
916
- });
917
- }
918
- default:
919
- throw new Error(`Unknown demo mode: ${mode}`);
920
- }
128
+ // V2 adapters — routing + transformation extracted to handler files
129
+ workflow: async (args) => {
130
+ return handleWorkflowAdapter(args, createClient, getDefaultEnvName);
131
+ },
132
+ sync: async (args) => {
133
+ return handleSyncAdapter(args, createClient, getDefaultEnvName, getSyncSDK);
134
+ },
135
+ demo: async (args) => {
136
+ return handleDemoAdapter(args, createClient);
137
+ },
138
+ debug: async (args) => {
139
+ return handleDebugAdapter(args, createClient, getDefaultEnvName);
140
+ },
921
141
  };
922
142
  // ─────────────────────────────────────────────────────────────────────────────
923
143
  // Helpers
@@ -928,24 +148,8 @@ toolHandlers.demo = async (args) => {
928
148
  */
929
149
  function determineOperation(toolName, args) {
930
150
  if (toolName === "persona") {
931
- if (args.analyze)
932
- return "analyze";
933
- if (args.update)
934
- return "update";
935
- if (args.create)
936
- return "create";
937
- if (args.delete)
938
- return "delete";
939
- if (args.snapshot)
940
- return "snapshot";
941
- if (args.history)
942
- return "history";
943
- if (args.restore)
944
- return "restore";
945
- if (args.compare)
946
- return "compare";
947
- if (args.sanitize)
948
- return "sanitize";
151
+ if (args.method)
152
+ return String(args.method);
949
153
  if (args.data)
950
154
  return "data";
951
155
  if (args.id)
@@ -953,24 +157,21 @@ function determineOperation(toolName, args) {
953
157
  return "list";
954
158
  }
955
159
  if (toolName === "catalog") {
956
- if (args.id)
957
- return "get";
958
- if (args.for)
959
- return "recommend"; // V2 schema uses 'for' parameter
960
- if (args.query)
961
- return "search";
160
+ if (args.method)
161
+ return String(args.method);
962
162
  return "list";
963
163
  }
964
164
  if (toolName === "sync") {
965
- if (args.execute)
966
- return "execute";
967
- if (args.status)
968
- return "status";
165
+ if (args.method)
166
+ return String(args.method);
969
167
  return "preview";
970
168
  }
971
169
  if (toolName === "toolkit_feedback") {
972
170
  return args.method ? String(args.method) : "submit";
973
171
  }
172
+ if (toolName === "debug") {
173
+ return args.method ? String(args.method) : "conversations";
174
+ }
974
175
  return toolName;
975
176
  }
976
177
  // ─────────────────────────────────────────────────────────────────────────────