@vibescope/mcp-server 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +60 -7
  2. package/dist/api-client.d.ts +251 -1
  3. package/dist/api-client.js +82 -3
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +96 -63
  6. package/dist/handlers/connectors.d.ts +45 -0
  7. package/dist/handlers/connectors.js +183 -0
  8. package/dist/handlers/cost.d.ts +10 -0
  9. package/dist/handlers/cost.js +112 -50
  10. package/dist/handlers/decisions.js +32 -19
  11. package/dist/handlers/deployment.js +144 -122
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +96 -7
  14. package/dist/handlers/fallback.js +29 -23
  15. package/dist/handlers/file-checkouts.d.ts +20 -0
  16. package/dist/handlers/file-checkouts.js +133 -0
  17. package/dist/handlers/findings.d.ts +6 -0
  18. package/dist/handlers/findings.js +96 -40
  19. package/dist/handlers/git-issues.js +40 -36
  20. package/dist/handlers/ideas.js +49 -31
  21. package/dist/handlers/index.d.ts +3 -0
  22. package/dist/handlers/index.js +9 -0
  23. package/dist/handlers/milestones.js +39 -32
  24. package/dist/handlers/organizations.js +99 -91
  25. package/dist/handlers/progress.js +24 -13
  26. package/dist/handlers/project.js +68 -28
  27. package/dist/handlers/requests.js +18 -14
  28. package/dist/handlers/roles.d.ts +18 -0
  29. package/dist/handlers/roles.js +130 -0
  30. package/dist/handlers/session.js +58 -17
  31. package/dist/handlers/sprints.js +93 -81
  32. package/dist/handlers/tasks.d.ts +2 -0
  33. package/dist/handlers/tasks.js +189 -91
  34. package/dist/handlers/types.d.ts +64 -2
  35. package/dist/handlers/types.js +48 -1
  36. package/dist/handlers/validation.js +21 -17
  37. package/dist/index.js +7 -2716
  38. package/dist/token-tracking.d.ts +74 -0
  39. package/dist/token-tracking.js +122 -0
  40. package/dist/tools.js +685 -9
  41. package/dist/utils.d.ts +5 -0
  42. package/dist/utils.js +17 -0
  43. package/docs/TOOLS.md +2053 -0
  44. package/package.json +4 -1
  45. package/scripts/generate-docs.ts +212 -0
  46. package/src/api-client.test.ts +718 -0
  47. package/src/api-client.ts +320 -6
  48. package/src/handlers/__test-setup__.ts +16 -0
  49. package/src/handlers/blockers.test.ts +31 -19
  50. package/src/handlers/blockers.ts +9 -8
  51. package/src/handlers/bodies-of-work.test.ts +55 -32
  52. package/src/handlers/bodies-of-work.ts +115 -115
  53. package/src/handlers/connectors.test.ts +834 -0
  54. package/src/handlers/connectors.ts +229 -0
  55. package/src/handlers/cost.test.ts +34 -44
  56. package/src/handlers/cost.ts +136 -85
  57. package/src/handlers/decisions.test.ts +37 -27
  58. package/src/handlers/decisions.ts +35 -30
  59. package/src/handlers/deployment.ts +180 -208
  60. package/src/handlers/discovery.test.ts +4 -5
  61. package/src/handlers/discovery.ts +98 -8
  62. package/src/handlers/fallback.test.ts +26 -22
  63. package/src/handlers/fallback.ts +36 -33
  64. package/src/handlers/file-checkouts.test.ts +670 -0
  65. package/src/handlers/file-checkouts.ts +165 -0
  66. package/src/handlers/findings.test.ts +178 -19
  67. package/src/handlers/findings.ts +112 -74
  68. package/src/handlers/git-issues.test.ts +51 -43
  69. package/src/handlers/git-issues.ts +44 -84
  70. package/src/handlers/ideas.test.ts +28 -23
  71. package/src/handlers/ideas.ts +61 -59
  72. package/src/handlers/index.ts +9 -0
  73. package/src/handlers/milestones.test.ts +33 -28
  74. package/src/handlers/milestones.ts +52 -50
  75. package/src/handlers/organizations.test.ts +104 -83
  76. package/src/handlers/organizations.ts +117 -142
  77. package/src/handlers/progress.test.ts +20 -14
  78. package/src/handlers/progress.ts +26 -24
  79. package/src/handlers/project.test.ts +34 -27
  80. package/src/handlers/project.ts +95 -63
  81. package/src/handlers/requests.test.ts +27 -18
  82. package/src/handlers/requests.ts +21 -17
  83. package/src/handlers/roles.test.ts +303 -0
  84. package/src/handlers/roles.ts +208 -0
  85. package/src/handlers/session.test.ts +47 -0
  86. package/src/handlers/session.ts +71 -26
  87. package/src/handlers/sprints.test.ts +71 -50
  88. package/src/handlers/sprints.ts +113 -146
  89. package/src/handlers/tasks.test.ts +77 -15
  90. package/src/handlers/tasks.ts +231 -156
  91. package/src/handlers/tool-categories.test.ts +66 -0
  92. package/src/handlers/types.ts +81 -2
  93. package/src/handlers/validation.test.ts +78 -45
  94. package/src/handlers/validation.ts +23 -25
  95. package/src/index.ts +12 -2732
  96. package/src/token-tracking.test.ts +453 -0
  97. package/src/token-tracking.ts +164 -0
  98. package/src/tools.ts +685 -9
  99. package/src/utils.test.ts +2 -2
  100. package/src/utils.ts +17 -0
  101. package/dist/config/tool-categories.d.ts +0 -31
  102. package/dist/config/tool-categories.js +0 -253
  103. package/dist/knowledge.d.ts +0 -6
  104. package/dist/knowledge.js +0 -218
