@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
@@ -18,40 +18,103 @@
18
18
  */
19
19
 
20
20
  import type { Handler, HandlerRegistry } from './types.js';
21
- import { validateRequired, validateUUID } from '../validators.js';
21
+ import { parseArgs, uuidValidator, createEnumValidator, ValidationError } from '../validators.js';
22
22
  import { getApiClient } from '../api-client.js';
23
23
 
24
24
  // Valid roles in order of permission level
25
25
  const ROLE_ORDER = ['viewer', 'member', 'admin', 'owner'] as const;
26
+ const ASSIGNABLE_ROLES = ['viewer', 'member', 'admin'] as const;
26
27
  type Role = (typeof ROLE_ORDER)[number];
28
+ type AssignableRole = (typeof ASSIGNABLE_ROLES)[number];
27
29
 
28
30
  // Valid share permissions
29
31
  const PERMISSION_ORDER = ['read', 'write', 'admin'] as const;
30
32
  type Permission = (typeof PERMISSION_ORDER)[number];
31
33
 
34
+ // ============================================================================
35
+ // Argument Schemas
36
+ // ============================================================================
37
+
38
+ const createOrganizationSchema = {
39
+ name: { type: 'string' as const, required: true as const },
40
+ description: { type: 'string' as const },
41
+ slug: { type: 'string' as const },
42
+ };
43
+
44
+ const updateOrganizationSchema = {
45
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
46
+ name: { type: 'string' as const },
47
+ description: { type: 'string' as const },
48
+ logo_url: { type: 'string' as const },
49
+ };
50
+
51
+ const deleteOrganizationSchema = {
52
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
53
+ };
54
+
55
+ const listOrgMembersSchema = {
56
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
57
+ };
58
+
59
+ const inviteMemberSchema = {
60
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
61
+ email: { type: 'string' as const, required: true as const },
62
+ role: { type: 'string' as const, default: 'member', validate: createEnumValidator(ASSIGNABLE_ROLES) },
63
+ };
64
+
65
+ const updateMemberRoleSchema = {
66
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
67
+ user_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
68
+ role: { type: 'string' as const, required: true as const, validate: createEnumValidator(ROLE_ORDER) },
69
+ };
70
+
71
+ const removeMemberSchema = {
72
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
73
+ user_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
74
+ };
75
+
76
+ const leaveOrganizationSchema = {
77
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
78
+ };
79
+
80
+ const shareProjectWithOrgSchema = {
81
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
82
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
83
+ permission: { type: 'string' as const, default: 'read', validate: createEnumValidator(PERMISSION_ORDER) },
84
+ };
85
+
86
+ const updateProjectShareSchema = {
87
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
88
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
89
+ permission: { type: 'string' as const, required: true as const, validate: createEnumValidator(PERMISSION_ORDER) },
90
+ };
91
+
92
+ const unshareProjectSchema = {
93
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
94
+ organization_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
95
+ };
96
+
97
+ const listProjectSharesSchema = {
98
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
99
+ };
100
+
32
101
  // ============================================================================
33
102
  // Organization Management
34
103
  // ============================================================================
35
104
 
