@vibescope/mcp-server 0.1.0 → 0.2.1

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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/api-client.d.ts +120 -2
  3. package/dist/api-client.js +51 -5
  4. package/dist/handlers/bodies-of-work.js +84 -50
  5. package/dist/handlers/cost.js +62 -54
  6. package/dist/handlers/decisions.js +29 -16
  7. package/dist/handlers/deployment.js +114 -107
  8. package/dist/handlers/discovery.d.ts +3 -0
  9. package/dist/handlers/discovery.js +55 -657
  10. package/dist/handlers/fallback.js +42 -28
  11. package/dist/handlers/file-checkouts.d.ts +18 -0
  12. package/dist/handlers/file-checkouts.js +101 -0
  13. package/dist/handlers/findings.d.ts +14 -1
  14. package/dist/handlers/findings.js +104 -28
  15. package/dist/handlers/git-issues.js +36 -32
  16. package/dist/handlers/ideas.js +44 -26
  17. package/dist/handlers/index.d.ts +2 -0
  18. package/dist/handlers/index.js +6 -0
  19. package/dist/handlers/milestones.js +34 -27
  20. package/dist/handlers/organizations.js +86 -78
  21. package/dist/handlers/progress.js +22 -11
  22. package/dist/handlers/project.js +62 -22
  23. package/dist/handlers/requests.js +15 -11
  24. package/dist/handlers/roles.d.ts +18 -0
  25. package/dist/handlers/roles.js +130 -0
  26. package/dist/handlers/session.js +52 -15
  27. package/dist/handlers/sprints.js +78 -65
  28. package/dist/handlers/tasks.js +135 -74
  29. package/dist/handlers/tool-docs.d.ts +4 -3
  30. package/dist/handlers/tool-docs.js +252 -5
  31. package/dist/handlers/validation.js +30 -14
  32. package/dist/index.js +25 -7
  33. package/dist/tools.js +417 -4
  34. package/package.json +1 -1
  35. package/src/api-client.ts +161 -8
  36. package/src/handlers/__test-setup__.ts +12 -0
  37. package/src/handlers/bodies-of-work.ts +127 -111
  38. package/src/handlers/cost.test.ts +34 -44
  39. package/src/handlers/cost.ts +77 -92
  40. package/src/handlers/decisions.test.ts +3 -2
  41. package/src/handlers/decisions.ts +32 -27
  42. package/src/handlers/deployment.ts +144 -190
  43. package/src/handlers/discovery.test.ts +4 -5
  44. package/src/handlers/discovery.ts +60 -746
  45. package/src/handlers/fallback.test.ts +78 -0
  46. package/src/handlers/fallback.ts +51 -38
  47. package/src/handlers/file-checkouts.test.ts +477 -0
  48. package/src/handlers/file-checkouts.ts +127 -0
  49. package/src/handlers/findings.test.ts +274 -2
  50. package/src/handlers/findings.ts +123 -57
  51. package/src/handlers/git-issues.ts +40 -80
  52. package/src/handlers/ideas.ts +56 -54
  53. package/src/handlers/index.ts +6 -0
  54. package/src/handlers/milestones.test.ts +1 -1
  55. package/src/handlers/milestones.ts +47 -45
  56. package/src/handlers/organizations.ts +104 -129
  57. package/src/handlers/progress.ts +24 -22
  58. package/src/handlers/project.ts +89 -57
  59. package/src/handlers/requests.ts +18 -14
  60. package/src/handlers/roles.test.ts +303 -0
  61. package/src/handlers/roles.ts +208 -0
  62. package/src/handlers/session.test.ts +37 -2
  63. package/src/handlers/session.ts +64 -21
  64. package/src/handlers/sprints.ts +114 -134
  65. package/src/handlers/tasks.test.ts +61 -0
  66. package/src/handlers/tasks.ts +170 -139
  67. package/src/handlers/tool-docs.ts +1024 -0
  68. package/src/handlers/validation.test.ts +53 -1
  69. package/src/handlers/validation.ts +32 -21
  70. package/src/index.ts +25 -7
  71. package/src/tools.ts +417 -4
  72. package/dist/config/tool-categories.d.ts +0 -31
  73. package/dist/config/tool-categories.js +0 -253
  74. package/dist/knowledge.d.ts +0 -6
  75. package/dist/knowledge.js +0 -218
  76. package/src/knowledge.ts +0 -230