@@ -0,0 +1,165 @@
1
+ /**
2
+ * File Checkouts Handlers
3
+ *
4
+ * Handles file checkout/checkin for multi-agent coordination:
5
+ * - checkout_file: Check out a file before editing
6
+ * - checkin_file: Check in a file after editing
7
+ * - get_file_checkouts: Get active checkouts for a project
8
+ * - abandon_checkout: Force release a checkout
9
+ * - is_file_available: Check if a file is available for checkout
10
+ */
11
+
12
+ import type { Handler, HandlerRegistry } from './types.js';
13
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
14
+ import { getApiClient } from '../api-client.js';
15
+
16
+ const VALID_CHECKOUT_STATUSES = ['checked_out', 'checked_in', 'abandoned'] as const;
17
+
18
+ // Argument schemas for type-safe parsing
19
+ const checkoutFileSchema = {
20
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
21
+ file_path: { type: 'string' as const, required: true as const },
22
+ reason: { type: 'string' as const },
23
+ };
24
+
25
+ const checkinFileSchema = {
26
+ checkout_id: { type: 'string' as const, validate: uuidValidator },
27
+ project_id: { type: 'string' as const, validate: uuidValidator },
28
+ file_path: { type: 'string' as const },
29
+ summary: { type: 'string' as const },
30
+ };
31
+
32
+ const getFileCheckoutsSchema = {
33
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
34
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_CHECKOUT_STATUSES) },
35
+ file_path: { type: 'string' as const },
36
+ limit: { type: 'number' as const, default: 50 },
37
+ };
38
+
39
+ const abandonCheckoutSchema = {
40
+ checkout_id: { type: 'string' as const, validate: uuidValidator },
41
+ project_id: { type: 'string' as const, validate: uuidValidator },
42
+ file_path: { type: 'string' as const },
43
+ };
44
+
45
+ const isFileAvailableSchema = {
46
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
47
+ file_path: { type: 'string' as const, required: true as const },
48
+ };
49
+
50
+ export const checkoutFile: Handler = async (args, ctx) => {
51
+ const { project_id, file_path, reason } = parseArgs(args, checkoutFileSchema);
52
+
53
+ const apiClient = getApiClient();
54
+ const response = await apiClient.checkoutFile(project_id, file_path, reason, ctx.session.currentSessionId || undefined);
55
+
56
+ if (!response.ok) {
57
+ return { result: { error: response.error || 'Failed to checkout file' }, isError: true };
58
+ }
59
+
60
+ return { result: response.data };
61
+ };
62
+
63
+ export const checkinFile: Handler = async (args, ctx) => {
64
+ const { checkout_id, project_id, file_path, summary } = parseArgs(args, checkinFileSchema);
65
+
66
+ // Validate that either checkout_id or both project_id and file_path are provided
67
+ if (!checkout_id && (!project_id || !file_path)) {
68
+ return { result: { error: 'Either checkout_id or both project_id and file_path are required' }, isError: true };
69
+ }
70
+
71
+ const apiClient = getApiClient();
72
+ const response = await apiClient.checkinFile({
73
+ checkout_id,
74
+ project_id,
75
+ file_path,
76
+ summary
77
+ }, ctx.session.currentSessionId || undefined);
78
+
79
+ if (!response.ok) {
80
+ return { result: { error: response.error || 'Failed to checkin file' }, isError: true };
81
+ }
82
+
83
+ return { result: response.data };
84
+ };
85
+
86
+ export const getFileCheckouts: Handler = async (args, _ctx) => {
87
+ const { project_id, status, file_path, limit } = parseArgs(args, getFileCheckoutsSchema);
88
+
89
+ const apiClient = getApiClient();
90
+ const response = await apiClient.getFileCheckouts(project_id, {
91
+ status,
92
+ file_path,
93
+ limit
94
+ });
95
+
96
+ if (!response.ok) {
97
+ return { result: { error: response.error || 'Failed to get file checkouts' }, isError: true };
98
+ }
99
+
100
+ return { result: response.data };
101
+ };
102
+
103
+ export const abandonCheckout: Handler = async (args, _ctx) => {
104
+ const { checkout_id, project_id, file_path } = parseArgs(args, abandonCheckoutSchema);
105
+
106
+ // Validate that either checkout_id or both project_id and file_path are provided
107
+ if (!checkout_id && (!project_id || !file_path)) {
108
+ return { result: { error: 'Either checkout_id or both project_id and file_path are required' }, isError: true };
109
+ }
110
+
111
+ const apiClient = getApiClient();
112
+ const response = await apiClient.abandonCheckout({
113
+ checkout_id,
114
+ project_id,
115
+ file_path
116
+ });
117
+
118
+ if (!response.ok) {
119
+ return { result: { error: response.error || 'Failed to abandon checkout' }, isError: true };
120
+ }
121
+
122
+ return { result: response.data };
123
+ };
124
+
125
+ export const isFileAvailable: Handler = async (args, _ctx) => {
126
+ const { project_id, file_path } = parseArgs(args, isFileAvailableSchema);
127
+
128
+ const apiClient = getApiClient();
129
+ const response = await apiClient.getFileCheckouts(project_id, {
130
+ status: 'checked_out',
131
+ file_path,
132
+ limit: 1
133
+ });
134
+
135
+ if (!response.ok) {
136
+ return { result: { error: response.error || 'Failed to check file availability' }, isError: true };
137
+ }
138
+
139
+ const checkouts = response.data?.checkouts || [];
140
+ const activeCheckout = checkouts.length > 0 ? checkouts[0] : null;
141
+
142
+ return {
143
+ result: {
144
+ available: !activeCheckout,
145
+ file_path,
146
+ checked_out_by: activeCheckout ? {
147
+ checkout_id: activeCheckout.id,
148
+ checked_out_by: activeCheckout.checked_out_by,
149
+ checked_out_at: activeCheckout.checked_out_at,
150
+ reason: activeCheckout.checkout_reason
151
+ } : null
152
+ }
153
+ };
154
+ };
155
+
156
+ /**
157
+ * File Checkouts handlers registry
158
+ */
159
+ export const fileCheckoutHandlers: HandlerRegistry = {
160
+ checkout_file: checkoutFile,
161
+ checkin_file: checkinFile,
162
+ get_file_checkouts: getFileCheckouts,
163
+ abandon_checkout: abandonCheckout,
164
+ is_file_available: isFileAvailable,
165
+ };
@@ -5,6 +5,7 @@ import {
5
5
  getFindingsStats,
6
6
  updateFinding,
7
7
  deleteFinding,
8
+ queryKnowledgeBase,
8
9
  } from './findings.js';
