@specforge/mcp 1.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,1622 @@
1
+ /**
2
+ * MCP Tools Registry
3
+ *
4
+ * This module defines and exports all available MCP tools.
5
+ * All operations from Epics 3-10 are registered here with proper JSON Schema definitions.
6
+ *
7
+ * Tool categories:
8
+ * - Core Operations: Project, Specification, Epic, Ticket, Dependency
9
+ * - Context & AI: Implementation context, actionable tickets, critical path
10
+ * - Workflow: Work sessions, progress reporting
11
+ * - Testing: Test results, validation
12
+ * - Discovery: Report and resolve discoveries
13
+ * - Status: Status reports, summaries, analytics
14
+ * - Search: Search, find by file/tag, find related
15
+ * - Git: Link commits and PRs, get linked items
16
+ */
17
+ import { ValidationError, ApiError, validateToolArgs, formatMCPError, transformError, } from '../validation/index.js';
18
+ /**
19
+ * Get list of all available tools
20
+ *
21
+ * This returns the tool definitions that will be sent to the AI client
22
+ * in response to ListTools requests.
23
+ *
24
+ * @returns Array of tool definitions
25
+ */
26
+ export function getTools() {
27
+ return [
28
+ // ========================================================================
29
+ // Core Operations - Projects (Epic 3)
30
+ // ========================================================================
31
+ {
32
+ name: 'list_projects',
33
+ description: 'List all projects accessible to the current user',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {},
37
+ required: [],
38
+ },
39
+ },
40
+ {
41
+ name: 'get_project',
42
+ description: 'Get details of a specific project including summary counts',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ projectId: {
47
+ type: 'string',
48
+ description: 'The ID of the project to retrieve',
49
+ },
50
+ },
51
+ required: ['projectId'],
52
+ },
53
+ },
54
+ // ========================================================================
55
+ // Core Operations - Specifications (Epic 3)
56
+ // ========================================================================
57
+ {
58
+ name: 'list_specifications',
59
+ description: 'List specifications for a project with optional status filter',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ projectId: {
64
+ type: 'string',
65
+ description: 'The ID of the project',
66
+ },
67
+ status: {
68
+ type: 'array',
69
+ items: {
70
+ type: 'string',
71
+ enum: ['draft', 'planning', 'ready', 'in_progress', 'completed'],
72
+ },
73
+ description: 'Filter by status (optional)',
74
+ },
75
+ },
76
+ required: ['projectId'],
77
+ },
78
+ },
79
+ {
80
+ name: 'get_specification',
81
+ description: 'Get full specification with goals, requirements, constraints, and guardrails',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ specificationId: {
86
+ type: 'string',
87
+ description: 'The ID of the specification to retrieve',
88
+ },
89
+ },
90
+ required: ['specificationId'],
91
+ },
92
+ },
93
+ // ========================================================================
94
+ // Core Operations - Epics (Epic 3)
95
+ // ========================================================================
96
+ {
97
+ name: 'list_epics',
98
+ description: 'List epics for a specification with optional status filter',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ specificationId: {
103
+ type: 'string',
104
+ description: 'The ID of the specification',
105
+ },
106
+ status: {
107
+ type: 'string',
108
+ enum: ['todo', 'in_progress', 'completed'],
109
+ description: 'Filter by status (optional)',
110
+ },
111
+ },
112
+ required: ['specificationId'],
113
+ },
114
+ },
115
+ {
116
+ name: 'get_epic',
117
+ description: 'Get epic with scope, goals, acceptance criteria, and ticket summary',
118
+ inputSchema: {
119
+ type: 'object',
120
+ properties: {
121
+ epicId: {
122
+ type: 'string',
123
+ description: 'The ID of the epic to retrieve',
124
+ },
125
+ },
126
+ required: ['epicId'],
127
+ },
128
+ },
129
+ // ========================================================================
130
+ // Core Operations - Tickets (Epic 3)
131
+ // ========================================================================
132
+ {
133
+ name: 'list_tickets',
134
+ description: 'List tickets for an epic with optional status filter',
135
+ inputSchema: {
136
+ type: 'object',
137
+ properties: {
138
+ epicId: {
139
+ type: 'string',
140
+ description: 'The ID of the epic',
141
+ },
142
+ status: {
143
+ type: 'array',
144
+ items: {
145
+ type: 'string',
146
+ enum: ['todo', 'queue', 'in_progress', 'blocked', 'done'],
147
+ },
148
+ description: 'Filter by status (optional)',
149
+ },
150
+ },
151
+ required: ['epicId'],
152
+ },
153
+ },
154
+ {
155
+ name: 'get_ticket',
156
+ description: 'Get full ticket details including implementation steps, acceptance criteria, and dependencies',
157
+ inputSchema: {
158
+ type: 'object',
159
+ properties: {
160
+ ticketId: {
161
+ type: 'string',
162
+ description: 'The ID of the ticket to retrieve',
163
+ },
164
+ },
165
+ required: ['ticketId'],
166
+ },
167
+ },
168
+ // ========================================================================
169
+ // Core Operations - Dependencies (Epic 3)
170
+ // ========================================================================
171
+ {
172
+ name: 'get_ticket_dependencies',
173
+ description: 'Get dependency information for a ticket - what it blocks and what blocks it',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ ticketId: {
178
+ type: 'string',
179
+ description: 'The ID of the ticket',
180
+ },
181
+ },
182
+ required: ['ticketId'],
183
+ },
184
+ },
185
+ // ========================================================================
186
+ // Context & AI Tools (Epic 4)
187
+ // ========================================================================
188
+ {
189
+ name: 'get_implementation_context',
190
+ description: 'Get full implementation context for a ticket including spec, epic, dependencies, and patterns',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ ticketId: {
195
+ type: 'string',
196
+ description: 'The ID of the ticket',
197
+ },
198
+ },
199
+ required: ['ticketId'],
200
+ },
201
+ },
202
+ {
203
+ name: 'get_next_actionable_tickets',
204
+ description: 'Get tickets that can be worked on now (all dependencies satisfied)',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ specificationId: {
209
+ type: 'string',
210
+ description: 'The ID of the specification',
211
+ },
212
+ limit: {
213
+ type: 'number',
214
+ description: 'Maximum number of tickets to return (default: 5)',
215
+ },
216
+ },
217
+ required: ['specificationId'],
218
+ },
219
+ },
220
+ {
221
+ name: 'get_blocked_tickets',
222
+ description: 'Get tickets that are blocked with reasons why',
223
+ inputSchema: {
224
+ type: 'object',
225
+ properties: {
226
+ specificationId: {
227
+ type: 'string',
228
+ description: 'The ID of the specification',
229
+ },
230
+ },
231
+ required: ['specificationId'],
232
+ },
233
+ },
234
+ {
235
+ name: 'get_critical_path',
236
+ description: 'Get the critical path (longest dependency chain) for a specification',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ specificationId: {
241
+ type: 'string',
242
+ description: 'The ID of the specification',
243
+ },
244
+ },
245
+ required: ['specificationId'],
246
+ },
247
+ },
248
+ {
249
+ name: 'get_patterns',
250
+ description: 'Extract tech stack, code patterns, and naming conventions from specification',
251
+ inputSchema: {
252
+ type: 'object',
253
+ properties: {
254
+ specificationId: {
255
+ type: 'string',
256
+ description: 'The ID of the specification',
257
+ },
258
+ },
259
+ required: ['specificationId'],
260
+ },
261
+ },
262
+ // ========================================================================
263
+ // Workflow & Tracking (Epic 5)
264
+ // ========================================================================
265
+ {
266
+ name: 'start_work_session',
267
+ description: 'Start working on a ticket - validates dependencies are satisfied',
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {
271
+ ticketId: {
272
+ type: 'string',
273
+ description: 'The ID of the ticket to start',
274
+ },
275
+ },
276
+ required: ['ticketId'],
277
+ },
278
+ },
279
+ {
280
+ name: 'complete_work_session',
281
+ description: 'Mark a ticket as complete with summary and results',
282
+ inputSchema: {
283
+ type: 'object',
284
+ properties: {
285
+ ticketId: {
286
+ type: 'string',
287
+ description: 'The ID of the ticket',
288
+ },
289
+ summary: {
290
+ type: 'string',
291
+ description: 'Summary of work completed',
292
+ },
293
+ filesModified: {
294
+ type: 'array',
295
+ items: { type: 'string' },
296
+ description: 'List of files modified',
297
+ },
298
+ filesCreated: {
299
+ type: 'array',
300
+ items: { type: 'string' },
301
+ description: 'List of files created',
302
+ },
303
+ },
304
+ required: ['ticketId', 'summary'],
305
+ },
306
+ },
307
+ {
308
+ name: 'report_progress',
309
+ description: 'Report incremental progress on a ticket',
310
+ inputSchema: {
311
+ type: 'object',
312
+ properties: {
313
+ ticketId: {
314
+ type: 'string',
315
+ description: 'The ID of the ticket',
316
+ },
317
+ progress: {
318
+ type: 'number',
319
+ minimum: 0,
320
+ maximum: 100,
321
+ description: 'Progress percentage (0-100)',
322
+ },
323
+ message: {
324
+ type: 'string',
325
+ description: 'Progress message',
326
+ },
327
+ },
328
+ required: ['ticketId', 'progress'],
329
+ },
330
+ },
331
+ {
332
+ name: 'get_work_summary',
333
+ description: 'Get summary of completed work with optional scope filtering',
334
+ inputSchema: {
335
+ type: 'object',
336
+ properties: {
337
+ scope: {
338
+ type: 'string',
339
+ enum: ['epic', 'specification', 'project', 'user'],
340
+ description: 'Scope of the summary',
341
+ },
342
+ scopeId: {
343
+ type: 'string',
344
+ description: 'ID of the scoped entity (epicId, specificationId, or projectId)',
345
+ },
346
+ startDate: {
347
+ type: 'string',
348
+ description: 'Start date filter (ISO 8601)',
349
+ },
350
+ endDate: {
351
+ type: 'string',
352
+ description: 'End date filter (ISO 8601)',
353
+ },
354
+ },
355
+ required: ['scope'],
356
+ },
357
+ },
358
+ // ========================================================================
359
+ // Testing Tools (Epic 6)
360
+ // ========================================================================
361
+ {
362
+ name: 'report_test_results',
363
+ description: 'Report test results for a ticket',
364
+ inputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ ticketId: {
368
+ type: 'string',
369
+ description: 'The ID of the ticket',
370
+ },
371
+ passed: {
372
+ type: 'boolean',
373
+ description: 'Whether the tests passed',
374
+ },
375
+ testType: {
376
+ type: 'string',
377
+ enum: ['unit', 'integration', 'e2e', 'manual'],
378
+ description: 'Type of test',
379
+ },
380
+ summary: {
381
+ type: 'string',
382
+ description: 'Summary of test results',
383
+ },
384
+ output: {
385
+ type: 'string',
386
+ description: 'Test output (will be truncated if too long)',
387
+ },
388
+ },
389
+ required: ['ticketId', 'passed', 'testType'],
390
+ },
391
+ },
392
+ {
393
+ name: 'get_ticket_test_status',
394
+ description: 'Get test status and history for a ticket',
395
+ inputSchema: {
396
+ type: 'object',
397
+ properties: {
398
+ ticketId: {
399
+ type: 'string',
400
+ description: 'The ID of the ticket',
401
+ },
402
+ },
403
+ required: ['ticketId'],
404
+ },
405
+ },
406
+ {
407
+ name: 'validate_ticket_completion',
408
+ description: 'Validate that a ticket is ready to be marked complete',
409
+ inputSchema: {
410
+ type: 'object',
411
+ properties: {
412
+ ticketId: {
413
+ type: 'string',
414
+ description: 'The ID of the ticket',
415
+ },
416
+ },
417
+ required: ['ticketId'],
418
+ },
419
+ },
420
+ // ========================================================================
421
+ // Discovery Tools (Epic 7)
422
+ // ========================================================================
423
+ {
424
+ name: 'report_discovery',
425
+ description: 'Report a discovery (new requirement, bug, or improvement) found during implementation',
426
+ inputSchema: {
427
+ type: 'object',
428
+ properties: {
429
+ ticketId: {
430
+ type: 'string',
431
+ description: 'The ID of the ticket where discovery was made',
432
+ },
433
+ type: {
434
+ type: 'string',
435
+ enum: ['bug', 'requirement', 'improvement', 'question'],
436
+ description: 'Type of discovery',
437
+ },
438
+ title: {
439
+ type: 'string',
440
+ description: 'Short title for the discovery',
441
+ },
442
+ description: {
443
+ type: 'string',
444
+ description: 'Detailed description',
445
+ },
446
+ priority: {
447
+ type: 'string',
448
+ enum: ['low', 'medium', 'high', 'critical'],
449
+ description: 'Priority level',
450
+ },
451
+ },
452
+ required: ['ticketId', 'type', 'title'],
453
+ },
454
+ },
455
+ {
456
+ name: 'get_pending_discoveries',
457
+ description: 'Get pending discoveries that need resolution',
458
+ inputSchema: {
459
+ type: 'object',
460
+ properties: {
461
+ specificationId: {
462
+ type: 'string',
463
+ description: 'Filter by specification (optional)',
464
+ },
465
+ projectId: {
466
+ type: 'string',
467
+ description: 'Filter by project (optional)',
468
+ },
469
+ },
470
+ required: [],
471
+ },
472
+ },
473
+ {
474
+ name: 'resolve_discovery',
475
+ description: 'Resolve a discovery as created (new ticket) or dismissed',
476
+ inputSchema: {
477
+ type: 'object',
478
+ properties: {
479
+ discoveryId: {
480
+ type: 'string',
481
+ description: 'The ID of the discovery',
482
+ },
483
+ resolution: {
484
+ type: 'string',
485
+ enum: ['created', 'dismissed'],
486
+ description: 'Resolution type',
487
+ },
488
+ createdTicketId: {
489
+ type: 'string',
490
+ description: 'ID of created ticket (if resolution is "created")',
491
+ },
492
+ notes: {
493
+ type: 'string',
494
+ description: 'Resolution notes',
495
+ },
496
+ },
497
+ required: ['discoveryId', 'resolution'],
498
+ },
499
+ },
500
+ // ========================================================================
501
+ // Status & Analytics Tools (Epic 8)
502
+ // ========================================================================
503
+ {
504
+ name: 'get_specification_status',
505
+ description: 'Get detailed status for a specification including progress, time tracking, and blockers',
506
+ inputSchema: {
507
+ type: 'object',
508
+ properties: {
509
+ specificationId: {
510
+ type: 'string',
511
+ description: 'The ID of the specification',
512
+ },
513
+ },
514
+ required: ['specificationId'],
515
+ },
516
+ },
517
+ {
518
+ name: 'get_epic_status',
519
+ description: 'Get detailed status for an epic including progress and actionable tickets',
520
+ inputSchema: {
521
+ type: 'object',
522
+ properties: {
523
+ epicId: {
524
+ type: 'string',
525
+ description: 'The ID of the epic',
526
+ },
527
+ },
528
+ required: ['epicId'],
529
+ },
530
+ },
531
+ {
532
+ name: 'get_implementation_summary',
533
+ description: 'Get cross-specification implementation summary with velocity metrics',
534
+ inputSchema: {
535
+ type: 'object',
536
+ properties: {
537
+ projectId: {
538
+ type: 'string',
539
+ description: 'Filter by project (optional)',
540
+ },
541
+ },
542
+ required: [],
543
+ },
544
+ },
545
+ {
546
+ name: 'get_time_report',
547
+ description: 'Get time tracking report with estimated vs actual analysis',
548
+ inputSchema: {
549
+ type: 'object',
550
+ properties: {
551
+ scope: {
552
+ type: 'string',
553
+ enum: ['epic', 'specification', 'project'],
554
+ description: 'Scope of the report',
555
+ },
556
+ scopeId: {
557
+ type: 'string',
558
+ description: 'ID of the scoped entity',
559
+ },
560
+ },
561
+ required: ['scope', 'scopeId'],
562
+ },
563
+ },
564
+ {
565
+ name: 'get_blockers_report',
566
+ description: 'Get report of all blockers with duration and grouping',
567
+ inputSchema: {
568
+ type: 'object',
569
+ properties: {
570
+ specificationId: {
571
+ type: 'string',
572
+ description: 'Filter by specification (optional)',
573
+ },
574
+ projectId: {
575
+ type: 'string',
576
+ description: 'Filter by project (optional)',
577
+ },
578
+ },
579
+ required: [],
580
+ },
581
+ },
582
+ // ========================================================================
583
+ // Search Tools (Epic 9)
584
+ // ========================================================================
585
+ {
586
+ name: 'search_tickets',
587
+ description: 'Full-text search across tickets with relevance ranking',
588
+ inputSchema: {
589
+ type: 'object',
590
+ properties: {
591
+ query: {
592
+ type: 'string',
593
+ description: 'Search query',
594
+ },
595
+ specificationId: {
596
+ type: 'string',
597
+ description: 'Limit search to specification (optional)',
598
+ },
599
+ projectId: {
600
+ type: 'string',
601
+ description: 'Limit search to project (optional)',
602
+ },
603
+ limit: {
604
+ type: 'number',
605
+ description: 'Maximum results (default: 20)',
606
+ },
607
+ offset: {
608
+ type: 'number',
609
+ description: 'Pagination offset (default: 0)',
610
+ },
611
+ },
612
+ required: ['query'],
613
+ },
614
+ },
615
+ {
616
+ name: 'find_tickets_by_file',
617
+ description: 'Find tickets that modify or create specific files (supports glob patterns)',
618
+ inputSchema: {
619
+ type: 'object',
620
+ properties: {
621
+ filePath: {
622
+ type: 'string',
623
+ description: 'File path or glob pattern (e.g., "src/components/*.tsx")',
624
+ },
625
+ specificationId: {
626
+ type: 'string',
627
+ description: 'Limit search to specification (optional)',
628
+ },
629
+ projectId: {
630
+ type: 'string',
631
+ description: 'Limit search to project (optional)',
632
+ },
633
+ },
634
+ required: ['filePath'],
635
+ },
636
+ },
637
+ {
638
+ name: 'find_tickets_by_tag',
639
+ description: 'Find tickets by tags with AND/OR logic',
640
+ inputSchema: {
641
+ type: 'object',
642
+ properties: {
643
+ tags: {
644
+ type: 'array',
645
+ items: { type: 'string' },
646
+ description: 'Tags to search for',
647
+ },
648
+ matchAll: {
649
+ type: 'boolean',
650
+ description: 'If true, match all tags (AND). If false, match any (OR). Default: false',
651
+ },
652
+ specificationId: {
653
+ type: 'string',
654
+ description: 'Limit search to specification (optional)',
655
+ },
656
+ projectId: {
657
+ type: 'string',
658
+ description: 'Limit search to project (optional)',
659
+ },
660
+ },
661
+ required: ['tags'],
662
+ },
663
+ },
664
+ {
665
+ name: 'find_related_tickets',
666
+ description: 'Find tickets related to a given ticket based on files, tags, and tech stack',
667
+ inputSchema: {
668
+ type: 'object',
669
+ properties: {
670
+ ticketId: {
671
+ type: 'string',
672
+ description: 'The ID of the ticket',
673
+ },
674
+ limit: {
675
+ type: 'number',
676
+ description: 'Maximum results (default: 10)',
677
+ },
678
+ },
679
+ required: ['ticketId'],
680
+ },
681
+ },
682
+ // ========================================================================
683
+ // Git Integration Tools (Epic 10)
684
+ // ========================================================================
685
+ {
686
+ name: 'link_commit',
687
+ description: 'Associate a git commit with a ticket',
688
+ inputSchema: {
689
+ type: 'object',
690
+ properties: {
691
+ ticketId: {
692
+ type: 'string',
693
+ description: 'The ID of the ticket',
694
+ },
695
+ sha: {
696
+ type: 'string',
697
+ description: 'Full 40-character commit SHA',
698
+ },
699
+ message: {
700
+ type: 'string',
701
+ description: 'Commit message',
702
+ },
703
+ author: {
704
+ type: 'string',
705
+ description: 'Commit author',
706
+ },
707
+ repoUrl: {
708
+ type: 'string',
709
+ description: 'Repository URL for generating commit link',
710
+ },
711
+ },
712
+ required: ['ticketId', 'sha'],
713
+ },
714
+ },
715
+ {
716
+ name: 'link_pull_request',
717
+ description: 'Associate a pull request with a ticket',
718
+ inputSchema: {
719
+ type: 'object',
720
+ properties: {
721
+ ticketId: {
722
+ type: 'string',
723
+ description: 'The ID of the ticket',
724
+ },
725
+ prNumber: {
726
+ type: 'number',
727
+ description: 'PR number (provide this or prUrl)',
728
+ },
729
+ prUrl: {
730
+ type: 'string',
731
+ description: 'Full PR URL (GitHub, GitLab, or Bitbucket)',
732
+ },
733
+ title: {
734
+ type: 'string',
735
+ description: 'PR title',
736
+ },
737
+ author: {
738
+ type: 'string',
739
+ description: 'PR author',
740
+ },
741
+ repoUrl: {
742
+ type: 'string',
743
+ description: 'Repository URL (required if using prNumber)',
744
+ },
745
+ },
746
+ required: ['ticketId'],
747
+ },
748
+ },
749
+ {
750
+ name: 'get_ticket_commits',
751
+ description: 'Get all commits linked to a ticket',
752
+ inputSchema: {
753
+ type: 'object',
754
+ properties: {
755
+ ticketId: {
756
+ type: 'string',
757
+ description: 'The ID of the ticket',
758
+ },
759
+ limit: {
760
+ type: 'number',
761
+ description: 'Maximum results (default: 20)',
762
+ },
763
+ offset: {
764
+ type: 'number',
765
+ description: 'Pagination offset (default: 0)',
766
+ },
767
+ },
768
+ required: ['ticketId'],
769
+ },
770
+ },
771
+ {
772
+ name: 'get_ticket_prs',
773
+ description: 'Get all pull requests linked to a ticket',
774
+ inputSchema: {
775
+ type: 'object',
776
+ properties: {
777
+ ticketId: {
778
+ type: 'string',
779
+ description: 'The ID of the ticket',
780
+ },
781
+ limit: {
782
+ type: 'number',
783
+ description: 'Maximum results (default: 20)',
784
+ },
785
+ offset: {
786
+ type: 'number',
787
+ description: 'Pagination offset (default: 0)',
788
+ },
789
+ },
790
+ required: ['ticketId'],
791
+ },
792
+ },
793
+ // ========================================================================
794
+ // Creation Operations
795
+ // ========================================================================
796
+ {
797
+ name: 'create_specification',
798
+ description: 'Create a new specification with optional nested epics and tickets for bulk creation',
799
+ inputSchema: {
800
+ type: 'object',
801
+ properties: {
802
+ projectId: {
803
+ type: 'string',
804
+ description: 'The ID of the project to create the specification in',
805
+ },
806
+ title: {
807
+ type: 'string',
808
+ description: 'Specification title',
809
+ },
810
+ description: {
811
+ type: 'string',
812
+ description: 'Brief description (2-3 sentences)',
813
+ },
814
+ content: {
815
+ type: 'string',
816
+ description: 'Full markdown content',
817
+ },
818
+ specificationTypeId: {
819
+ type: 'string',
820
+ description: 'Specification type ID (uses default if not provided)',
821
+ },
822
+ background: {
823
+ type: 'string',
824
+ description: 'Problem context and why this exists',
825
+ },
826
+ scope: {
827
+ type: 'string',
828
+ description: "What's in/out of scope",
829
+ },
830
+ goals: {
831
+ type: 'array',
832
+ items: { type: 'string' },
833
+ description: 'Business/user/technical objectives',
834
+ },
835
+ requirements: {
836
+ type: 'array',
837
+ items: { type: 'string' },
838
+ description: 'Functional requirements',
839
+ },
840
+ acceptanceCriteria: {
841
+ type: 'array',
842
+ items: { type: 'string' },
843
+ description: 'Success criteria',
844
+ },
845
+ techStack: {
846
+ type: 'array',
847
+ items: { type: 'string' },
848
+ description: 'Technologies to use',
849
+ },
850
+ priority: {
851
+ type: 'string',
852
+ enum: ['high', 'medium', 'low'],
853
+ description: 'Priority level',
854
+ },
855
+ tags: {
856
+ type: 'array',
857
+ items: { type: 'string' },
858
+ description: 'Categorization tags',
859
+ },
860
+ estimatedHours: {
861
+ type: 'number',
862
+ description: 'Total estimated effort in hours',
863
+ },
864
+ epics: {
865
+ type: 'array',
866
+ description: 'Nested epics to create (bulk creation)',
867
+ items: {
868
+ type: 'object',
869
+ properties: {
870
+ title: { type: 'string' },
871
+ description: { type: 'string' },
872
+ objective: { type: 'string' },
873
+ tickets: {
874
+ type: 'array',
875
+ items: {
876
+ type: 'object',
877
+ properties: {
878
+ title: { type: 'string' },
879
+ description: { type: 'string' },
880
+ acceptanceCriteria: { type: 'array', items: { type: 'string' } },
881
+ estimatedHours: { type: 'number' },
882
+ },
883
+ required: ['title'],
884
+ },
885
+ },
886
+ },
887
+ required: ['title', 'description', 'objective'],
888
+ },
889
+ },
890
+ },
891
+ required: ['projectId', 'title'],
892
+ },
893
+ },
894
+ {
895
+ name: 'create_epic',
896
+ description: 'Create a new epic in a specification with optional nested tickets for bulk creation',
897
+ inputSchema: {
898
+ type: 'object',
899
+ properties: {
900
+ specificationId: {
901
+ type: 'string',
902
+ description: 'The ID of the specification',
903
+ },
904
+ title: {
905
+ type: 'string',
906
+ description: 'Epic title',
907
+ },
908
+ description: {
909
+ type: 'string',
910
+ description: 'Detailed description',
911
+ },
912
+ objective: {
913
+ type: 'string',
914
+ description: 'What this epic achieves',
915
+ },
916
+ epicNumber: {
917
+ type: 'number',
918
+ description: 'Epic number (auto-incremented if not provided)',
919
+ },
920
+ content: {
921
+ type: 'string',
922
+ description: 'Full epic content (markdown)',
923
+ },
924
+ scope: {
925
+ type: 'string',
926
+ description: "What's in/out of scope for this epic",
927
+ },
928
+ goals: {
929
+ type: 'array',
930
+ items: { type: 'string' },
931
+ description: 'Epic-specific goals',
932
+ },
933
+ acceptanceCriteria: {
934
+ type: 'array',
935
+ items: { type: 'string' },
936
+ description: 'Epic-level success criteria',
937
+ },
938
+ priority: {
939
+ type: 'string',
940
+ enum: ['high', 'medium', 'low'],
941
+ description: 'Priority level',
942
+ },
943
+ tags: {
944
+ type: 'array',
945
+ items: { type: 'string' },
946
+ description: 'Categorization tags',
947
+ },
948
+ estimatedHours: {
949
+ type: 'number',
950
+ description: 'Total estimated hours for epic',
951
+ },
952
+ tickets: {
953
+ type: 'array',
954
+ description: 'Nested tickets to create (bulk creation)',
955
+ items: {
956
+ type: 'object',
957
+ properties: {
958
+ title: { type: 'string' },
959
+ description: { type: 'string' },
960
+ acceptanceCriteria: { type: 'array', items: { type: 'string' } },
961
+ estimatedHours: { type: 'number' },
962
+ complexity: { type: 'string', enum: ['small', 'medium', 'large', 'xlarge'] },
963
+ },
964
+ required: ['title'],
965
+ },
966
+ },
967
+ },
968
+ required: ['specificationId', 'title', 'description', 'objective'],
969
+ },
970
+ },
971
+ {
972
+ name: 'create_ticket',
973
+ description: 'Create a new ticket in an epic',
974
+ inputSchema: {
975
+ type: 'object',
976
+ properties: {
977
+ epicId: {
978
+ type: 'string',
979
+ description: 'The ID of the epic',
980
+ },
981
+ title: {
982
+ type: 'string',
983
+ description: 'Ticket title',
984
+ },
985
+ ticketNumber: {
986
+ type: 'number',
987
+ description: 'Ticket number (auto-incremented if not provided)',
988
+ },
989
+ description: {
990
+ type: 'string',
991
+ description: 'Ticket description',
992
+ },
993
+ implementation: {
994
+ type: 'object',
995
+ description: 'Detailed implementation steps (JSON)',
996
+ },
997
+ technicalDetails: {
998
+ type: 'object',
999
+ description: 'Technical details like stack, endpoints (JSON)',
1000
+ },
1001
+ acceptanceCriteria: {
1002
+ type: 'array',
1003
+ items: { type: 'string' },
1004
+ description: 'Success criteria',
1005
+ },
1006
+ priority: {
1007
+ type: 'string',
1008
+ enum: ['high', 'medium', 'low'],
1009
+ description: 'Priority level',
1010
+ },
1011
+ complexity: {
1012
+ type: 'string',
1013
+ enum: ['small', 'medium', 'large', 'xlarge'],
1014
+ description: 'Size/complexity indicator',
1015
+ },
1016
+ tags: {
1017
+ type: 'array',
1018
+ items: { type: 'string' },
1019
+ description: 'Categorization tags',
1020
+ },
1021
+ estimatedHours: {
1022
+ type: 'number',
1023
+ description: 'Estimated effort in hours',
1024
+ },
1025
+ dependsOn: {
1026
+ type: 'array',
1027
+ items: { type: 'string' },
1028
+ description: 'Array of ticket IDs this ticket depends on',
1029
+ },
1030
+ },
1031
+ required: ['epicId', 'title'],
1032
+ },
1033
+ },
1034
+ {
1035
+ name: 'import_specification',
1036
+ description: 'Import a markdown file as a complete specification with AI-powered parsing. Parses epics from ## headers and tickets from ### headers.',
1037
+ inputSchema: {
1038
+ type: 'object',
1039
+ properties: {
1040
+ projectId: {
1041
+ type: 'string',
1042
+ description: 'The ID of the project to import into',
1043
+ },
1044
+ content: {
1045
+ type: 'string',
1046
+ description: 'Raw markdown content to parse and import',
1047
+ },
1048
+ title: {
1049
+ type: 'string',
1050
+ description: 'Override the parsed title (optional)',
1051
+ },
1052
+ specificationTypeId: {
1053
+ type: 'string',
1054
+ description: 'Specification type ID (uses default if not provided)',
1055
+ },
1056
+ },
1057
+ required: ['projectId', 'content'],
1058
+ },
1059
+ },
1060
+ ];
1061
+ }
1062
+ /**
1063
+ * Retry configuration for transient failures
1064
+ */
1065
+ const RETRY_CONFIG = {
1066
+ maxRetries: 3,
1067
+ initialDelayMs: 500,
1068
+ maxDelayMs: 5000,
1069
+ backoffMultiplier: 2,
1070
+ };
1071
+ /**
1072
+ * Transient error patterns that should trigger retry
1073
+ */
1074
+ const TRANSIENT_ERROR_PATTERNS = [
1075
+ /ETIMEDOUT/i,
1076
+ /ECONNRESET/i,
1077
+ /ECONNREFUSED/i,
1078
+ /socket hang up/i,
1079
+ /network error/i,
1080
+ /too many requests/i,
1081
+ /rate limit/i,
1082
+ /5\d{2}/, // 5xx status codes
1083
+ ];
1084
+ /**
1085
+ * Check if an error is transient and should be retried
1086
+ */
1087
+ function isTransientError(error) {
1088
+ return TRANSIENT_ERROR_PATTERNS.some(pattern => pattern.test(error.message));
1089
+ }
1090
+ /**
1091
+ * Sleep for a given number of milliseconds
1092
+ */
1093
+ function sleep(ms) {
1094
+ return new Promise(resolve => setTimeout(resolve, ms));
1095
+ }
1096
+ /**
1097
+ * Execute a function with retry logic for transient failures
1098
+ */
1099
+ async function withRetry(fn, toolName, debug) {
1100
+ let lastError = null;
1101
+ let delay = RETRY_CONFIG.initialDelayMs;
1102
+ for (let attempt = 1; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
1103
+ try {
1104
+ return await fn();
1105
+ }
1106
+ catch (error) {
1107
+ lastError = error instanceof Error ? error : new Error(String(error));
1108
+ if (attempt < RETRY_CONFIG.maxRetries && isTransientError(lastError)) {
1109
+ if (debug) {
1110
+ console.error(`[DEBUG] Tool ${toolName} attempt ${attempt} failed with transient error, retrying in ${delay}ms:`, lastError.message);
1111
+ }
1112
+ await sleep(delay);
1113
+ delay = Math.min(delay * RETRY_CONFIG.backoffMultiplier, RETRY_CONFIG.maxDelayMs);
1114
+ }
1115
+ else {
1116
+ break;
1117
+ }
1118
+ }
1119
+ }
1120
+ throw lastError;
1121
+ }
1122
+ /**
1123
+ * Create tool handlers that call the API with proper request formatting
1124
+ *
1125
+ * Each handler:
1126
+ * - Validates required arguments
1127
+ * - Formats the request for the API
1128
+ * - Parses and transforms the response
1129
+ * - Handles errors appropriately
1130
+ *
1131
+ * @param apiClient - The API client to use for making requests
1132
+ * @returns Record of tool handlers keyed by tool name
1133
+ */
1134
+ export function createToolHandlers(apiClient) {
1135
+ return {
1136
+ // ========================================================================
1137
+ // Core Operations - Projects
1138
+ // ========================================================================
1139
+ list_projects: async (_client, _args) => {
1140
+ return await apiClient.call('list_projects', {});
1141
+ },
1142
+ get_project: async (_client, args) => {
1143
+ validateRequired(args, 'projectId');
1144
+ return await apiClient.call('get_project', {
1145
+ projectId: args.projectId,
1146
+ });
1147
+ },
1148
+ // ========================================================================
1149
+ // Core Operations - Specifications
1150
+ // ========================================================================
1151
+ list_specifications: async (_client, args) => {
1152
+ validateRequired(args, 'projectId');
1153
+ return await apiClient.call('list_specifications', {
1154
+ projectId: args.projectId,
1155
+ status: args.status,
1156
+ });
1157
+ },
1158
+ get_specification: async (_client, args) => {
1159
+ validateRequired(args, 'specificationId');
1160
+ return await apiClient.call('get_specification', {
1161
+ specificationId: args.specificationId,
1162
+ });
1163
+ },
1164
+ // ========================================================================
1165
+ // Core Operations - Epics
1166
+ // ========================================================================
1167
+ list_epics: async (_client, args) => {
1168
+ validateRequired(args, 'specificationId');
1169
+ return await apiClient.call('list_epics', {
1170
+ specificationId: args.specificationId,
1171
+ status: args.status,
1172
+ });
1173
+ },
1174
+ get_epic: async (_client, args) => {
1175
+ validateRequired(args, 'epicId');
1176
+ return await apiClient.call('get_epic', {
1177
+ epicId: args.epicId,
1178
+ });
1179
+ },
1180
+ // ========================================================================
1181
+ // Core Operations - Tickets
1182
+ // ========================================================================
1183
+ list_tickets: async (_client, args) => {
1184
+ validateRequired(args, 'epicId');
1185
+ return await apiClient.call('list_tickets', {
1186
+ epicId: args.epicId,
1187
+ status: args.status,
1188
+ });
1189
+ },
1190
+ get_ticket: async (_client, args) => {
1191
+ validateRequired(args, 'ticketId');
1192
+ return await apiClient.call('get_ticket', {
1193
+ ticketId: args.ticketId,
1194
+ });
1195
+ },
1196
+ // ========================================================================
1197
+ // Core Operations - Dependencies
1198
+ // ========================================================================
1199
+ get_ticket_dependencies: async (_client, args) => {
1200
+ validateRequired(args, 'ticketId');
1201
+ return await apiClient.call('get_ticket_dependencies', {
1202
+ ticketId: args.ticketId,
1203
+ });
1204
+ },
1205
+ // ========================================================================
1206
+ // Context & AI Tools
1207
+ // ========================================================================
1208
+ get_implementation_context: async (_client, args) => {
1209
+ validateRequired(args, 'ticketId');
1210
+ return await apiClient.call('get_implementation_context', {
1211
+ ticketId: args.ticketId,
1212
+ });
1213
+ },
1214
+ get_next_actionable_tickets: async (_client, args) => {
1215
+ validateRequired(args, 'specificationId');
1216
+ return await apiClient.call('get_next_actionable_tickets', {
1217
+ specificationId: args.specificationId,
1218
+ limit: args.limit ?? 5,
1219
+ });
1220
+ },
1221
+ get_blocked_tickets: async (_client, args) => {
1222
+ validateRequired(args, 'specificationId');
1223
+ return await apiClient.call('get_blocked_tickets', {
1224
+ specificationId: args.specificationId,
1225
+ });
1226
+ },
1227
+ get_critical_path: async (_client, args) => {
1228
+ validateRequired(args, 'specificationId');
1229
+ return await apiClient.call('get_critical_path', {
1230
+ specificationId: args.specificationId,
1231
+ });
1232
+ },
1233
+ get_patterns: async (_client, args) => {
1234
+ validateRequired(args, 'specificationId');
1235
+ return await apiClient.call('get_patterns', {
1236
+ specificationId: args.specificationId,
1237
+ });
1238
+ },
1239
+ // ========================================================================
1240
+ // Workflow & Tracking
1241
+ // ========================================================================
1242
+ start_work_session: async (_client, args) => {
1243
+ validateRequired(args, 'ticketId');
1244
+ return await apiClient.call('start_work_session', {
1245
+ ticketId: args.ticketId,
1246
+ });
1247
+ },
1248
+ complete_work_session: async (_client, args) => {
1249
+ validateRequired(args, 'ticketId', 'summary');
1250
+ return await apiClient.call('complete_work_session', {
1251
+ ticketId: args.ticketId,
1252
+ summary: args.summary,
1253
+ filesModified: args.filesModified,
1254
+ filesCreated: args.filesCreated,
1255
+ });
1256
+ },
1257
+ report_progress: async (_client, args) => {
1258
+ validateRequired(args, 'ticketId', 'progress');
1259
+ validateRange(args.progress, 'progress', 0, 100);
1260
+ return await apiClient.call('report_progress', {
1261
+ ticketId: args.ticketId,
1262
+ progress: args.progress,
1263
+ message: args.message,
1264
+ });
1265
+ },
1266
+ get_work_summary: async (_client, args) => {
1267
+ validateRequired(args, 'scope');
1268
+ return await apiClient.call('get_work_summary', {
1269
+ scope: args.scope,
1270
+ scopeId: args.scopeId,
1271
+ startDate: args.startDate,
1272
+ endDate: args.endDate,
1273
+ });
1274
+ },
1275
+ // ========================================================================
1276
+ // Testing Tools
1277
+ // ========================================================================
1278
+ report_test_results: async (_client, args) => {
1279
+ validateRequired(args, 'ticketId', 'passed', 'testType');
1280
+ return await apiClient.call('report_test_results', {
1281
+ ticketId: args.ticketId,
1282
+ passed: args.passed,
1283
+ testType: args.testType,
1284
+ summary: args.summary,
1285
+ output: args.output,
1286
+ });
1287
+ },
1288
+ get_ticket_test_status: async (_client, args) => {
1289
+ validateRequired(args, 'ticketId');
1290
+ return await apiClient.call('get_ticket_test_status', {
1291
+ ticketId: args.ticketId,
1292
+ });
1293
+ },
1294
+ validate_ticket_completion: async (_client, args) => {
1295
+ validateRequired(args, 'ticketId');
1296
+ return await apiClient.call('validate_ticket_completion', {
1297
+ ticketId: args.ticketId,
1298
+ });
1299
+ },
1300
+ // ========================================================================
1301
+ // Discovery Tools
1302
+ // ========================================================================
1303
+ report_discovery: async (_client, args) => {
1304
+ validateRequired(args, 'ticketId', 'type', 'title');
1305
+ return await apiClient.call('report_discovery', {
1306
+ ticketId: args.ticketId,
1307
+ type: args.type,
1308
+ title: args.title,
1309
+ description: args.description,
1310
+ priority: args.priority,
1311
+ });
1312
+ },
1313
+ get_pending_discoveries: async (_client, args) => {
1314
+ return await apiClient.call('get_pending_discoveries', {
1315
+ specificationId: args.specificationId,
1316
+ projectId: args.projectId,
1317
+ });
1318
+ },
1319
+ resolve_discovery: async (_client, args) => {
1320
+ validateRequired(args, 'discoveryId', 'resolution');
1321
+ return await apiClient.call('resolve_discovery', {
1322
+ discoveryId: args.discoveryId,
1323
+ resolution: args.resolution,
1324
+ createdTicketId: args.createdTicketId,
1325
+ notes: args.notes,
1326
+ });
1327
+ },
1328
+ // ========================================================================
1329
+ // Status & Analytics Tools
1330
+ // ========================================================================
1331
+ get_specification_status: async (_client, args) => {
1332
+ validateRequired(args, 'specificationId');
1333
+ return await apiClient.call('get_specification_status', {
1334
+ specificationId: args.specificationId,
1335
+ });
1336
+ },
1337
+ get_epic_status: async (_client, args) => {
1338
+ validateRequired(args, 'epicId');
1339
+ return await apiClient.call('get_epic_status', {
1340
+ epicId: args.epicId,
1341
+ });
1342
+ },
1343
+ get_implementation_summary: async (_client, args) => {
1344
+ return await apiClient.call('get_implementation_summary', {
1345
+ projectId: args.projectId,
1346
+ });
1347
+ },
1348
+ get_time_report: async (_client, args) => {
1349
+ validateRequired(args, 'scope', 'scopeId');
1350
+ return await apiClient.call('get_time_report', {
1351
+ scope: args.scope,
1352
+ scopeId: args.scopeId,
1353
+ });
1354
+ },
1355
+ get_blockers_report: async (_client, args) => {
1356
+ return await apiClient.call('get_blockers_report', {
1357
+ specificationId: args.specificationId,
1358
+ projectId: args.projectId,
1359
+ });
1360
+ },
1361
+ // ========================================================================
1362
+ // Search Tools
1363
+ // ========================================================================
1364
+ search_tickets: async (_client, args) => {
1365
+ validateRequired(args, 'query');
1366
+ return await apiClient.call('search_tickets', {
1367
+ query: args.query,
1368
+ specificationId: args.specificationId,
1369
+ projectId: args.projectId,
1370
+ limit: args.limit ?? 20,
1371
+ offset: args.offset ?? 0,
1372
+ });
1373
+ },
1374
+ find_tickets_by_file: async (_client, args) => {
1375
+ validateRequired(args, 'filePath');
1376
+ return await apiClient.call('find_tickets_by_file', {
1377
+ filePath: args.filePath,
1378
+ specificationId: args.specificationId,
1379
+ projectId: args.projectId,
1380
+ });
1381
+ },
1382
+ find_tickets_by_tag: async (_client, args) => {
1383
+ validateRequired(args, 'tags');
1384
+ return await apiClient.call('find_tickets_by_tag', {
1385
+ tags: args.tags,
1386
+ matchAll: args.matchAll ?? false,
1387
+ specificationId: args.specificationId,
1388
+ projectId: args.projectId,
1389
+ });
1390
+ },
1391
+ find_related_tickets: async (_client, args) => {
1392
+ validateRequired(args, 'ticketId');
1393
+ return await apiClient.call('find_related_tickets', {
1394
+ ticketId: args.ticketId,
1395
+ limit: args.limit ?? 10,
1396
+ });
1397
+ },
1398
+ // ========================================================================
1399
+ // Git Integration Tools
1400
+ // ========================================================================
1401
+ link_commit: async (_client, args) => {
1402
+ validateRequired(args, 'ticketId', 'sha');
1403
+ validateSha(args.sha);
1404
+ return await apiClient.call('link_commit', {
1405
+ ticketId: args.ticketId,
1406
+ sha: args.sha,
1407
+ message: args.message,
1408
+ author: args.author,
1409
+ repoUrl: args.repoUrl,
1410
+ });
1411
+ },
1412
+ link_pull_request: async (_client, args) => {
1413
+ validateRequired(args, 'ticketId');
1414
+ if (!args.prNumber && !args.prUrl) {
1415
+ throw new Error('Either prNumber or prUrl must be provided');
1416
+ }
1417
+ return await apiClient.call('link_pull_request', {
1418
+ ticketId: args.ticketId,
1419
+ prNumber: args.prNumber,
1420
+ prUrl: args.prUrl,
1421
+ title: args.title,
1422
+ author: args.author,
1423
+ repoUrl: args.repoUrl,
1424
+ });
1425
+ },
1426
+ get_ticket_commits: async (_client, args) => {
1427
+ validateRequired(args, 'ticketId');
1428
+ return await apiClient.call('get_ticket_commits', {
1429
+ ticketId: args.ticketId,
1430
+ limit: args.limit ?? 20,
1431
+ offset: args.offset ?? 0,
1432
+ });
1433
+ },
1434
+ get_ticket_prs: async (_client, args) => {
1435
+ validateRequired(args, 'ticketId');
1436
+ return await apiClient.call('get_ticket_prs', {
1437
+ ticketId: args.ticketId,
1438
+ limit: args.limit ?? 20,
1439
+ offset: args.offset ?? 0,
1440
+ });
1441
+ },
1442
+ // ========================================================================
1443
+ // Creation Operations
1444
+ // ========================================================================
1445
+ create_specification: async (_client, args) => {
1446
+ validateRequired(args, 'projectId', 'title');
1447
+ return await apiClient.call('create_specification', {
1448
+ projectId: args.projectId,
1449
+ title: args.title,
1450
+ description: args.description,
1451
+ content: args.content,
1452
+ specificationTypeId: args.specificationTypeId,
1453
+ background: args.background,
1454
+ scope: args.scope,
1455
+ goals: args.goals,
1456
+ requirements: args.requirements,
1457
+ acceptanceCriteria: args.acceptanceCriteria,
1458
+ techStack: args.techStack,
1459
+ priority: args.priority,
1460
+ tags: args.tags,
1461
+ estimatedHours: args.estimatedHours,
1462
+ epics: args.epics,
1463
+ });
1464
+ },
1465
+ create_epic: async (_client, args) => {
1466
+ validateRequired(args, 'specificationId', 'title', 'description', 'objective');
1467
+ return await apiClient.call('create_epic', {
1468
+ specificationId: args.specificationId,
1469
+ title: args.title,
1470
+ description: args.description,
1471
+ objective: args.objective,
1472
+ epicNumber: args.epicNumber,
1473
+ content: args.content,
1474
+ scope: args.scope,
1475
+ goals: args.goals,
1476
+ acceptanceCriteria: args.acceptanceCriteria,
1477
+ priority: args.priority,
1478
+ tags: args.tags,
1479
+ estimatedHours: args.estimatedHours,
1480
+ order: args.order,
1481
+ tickets: args.tickets,
1482
+ });
1483
+ },
1484
+ create_ticket: async (_client, args) => {
1485
+ validateRequired(args, 'epicId', 'title');
1486
+ return await apiClient.call('create_ticket', {
1487
+ epicId: args.epicId,
1488
+ title: args.title,
1489
+ ticketNumber: args.ticketNumber,
1490
+ description: args.description,
1491
+ implementation: args.implementation,
1492
+ technicalDetails: args.technicalDetails,
1493
+ acceptanceCriteria: args.acceptanceCriteria,
1494
+ priority: args.priority,
1495
+ complexity: args.complexity,
1496
+ tags: args.tags,
1497
+ estimatedHours: args.estimatedHours,
1498
+ order: args.order,
1499
+ dependsOn: args.dependsOn,
1500
+ });
1501
+ },
1502
+ import_specification: async (_client, args) => {
1503
+ validateRequired(args, 'projectId', 'content');
1504
+ return await apiClient.call('import_specification', {
1505
+ projectId: args.projectId,
1506
+ content: args.content,
1507
+ title: args.title,
1508
+ specificationTypeId: args.specificationTypeId,
1509
+ });
1510
+ },
1511
+ };
1512
+ }
1513
+ /**
1514
+ * Validate that required arguments are present
1515
+ * @throws Error if any required argument is missing
1516
+ */
1517
+ function validateRequired(args, ...required) {
1518
+ for (const field of required) {
1519
+ if (args[field] === undefined || args[field] === null || args[field] === '') {
1520
+ throw new Error(`Missing required argument: ${field}`);
1521
+ }
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Validate that a number is within a range
1526
+ * @throws Error if the value is out of range
1527
+ */
1528
+ function validateRange(value, field, min, max) {
1529
+ if (typeof value !== 'number' || value < min || value > max) {
1530
+ throw new Error(`${field} must be between ${min} and ${max}`);
1531
+ }
1532
+ }
1533
+ /**
1534
+ * Validate SHA format (40 hexadecimal characters)
1535
+ * @throws Error if the SHA is invalid
1536
+ */
1537
+ function validateSha(sha) {
1538
+ if (!/^[0-9a-f]{40}$/i.test(sha)) {
1539
+ throw new Error('sha must be a 40-character hexadecimal string');
1540
+ }
1541
+ }
1542
+ // Create a cached instance of tool handlers
1543
+ let cachedHandlers = null;
1544
+ /**
1545
+ * Handle a tool call by routing to the appropriate handler
1546
+ *
1547
+ * @param apiClient - The API client to use for making requests
1548
+ * @param toolName - Name of the tool being called
1549
+ * @param args - Arguments passed to the tool
1550
+ * @param debug - Whether to enable debug logging
1551
+ * @returns Promise resolving to the tool result
1552
+ */
1553
+ export async function handleToolCall(apiClient, toolName, args, debug = false) {
1554
+ const startTime = Date.now();
1555
+ // Create handlers if not cached
1556
+ if (!cachedHandlers) {
1557
+ cachedHandlers = createToolHandlers(apiClient);
1558
+ }
1559
+ // Get the handler for this tool
1560
+ const handler = cachedHandlers[toolName];
1561
+ if (!handler) {
1562
+ throw new Error(`Unknown tool: ${toolName}`);
1563
+ }
1564
+ if (debug) {
1565
+ console.error(`[DEBUG] Calling tool: ${toolName}`, {
1566
+ args: JSON.stringify(args),
1567
+ });
1568
+ }
1569
+ try {
1570
+ // Run comprehensive validation before executing
1571
+ validateToolArgs(toolName, args);
1572
+ // Execute with retry logic
1573
+ const result = await withRetry(() => handler(apiClient, args), toolName, debug);
1574
+ const duration = Date.now() - startTime;
1575
+ if (debug) {
1576
+ console.error(`[DEBUG] Tool ${toolName} completed in ${duration}ms`);
1577
+ }
1578
+ return result;
1579
+ }
1580
+ catch (error) {
1581
+ const duration = Date.now() - startTime;
1582
+ // Transform error for proper handling
1583
+ const transformedError = transformError(error);
1584
+ const message = transformedError.message;
1585
+ if (debug) {
1586
+ console.error(`[DEBUG] Tool ${toolName} failed after ${duration}ms:`, message);
1587
+ if (transformedError instanceof ValidationError) {
1588
+ console.error(`[DEBUG] Validation field: ${transformedError.field}, code: ${transformedError.code}`);
1589
+ }
1590
+ }
1591
+ // Rethrow with context
1592
+ if (transformedError instanceof ValidationError) {
1593
+ throw transformedError;
1594
+ }
1595
+ if (transformedError instanceof ApiError) {
1596
+ throw transformedError;
1597
+ }
1598
+ throw new Error(`Tool ${toolName} failed: ${message}`);
1599
+ }
1600
+ }
1601
+ /**
1602
+ * Handle a tool call and return MCP-formatted response
1603
+ * This wraps handleToolCall with proper error formatting for MCP protocol
1604
+ *
1605
+ * @param apiClient - The API client to use for making requests
1606
+ * @param toolName - Name of the tool being called
1607
+ * @param args - Arguments passed to the tool
1608
+ * @param debug - Whether to enable debug logging
1609
+ * @returns Promise resolving to either the result or an MCP error response
1610
+ */
1611
+ export async function handleToolCallSafe(apiClient, toolName, args, debug = false) {
1612
+ try {
1613
+ return await handleToolCall(apiClient, toolName, args, debug);
1614
+ }
1615
+ catch (error) {
1616
+ const err = error instanceof Error ? error : new Error(String(error));
1617
+ return formatMCPError(err);
1618
+ }
1619
+ }
1620
+ // Re-export validation utilities for use by other modules
1621
+ export { ValidationError, ApiError, validateToolArgs, formatMCPError, transformError, } from '../validation/index.js';
1622
+ //# sourceMappingURL=index.js.map