36
- export const listOrganizations: Handler = async (_args, ctx) => {
105
+ export const listOrganizations: Handler = async (_args, _ctx) => {
37
106
  const apiClient = getApiClient();
38
107
  const response = await apiClient.listOrganizations();
39
108
 
40
109
  if (!response.ok) {
41
- throw new Error(response.error || 'Failed to list organizations');
110
+ return { result: { error: response.error || 'Failed to list organizations' }, isError: true };
42
111
  }
43
112
 
44
113
  return { result: response.data };
45
114
  };
46
115
 
47
- export const createOrganization: Handler = async (args, ctx) => {
48
- const { name, description, slug } = args as {
49
- name: string;
50
- description?: string;
51
- slug?: string;
52
- };
53
-
54
- validateRequired(name, 'name');
116
+ export const createOrganization: Handler = async (args, _ctx) => {
117
+ const { name, description, slug } = parseArgs(args, createOrganizationSchema);
55
118
 
56
119
  const apiClient = getApiClient();
57
120
  const response = await apiClient.createOrganization({
@@ -61,53 +124,43 @@ export const createOrganization: Handler = async (args, ctx) => {
61
124
  });
62
125
 
63
126
  if (!response.ok) {
64
- throw new Error(response.error || 'Failed to create organization');
127
+ return { result: { error: response.error || 'Failed to create organization' }, isError: true };
65
128
  }
66
129
 
67
130
  return { result: response.data };
68
131
  };
69
132
 
70
- export const updateOrganization: Handler = async (args, ctx) => {
71
- const { organization_id, name, description, logo_url } = args as {
72
- organization_id: string;
73
- name?: string;
74
- description?: string;
75
- logo_url?: string;
76
- };
133
+ export const updateOrganization: Handler = async (args, _ctx) => {
134
+ const { organization_id, name, description, logo_url } = parseArgs(args, updateOrganizationSchema);
77
135
 
78
- validateRequired(organization_id, 'organization_id');
79
- validateUUID(organization_id, 'organization_id');
136
+ // Check that at least one update is provided
137
+ if (name === undefined && description === undefined && logo_url === undefined) {
138
+ throw new ValidationError('No updates provided');
139
+ }
80
140
 
81
141
  const updates: Record<string, unknown> = {};
82
142
  if (name !== undefined) updates.name = name;
83
143
  if (description !== undefined) updates.description = description;
84
144
  if (logo_url !== undefined) updates.logo_url = logo_url;
85
145
 
86
- if (Object.keys(updates).length === 0) {
87
- throw new Error('No updates provided');
88
- }
89
-
90
146
  const apiClient = getApiClient();
91
147
  const response = await apiClient.updateOrganization(organization_id, updates);
92
148
 
93
149
  if (!response.ok) {
94
- throw new Error(response.error || 'Failed to update organization');
150
+ return { result: { error: response.error || 'Failed to update organization' }, isError: true };
95
151
  }
96
152
 
97
153
  return { result: response.data };
98
154
  };
99
155
 
100
- export const deleteOrganization: Handler = async (args, ctx) => {
101
- const { organization_id } = args as { organization_id: string };
102
-
103
- validateRequired(organization_id, 'organization_id');
104
- validateUUID(organization_id, 'organization_id');
156
+ export const deleteOrganization: Handler = async (args, _ctx) => {
157
+ const { organization_id } = parseArgs(args, deleteOrganizationSchema);
105
158
 
106
159
  const apiClient = getApiClient();
107
160
  const response = await apiClient.deleteOrganization(organization_id);
108
161
 
109
162
  if (!response.ok) {
110
- throw new Error(response.error || 'Failed to delete organization');
163
+ return { result: { error: response.error || 'Failed to delete organization' }, isError: true };
111
164
  }
112
165
 
113
166
  return { result: response.data };
@@ -117,110 +170,70 @@ export const deleteOrganization: Handler = async (args, ctx) => {
117
170
  // Member Management
118
171
  // ============================================================================
119
172
 
120
- export const listOrgMembers: Handler = async (args, ctx) => {
121
- const { organization_id } = args as { organization_id: string };
122
-
123
- validateRequired(organization_id, 'organization_id');
124
- validateUUID(organization_id, 'organization_id');
173
+ export const listOrgMembers: Handler = async (args, _ctx) => {
174
+ const { organization_id } = parseArgs(args, listOrgMembersSchema);
125
175
 
126
176
  const apiClient = getApiClient();
127
177
  const response = await apiClient.listOrgMembers(organization_id);
128
178
 
129
179
  if (!response.ok) {
130
- throw new Error(response.error || 'Failed to list members');
180
+ return { result: { error: response.error || 'Failed to list members' }, isError: true };
131
181
  }
132
182
 
133
183
  return { result: response.data };
134
184
  };
135
185
 
136
- export const inviteMember: Handler = async (args, ctx) => {
137
- const { organization_id, email, role = 'member' } = args as {
138
- organization_id: string;
139
- email: string;
140
- role?: 'admin' | 'member' | 'viewer';
141
- };
142
-
143
- validateRequired(organization_id, 'organization_id');
144
- validateRequired(email, 'email');
145
- validateUUID(organization_id, 'organization_id');
146
-
147
- if (!['admin', 'member', 'viewer'].includes(role)) {
148
- throw new Error('Invalid role. Must be admin, member, or viewer.');
149
- }
186
+ export const inviteMember: Handler = async (args, _ctx) => {
187
+ const { organization_id, email, role } = parseArgs(args, inviteMemberSchema);
150
188
 
151
189
  const apiClient = getApiClient();
152
- const response = await apiClient.inviteMember(organization_id, email, role);
190
+ const response = await apiClient.inviteMember(organization_id, email, role as AssignableRole);
153
191
 
154
192
  if (!response.ok) {
155
- throw new Error(response.error || 'Failed to create invite');
193
+ return { result: { error: response.error || 'Failed to create invite' }, isError: true };
156
194
  }
157
195
 
158
196
  return { result: response.data };
159
197
  };
160
198
 
161
- export const updateMemberRole: Handler = async (args, ctx) => {
162
- const { organization_id, user_id, role } = args as {
163
- organization_id: string;
164
- user_id: string;
165
- role: Role;
166
- };
167
-
168
- validateRequired(organization_id, 'organization_id');
169
- validateRequired(user_id, 'user_id');
170
- validateRequired(role, 'role');
171
- validateUUID(organization_id, 'organization_id');
172
- validateUUID(user_id, 'user_id');
173
-
174
- if (!ROLE_ORDER.includes(role)) {
175
- throw new Error(`Invalid role. Must be one of: ${ROLE_ORDER.join(', ')}`);
176
- }
199
+ export const updateMemberRole: Handler = async (args, _ctx) => {
200
+ const { organization_id, user_id, role } = parseArgs(args, updateMemberRoleSchema);
177
201
 
178
202
  if (role === 'owner') {
179
- throw new Error('Cannot assign owner role. Use transfer ownership instead.');
203
+ throw new ValidationError('Cannot assign owner role. Use transfer ownership instead.');
180
204
  }
181
205
 
182
206
  const apiClient = getApiClient();
183
- const response = await apiClient.updateMemberRole(organization_id, user_id, role);
207
+ const response = await apiClient.updateMemberRole(organization_id, user_id, role as Role);
184
208
 
185
209
  if (!response.ok) {
186
- throw new Error(response.error || 'Failed to update member role');
210
+ return { result: { error: response.error || 'Failed to update member role' }, isError: true };
187
211
  }
188
212
 
189
213
  return { result: response.data };
190
214
  };
191
215
 
192
- export const removeMember: Handler = async (args, ctx) => {
193
- const { organization_id, user_id } = args as {
194
- organization_id: string;
195
- user_id: string;
196
- };
197
-
198
- validateRequired(organization_id, 'organization_id');
199
- validateRequired(user_id, 'user_id');
200
- validateUUID(organization_id, 'organization_id');
201
- validateUUID(user_id, 'user_id');
216
+ export const removeMember: Handler = async (args, _ctx) => {
217
+ const { organization_id, user_id } = parseArgs(args, removeMemberSchema);
202
218
 
203
219
  const apiClient = getApiClient();
204
220
  const response = await apiClient.removeMember(organization_id, user_id);
205
221
 
206
222
  if (!response.ok) {
207
- throw new Error(response.error || 'Failed to remove member');
223
+ return { result: { error: response.error || 'Failed to remove member' }, isError: true };
208
224
  }
209
225
 
210
226
  return { result: response.data };
211
227
  };
212
228
 
213
- export const leaveOrganization: Handler = async (args, ctx) => {
214
- const { organization_id } = args as { organization_id: string };
215
-
216
- validateRequired(organization_id, 'organization_id');
217
- validateUUID(organization_id, 'organization_id');
229
+ export const leaveOrganization: Handler = async (args, _ctx) => {
230
+ const { organization_id } = parseArgs(args, leaveOrganizationSchema);
218
231
 
219
232
  const apiClient = getApiClient();
220
233
  const response = await apiClient.leaveOrganization(organization_id);
221
234
 
222
235
  if (!response.ok) {
223
- throw new Error(response.error || 'Failed to leave organization');
236
+ return { result: { error: response.error || 'Failed to leave organization' }, isError: true };
224
237
  }
225
238
 
226
239
  return { result: response.data };
@@ -230,91 +243,53 @@ export const leaveOrganization: Handler = async (args, ctx) => {
230
243
  // Project Sharing
231
244
  // ============================================================================
232
245
 
233
- export const shareProjectWithOrg: Handler = async (args, ctx) => {
234
- const { project_id, organization_id, permission = 'read' } = args as {
235
- project_id: string;
236
- organization_id: string;
237
- permission?: Permission;
238
- };
239
-
240
- validateRequired(project_id, 'project_id');
241
- validateRequired(organization_id, 'organization_id');
242
- validateUUID(project_id, 'project_id');
243
- validateUUID(organization_id, 'organization_id');
244
-
245
- if (!PERMISSION_ORDER.includes(permission)) {
246
- throw new Error(`Invalid permission. Must be one of: ${PERMISSION_ORDER.join(', ')}`);
247
- }
246
+ export const shareProjectWithOrg: Handler = async (args, _ctx) => {
247
+ const { project_id, organization_id, permission } = parseArgs(args, shareProjectWithOrgSchema);
248
248
 
249
249
  const apiClient = getApiClient();
250
- const response = await apiClient.shareProjectWithOrg(project_id, organization_id, permission);
250
+ const response = await apiClient.shareProjectWithOrg(project_id, organization_id, permission as Permission);
251
251
 
252
252
  if (!response.ok) {
253
- throw new Error(response.error || 'Failed to share project');
253
+ return { result: { error: response.error || 'Failed to share project' }, isError: true };
254
254
  }
255
255
 
256
256
  return { result: response.data };
257
257
  };
258
258
 
259
- export const updateProjectShare: Handler = async (args, ctx) => {
260
- const { project_id, organization_id, permission } = args as {
261
- project_id: string;
262
- organization_id: string;
263
- permission: Permission;
264
- };
265
-
266
- validateRequired(project_id, 'project_id');
267
- validateRequired(organization_id, 'organization_id');
268
- validateRequired(permission, 'permission');
269
- validateUUID(project_id, 'project_id');
270
- validateUUID(organization_id, 'organization_id');
271
-
272
- if (!PERMISSION_ORDER.includes(permission)) {
273
- throw new Error(`Invalid permission. Must be one of: ${PERMISSION_ORDER.join(', ')}`);
274
- }
259
+ export const updateProjectShare: Handler = async (args, _ctx) => {
260
+ const { project_id, organization_id, permission } = parseArgs(args, updateProjectShareSchema);
275
261
 
276
262
  const apiClient = getApiClient();
277
- const response = await apiClient.updateProjectShare(project_id, organization_id, permission);
263
+ const response = await apiClient.updateProjectShare(project_id, organization_id, permission as Permission);
278
264
 
279
265
  if (!response.ok) {
280
- throw new Error(response.error || 'Failed to update share');
266
+ return { result: { error: response.error || 'Failed to update share' }, isError: true };
281
267
  }
282
268
 
283
269
  return { result: response.data };
284
270
  };
285
271
 
286
- export const unshareProject: Handler = async (args, ctx) => {
287
- const { project_id, organization_id } = args as {
288
- project_id: string;
289
- organization_id: string;
290
- };
291
-
292
- validateRequired(project_id, 'project_id');
293
- validateRequired(organization_id, 'organization_id');
294
- validateUUID(project_id, 'project_id');
295
- validateUUID(organization_id, 'organization_id');
272
+ export const unshareProject: Handler = async (args, _ctx) => {
273
+ const { project_id, organization_id } = parseArgs(args, unshareProjectSchema);
296
274
 
297
275
  const apiClient = getApiClient();
298
276
  const response = await apiClient.unshareProject(project_id, organization_id);
299
277
 
300
278
  if (!response.ok) {
301
- throw new Error(response.error || 'Failed to unshare project');
279
+ return { result: { error: response.error || 'Failed to unshare project' }, isError: true };
302
280
  }
303
281
 
304
282
  return { result: response.data };
305
283
  };
306
284
 
307
- export const listProjectShares: Handler = async (args, ctx) => {
308
- const { project_id } = args as { project_id: string };
309
-
310
- validateRequired(project_id, 'project_id');
311
- validateUUID(project_id, 'project_id');
285
+ export const listProjectShares: Handler = async (args, _ctx) => {
286
+ const { project_id } = parseArgs(args, listProjectSharesSchema);
312
287
 
313
288
  const apiClient = getApiClient();
314
289
  const response = await apiClient.listProjectShares(project_id);
315
290
 
316
291
  if (!response.ok) {
317
- throw new Error(response.error || 'Failed to list shares');
292
+ return { result: { error: response.error || 'Failed to list shares' }, isError: true };
318
293
  }
319
294
 
320
295
  return { result: response.data };
@@ -103,19 +103,22 @@ describe('logProgress', () => {
103
103
  expect(mockApiClient.logProgress).toHaveBeenCalled();
104
104
  });
105
105
 
106
- it('should throw error when API call fails', async () => {
106
+ it('should return error when API call fails', async () => {
107
107
  mockApiClient.logProgress.mockResolvedValue({
108
108
  ok: false,
109
109
  error: 'Insert failed',
110
110
  });
111
111
  const ctx = createMockContext();
112
112
 
113
- await expect(
114
- logProgress({
115
- project_id: '123e4567-e89b-12d3-a456-426614174000',
116
- summary: 'Progress',
117
- }, ctx)
118
- ).rejects.toThrow('Failed to log progress: Insert failed');
113
+ const result = await logProgress({
114
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
115
+ summary: 'Progress',
116
+ }, ctx);
117
+
118
+ expect(result.isError).toBe(true);
119
+ expect(result.result).toMatchObject({
120
+ error: 'Insert failed',
121
+ });
119
122
  });
120
123
 
121
124
  it('should throw error for missing project_id', async () => {
@@ -246,18 +249,21 @@ describe('getActivityFeed', () => {
246
249
  );
247
250
  });
248
251
 
249
- it('should throw error when API call fails', async () => {
252
+ it('should return error when API call fails', async () => {
250
253
  mockApiClient.getActivityFeed.mockResolvedValue({
251
254
  ok: false,
252
255
  error: 'Query failed',
253
256
  });
254
257
  const ctx = createMockContext();
255
258
 
256
- await expect(
257
- getActivityFeed(
258
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
259
- ctx
260
- )
261
- ).rejects.toThrow('Failed to fetch activity feed: Query failed');
259
+ const result = await getActivityFeed(
260
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
261
+ ctx
262
+ );
263
+
264
+ expect(result.isError).toBe(true);
265
+ expect(result.result).toMatchObject({
266
+ error: 'Query failed',
267
+ });
262
268
  });
263
269
  });
@@ -9,20 +9,27 @@
9
9
  */
10
10
 
11
11
  import type { Handler, HandlerRegistry } from './types.js';
12
- import { validateRequired, validateUUID } from '../validators.js';
12
+ import { parseArgs, uuidValidator } from '../validators.js';
13
13
  import { getApiClient } from '../api-client.js';
14
14
 
15
- export const logProgress: Handler = async (args, ctx) => {
16
- const { project_id, task_id, summary, details } = args as {
17
- project_id: string;
18
- task_id?: string;
19
- summary: string;
20
- details?: string;
21
- };
15
+ // Argument schemas for type-safe parsing
16
+ const logProgressSchema = {
17
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
18
+ task_id: { type: 'string' as const, validate: uuidValidator },
19
+ summary: { type: 'string' as const, required: true as const },
20
+ details: { type: 'string' as const },
21
+ };
22
+
23
+ const getActivityFeedSchema = {
24
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
25
+ limit: { type: 'number' as const, default: 50 },
26
+ since: { type: 'string' as const },
27
+ types: { type: 'array' as const },
28
+ created_by: { type: 'string' as const },
29
+ };
22
30
 
23
- validateRequired(project_id, 'project_id');
24
- validateUUID(project_id, 'project_id');
25
- validateRequired(summary, 'summary');
31
+ export const logProgress: Handler = async (args, ctx) => {
32
+ const { project_id, task_id, summary, details } = parseArgs(args, logProgressSchema);
26
33
 
27
34
  const { session } = ctx;
28
35
  const apiClient = getApiClient();
@@ -35,32 +42,27 @@ export const logProgress: Handler = async (args, ctx) => {
35
42
  });
36
43
 
37
44
  if (!response.ok) {
38
- throw new Error(`Failed to log progress: ${response.error}`);
45
+ return { result: { error: response.error || 'Failed to log progress' }, isError: true };
39
46
  }
40
47
 
41
48
  return { result: { success: true, progress_id: response.data?.progress_id } };
42
49
  };
43
50
 
44
- export const getActivityFeed: Handler = async (args, ctx) => {
45
- const { project_id, limit = 50, since } = args as {
46
- project_id: string;
47
- limit?: number;
48
- since?: string;
49
- };
50
-
51
- validateRequired(project_id, 'project_id');
52
- validateUUID(project_id, 'project_id');
51
+ export const getActivityFeed: Handler = async (args, _ctx) => {
52
+ const { project_id, limit, since, types, created_by } = parseArgs(args, getActivityFeedSchema);
53
53
 
54
54
  const apiClient = getApiClient();
55
- const effectiveLimit = Math.min(limit, 200);
55
+ const effectiveLimit = Math.min(limit ?? 50, 200);
56
56
 
57
57
  const response = await apiClient.getActivityFeed(project_id, {
58
58
  limit: effectiveLimit,
59
- since
59
+ since,
60
+ types: types as string[] | undefined,
61
+ created_by
60
62
  });
61
63
 
62
64
  if (!response.ok) {
63
- throw new Error(`Failed to fetch activity feed: ${response.error}`);
65
+ return { result: { error: response.error || 'Failed to fetch activity feed' }, isError: true };
64
66
  }
65
67
 
66
68
  return { result: { activities: response.data?.activities || [] } };
@@ -120,14 +120,17 @@ describe('getProjectContext', () => {
120
120
  expect(result.result).toHaveProperty('active_tasks');
121
121
  });
122
122
 
123
- it('should throw error when API call fails', async () => {
123
+ it('should return error when API call fails', async () => {
124
124
  mockApiClient.listProjects.mockResolvedValue({
125
125
  ok: false,
126
126
  error: 'Database error',
127
127
  });
128
128
  const ctx = createMockContext();
129
129
 
130
- await expect(getProjectContext({}, ctx)).rejects.toThrow('Database error');
130
+ const result = await getProjectContext({}, ctx);
131
+
132
+ expect(result.isError).toBe(true);
133
+ expect(result.result).toMatchObject({ error: 'Database error' });
131
134
  });
132
135
  });
133
136
 
@@ -237,16 +240,17 @@ describe('getGitWorkflow', () => {
237
240
  expect((result.result as { instructions: string[] }).instructions[0]).toContain('No git workflow configured');
238
241
  });
239
242
 
240
- it('should throw error when project not found', async () => {
243
+ it('should return error when project not found', async () => {
241
244
  mockApiClient.getGitWorkflow.mockResolvedValue({
242
245
  ok: false,
243
246
  error: 'Project not found',
244
247
  });
245
248
  const ctx = createMockContext();
246
249
 
247
- await expect(
248
- getGitWorkflow({ project_id: VALID_UUID }, ctx)
249
- ).rejects.toThrow('Project not found');
250
+ const result = await getGitWorkflow({ project_id: VALID_UUID }, ctx);
251
+
252
+ expect(result.isError).toBe(true);
253
+ expect(result.result).toMatchObject({ error: 'Project not found' });
250
254
  });
251
255
  });
252
256
 
@@ -327,16 +331,17 @@ describe('createProject', () => {
327
331
  });
328
332
  });
329
333
 
330
- it('should throw error when insert fails', async () => {
334
+ it('should return error when insert fails', async () => {
331
335
  mockApiClient.createProject.mockResolvedValue({
332
336
  ok: false,
333
- error: 'Failed to create project: Insert failed',
337
+ error: 'Failed to create project',
334
338
  });
335
339
  const ctx = createMockContext();
336
340
 
337
- await expect(
338
- createProject({ name: 'Test Project' }, ctx)
339
- ).rejects.toThrow('Failed to create project: Insert failed');
341
+ const result = await createProject({ name: 'Test Project' }, ctx);
342
+
343
+ expect(result.isError).toBe(true);
344
+ expect(result.result).toMatchObject({ error: 'Failed to create project' });
340
345
  });
341
346
 
342
347
  it('should throw error when name is missing', async () => {
@@ -445,19 +450,20 @@ describe('updateProject', () => {
445
450
  );
446
451
  });
447
452
 
448
- it('should throw error when update fails', async () => {
453
+ it('should return error when update fails', async () => {
449
454
  mockApiClient.updateProject.mockResolvedValue({
450
455
  ok: false,
451
- error: 'Failed to update project: Update failed',
456
+ error: 'Failed to update project',
452
457
  });
453
458
  const ctx = createMockContext();
454
459
 
455
- await expect(
456
- updateProject({
457
- project_id: VALID_UUID,
458
- name: 'Test',
459
- }, ctx)
460
- ).rejects.toThrow('Failed to update project: Update failed');
460
+ const result = await updateProject({
461
+ project_id: VALID_UUID,
462
+ name: 'Test',
463
+ }, ctx);
464
+
465
+ expect(result.isError).toBe(true);
466
+ expect(result.result).toMatchObject({ error: 'Failed to update project' });
461
467
  });
462
468
  });
463
469
 
@@ -522,18 +528,19 @@ describe('updateProjectReadme', () => {
522
528
  );
523
529
  });
524
530
 
525
- it('should throw error when update fails', async () => {
531
+ it('should return error when update fails', async () => {
526
532
  mockApiClient.updateProjectReadme.mockResolvedValue({
527
533
  ok: false,
528
- error: 'Failed to update README: Update failed',
534
+ error: 'Failed to update README',
529
535
  });
530
536
  const ctx = createMockContext();
531
537
 
532
- await expect(
533
- updateProjectReadme({
534
- project_id: VALID_UUID,
535
- readme_content: '# README',
536
- }, ctx)
537
- ).rejects.toThrow('Failed to update README: Update failed');
538
+ const result = await updateProjectReadme({
539
+ project_id: VALID_UUID,
540
+ readme_content: '# README',
541
+ }, ctx);
542
+
543
+ expect(result.isError).toBe(true);
544
+ expect(result.result).toMatchObject({ error: 'Failed to update README' });
538
545
  });
539
546
  });