9
10
  import { ValidationError } from '../validators.js';
10
11
  import { createMockContext } from './__test-utils__.js';
@@ -105,16 +106,17 @@ describe('addFinding', () => {
105
106
  );
106
107
  });
107
108
 
108
- it('should throw error when API call fails', async () => {
109
+ it('should return error when API call fails', async () => {
109
110
  mockApiClient.addFinding.mockResolvedValue({
110
111
  ok: false,
111
112
  error: 'Insert failed',
112
113
  });
113
114
  const ctx = createMockContext();
114
115
 
115
- await expect(
116
- addFinding({ project_id: VALID_UUID, title: 'Test' }, ctx)
117
- ).rejects.toThrow('Insert failed');
116
+ const result = await addFinding({ project_id: VALID_UUID, title: 'Test' }, ctx);
117
+
118
+ expect(result.isError).toBe(true);
119
+ expect(result.result).toMatchObject({ error: 'Insert failed' });
118
120
  });
119
121
  });
120
122
 
@@ -258,16 +260,17 @@ describe('getFindings', () => {
258
260
  );
259
261
  });
260
262
 
261
- it('should throw error when API call fails', async () => {
263
+ it('should return error when API call fails', async () => {
262
264
  mockApiClient.getFindings.mockResolvedValue({
263
265
  ok: false,
264
266
  error: 'Query failed',
265
267
  });
266
268
  const ctx = createMockContext();
267
269
 
268
- await expect(
269
- getFindings({ project_id: VALID_UUID }, ctx)
270
- ).rejects.toThrow('Query failed');
270
+ const result = await getFindings({ project_id: VALID_UUID }, ctx);
271
+
272
+ expect(result.isError).toBe(true);
273
+ expect(result.result).toMatchObject({ error: 'Query failed' });
271
274
  });
272
275
  });
