@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
@@ -0,0 +1,303 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ getRoleSettings,
4
+ updateRoleSettings,
5
+ setSessionRole,
6
+ getAgentsByRole,
7
+ } from './roles.js';
8
+ import { createMockContext } from './__test-utils__.js';
9
+ import { mockApiClient } from './__test-setup__.js';
10
+
11
+ // ============================================================================
12
+ // getRoleSettings Tests
13
+ // ============================================================================
14
+
15
+ describe('getRoleSettings', () => {
16
+ beforeEach(() => vi.clearAllMocks());
17
+
18
+ it('should return error for missing project_id', async () => {
19
+ const ctx = createMockContext();
20
+
21
+ const result = await getRoleSettings({}, ctx);
22
+
23
+ expect(result.result).toMatchObject({
24
+ error: 'project_id is required',
25
+ });
26
+ });
27
+
28
+ it('should get role settings successfully', async () => {
29
+ const mockRoles = [
30
+ { role: 'developer', enabled: true, display_name: 'Developer' },
31
+ { role: 'validator', enabled: true, display_name: 'Validator' },
32
+ ];
33
+ mockApiClient.proxy.mockResolvedValue({
34
+ ok: true,
35
+ data: { roles: mockRoles },
36
+ });
37
+ const ctx = createMockContext();
38
+
39
+ const result = await getRoleSettings(
40
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
41
+ ctx
42
+ );
43
+
44
+ expect(result.result).toMatchObject({ roles: mockRoles });
45
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
46
+ 'get_role_settings',
47
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' }
48
+ );
49
+ });
50
+
51
+ it('should return error from API', async () => {
52
+ mockApiClient.proxy.mockResolvedValue({
53
+ ok: false,
54
+ error: 'Project not found',
55
+ });
56
+ const ctx = createMockContext();
57
+
58
+ const result = await getRoleSettings(
59
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
60
+ ctx
61
+ );
62
+
63
+ expect(result.result).toMatchObject({
64
+ error: 'Project not found',
65
+ });
66
+ });
67
+ });
68
+
69
+ // ============================================================================
70
+ // updateRoleSettings Tests
71
+ // ============================================================================
72
+
73
+ describe('updateRoleSettings', () => {
74
+ beforeEach(() => vi.clearAllMocks());
75
+
76
+ it('should return error for missing project_id', async () => {
77
+ const ctx = createMockContext();
78
+
79
+ const result = await updateRoleSettings({ role: 'developer' }, ctx);
80
+
81
+ expect(result.result).toMatchObject({
82
+ error: 'project_id is required',
83
+ });
84
+ });
85
+
86
+ it('should return error for missing role', async () => {
87
+ const ctx = createMockContext();
88
+
89
+ const result = await updateRoleSettings(
90
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
91
+ ctx
92
+ );
93
+
94
+ expect(result.result).toMatchObject({
95
+ error: 'role is required',
96
+ });
97
+ });
98
+
99
+ it('should update role settings successfully', async () => {
100
+ mockApiClient.proxy.mockResolvedValue({
101
+ ok: true,
102
+ data: { success: true, role: 'validator' },
103
+ });
104
+ const ctx = createMockContext();
105
+
106
+ const result = await updateRoleSettings(
107
+ {
108
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
109
+ role: 'validator',
110
+ enabled: true,
111
+ auto_assign_validation: true,
112
+ },
113
+ ctx
114
+ );
115
+
116
+ expect(result.result).toMatchObject({
117
+ success: true,
118
+ role: 'validator',
119
+ });
120
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
121
+ 'update_role_settings',
122
+ expect.objectContaining({
123
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
124
+ role: 'validator',
125
+ enabled: true,
126
+ auto_assign_validation: true,
127
+ })
128
+ );
129
+ });
130
+
131
+ it('should return error from API', async () => {
132
+ mockApiClient.proxy.mockResolvedValue({
133
+ ok: false,
134
+ error: 'Access denied',
135
+ });
136
+ const ctx = createMockContext();
137
+
138
+ const result = await updateRoleSettings(
139
+ {
140
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
141
+ role: 'validator',
142
+ },
143
+ ctx
144
+ );
145
+
146
+ expect(result.result).toMatchObject({
147
+ error: 'Access denied',
148
+ });
149
+ });
150
+ });
151
+
152
+ // ============================================================================
153
+ // setSessionRole Tests
154
+ // ============================================================================
155
+
156
+ describe('setSessionRole', () => {
157
+ beforeEach(() => vi.clearAllMocks());
158
+
159
+ it('should return error for missing role', async () => {
160
+ const ctx = createMockContext();
161
+
162
+ const result = await setSessionRole({}, ctx);
163
+
164
+ expect(result.result).toMatchObject({
165
+ error: 'role is required',
166
+ });
167
+ });
168
+
169
+ it('should return error for invalid role', async () => {
170
+ const ctx = createMockContext();
171
+
172
+ const result = await setSessionRole({ role: 'invalid_role' }, ctx);
173
+
174
+ expect(result.result).toMatchObject({
175
+ error: expect.stringContaining('Invalid role: invalid_role'),
176
+ });
177
+ });
178
+
179
+ it('should set local role when no active session', async () => {
180
+ const ctx = createMockContext({ sessionId: null });
181
+
182
+ const result = await setSessionRole({ role: 'validator' }, ctx);
183
+
184
+ expect(result.result).toMatchObject({
185
+ success: true,
186
+ role: 'validator',
187
+ message: expect.stringContaining('Local role set to validator'),
188
+ });
189
+ });
190
+
191
+ it('should update session role on server when session active', async () => {
192
+ mockApiClient.proxy.mockResolvedValue({
193
+ ok: true,
194
+ data: { success: true, session_id: 'session-123', role: 'deployer' },
195
+ });
196
+ const ctx = createMockContext({ sessionId: 'session-123' });
197
+
198
+ const result = await setSessionRole({ role: 'deployer' }, ctx);
199
+
200
+ expect(result.result).toMatchObject({
201
+ success: true,
202
+ session_id: 'session-123',
203
+ role: 'deployer',
204
+ });
205
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
206
+ 'set_session_role',
207
+ expect.objectContaining({
208
+ session_id: 'session-123',
209
+ role: 'deployer',
210
+ })
211
+ );
212
+ });
213
+
214
+ it('should update local state when setting role', async () => {
215
+ const updateSession = vi.fn();
216
+ const ctx = createMockContext({ sessionId: null });
217
+ ctx.updateSession = updateSession;
218
+
219
+ await setSessionRole({ role: 'reviewer' }, ctx);
220
+
221
+ expect(updateSession).toHaveBeenCalledWith({ currentRole: 'reviewer' });
222
+ });
223
+
224
+ it('should return error from API when updating session', async () => {
225
+ mockApiClient.proxy.mockResolvedValue({
226
+ ok: false,
227
+ error: 'Session not found',
228
+ });
229
+ const ctx = createMockContext({ sessionId: 'session-123' });
230
+
231
+ const result = await setSessionRole({ role: 'maintainer' }, ctx);
232
+
233
+ expect(result.result).toMatchObject({
234
+ error: 'Session not found',
235
+ });
236
+ });
237
+ });
238
+
239
+ // ============================================================================
240
+ // getAgentsByRole Tests
241
+ // ============================================================================
242
+
243
+ describe('getAgentsByRole', () => {
244
+ beforeEach(() => vi.clearAllMocks());
245
+
246
+ it('should return error for missing project_id', async () => {
247
+ const ctx = createMockContext();
248
+
249
+ const result = await getAgentsByRole({}, ctx);
250
+
251
+ expect(result.result).toMatchObject({
252
+ error: 'project_id is required',
253
+ });
254
+ });
255
+
256
+ it('should get agents by role successfully', async () => {
257
+ const mockAgentsByRole = {
258
+ developer: [
259
+ { session_id: 'session-1', agent_name: 'Edge', status: 'active' },
260
+ ],
261
+ validator: [],
262
+ deployer: [],
263
+ reviewer: [],
264
+ maintainer: [],
265
+ };
266
+ mockApiClient.proxy.mockResolvedValue({
267
+ ok: true,
268
+ data: { agents_by_role: mockAgentsByRole, total_active: 1 },
269
+ });
270
+ const ctx = createMockContext();
271
+
272
+ const result = await getAgentsByRole(
273
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
274
+ ctx
275
+ );
276
+
277
+ expect(result.result).toMatchObject({
278
+ agents_by_role: mockAgentsByRole,
279
+ total_active: 1,
280
+ });
281
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
282
+ 'get_agents_by_role',
283
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' }
284
+ );
285
+ });
286
+
287
+ it('should return error from API', async () => {
288
+ mockApiClient.proxy.mockResolvedValue({
289
+ ok: false,
290
+ error: 'Project not found',
291
+ });
292
+ const ctx = createMockContext();
293
+
294
+ const result = await getAgentsByRole(
295
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
296
+ ctx
297
+ );
298
+
299
+ expect(result.result).toMatchObject({
300
+ error: 'Project not found',
301
+ });
302
+ });
303
+ });
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Role Management Handlers
3
+ *
4
+ * Handles agent role configuration and assignment:
5
+ * - get_role_settings: Get role configuration for a project
6
+ * - update_role_settings: Update role settings for a project
7
+ * - set_session_role: Set the role for the current session
8
+ * - get_agents_by_role: Get active agents grouped by their assigned roles
9
+ */
10
+
11
+ import type { Handler, HandlerRegistry, AgentRole } from './types.js';
12
+ import { getApiClient } from '../api-client.js';
13
+
14
+ export const getRoleSettings: Handler = async (args, _ctx) => {
15
+ const { project_id } = args as { project_id: string };
16
+
17
+ if (!project_id) {
18
+ return {
19
+ result: { error: 'project_id is required' },
20
+ };
21
+ }
22
+
23
+ const apiClient = getApiClient();
24
+ const response = await apiClient.proxy<{
25
+ roles: Array<{
26
+ role: AgentRole;
27
+ enabled: boolean;
28
+ display_name: string | null;
29
+ description: string | null;
30
+ priority_filter: number[] | null;
31
+ fallback_activities: string[] | null;
32
+ auto_assign_validation: boolean;
33
+ auto_assign_deployment: boolean;
34
+ }>;
35
+ }>('get_role_settings', { project_id });
36
+
37
+ if (!response.ok) {
38
+ return {
39
+ result: { error: response.error || 'Failed to get role settings' },
40
+ };
41
+ }
42
+
43
+ return { result: response.data };
44
+ };
45
+
46
+ export const updateRoleSettings: Handler = async (args, _ctx) => {
47
+ const {
48
+ project_id,
49
+ role,
50
+ enabled,
51
+ display_name,
52
+ description,
53
+ priority_filter,
54
+ fallback_activities,
55
+ auto_assign_validation,
56
+ auto_assign_deployment,
57
+ } = args as {
58
+ project_id: string;
59
+ role: AgentRole;
60
+ enabled?: boolean;
61
+ display_name?: string;
62
+ description?: string;
63
+ priority_filter?: number[];
64
+ fallback_activities?: string[];
65
+ auto_assign_validation?: boolean;
66
+ auto_assign_deployment?: boolean;
67
+ };
68
+
69
+ if (!project_id) {
70
+ return {
71
+ result: { error: 'project_id is required' },
72
+ };
73
+ }
74
+
75
+ if (!role) {
76
+ return {
77
+ result: { error: 'role is required' },
78
+ };
79
+ }
80
+
81
+ const apiClient = getApiClient();
82
+ const response = await apiClient.proxy<{
83
+ success: boolean;
84
+ role: AgentRole;
85
+ }>('update_role_settings', {
86
+ project_id,
87
+ role,
88
+ enabled,
89
+ display_name,
90
+ description,
91
+ priority_filter,
92
+ fallback_activities,
93
+ auto_assign_validation,
94
+ auto_assign_deployment,
95
+ });
96
+
97
+ if (!response.ok) {
98
+ return {
99
+ result: { error: response.error || 'Failed to update role settings' },
100
+ };
101
+ }
102
+
103
+ return { result: response.data };
104
+ };
105
+
106
+ export const setSessionRole: Handler = async (args, ctx) => {
107
+ const { role, role_config } = args as {
108
+ role: AgentRole;
109
+ role_config?: Record<string, unknown>;
110
+ };
111
+ const { session, updateSession } = ctx;
112
+
113
+ if (!role) {
114
+ return {
115
+ result: { error: 'role is required' },
116
+ };
117
+ }
118
+
119
+ const validRoles: AgentRole[] = ['developer', 'validator', 'deployer', 'reviewer', 'maintainer'];
120
+ if (!validRoles.includes(role)) {
121
+ return {
122
+ result: {
123
+ error: `Invalid role: ${role}. Must be one of: ${validRoles.join(', ')}`,
124
+ },
125
+ };
126
+ }
127
+
128
+ // Update local session state
129
+ updateSession({ currentRole: role });
130
+
131
+ // If there's an active session, update it on the server too
132
+ if (session.currentSessionId) {
133
+ const apiClient = getApiClient();
134
+ const response = await apiClient.proxy<{
135
+ success: boolean;
136
+ session_id: string;
137
+ role: AgentRole;
138
+ }>('set_session_role', {
139
+ session_id: session.currentSessionId,
140
+ role,
141
+ role_config,
142
+ });
143
+
144
+ if (!response.ok) {
145
+ return {
146
+ result: { error: response.error || 'Failed to update session role' },
147
+ };
148
+ }
149
+
150
+ return {
151
+ result: {
152
+ success: true,
153
+ session_id: session.currentSessionId,
154
+ role,
155
+ message: `Session role updated to ${role}. Task filtering and fallback suggestions will now be optimized for this role.`,
156
+ },
157
+ };
158
+ }
159
+
160
+ return {
161
+ result: {
162
+ success: true,
163
+ role,
164
+ message: `Local role set to ${role}. Start a session to persist this role.`,
165
+ },
166
+ };
167
+ };
168
+
169
+ export const getAgentsByRole: Handler = async (args, _ctx) => {
170
+ const { project_id } = args as { project_id: string };
171
+
172
+ if (!project_id) {
173
+ return {
174
+ result: { error: 'project_id is required' },
175
+ };
176
+ }
177
+
178
+ const apiClient = getApiClient();
179
+ const response = await apiClient.proxy<{
180
+ agents_by_role: Record<AgentRole, Array<{
181
+ session_id: string;
182
+ agent_name: string;
183
+ status: string;
184
+ current_task_id: string | null;
185
+ current_task_title: string | null;
186
+ last_synced_at: string;
187
+ }>>;
188
+ total_active: number;
189
+ }>('get_agents_by_role', { project_id });
190
+
191
+ if (!response.ok) {
192
+ return {
193
+ result: { error: response.error || 'Failed to get agents by role' },
194
+ };
195
+ }
196
+
197
+ return { result: response.data };
198
+ };
199
+
200
+ /**
201
+ * Role handlers registry
202
+ */
203
+ export const roleHandlers: HandlerRegistry = {
204
+ get_role_settings: getRoleSettings,
205
+ update_role_settings: updateRoleSettings,
206
+ set_session_role: setSessionRole,
207
+ get_agents_by_role: getAgentsByRole,
208
+ };
@@ -10,22 +10,47 @@
10
10
  */