@@ -0,0 +1,127 @@
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
+ */
10
+
11
+ import type { Handler, HandlerRegistry } from './types.js';
12
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
13
+ import { getApiClient } from '../api-client.js';
14
+
15
+ const VALID_CHECKOUT_STATUSES = ['checked_out', 'checked_in', 'abandoned'] as const;
16
+
17
+ // Argument schemas for type-safe parsing
18
+ const checkoutFileSchema = {
19
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
20
+ file_path: { type: 'string' as const, required: true as const },
21
+ reason: { type: 'string' as const },
22
+ };
23
+
24
+ const checkinFileSchema = {
25
+ checkout_id: { type: 'string' as const, validate: uuidValidator },
26
+ project_id: { type: 'string' as const, validate: uuidValidator },
27
+ file_path: { type: 'string' as const },
28
+ summary: { type: 'string' as const },
29
+ };
30
+
31
+ const getFileCheckoutsSchema = {
32
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
33
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_CHECKOUT_STATUSES) },
34
+ file_path: { type: 'string' as const },
35
+ limit: { type: 'number' as const, default: 50 },
36
+ };
37
+
38
+ const abandonCheckoutSchema = {
39
+ checkout_id: { type: 'string' as const, validate: uuidValidator },
40
+ project_id: { type: 'string' as const, validate: uuidValidator },
41
+ file_path: { type: 'string' as const },
42
+ };
43
+
44
+ export const checkoutFile: Handler = async (args, ctx) => {
45
+ const { project_id, file_path, reason } = parseArgs(args, checkoutFileSchema);
46
+
47
+ const apiClient = getApiClient();
48
+ const response = await apiClient.checkoutFile(project_id, file_path, reason, ctx.session.currentSessionId || undefined);
49
+
50
+ if (!response.ok) {
51
+ throw new Error(response.error || 'Failed to checkout file');
52
+ }
53
+
54
+ return { result: response.data };
55
+ };
56
+
57
+ export const checkinFile: Handler = async (args, ctx) => {
58
+ const { checkout_id, project_id, file_path, summary } = parseArgs(args, checkinFileSchema);
59
+
60
+ // Validate that either checkout_id or both project_id and file_path are provided
61
+ if (!checkout_id && (!project_id || !file_path)) {
62
+ throw new Error('Either checkout_id or both project_id and file_path are required');
63
+ }
64
+
65
+ const apiClient = getApiClient();
66
+ const response = await apiClient.checkinFile({
67
+ checkout_id,
68
+ project_id,
69
+ file_path,
70
+ summary
71
+ }, ctx.session.currentSessionId || undefined);
72
+
73
+ if (!response.ok) {
74
+ throw new Error(response.error || 'Failed to checkin file');
75
+ }
76
+
77
+ return { result: response.data };
78
+ };
79
+
80
+ export const getFileCheckouts: Handler = async (args, _ctx) => {
81
+ const { project_id, status, file_path, limit } = parseArgs(args, getFileCheckoutsSchema);
82
+
83
+ const apiClient = getApiClient();
84
+ const response = await apiClient.getFileCheckouts(project_id, {
85
+ status,
86
+ file_path,
87
+ limit
88
+ });
89
+
90
+ if (!response.ok) {
91
+ throw new Error(response.error || 'Failed to get file checkouts');
92
+ }
93
+
94
+ return { result: response.data };
95
+ };
96
+
97
+ export const abandonCheckout: Handler = async (args, _ctx) => {
98
+ const { checkout_id, project_id, file_path } = parseArgs(args, abandonCheckoutSchema);
99
+
100
+ // Validate that either checkout_id or both project_id and file_path are provided
101
+ if (!checkout_id && (!project_id || !file_path)) {
102
+ throw new Error('Either checkout_id or both project_id and file_path are required');
103
+ }
104
+
105
+ const apiClient = getApiClient();
106
+ const response = await apiClient.abandonCheckout({
107
+ checkout_id,
108
+ project_id,
109
+ file_path
110
+ });
111
+
112
+ if (!response.ok) {
113
+ throw new Error(response.error || 'Failed to abandon checkout');
114
+ }
115
+
116
+ return { result: response.data };
117
+ };
118
+
119
+ /**
120
+ * File Checkouts handlers registry
121
+ */
122
+ export const fileCheckoutHandlers: HandlerRegistry = {
123
+ checkout_file: checkoutFile,
124
+ checkin_file: checkinFile,
125
+ get_file_checkouts: getFileCheckouts,
126
+ abandon_checkout: abandonCheckout,
127
+ };
@@ -2,8 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import {
3
3
  addFinding,
4
4
  getFindings,
5
+ getFindingsStats,
5
6
  updateFinding,
6
7
  deleteFinding,
8
+ queryKnowledgeBase,
7
9
  } from './findings.js';