273
276
 
@@ -338,16 +341,17 @@ describe('updateFinding', () => {
338
341
  );
339
342
  });
340
343
 
341
- it('should throw error when API call fails', async () => {
344
+ it('should return error when API call fails', async () => {
342
345
  mockApiClient.updateFinding.mockResolvedValue({
343
346
  ok: false,
344
347
  error: 'Update failed',
345
348
  });
346
349
  const ctx = createMockContext();
347
350
 
348
- await expect(
349
- updateFinding({ finding_id: VALID_UUID, title: 'Test' }, ctx)
350
- ).rejects.toThrow('Update failed');
351
+ const result = await updateFinding({ finding_id: VALID_UUID, title: 'Test' }, ctx);
352
+
353
+ expect(result.isError).toBe(true);
354
+ expect(result.result).toMatchObject({ error: 'Update failed' });
351
355
  });
352
356
  });
353
357
 
@@ -396,16 +400,17 @@ describe('deleteFinding', () => {
396
400
  expect(mockApiClient.deleteFinding).toHaveBeenCalledWith(VALID_UUID);
397
401
  });
398
402
 
399
- it('should throw error when API call fails', async () => {
403
+ it('should return error when API call fails', async () => {
400
404
  mockApiClient.deleteFinding.mockResolvedValue({
401
405
  ok: false,
402
406
  error: 'Delete failed',
403
407
  });
404
408
  const ctx = createMockContext();
405
409
 
406
- await expect(
407
- deleteFinding({ finding_id: VALID_UUID }, ctx)
408
- ).rejects.toThrow('Delete failed');
410
+ const result = await deleteFinding({ finding_id: VALID_UUID }, ctx);
411
+
412
+ expect(result.isError).toBe(true);
413
+ expect(result.result).toMatchObject({ error: 'Delete failed' });
409
414
  });
410
415
  });
411
416
 