11
11
 
12
12
  import type { Handler, HandlerRegistry, TokenUsage } from './types.js';
13
+ import { parseArgs, createEnumValidator } from '../validators.js';
13
14
  import { getApiClient } from '../api-client.js';
14
15
 
16
+ const VALID_MODES = ['lite', 'full'] as const;
17
+ const VALID_MODELS = ['opus', 'sonnet', 'haiku'] as const;
18
+ const VALID_ROLES = ['developer', 'validator', 'deployer', 'reviewer', 'maintainer'] as const;
19
+
20
+ type SessionMode = typeof VALID_MODES[number];
21
+ type SessionModel = typeof VALID_MODELS[number];
22
+ type SessionRole = typeof VALID_ROLES[number];
23
+
24
+ // Argument schemas for type-safe parsing
25
+ const startWorkSessionSchema = {
26
+ project_id: { type: 'string' as const },
27
+ git_url: { type: 'string' as const },
28
+ mode: { type: 'string' as const, default: 'lite', validate: createEnumValidator(VALID_MODES) },
29
+ model: { type: 'string' as const, validate: createEnumValidator(VALID_MODELS) },
30
+ role: { type: 'string' as const, default: 'developer', validate: createEnumValidator(VALID_ROLES) },
31
+ };
32
+
33
+ const heartbeatSchema = {
34
+ session_id: { type: 'string' as const },
35
+ current_worktree_path: { type: 'string' as const },
36
+ };
37
+
38
+ const endWorkSessionSchema = {
39
+ session_id: { type: 'string' as const },
40
+ };
41
+
42
+ const getHelpSchema = {
43
+ topic: { type: 'string' as const, required: true as const },
44
+ };
45
+
15
46
  export const startWorkSession: Handler = async (args, ctx) => {
16
- const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args as {
17
- project_id?: string;
18
- git_url?: string;
19
- mode?: 'lite' | 'full';
20
- model?: 'opus' | 'sonnet' | 'haiku';
21
- role?: 'developer' | 'validator' | 'deployer' | 'reviewer' | 'maintainer';
22
- };
47
+ const { project_id, git_url, mode, model, role } = parseArgs(args, startWorkSessionSchema);
23
48
 
24
49
  const { session, updateSession } = ctx;
25
50
 
26
51
  // Reset token tracking for new session with model info
27
52
  const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
28
- const validModel = normalizedModel && ['opus', 'sonnet', 'haiku'].includes(normalizedModel)
53
+ const validModel = normalizedModel && VALID_MODELS.includes(normalizedModel as SessionModel)
29
54
  ? normalizedModel
30
55
  : null;
31
56
 
@@ -52,9 +77,9 @@ export const startWorkSession: Handler = async (args, ctx) => {
52
77
  const response = await apiClient.startSession({
53
78
  project_id,
54
79
  git_url,
55
- mode,
56
- model,
57
- role
80
+ mode: mode as SessionMode,
81
+ model: model as SessionModel | undefined,
82
+ role: role as SessionRole
58
83
  });
59
84
 
60
85
  if (!response.ok) {
@@ -139,10 +164,7 @@ export const startWorkSession: Handler = async (args, ctx) => {
139
164
  };
140
165
 
141
166
  export const heartbeat: Handler = async (args, ctx) => {
142
- const { session_id, current_worktree_path } = args as {
143
- session_id?: string;
144
- current_worktree_path?: string | null;
145
- };
167
+ const { session_id, current_worktree_path } = parseArgs(args, heartbeatSchema);
146
168
  const { session } = ctx;
147
169
  const targetSession = session_id || session.currentSessionId;
148
170
 
@@ -186,7 +208,7 @@ export const heartbeat: Handler = async (args, ctx) => {
186
208
  };
187
209
 
188
210
  export const endWorkSession: Handler = async (args, ctx) => {
189
- const { session_id } = args as { session_id?: string };
211
+ const { session_id } = parseArgs(args, endWorkSessionSchema);
190
212
  const { session, updateSession } = ctx;
191
213
  const targetSession = session_id || session.currentSessionId;
192
214
 
@@ -251,7 +273,7 @@ export const endWorkSession: Handler = async (args, ctx) => {
251
273
  };
252
274
 
253
275
  export const getHelp: Handler = async (args, _ctx) => {
254
- const { topic } = args as { topic: string };
276
+ const { topic } = parseArgs(args, getHelpSchema);
255
277
 
256
278
  const apiClient = getApiClient();
257
279
  const response = await apiClient.getHelpTopic(topic);