8
10
  import { ValidationError } from '../validators.js';
9
11
  import { createMockContext } from './__test-utils__.js';
@@ -171,12 +173,74 @@ describe('getFindings', () => {
171
173
 
172
174
  expect(mockApiClient.getFindings).toHaveBeenCalledWith(
173
175
  VALID_UUID,
174
- {
176
+ expect.objectContaining({
175
177
  category: 'security',
176
178
  severity: 'critical',
177
179
  status: 'open',
178
180
  limit: 10,
179
- }
181
+ })
182
+ );
183
+ });
184
+
185
+ it('should pass summary_only parameter to API client', async () => {
186
+ mockApiClient.getFindings.mockResolvedValue({
187
+ ok: true,
188
+ data: { findings: [], total_count: 0, has_more: false },
189
+ });
190
+ const ctx = createMockContext();
191
+
192
+ await getFindings({
193
+ project_id: VALID_UUID,
194
+ summary_only: true
195
+ }, ctx);
196
+
197
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
198
+ VALID_UUID,
199
+ expect.objectContaining({
200
+ summary_only: true,
201
+ })
202
+ );
203
+ });
204
+
205
+ it('should pass search_query parameter to API client', async () => {
206
+ mockApiClient.getFindings.mockResolvedValue({
207
+ ok: true,
208
+ data: { findings: [], total_count: 0, has_more: false },
209
+ });
210
+ const ctx = createMockContext();
211
+
212
+ await getFindings({
213
+ project_id: VALID_UUID,
214
+ search_query: 'security'
215
+ }, ctx);
216
+
217
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
218
+ VALID_UUID,
219
+ expect.objectContaining({
220
+ search_query: 'security',
221
+ })
222
+ );
223
+ });
224
+
225
+ it('should pass offset parameter to API client', async () => {
226
+ mockApiClient.getFindings.mockResolvedValue({
227
+ ok: true,
228
+ data: { findings: [], total_count: 100, has_more: true },
229
+ });
230
+ const ctx = createMockContext();
231
+
232
+ await getFindings({
233
+ project_id: VALID_UUID,
234
+ offset: 50,
235
+ limit: 25
236
+ }, ctx);
237
+
238
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
239
+ VALID_UUID,
240
+ expect.objectContaining({
241
+ offset: 50,
242
+ limit: 25,
243
+ })
180
244
  );
181
245
  });
182
246
 
@@ -345,3 +409,211 @@ describe('deleteFinding', () => {
345
409
  ).rejects.toThrow('Delete failed');
346
410
  });
347
411
  });
