@vibescope/mcp-server 0.2.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 (65) hide show
  1. package/dist/api-client.d.ts +64 -1
  2. package/dist/api-client.js +34 -3
  3. package/dist/handlers/bodies-of-work.js +82 -49
  4. package/dist/handlers/cost.js +62 -54
  5. package/dist/handlers/decisions.js +29 -16
  6. package/dist/handlers/deployment.js +112 -106
  7. package/dist/handlers/discovery.js +35 -5
  8. package/dist/handlers/fallback.js +24 -19
  9. package/dist/handlers/file-checkouts.d.ts +18 -0
  10. package/dist/handlers/file-checkouts.js +101 -0
  11. package/dist/handlers/findings.d.ts +6 -0
  12. package/dist/handlers/findings.js +85 -30
  13. package/dist/handlers/git-issues.js +36 -32
  14. package/dist/handlers/ideas.js +44 -26
  15. package/dist/handlers/index.d.ts +2 -0
  16. package/dist/handlers/index.js +6 -0
  17. package/dist/handlers/milestones.js +34 -27
  18. package/dist/handlers/organizations.js +86 -78
  19. package/dist/handlers/progress.js +22 -11
  20. package/dist/handlers/project.js +62 -22
  21. package/dist/handlers/requests.js +15 -11
  22. package/dist/handlers/roles.d.ts +18 -0
  23. package/dist/handlers/roles.js +130 -0
  24. package/dist/handlers/session.js +30 -8
  25. package/dist/handlers/sprints.js +76 -64
  26. package/dist/handlers/tasks.js +113 -73
  27. package/dist/handlers/validation.js +18 -14
  28. package/dist/tools.js +387 -0
  29. package/package.json +1 -1
  30. package/src/api-client.ts +89 -6
  31. package/src/handlers/__test-setup__.ts +7 -0
  32. package/src/handlers/bodies-of-work.ts +101 -101
  33. package/src/handlers/cost.test.ts +34 -44
  34. package/src/handlers/cost.ts +77 -92
  35. package/src/handlers/decisions.test.ts +3 -2
  36. package/src/handlers/decisions.ts +32 -27
  37. package/src/handlers/deployment.ts +142 -190
  38. package/src/handlers/discovery.test.ts +4 -5
  39. package/src/handlers/discovery.ts +37 -6
  40. package/src/handlers/fallback.ts +31 -29
  41. package/src/handlers/file-checkouts.test.ts +477 -0
  42. package/src/handlers/file-checkouts.ts +127 -0
  43. package/src/handlers/findings.test.ts +145 -0
  44. package/src/handlers/findings.ts +101 -64
  45. package/src/handlers/git-issues.ts +40 -80
  46. package/src/handlers/ideas.ts +56 -54
  47. package/src/handlers/index.ts +6 -0
  48. package/src/handlers/milestones.test.ts +1 -1
  49. package/src/handlers/milestones.ts +47 -45
  50. package/src/handlers/organizations.ts +104 -129
  51. package/src/handlers/progress.ts +24 -22
  52. package/src/handlers/project.ts +89 -57
  53. package/src/handlers/requests.ts +18 -14
  54. package/src/handlers/roles.test.ts +303 -0
  55. package/src/handlers/roles.ts +208 -0
  56. package/src/handlers/session.ts +39 -17
  57. package/src/handlers/sprints.ts +96 -129
  58. package/src/handlers/tasks.ts +144 -138
  59. package/src/handlers/validation.test.ts +1 -1
  60. package/src/handlers/validation.ts +20 -22
  61. package/src/tools.ts +387 -0
  62. package/dist/config/tool-categories.d.ts +0 -31
  63. package/dist/config/tool-categories.js +0 -253
  64. package/dist/knowledge.d.ts +0 -6
  65. package/dist/knowledge.js +0 -218
@@ -19,14 +19,96 @@
19
19
  */
20
20
 
21
21
  import type { Handler, HandlerRegistry } from './types.js';
22
- import { validateRequired, validateUUID } from '../validators.js';
22
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
23
23
  import { getApiClient } from '../api-client.js';
24
24
 