@@ -460,15 +465,169 @@ describe('getFindingsStats', () => {
460
465
  expect(mockApiClient.getFindingsStats).toHaveBeenCalledWith(VALID_UUID);
461
466
  });
462
467
 
463
- it('should throw error when API call fails', async () => {
468
+ it('should return error when API call fails', async () => {
464
469
  mockApiClient.getFindingsStats.mockResolvedValue({
465
470
  ok: false,
466
471
  error: 'Query failed',
467
472
  });
468
473
  const ctx = createMockContext();
469
474
 
475
+ const result = await getFindingsStats({ project_id: VALID_UUID }, ctx);
476
+
477
+ expect(result.isError).toBe(true);
478
+ expect(result.result).toMatchObject({ error: 'Query failed' });
479
+ });
480
+ });
481
+
482
+ // ============================================================================
483
+ // queryKnowledgeBase Tests
484
+ // ============================================================================
485
+
486
+ describe('queryKnowledgeBase', () => {
487
+ beforeEach(() => vi.clearAllMocks());
488
+
489
+ it('should throw error for missing project_id', async () => {
490
+ const ctx = createMockContext();
491
+
492
+ await expect(queryKnowledgeBase({}, ctx)).rejects.toThrow(ValidationError);
493
+ });
494
+
495
+ it('should throw error for invalid project_id UUID', async () => {
496
+ const ctx = createMockContext();
497
+
470
498
  await expect(
471
- getFindingsStats({ project_id: VALID_UUID }, ctx)
472
- ).rejects.toThrow('Query failed');
499
+ queryKnowledgeBase({ project_id: 'invalid' }, ctx)
500
+ ).rejects.toThrow(ValidationError);
501
+ });
502
+
503
+ it('should throw error for invalid scope value', async () => {
504
+ const ctx = createMockContext();
505
+
506
+ await expect(
507
+ queryKnowledgeBase({ project_id: VALID_UUID, scope: 'invalid_scope' }, ctx)
508
+ ).rejects.toThrow(ValidationError);
509
+ });
510
+
511
+ it('should query with default parameters', async () => {
512
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
513
+ ok: true,
514
+ data: {
515
+ findings: [],
516
+ decisions: [],
517
+ completed_tasks: [],
518
+ resolved_blockers: [],
519
+ },
520
+ });
521
+ const ctx = createMockContext();
522
+
523
+ const result = await queryKnowledgeBase({ project_id: VALID_UUID }, ctx);
524
+
525
+ expect(result.result).toMatchObject({
526
+ findings: [],
527
+ decisions: [],
528
+ });
529
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
530
+ VALID_UUID,
531
+ expect.objectContaining({
532
+ scope: 'summary',
533
+ limit: 5,
534
+ })
535
+ );
536
+ });
537
+
538
+ it('should pass scope parameter', async () => {
539
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
540
+ ok: true,
541
+ data: { findings: [] },
542
+ });
543
+ const ctx = createMockContext();
544
+
545
+ await queryKnowledgeBase({ project_id: VALID_UUID, scope: 'detailed' }, ctx);
546
+
547
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
548
+ VALID_UUID,
549
+ expect.objectContaining({ scope: 'detailed' })
550
+ );
551
+ });
552
+
553
+ it('should pass categories filter', async () => {
554
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
555
+ ok: true,
556
+ data: { findings: [], decisions: [] },
557
+ });
558
+ const ctx = createMockContext();
559
+
560
+ await queryKnowledgeBase({
561
+ project_id: VALID_UUID,
562
+ categories: ['findings', 'decisions']
563
+ }, ctx);
564
+
565
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
566
+ VALID_UUID,
567
+ expect.objectContaining({
568
+ categories: ['findings', 'decisions']
569
+ })
570
+ );
571
+ });
572
+
573
+ it('should cap limit at 20', async () => {
574
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
575
+ ok: true,
576
+ data: { findings: [] },
577
+ });
578
+ const ctx = createMockContext();
579
+
580
+ await queryKnowledgeBase({ project_id: VALID_UUID, limit: 100 }, ctx);
581
+
582
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
583
+ VALID_UUID,
584
+ expect.objectContaining({ limit: 20 })
585
+ );
586
+ });
587
+
588
+ it('should enforce minimum limit of 1', async () => {
589
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
590
+ ok: true,
591
+ data: { findings: [] },
592
+ });
593
+ const ctx = createMockContext();
594
+
595
+ await queryKnowledgeBase({ project_id: VALID_UUID, limit: -5 }, ctx);
596
+
597
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
598
+ VALID_UUID,
599
+ expect.objectContaining({ limit: 1 })
600
+ );
601
+ });
602
+
603
+ it('should pass search_query', async () => {
604
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
605
+ ok: true,
606
+ data: { findings: [] },
607
+ });
608
+ const ctx = createMockContext();
609
+
610
+ await queryKnowledgeBase({
611
+ project_id: VALID_UUID,
612
+ search_query: 'security'
613
+ }, ctx);
614
+
615
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
616
+ VALID_UUID,
617
+ expect.objectContaining({ search_query: 'security' })
618
+ );
619
+ });
620
+
621
+ it('should return error when API call fails', async () => {
622
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
623
+ ok: false,
624
+ error: 'Query failed',
625
+ });
626
+ const ctx = createMockContext();
627
+
628
+ const result = await queryKnowledgeBase({ project_id: VALID_UUID }, ctx);
629
+
630
+ expect(result.isError).toBe(true);
631
+ expect(result.result).toMatchObject({ error: 'Query failed' });
473
632
  });