412
+
413
+ // ============================================================================
414
+ // getFindingsStats Tests
415
+ // ============================================================================
416
+
417
+ describe('getFindingsStats', () => {
418
+ beforeEach(() => vi.clearAllMocks());
419
+
420
+ it('should throw error for missing project_id', async () => {
421
+ const ctx = createMockContext();
422
+
423
+ await expect(getFindingsStats({}, ctx)).rejects.toThrow(ValidationError);
424
+ });
425
+
426
+ it('should throw error for invalid project_id UUID', async () => {
427
+ const ctx = createMockContext();
428
+
429
+ await expect(
430
+ getFindingsStats({ project_id: 'invalid' }, ctx)
431
+ ).rejects.toThrow(ValidationError);
432
+ });
433
+
434
+ it('should return findings stats for project', async () => {
435
+ const mockStats = {
436
+ total: 10,
437
+ by_status: { open: 5, addressed: 3, dismissed: 2 },
438
+ by_severity: { critical: 1, high: 3, medium: 4, low: 2 },
439
+ by_category: { security: 3, performance: 4, code_quality: 3 },
440
+ };
441
+ mockApiClient.getFindingsStats.mockResolvedValue({
442
+ ok: true,
443
+ data: mockStats,
444
+ });
445
+ const ctx = createMockContext();
446
+
447
+ const result = await getFindingsStats({ project_id: VALID_UUID }, ctx);
448
+
449
+ expect(result.result).toMatchObject(mockStats);
450
+ });
451
+
452
+ it('should call API client getFindingsStats with project_id', async () => {
453
+ mockApiClient.getFindingsStats.mockResolvedValue({
454
+ ok: true,
455
+ data: { total: 0, by_status: {}, by_severity: {}, by_category: {} },
456
+ });
457
+ const ctx = createMockContext();
458
+
459
+ await getFindingsStats({ project_id: VALID_UUID }, ctx);
460
+
461
+ expect(mockApiClient.getFindingsStats).toHaveBeenCalledWith(VALID_UUID);
462
+ });
463
+
464
+ it('should throw error when API call fails', async () => {
465
+ mockApiClient.getFindingsStats.mockResolvedValue({
466
+ ok: false,
467
+ error: 'Query failed',
468
+ });
469
+ const ctx = createMockContext();
470
+
471
+ await expect(
472
+ getFindingsStats({ project_id: VALID_UUID }, ctx)
473
+ ).rejects.toThrow('Query failed');
474
+ });
475
+ });
476
+
477
+ // ============================================================================
478
+ // queryKnowledgeBase Tests
479
+ // ============================================================================
480
+
481
+ describe('queryKnowledgeBase', () => {
482
+ beforeEach(() => vi.clearAllMocks());
483
+
484
+ it('should throw error for missing project_id', async () => {
485
+ const ctx = createMockContext();
486
+
487
+ await expect(queryKnowledgeBase({}, ctx)).rejects.toThrow(ValidationError);
488
+ });
489
+
490
+ it('should throw error for invalid project_id UUID', async () => {
491
+ const ctx = createMockContext();
492
+
493
+ await expect(
494
+ queryKnowledgeBase({ project_id: 'invalid' }, ctx)
495
+ ).rejects.toThrow(ValidationError);
496
+ });
497
+
498
+ it('should query with default parameters', async () => {
499
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
500
+ ok: true,
501
+ data: {
502
+ findings: [],
503
+ decisions: [],
504
+ completed_tasks: [],
505
+ resolved_blockers: [],
506
+ },
507
+ });
508
+ const ctx = createMockContext();
509
+
510
+ const result = await queryKnowledgeBase({ project_id: VALID_UUID }, ctx);
511
+
512
+ expect(result.result).toMatchObject({
513
+ findings: [],
514
+ decisions: [],
515
+ });
516
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
517
+ VALID_UUID,
518
+ expect.objectContaining({
519
+ scope: 'summary',
520
+ limit: 5,
521
+ })
522
+ );
523
+ });
524
+
525
+ it('should pass scope parameter', async () => {
526
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
527
+ ok: true,
528
+ data: { findings: [] },
529
+ });
530
+ const ctx = createMockContext();
531
+
532
+ await queryKnowledgeBase({ project_id: VALID_UUID, scope: 'detailed' }, ctx);
533
+
534
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
535
+ VALID_UUID,
536
+ expect.objectContaining({ scope: 'detailed' })
537
+ );
538
+ });
539
+
540
+ it('should pass categories filter', async () => {
541
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
542
+ ok: true,
543
+ data: { findings: [], decisions: [] },
544
+ });
545
+ const ctx = createMockContext();
546
+
547
+ await queryKnowledgeBase({
548
+ project_id: VALID_UUID,
549
+ categories: ['findings', 'decisions']
550
+ }, ctx);
551
+
552
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
553
+ VALID_UUID,
554
+ expect.objectContaining({
555
+ categories: ['findings', 'decisions']
556
+ })
557
+ );
558
+ });
559
+
560
+ it('should cap limit at 20', async () => {
561
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
562
+ ok: true,
563
+ data: { findings: [] },
564
+ });
565
+ const ctx = createMockContext();
566
+
567
+ await queryKnowledgeBase({ project_id: VALID_UUID, limit: 100 }, ctx);
568
+
569
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
570
+ VALID_UUID,
571
+ expect.objectContaining({ limit: 20 })
572
+ );
573
+ });
574
+
575
+ it('should enforce minimum limit of 1', async () => {
576
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
577
+ ok: true,
578
+ data: { findings: [] },
579
+ });
580
+ const ctx = createMockContext();
581
+
582
+ await queryKnowledgeBase({ project_id: VALID_UUID, limit: -5 }, ctx);
583
+
584
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
585
+ VALID_UUID,
586
+ expect.objectContaining({ limit: 1 })
587
+ );
588
+ });
589
+
590
+ it('should pass search_query', async () => {
591
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
592
+ ok: true,
593
+ data: { findings: [] },
594
+ });
595
+ const ctx = createMockContext();
596
+
597
+ await queryKnowledgeBase({
598
+ project_id: VALID_UUID,
599
+ search_query: 'security'
600
+ }, ctx);
601
+
602
+ expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
603
+ VALID_UUID,
604
+ expect.objectContaining({ search_query: 'security' })
605
+ );
606
+ });
607
+
608
+ it('should throw error when API call fails', async () => {
609
+ mockApiClient.queryKnowledgeBase.mockResolvedValue({
610
+ ok: false,
611
+ error: 'Query failed',
612
+ });
613
+ const ctx = createMockContext();
614
+
615
+ await expect(
616
+ queryKnowledgeBase({ project_id: VALID_UUID }, ctx)
617
+ ).rejects.toThrow('Query failed');
618
+ });
619
+ });
@@ -3,42 +3,83 @@
3
3
  *
