session-collab-mcp 0.5.0 → 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.
@@ -3,8 +3,8 @@
3
3
  import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
4
4
  import type { McpTool, McpToolResult } from '../protocol';
5
5
  import { createToolResult } from '../protocol';
6
- import type { DecisionCategory } from '../../db/types';
7
6
  import { addDecision, listDecisions, getSession } from '../../db/queries';
7
+ import { validateInput, decisionAddSchema, decisionListSchema } from '../schemas';
8
8
 
9
9
  export const decisionTools: McpTool[] = [
10
10
  {
@@ -61,13 +61,17 @@ export async function handleDecisionTool(
61
61
  ): Promise<McpToolResult> {
62
62
  switch (name) {
63
63
  case 'collab_decision_add': {
64
- const sessionId = args.session_id as string;
65
- const category = args.category as DecisionCategory | undefined;
66
- const title = args.title as string;
67
- const description = args.description as string;
64
+ const validation = validateInput(decisionAddSchema, args);
65
+ if (!validation.success) {
66
+ return createToolResult(
67
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
68
+ true
69
+ );
70
+ }
71
+ const input = validation.data;
68
72
 
69
73
  // Verify session
70
- const session = await getSession(db, sessionId);
74
+ const session = await getSession(db, input.session_id);
71
75
  if (!session || session.status !== 'active') {
72
76
  return createToolResult(
73
77
  JSON.stringify({
@@ -79,10 +83,10 @@ export async function handleDecisionTool(
79
83
  }
80
84
 
81
85
  const decision = await addDecision(db, {
82
- session_id: sessionId,
83
- category,
84
- title,
85
- description,
86
+ session_id: input.session_id,
87
+ category: input.category,
88
+ title: input.title,
89
+ description: input.description,
86
90
  });
87
91
 
88
92
  return createToolResult(
@@ -95,10 +99,19 @@ export async function handleDecisionTool(
95
99
  }
96
100
 
97
101
  case 'collab_decision_list': {
98
- const category = args.category as DecisionCategory | undefined;
99
- const limit = (args.limit as number) ?? 20;
102
+ const validation = validateInput(decisionListSchema, args);
103
+ if (!validation.success) {
104
+ return createToolResult(
105
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
106
+ true
107
+ );
108
+ }
109
+ const input = validation.data;
100
110
 
101
- const decisions = await listDecisions(db, { category, limit });
111
+ const decisions = await listDecisions(db, {
112
+ category: input.category,
113
+ limit: input.limit ?? 20,
114
+ });
102
115
 
103
116
  return createToolResult(
104
117
  JSON.stringify(
@@ -3,8 +3,15 @@
3
3
  import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
4
4
  import type { McpTool, McpToolResult } from '../protocol';
5
5
  import { createToolResult } from '../protocol';
6
- import type { SymbolType, ConflictInfo, ReferenceInput } from '../../db/types';
6
+ import type { SymbolType, ConflictInfo } from '../../db/types';
7
7
  import { storeReferences, analyzeClaimImpact, clearSessionReferences } from '../../db/queries';
8
+ import {
9
+ validateInput,
10
+ analyzeSymbolsSchema,
11
+ validateSymbolsSchema,
12
+ storeReferencesSchema,
13
+ impactAnalysisSchema,
14
+ } from '../schemas';
8
15
 
9
16
  // LSP Symbol Kind mapping (from LSP spec)
10
17
  const LSP_SYMBOL_KIND_MAP: Record<number, SymbolType> = {
@@ -305,24 +312,17 @@ export async function handleLspTool(
305
312
  ): Promise<McpToolResult> {
306
313
  switch (name) {
307
314
  case 'collab_analyze_symbols': {
308
- const sessionId = args.session_id as string;
309
- const files = args.files as FileSymbolInput[] | undefined;
310
- const references = args.references as SymbolReference[] | undefined;
311
- const checkSymbols = args.check_symbols as string[] | undefined;
312
-
313
- if (!sessionId) {
314
- return createToolResult(
315
- JSON.stringify({ error: 'INVALID_INPUT', message: 'session_id is required' }),
316
- true
317
- );
318
- }
319
-
320
- if (!files || !Array.isArray(files) || files.length === 0) {
315
+ const validation = validateInput(analyzeSymbolsSchema, args);
316
+ if (!validation.success) {
321
317
  return createToolResult(
322
- JSON.stringify({ error: 'INVALID_INPUT', message: 'files array is required' }),
318
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
323
319
  true
324
320
  );
325
321
  }
322
+ const input = validation.data;
323
+ const files = input.files as FileSymbolInput[];
324
+ const references = input.references as SymbolReference[] | undefined;
325
+ const checkSymbols = input.check_symbols;
326
326
 
327
327
  // Build reference lookup map if provided
328
328
  const referenceMap = new Map<string, SymbolReference>();
@@ -352,10 +352,10 @@ export async function handleLspTool(
352
352
  const symbolNames = [...new Set(symbolsToAnalyze.map((s) => s.name))];
353
353
 
354
354
  // Check for symbol-level claims
355
- const symbolConflicts = await querySymbolConflicts(db, fileList, symbolNames, sessionId);
355
+ const symbolConflicts = await querySymbolConflicts(db, fileList, symbolNames, input.session_id);
356
356
 
357
357
  // Check for file-level claims
358
- const fileConflicts = await queryFileConflicts(db, fileList, sessionId);
358
+ const fileConflicts = await queryFileConflicts(db, fileList, input.session_id);
359
359
 
360
360
  // Build result with conflict status for each symbol
361
361
  const analyzedSymbols: AnalyzedSymbol[] = [];
@@ -455,23 +455,18 @@ export async function handleLspTool(
455
455
  }
456
456
 
457
457
  case 'collab_validate_symbols': {
458
- const file = args.file as string;
459
- const symbols = args.symbols as string[];
460
- const lspSymbols = args.lsp_symbols as Array<{ name: string; kind: number }>;
461
-
462
- if (!file || !symbols || !lspSymbols) {
458
+ const validation = validateInput(validateSymbolsSchema, args);
459
+ if (!validation.success) {
463
460
  return createToolResult(
464
- JSON.stringify({
465
- error: 'INVALID_INPUT',
466
- message: 'file, symbols, and lsp_symbols are required',
467
- }),
461
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
468
462
  true
469
463
  );
470
464
  }
465
+ const input = validation.data;
471
466
 
472
467
  // Build set of available symbol names from LSP data
473
468
  const availableSymbols = new Set<string>();
474
- for (const lspSym of lspSymbols) {
469
+ for (const lspSym of input.lsp_symbols) {
475
470
  availableSymbols.add(lspSym.name);
476
471
  }
477
472
 
@@ -480,7 +475,7 @@ export async function handleLspTool(
480
475
  const invalid: string[] = [];
481
476
  const suggestions: Record<string, string[]> = {};
482
477
 
483
- for (const sym of symbols) {
478
+ for (const sym of input.symbols) {
484
479
  if (availableSymbols.has(sym)) {
485
480
  valid.push(sym);
486
481
  } else {
@@ -502,7 +497,7 @@ export async function handleLspTool(
502
497
  return createToolResult(
503
498
  JSON.stringify({
504
499
  valid: allValid,
505
- file,
500
+ file: input.file,
506
501
  valid_symbols: valid,
507
502
  invalid_symbols: invalid,
508
503
  suggestions: Object.keys(suggestions).length > 0 ? suggestions : undefined,
@@ -515,32 +510,23 @@ export async function handleLspTool(
515
510
  }
516
511
 
517
512
  case 'collab_store_references': {
518
- const sessionId = args.session_id as string;
519
- const references = args.references as ReferenceInput[];
520
- const clearExisting = args.clear_existing as boolean | undefined;
521
-
522
- if (!sessionId) {
523
- return createToolResult(
524
- JSON.stringify({ error: 'INVALID_INPUT', message: 'session_id is required' }),
525
- true
526
- );
527
- }
528
-
529
- if (!references || !Array.isArray(references)) {
513
+ const validation = validateInput(storeReferencesSchema, args);
514
+ if (!validation.success) {
530
515
  return createToolResult(
531
- JSON.stringify({ error: 'INVALID_INPUT', message: 'references array is required' }),
516
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
532
517
  true
533
518
  );
534
519
  }
520
+ const input = validation.data;
535
521
 
536
522
  // Clear existing references if requested
537
523
  let cleared = 0;
538
- if (clearExisting) {
539
- cleared = await clearSessionReferences(db, sessionId);
524
+ if (input.clear_existing) {
525
+ cleared = await clearSessionReferences(db, input.session_id);
540
526
  }
541
527
 
542
528
  // Store new references
543
- const result = await storeReferences(db, sessionId, references);
529
+ const result = await storeReferences(db, input.session_id, input.references);
544
530
 
545
531
  return createToolResult(
546
532
  JSON.stringify({
@@ -554,21 +540,16 @@ export async function handleLspTool(
554
540
  }
555
541
 
556
542
  case 'collab_impact_analysis': {
557
- const sessionId = args.session_id as string;
558
- const file = args.file as string;
559
- const symbol = args.symbol as string;
560
-
561
- if (!sessionId || !file || !symbol) {
543
+ const validation = validateInput(impactAnalysisSchema, args);
544
+ if (!validation.success) {
562
545
  return createToolResult(
563
- JSON.stringify({
564
- error: 'INVALID_INPUT',
565
- message: 'session_id, file, and symbol are required',
566
- }),
546
+ JSON.stringify({ error: 'INVALID_INPUT', message: validation.error }),
567
547
  true
568
548
  );
569
549
  }
550
+ const input = validation.data;
570
551
 
571
- const impact = await analyzeClaimImpact(db, file, symbol, sessionId);
552
+ const impact = await analyzeClaimImpact(db, input.file, input.symbol, input.session_id);
572
553
 
573
554
  // Determine risk level
574
555
  let riskLevel: 'low' | 'medium' | 'high';
@@ -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({