session-collab-mcp 0.4.7 → 0.5.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.
@@ -4,6 +4,7 @@ import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
4
4
  import type { McpTool, McpToolResult } from '../protocol';
5
5
  import { createToolResult } from '../protocol';
6
6
  import { sendMessage, listMessages, getSession } from '../../db/queries';
7
+ import { validateInput, messageSendSchema, messageListSchema } from '../schemas';
7
8
 
8
9
  export const messageTools: McpTool[] = [
9
10
  {
@@ -59,12 +60,17 @@ export async function handleMessageTool(
59
60
  ): Promise<McpToolResult> {
60
61
  switch (name) {
61
62
  case 'collab_message_send': {
62
- const fromSessionId = args.from_session_id as string;
63
- const toSessionId = args.to_session_id as string | undefined;
64
- const content = args.content as string;
63
+ const validation = validateInput(messageSendSchema, args);
64
+ if (!validation.success) {
65
+ return createToolResult(
66
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
67
+ true
68
+ );
69
+ }
70
+ const input = validation.data;
65
71
 
66
72
  // Verify sender session
67
- const fromSession = await getSession(db, fromSessionId);
73
+ const fromSession = await getSession(db, input.from_session_id);
68
74
  if (!fromSession || fromSession.status !== 'active') {
69
75
  return createToolResult(
70
76
  JSON.stringify({
@@ -76,8 +82,8 @@ export async function handleMessageTool(
76
82
  }
77
83
 
78
84
  // Verify target session if specified
79
- if (toSessionId) {
80
- const toSession = await getSession(db, toSessionId);
85
+ if (input.to_session_id) {
86
+ const toSession = await getSession(db, input.to_session_id);
81
87
  if (!toSession || toSession.status !== 'active') {
82
88
  return createToolResult(
83
89
  JSON.stringify({
@@ -90,28 +96,36 @@ export async function handleMessageTool(
90
96
  }
91
97
 
92
98
  const message = await sendMessage(db, {
93
- from_session_id: fromSessionId,
94
- to_session_id: toSessionId,
95
- content,
99
+ from_session_id: input.from_session_id,
100
+ to_session_id: input.to_session_id,
101
+ content: input.content,
96
102
  });
97
103
 
98
104
  return createToolResult(
99
105
  JSON.stringify({
100
106
  success: true,
101
107
  message_id: message.id,
102
- sent_to: toSessionId ?? 'all sessions (broadcast)',
108
+ sent_to: input.to_session_id ?? 'all sessions (broadcast)',
103
109
  message: 'Message sent successfully.',
104
110
  })
105
111
  );
106
112
  }
107
113
 
108
114
  case 'collab_message_list': {
109
- const sessionId = args.session_id as string;
110
- const unreadOnly = (args.unread_only as boolean) ?? true;
111
- const markAsRead = (args.mark_as_read as boolean) ?? true;
115
+ const validation = validateInput(messageListSchema, args);
116
+ if (!validation.success) {
117
+ return createToolResult(
118
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
119
+ true
120
+ );
121
+ }
122
+ const input = validation.data;
123
+
124
+ const unreadOnly = input.unread_only ?? true;
125
+ const markAsRead = input.mark_as_read ?? true;
112
126
 
113
127
  const messages = await listMessages(db, {
114
- session_id: sessionId,
128
+ session_id: input.session_id,
115
129
  unread_only: unreadOnly,
116
130
  mark_as_read: markAsRead,
117
131
  });
@@ -14,8 +14,17 @@ import {
14
14
  cleanupStaleSessions,
15
15
  listClaims,
16
16
  } from '../../db/queries';
17
- import type { TodoItem, SessionConfig, ConflictMode } from '../../db/types';
17
+ import type { TodoItem, SessionConfig } from '../../db/types';
18
18
  import { DEFAULT_SESSION_CONFIG } from '../../db/types';
19
+ import {
20
+ validateInput,
21
+ sessionStartSchema,
22
+ sessionEndSchema,
23
+ sessionListSchema,
24
+ sessionHeartbeatSchema,
25
+ statusUpdateSchema,
26
+ configSchema,
27
+ } from '../schemas';
19
28
 
20
29
  export const sessionTools: McpTool[] = [
21
30
  {
@@ -177,17 +186,26 @@ export async function handleSessionTool(
177
186
  ): Promise<McpToolResult> {
178
187
  switch (name) {
179
188
  case 'collab_session_start': {
189
+ const validation = validateInput(sessionStartSchema, args);
190
+ if (!validation.success) {
191
+ return createToolResult(
192
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
193
+ true
194
+ );
195
+ }
196
+ const input = validation.data;
197
+
180
198
  // Cleanup stale sessions first
181
199
  await cleanupStaleSessions(db, 30);
182
200
 
183
201
  const session = await createSession(db, {
184
- name: args.name as string | undefined,
185
- project_root: args.project_root as string,
186
- machine_id: args.machine_id as string | undefined,
202
+ name: input.name,
203
+ project_root: input.project_root,
204
+ machine_id: input.machine_id,
187
205
  user_id: userId,
188
206
  });
189
207
 
190
- const activeSessions = await listSessions(db, { project_root: args.project_root as string, user_id: userId });
208
+ const activeSessions = await listSessions(db, { project_root: input.project_root, user_id: userId });
191
209
 
192
210
  return createToolResult(
193
211
  JSON.stringify(
@@ -208,10 +226,16 @@ export async function handleSessionTool(
208
226
  }
209
227
 
210
228
  case 'collab_session_end': {
211
- const sessionId = args.session_id as string;
212
- const releaseClaims = (args.release_claims as 'complete' | 'abandon') ?? 'abandon';
229
+ const validation = validateInput(sessionEndSchema, args);
230
+ if (!validation.success) {
231
+ return createToolResult(
232
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
233
+ true
234
+ );
235
+ }
236
+ const input = validation.data;
213
237
 
214
- const session = await getSession(db, sessionId);
238
+ const session = await getSession(db, input.session_id);
215
239
  if (!session) {
216
240
  return createToolResult(
217
241
  JSON.stringify({ error: 'SESSION_NOT_FOUND', message: 'Session not found' }),
@@ -219,21 +243,30 @@ export async function handleSessionTool(
219
243
  );
220
244
  }
221
245
 
222
- await endSession(db, sessionId, releaseClaims);
246
+ await endSession(db, input.session_id, input.release_claims);
223
247
 
224
248
  return createToolResult(
225
249
  JSON.stringify({
226
250
  success: true,
227
- message: `Session ended. All claims marked as ${releaseClaims === 'complete' ? 'completed' : 'abandoned'}.`,
251
+ message: `Session ended. All claims marked as ${input.release_claims === 'complete' ? 'completed' : 'abandoned'}.`,
228
252
  })
229
253
  );
230
254
  }
231
255
 
232
256
  case 'collab_session_list': {
257
+ const validation = validateInput(sessionListSchema, args);
258
+ if (!validation.success) {
259
+ return createToolResult(
260
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
261
+ true
262
+ );
263
+ }
264
+ const input = validation.data;
265
+
233
266
  // Do not filter by user_id - collaboration tool should show all sessions
234
267
  const sessions = await listSessions(db, {
235
- include_inactive: args.include_inactive as boolean,
236
- project_root: args.project_root as string | undefined,
268
+ include_inactive: input.include_inactive,
269
+ project_root: input.project_root,
237
270
  });
238
271
 
239
272
  // Get active claims count for each session and include status info
@@ -278,13 +311,18 @@ export async function handleSessionTool(
278
311
  }
279
312
 
280
313
  case 'collab_session_heartbeat': {
281
- const sessionId = args.session_id as string;
282
- const currentTask = args.current_task as string | undefined;
283
- const todos = args.todos as TodoItem[] | undefined;
314
+ const validation = validateInput(sessionHeartbeatSchema, args);
315
+ if (!validation.success) {
316
+ return createToolResult(
317
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
318
+ true
319
+ );
320
+ }
321
+ const input = validation.data;
284
322
 
285
- const updated = await updateSessionHeartbeat(db, sessionId, {
286
- current_task: currentTask,
287
- todos,
323
+ const updated = await updateSessionHeartbeat(db, input.session_id, {
324
+ current_task: input.current_task,
325
+ todos: input.todos as TodoItem[] | undefined,
288
326
  });
289
327
 
290
328
  if (!updated) {
@@ -301,18 +339,24 @@ export async function handleSessionTool(
301
339
  JSON.stringify({
302
340
  success: true,
303
341
  message: 'Heartbeat updated',
304
- status_synced: !!(currentTask || todos),
342
+ status_synced: !!(input.current_task || input.todos),
305
343
  })
306
344
  );
307
345
  }
308
346
 
309
347
  case 'collab_status_update': {
310
- const sessionId = args.session_id as string;
311
- const currentTask = args.current_task as string | undefined;
312
- const todos = args.todos as TodoItem[] | undefined;
348
+ const validation = validateInput(statusUpdateSchema, args);
349
+ if (!validation.success) {
350
+ return createToolResult(
351
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
352
+ true
353
+ );
354
+ }
355
+ const input = validation.data;
356
+ const todos = input.todos as TodoItem[] | undefined;
313
357
 
314
358
  // Validate session exists
315
- const session = await getSession(db, sessionId);
359
+ const session = await getSession(db, input.session_id);
316
360
  if (!session || session.status !== 'active') {
317
361
  return createToolResult(
318
362
  JSON.stringify({
@@ -323,8 +367,8 @@ export async function handleSessionTool(
323
367
  );
324
368
  }
325
369
 
326
- await updateSessionStatus(db, sessionId, {
327
- current_task: currentTask,
370
+ await updateSessionStatus(db, input.session_id, {
371
+ current_task: input.current_task,
328
372
  todos,
329
373
  });
330
374
 
@@ -343,28 +387,24 @@ export async function handleSessionTool(
343
387
  JSON.stringify({
344
388
  success: true,
345
389
  message: 'Status updated successfully.',
346
- current_task: currentTask ?? null,
390
+ current_task: input.current_task ?? null,
347
391
  progress,
348
392
  })
349
393
  );
350
394
  }
351
395
 
352
396
  case 'collab_config': {
353
- const sessionId = args.session_id as string;
354
-
355
- // Validate session_id input
356
- if (!sessionId || typeof sessionId !== 'string') {
397
+ const validation = validateInput(configSchema, args);
398
+ if (!validation.success) {
357
399
  return createToolResult(
358
- JSON.stringify({
359
- error: 'INVALID_INPUT',
360
- message: 'session_id is required',
361
- }),
400
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
362
401
  true
363
402
  );
364
403
  }
404
+ const input = validation.data;
365
405
 
366
406
  // Validate session exists
367
- const session = await getSession(db, sessionId);
407
+ const session = await getSession(db, input.session_id);
368
408
  if (!session || session.status !== 'active') {
369
409
  return createToolResult(
370
410
  JSON.stringify({
@@ -387,17 +427,17 @@ export async function handleSessionTool(
387
427
 
388
428
  // Update with new values
389
429
  const newConfig: SessionConfig = {
390
- mode: (args.mode as ConflictMode) ?? currentConfig.mode,
391
- allow_release_others: args.allow_release_others !== undefined
392
- ? (args.allow_release_others as boolean)
430
+ mode: input.mode ?? currentConfig.mode,
431
+ allow_release_others: input.allow_release_others !== undefined
432
+ ? input.allow_release_others
393
433
  : currentConfig.allow_release_others,
394
- auto_release_stale: args.auto_release_stale !== undefined
395
- ? (args.auto_release_stale as boolean)
434
+ auto_release_stale: input.auto_release_stale !== undefined
435
+ ? input.auto_release_stale
396
436
  : currentConfig.auto_release_stale,
397
- stale_threshold_hours: (args.stale_threshold_hours as number) ?? currentConfig.stale_threshold_hours,
437
+ stale_threshold_hours: input.stale_threshold_hours ?? currentConfig.stale_threshold_hours,
398
438
  };
399
439
 
400
- await updateSessionConfig(db, sessionId, newConfig);
440
+ await updateSessionConfig(db, input.session_id, newConfig);
401
441
 
402
442
  return createToolResult(
403
443
  JSON.stringify({