4
4
  * Handles audit findings and knowledge base:
5
5
  * - add_finding
6
- * - get_findings
6
+ * - get_findings (supports summary_only for reduced tokens)
7
+ * - get_findings_stats (aggregate counts for minimal tokens)
7
8
  * - update_finding
8
9
  * - delete_finding
9
10
  */
10
11
 
11
12
  import type { Handler, HandlerRegistry } from './types.js';
12
- import { validateRequired, validateUUID } from '../validators.js';
13
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
13
14
  import { getApiClient } from '../api-client.js';
14
15
 
15
- type FindingCategory = 'performance' | 'security' | 'code_quality' | 'accessibility' | 'documentation' | 'architecture' | 'testing' | 'other';
16
- type FindingSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical';
17
- type FindingStatus = 'open' | 'addressed' | 'dismissed' | 'wontfix';
16
+ const VALID_FINDING_CATEGORIES = ['performance', 'security', 'code_quality', 'accessibility', 'documentation', 'architecture', 'testing', 'other'] as const;
17
+ const VALID_FINDING_SEVERITIES = ['info', 'low', 'medium', 'high', 'critical'] as const;
18
+ const VALID_FINDING_STATUSES = ['open', 'addressed', 'dismissed', 'wontfix'] as const;
19
+
20
+ type FindingCategory = typeof VALID_FINDING_CATEGORIES[number];
21
+ type FindingSeverity = typeof VALID_FINDING_SEVERITIES[number];
22
+ type FindingStatus = typeof VALID_FINDING_STATUSES[number];
23
+
24
+ // Argument schemas for type-safe parsing
25
+ const addFindingSchema = {
26
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
27
+ title: { type: 'string' as const, required: true as const },
28
+ description: { type: 'string' as const },
29
+ category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
30
+ severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
31
+ file_path: { type: 'string' as const },
32
+ line_number: { type: 'number' as const },
33
+ related_task_id: { type: 'string' as const, validate: uuidValidator },
34
+ };
35
+
36
+ const getFindingsSchema = {
37
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
38
+ category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
39
+ severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
40
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
41
+ limit: { type: 'number' as const, default: 50 },
42
+ offset: { type: 'number' as const, default: 0 },
43
+ search_query: { type: 'string' as const },
44
+ summary_only: { type: 'boolean' as const, default: false },
45
+ };
46
+
47
+ const getFindingsStatsSchema = {
48
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
49
+ };
50
+
51
+ const updateFindingSchema = {
52
+ finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
53
+ title: { type: 'string' as const },
54
+ description: { type: 'string' as const },
55
+ severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
56
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
57
+ resolution_note: { type: 'string' as const },
58
+ };
59
+
60
+ const deleteFindingSchema = {
61
+ finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
62
+ };
63
+
64
+ const VALID_SCOPES = ['summary', 'detailed'] as const;
65
+
66
+ const queryKnowledgeBaseSchema = {
67
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
68
+ scope: { type: 'string' as const, default: 'summary', validate: createEnumValidator(VALID_SCOPES) },
69
+ categories: { type: 'array' as const },
70
+ limit: { type: 'number' as const, default: 5 },
71
+ search_query: { type: 'string' as const },
72
+ };
18
73
 