474
633
  });
@@ -10,68 +10,97 @@
10
10
  */
11
11
 
12
12
  import type { Handler, HandlerRegistry } from './types.js';
13
- import { validateRequired, validateUUID } from '../validators.js';
13
+ import { success, error } from './types.js';
14
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
14
15
  import { getApiClient } from '../api-client.js';
15
16
 
16
- type FindingCategory = 'performance' | 'security' | 'code_quality' | 'accessibility' | 'documentation' | 'architecture' | 'testing' | 'other';
17
- type FindingSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical';
18
- type FindingStatus = 'open' | 'addressed' | 'dismissed' | 'wontfix';
17
+ const VALID_FINDING_CATEGORIES = ['performance', 'security', 'code_quality', 'accessibility', 'documentation', 'architecture', 'testing', 'other'] as const;
18
+ const VALID_FINDING_SEVERITIES = ['info', 'low', 'medium', 'high', 'critical'] as const;
19
+ const VALID_FINDING_STATUSES = ['open', 'addressed', 'dismissed', 'wontfix'] as const;
20
+
21
+ type FindingCategory = typeof VALID_FINDING_CATEGORIES[number];
22
+ type FindingSeverity = typeof VALID_FINDING_SEVERITIES[number];
23
+ type FindingStatus = typeof VALID_FINDING_STATUSES[number];
24
+
25
+ // Argument schemas for type-safe parsing
26
+ const addFindingSchema = {
27
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
28
+ title: { type: 'string' as const, required: true as const },
29
+ description: { type: 'string' as const },
30
+ category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
31
+ severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
32
+ file_path: { type: 'string' as const },
33
+ line_number: { type: 'number' as const },
34
+ related_task_id: { type: 'string' as const, validate: uuidValidator },
35
+ };
36
+
37
+ const getFindingsSchema = {
38
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
39
+ category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
40
+ severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
41
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
42
+ limit: { type: 'number' as const, default: 50 },
43
+ offset: { type: 'number' as const, default: 0 },
44
+ search_query: { type: 'string' as const },
45
+ summary_only: { type: 'boolean' as const, default: false },
46
+ };
47
+
48
+ const getFindingsStatsSchema = {
49
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
50
+ };
51
+
52
+ const updateFindingSchema = {
53
+ finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
54
+ title: { type: 'string' as const },
55
+ description: { type: 'string' as const },
56
+ severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
57
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
58
+ resolution_note: { type: 'string' as const },
59
+ };
60
+
61
+ const deleteFindingSchema = {
62
+ finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
63
+ };
64
+
65
+ const VALID_SCOPES = ['summary', 'detailed'] as const;
66
+
67
+ const queryKnowledgeBaseSchema = {
68
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
69
+ scope: { type: 'string' as const, default: 'summary', validate: createEnumValidator(VALID_SCOPES) },
70
+ categories: { type: 'array' as const },
71
+ limit: { type: 'number' as const, default: 5 },
72
+ search_query: { type: 'string' as const },
73
+ };
19
74
 
