@vibescope/mcp-server 0.2.2 → 0.2.4

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 (80) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +35 -20
  3. package/dist/api-client.d.ts +276 -8
  4. package/dist/api-client.js +128 -9
  5. package/dist/handlers/blockers.d.ts +11 -0
  6. package/dist/handlers/blockers.js +37 -2
  7. package/dist/handlers/bodies-of-work.d.ts +2 -0
  8. package/dist/handlers/bodies-of-work.js +30 -1
  9. package/dist/handlers/connectors.js +2 -2
  10. package/dist/handlers/decisions.d.ts +11 -0
  11. package/dist/handlers/decisions.js +37 -2
  12. package/dist/handlers/deployment.d.ts +6 -0
  13. package/dist/handlers/deployment.js +33 -5
  14. package/dist/handlers/discovery.js +27 -11
  15. package/dist/handlers/fallback.js +12 -6
  16. package/dist/handlers/file-checkouts.d.ts +1 -0
  17. package/dist/handlers/file-checkouts.js +17 -2
  18. package/dist/handlers/findings.d.ts +5 -0
  19. package/dist/handlers/findings.js +19 -2
  20. package/dist/handlers/git-issues.js +4 -2
  21. package/dist/handlers/ideas.d.ts +5 -0
  22. package/dist/handlers/ideas.js +19 -2
  23. package/dist/handlers/progress.js +2 -2
  24. package/dist/handlers/project.d.ts +1 -0
  25. package/dist/handlers/project.js +35 -2
  26. package/dist/handlers/requests.js +6 -3
  27. package/dist/handlers/roles.js +13 -2
  28. package/dist/handlers/session.d.ts +12 -0
  29. package/dist/handlers/session.js +288 -25
  30. package/dist/handlers/sprints.d.ts +2 -0
  31. package/dist/handlers/sprints.js +30 -1
  32. package/dist/handlers/tasks.d.ts +25 -2
  33. package/dist/handlers/tasks.js +228 -35
  34. package/dist/handlers/tool-docs.js +72 -5
  35. package/dist/templates/agent-guidelines.d.ts +18 -0
  36. package/dist/templates/agent-guidelines.js +207 -0
  37. package/dist/tools.js +478 -125
  38. package/dist/utils.d.ts +5 -2
  39. package/dist/utils.js +90 -51
  40. package/package.json +51 -46
  41. package/scripts/version-bump.ts +203 -0
  42. package/src/api-client.test.ts +8 -3
  43. package/src/api-client.ts +376 -13
  44. package/src/handlers/__test-setup__.ts +5 -0
  45. package/src/handlers/blockers.test.ts +76 -0
  46. package/src/handlers/blockers.ts +56 -2
  47. package/src/handlers/bodies-of-work.ts +59 -1
  48. package/src/handlers/connectors.ts +2 -2
  49. package/src/handlers/decisions.test.ts +71 -2
  50. package/src/handlers/decisions.ts +56 -2
  51. package/src/handlers/deployment.test.ts +81 -0
  52. package/src/handlers/deployment.ts +38 -5
  53. package/src/handlers/discovery.ts +27 -11
  54. package/src/handlers/fallback.test.ts +11 -10
  55. package/src/handlers/fallback.ts +14 -8
  56. package/src/handlers/file-checkouts.test.ts +83 -3
  57. package/src/handlers/file-checkouts.ts +22 -2
  58. package/src/handlers/findings.test.ts +2 -2
  59. package/src/handlers/findings.ts +38 -2
  60. package/src/handlers/git-issues.test.ts +2 -2
  61. package/src/handlers/git-issues.ts +4 -2
  62. package/src/handlers/ideas.test.ts +1 -1
  63. package/src/handlers/ideas.ts +34 -2
  64. package/src/handlers/progress.ts +2 -2
  65. package/src/handlers/project.ts +47 -2
  66. package/src/handlers/requests.test.ts +38 -7
  67. package/src/handlers/requests.ts +6 -3
  68. package/src/handlers/roles.test.ts +1 -1
  69. package/src/handlers/roles.ts +20 -2
  70. package/src/handlers/session.test.ts +303 -4
  71. package/src/handlers/session.ts +335 -28
  72. package/src/handlers/sprints.ts +61 -1
  73. package/src/handlers/tasks.test.ts +0 -73
  74. package/src/handlers/tasks.ts +269 -40
  75. package/src/handlers/tool-docs.ts +77 -5
  76. package/src/handlers/types.test.ts +259 -0
  77. package/src/templates/agent-guidelines.ts +210 -0
  78. package/src/tools.ts +479 -125
  79. package/src/utils.test.ts +7 -5
  80. package/src/utils.ts +95 -51