19
74
  export const addFinding: Handler = async (args, ctx) => {
20
- const { project_id, category, title, description, severity, file_path, line_number, related_task_id } = args as {
21
- project_id: string;
22
- category?: FindingCategory;
23
- title: string;
24
- description?: string;
25
- severity?: FindingSeverity;
26
- file_path?: string;
27
- line_number?: number;
28
- related_task_id?: string;
29
- };
30
-
31
- validateRequired(project_id, 'project_id');
32
- validateUUID(project_id, 'project_id');
33
- validateRequired(title, 'title');
34
- if (related_task_id) validateUUID(related_task_id, 'related_task_id');
75
+ const { project_id, title, description, category, severity, file_path, line_number, related_task_id } = parseArgs(args, addFindingSchema);
35
76
 
36
77
  const apiClient = getApiClient();
37
78
  const response = await apiClient.addFinding(project_id, {
38
79
  title,
39
80
  description,
40
- category,
41
- severity,
81
+ category: category as FindingCategory | undefined,
82
+ severity: severity as FindingSeverity | undefined,
42
83
  file_path,
43
84
  line_number,
44
85
  related_task_id
@@ -51,26 +92,18 @@ export const addFinding: Handler = async (args, ctx) => {
51
92
  return { result: response.data };
52
93
  };
53
94
 
54
- export const getFindings: Handler = async (args, ctx) => {
55
- const { project_id, category, severity, status, limit = 50, offset = 0, search_query } = args as {
56
- project_id: string;
57
- category?: FindingCategory;
58
- severity?: FindingSeverity;
59
- status?: FindingStatus;
60
- limit?: number;
61
- offset?: number;
62
- search_query?: string;
63
- };
64
-
65
- validateRequired(project_id, 'project_id');
66
- validateUUID(project_id, 'project_id');
95
+ export const getFindings: Handler = async (args, _ctx) => {
96
+ const { project_id, category, severity, status, limit, offset, search_query, summary_only } = parseArgs(args, getFindingsSchema);
67
97
 
68
98
  const apiClient = getApiClient();
69
99
  const response = await apiClient.getFindings(project_id, {
70
- category,
71
- severity,
72
- status,
73
- limit
100
+ category: category as FindingCategory | undefined,
101
+ severity: severity as FindingSeverity | undefined,
102
+ status: status as FindingStatus | undefined,
103
+ limit,
104
+ offset,
105
+ search_query,
106
+ summary_only
74
107
  });
75
108
 
76
109
  if (!response.ok) {
@@ -80,25 +113,33 @@ export const getFindings: Handler = async (args, ctx) => {
80
113
  return { result: response.data };
81
114
  };
82
115
 
83
- export const updateFinding: Handler = async (args, ctx) => {
84
- const { finding_id, status, resolution_note, title, description, severity } = args as {
85
- finding_id: string;
86
- status?: FindingStatus;
87
- resolution_note?: string;
88
- title?: string;
89
- description?: string;
90
- severity?: FindingSeverity;
91
- };
116
+ /**
117
+ * Get aggregate statistics about findings for a project.
118
+ * Returns counts by category, severity, and status without the actual finding data.
119
+ * This is much more token-efficient than get_findings for understanding the overall state.
120
+ */
121
+ export const getFindingsStats: Handler = async (args, _ctx) => {
122
+ const { project_id } = parseArgs(args, getFindingsStatsSchema);
92
123
 
93
- validateRequired(finding_id, 'finding_id');
94
- validateUUID(finding_id, 'finding_id');
124
+ const apiClient = getApiClient();
125
+ const response = await apiClient.getFindingsStats(project_id);
126
+
127
+ if (!response.ok) {
128
+ throw new Error(response.error || 'Failed to get findings stats');
129
+ }
130
+
131
+ return { result: response.data };
132
+ };
133
+
134
+ export const updateFinding: Handler = async (args, _ctx) => {
135
+ const { finding_id, title, description, severity, status, resolution_note } = parseArgs(args, updateFindingSchema);
95
136
 
96
137
  const apiClient = getApiClient();
97
138
  const response = await apiClient.updateFinding(finding_id, {
98
139
  title,
99
140
  description,
100
- severity,
101
- status,
141
+ severity: severity as FindingSeverity | undefined,
142
+ status: status as FindingStatus | undefined,
102
143
  resolution_note
103
144
  });
104
145
 
@@ -109,11 +150,8 @@ export const updateFinding: Handler = async (args, ctx) => {
109
150
  return { result: response.data };
110
151
  };
111
152
 
112
- export const deleteFinding: Handler = async (args, ctx) => {
113
- const { finding_id } = args as { finding_id: string };
114
-
115
- validateRequired(finding_id, 'finding_id');
116
- validateUUID(finding_id, 'finding_id');
153
+ export const deleteFinding: Handler = async (args, _ctx) => {
154
+ const { finding_id } = parseArgs(args, deleteFindingSchema);
117
155
 
118
156
  const apiClient = getApiClient();
119
157
  const response = await apiClient.deleteFinding(finding_id);
@@ -125,12 +163,40 @@ export const deleteFinding: Handler = async (args, ctx) => {
125
163
  return { result: response.data };
126
164
  };
127
165
 
166
+ /**
167
+ * Query aggregated project knowledge in a single call.
168
+ * Returns findings, Q&A, decisions, completed tasks, and resolved blockers.
169
+ * Use this instead of multiple separate tool calls to reduce token usage.
170
+ */
171
+ export const queryKnowledgeBase: Handler = async (args, _ctx) => {
172
+ const { project_id, scope, categories, limit, search_query } = parseArgs(args, queryKnowledgeBaseSchema);
173
+
174
+ // Validate limit range
175
+ const effectiveLimit = Math.min(Math.max(1, limit ?? 5), 20);
176
+
177
+ const apiClient = getApiClient();
178
+ const response = await apiClient.queryKnowledgeBase(project_id, {
179
+ scope: scope as 'summary' | 'detailed' | undefined,
180
+ categories: categories as string[] | undefined,
181
+ limit: effectiveLimit,
182
+ search_query
183
+ });
184
+
185
+ if (!response.ok) {
186
+ throw new Error(response.error || 'Failed to query knowledge base');
187
+ }
188
+
189
+ return { result: response.data };
190
+ };
191
+
128
192
  /**
129
193
  * Findings handlers registry
130
194
  */
131
195
  export const findingHandlers: HandlerRegistry = {
132
196
  add_finding: addFinding,
133
197
  get_findings: getFindings,
198
+ get_findings_stats: getFindingsStats,
134
199
  update_finding: updateFinding,
135
200
  delete_finding: deleteFinding,
201
+ query_knowledge_base: queryKnowledgeBase,
136
202
  };