25
- type BodyOfWorkStatus = 'draft' | 'active' | 'completed' | 'cancelled';
26
- type TaskPhase = 'pre' | 'core' | 'post';
27
- type DeployEnvironment = 'development' | 'staging' | 'production';
28
- type VersionBump = 'patch' | 'minor' | 'major';
29
- type DeployTrigger = 'all_completed' | 'all_completed_validated';
25
+ const BODY_OF_WORK_STATUSES = ['draft', 'active', 'completed', 'cancelled'] as const;
26
+ const TASK_PHASES = ['pre', 'core', 'post'] as const;
27
+ const DEPLOY_ENVIRONMENTS = ['development', 'staging', 'production'] as const;
28
+ const VERSION_BUMPS = ['patch', 'minor', 'major'] as const;
29
+ const DEPLOY_TRIGGERS = ['all_completed', 'all_completed_validated'] as const;
30
+
31
+ type BodyOfWorkStatus = typeof BODY_OF_WORK_STATUSES[number];
32
+ type TaskPhase = typeof TASK_PHASES[number];
33
+ type DeployEnvironment = typeof DEPLOY_ENVIRONMENTS[number];
34
+ type VersionBump = typeof VERSION_BUMPS[number];
35
+ type DeployTrigger = typeof DEPLOY_TRIGGERS[number];
36
+
37
+ // ============================================================================
38
+ // Argument Schemas
39
+ // ============================================================================
40
+
41
+ const createBodyOfWorkSchema = {
42
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
43
+ title: { type: 'string' as const, required: true as const },
44
+ description: { type: 'string' as const },
45
+ auto_deploy_on_completion: { type: 'boolean' as const },
46
+ deploy_environment: { type: 'string' as const, validate: createEnumValidator(DEPLOY_ENVIRONMENTS) },
47
+ deploy_version_bump: { type: 'string' as const, validate: createEnumValidator(VERSION_BUMPS) },
48
+ deploy_trigger: { type: 'string' as const, validate: createEnumValidator(DEPLOY_TRIGGERS) },
49
+ };
50
+
51
+ const updateBodyOfWorkSchema = {
52
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
53
+ title: { type: 'string' as const },
54
+ description: { type: 'string' as const },
55
+ auto_deploy_on_completion: { type: 'boolean' as const },
56
+ deploy_environment: { type: 'string' as const, validate: createEnumValidator(DEPLOY_ENVIRONMENTS) },
57
+ deploy_version_bump: { type: 'string' as const, validate: createEnumValidator(VERSION_BUMPS) },
58
+ deploy_trigger: { type: 'string' as const, validate: createEnumValidator(DEPLOY_TRIGGERS) },
59
+ };
60
+
61
+ const getBodyOfWorkSchema = {
62
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
63
+ summary_only: { type: 'boolean' as const, default: false },
64
+ };
65
+
66
+ const getBodiesOfWorkSchema = {
67
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
68
+ status: { type: 'string' as const, validate: createEnumValidator(BODY_OF_WORK_STATUSES) },
69
+ limit: { type: 'number' as const, default: 50 },
70
+ offset: { type: 'number' as const, default: 0 },
71
+ search_query: { type: 'string' as const },
72
+ };
73
+
74
+ const deleteBodyOfWorkSchema = {
75
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
76
+ };
77
+
78
+ const addTaskToBodyOfWorkSchema = {
79
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
80
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
81
+ phase: { type: 'string' as const, validate: createEnumValidator(TASK_PHASES) },
82
+ order_index: { type: 'number' as const },
83
+ };
84
+
85
+ const removeTaskFromBodyOfWorkSchema = {
86
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
87
+ };
88
+
89
+ const activateBodyOfWorkSchema = {
90
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
91
+ };
92
+
93
+ const addTaskDependencySchema = {
94
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
95
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
96
+ depends_on_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
97
+ };
98
+
99
+ const removeTaskDependencySchema = {
100
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
101
+ depends_on_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
102
+ };
103
+
104
+ const getTaskDependenciesSchema = {
105
+ body_of_work_id: { type: 'string' as const, validate: uuidValidator },
106
+ task_id: { type: 'string' as const, validate: uuidValidator },
107
+ };
108
+
109
+ const getNextBodyOfWorkTaskSchema = {
110
+ body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
111
+ };
30
112
 