20
75
  export const addFinding: Handler = async (args, ctx) => {
21
- const { project_id, category, title, description, severity, file_path, line_number, related_task_id } = args as {
22
- project_id: string;
23
- category?: FindingCategory;
24
- title: string;
25
- description?: string;
26
- severity?: FindingSeverity;
27
- file_path?: string;
28
- line_number?: number;
29
- related_task_id?: string;
30
- };
31
-
32
- validateRequired(project_id, 'project_id');
33
- validateUUID(project_id, 'project_id');
34
- validateRequired(title, 'title');
35
- if (related_task_id) validateUUID(related_task_id, 'related_task_id');
76
+ const { project_id, title, description, category, severity, file_path, line_number, related_task_id } = parseArgs(args, addFindingSchema);
36
77
 
37
78
  const apiClient = getApiClient();
38
79
  const response = await apiClient.addFinding(project_id, {
39
80
  title,
40
81
  description,
41
- category,
42
- severity,
82
+ category: category as FindingCategory | undefined,
83
+ severity: severity as FindingSeverity | undefined,
43
84
  file_path,
44
85
  line_number,
45
86
  related_task_id
46
87
  }, ctx.session.currentSessionId || undefined);
47
88
 
48
89
  if (!response.ok) {
49
- throw new Error(response.error || 'Failed to add finding');
90
+ return error(response.error || 'Failed to add finding');
50
91
  }
51
92
 
52
- return { result: response.data };
93
+ return success(response.data);
53
94
  };
54
95
 
