claude-multi-session 1.0.0

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.
@@ -0,0 +1,808 @@
1
+ /**
2
+ * MCP Server — Model Context Protocol server for claude-multi-session.
3
+ *
4
+ * This turns the multi-session system into NATIVE Claude Code tools.
5
+ * Instead of Claude running CLI commands via Bash, it calls tools directly:
6
+ *
7
+ * mcp__multi-session__spawn_session
8
+ * mcp__multi-session__send_message
9
+ * mcp__multi-session__delegate_task
10
+ * etc.
11
+ *
12
+ * Protocol: JSON-RPC 2.0 over stdio (newline-delimited JSON).
13
+ * Zero dependencies — implements the MCP protocol manually.
14
+ *
15
+ * How MCP works:
16
+ * 1. Claude Code starts this server as a child process
17
+ * 2. Claude sends JSON-RPC requests on stdin
18
+ * 3. Server responds with JSON-RPC responses on stdout
19
+ * 4. All debug/log output goes to stderr (never stdout)
20
+ *
21
+ * JSON-RPC methods handled:
22
+ * - initialize → capability negotiation
23
+ * - notifications/initialized → client confirms init (no response)
24
+ * - tools/list → return available tools
25
+ * - tools/call → execute a tool and return result
26
+ */
27
+
28
+ const readline = require('readline');
29
+ const SessionManager = require('./manager');
30
+ const Delegate = require('./delegate');
31
+
32
+ // =============================================================================
33
+ // Server State — persists across all tool calls
34
+ // =============================================================================
35
+
36
+ // Single SessionManager instance for the entire MCP server lifetime.
37
+ // This means spawned sessions stay alive between tool calls — much more
38
+ // efficient than the CLI where each command creates a new manager.
39
+ const manager = new SessionManager();
40
+
41
+ // Track delegates per session (for continue/finish/abort)
42
+ const delegates = new Map();
43
+
44
+ // =============================================================================
45
+ // Tool Definitions — these appear in Claude's tool list
46
+ // =============================================================================
47
+
48
+ const TOOLS = [
49
+ // ── Session Management ──────────────────────────────────────────────────
50
+ {
51
+ name: 'spawn_session',
52
+ description:
53
+ 'Start a new Claude Code session. Spawns a long-lived streaming process. ' +
54
+ 'Optionally sends an initial prompt and returns the response. ' +
55
+ 'The session stays alive for follow-up messages via send_message.',
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {
59
+ name: { type: 'string', description: 'Unique session name (e.g. "fix-auth", "build-login")' },
60
+ prompt: { type: 'string', description: 'Initial prompt to send (optional — if omitted, session starts idle)' },
61
+ model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model to use (default: sonnet)' },
62
+ work_dir: { type: 'string', description: 'Working directory for the session (default: current directory)' },
63
+ permission_mode:{ type: 'string', enum: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], description: 'Permission mode (default: default)' },
64
+ allowed_tools: { type: 'array', items: { type: 'string' }, description: 'Restrict to specific tools (e.g. ["Read", "Glob", "Grep"])' },
65
+ system_prompt: { type: 'string', description: 'Text to append to system prompt' },
66
+ max_budget: { type: 'number', description: 'Max budget in USD for the session' },
67
+ agent: { type: 'string', description: 'Agent to use for the session' },
68
+ },
69
+ required: ['name'],
70
+ },
71
+ },
72
+
73
+ {
74
+ name: 'send_message',
75
+ description:
76
+ 'Send a follow-up message to an existing session. ' +
77
+ 'The session keeps full conversation context — no restart needed. ' +
78
+ 'If the session was stopped, it auto-resumes using the saved session ID.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ name: { type: 'string', description: 'Session name' },
83
+ message: { type: 'string', description: 'Message to send' },
84
+ },
85
+ required: ['name', 'message'],
86
+ },
87
+ },
88
+
89
+ {
90
+ name: 'resume_session',
91
+ description:
92
+ 'Resume a stopped/paused session. Uses --resume with the saved session ID ' +
93
+ 'to restore the full conversation. Optionally sends a message immediately.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ name: { type: 'string', description: 'Session name to resume' },
98
+ message: { type: 'string', description: 'Optional message to send after resuming' },
99
+ },
100
+ required: ['name'],
101
+ },
102
+ },
103
+
104
+ {
105
+ name: 'pause_session',
106
+ description: 'Pause a session. The process stays alive but no messages are accepted until resumed.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ name: { type: 'string', description: 'Session name to pause' },
111
+ },
112
+ required: ['name'],
113
+ },
114
+ },
115
+
116
+ {
117
+ name: 'fork_session',
118
+ description:
119
+ 'Fork a session into a new one. The new session starts with all the parent\'s ' +
120
+ 'conversation context. Useful for trying different approaches from the same starting point.',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ name: { type: 'string', description: 'Source session name to fork from' },
125
+ new_name: { type: 'string', description: 'Name for the new forked session' },
126
+ message: { type: 'string', description: 'Initial message for the fork (default: "Continue from the forked conversation.")' },
127
+ model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model for the forked session' },
128
+ },
129
+ required: ['name', 'new_name'],
130
+ },
131
+ },
132
+
133
+ {
134
+ name: 'stop_session',
135
+ description: 'Gracefully stop a session. Saves state so it can be resumed later with resume_session.',
136
+ inputSchema: {
137
+ type: 'object',
138
+ properties: {
139
+ name: { type: 'string', description: 'Session name to stop' },
140
+ },
141
+ required: ['name'],
142
+ },
143
+ },
144
+
145
+ {
146
+ name: 'kill_session',
147
+ description: 'Force kill a session immediately. Use when stop_session is not working.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ name: { type: 'string', description: 'Session name to kill' },
152
+ },
153
+ required: ['name'],
154
+ },
155
+ },
156
+
157
+ // ── Information ─────────────────────────────────────────────────────────
158
+ {
159
+ name: 'get_session_status',
160
+ description: 'Get detailed status of a session — status, model, cost, turns, interactions.',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ name: { type: 'string', description: 'Session name' },
165
+ },
166
+ required: ['name'],
167
+ },
168
+ },
169
+
170
+ {
171
+ name: 'get_last_output',
172
+ description: 'Get the last response text from a session.',
173
+ inputSchema: {
174
+ type: 'object',
175
+ properties: {
176
+ name: { type: 'string', description: 'Session name' },
177
+ },
178
+ required: ['name'],
179
+ },
180
+ },
181
+
182
+ {
183
+ name: 'list_sessions',
184
+ description:
185
+ 'List all sessions (both alive and stopped). Shows name, status, model, cost, turns. ' +
186
+ 'Optionally filter by status.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ status: { type: 'string', enum: ['ready', 'paused', 'stopped', 'killed', 'busy'], description: 'Filter by status (optional)' },
191
+ },
192
+ },
193
+ },
194
+
195
+ {
196
+ name: 'get_history',
197
+ description: 'Get full interaction history (all prompts and responses) for a session.',
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {
201
+ name: { type: 'string', description: 'Session name' },
202
+ },
203
+ required: ['name'],
204
+ },
205
+ },
206
+
207
+ {
208
+ name: 'delete_session',
209
+ description: 'Permanently delete a session and all its data. Cannot be undone.',
210
+ inputSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ name: { type: 'string', description: 'Session name to delete' },
214
+ },
215
+ required: ['name'],
216
+ },
217
+ },
218
+
219
+ // ── Delegate (Control Loop + Safety) ────────────────────────────────────
220
+ {
221
+ name: 'delegate_task',
222
+ description:
223
+ 'Smart task delegation with control loop and safety net. ' +
224
+ 'Spawns a child session, sends the task, monitors for issues ' +
225
+ '(permission denials, cost overruns, turn limits), and returns structured output. ' +
226
+ 'Use this for autonomous task execution with safety guardrails. ' +
227
+ 'After delegation, use continue_task to send corrections if the result is not satisfactory.',
228
+ inputSchema: {
229
+ type: 'object',
230
+ properties: {
231
+ name: { type: 'string', description: 'Unique name for this delegated task' },
232
+ task: { type: 'string', description: 'Task description for the child session' },
233
+ model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model to use (default: sonnet)' },
234
+ preset: { type: 'string', enum: ['read-only', 'review', 'edit', 'full', 'plan'], description: 'Permission preset (default: edit)' },
235
+ max_cost: { type: 'number', description: 'Max cost in USD before auto-kill (default: 2.00)' },
236
+ max_turns: { type: 'number', description: 'Max agent turns before auto-kill (default: 50)' },
237
+ context: { type: 'string', description: 'Extra context to prepend to the task' },
238
+ work_dir: { type: 'string', description: 'Working directory for the child session' },
239
+ system_prompt: { type: 'string', description: 'Text to append to system prompt' },
240
+ agent: { type: 'string', description: 'Agent to use' },
241
+ safety: { type: 'boolean', description: 'Enable safety net (default: true). Set false to disable cost/turn/path limits.' },
242
+ },
243
+ required: ['name', 'task'],
244
+ },
245
+ },
246
+
247
+ {
248
+ name: 'continue_task',
249
+ description:
250
+ 'Send a follow-up correction or instruction to a delegated task. ' +
251
+ 'The child session retains its full conversation memory — it remembers ' +
252
+ 'what it did, what files it touched, and what the original task was. ' +
253
+ 'Use this to correct mistakes, request changes, or add requirements.',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: {
257
+ name: { type: 'string', description: 'Name of the delegated task to continue' },
258
+ message: { type: 'string', description: 'Follow-up instruction or correction' },
259
+ },
260
+ required: ['name', 'message'],
261
+ },
262
+ },
263
+
264
+ {
265
+ name: 'finish_task',
266
+ description: 'Finish a delegated task — stop the session cleanly. Call this when the task result is satisfactory.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ name: { type: 'string', description: 'Name of the delegated task to finish' },
271
+ },
272
+ required: ['name'],
273
+ },
274
+ },
275
+
276
+ {
277
+ name: 'abort_task',
278
+ description: 'Abort a delegated task immediately — force kill the session. Use when the task is going off track and corrections are not working.',
279
+ inputSchema: {
280
+ type: 'object',
281
+ properties: {
282
+ name: { type: 'string', description: 'Name of the delegated task to abort' },
283
+ },
284
+ required: ['name'],
285
+ },
286
+ },
287
+
288
+ // ── Batch ───────────────────────────────────────────────────────────────
289
+ {
290
+ name: 'batch_spawn',
291
+ description:
292
+ 'Spawn multiple sessions in parallel. Each session gets its own name, prompt, and model. ' +
293
+ 'Returns results for all sessions. Use for parallel task execution.',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ sessions: {
298
+ type: 'array',
299
+ description: 'Array of session specs to spawn in parallel',
300
+ items: {
301
+ type: 'object',
302
+ properties: {
303
+ name: { type: 'string', description: 'Session name' },
304
+ prompt: { type: 'string', description: 'Initial prompt' },
305
+ model: { type: 'string', description: 'Model to use' },
306
+ },
307
+ required: ['name', 'prompt'],
308
+ },
309
+ },
310
+ },
311
+ required: ['sessions'],
312
+ },
313
+ },
314
+ ];
315
+
316
+ // =============================================================================
317
+ // Tool Handlers — execute each tool and return result
318
+ // =============================================================================
319
+
320
+ /**
321
+ * Execute a tool by name with given arguments.
322
+ * Returns { content: [{ type: "text", text: "..." }], isError?: true }
323
+ */
324
+ async function executeTool(toolName, args) {
325
+ try {
326
+ switch (toolName) {
327
+ // ── Session Management ────────────────────────────────────────────
328
+ case 'spawn_session':
329
+ return await handleSpawn(args);
330
+ case 'send_message':
331
+ return await handleSend(args);
332
+ case 'resume_session':
333
+ return await handleResume(args);
334
+ case 'pause_session':
335
+ return handlePause(args);
336
+ case 'fork_session':
337
+ return await handleFork(args);
338
+ case 'stop_session':
339
+ return handleStop(args);
340
+ case 'kill_session':
341
+ return handleKill(args);
342
+
343
+ // ── Information ───────────────────────────────────────────────────
344
+ case 'get_session_status':
345
+ return handleStatus(args);
346
+ case 'get_last_output':
347
+ return handleLastOutput(args);
348
+ case 'list_sessions':
349
+ return handleList(args);
350
+ case 'get_history':
351
+ return handleHistory(args);
352
+ case 'delete_session':
353
+ return handleDelete(args);
354
+
355
+ // ── Delegate ──────────────────────────────────────────────────────
356
+ case 'delegate_task':
357
+ return await handleDelegate(args);
358
+ case 'continue_task':
359
+ return await handleContinue(args);
360
+ case 'finish_task':
361
+ return handleFinish(args);
362
+ case 'abort_task':
363
+ return handleAbort(args);
364
+
365
+ // ── Batch ─────────────────────────────────────────────────────────
366
+ case 'batch_spawn':
367
+ return await handleBatch(args);
368
+
369
+ default:
370
+ return errorResult(`Unknown tool: ${toolName}`);
371
+ }
372
+ } catch (err) {
373
+ return errorResult(err.message);
374
+ }
375
+ }
376
+
377
+ // ── Session Handlers ──────────────────────────────────────────────────────
378
+
379
+ async function handleSpawn(args) {
380
+ const { session, response } = await manager.spawn(args.name, {
381
+ prompt: args.prompt,
382
+ model: args.model,
383
+ workDir: args.work_dir,
384
+ permissionMode: args.permission_mode,
385
+ allowedTools: args.allowed_tools,
386
+ systemPrompt: args.system_prompt,
387
+ maxBudget: args.max_budget,
388
+ agent: args.agent,
389
+ });
390
+
391
+ const result = {
392
+ session_name: session.name,
393
+ status: session.status,
394
+ model: session.model,
395
+ session_id: session.id,
396
+ };
397
+
398
+ if (response) {
399
+ result.response = response.text;
400
+ result.cost = response.cost;
401
+ result.turns = response.turns;
402
+ result.duration_ms = response.duration;
403
+ } else {
404
+ result.message = 'Session started (idle). Send a message with send_message.';
405
+ }
406
+
407
+ return textResult(JSON.stringify(result, null, 2));
408
+ }
409
+
410
+ async function handleSend(args) {
411
+ // Auto-resume if session is not alive
412
+ if (!manager.sessions.has(args.name)) {
413
+ log(`Session "${args.name}" not alive. Auto-resuming...`);
414
+ const response = await manager.resume(args.name, args.message);
415
+ return textResult(JSON.stringify({
416
+ session_name: args.name,
417
+ auto_resumed: true,
418
+ response: response?.text || '',
419
+ cost: response?.cost || 0,
420
+ turns: response?.turns || 0,
421
+ duration_ms: response?.duration || 0,
422
+ }, null, 2));
423
+ }
424
+
425
+ const response = await manager.send(args.name, args.message);
426
+ return textResult(JSON.stringify({
427
+ session_name: args.name,
428
+ response: response.text,
429
+ cost: response.cost,
430
+ turns: response.turns,
431
+ duration_ms: response.duration,
432
+ }, null, 2));
433
+ }
434
+
435
+ async function handleResume(args) {
436
+ const response = await manager.resume(args.name, args.message);
437
+ return textResult(JSON.stringify({
438
+ session_name: args.name,
439
+ status: 'resumed',
440
+ response: response?.text || null,
441
+ cost: response?.cost || 0,
442
+ turns: response?.turns || 0,
443
+ }, null, 2));
444
+ }
445
+
446
+ function handlePause(args) {
447
+ manager.pause(args.name);
448
+ return textResult(JSON.stringify({
449
+ session_name: args.name,
450
+ status: 'paused',
451
+ }, null, 2));
452
+ }
453
+
454
+ async function handleFork(args) {
455
+ const { session, response } = await manager.fork(args.name, args.new_name, {
456
+ message: args.message,
457
+ model: args.model,
458
+ });
459
+
460
+ return textResult(JSON.stringify({
461
+ source: args.name,
462
+ forked_as: args.new_name,
463
+ status: session.status,
464
+ response: response?.text || '',
465
+ cost: response?.cost || 0,
466
+ turns: response?.turns || 0,
467
+ }, null, 2));
468
+ }
469
+
470
+ function handleStop(args) {
471
+ manager.stop(args.name);
472
+ return textResult(JSON.stringify({
473
+ session_name: args.name,
474
+ status: 'stopped',
475
+ message: 'Session stopped. Can be resumed later with resume_session.',
476
+ }, null, 2));
477
+ }
478
+
479
+ function handleKill(args) {
480
+ manager.kill(args.name);
481
+ return textResult(JSON.stringify({
482
+ session_name: args.name,
483
+ status: 'killed',
484
+ }, null, 2));
485
+ }
486
+
487
+ // ── Information Handlers ──────────────────────────────────────────────────
488
+
489
+ function handleStatus(args) {
490
+ const info = manager.status(args.name);
491
+ return textResult(JSON.stringify({
492
+ name: info.name,
493
+ status: info.status,
494
+ model: info.model,
495
+ session_id: info.claudeSessionId || info.id,
496
+ work_dir: info.workDir,
497
+ total_cost: info.totalCostUsd || 0,
498
+ total_turns: info.totalTurns || 0,
499
+ interactions: info.interactionCount || (info.interactions || []).length,
500
+ pid: info.pid || null,
501
+ }, null, 2));
502
+ }
503
+
504
+ function handleLastOutput(args) {
505
+ const last = manager.lastOutput(args.name);
506
+ if (!last) {
507
+ return textResult(JSON.stringify({ session_name: args.name, output: null, message: 'No output yet.' }, null, 2));
508
+ }
509
+ return textResult(JSON.stringify({
510
+ session_name: args.name,
511
+ prompt: last.prompt,
512
+ response: last.response,
513
+ cost: last.cost,
514
+ timestamp: last.timestamp,
515
+ }, null, 2));
516
+ }
517
+
518
+ function handleList(args) {
519
+ const sessions = manager.list(args.status);
520
+ const summary = sessions.map(s => ({
521
+ name: s.name,
522
+ status: s.status,
523
+ model: s.model,
524
+ total_cost: s.totalCostUsd || 0,
525
+ total_turns: s.totalTurns || 0,
526
+ interactions: s.interactionCount || (s.interactions || []).length,
527
+ }));
528
+ return textResult(JSON.stringify({ total: summary.length, sessions: summary }, null, 2));
529
+ }
530
+
531
+ function handleHistory(args) {
532
+ const hist = manager.history(args.name);
533
+ return textResult(JSON.stringify({
534
+ session_name: args.name,
535
+ interaction_count: hist.length,
536
+ interactions: hist.map((h, i) => ({
537
+ index: i,
538
+ prompt: h.prompt,
539
+ response: h.response,
540
+ cost: h.cost,
541
+ turns: h.turns,
542
+ duration_ms: h.duration,
543
+ timestamp: h.timestamp,
544
+ })),
545
+ }, null, 2));
546
+ }
547
+
548
+ function handleDelete(args) {
549
+ manager.delete(args.name);
550
+ return textResult(JSON.stringify({
551
+ session_name: args.name,
552
+ status: 'deleted',
553
+ message: 'Session permanently deleted.',
554
+ }, null, 2));
555
+ }
556
+
557
+ // ── Delegate Handlers ─────────────────────────────────────────────────────
558
+
559
+ async function handleDelegate(args) {
560
+ // Create or reuse a delegate instance for this session
561
+ let delegate = delegates.get(args.name);
562
+ if (!delegate) {
563
+ delegate = new Delegate(manager);
564
+ delegates.set(args.name, delegate);
565
+ }
566
+
567
+ const result = await delegate.run(args.name, {
568
+ task: args.task,
569
+ model: args.model,
570
+ preset: args.preset,
571
+ workDir: args.work_dir,
572
+ maxCost: args.max_cost,
573
+ maxTurns: args.max_turns,
574
+ context: args.context,
575
+ systemPrompt: args.system_prompt,
576
+ agent: args.agent,
577
+ safety: args.safety,
578
+ });
579
+
580
+ return textResult(JSON.stringify(result, null, 2));
581
+ }
582
+
583
+ async function handleContinue(args) {
584
+ // Get or create delegate for this session
585
+ let delegate = delegates.get(args.name);
586
+ if (!delegate) {
587
+ delegate = new Delegate(manager);
588
+ delegates.set(args.name, delegate);
589
+ }
590
+
591
+ const result = await delegate.continue(args.name, args.message);
592
+ return textResult(JSON.stringify(result, null, 2));
593
+ }
594
+
595
+ function handleFinish(args) {
596
+ const delegate = delegates.get(args.name);
597
+ if (delegate) {
598
+ delegate.finish(args.name);
599
+ delegates.delete(args.name);
600
+ } else {
601
+ // No delegate — just stop the session
602
+ try { manager.stop(args.name); } catch (e) {}
603
+ }
604
+ return textResult(JSON.stringify({
605
+ session_name: args.name,
606
+ status: 'finished',
607
+ message: 'Task finished. Session stopped.',
608
+ }, null, 2));
609
+ }
610
+
611
+ function handleAbort(args) {
612
+ const delegate = delegates.get(args.name);
613
+ if (delegate) {
614
+ delegate.abort(args.name);
615
+ delegates.delete(args.name);
616
+ } else {
617
+ try { manager.kill(args.name); } catch (e) {}
618
+ }
619
+ return textResult(JSON.stringify({
620
+ session_name: args.name,
621
+ status: 'aborted',
622
+ message: 'Task aborted. Session killed.',
623
+ }, null, 2));
624
+ }
625
+
626
+ // ── Batch Handler ─────────────────────────────────────────────────────────
627
+
628
+ async function handleBatch(args) {
629
+ const results = await manager.batch(args.sessions);
630
+ const summary = results.map(r => ({
631
+ name: r.session?.name || 'unknown',
632
+ status: r.error ? 'failed' : 'completed',
633
+ response: r.response?.text || null,
634
+ cost: r.response?.cost || 0,
635
+ error: r.error || null,
636
+ }));
637
+ return textResult(JSON.stringify({ total: summary.length, results: summary }, null, 2));
638
+ }
639
+
640
+ // =============================================================================
641
+ // Result Helpers
642
+ // =============================================================================
643
+
644
+ function textResult(text) {
645
+ return { content: [{ type: 'text', text }] };
646
+ }
647
+
648
+ function errorResult(message) {
649
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
650
+ }
651
+
652
+ // =============================================================================
653
+ // MCP Protocol Handler — JSON-RPC 2.0 over stdio
654
+ // =============================================================================
655
+
656
+ /**
657
+ * Log to stderr (NEVER stdout — stdout is for MCP protocol only).
658
+ */
659
+ function log(msg) {
660
+ process.stderr.write(`[multi-session-mcp] ${msg}\n`);
661
+ }
662
+
663
+ /**
664
+ * Send a JSON-RPC response on stdout.
665
+ */
666
+ function sendResponse(id, result) {
667
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, result });
668
+ process.stdout.write(msg + '\n');
669
+ }
670
+
671
+ /**
672
+ * Send a JSON-RPC error on stdout.
673
+ */
674
+ function sendError(id, code, message) {
675
+ const msg = JSON.stringify({
676
+ jsonrpc: '2.0',
677
+ id,
678
+ error: { code, message },
679
+ });
680
+ process.stdout.write(msg + '\n');
681
+ }
682
+
683
+ /**
684
+ * Handle an incoming JSON-RPC message.
685
+ */
686
+ async function handleMessage(message) {
687
+ const { id, method, params } = message;
688
+
689
+ switch (method) {
690
+ // ── Initialize ────────────────────────────────────────────────────
691
+ case 'initialize':
692
+ sendResponse(id, {
693
+ protocolVersion: '2024-11-05',
694
+ capabilities: {
695
+ tools: {},
696
+ },
697
+ serverInfo: {
698
+ name: 'claude-multi-session',
699
+ version: '1.0.0',
700
+ },
701
+ });
702
+ break;
703
+
704
+ // ── Initialized notification (no response needed) ─────────────────
705
+ case 'notifications/initialized':
706
+ log('Client initialized.');
707
+ break;
708
+
709
+ // ── List tools ────────────────────────────────────────────────────
710
+ case 'tools/list':
711
+ sendResponse(id, { tools: TOOLS });
712
+ break;
713
+
714
+ // ── Call a tool ───────────────────────────────────────────────────
715
+ case 'tools/call':
716
+ if (!params || !params.name) {
717
+ sendError(id, -32602, 'Missing tool name');
718
+ break;
719
+ }
720
+ try {
721
+ const result = await executeTool(params.name, params.arguments || {});
722
+ sendResponse(id, result);
723
+ } catch (err) {
724
+ sendResponse(id, errorResult(err.message));
725
+ }
726
+ break;
727
+
728
+ // ── Ping ──────────────────────────────────────────────────────────
729
+ case 'ping':
730
+ sendResponse(id, {});
731
+ break;
732
+
733
+ // ── Unknown method ────────────────────────────────────────────────
734
+ default:
735
+ if (id !== undefined) {
736
+ sendError(id, -32601, `Method not found: ${method}`);
737
+ }
738
+ // Notifications (no id) are silently ignored
739
+ break;
740
+ }
741
+ }
742
+
743
+ // =============================================================================
744
+ // Start Server — read from stdin, write to stdout
745
+ // =============================================================================
746
+
747
+ function startServer() {
748
+ log('Starting MCP server...');
749
+
750
+ // Read newline-delimited JSON from stdin
751
+ const rl = readline.createInterface({
752
+ input: process.stdin,
753
+ terminal: false,
754
+ });
755
+
756
+ rl.on('line', async (line) => {
757
+ if (!line.trim()) return;
758
+
759
+ let message;
760
+ try {
761
+ message = JSON.parse(line);
762
+ } catch (e) {
763
+ log(`Failed to parse message: ${e.message}`);
764
+ // Send parse error if we can extract an id
765
+ process.stdout.write(JSON.stringify({
766
+ jsonrpc: '2.0',
767
+ id: null,
768
+ error: { code: -32700, message: 'Parse error' },
769
+ }) + '\n');
770
+ return;
771
+ }
772
+
773
+ // Validate JSON-RPC 2.0 format
774
+ if (message.jsonrpc !== '2.0') {
775
+ log(`Invalid JSON-RPC version: ${message.jsonrpc}`);
776
+ if (message.id !== undefined) {
777
+ sendError(message.id, -32600, 'Invalid Request: must use jsonrpc 2.0');
778
+ }
779
+ return;
780
+ }
781
+
782
+ await handleMessage(message);
783
+ });
784
+
785
+ rl.on('close', () => {
786
+ log('stdin closed. Shutting down...');
787
+ // Clean up — stop all live sessions
788
+ manager.stopAll();
789
+ process.exit(0);
790
+ });
791
+
792
+ // Handle process signals gracefully
793
+ process.on('SIGTERM', () => {
794
+ log('SIGTERM received. Shutting down...');
795
+ manager.stopAll();
796
+ process.exit(0);
797
+ });
798
+
799
+ process.on('SIGINT', () => {
800
+ log('SIGINT received. Shutting down...');
801
+ manager.stopAll();
802
+ process.exit(0);
803
+ });
804
+
805
+ log('MCP server ready. Waiting for messages...');
806
+ }
807
+
808
+ module.exports = { startServer, TOOLS, executeTool };