31
113
  export const createBodyOfWork: Handler = async (args, ctx) => {
32
114
  const {
@@ -37,19 +119,7 @@ export const createBodyOfWork: Handler = async (args, ctx) => {
37
119
  deploy_environment,
38
120
  deploy_version_bump,
39
121
  deploy_trigger,
40
- } = args as {
41
- project_id: string;
42
- title: string;
43
- description?: string;
44
- auto_deploy_on_completion?: boolean;
45
- deploy_environment?: DeployEnvironment;
46
- deploy_version_bump?: VersionBump;
47
- deploy_trigger?: DeployTrigger;
48
- };
49
-
50
- validateRequired(project_id, 'project_id');
51
- validateUUID(project_id, 'project_id');
52
- validateRequired(title, 'title');
122
+ } = parseArgs(args, createBodyOfWorkSchema);
53
123
 
54
124
  const { session } = ctx;
55
125
  const apiClient = getApiClient();
@@ -95,18 +165,7 @@ export const updateBodyOfWork: Handler = async (args, ctx) => {
95
165
  deploy_environment,
96
166
  deploy_version_bump,
97
167
  deploy_trigger,
98
- } = args as {
99
- body_of_work_id: string;
100
- title?: string;
101
- description?: string;
102
- auto_deploy_on_completion?: boolean;
103
- deploy_environment?: DeployEnvironment;
104
- deploy_version_bump?: VersionBump;
105
- deploy_trigger?: DeployTrigger;
106
- };
107
-
108
- validateRequired(body_of_work_id, 'body_of_work_id');
109
- validateUUID(body_of_work_id, 'body_of_work_id');
168
+ } = parseArgs(args, updateBodyOfWorkSchema);
110
169
 
111
170
  // Check if any updates provided