55
- export const getFindings: Handler = async (args, ctx) => {
56
- const { project_id, category, severity, status, limit = 50, offset = 0, search_query, summary_only = false } = args as {
57
- project_id: string;
58
- category?: FindingCategory;
59
- severity?: FindingSeverity;
60
- status?: FindingStatus;
61
- limit?: number;
62
- offset?: number;
63
- search_query?: string;
64
- summary_only?: boolean;
65
- };
66
-
67
- validateRequired(project_id, 'project_id');
68
- validateUUID(project_id, 'project_id');
96
+ export const getFindings: Handler = async (args, _ctx) => {
97
+ const { project_id, category, severity, status, limit, offset, search_query, summary_only } = parseArgs(args, getFindingsSchema);
69
98
 
70
99
  const apiClient = getApiClient();
71
100
  const response = await apiClient.getFindings(project_id, {
72
- category,
73
- severity,
74
- status,
101
+ category: category as FindingCategory | undefined,
102
+ severity: severity as FindingSeverity | undefined,
103
+ status: status as FindingStatus | undefined,
75
104
  limit,
76
105
  offset,
77
106
  search_query,
@@ -79,10 +108,10 @@ export const getFindings: Handler = async (args, ctx) => {
79
108
  });
80
109
 
81
110
  if (!response.ok) {
82
- throw new Error(response.error || 'Failed to get findings');
111
+ return error(response.error || 'Failed to get findings');
83
112
  }
84
113
 
85
- return { result: response.data };
114
+ return success(response.data);
86
115
  };
87
116
 
88
117
  /**
@@ -90,67 +119,75 @@ export const getFindings: Handler = async (args, ctx) => {
90
119
  * Returns counts by category, severity, and status without the actual finding data.
91
120
  * This is much more token-efficient than get_findings for understanding the overall state.
92
121
  */
93
- export const getFindingsStats: Handler = async (args, ctx) => {
94
- const { project_id } = args as {
95
- project_id: string;
96
- };
97
-
98
- validateRequired(project_id, 'project_id');
99
- validateUUID(project_id, 'project_id');
122
+ export const getFindingsStats: Handler = async (args, _ctx) => {
123
+ const { project_id } = parseArgs(args, getFindingsStatsSchema);
100
124
 
101
125
  const apiClient = getApiClient();
102
126
  const response = await apiClient.getFindingsStats(project_id);
103
127
 
104
128
  if (!response.ok) {
105
- throw new Error(response.error || 'Failed to get findings stats');
129
+ return error(response.error || 'Failed to get findings stats');
106
130
  }
107
131
 
108
- return { result: response.data };
132
+ return success(response.data);
109
133
  };
110
134
 
111
- export const updateFinding: Handler = async (args, ctx) => {
112
- const { finding_id, status, resolution_note, title, description, severity } = args as {
113
- finding_id: string;
114
- status?: FindingStatus;
115
- resolution_note?: string;
116
- title?: string;
117
- description?: string;
118
- severity?: FindingSeverity;
119
- };
120
-
121
- validateRequired(finding_id, 'finding_id');
122
- validateUUID(finding_id, 'finding_id');
135
+ export const updateFinding: Handler = async (args, _ctx) => {
136
+ const { finding_id, title, description, severity, status, resolution_note } = parseArgs(args, updateFindingSchema);
123
137
 
124
138
  const apiClient = getApiClient();
125
139
  const response = await apiClient.updateFinding(finding_id, {
126
140
  title,
127
141
  description,
128
- severity,
129
- status,
142
+ severity: severity as FindingSeverity | undefined,
143
+ status: status as FindingStatus | undefined,
130
144
  resolution_note
131
145
  });
132
146
 
133
147
  if (!response.ok) {
134
- throw new Error(response.error || 'Failed to update finding');
148
+ return error(response.error || 'Failed to update finding');
135
149
  }
136
150
 
137
- return { result: response.data };
151
+ return success(response.data);
138
152
  };
139
153
 
140
- export const deleteFinding: Handler = async (args, ctx) => {
141
- const { finding_id } = args as { finding_id: string };
142
-
143
- validateRequired(finding_id, 'finding_id');
144
- validateUUID(finding_id, 'finding_id');
154
+ export const deleteFinding: Handler = async (args, _ctx) => {
155
+ const { finding_id } = parseArgs(args, deleteFindingSchema);
145
156
 
146
157
  const apiClient = getApiClient();
147
158
  const response = await apiClient.deleteFinding(finding_id);
148
159
 
149
160
  if (!response.ok) {
150
- throw new Error(response.error || 'Failed to delete finding');
161
+ return error(response.error || 'Failed to delete finding');
162
+ }
163
+
164
+ return success(response.data);
165
+ };
166
+
167
+ /**
168
+ * Query aggregated project knowledge in a single call.
169
+ * Returns findings, Q&A, decisions, completed tasks, and resolved blockers.
170
+ * Use this instead of multiple separate tool calls to reduce token usage.
171
+ */
172
+ export const queryKnowledgeBase: Handler = async (args, _ctx) => {
173
+ const { project_id, scope, categories, limit, search_query } = parseArgs(args, queryKnowledgeBaseSchema);
174
+
175
+ // Validate limit range
176
+ const effectiveLimit = Math.min(Math.max(1, limit ?? 5), 20);
177
+
178
+ const apiClient = getApiClient();
179
+ const response = await apiClient.queryKnowledgeBase(project_id, {
180
+ scope: scope as 'summary' | 'detailed' | undefined,
181
+ categories: categories as string[] | undefined,
182
+ limit: effectiveLimit,
183
+ search_query
184
+ });
185
+
186
+ if (!response.ok) {
187
+ return error(response.error || 'Failed to query knowledge base');
151
188
  }
152
189
 
153
- return { result: response.data };
190
+ return success(response.data);
154
191
  };
155
192
 
156
193
  /**
@@ -162,4 +199,5 @@ export const findingHandlers: HandlerRegistry = {
162
199
  get_findings_stats: getFindingsStats,
163
200
  update_finding: updateFinding,
164
201
  delete_finding: deleteFinding,
202
+ query_knowledge_base: queryKnowledgeBase,
165
203
  };