agent-planner-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tools.js ADDED
@@ -0,0 +1,995 @@
1
+ /**
2
+ * MCP Tools Implementation
3
+ *
4
+ * Provides comprehensive planning tools for AI agents:
5
+ * - Full CRUD operations on all entities
6
+ * - Unified search across all scopes
7
+ * - Batch operations for efficiency
8
+ * - Rich context retrieval
9
+ * - Text responses for Claude Desktop compatibility
10
+ */
11
+
12
+ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
13
+ const apiClient = require('./api-client');
14
+
15
+ /**
16
+ * Format JSON data as text for Claude Desktop
17
+ */
18
+ function formatResponse(data) {
19
+ // If data is an error object with a message, return just the message
20
+ if (data && data.error) {
21
+ return {
22
+ isError: true,
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: data.error
27
+ }
28
+ ]
29
+ };
30
+ }
31
+
32
+ // For successful responses, stringify the data
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: JSON.stringify(data, null, 2)
38
+ }
39
+ ]
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Setup tools for the MCP server
45
+ * @param {Server} server - MCP server instance
46
+ */
47
+ function setupTools(server) {
48
+ // Suppress console logs when not in debug mode
49
+ if (process.env.NODE_ENV !== 'development') {
50
+ // Silent mode for production
51
+ } else {
52
+ console.error('Setting up MCP tools...');
53
+ }
54
+
55
+ // Handler for listing available tools
56
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
57
+ return {
58
+ tools: [
59
+ // ===== UNIFIED SEARCH TOOL =====
60
+ {
61
+ name: "search",
62
+ description: "Universal search tool for plans, nodes, and content",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ scope: {
67
+ type: "string",
68
+ description: "Search scope",
69
+ enum: ["global", "plans", "plan", "node"],
70
+ default: "global"
71
+ },
72
+ scope_id: {
73
+ type: "string",
74
+ description: "Plan ID (if scope is 'plan') or Node ID (if scope is 'node')"
75
+ },
76
+ query: {
77
+ type: "string",
78
+ description: "Search query"
79
+ },
80
+ filters: {
81
+ type: "object",
82
+ description: "Optional filters",
83
+ properties: {
84
+ status: {
85
+ type: "string",
86
+ description: "Filter by status",
87
+ enum: ["draft", "active", "completed", "archived", "not_started", "in_progress", "blocked"]
88
+ },
89
+ type: {
90
+ type: "string",
91
+ description: "Filter by type",
92
+ enum: ["plan", "node", "phase", "task", "milestone", "artifact", "log"]
93
+ },
94
+ limit: {
95
+ type: "integer",
96
+ description: "Maximum number of results",
97
+ default: 20
98
+ }
99
+ }
100
+ }
101
+ },
102
+ required: ["query"]
103
+ }
104
+ },
105
+
106
+ // ===== PLAN MANAGEMENT TOOLS =====
107
+ {
108
+ name: "list_plans",
109
+ description: "List all plans or filter by status",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ status: {
114
+ type: "string",
115
+ description: "Optional filter by plan status",
116
+ enum: ["draft", "active", "completed", "archived"]
117
+ }
118
+ }
119
+ }
120
+ },
121
+ {
122
+ name: "create_plan",
123
+ description: "Create a new plan",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ title: { type: "string", description: "Plan title" },
128
+ description: { type: "string", description: "Plan description" },
129
+ status: {
130
+ type: "string",
131
+ description: "Plan status",
132
+ enum: ["draft", "active", "completed", "archived"],
133
+ default: "draft"
134
+ }
135
+ },
136
+ required: ["title"]
137
+ }
138
+ },
139
+ {
140
+ name: "update_plan",
141
+ description: "Update an existing plan",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ plan_id: { type: "string", description: "Plan ID" },
146
+ title: { type: "string", description: "New plan title" },
147
+ description: { type: "string", description: "New plan description" },
148
+ status: {
149
+ type: "string",
150
+ description: "New plan status",
151
+ enum: ["draft", "active", "completed", "archived"]
152
+ }
153
+ },
154
+ required: ["plan_id"]
155
+ }
156
+ },
157
+ {
158
+ name: "delete_plan",
159
+ description: "Delete a plan",
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ plan_id: { type: "string", description: "Plan ID to delete" }
164
+ },
165
+ required: ["plan_id"]
166
+ }
167
+ },
168
+
169
+ // ===== NODE MANAGEMENT TOOLS =====
170
+ {
171
+ name: "create_node",
172
+ description: "Create a new node in a plan",
173
+ inputSchema: {
174
+ type: "object",
175
+ properties: {
176
+ plan_id: { type: "string", description: "Plan ID" },
177
+ parent_id: { type: "string", description: "Parent node ID (optional, defaults to root)" },
178
+ node_type: {
179
+ type: "string",
180
+ description: "Node type",
181
+ enum: ["phase", "task", "milestone"]
182
+ },
183
+ title: { type: "string", description: "Node title" },
184
+ description: { type: "string", description: "Node description" },
185
+ status: {
186
+ type: "string",
187
+ description: "Node status",
188
+ enum: ["not_started", "in_progress", "completed", "blocked"],
189
+ default: "not_started"
190
+ },
191
+ context: { type: "string", description: "Additional context for the node" },
192
+ agent_instructions: { type: "string", description: "Instructions for AI agents working on this node" },
193
+ acceptance_criteria: { type: "string", description: "Criteria for node completion" },
194
+ due_date: { type: "string", description: "Due date (ISO format)" },
195
+ metadata: { type: "object", description: "Additional metadata" }
196
+ },
197
+ required: ["plan_id", "node_type", "title"]
198
+ }
199
+ },
200
+ {
201
+ name: "update_node",
202
+ description: "Update a node's properties",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ plan_id: { type: "string", description: "Plan ID" },
207
+ node_id: { type: "string", description: "Node ID" },
208
+ title: { type: "string", description: "New node title" },
209
+ description: { type: "string", description: "New node description" },
210
+ status: {
211
+ type: "string",
212
+ description: "New node status",
213
+ enum: ["not_started", "in_progress", "completed", "blocked"]
214
+ },
215
+ context: { type: "string", description: "New context" },
216
+ agent_instructions: { type: "string", description: "New agent instructions" },
217
+ acceptance_criteria: { type: "string", description: "New acceptance criteria" },
218
+ due_date: { type: "string", description: "New due date (ISO format)" },
219
+ metadata: { type: "object", description: "New metadata" }
220
+ },
221
+ required: ["plan_id", "node_id"]
222
+ }
223
+ },
224
+ {
225
+ name: "delete_node",
226
+ description: "Delete a node and all its children",
227
+ inputSchema: {
228
+ type: "object",
229
+ properties: {
230
+ plan_id: { type: "string", description: "Plan ID" },
231
+ node_id: { type: "string", description: "Node ID to delete" }
232
+ },
233
+ required: ["plan_id", "node_id"]
234
+ }
235
+ },
236
+ {
237
+ name: "move_node",
238
+ description: "Move a node to a different parent or position",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ plan_id: { type: "string", description: "Plan ID" },
243
+ node_id: { type: "string", description: "Node ID to move" },
244
+ parent_id: { type: "string", description: "New parent node ID" },
245
+ order_index: { type: "integer", description: "New position index" }
246
+ },
247
+ required: ["plan_id", "node_id"]
248
+ }
249
+ },
250
+ {
251
+ name: "get_node_context",
252
+ description: "Get comprehensive context for a node including children, logs, and artifacts",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ plan_id: { type: "string", description: "Plan ID" },
257
+ node_id: { type: "string", description: "Node ID" }
258
+ },
259
+ required: ["plan_id", "node_id"]
260
+ }
261
+ },
262
+ {
263
+ name: "get_node_ancestry",
264
+ description: "Get the path from root to a specific node",
265
+ inputSchema: {
266
+ type: "object",
267
+ properties: {
268
+ plan_id: { type: "string", description: "Plan ID" },
269
+ node_id: { type: "string", description: "Node ID" }
270
+ },
271
+ required: ["plan_id", "node_id"]
272
+ }
273
+ },
274
+
275
+ // ===== LOGGING TOOLS (Replaces Comments) =====
276
+ {
277
+ name: "add_log",
278
+ description: "Add a log entry to a node (replaces comments)",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ plan_id: { type: "string", description: "Plan ID" },
283
+ node_id: { type: "string", description: "Node ID" },
284
+ content: { type: "string", description: "Log content" },
285
+ log_type: {
286
+ type: "string",
287
+ description: "Type of log entry",
288
+ enum: ["progress", "reasoning", "challenge", "decision", "comment"],
289
+ default: "comment"
290
+ },
291
+ tags: {
292
+ type: "array",
293
+ description: "Tags for categorizing the log entry",
294
+ items: { type: "string" }
295
+ }
296
+ },
297
+ required: ["plan_id", "node_id", "content"]
298
+ }
299
+ },
300
+ {
301
+ name: "get_logs",
302
+ description: "Get log entries for a node",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ plan_id: { type: "string", description: "Plan ID" },
307
+ node_id: { type: "string", description: "Node ID" },
308
+ log_type: {
309
+ type: "string",
310
+ description: "Filter by log type",
311
+ enum: ["progress", "reasoning", "challenge", "decision", "comment"]
312
+ },
313
+ limit: {
314
+ type: "integer",
315
+ description: "Maximum number of logs to return",
316
+ default: 50
317
+ }
318
+ },
319
+ required: ["plan_id", "node_id"]
320
+ }
321
+ },
322
+
323
+ // ===== ARTIFACT MANAGEMENT =====
324
+ {
325
+ name: "manage_artifact",
326
+ description: "Add, get, or search for artifacts",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ action: {
331
+ type: "string",
332
+ description: "Action to perform",
333
+ enum: ["add", "get", "search", "list"]
334
+ },
335
+ plan_id: { type: "string", description: "Plan ID" },
336
+ node_id: { type: "string", description: "Node ID" },
337
+ artifact_id: { type: "string", description: "Artifact ID (for 'get' action)" },
338
+ name: { type: "string", description: "Artifact name (for 'add' or 'search')" },
339
+ content_type: { type: "string", description: "Content MIME type (for 'add')" },
340
+ url: { type: "string", description: "URL where artifact can be accessed (for 'add')" },
341
+ metadata: { type: "object", description: "Additional metadata (for 'add')" }
342
+ },
343
+ required: ["action", "plan_id", "node_id"]
344
+ }
345
+ },
346
+
347
+ // ===== BATCH OPERATIONS =====
348
+ {
349
+ name: "batch_update_nodes",
350
+ description: "Update multiple nodes at once",
351
+ inputSchema: {
352
+ type: "object",
353
+ properties: {
354
+ plan_id: { type: "string", description: "Plan ID" },
355
+ updates: {
356
+ type: "array",
357
+ description: "List of node updates",
358
+ items: {
359
+ type: "object",
360
+ properties: {
361
+ node_id: { type: "string", description: "Node ID" },
362
+ status: {
363
+ type: "string",
364
+ enum: ["not_started", "in_progress", "completed", "blocked"]
365
+ },
366
+ title: { type: "string" },
367
+ description: { type: "string" }
368
+ },
369
+ required: ["node_id"]
370
+ }
371
+ }
372
+ },
373
+ required: ["plan_id", "updates"]
374
+ }
375
+ },
376
+ {
377
+ name: "batch_get_artifacts",
378
+ description: "Get multiple artifacts at once",
379
+ inputSchema: {
380
+ type: "object",
381
+ properties: {
382
+ plan_id: { type: "string", description: "Plan ID" },
383
+ artifact_requests: {
384
+ type: "array",
385
+ description: "List of artifact requests",
386
+ items: {
387
+ type: "object",
388
+ properties: {
389
+ node_id: { type: "string", description: "Node ID" },
390
+ artifact_id: { type: "string", description: "Artifact ID" }
391
+ },
392
+ required: ["node_id", "artifact_id"]
393
+ }
394
+ }
395
+ },
396
+ required: ["plan_id", "artifact_requests"]
397
+ }
398
+ },
399
+
400
+ // ===== PLAN STRUCTURE & SUMMARY =====
401
+ {
402
+ name: "get_plan_structure",
403
+ description: "Get the complete hierarchical structure of a plan",
404
+ inputSchema: {
405
+ type: "object",
406
+ properties: {
407
+ plan_id: { type: "string", description: "Plan ID" },
408
+ include_details: {
409
+ type: "boolean",
410
+ description: "Include full node details",
411
+ default: false
412
+ }
413
+ },
414
+ required: ["plan_id"]
415
+ }
416
+ },
417
+ {
418
+ name: "get_plan_summary",
419
+ description: "Get a comprehensive summary with statistics",
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ plan_id: { type: "string", description: "Plan ID" }
424
+ },
425
+ required: ["plan_id"]
426
+ }
427
+ }
428
+ ]
429
+ };
430
+ });
431
+
432
+ // Handler for calling tools
433
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
434
+ const { name, arguments: args } = request.params;
435
+
436
+ // Only log in development mode
437
+ if (process.env.NODE_ENV === 'development') {
438
+ console.error(`Calling tool: ${name} with arguments:`, args);
439
+ }
440
+
441
+ try {
442
+ // ===== UNIFIED SEARCH TOOL =====
443
+ if (name === "search") {
444
+ const { scope, scope_id, query, filters = {} } = args;
445
+
446
+ let results = [];
447
+
448
+ switch (scope) {
449
+ case "global":
450
+ // Global search across all plans
451
+ const searchWrapper = require('./tools/search-wrapper');
452
+ results = await searchWrapper.globalSearch(query);
453
+ break;
454
+
455
+ case "plans":
456
+ // Search only in plan titles/descriptions
457
+ const plans = await apiClient.plans.getPlans();
458
+
459
+ // Handle wildcard queries
460
+ if (query === '*' || query === '' || !query) {
461
+ // Return all plans (with optional status filter)
462
+ results = plans.filter(plan =>
463
+ !filters.status || plan.status === filters.status
464
+ );
465
+ } else {
466
+ // Normal search
467
+ const queryLower = query.toLowerCase();
468
+ results = plans.filter(plan => {
469
+ const titleMatch = plan.title.toLowerCase().includes(queryLower);
470
+ const descMatch = plan.description?.toLowerCase().includes(queryLower);
471
+ const statusMatch = !filters.status || plan.status === filters.status;
472
+ return (titleMatch || descMatch) && statusMatch;
473
+ });
474
+ }
475
+ break;
476
+
477
+ case "plan":
478
+ // Search within a specific plan
479
+ if (!scope_id) {
480
+ throw new Error("scope_id (plan_id) is required when scope is 'plan'");
481
+ }
482
+ const searchWrapperPlan = require('./tools/search-wrapper');
483
+ results = await searchWrapperPlan.searchPlan(scope_id, query);
484
+ break;
485
+
486
+ case "node":
487
+ // Search within a specific node's children
488
+ if (!scope_id) {
489
+ throw new Error("scope_id (node_id) is required when scope is 'node'");
490
+ }
491
+ // This would need a specific implementation
492
+ results = [];
493
+ break;
494
+
495
+ default:
496
+ // Default to global search
497
+ const searchWrapperDefault = require('./tools/search-wrapper');
498
+ results = await searchWrapperDefault.globalSearch(query);
499
+ }
500
+
501
+ // Apply filters
502
+ if (filters.type) {
503
+ results = results.filter(item => item.type === filters.type);
504
+ }
505
+ if (filters.limit) {
506
+ results = results.slice(0, filters.limit);
507
+ }
508
+
509
+ return formatResponse({
510
+ query,
511
+ scope,
512
+ scope_id,
513
+ filters,
514
+ count: results.length,
515
+ results
516
+ });
517
+ }
518
+
519
+ // ===== PLAN MANAGEMENT =====
520
+ if (name === "list_plans") {
521
+ const { status } = args;
522
+ const plans = await apiClient.plans.getPlans();
523
+ const filteredPlans = status ? plans.filter(p => p.status === status) : plans;
524
+ return formatResponse(filteredPlans);
525
+ }
526
+
527
+ if (name === "create_plan") {
528
+ const result = await apiClient.plans.createPlan(args);
529
+ return formatResponse(result);
530
+ }
531
+
532
+ if (name === "update_plan") {
533
+ const { plan_id, ...planData } = args;
534
+ const result = await apiClient.plans.updatePlan(plan_id, planData);
535
+ return formatResponse(result);
536
+ }
537
+
538
+ if (name === "delete_plan") {
539
+ const { plan_id } = args;
540
+ await apiClient.plans.deletePlan(plan_id);
541
+ return formatResponse({
542
+ success: true,
543
+ message: `Plan ${plan_id} deleted successfully`
544
+ });
545
+ }
546
+
547
+ // ===== NODE MANAGEMENT =====
548
+ if (name === "create_node") {
549
+ const { plan_id, ...nodeData } = args;
550
+ const result = await apiClient.nodes.createNode(plan_id, nodeData);
551
+ return formatResponse(result);
552
+ }
553
+
554
+ if (name === "update_node") {
555
+ const { plan_id, node_id, ...nodeData } = args;
556
+ const result = await apiClient.nodes.updateNode(plan_id, node_id, nodeData);
557
+ return formatResponse(result);
558
+ }
559
+
560
+ if (name === "delete_node") {
561
+ const { plan_id, node_id } = args;
562
+ await apiClient.nodes.deleteNode(plan_id, node_id);
563
+ return formatResponse({
564
+ success: true,
565
+ message: `Node ${node_id} and its children deleted successfully`
566
+ });
567
+ }
568
+
569
+ if (name === "move_node") {
570
+ const { plan_id, node_id, parent_id, order_index } = args;
571
+
572
+ try {
573
+ // Call the move endpoint - using POST as per API definition
574
+ const response = await apiClient.axiosInstance.post(
575
+ `/plans/${plan_id}/nodes/${node_id}/move`,
576
+ {
577
+ parent_id: parent_id || null,
578
+ order_index: order_index !== undefined ? order_index : null
579
+ }
580
+ );
581
+
582
+ return formatResponse(response.data);
583
+ } catch (error) {
584
+ // If endpoint still doesn't work, try updating the node directly
585
+ if (error.response && error.response.status === 404) {
586
+ console.error('Move endpoint not found, trying direct update');
587
+ // Fallback to updating the node's parent_id via regular update
588
+ const updateResponse = await apiClient.nodes.updateNode(plan_id, node_id, {
589
+ parent_id: parent_id || null,
590
+ order_index: order_index !== undefined ? order_index : null
591
+ });
592
+ return formatResponse(updateResponse);
593
+ }
594
+ throw error;
595
+ }
596
+ }
597
+
598
+ if (name === "get_node_context") {
599
+ const { plan_id, node_id } = args;
600
+
601
+ // Get node with context
602
+ const response = await apiClient.axiosInstance.get(
603
+ `/plans/${plan_id}/nodes/${node_id}/context`
604
+ );
605
+
606
+ return formatResponse(response.data);
607
+ }
608
+
609
+ if (name === "get_node_ancestry") {
610
+ const { plan_id, node_id } = args;
611
+
612
+ // Get node ancestry
613
+ const response = await apiClient.axiosInstance.get(
614
+ `/plans/${plan_id}/nodes/${node_id}/ancestry`
615
+ );
616
+
617
+ return formatResponse(response.data);
618
+ }
619
+
620
+ // ===== LOGGING =====
621
+ if (name === "add_log") {
622
+ const { plan_id, node_id, content, log_type = "comment", tags } = args;
623
+
624
+ const logData = {
625
+ content,
626
+ log_type,
627
+ tags
628
+ };
629
+
630
+ const result = await apiClient.logs.addLogEntry(plan_id, node_id, logData);
631
+ return formatResponse(result);
632
+ }
633
+
634
+ if (name === "get_logs") {
635
+ const { plan_id, node_id, log_type, limit = 50 } = args;
636
+
637
+ let logs = await apiClient.logs.getLogs(plan_id, node_id);
638
+
639
+ // Apply filters
640
+ if (log_type) {
641
+ logs = logs.filter(log => log.log_type === log_type);
642
+ }
643
+
644
+ // Apply limit
645
+ logs = logs.slice(0, limit);
646
+
647
+ return formatResponse(logs);
648
+ }
649
+
650
+ // ===== ARTIFACT MANAGEMENT =====
651
+ if (name === "manage_artifact") {
652
+ const { action, plan_id, node_id, ...params } = args;
653
+
654
+ switch (action) {
655
+ case "add":
656
+ const { name, content_type, url, metadata } = params;
657
+ const newArtifact = await apiClient.artifacts.addArtifact(plan_id, node_id, {
658
+ name,
659
+ content_type,
660
+ url,
661
+ metadata
662
+ });
663
+ return formatResponse(newArtifact);
664
+
665
+ case "get":
666
+ const { artifact_id } = params;
667
+ const artifact = await apiClient.artifacts.getArtifact(plan_id, node_id, artifact_id);
668
+ const content = await apiClient.artifacts.getArtifactContent(plan_id, node_id, artifact_id);
669
+ return formatResponse({
670
+ ...artifact,
671
+ content
672
+ });
673
+
674
+ case "search":
675
+ const { name: searchName } = params;
676
+ const artifacts = await apiClient.artifacts.getArtifacts(plan_id, node_id);
677
+ const searchLower = searchName.toLowerCase();
678
+ const matches = artifacts.filter(a =>
679
+ a.name.toLowerCase().includes(searchLower)
680
+ );
681
+ return formatResponse(matches);
682
+
683
+ case "list":
684
+ const allArtifacts = await apiClient.artifacts.getArtifacts(plan_id, node_id);
685
+ return formatResponse(allArtifacts);
686
+
687
+ default:
688
+ throw new Error(`Unknown artifact action: ${action}`);
689
+ }
690
+ }
691
+
692
+ // ===== BATCH OPERATIONS =====
693
+ if (name === "batch_update_nodes") {
694
+ const { plan_id, updates } = args;
695
+
696
+ const results = [];
697
+ const errors = [];
698
+
699
+ for (const update of updates) {
700
+ const { node_id, ...updateData } = update;
701
+ try {
702
+ const result = await apiClient.nodes.updateNode(plan_id, node_id, updateData);
703
+ results.push({ node_id, success: true, data: result });
704
+ } catch (error) {
705
+ errors.push({ node_id, success: false, error: error.message });
706
+ }
707
+ }
708
+
709
+ return formatResponse({
710
+ total: updates.length,
711
+ successful: results.length,
712
+ failed: errors.length,
713
+ results,
714
+ errors
715
+ });
716
+ }
717
+
718
+ if (name === "batch_get_artifacts") {
719
+ const { plan_id, artifact_requests } = args;
720
+
721
+ const results = [];
722
+ const errors = [];
723
+
724
+ for (const request of artifact_requests) {
725
+ const { node_id, artifact_id } = request;
726
+ try {
727
+ const artifact = await apiClient.artifacts.getArtifact(plan_id, node_id, artifact_id);
728
+ const content = await apiClient.artifacts.getArtifactContent(plan_id, node_id, artifact_id);
729
+ results.push({
730
+ node_id,
731
+ artifact_id,
732
+ success: true,
733
+ data: { ...artifact, content }
734
+ });
735
+ } catch (error) {
736
+ errors.push({
737
+ node_id,
738
+ artifact_id,
739
+ success: false,
740
+ error: error.message
741
+ });
742
+ }
743
+ }
744
+
745
+ return formatResponse({
746
+ total: artifact_requests.length,
747
+ successful: results.length,
748
+ failed: errors.length,
749
+ results,
750
+ errors
751
+ });
752
+ }
753
+
754
+ // ===== PLAN STRUCTURE & SUMMARY =====
755
+ if (name === "get_plan_structure") {
756
+ const { plan_id, include_details = false } = args;
757
+
758
+ const plan = await apiClient.plans.getPlan(plan_id);
759
+ const nodes = await apiClient.nodes.getNodes(plan_id);
760
+
761
+ // The API already returns a tree structure, not a flat list
762
+ // If it's already hierarchical, use it directly
763
+ let structure;
764
+ if (Array.isArray(nodes) && nodes.length > 0 && nodes[0].children !== undefined) {
765
+ // Already hierarchical - use directly
766
+ structure = nodes;
767
+ } else {
768
+ // Flat list - build hierarchy
769
+ structure = buildNodeHierarchy(nodes, include_details);
770
+ }
771
+
772
+ return formatResponse({
773
+ plan: {
774
+ id: plan.id,
775
+ title: plan.title,
776
+ status: plan.status,
777
+ description: plan.description
778
+ },
779
+ structure
780
+ });
781
+ }
782
+
783
+ if (name === "get_plan_summary") {
784
+ const { plan_id } = args;
785
+
786
+ const plan = await apiClient.plans.getPlan(plan_id);
787
+ const nodes = await apiClient.nodes.getNodes(plan_id);
788
+
789
+ // Calculate statistics
790
+ const stats = calculatePlanStatistics(nodes);
791
+
792
+ return formatResponse({
793
+ plan: {
794
+ id: plan.id,
795
+ title: plan.title,
796
+ status: plan.status,
797
+ description: plan.description,
798
+ created_at: plan.created_at,
799
+ updated_at: plan.updated_at
800
+ },
801
+ statistics: stats,
802
+ progress_percentage: stats.total > 0
803
+ ? ((stats.status_counts.completed / stats.total) * 100).toFixed(1)
804
+ : 0
805
+ });
806
+ }
807
+
808
+ // Tool not found
809
+ throw new Error(`Unknown tool: ${name}`);
810
+ } catch (error) {
811
+ if (process.env.NODE_ENV === 'development') {
812
+ console.error(`Error calling tool ${name}:`, error);
813
+ }
814
+ return {
815
+ isError: true,
816
+ content: [
817
+ {
818
+ type: "text",
819
+ text: `Error: ${error.message}`
820
+ }
821
+ ]
822
+ };
823
+ }
824
+ });
825
+
826
+ if (process.env.NODE_ENV === 'development') {
827
+ console.error('Tools setup complete');
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Build hierarchical node structure
833
+ */
834
+ function buildNodeHierarchy(nodes, includeDetails = false) {
835
+ if (!nodes || nodes.length === 0) {
836
+ return [];
837
+ }
838
+
839
+ // Debug logging to understand the structure
840
+ if (process.env.NODE_ENV === 'development') {
841
+ console.error('Building hierarchy for nodes:', nodes.length);
842
+ if (nodes[0]) {
843
+ console.error('Sample node:', {
844
+ id: nodes[0].id,
845
+ parent_id: nodes[0].parent_id,
846
+ node_type: nodes[0].node_type
847
+ });
848
+ }
849
+ }
850
+
851
+ const nodeMap = new Map();
852
+ const rootNodes = [];
853
+
854
+ // First pass: create all nodes in the map
855
+ nodes.forEach(node => {
856
+ const nodeData = includeDetails ? { ...node } : {
857
+ id: node.id,
858
+ title: node.title,
859
+ node_type: node.node_type,
860
+ status: node.status,
861
+ parent_id: node.parent_id,
862
+ order_index: node.order_index
863
+ };
864
+
865
+ // Initialize with empty children array
866
+ nodeMap.set(node.id, {
867
+ ...nodeData,
868
+ children: []
869
+ });
870
+ });
871
+
872
+ // Second pass: build parent-child relationships
873
+ nodes.forEach(node => {
874
+ const currentNode = nodeMap.get(node.id);
875
+
876
+ if (node.parent_id) {
877
+ const parent = nodeMap.get(node.parent_id);
878
+ if (parent) {
879
+ // Add as child to parent
880
+ parent.children.push(currentNode);
881
+ } else {
882
+ // Parent not found, treat as root
883
+ if (process.env.NODE_ENV === 'development') {
884
+ console.error(`Parent ${node.parent_id} not found for node ${node.id}`);
885
+ }
886
+ rootNodes.push(currentNode);
887
+ }
888
+ } else {
889
+ // No parent_id means it's a root node
890
+ rootNodes.push(currentNode);
891
+ }
892
+ });
893
+
894
+ // Special case: if we have a single root node of type 'root', return its children
895
+ if (rootNodes.length === 1 && rootNodes[0].node_type === 'root') {
896
+ // Return the root node itself with its children
897
+ const rootNode = rootNodes[0];
898
+
899
+ // Sort children by order_index
900
+ const sortNodes = (nodeArray) => {
901
+ nodeArray.sort((a, b) => {
902
+ const orderA = a.order_index ?? 999;
903
+ const orderB = b.order_index ?? 999;
904
+ return orderA - orderB;
905
+ });
906
+
907
+ nodeArray.forEach(node => {
908
+ if (node.children && node.children.length > 0) {
909
+ sortNodes(node.children);
910
+ }
911
+ });
912
+ };
913
+
914
+ sortNodes(rootNode.children);
915
+ return [rootNode]; // Return root with its properly sorted children
916
+ }
917
+
918
+ // Sort all root nodes and their children
919
+ const sortNodes = (nodeArray) => {
920
+ nodeArray.sort((a, b) => {
921
+ const orderA = a.order_index ?? 999;
922
+ const orderB = b.order_index ?? 999;
923
+ return orderA - orderB;
924
+ });
925
+
926
+ nodeArray.forEach(node => {
927
+ if (node.children && node.children.length > 0) {
928
+ sortNodes(node.children);
929
+ }
930
+ });
931
+ };
932
+
933
+ sortNodes(rootNodes);
934
+
935
+ return rootNodes;
936
+ }
937
+
938
+ /**
939
+ * Calculate plan statistics
940
+ */
941
+ function calculatePlanStatistics(nodes) {
942
+ const stats = {
943
+ total: 0,
944
+ type_counts: {
945
+ root: 0,
946
+ phase: 0,
947
+ task: 0,
948
+ milestone: 0
949
+ },
950
+ status_counts: {
951
+ not_started: 0,
952
+ in_progress: 0,
953
+ completed: 0,
954
+ blocked: 0
955
+ },
956
+ in_progress_nodes: [],
957
+ blocked_nodes: []
958
+ };
959
+
960
+ const processNode = (node) => {
961
+ stats.total++;
962
+
963
+ if (node.node_type && stats.type_counts[node.node_type] !== undefined) {
964
+ stats.type_counts[node.node_type]++;
965
+ }
966
+
967
+ if (node.status && stats.status_counts[node.status] !== undefined) {
968
+ stats.status_counts[node.status]++;
969
+
970
+ if (node.status === 'in_progress') {
971
+ stats.in_progress_nodes.push({
972
+ id: node.id,
973
+ title: node.title,
974
+ type: node.node_type
975
+ });
976
+ } else if (node.status === 'blocked') {
977
+ stats.blocked_nodes.push({
978
+ id: node.id,
979
+ title: node.title,
980
+ type: node.node_type
981
+ });
982
+ }
983
+ }
984
+
985
+ if (node.children && node.children.length > 0) {
986
+ node.children.forEach(processNode);
987
+ }
988
+ };
989
+
990
+ nodes.forEach(processNode);
991
+
992
+ return stats;
993
+ }
994
+
995
+ module.exports = { setupTools };