112
171
  if (title === undefined && description === undefined && auto_deploy_on_completion === undefined &&
@@ -134,10 +193,7 @@ export const updateBodyOfWork: Handler = async (args, ctx) => {
134
193
  };
135
194
 
136
195
  export const getBodyOfWork: Handler = async (args, ctx) => {
137
- const { body_of_work_id, summary_only = false } = args as { body_of_work_id: string; summary_only?: boolean };
138
-
139
- validateRequired(body_of_work_id, 'body_of_work_id');
140
- validateUUID(body_of_work_id, 'body_of_work_id');
196
+ const { body_of_work_id, summary_only } = parseArgs(args, getBodyOfWorkSchema);
141
197
 
142
198
  const apiClient = getApiClient();
143
199
 
@@ -177,16 +233,7 @@ export const getBodyOfWork: Handler = async (args, ctx) => {
177
233
  };
178
234
 
179
235
  export const getBodiesOfWork: Handler = async (args, ctx) => {
180
- const { project_id, status, limit = 50, offset = 0, search_query } = args as {
181
- project_id: string;
182
- status?: BodyOfWorkStatus;
183
- limit?: number;
184
- offset?: number;
185
- search_query?: string;
186
- };
187
-
188
- validateRequired(project_id, 'project_id');
189
- validateUUID(project_id, 'project_id');
236
+ const { project_id, status, limit, offset, search_query } = parseArgs(args, getBodiesOfWorkSchema);
190
237
 
191
238
  const apiClient = getApiClient();
192
239
 
@@ -208,7 +255,7 @@ export const getBodiesOfWork: Handler = async (args, ctx) => {
208
255
  }>('get_bodies_of_work', {
209
256
  project_id,
210
257
  status,
211
- limit: Math.min(limit, 100),
258
+ limit: Math.min(limit ?? 50, 100),
212
259
  offset,
213
260
  search_query
214
261
  });
@@ -221,10 +268,7 @@ export const getBodiesOfWork: Handler = async (args, ctx) => {
221
268
  };
222
269
 
223
270
  export const deleteBodyOfWork: Handler = async (args, ctx) => {
224
- const { body_of_work_id } = args as { body_of_work_id: string };
225
-
226
- validateRequired(body_of_work_id, 'body_of_work_id');
227
- validateUUID(body_of_work_id, 'body_of_work_id');
271
+ const { body_of_work_id } = parseArgs(args, deleteBodyOfWorkSchema);
228
272
 
229
273
  const apiClient = getApiClient();
230
274
 
@@ -240,17 +284,7 @@ export const deleteBodyOfWork: Handler = async (args, ctx) => {
240
284
  };
241
285
 
242
286
  export const addTaskToBodyOfWork: Handler = async (args, ctx) => {
243
- const { body_of_work_id, task_id, phase, order_index } = args as {
244
- body_of_work_id: string;
245
- task_id: string;
246
- phase?: TaskPhase;
247
- order_index?: number;
248
- };
249
-
250
- validateRequired(body_of_work_id, 'body_of_work_id');
251
- validateUUID(body_of_work_id, 'body_of_work_id');
252
- validateRequired(task_id, 'task_id');
253
- validateUUID(task_id, 'task_id');
287
+ const { body_of_work_id, task_id, phase, order_index } = parseArgs(args, addTaskToBodyOfWorkSchema);
254
288
 
255
289
  const apiClient = getApiClient();
256
290
 
@@ -275,10 +309,7 @@ export const addTaskToBodyOfWork: Handler = async (args, ctx) => {
275
309
  };
276
310
 
277
311
  export const removeTaskFromBodyOfWork: Handler = async (args, ctx) => {
278
- const { task_id } = args as { task_id: string };
279
-
280
- validateRequired(task_id, 'task_id');
281
- validateUUID(task_id, 'task_id');
312
+ const { task_id } = parseArgs(args, removeTaskFromBodyOfWorkSchema);
282
313
 
283
314
  const apiClient = getApiClient();
284
315
 
@@ -296,10 +327,7 @@ export const removeTaskFromBodyOfWork: Handler = async (args, ctx) => {
296
327
  };
297
328
 
298
329
  export const activateBodyOfWork: Handler = async (args, ctx) => {
299
- const { body_of_work_id } = args as { body_of_work_id: string };
300
-
301
- validateRequired(body_of_work_id, 'body_of_work_id');
302
- validateUUID(body_of_work_id, 'body_of_work_id');
330
+ const { body_of_work_id } = parseArgs(args, activateBodyOfWorkSchema);
303
331
 
304
332
  const apiClient = getApiClient();
305
333
 
@@ -319,18 +347,7 @@ export const activateBodyOfWork: Handler = async (args, ctx) => {
319
347
  };
320
348
 
321
349
  export const addTaskDependency: Handler = async (args, ctx) => {
322
- const { body_of_work_id, task_id, depends_on_task_id } = args as {
323
- body_of_work_id: string;
324
- task_id: string;
325
- depends_on_task_id: string;
326
- };
327
-
328
- validateRequired(body_of_work_id, 'body_of_work_id');
329
- validateUUID(body_of_work_id, 'body_of_work_id');
330
- validateRequired(task_id, 'task_id');
331
- validateUUID(task_id, 'task_id');
332
- validateRequired(depends_on_task_id, 'depends_on_task_id');
333
- validateUUID(depends_on_task_id, 'depends_on_task_id');
350
+ const { body_of_work_id, task_id, depends_on_task_id } = parseArgs(args, addTaskDependencySchema);
334
351
 
335
352
  if (task_id === depends_on_task_id) {
336
353
  throw new Error('A task cannot depend on itself');
@@ -358,15 +375,7 @@ export const addTaskDependency: Handler = async (args, ctx) => {
358
375
  };
359
376
 
360
377
  export const removeTaskDependency: Handler = async (args, ctx) => {
361
- const { task_id, depends_on_task_id } = args as {
362
- task_id: string;
363
- depends_on_task_id: string;
364
- };
365
-
366
- validateRequired(task_id, 'task_id');
367
- validateUUID(task_id, 'task_id');
368
- validateRequired(depends_on_task_id, 'depends_on_task_id');
369
- validateUUID(depends_on_task_id, 'depends_on_task_id');
378
+ const { task_id, depends_on_task_id } = parseArgs(args, removeTaskDependencySchema);
370
379
 
371
380
  const apiClient = getApiClient();
372
381
 
@@ -387,18 +396,12 @@ export const removeTaskDependency: Handler = async (args, ctx) => {
387
396
  };
388
397
 
389
398
  export const getTaskDependencies: Handler = async (args, ctx) => {
390
- const { body_of_work_id, task_id } = args as {
391
- body_of_work_id?: string;
392
- task_id?: string;
393
- };
399
+ const { body_of_work_id, task_id } = parseArgs(args, getTaskDependenciesSchema);
394
400
 
395
401
  if (!body_of_work_id && !task_id) {
396
402
  throw new Error('Either body_of_work_id or task_id is required');
397
403
  }
398
404
 
399
- if (body_of_work_id) validateUUID(body_of_work_id, 'body_of_work_id');
400
- if (task_id) validateUUID(task_id, 'task_id');
401
-
402
405
  const apiClient = getApiClient();
403
406
 
404
407
  const response = await apiClient.proxy<{
@@ -421,10 +424,7 @@ export const getTaskDependencies: Handler = async (args, ctx) => {
421
424
  };
422
425
 
423
426
  export const getNextBodyOfWorkTask: Handler = async (args, ctx) => {
424
- const { body_of_work_id } = args as { body_of_work_id: string };
425
-
426
- validateRequired(body_of_work_id, 'body_of_work_id');
427
- validateUUID(body_of_work_id, 'body_of_work_id');
427
+ const { body_of_work_id } = parseArgs(args, getNextBodyOfWorkTaskSchema);
428
428
 
429
429
  const apiClient = getApiClient();
430
430
 
@@ -13,6 +13,7 @@ import {
13
13
  } from './cost.js';
14
14
  import { createMockContext } from './__test-utils__.js';
15
15
  import { mockApiClient } from './__test-setup__.js';
16
+ import { ValidationError } from '../validators.js';
16
17
 
17
18
  const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
18
19
 
@@ -26,13 +27,11 @@ describe('Cost Handlers', () => {
26
27
  // ============================================================================
27
28
 
28
29
  describe('getCostSummary', () => {
29
- it('should return error when project_id is missing', async () => {
30
+ it('should throw ValidationError when project_id is missing', async () => {
30
31
  const ctx = createMockContext();
31
32
 
32
- const result = await getCostSummary({}, ctx);
33
-
34
- expect(result.isError).toBe(true);
35
- expect(result.result).toEqual({ error: 'project_id is required' });
33
+ await expect(getCostSummary({}, ctx)).rejects.toThrow(ValidationError);
34
+ await expect(getCostSummary({}, ctx)).rejects.toThrow('Missing required field: project_id');
36
35
  });
37
36
 
38
37
  it('should return daily cost summary with totals', async () => {
@@ -185,40 +184,37 @@ describe('Cost Handlers', () => {
185
184
  // ============================================================================
186
185
 
187
186
  describe('addCostAlert', () => {
188
- it('should return error when threshold_amount is missing', async () => {
187
+ it('should throw ValidationError when threshold_amount is missing', async () => {
189
188
  const ctx = createMockContext();
190
189
 
191
- const result = await addCostAlert(
192
- { threshold_period: 'daily' },
193
- ctx
194
- );
195
-
196
- expect(result.isError).toBe(true);
197
- expect(result.result.error).toContain('threshold_amount must be a positive number');
190
+ await expect(
191
+ addCostAlert({ threshold_period: 'daily' }, ctx)
192
+ ).rejects.toThrow(ValidationError);
193
+ await expect(
194
+ addCostAlert({ threshold_period: 'daily' }, ctx)
195
+ ).rejects.toThrow('Missing required field: threshold_amount');
198
196
  });
199
197
 
200
- it('should return error when threshold_amount is not positive', async () => {
198
+ it('should throw ValidationError when threshold_amount is not positive', async () => {
201
199
  const ctx = createMockContext();
202
200
 
203
- const result = await addCostAlert(
204
- { threshold_amount: -5, threshold_period: 'daily' },
205
- ctx
206
- );
207
-
208
- expect(result.isError).toBe(true);
209
- expect(result.result.error).toContain('threshold_amount must be a positive number');
201
+ await expect(
202
+ addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
203
+ ).rejects.toThrow(ValidationError);
204
+ await expect(
205
+ addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
206
+ ).rejects.toThrow('threshold_amount must be a positive number');
210
207
  });
211
208
 
212
- it('should return error when threshold_period is invalid', async () => {
209
+ it('should throw ValidationError when threshold_period is invalid', async () => {
213
210
  const ctx = createMockContext();
214
211
 
215
- const result = await addCostAlert(
216
- { threshold_amount: 10, threshold_period: 'yearly' },
217
- ctx
218
- );
219
-
220
- expect(result.isError).toBe(true);
221
- expect(result.result.error).toContain('threshold_period must be');
212
+ await expect(
213
+ addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
214
+ ).rejects.toThrow(ValidationError);
215
+ await expect(
216
+ addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
217
+ ).rejects.toThrow('Invalid threshold_period');
222
218
  });
223
219
 
224
220
  it('should create alert successfully', async () => {
@@ -291,13 +287,11 @@ describe('Cost Handlers', () => {
291
287
  // ============================================================================
292
288
 
293
289
  describe('updateCostAlert', () => {
294
- it('should return error when alert_id is missing', async () => {
290
+ it('should throw ValidationError when alert_id is missing', async () => {
295
291
  const ctx = createMockContext();
296
292
 
297
- const result = await updateCostAlert({}, ctx);
298
-
299
- expect(result.isError).toBe(true);
300
- expect(result.result.error).toBe('alert_id is required');
293
+ await expect(updateCostAlert({}, ctx)).rejects.toThrow(ValidationError);
294
+ await expect(updateCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
301
295
  });
302
296
 
303
297
  it('should return error when no updates provided', async () => {
@@ -367,13 +361,11 @@ describe('Cost Handlers', () => {
367
361
  // ============================================================================
368
362
 
369
363
  describe('deleteCostAlert', () => {
370
- it('should return error when alert_id is missing', async () => {
364
+ it('should throw ValidationError when alert_id is missing', async () => {
371
365
  const ctx = createMockContext();
372
366
 
373
- const result = await deleteCostAlert({}, ctx);
374
-
375
- expect(result.isError).toBe(true);
376
- expect(result.result.error).toBe('alert_id is required');
367
+ await expect(deleteCostAlert({}, ctx)).rejects.toThrow(ValidationError);
368
+ await expect(deleteCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
377
369
  });
378
370
 
379
371
  it('should delete alert successfully', async () => {
@@ -408,13 +400,11 @@ describe('Cost Handlers', () => {
408
400
  // ============================================================================
409
401
 
410
402
  describe('getTaskCosts', () => {
411
- it('should return error when project_id is missing', async () => {
403
+ it('should throw ValidationError when project_id is missing', async () => {
412
404
  const ctx = createMockContext();
413
405
 
414
- const result = await getTaskCosts({}, ctx);
415
-
416
- expect(result.isError).toBe(true);
417
- expect(result.result.error).toBe('project_id is required');
406
+ await expect(getTaskCosts({}, ctx)).rejects.toThrow(ValidationError);
407
+ await expect(getTaskCosts({}, ctx)).rejects.toThrow('Missing required field: project_id');
418
408
  });
419
409
 
420
410
  it('should return task costs with total', async () => {
@@ -11,27 +11,68 @@
11
11
  */
12
12
 
13
13
  import type { Handler, HandlerRegistry } from './types.js';
14
+ import { parseArgs, uuidValidator, createEnumValidator, ValidationError } from '../validators.js';
14
15
  import { getApiClient } from '../api-client.js';
15
16
 
17
+ const VALID_PERIODS = ['daily', 'weekly', 'monthly'] as const;
18
+ const VALID_ALERT_TYPES = ['warning', 'critical'] as const;
19
+
20
+ type Period = typeof VALID_PERIODS[number];
21
+ type AlertType = typeof VALID_ALERT_TYPES[number];
22
+
23
+ // Argument schemas for type-safe parsing
24
+ const getCostSummarySchema = {
25
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
26
+ period: { type: 'string' as const, default: 'daily', validate: createEnumValidator(VALID_PERIODS) },
27
+ limit: { type: 'number' as const, default: 30 },
28
+ };
29
+
30
+ const getCostAlertsSchema = {
31
+ project_id: { type: 'string' as const, validate: uuidValidator },
32
+ };
33
+
34
+ const addCostAlertSchema = {
35
+ project_id: { type: 'string' as const, validate: uuidValidator },
36
+ threshold_amount: { type: 'number' as const, required: true as const },
37
+ threshold_period: { type: 'string' as const, required: true as const, validate: createEnumValidator(VALID_PERIODS) },
38
+ alert_type: { type: 'string' as const, default: 'warning', validate: createEnumValidator(VALID_ALERT_TYPES) },
39
+ };
40
+
41
+ const updateCostAlertSchema = {
42
+ alert_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
43
+ threshold_amount: { type: 'number' as const },
44
+ threshold_period: { type: 'string' as const, validate: createEnumValidator(VALID_PERIODS) },
45
+ alert_type: { type: 'string' as const, validate: createEnumValidator(VALID_ALERT_TYPES) },
46
+ enabled: { type: 'boolean' as const },
47
+ };
48
+
49
+ const deleteCostAlertSchema = {
50
+ alert_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
51
+ };
52
+
53
+ const getTaskCostsSchema = {
54
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
55
+ limit: { type: 'number' as const, default: 20 },
56
+ };
57
+
58
+ // Custom validator for positive numbers
59
+ function validatePositiveNumber(value: number | undefined, fieldName: string): void {
60
+ if (value !== undefined && value <= 0) {
61
+ throw new ValidationError(`${fieldName} must be a positive number`, { field: fieldName });
62
+ }
63
+ }
64
+
16
65
  /**
17
66
  * Get cost summary for a project (daily, weekly, or monthly)
18
67
  */
19
- export const getCostSummary: Handler = async (args, ctx) => {
20
- const { project_id, period = 'daily', limit = 30 } = args as {
21
- project_id: string;
22
- period?: 'daily' | 'weekly' | 'monthly';
23
- limit?: number;
24
- };
25
-
26
- if (!project_id) {
27
- return {
28
- result: { error: 'project_id is required' },
29
- isError: true,
30
- };
31
- }
68
+ export const getCostSummary: Handler = async (args, _ctx) => {
69
+ const { project_id, period, limit } = parseArgs(args, getCostSummarySchema);
32
70
 
33
71
  const apiClient = getApiClient();
34
- const response = await apiClient.getCostSummary(project_id, { period, limit });
72
+ const response = await apiClient.getCostSummary(project_id, {
73
+ period: period as Period,
74
+ limit
75
+ });
35
76
 
36
77
  if (!response.ok) {
37
78
  return {
@@ -46,8 +87,8 @@ export const getCostSummary: Handler = async (args, ctx) => {
46
87
  /**
47
88
  * Get cost alerts for the current user
48
89
  */
49
- export const getCostAlerts: Handler = async (args, ctx) => {
50
- const { project_id } = args as { project_id?: string };
90
+ export const getCostAlerts: Handler = async (args, _ctx) => {
91
+ const { project_id } = parseArgs(args, getCostAlertsSchema);
51
92
 
52
93
  const apiClient = getApiClient();
53
94
  const response = await apiClient.getCostAlerts();
@@ -65,39 +106,18 @@ export const getCostAlerts: Handler = async (args, ctx) => {
65
106
  /**
66
107
  * Add a cost alert
67
108
  */
68
- export const addCostAlert: Handler = async (args, ctx) => {
69
- const {
70
- project_id,
71
- threshold_amount,
72
- threshold_period,
73
- alert_type = 'warning',
74
- } = args as {
75
- project_id?: string;
76
- threshold_amount: number;
77
- threshold_period: 'daily' | 'weekly' | 'monthly';
78
- alert_type?: 'warning' | 'critical';
79
- };
80
-
81
- if (!threshold_amount || threshold_amount <= 0) {
82
- return {
83
- result: { error: 'threshold_amount must be a positive number' },
84
- isError: true,
85
- };
86
- }
109
+ export const addCostAlert: Handler = async (args, _ctx) => {
110
+ const { project_id, threshold_amount, threshold_period, alert_type } = parseArgs(args, addCostAlertSchema);
87
111
 
88
- if (!threshold_period || !['daily', 'weekly', 'monthly'].includes(threshold_period)) {
89
- return {
90
- result: { error: 'threshold_period must be "daily", "weekly", or "monthly"' },
91
- isError: true,
92
- };
93
- }
112
+ // Additional validation for positive amount
113
+ validatePositiveNumber(threshold_amount, 'threshold_amount');
94
114
 
95
115
  const apiClient = getApiClient();
96
116
  const response = await apiClient.addCostAlert({
97
117
  project_id,
98
- threshold_amount,
99
- threshold_period,
100
- alert_type
118
+ threshold_amount: threshold_amount!,
119
+ threshold_period: threshold_period as Period,
120
+ alert_type: alert_type as AlertType
101
121
  });
102
122
 
103
123
  if (!response.ok) {
@@ -113,46 +133,28 @@ export const addCostAlert: Handler = async (args, ctx) => {
113
133
  /**
114
134
  * Update a cost alert
115
135
  */
116
- export const updateCostAlert: Handler = async (args, ctx) => {
117
- const {
118
- alert_id,
119
- threshold_amount,
120
- threshold_period,
121
- alert_type,
122
- enabled,
123
- } = args as {
124
- alert_id: string;
125
- threshold_amount?: number;
126
- threshold_period?: 'daily' | 'weekly' | 'monthly';
127
- alert_type?: 'warning' | 'critical';
128
- enabled?: boolean;
129
- };
136
+ export const updateCostAlert: Handler = async (args, _ctx) => {
137
+ const { alert_id, threshold_amount, threshold_period, alert_type, enabled } = parseArgs(args, updateCostAlertSchema);
130
138
 
131
- if (!alert_id) {
139
+ // Check that at least one update is provided
140
+ if (threshold_amount === undefined && threshold_period === undefined && alert_type === undefined && enabled === undefined) {
132
141
  return {
133
- result: { error: 'alert_id is required' },
142
+ result: { error: 'No updates provided' },
134
143
  isError: true,
135
144
  };
136
145
  }
137
146
 
138
147
  const updates: {
139
148
  threshold_amount?: number;
140
- threshold_period?: 'daily' | 'weekly' | 'monthly';
141
- alert_type?: 'warning' | 'critical';
149
+ threshold_period?: Period;
150
+ alert_type?: AlertType;
142
151
  enabled?: boolean;
143
152
  } = {};
144
153
  if (threshold_amount !== undefined) updates.threshold_amount = threshold_amount;
145
- if (threshold_period !== undefined) updates.threshold_period = threshold_period;
146
- if (alert_type !== undefined) updates.alert_type = alert_type;
154
+ if (threshold_period !== undefined) updates.threshold_period = threshold_period as Period;
155
+ if (alert_type !== undefined) updates.alert_type = alert_type as AlertType;
147
156
  if (enabled !== undefined) updates.enabled = enabled;
148
157
 
149
- if (Object.keys(updates).length === 0) {
150
- return {
151
- result: { error: 'No updates provided' },
152
- isError: true,
153
- };
154
- }
155
-
156
158
  const apiClient = getApiClient();
157
159
  const response = await apiClient.updateCostAlert(alert_id, updates);
158
160
 
@@ -169,15 +171,8 @@ export const updateCostAlert: Handler = async (args, ctx) => {
169
171
  /**
170
172
  * Delete a cost alert
171
173
  */
172
- export const deleteCostAlert: Handler = async (args, ctx) => {
173
- const { alert_id } = args as { alert_id: string };
174
-
175
- if (!alert_id) {
176
- return {
177
- result: { error: 'alert_id is required' },
178
- isError: true,
179
- };
180
- }
174
+ export const deleteCostAlert: Handler = async (args, _ctx) => {
175
+ const { alert_id } = parseArgs(args, deleteCostAlertSchema);
181
176
 
182
177
  const apiClient = getApiClient();
183
178
  const response = await apiClient.deleteCostAlert(alert_id);
@@ -195,18 +190,8 @@ export const deleteCostAlert: Handler = async (args, ctx) => {
195
190
  /**
196
191
  * Get task costs for a project
197
192
  */
198
- export const getTaskCosts: Handler = async (args, ctx) => {
199
- const { project_id, limit = 20 } = args as {
200
- project_id: string;
201
- limit?: number;
202
- };
203
-
204
- if (!project_id) {
205
- return {
206
- result: { error: 'project_id is required' },
207
- isError: true,
208
- };
209
- }
193
+ export const getTaskCosts: Handler = async (args, _ctx) => {
194
+ const { project_id, limit } = parseArgs(args, getTaskCostsSchema);
210
195
 
211
196
  const apiClient = getApiClient();
212
197
  const response = await apiClient.getTaskCosts(project_id, limit);