@@ -29,10 +29,14 @@ const resolveBlockerSchema = {
29
29
  resolution_note: { type: 'string' as const },
30
30
  };
31
31
 
32
+ const getBlockerSchema = {
33
+ blocker_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
34
+ };
35
+
32
36
  const getBlockersSchema = {
33
37
  project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
34
38
  status: { type: 'string' as const, default: 'open', validate: createEnumValidator(VALID_BLOCKER_STATUSES) },
35
- limit: { type: 'number' as const, default: 50 },
39
+ limit: { type: 'number' as const, default: 10 },
36
40
  offset: { type: 'number' as const, default: 0 },
37
41
  search_query: { type: 'string' as const },
38
42
  };
@@ -41,6 +45,10 @@ const deleteBlockerSchema = {
41
45
  blocker_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
42
46
  };
43
47
 
48
+ const getBlockersStatsSchema = {
49
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
50
+ };
51
+
44
52
  export const addBlocker: Handler = async (args, ctx) => {
45
53
  const { project_id, description } = parseArgs(args, addBlockerSchema);
46
54
 
@@ -67,13 +75,39 @@ export const resolveBlocker: Handler = async (args, _ctx) => {
67
75
  return success(response.data);
68
76
  };
69
77
 
78
+ /**
79
+ * Get a single blocker by ID.
80
+ * More token-efficient than get_blockers when you need details for a specific blocker.
81
+ */
82
+ export const getBlocker: Handler = async (args, _ctx) => {
83
+ const { blocker_id } = parseArgs(args, getBlockerSchema);
84
+
85
+ const apiClient = getApiClient();
86
+ const response = await apiClient.proxy<{
87
+ blocker: {
88
+ id: string;
89
+ description: string;
90
+ status: string;
91
+ resolution_note?: string;
92
+ created_at: string;
93
+ resolved_at?: string;
94
+ };
95
+ }>('get_blocker', { blocker_id });
96
+
97
+ if (!response.ok) {
98
+ return error(response.error || 'Failed to get blocker');
99
+ }
100
+
101
+ return success(response.data);
102
+ };
103
+
70
104
  export const getBlockers: Handler = async (args, _ctx) => {
71
105
  const { project_id, status, limit, offset, search_query } = parseArgs(args, getBlockersSchema);
72
106
 
73
107
  const apiClient = getApiClient();
74
108
  const response = await apiClient.getBlockers(project_id, {
75
109
  status,
76
- limit,
110
+ limit: Math.min(limit ?? 10, 200),
77
111
  offset,
78
112
  search_query
79
113
  });
@@ -98,12 +132,32 @@ export const deleteBlocker: Handler = async (args, _ctx) => {
98
132
  return success(response.data);
99
133
  };
100
134
 
135
+ /**
136
+ * Get aggregate statistics about blockers for a project.
137
+ * Returns total count and breakdown by status without the actual blocker data.
138
+ * This is much more token-efficient than get_blockers for understanding the overall state.
139
+ */
140
+ export const getBlockersStats: Handler = async (args, _ctx) => {
141
+ const { project_id } = parseArgs(args, getBlockersStatsSchema);
142
+
143
+ const apiClient = getApiClient();
144
+ const response = await apiClient.getBlockersStats(project_id);
145
+
146
+ if (!response.ok) {
147
+ return error(response.error || 'Failed to get blockers stats');
148
+ }
149
+
150
+ return success(response.data);
151
+ };
152
+
101
153
  /**
102
154
  * Blockers handlers registry
103
155
  */
104
156
  export const blockerHandlers: HandlerRegistry = {
105
157
  add_blocker: addBlocker,
106
158
  resolve_blocker: resolveBlocker,
159
+ get_blocker: getBlocker,
107
160
  get_blockers: getBlockers,
161
+ get_blockers_stats: getBlockersStats,
108
162
  delete_blocker: deleteBlocker,
109
163
  };
@@ -22,7 +22,7 @@ import type { Handler, HandlerRegistry } from './types.js';
22
22
  import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
23
23
  import { getApiClient } from '../api-client.js';
24
24
 
25
- const BODY_OF_WORK_STATUSES = ['draft', 'active', 'completed', 'cancelled'] as const;
25
+ const BODY_OF_WORK_STATUSES = ['draft', 'active', 'completed', 'cancelled', 'archived'] as const;
26
26
  const TASK_PHASES = ['pre', 'core', 'post'] as const;
27
27
  const DEPLOY_ENVIRONMENTS = ['development', 'staging', 'production'] as const;
28
28
  const VERSION_BUMPS = ['patch', 'minor', 'major'] as const;
@@ -205,6 +205,8 @@ export const getBodyOfWork: Handler = async (args, ctx) => {
205
205
  description?: string;
206
206
  status: string;
207
207
  progress_percentage: number;
208
+ total_estimated_minutes?: number;
209
+ completed_estimated_minutes?: number;
208
210
  };
209
211
  // Full response includes tasks grouped by phase
210
212
  tasks?: {
@@ -244,6 +246,8 @@ export const getBodiesOfWork: Handler = async (args, ctx) => {
244
246
  description?: string;
245
247
  status: string;
246
248
  progress_percentage: number;
249
+ total_estimated_minutes?: number;
250
+ completed_estimated_minutes?: number;
247
251
  task_counts: {
248
252
  pre: { total: number; completed: number };
249
253
  core: { total: number; completed: number };
@@ -449,6 +453,58 @@ export const getNextBodyOfWorkTask: Handler = async (args, ctx) => {
449
453
  return { result: response.data };
450
454
  };
451
455
 
456
+ // ============================================================================
457
+ // Archive / Unarchive Handlers
458
+ // ============================================================================
459
+
460
+ const archiveBodyOfWorkSchema = {
461
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
462
+ };
463
+
464
+ const unarchiveBodyOfWorkSchema = {
465
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
466
+ };
467
+
468
+ export const archiveBodyOfWork: Handler = async (args, ctx) => {
469
+ const { body_of_work_id } = parseArgs(args, archiveBodyOfWorkSchema);
470
+
471
+ const apiClient = getApiClient();
472
+
473
+ const response = await apiClient.proxy<{
474
+ success: boolean;
475
+ body_of_work_id: string;
476
+ title: string;
477
+ previous_status: string;
478
+ new_status: string;
479
+ }>('archive_body_of_work', { body_of_work_id });
480
+
481
+ if (!response.ok) {
482
+ return { result: { error: response.error || 'Failed to archive body of work' }, isError: true };
483
+ }
484
+
485
+ return { result: response.data };
486
+ };
487
+
488
+ export const unarchiveBodyOfWork: Handler = async (args, ctx) => {
489
+ const { body_of_work_id } = parseArgs(args, unarchiveBodyOfWorkSchema);
490
+
491
+ const apiClient = getApiClient();
492
+
493
+ const response = await apiClient.proxy<{
494
+ success: boolean;
495
+ body_of_work_id: string;
496
+ title: string;
497
+ previous_status: string;
498
+ new_status: string;
499
+ }>('unarchive_body_of_work', { body_of_work_id });
500
+
501
+ if (!response.ok) {
502
+ return { result: { error: response.error || 'Failed to unarchive body of work' }, isError: true };
503
+ }
504
+
505
+ return { result: response.data };
506
+ };
507
+
452
508
  /**
453
509
  * Bodies of Work handlers registry
454
510
  */
@@ -465,4 +521,6 @@ export const bodiesOfWorkHandlers: HandlerRegistry = {
465
521
  remove_task_dependency: removeTaskDependency,
466
522
  get_task_dependencies: getTaskDependencies,
467
523
  get_next_body_of_work_task: getNextBodyOfWorkTask,
524
+ archive_body_of_work: archiveBodyOfWork,
525
+ unarchive_body_of_work: unarchiveBodyOfWork,
468
526
  };
@@ -86,7 +86,7 @@ export const getConnectors: Handler = async (args, _ctx) => {
86
86
  const response = await apiClient.getConnectors(project_id, {
87
87
  type,
88
88
  status,
89
- limit,
89
+ limit: Math.min(limit ?? 50, 50),
90
90
  offset
91
91
  });
92
92
 
@@ -204,7 +204,7 @@ export const getConnectorEvents: Handler = async (args, _ctx) => {
204
204
  connector_id,
205
205
  project_id,
206
206
  status,
207
- limit,
207
+ limit: Math.min(limit ?? 50, 50),
208
208
  offset
209
209
  });
210
210
 
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { logDecision, getDecisions, deleteDecision } from './decisions.js';
2
+ import { logDecision, getDecisions, getDecisionsStats, deleteDecision } from './decisions.js';
3
3
  import { ValidationError } from '../validators.js';
4
4
  import { createMockContext } from './__test-utils__.js';
5
5
  import { mockApiClient } from './__test-setup__.js';
@@ -215,7 +215,7 @@ describe('getDecisions', () => {
215
215
 
216
216
  expect(mockApiClient.getDecisions).toHaveBeenCalledWith(
217
217
  '123e4567-e89b-12d3-a456-426614174000',
218
- { limit: 50, offset: 0, search_query: undefined }
218
+ { limit: 10, offset: 0, search_query: undefined }
219
219
  );
220
220
  });
221
221
 
@@ -311,3 +311,72 @@ describe('deleteDecision', () => {
311
311
  });
312
312
  });
313
313
  });
314
+
315
+ // ============================================================================
316
+ // getDecisionsStats Tests
317
+ // ============================================================================
318
+
319
+ describe('getDecisionsStats', () => {
320
+ beforeEach(() => vi.clearAllMocks());
321
+
322
+ it('should throw error for missing project_id', async () => {
323
+ const ctx = createMockContext();
324
+
325
+ await expect(getDecisionsStats({}, ctx)).rejects.toThrow(ValidationError);
326
+ });
327
+
328
+ it('should throw error for invalid project_id UUID', async () => {
329
+ const ctx = createMockContext();
330
+
331
+ await expect(
332
+ getDecisionsStats({ project_id: 'invalid' }, ctx)
333
+ ).rejects.toThrow(ValidationError);
334
+ });
335
+
336
+ it('should return decisions stats', async () => {
337
+ mockApiClient.getDecisionsStats.mockResolvedValue({
338
+ ok: true,
339
+ data: { total: 10 },
340
+ });
341
+ const ctx = createMockContext();
342
+
343
+ const result = await getDecisionsStats(
344
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
345
+ ctx
346
+ );
347
+
348
+ expect(result.result).toMatchObject({ total: 10 });
349
+ });
350
+
351
+ it('should call API client getDecisionsStats', async () => {
352
+ mockApiClient.getDecisionsStats.mockResolvedValue({
353
+ ok: true,
354
+ data: { total: 0 },
355
+ });
356
+ const ctx = createMockContext();
357
+
358
+ await getDecisionsStats(
359
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
360
+ ctx
361
+ );
362
+
363
+ expect(mockApiClient.getDecisionsStats).toHaveBeenCalledWith(
364
+ '123e4567-e89b-12d3-a456-426614174000'
365
+ );
366
+ });
367
+
368
+ it('should return error when API call fails', async () => {
369
+ mockApiClient.getDecisionsStats.mockResolvedValue({
370
+ ok: false,
371
+ error: 'Query failed',
372
+ });
373
+ const ctx = createMockContext();
374
+
375
+ const result = await getDecisionsStats({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
376
+
377
+ expect(result.isError).toBe(true);
378
+ expect(result.result).toMatchObject({
379
+ error: 'Query failed',
380
+ });
381
+ });
382
+ });
@@ -22,9 +22,13 @@ const logDecisionSchema = {
22
22
  alternatives_considered: { type: 'array' as const },
23
23
  };
24
24
 
25
+ const getDecisionSchema = {
26
+ decision_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
27
+ };
28
+
25
29
  const getDecisionsSchema = {
26
30
  project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
27
- limit: { type: 'number' as const, default: 50 },
31
+ limit: { type: 'number' as const, default: 10 },
28
32
  offset: { type: 'number' as const, default: 0 },
29
33
  search_query: { type: 'string' as const },
30
34
  };
@@ -33,6 +37,10 @@ const deleteDecisionSchema = {
33
37
  decision_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
34
38
  };
35
39
 
40
+ const getDecisionsStatsSchema = {
41
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
42
+ };
43
+
36
44
  export const logDecision: Handler = async (args, ctx) => {
37
45
  const { project_id, title, description, rationale, alternatives_considered } = parseArgs(args, logDecisionSchema);
38
46
 
@@ -53,13 +61,39 @@ export const logDecision: Handler = async (args, ctx) => {
53
61
  return { result: { success: true, title, decision_id: response.data?.decision_id } };
54
62
  };
55
63
 
64
+ /**
65
+ * Get a single decision by ID.
66
+ * More token-efficient than get_decisions when you need details for a specific decision.
67
+ */
68
+ export const getDecision: Handler = async (args, _ctx) => {
69
+ const { decision_id } = parseArgs(args, getDecisionSchema);
70
+
71
+ const apiClient = getApiClient();
72
+ const response = await apiClient.proxy<{
73
+ decision: {
74
+ id: string;
75
+ title: string;
76
+ description: string;
77
+ rationale?: string;
78
+ alternatives_considered?: string[];
79
+ created_at: string;
80
+ };
81
+ }>('get_decision', { decision_id });
82
+
83
+ if (!response.ok) {
84
+ return { result: { error: response.error || 'Failed to get decision' }, isError: true };
85
+ }
86
+
87
+ return { result: response.data };
88
+ };
89
+
56
90
  export const getDecisions: Handler = async (args, _ctx) => {
57
91
  const { project_id, limit, offset, search_query } = parseArgs(args, getDecisionsSchema);
58
92
 
59
93
  const apiClient = getApiClient();
60
94
 
61
95
  const response = await apiClient.getDecisions(project_id, {
62
- limit,
96
+ limit: Math.min(limit ?? 10, 200),
63
97
  offset,
64
98
  search_query
65
99
  });
@@ -89,11 +123,31 @@ export const deleteDecision: Handler = async (args, _ctx) => {
89
123
  return { result: { success: true } };
90
124
  };
91
125
 
126
+ /**
127
+ * Get aggregate statistics about decisions for a project.
128
+ * Returns total count without the actual decision data.
129
+ * This is more token-efficient than get_decisions for understanding overall state.
130
+ */
131
+ export const getDecisionsStats: Handler = async (args, _ctx) => {
132
+ const { project_id } = parseArgs(args, getDecisionsStatsSchema);
133
+
134
+ const apiClient = getApiClient();
135
+ const response = await apiClient.getDecisionsStats(project_id);
136
+
137
+ if (!response.ok) {
138
+ return { result: { error: response.error || 'Failed to get decisions stats' }, isError: true };
139
+ }
140
+
141
+ return { result: response.data };
142
+ };
143
+
92
144
  /**
93
145
  * Decisions handlers registry
94
146
  */
95
147
  export const decisionHandlers: HandlerRegistry = {
96
148
  log_decision: logDecision,
149
+ get_decision: getDecision,
97
150
  get_decisions: getDecisions,
151
+ get_decisions_stats: getDecisionsStats,
98
152
  delete_decision: deleteDecision,
99
153
  };
@@ -8,6 +8,7 @@ import {
8
8
  cancelDeployment,
9
9
  addDeploymentRequirement,
10
10
  getDeploymentRequirements,
11
+ getDeploymentRequirementsStats,
11
12
  } from './deployment.js';
12
13
  import { ValidationError } from '../validators.js';
13
14
  import { createMockContext } from './__test-utils__.js';
@@ -467,4 +468,84 @@ describe('getDeploymentRequirements', () => {
467
468
  expect((result.result as { requirements: unknown[] }).requirements).toHaveLength(2);
468
469
  expect(result.result).toHaveProperty('deployment_blocked', true);
469
470
  });
471
+
472
+ it('should pass pagination params to API client', async () => {
473
+ mockApiClient.getDeploymentRequirements.mockResolvedValue({
474
+ ok: true,
475
+ data: { requirements: [], deployment_blocked: false },
476
+ });
477
+ const ctx = createMockContext();
478
+
479
+ await getDeploymentRequirements(
480
+ { project_id: VALID_UUID, limit: 10, offset: 5 },
481
+ ctx
482
+ );
483
+
484
+ expect(mockApiClient.getDeploymentRequirements).toHaveBeenCalledWith(
485
+ VALID_UUID,
486
+ expect.objectContaining({ limit: 10, offset: 5 })
487
+ );
488
+ });
489
+ });
490
+
491
+ // ============================================================================
492
+ // getDeploymentRequirementsStats Tests
493
+ // ============================================================================
494
+
495
+ describe('getDeploymentRequirementsStats', () => {
496
+ beforeEach(() => vi.clearAllMocks());
497
+
498
+ it('should throw error for missing project_id', async () => {
499
+ const ctx = createMockContext();
500
+ await expect(getDeploymentRequirementsStats({}, ctx)).rejects.toThrow(ValidationError);
501
+ });
502
+
503
+ it('should throw error for invalid project_id UUID', async () => {
504
+ const ctx = createMockContext();
505
+ await expect(
506
+ getDeploymentRequirementsStats({ project_id: 'invalid' }, ctx)
507
+ ).rejects.toThrow(ValidationError);
508
+ });
509
+
510
+ it('should return stats from API client', async () => {
511
+ const mockStats = {
512
+ total: 5,
513
+ by_status: { pending: 3, completed: 2 },
514
+ by_stage: { preparation: 2, deployment: 2, verification: 1 },
515
+ by_type: { migration: 2, env_var: 2, manual: 1 },
516
+ };
517
+ mockApiClient.getDeploymentRequirementsStats.mockResolvedValue({
518
+ ok: true,
519
+ data: mockStats,
520
+ });
521
+ const ctx = createMockContext();
522
+
523
+ const result = await getDeploymentRequirementsStats({ project_id: VALID_UUID }, ctx);
524
+
525
+ expect(result.result).toMatchObject(mockStats);
526
+ });
527
+
528
+ it('should call API client with correct project_id', async () => {
529
+ mockApiClient.getDeploymentRequirementsStats.mockResolvedValue({
530
+ ok: true,
531
+ data: { total: 0, by_status: {}, by_stage: {}, by_type: {} },
532
+ });
533
+ const ctx = createMockContext();
534
+
535
+ await getDeploymentRequirementsStats({ project_id: VALID_UUID }, ctx);
536
+
537
+ expect(mockApiClient.getDeploymentRequirementsStats).toHaveBeenCalledWith(VALID_UUID);
538
+ });
539
+
540
+ it('should handle API error', async () => {
541
+ mockApiClient.getDeploymentRequirementsStats.mockResolvedValue({
542
+ ok: false,
543
+ error: 'Database error',
544
+ });
545
+ const ctx = createMockContext();
546
+
547
+ const result = await getDeploymentRequirementsStats({ project_id: VALID_UUID }, ctx);
548
+
549
+ expect(result.result).toHaveProperty('error');
550
+ });
470
551
  });
@@ -87,7 +87,7 @@ const addDeploymentRequirementSchema = {
87
87
  file_path: { type: 'string' as const },
88
88
  stage: { type: 'string' as const, default: 'preparation', validate: createEnumValidator(REQUIREMENT_STAGES) },
89
89
  blocking: { type: 'boolean' as const, default: false },
90
- recurring: { type: 'boolean' as const, default: false },
90
+ recurring: { type: 'boolean' as const, default: true },
91
91
  };
92
92
 
93
93
  const completeDeploymentRequirementSchema = {
@@ -98,6 +98,12 @@ const getDeploymentRequirementsSchema = {
98
98
  project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
99
99
  status: { type: 'string' as const, default: 'pending', validate: createEnumValidator(REQUIREMENT_STATUSES) },
100
100
  stage: { type: 'string' as const, validate: createEnumValidator([...REQUIREMENT_STAGES, 'all'] as const) },
101
+ limit: { type: 'number' as const, default: 50 },
102
+ offset: { type: 'number' as const, default: 0 },
103
+ };
104
+
105
+ const getDeploymentRequirementsStatsSchema = {
106
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
101
107
  };
102
108
 
103
109
  const scheduleDeploymentSchema = {
@@ -115,6 +121,8 @@ const scheduleDeploymentSchema = {
115
121
  const getScheduledDeploymentsSchema = {
116
122
  project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
117
123
  include_disabled: { type: 'boolean' as const, default: false },
124
+ limit: { type: 'number' as const, default: 50 },
125
+ offset: { type: 'number' as const, default: 0 },
118
126
  };
119
127
 
120
128
  const updateScheduledDeploymentSchema = {
@@ -291,12 +299,14 @@ export const completeDeploymentRequirement: Handler = async (args, ctx) => {
291
299
  };
292
300
 
293
301
  export const getDeploymentRequirements: Handler = async (args, ctx) => {
294
- const { project_id, status, stage } = parseArgs(args, getDeploymentRequirementsSchema);
302
+ const { project_id, status, stage, limit, offset } = parseArgs(args, getDeploymentRequirementsSchema);
295
303
 
296
304
  const apiClient = getApiClient();
297
305
  const response = await apiClient.getDeploymentRequirements(project_id, {
298
306
  status: status as 'pending' | 'completed' | 'converted_to_task' | 'all',
299
- stage: stage as 'preparation' | 'deployment' | 'verification' | 'all' | undefined
307
+ stage: stage as 'preparation' | 'deployment' | 'verification' | 'all' | undefined,
308
+ limit,
309
+ offset
300
310
  });
301
311
 
302
312
  if (!response.ok) {
@@ -306,6 +316,24 @@ export const getDeploymentRequirements: Handler = async (args, ctx) => {
306
316
  return { result: response.data };
307
317
  };
308
318
 
319
+ /**
320
+ * Get aggregate statistics about deployment requirements for a project.
321
+ * Returns total count and breakdowns by status, stage, and type.
322
+ * More token-efficient than get_deployment_requirements when you just need to understand the overall state.
323
+ */
324
+ export const getDeploymentRequirementsStats: Handler = async (args, ctx) => {
325
+ const { project_id } = parseArgs(args, getDeploymentRequirementsStatsSchema);
326
+
327
+ const apiClient = getApiClient();
328
+ const response = await apiClient.getDeploymentRequirementsStats(project_id);
329
+
330
+ if (!response.ok) {
331
+ return { result: { error: response.error || 'Failed to get deployment requirements stats' }, isError: true };
332
+ }
333
+
334
+ return { result: response.data };
335
+ };
336
+
309
337
  // ============================================================================
310
338
  // Scheduled Deployments
311
339
  // ============================================================================
@@ -366,10 +394,14 @@ export const scheduleDeployment: Handler = async (args, ctx) => {
366
394
  };
367
395
 
368
396
  export const getScheduledDeployments: Handler = async (args, ctx) => {
369
- const { project_id, include_disabled } = parseArgs(args, getScheduledDeploymentsSchema);
397
+ const { project_id, include_disabled, limit, offset } = parseArgs(args, getScheduledDeploymentsSchema);
370
398
 
371
399
  const apiClient = getApiClient();
372
- const response = await apiClient.getScheduledDeployments(project_id, include_disabled);
400
+ const response = await apiClient.getScheduledDeployments(project_id, {
401
+ includeDisabled: include_disabled,
402
+ limit,
403
+ offset
404
+ });
373
405
 
374
406
  if (!response.ok) {
375
407
  return { result: { error: response.error || 'Failed to get scheduled deployments' }, isError: true };
@@ -498,6 +530,7 @@ export const deploymentHandlers: HandlerRegistry = {
498
530
  add_deployment_requirement: addDeploymentRequirement,
499
531
  complete_deployment_requirement: completeDeploymentRequirement,
500
532
  get_deployment_requirements: getDeploymentRequirements,
533
+ get_deployment_requirements_stats: getDeploymentRequirementsStats,
501
534
  // Scheduled deployments
502
535
  schedule_deployment: scheduleDeployment,
503
536
  get_scheduled_deployments: getScheduledDeployments,