@claudetools/tools 0.1.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.
@@ -0,0 +1,1108 @@
1
+ // =============================================================================
2
+ // MCP Tool Execution Handlers
3
+ // =============================================================================
4
+ import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { mcpLogger } from '../logger.js';
6
+ import { getDefaultProjectId, DEFAULT_USER_ID, lastContextUsed, setLastContextUsed } from '../helpers/config.js';
7
+ import { EXPERT_WORKERS, matchTaskToWorker } from '../helpers/workers.js';
8
+ import { searchMemory, addMemory, storeFact, getContext, getSummary, getEntities, injectContext, apiRequest } from '../helpers/api-client.js';
9
+ import { queryDependencies, analyzeImpact } from '../helpers/dependencies.js';
10
+ import { checkPatterns } from '../helpers/patterns.js';
11
+ import { formatContextForClaude } from '../helpers/formatter.js';
12
+ import { createTask, listTasks, getTask, claimTask, releaseTask, updateTaskStatus, addTaskContext, getTaskSummary, heartbeatTask, parseJsonArray, getDispatchableTasks, getExecutionContext, resolveTaskDependencies, } from '../helpers/tasks.js';
13
+ export function registerToolHandlers(server) {
14
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
15
+ const { name, arguments: args } = request.params;
16
+ // Resolve project ID - use provided arg, or get default (may throw if not configured)
17
+ let projectId;
18
+ try {
19
+ projectId = args?.project_id || getDefaultProjectId();
20
+ }
21
+ catch (error) {
22
+ const message = error instanceof Error ? error.message : 'Failed to resolve project ID';
23
+ mcpLogger.error('TOOL', `Project ID resolution failed for ${name}`, error);
24
+ return {
25
+ content: [
26
+ {
27
+ type: 'text',
28
+ text: `Error: ${message}`,
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ const timer = mcpLogger.startTimer();
35
+ // Log tool call
36
+ mcpLogger.toolCall(name, args || {});
37
+ try {
38
+ switch (name) {
39
+ case 'memory_explain': {
40
+ if (!lastContextUsed) {
41
+ return {
42
+ content: [
43
+ {
44
+ type: 'text',
45
+ text: 'No memory context was injected in the last response. This could mean:\n- The query did not trigger memory need detection\n- No relevant facts were found\n- Auto-inject is disabled',
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ const meta = lastContextUsed.metadata;
51
+ let explanation = '# Memory Context Explanation\n\n';
52
+ explanation += `**Memory Needed:** ${meta.memoryNeeded ? 'Yes' : 'No'}\n`;
53
+ explanation += `**Retrieval Time:** ${meta.retrievalTimeMs}ms\n`;
54
+ explanation += `**Facts Scored:** ${meta.factsScored}\n`;
55
+ explanation += `**Facts Included:** ${meta.factsIncluded}\n`;
56
+ explanation += `**Average Relevance:** ${(meta.avgRelevanceScore * 100).toFixed(1)}%\n\n`;
57
+ if (lastContextUsed.augmentedSystemPrompt) {
58
+ explanation += '## Injected Context\n\n';
59
+ explanation += '```\n' + lastContextUsed.augmentedSystemPrompt + '\n```\n';
60
+ }
61
+ return {
62
+ content: [{ type: 'text', text: explanation }],
63
+ };
64
+ }
65
+ case 'memory_inject': {
66
+ const query = args?.query;
67
+ const result = await injectContext(projectId, query);
68
+ // Store for explain
69
+ setLastContextUsed(result);
70
+ if (!result.augmentedSystemPrompt) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: 'text',
75
+ text: `No relevant context found for query. Reason: ${result.metadata.reason || 'below threshold'}`,
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: `Context injected (${result.metadata.factsIncluded} facts, ${result.metadata.retrievalTimeMs}ms):\n\n${result.augmentedSystemPrompt}`,
85
+ },
86
+ ],
87
+ };
88
+ }
89
+ case 'memory_search': {
90
+ const query = args?.query;
91
+ const limit = args?.limit || 10;
92
+ mcpLogger.searchQuery(query, 'hybrid');
93
+ const searchTimer = mcpLogger.startTimer();
94
+ const context = await searchMemory(projectId, query, limit);
95
+ mcpLogger.searchResults(context.relevant_facts.length, context.relevant_entities.length, searchTimer());
96
+ mcpLogger.toolResult(name, true, timer(), `${context.relevant_facts.length} facts, ${context.relevant_entities.length} entities`);
97
+ return {
98
+ content: [
99
+ {
100
+ type: 'text',
101
+ text: formatContextForClaude(context),
102
+ },
103
+ ],
104
+ };
105
+ }
106
+ case 'memory_add': {
107
+ const sessionId = args?.session_id;
108
+ const messages = args?.messages;
109
+ const extractFacts = args?.extract_facts ?? true;
110
+ const result = await addMemory(projectId, sessionId, messages, extractFacts);
111
+ return {
112
+ content: [
113
+ {
114
+ type: 'text',
115
+ text: `Added ${result.episode_ids.length} episode(s) to memory.`,
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ case 'memory_store_fact': {
121
+ const entity1 = args?.entity1;
122
+ const relationship = args?.relationship;
123
+ const entity2 = args?.entity2;
124
+ const context = args?.context;
125
+ const result = await storeFact(projectId, entity1, relationship, entity2, context);
126
+ mcpLogger.memoryStore(entity1, relationship, entity2);
127
+ mcpLogger.toolResult(name, true, timer(), `ID: ${result.fact_id}`);
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: `Stored fact: "${entity1} ${relationship} ${entity2}" (ID: ${result.fact_id})`,
133
+ },
134
+ ],
135
+ };
136
+ }
137
+ case 'memory_get_context': {
138
+ const query = args?.query;
139
+ const context = await getContext(projectId, query);
140
+ return {
141
+ content: [
142
+ {
143
+ type: 'text',
144
+ text: formatContextForClaude(context),
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ case 'memory_summary': {
150
+ const summary = await getSummary(projectId);
151
+ return {
152
+ content: [
153
+ {
154
+ type: 'text',
155
+ text: summary,
156
+ },
157
+ ],
158
+ };
159
+ }
160
+ case 'memory_list_entities': {
161
+ const entities = await getEntities(projectId);
162
+ if (entities.length === 0) {
163
+ return {
164
+ content: [{ type: 'text', text: 'No entities found in memory.' }],
165
+ };
166
+ }
167
+ const formatted = entities
168
+ .map((e) => `- **${e.name}** [${e.labels.join(', ')}]`)
169
+ .join('\n');
170
+ return {
171
+ content: [
172
+ {
173
+ type: 'text',
174
+ text: `## Known Entities\n\n${formatted}`,
175
+ },
176
+ ],
177
+ };
178
+ }
179
+ case 'query_dependencies': {
180
+ const functionName = args?.function_name;
181
+ const direction = args?.direction || 'both';
182
+ const depth = args?.depth || 1;
183
+ const queryTimer = mcpLogger.startTimer();
184
+ const result = await queryDependencies(projectId, functionName, direction, depth);
185
+ mcpLogger.queryDependencies(functionName, direction, result.forward.length + result.reverse.length, queryTimer());
186
+ let output = `# Dependencies for \`${functionName}\`\n\n`;
187
+ if (direction === 'forward' || direction === 'both') {
188
+ output += `## Forward Dependencies (What ${functionName} calls)\n\n`;
189
+ if (result.forward.length === 0) {
190
+ output += `No functions called by \`${functionName}\`\n\n`;
191
+ }
192
+ else {
193
+ result.forward.forEach((dep) => {
194
+ output += `- **${dep.function}**\n`;
195
+ output += ` - Context: ${dep.context}\n`;
196
+ });
197
+ output += '\n';
198
+ }
199
+ }
200
+ if (direction === 'reverse' || direction === 'both') {
201
+ output += `## Reverse Dependencies (What calls ${functionName})\n\n`;
202
+ if (result.reverse.length === 0) {
203
+ output += `No functions call \`${functionName}\`\n\n`;
204
+ }
205
+ else {
206
+ result.reverse.forEach((dep) => {
207
+ output += `- **${dep.function}**\n`;
208
+ output += ` - Context: ${dep.context}\n`;
209
+ });
210
+ output += '\n';
211
+ }
212
+ }
213
+ output += `\n**Summary:** ${result.forward.length} forward, ${result.reverse.length} reverse dependencies`;
214
+ return {
215
+ content: [
216
+ {
217
+ type: 'text',
218
+ text: output,
219
+ },
220
+ ],
221
+ };
222
+ }
223
+ case 'analyze_impact': {
224
+ const functionName = args?.function_name;
225
+ const analysisType = args?.analysis_type || 'change';
226
+ const maxDepth = Math.min(args?.max_depth || 3, 5);
227
+ const impactTimer = mcpLogger.startTimer();
228
+ const result = await analyzeImpact(projectId, functionName, analysisType, maxDepth);
229
+ mcpLogger.impactAnalysis(functionName, analysisType, result.riskLevel, result.totalAffected, impactTimer());
230
+ let output = `# Impact Analysis for \`${functionName}\`\n\n`;
231
+ output += `**Analysis Type:** ${analysisType}\n`;
232
+ output += `**Risk Level:** ${result.riskLevel}\n\n`;
233
+ // Direct callers
234
+ output += `## Direct Impact (Depth 1)\n\n`;
235
+ if (result.directCallers.length === 0) {
236
+ output += `No functions directly call \`${functionName}\`\n\n`;
237
+ }
238
+ else {
239
+ output += `Functions that call ${functionName} directly:\n\n`;
240
+ result.directCallers.forEach((caller) => {
241
+ output += `- **${caller.function}** (${caller.risk} risk)\n`;
242
+ });
243
+ output += '\n';
244
+ }
245
+ // Indirect callers
246
+ if (result.indirectCallers.length > 0) {
247
+ output += `## Indirect Impact (Depth 2+)\n\n`;
248
+ output += `Functions affected through call chain:\n\n`;
249
+ result.indirectCallers.forEach((caller) => {
250
+ output += `- **${caller.function}** (depth ${caller.depth})\n`;
251
+ output += ` - Path: ${caller.path.join(' -> ')}\n`;
252
+ });
253
+ output += '\n';
254
+ }
255
+ // Summary
256
+ output += `## Summary\n\n`;
257
+ output += `- **Direct affected:** ${result.directCallers.length} function(s)\n`;
258
+ output += `- **Indirect affected:** ${result.indirectCallers.length} function(s)\n`;
259
+ output += `- **Total affected:** ${result.totalAffected} function(s)\n`;
260
+ output += `- **Risk level:** ${result.riskLevel}\n\n`;
261
+ // Recommendations
262
+ output += `## Recommendations\n\n`;
263
+ result.recommendations.forEach((rec) => {
264
+ output += `- ${rec}\n`;
265
+ });
266
+ return {
267
+ content: [
268
+ {
269
+ type: 'text',
270
+ text: output,
271
+ },
272
+ ],
273
+ };
274
+ }
275
+ case 'check_patterns': {
276
+ const code = args?.code;
277
+ const filePath = args?.file_path;
278
+ const checkTypesArg = args?.check_types;
279
+ const checkTypes = checkTypesArg?.map(t => t) || ['all'];
280
+ const patternTimer = mcpLogger.startTimer();
281
+ const result = checkPatterns(code, checkTypes);
282
+ mcpLogger.patternCheck(code.length, result.warnings.length, result.securityScore, result.performanceScore, patternTimer());
283
+ let output = `# Pattern Analysis\n\n`;
284
+ if (filePath) {
285
+ output += `**File:** ${filePath}\n`;
286
+ }
287
+ output += `**Overall Risk:** ${result.overallRisk}\n`;
288
+ output += `**Security Score:** ${result.securityScore}/100\n`;
289
+ output += `**Performance Score:** ${result.performanceScore}/100\n\n`;
290
+ if (result.warnings.length === 0) {
291
+ output += `No patterns detected. Code looks clean.\n`;
292
+ }
293
+ else {
294
+ // Group warnings by type
295
+ const securityWarnings = result.warnings.filter(w => w.type === 'security');
296
+ const performanceWarnings = result.warnings.filter(w => w.type === 'performance');
297
+ if (securityWarnings.length > 0) {
298
+ output += `## Security Warnings (${securityWarnings.length})\n\n`;
299
+ securityWarnings.forEach((w) => {
300
+ output += `### ${w.severity}: ${w.pattern}\n`;
301
+ if (w.line)
302
+ output += `- **Line:** ${w.line}\n`;
303
+ output += `- **Issue:** ${w.description}\n`;
304
+ output += `- **Fix:** ${w.recommendation}\n\n`;
305
+ });
306
+ }
307
+ if (performanceWarnings.length > 0) {
308
+ output += `## Performance Warnings (${performanceWarnings.length})\n\n`;
309
+ performanceWarnings.forEach((w) => {
310
+ output += `### ${w.severity}: ${w.pattern}\n`;
311
+ if (w.line)
312
+ output += `- **Line:** ${w.line}\n`;
313
+ output += `- **Issue:** ${w.description}\n`;
314
+ output += `- **Fix:** ${w.recommendation}\n\n`;
315
+ });
316
+ }
317
+ }
318
+ output += `---\n`;
319
+ output += `**Total Warnings:** ${result.warnings.length}\n`;
320
+ output += `**Critical:** ${result.warnings.filter(w => w.severity === 'CRITICAL').length}\n`;
321
+ output += `**High:** ${result.warnings.filter(w => w.severity === 'HIGH').length}\n`;
322
+ output += `**Medium:** ${result.warnings.filter(w => w.severity === 'MEDIUM').length}\n`;
323
+ output += `**Low:** ${result.warnings.filter(w => w.severity === 'LOW').length}\n`;
324
+ return {
325
+ content: [
326
+ {
327
+ type: 'text',
328
+ text: output,
329
+ },
330
+ ],
331
+ };
332
+ }
333
+ // =========================================================================
334
+ // TASK MANAGEMENT TOOL HANDLERS
335
+ // =========================================================================
336
+ case 'task_plan_draft': {
337
+ // This tool does NOT create anything in the database
338
+ // It just formats a plan for user review
339
+ const goal = args?.goal;
340
+ const epicTitle = args?.epic_title;
341
+ const approach = args?.approach;
342
+ const context = args?.context;
343
+ const tasks = args?.tasks;
344
+ const risks = args?.risks;
345
+ const questions = args?.questions;
346
+ const priority = args?.priority || 'medium';
347
+ mcpLogger.toolResult(name, true, timer());
348
+ let output = `## Plan: ${epicTitle}\n\n`;
349
+ output += `**Goal:** ${goal}\n\n`;
350
+ if (context) {
351
+ output += `**Context:** ${context}\n\n`;
352
+ }
353
+ if (approach) {
354
+ output += `**Approach:** ${approach}\n\n`;
355
+ }
356
+ output += `### Tasks\n\n`;
357
+ tasks.forEach((t, i) => {
358
+ const effort = t.effort ? ` (${t.effort})` : '';
359
+ output += `${i + 1}. **${t.title}**${effort}\n`;
360
+ if (t.description)
361
+ output += ` ${t.description}\n`;
362
+ if (t.acceptance_criteria?.length) {
363
+ output += ` - Criteria: ${t.acceptance_criteria.join('; ')}\n`;
364
+ }
365
+ output += '\n';
366
+ });
367
+ if (risks?.length) {
368
+ output += `### Risks\n`;
369
+ risks.forEach(r => { output += `- ${r}\n`; });
370
+ output += '\n';
371
+ }
372
+ if (questions?.length) {
373
+ output += `### Questions\n`;
374
+ questions.forEach(q => { output += `- ${q}\n`; });
375
+ output += '\n';
376
+ }
377
+ output += `---\n`;
378
+ output += `Reply **"go"** to commit this plan, or provide feedback to adjust.\n`;
379
+ return {
380
+ content: [{ type: 'text', text: output }],
381
+ };
382
+ }
383
+ case 'task_plan': {
384
+ const goal = args?.goal;
385
+ const epicTitle = args?.epic_title;
386
+ const tasks = args?.tasks;
387
+ const priority = args?.priority || 'medium';
388
+ // Create the epic first
389
+ const epicResult = await createTask(DEFAULT_USER_ID, projectId, {
390
+ type: 'epic',
391
+ title: epicTitle,
392
+ description: goal,
393
+ priority,
394
+ });
395
+ const epic = epicResult.data;
396
+ const createdTasks = [];
397
+ // Create each task under the epic with worker assignment
398
+ for (const taskDef of tasks) {
399
+ // Match task to expert worker
400
+ const worker = matchTaskToWorker({
401
+ title: taskDef.title,
402
+ description: taskDef.description,
403
+ domain: taskDef.domain,
404
+ });
405
+ const taskResult = await createTask(DEFAULT_USER_ID, projectId, {
406
+ type: 'task',
407
+ title: taskDef.title,
408
+ description: taskDef.description,
409
+ parent_id: epic.id,
410
+ priority,
411
+ estimated_effort: taskDef.effort,
412
+ agent_type: worker.id,
413
+ domain: taskDef.domain,
414
+ });
415
+ createdTasks.push({ task: taskResult.data, worker });
416
+ }
417
+ mcpLogger.toolResult(name, true, timer());
418
+ // Group tasks by worker for parallel dispatch overview
419
+ const byWorker = new Map();
420
+ for (const item of createdTasks) {
421
+ const existing = byWorker.get(item.worker.id) || [];
422
+ existing.push(item);
423
+ byWorker.set(item.worker.id, existing);
424
+ }
425
+ let output = `# Work Plan Created\n\n`;
426
+ output += `## Epic: ${epic.title}\n`;
427
+ output += `**ID:** \`${epic.id}\`\n`;
428
+ output += `**Priority:** ${priority}\n\n`;
429
+ output += `**Goal:** ${goal}\n\n`;
430
+ output += `## Tasks (${createdTasks.length})\n\n`;
431
+ createdTasks.forEach((item, i) => {
432
+ output += `${i + 1}. **${item.task.title}**\n`;
433
+ output += ` - ID: \`${item.task.id}\`\n`;
434
+ output += ` - Worker: ${item.worker.name} (\`${item.worker.id}\`)\n`;
435
+ if (item.task.estimated_effort)
436
+ output += ` - Effort: ${item.task.estimated_effort}\n`;
437
+ if (item.task.description)
438
+ output += ` - ${item.task.description.slice(0, 100)}${item.task.description.length > 100 ? '...' : ''}\n`;
439
+ output += '\n';
440
+ });
441
+ output += `## Worker Distribution\n\n`;
442
+ for (const [workerId, items] of byWorker) {
443
+ const worker = EXPERT_WORKERS[workerId];
444
+ output += `- **${worker.name}**: ${items.length} task(s)\n`;
445
+ }
446
+ output += `\n---\n\n`;
447
+ output += `## Execution Options\n\n`;
448
+ output += `**Sequential:** Use \`task_start\` with each task ID.\n\n`;
449
+ output += `**Parallel:** Use \`task_dispatch\` to spawn workers:\n`;
450
+ output += `\`\`\`\ntask_dispatch(epic_id="${epic.id}")\n\`\`\`\n`;
451
+ return {
452
+ content: [{ type: 'text', text: output }],
453
+ };
454
+ }
455
+ case 'task_start': {
456
+ const taskId = args?.task_id;
457
+ const agentId = args?.agent_id || 'claude-code';
458
+ // Claim the task
459
+ const claimResult = await claimTask(DEFAULT_USER_ID, projectId, taskId, agentId, 60);
460
+ // Update status to in_progress
461
+ await updateTaskStatus(DEFAULT_USER_ID, projectId, taskId, 'in_progress', agentId);
462
+ mcpLogger.toolResult(name, true, timer());
463
+ const data = claimResult.data;
464
+ let output = `# Starting: ${data.task.title}\n\n`;
465
+ output += `**Task ID:** ${taskId}\n`;
466
+ output += `**Status:** in_progress\n`;
467
+ output += `**Lock expires:** ${data.lock_expires_at}\n\n`;
468
+ if (data.task.description) {
469
+ output += `## Description\n${data.task.description}\n\n`;
470
+ }
471
+ const criteria = parseJsonArray(data.task.acceptance_criteria);
472
+ if (criteria.length) {
473
+ output += `## Acceptance Criteria\n`;
474
+ criteria.forEach((c, i) => {
475
+ output += `${i + 1}. ${c}\n`;
476
+ });
477
+ output += '\n';
478
+ }
479
+ if (data.context?.length) {
480
+ output += `## Previous Context\n`;
481
+ data.context.forEach((c) => {
482
+ output += `### ${c.context_type} (${c.added_by})\n`;
483
+ output += `${c.content}\n\n`;
484
+ });
485
+ }
486
+ output += `---\n`;
487
+ output += `Use \`task_complete\` when done, or \`task_add_context\` to log progress.\n`;
488
+ return {
489
+ content: [{ type: 'text', text: output }],
490
+ };
491
+ }
492
+ case 'task_complete': {
493
+ const taskId = args?.task_id;
494
+ const summary = args?.summary;
495
+ const agentId = args?.agent_id || 'claude-code';
496
+ // Add completion context
497
+ await addTaskContext(DEFAULT_USER_ID, projectId, taskId, 'work_log', summary, agentId);
498
+ // Release and mark as done
499
+ const releaseResult = await releaseTask(DEFAULT_USER_ID, projectId, taskId, agentId, 'done', summary);
500
+ const task = releaseResult.data.task;
501
+ // Check for newly unblocked tasks (orchestration awareness)
502
+ const newlyUnblocked = await resolveTaskDependencies(DEFAULT_USER_ID, projectId, taskId, task.parent_id || undefined);
503
+ mcpLogger.toolResult(name, true, timer());
504
+ let output = `# Completed: ${task.title}\n\n`;
505
+ output += `**Task ID:** \`${taskId}\`\n`;
506
+ output += `**Status:** done ✅\n\n`;
507
+ output += `## Summary\n${summary}\n\n`;
508
+ // Show newly unblocked tasks for orchestration
509
+ if (newlyUnblocked.length > 0) {
510
+ output += `---\n\n`;
511
+ output += `## 🔓 Newly Unblocked Tasks (${newlyUnblocked.length})\n\n`;
512
+ for (const unblocked of newlyUnblocked) {
513
+ const worker = matchTaskToWorker({
514
+ title: unblocked.title,
515
+ description: unblocked.description || undefined,
516
+ domain: unblocked.domain || undefined,
517
+ });
518
+ output += `- **${unblocked.title}** (\`${unblocked.id}\`)\n`;
519
+ output += ` → Worker: ${worker.name}\n`;
520
+ }
521
+ output += `\n**Next:** Use \`task_dispatch\` to spawn parallel workers for unblocked tasks.\n`;
522
+ }
523
+ return {
524
+ content: [{ type: 'text', text: output }],
525
+ };
526
+ }
527
+ case 'task_create': {
528
+ const taskType = args?.type;
529
+ const title = args?.title;
530
+ const description = args?.description;
531
+ const parentId = args?.parent_id;
532
+ const priority = args?.priority;
533
+ const acceptanceCriteria = args?.acceptance_criteria;
534
+ const estimatedEffort = args?.estimated_effort;
535
+ const tags = args?.tags;
536
+ const blockedBy = args?.blocked_by;
537
+ const result = await createTask(DEFAULT_USER_ID, projectId, {
538
+ type: taskType,
539
+ title,
540
+ description,
541
+ parent_id: parentId,
542
+ priority,
543
+ acceptance_criteria: acceptanceCriteria,
544
+ estimated_effort: estimatedEffort,
545
+ tags,
546
+ blocked_by: blockedBy,
547
+ });
548
+ mcpLogger.toolResult(name, true, timer());
549
+ const task = result.data;
550
+ let output = `# Task Created\n\n`;
551
+ output += `**ID:** ${task.id}\n`;
552
+ output += `**Type:** ${task.type}\n`;
553
+ output += `**Title:** ${task.title}\n`;
554
+ output += `**Status:** ${task.status}\n`;
555
+ output += `**Priority:** ${task.priority}\n`;
556
+ if (task.parent_id)
557
+ output += `**Parent:** ${task.parent_id}\n`;
558
+ if (task.description)
559
+ output += `\n**Description:**\n${task.description}\n`;
560
+ const taskCriteria = parseJsonArray(task.acceptance_criteria);
561
+ if (taskCriteria.length) {
562
+ output += `\n**Acceptance Criteria:**\n`;
563
+ taskCriteria.forEach((c, i) => {
564
+ output += `${i + 1}. ${c}\n`;
565
+ });
566
+ }
567
+ return {
568
+ content: [{ type: 'text', text: output }],
569
+ };
570
+ }
571
+ case 'task_list': {
572
+ const filters = {
573
+ type: args?.type,
574
+ status: args?.status,
575
+ parent_id: args?.parent_id,
576
+ assigned_to: args?.assigned_to,
577
+ limit: args?.limit,
578
+ };
579
+ const result = await listTasks(DEFAULT_USER_ID, projectId, filters);
580
+ mcpLogger.toolResult(name, true, timer());
581
+ const tasks = result.data;
582
+ let output = `# Tasks (${tasks.length})\n\n`;
583
+ if (tasks.length === 0) {
584
+ output += `No tasks found matching the filters.\n`;
585
+ }
586
+ else {
587
+ // Group by type for better organization
588
+ const epics = tasks.filter(t => t.type === 'epic');
589
+ const regularTasks = tasks.filter(t => t.type === 'task');
590
+ const subtasks = tasks.filter(t => t.type === 'subtask');
591
+ const formatTask = (t) => {
592
+ const statusEmoji = {
593
+ backlog: '📋', ready: '🟢', in_progress: '🔄',
594
+ blocked: '🚫', review: '👀', done: '✅', cancelled: '❌'
595
+ };
596
+ const emoji = statusEmoji[t.status] || '📝';
597
+ let line = `- ${emoji} **${t.title}** (${t.id.slice(0, 8)}...)`;
598
+ line += ` [${t.status}]`;
599
+ if (t.priority !== 'medium')
600
+ line += ` [${t.priority}]`;
601
+ if (t.assigned_to)
602
+ line += ` → ${t.assigned_to}`;
603
+ return line;
604
+ };
605
+ if (epics.length > 0) {
606
+ output += `## Epics\n`;
607
+ epics.forEach(t => { output += formatTask(t) + '\n'; });
608
+ output += '\n';
609
+ }
610
+ if (regularTasks.length > 0) {
611
+ output += `## Tasks\n`;
612
+ regularTasks.forEach(t => { output += formatTask(t) + '\n'; });
613
+ output += '\n';
614
+ }
615
+ if (subtasks.length > 0) {
616
+ output += `## Subtasks\n`;
617
+ subtasks.forEach(t => { output += formatTask(t) + '\n'; });
618
+ }
619
+ }
620
+ return {
621
+ content: [{ type: 'text', text: output }],
622
+ };
623
+ }
624
+ case 'task_get': {
625
+ const taskId = args?.task_id;
626
+ const full = args?.full || false;
627
+ const result = await getTask(DEFAULT_USER_ID, projectId, taskId, full);
628
+ mcpLogger.toolResult(name, true, timer());
629
+ let output = '';
630
+ if (full) {
631
+ const data = result.data;
632
+ const task = data.task;
633
+ output = `# ${task.title}\n\n`;
634
+ output += `**ID:** ${task.id}\n`;
635
+ output += `**Type:** ${task.type}\n`;
636
+ output += `**Status:** ${task.status}\n`;
637
+ output += `**Priority:** ${task.priority}\n`;
638
+ if (task.assigned_to)
639
+ output += `**Assigned To:** ${task.assigned_to}\n`;
640
+ if (task.estimated_effort)
641
+ output += `**Effort:** ${task.estimated_effort}\n`;
642
+ const taskTags = parseJsonArray(task.tags);
643
+ if (taskTags.length)
644
+ output += `**Tags:** ${taskTags.join(', ')}\n`;
645
+ if (task.description)
646
+ output += `\n**Description:**\n${task.description}\n`;
647
+ const fullCriteria = parseJsonArray(task.acceptance_criteria);
648
+ if (fullCriteria.length) {
649
+ output += `\n## Acceptance Criteria\n`;
650
+ fullCriteria.forEach((c, i) => {
651
+ output += `${i + 1}. ${c}\n`;
652
+ });
653
+ }
654
+ if (data.parent) {
655
+ output += `\n## Parent\n`;
656
+ output += `- **${data.parent.title}** (${data.parent.id.slice(0, 8)}...) [${data.parent.status}]\n`;
657
+ }
658
+ if (data.subtasks?.length) {
659
+ output += `\n## Subtasks (${data.subtasks.length})\n`;
660
+ data.subtasks.forEach(s => {
661
+ output += `- ${s.title} (${s.id.slice(0, 8)}...) [${s.status}]\n`;
662
+ });
663
+ }
664
+ if (data.context?.length) {
665
+ output += `\n## Context (${data.context.length})\n`;
666
+ data.context.forEach(c => {
667
+ output += `### ${c.context_type} (by ${c.added_by})\n`;
668
+ output += `${c.content}\n\n`;
669
+ });
670
+ }
671
+ }
672
+ else {
673
+ const task = result.data;
674
+ output = `# ${task.title}\n\n`;
675
+ output += `**ID:** ${task.id}\n`;
676
+ output += `**Type:** ${task.type}\n`;
677
+ output += `**Status:** ${task.status}\n`;
678
+ output += `**Priority:** ${task.priority}\n`;
679
+ if (task.description)
680
+ output += `\n**Description:**\n${task.description}\n`;
681
+ }
682
+ return {
683
+ content: [{ type: 'text', text: output }],
684
+ };
685
+ }
686
+ case 'task_claim': {
687
+ const taskId = args?.task_id;
688
+ const agentId = args?.agent_id;
689
+ const lockDuration = args?.lock_duration_minutes || 30;
690
+ const result = await claimTask(DEFAULT_USER_ID, projectId, taskId, agentId, lockDuration);
691
+ mcpLogger.toolResult(name, true, timer());
692
+ const data = result.data;
693
+ let output = `# Task Claimed\n\n`;
694
+ output += `**Task:** ${data.task.title}\n`;
695
+ output += `**Claimed:** ${data.claimed ? '✅ Yes' : '❌ No'}\n`;
696
+ output += `**Lock Expires:** ${data.lock_expires_at}\n`;
697
+ output += `**Agent:** ${agentId}\n`;
698
+ if (data.context?.length) {
699
+ output += `\n## Previous Context\n`;
700
+ data.context.forEach(c => {
701
+ output += `### ${c.context_type}\n${c.content}\n\n`;
702
+ });
703
+ }
704
+ return {
705
+ content: [{ type: 'text', text: output }],
706
+ };
707
+ }
708
+ case 'task_release': {
709
+ const taskId = args?.task_id;
710
+ const agentId = args?.agent_id;
711
+ const newStatus = args?.new_status;
712
+ const workLog = args?.work_log;
713
+ const result = await releaseTask(DEFAULT_USER_ID, projectId, taskId, agentId, newStatus, workLog);
714
+ mcpLogger.toolResult(name, true, timer());
715
+ const data = result.data;
716
+ let output = `# Task Released\n\n`;
717
+ output += `**Task:** ${data.task.title}\n`;
718
+ output += `**Released:** ${data.released ? '✅ Yes' : '❌ No'}\n`;
719
+ output += `**New Status:** ${data.task.status}\n`;
720
+ if (workLog)
721
+ output += `**Work Log:** ${workLog}\n`;
722
+ return {
723
+ content: [{ type: 'text', text: output }],
724
+ };
725
+ }
726
+ case 'task_update_status': {
727
+ const taskId = args?.task_id;
728
+ const status = args?.status;
729
+ const agentId = args?.agent_id;
730
+ const result = await updateTaskStatus(DEFAULT_USER_ID, projectId, taskId, status, agentId);
731
+ mcpLogger.toolResult(name, true, timer());
732
+ const task = result.data;
733
+ let output = `# Status Updated\n\n`;
734
+ output += `**Task:** ${task.title}\n`;
735
+ output += `**New Status:** ${task.status}\n`;
736
+ output += `**Updated At:** ${task.updated_at}\n`;
737
+ return {
738
+ content: [{ type: 'text', text: output }],
739
+ };
740
+ }
741
+ case 'task_add_context': {
742
+ const taskId = args?.task_id;
743
+ const contextType = args?.context_type;
744
+ const content = args?.content;
745
+ const addedBy = args?.added_by;
746
+ const source = args?.source;
747
+ const result = await addTaskContext(DEFAULT_USER_ID, projectId, taskId, contextType, content, addedBy, source);
748
+ mcpLogger.toolResult(name, true, timer());
749
+ const ctx = result.data;
750
+ let output = `# Context Added\n\n`;
751
+ output += `**ID:** ${ctx.id}\n`;
752
+ output += `**Type:** ${ctx.context_type}\n`;
753
+ output += `**Added By:** ${ctx.added_by}\n`;
754
+ if (ctx.source)
755
+ output += `**Source:** ${ctx.source}\n`;
756
+ output += `\n**Content:**\n${ctx.content}\n`;
757
+ return {
758
+ content: [{ type: 'text', text: output }],
759
+ };
760
+ }
761
+ case 'task_summary': {
762
+ const result = await getTaskSummary(DEFAULT_USER_ID, projectId);
763
+ mcpLogger.toolResult(name, true, timer());
764
+ const data = result.data;
765
+ let output = `# Task Summary\n\n`;
766
+ output += `## By Status\n`;
767
+ const statusEmoji = {
768
+ backlog: '📋', ready: '🟢', in_progress: '🔄',
769
+ blocked: '🚫', review: '👀', done: '✅', cancelled: '❌'
770
+ };
771
+ Object.entries(data.by_status).forEach(([status, count]) => {
772
+ const emoji = statusEmoji[status] || '📝';
773
+ output += `- ${emoji} ${status}: **${count}**\n`;
774
+ });
775
+ output += `\n## By Type\n`;
776
+ Object.entries(data.by_type).forEach(([type, count]) => {
777
+ output += `- ${type}: **${count}**\n`;
778
+ });
779
+ output += `\n## Active Agents\n`;
780
+ output += `**${data.active_agents}** agent(s) currently working on tasks\n`;
781
+ if (data.recent_events?.length) {
782
+ output += `\n## Recent Activity\n`;
783
+ data.recent_events.slice(0, 5).forEach(e => {
784
+ output += `- ${e.event_type}: ${e.task_title} (${new Date(e.created_at).toLocaleString()})\n`;
785
+ });
786
+ }
787
+ return {
788
+ content: [{ type: 'text', text: output }],
789
+ };
790
+ }
791
+ case 'task_heartbeat': {
792
+ const taskId = args?.task_id;
793
+ const agentId = args?.agent_id;
794
+ const extendMinutes = args?.extend_minutes || 30;
795
+ const result = await heartbeatTask(DEFAULT_USER_ID, projectId, taskId, agentId, extendMinutes);
796
+ mcpLogger.toolResult(name, true, timer());
797
+ const data = result.data;
798
+ let output = `# Heartbeat\n\n`;
799
+ output += `**Extended:** ${data.extended ? '✅ Yes' : '❌ No'}\n`;
800
+ output += `**New Expiry:** ${data.new_expires_at}\n`;
801
+ return {
802
+ content: [{ type: 'text', text: output }],
803
+ };
804
+ }
805
+ // =========================================================================
806
+ // ORCHESTRATION HANDLERS
807
+ // =========================================================================
808
+ case 'task_dispatch': {
809
+ const epicId = args?.epic_id;
810
+ const maxParallel = args?.max_parallel || 5;
811
+ const dispatchable = await getDispatchableTasks(DEFAULT_USER_ID, projectId, epicId, maxParallel);
812
+ mcpLogger.toolResult(name, true, timer());
813
+ if (dispatchable.length === 0) {
814
+ return {
815
+ content: [{
816
+ type: 'text',
817
+ text: '# No Tasks Ready for Dispatch\n\nAll tasks are either completed, blocked, or already claimed by other workers.',
818
+ }],
819
+ };
820
+ }
821
+ let output = `# Tasks Ready for Dispatch\n\n`;
822
+ output += `**Count:** ${dispatchable.length} task(s) ready for parallel execution\n\n`;
823
+ for (const item of dispatchable) {
824
+ output += `---\n\n`;
825
+ output += `## ${item.task.title}\n`;
826
+ output += `**Task ID:** \`${item.task.id}\`\n`;
827
+ output += `**Worker:** ${item.worker.name} (\`${item.worker.id}\`)\n`;
828
+ output += `**Effort:** ${item.task.estimated_effort || 'Not estimated'}\n`;
829
+ if (item.task.description) {
830
+ output += `**Description:** ${item.task.description.slice(0, 200)}${item.task.description.length > 200 ? '...' : ''}\n`;
831
+ }
832
+ if (item.parentContext) {
833
+ output += `**Epic:** ${item.parentContext.title}\n`;
834
+ }
835
+ output += `**Capabilities:** ${item.worker.capabilities.join(', ')}\n\n`;
836
+ }
837
+ output += `---\n\n`;
838
+ output += `## Spawning Instructions\n\n`;
839
+ output += `Use the Task tool to spawn parallel workers:\n\n`;
840
+ output += `\`\`\`\n`;
841
+ for (const item of dispatchable) {
842
+ output += `Task(subagent_type="${item.worker.id}", prompt="Execute task ${item.task.id}: ${item.task.title}")\n`;
843
+ }
844
+ output += `\`\`\`\n`;
845
+ return {
846
+ content: [{ type: 'text', text: output }],
847
+ };
848
+ }
849
+ case 'task_execute': {
850
+ const taskId = args?.task_id;
851
+ const context = await getExecutionContext(DEFAULT_USER_ID, projectId, taskId);
852
+ mcpLogger.toolResult(name, true, timer());
853
+ let output = `# Execution Context for Worker\n\n`;
854
+ output += `**Task:** ${context.task.title}\n`;
855
+ output += `**Task ID:** \`${context.task.id}\`\n`;
856
+ output += `**Worker Type:** ${context.worker.name} (\`${context.worker.id}\`)\n`;
857
+ output += `**Status:** ${context.task.status}\n\n`;
858
+ output += `## System Prompt for Worker\n\n`;
859
+ output += `\`\`\`\n${context.systemPrompt}\`\`\`\n\n`;
860
+ if (context.parentTask) {
861
+ output += `## Parent Epic\n`;
862
+ output += `- **${context.parentTask.title}** (\`${context.parentTask.id}\`)\n`;
863
+ if (context.parentTask.description) {
864
+ output += `- ${context.parentTask.description}\n`;
865
+ }
866
+ output += `\n`;
867
+ }
868
+ if (context.siblingTasks && context.siblingTasks.length > 0) {
869
+ output += `## Sibling Tasks\n`;
870
+ for (const sibling of context.siblingTasks) {
871
+ const statusEmoji = { done: '✅', in_progress: '🔄', ready: '🟢', blocked: '🚫', backlog: '📋' }[sibling.status] || '📝';
872
+ output += `- ${statusEmoji} ${sibling.title} [${sibling.status}]\n`;
873
+ }
874
+ output += `\n`;
875
+ }
876
+ output += `## Worker Capabilities\n`;
877
+ output += context.worker.capabilities.map(c => `- ${c}`).join('\n') + '\n\n';
878
+ output += `## Domain Patterns\n`;
879
+ output += context.worker.domains.map(d => `- \`${d}\``).join('\n') + '\n';
880
+ return {
881
+ content: [{ type: 'text', text: output }],
882
+ };
883
+ }
884
+ case 'task_resolve_dependencies': {
885
+ const completedTaskId = args?.completed_task_id;
886
+ const epicId = args?.epic_id;
887
+ const newlyUnblocked = await resolveTaskDependencies(DEFAULT_USER_ID, projectId, completedTaskId, epicId);
888
+ mcpLogger.toolResult(name, true, timer());
889
+ if (newlyUnblocked.length === 0) {
890
+ return {
891
+ content: [{
892
+ type: 'text',
893
+ text: `# No New Tasks Unblocked\n\nCompletion of task \`${completedTaskId}\` did not unblock any waiting tasks.`,
894
+ }],
895
+ };
896
+ }
897
+ let output = `# Newly Unblocked Tasks\n\n`;
898
+ output += `Completion of task \`${completedTaskId}\` has unblocked **${newlyUnblocked.length}** task(s):\n\n`;
899
+ for (const task of newlyUnblocked) {
900
+ const worker = matchTaskToWorker({
901
+ title: task.title,
902
+ description: task.description || undefined,
903
+ domain: task.domain || undefined,
904
+ });
905
+ output += `- **${task.title}** (\`${task.id}\`) → ${worker.name}\n`;
906
+ }
907
+ output += `\n## Next Steps\n`;
908
+ output += `These tasks are now ready for dispatch. Use \`task_dispatch\` to spawn workers.\n`;
909
+ return {
910
+ content: [{ type: 'text', text: output }],
911
+ };
912
+ }
913
+ case 'task_list_workers': {
914
+ mcpLogger.toolResult(name, true, timer());
915
+ let output = `# Available Expert Workers\n\n`;
916
+ for (const [id, worker] of Object.entries(EXPERT_WORKERS)) {
917
+ output += `## ${worker.name}\n`;
918
+ output += `**ID:** \`${id}\`\n`;
919
+ output += `**Description:** ${worker.description}\n\n`;
920
+ output += `**Domain Patterns:**\n`;
921
+ output += worker.domains.map(d => `- \`${d}\``).join('\n') + '\n\n';
922
+ output += `**Capabilities:**\n`;
923
+ output += worker.capabilities.map(c => `- ${c}`).join('\n') + '\n\n';
924
+ output += `---\n\n`;
925
+ }
926
+ return {
927
+ content: [{ type: 'text', text: output }],
928
+ };
929
+ }
930
+ // =========================================================================
931
+ // CODEBASE MAPPING HANDLERS
932
+ // =========================================================================
933
+ case 'codebase_map': {
934
+ const result = await apiRequest(`/api/v1/codebase/${projectId}/map`);
935
+ mcpLogger.toolResult(name, true, timer());
936
+ if (!result.success || typeof result === 'object' && 'error' in result) {
937
+ return {
938
+ content: [{
939
+ type: 'text',
940
+ text: `# No Codebase Map Found\n\nThe codebase map hasn't been generated yet. Use the file watcher or API to generate one.`,
941
+ }],
942
+ };
943
+ }
944
+ // The API returns text/markdown directly
945
+ return {
946
+ content: [{ type: 'text', text: result }],
947
+ };
948
+ }
949
+ case 'codebase_find': {
950
+ const query = args?.query;
951
+ const searchType = args?.type || 'all';
952
+ const exported = args?.exported;
953
+ const limit = args?.limit || 20;
954
+ const params = new URLSearchParams();
955
+ params.set('q', query);
956
+ params.set('type', searchType);
957
+ if (exported !== undefined)
958
+ params.set('exported', String(exported));
959
+ params.set('limit', String(limit));
960
+ const result = await apiRequest(`/api/v1/codebase/${projectId}/search?${params}`);
961
+ mcpLogger.toolResult(name, true, timer());
962
+ if (!result.success || !result.data) {
963
+ return {
964
+ content: [{
965
+ type: 'text',
966
+ text: `# Search Failed\n\nNo codebase index found. Generate a map first.`,
967
+ }],
968
+ };
969
+ }
970
+ let output = `# Search Results: "${query}"\n\n`;
971
+ output += `Found **${result.data.count}** matches\n\n`;
972
+ if (result.data.results.length === 0) {
973
+ output += `No results found. Try a different query or check spelling.`;
974
+ }
975
+ else {
976
+ output += `| Type | Name | Location | Context |\n`;
977
+ output += `|------|------|----------|----------|\n`;
978
+ for (const r of result.data.results) {
979
+ const loc = r.line ? `${r.path}:${r.line}` : r.path;
980
+ output += `| ${r.type} | ${r.name} | \`${loc}\` | ${r.context || r.role || ''} |\n`;
981
+ }
982
+ }
983
+ return {
984
+ content: [{ type: 'text', text: output }],
985
+ };
986
+ }
987
+ case 'codebase_context': {
988
+ const target = args?.target;
989
+ const result = await apiRequest(`/api/v1/codebase/${projectId}/context/${encodeURIComponent(target)}`);
990
+ mcpLogger.toolResult(name, true, timer());
991
+ if (!result.success || !result.data) {
992
+ return {
993
+ content: [{
994
+ type: 'text',
995
+ text: `# Not Found\n\nTarget "${target}" not found in codebase index.`,
996
+ }],
997
+ };
998
+ }
999
+ const data = result.data;
1000
+ let output = `# Context: ${target}\n\n`;
1001
+ if (data.file) {
1002
+ output += `## File Info\n`;
1003
+ output += `- **Path:** \`${data.file.path}\`\n`;
1004
+ output += `- **Role:** ${data.file.role}\n`;
1005
+ output += `- **Lines:** ${data.file.linesOfCode}\n\n`;
1006
+ if (data.file.symbols.length > 0) {
1007
+ output += `## Symbols (${data.file.symbols.length})\n`;
1008
+ for (const s of data.file.symbols.slice(0, 15)) {
1009
+ output += `- ${s.exported ? '📤' : '📦'} \`${s.name}\` (${s.type}) line ${s.line}\n`;
1010
+ }
1011
+ if (data.file.symbols.length > 15) {
1012
+ output += `- ... and ${data.file.symbols.length - 15} more\n`;
1013
+ }
1014
+ output += '\n';
1015
+ }
1016
+ }
1017
+ if (data.symbol) {
1018
+ output += `## Symbol: ${data.symbol.name}\n`;
1019
+ for (const loc of data.symbol.locations) {
1020
+ output += `- ${loc.exported ? '📤' : '📦'} \`${loc.file}:${loc.line}\` (${loc.type})\n`;
1021
+ }
1022
+ output += '\n';
1023
+ }
1024
+ if (data.callers && data.callers.length > 0) {
1025
+ output += `## Called By (${data.callers.length})\n`;
1026
+ output += data.callers.map(c => `- \`${c}\``).join('\n') + '\n\n';
1027
+ }
1028
+ if (data.callees && data.callees.length > 0) {
1029
+ output += `## Calls (${data.callees.length})\n`;
1030
+ output += data.callees.map(c => `- \`${c}\``).join('\n') + '\n\n';
1031
+ }
1032
+ if (data.dependents && data.dependents.length > 0) {
1033
+ output += `## Imported By (${data.dependents.length})\n`;
1034
+ output += data.dependents.map(d => `- \`${d}\``).join('\n') + '\n\n';
1035
+ }
1036
+ if (data.dependencies && data.dependencies.length > 0) {
1037
+ output += `## Imports (${data.dependencies.length})\n`;
1038
+ output += data.dependencies.map(d => `- \`${d}\``).join('\n') + '\n\n';
1039
+ }
1040
+ return {
1041
+ content: [{ type: 'text', text: output }],
1042
+ };
1043
+ }
1044
+ case 'codebase_callgraph': {
1045
+ const functionName = args?.function_name;
1046
+ const depth = Math.min(args?.depth || 1, 3);
1047
+ const result = await apiRequest(`/api/v1/codebase/${projectId}/callgraph/${encodeURIComponent(functionName)}?depth=${depth}`);
1048
+ mcpLogger.toolResult(name, true, timer());
1049
+ if (!result.success || !result.data) {
1050
+ return {
1051
+ content: [{
1052
+ type: 'text',
1053
+ text: `# Call Graph Not Found\n\nFunction "${functionName}" not found in call graph.`,
1054
+ }],
1055
+ };
1056
+ }
1057
+ const data = result.data;
1058
+ let output = `# Call Graph: ${functionName}\n\n`;
1059
+ output += `**Depth:** ${data.depth}\n\n`;
1060
+ output += `## What ${functionName} Calls\n\n`;
1061
+ if (Object.keys(data.callees).length === 0) {
1062
+ output += `No outgoing calls found.\n\n`;
1063
+ }
1064
+ else {
1065
+ for (const [fn, calls] of Object.entries(data.callees)) {
1066
+ if (calls.length === 0)
1067
+ continue;
1068
+ output += `**${fn}** calls:\n`;
1069
+ output += calls.map(c => ` → ${c}`).join('\n') + '\n\n';
1070
+ }
1071
+ }
1072
+ output += `## What Calls ${functionName}\n\n`;
1073
+ if (Object.keys(data.callers).length === 0) {
1074
+ output += `No incoming calls found.\n\n`;
1075
+ }
1076
+ else {
1077
+ for (const [fn, callers] of Object.entries(data.callers)) {
1078
+ if (callers.length === 0)
1079
+ continue;
1080
+ output += `**${fn}** called by:\n`;
1081
+ output += callers.map(c => ` ← ${c}`).join('\n') + '\n\n';
1082
+ }
1083
+ }
1084
+ return {
1085
+ content: [{ type: 'text', text: output }],
1086
+ };
1087
+ }
1088
+ default:
1089
+ throw new Error(`Unknown tool: ${name}`);
1090
+ }
1091
+ }
1092
+ catch (error) {
1093
+ const message = error instanceof Error ? error.message : 'Unknown error';
1094
+ mcpLogger.error('TOOL', `${name} failed`, error);
1095
+ mcpLogger.toolResult(name, false, timer());
1096
+ return {
1097
+ content: [
1098
+ {
1099
+ type: 'text',
1100
+ text: `Error: ${message}`,
1101
+ },
1102
+ ],
1103
+ isError: true,
1104
+ };
1105
+ }
1106
+ });
1107
+ // -----------------------------------------------------------------------------
1108
+ }