ai-cli-mcp 2.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.
Files changed (69) hide show
  1. package/.claude/settings.local.json +19 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/test.yml +43 -0
  4. package/.vscode/settings.json +3 -0
  5. package/AGENT.md +57 -0
  6. package/CHANGELOG.md +126 -0
  7. package/LICENSE +22 -0
  8. package/README.md +329 -0
  9. package/RELEASE.md +74 -0
  10. package/data/rooms/refactor-haiku-alias-main/messages.jsonl +5 -0
  11. package/data/rooms/refactor-haiku-alias-main/presence.json +20 -0
  12. package/data/rooms.json +10 -0
  13. package/dist/__tests__/e2e.test.js +238 -0
  14. package/dist/__tests__/edge-cases.test.js +135 -0
  15. package/dist/__tests__/error-cases.test.js +296 -0
  16. package/dist/__tests__/mocks.js +32 -0
  17. package/dist/__tests__/model-alias.test.js +36 -0
  18. package/dist/__tests__/process-management.test.js +632 -0
  19. package/dist/__tests__/server.test.js +665 -0
  20. package/dist/__tests__/setup.js +11 -0
  21. package/dist/__tests__/utils/claude-mock.js +80 -0
  22. package/dist/__tests__/utils/mcp-client.js +104 -0
  23. package/dist/__tests__/utils/persistent-mock.js +25 -0
  24. package/dist/__tests__/utils/test-helpers.js +11 -0
  25. package/dist/__tests__/validation.test.js +212 -0
  26. package/dist/__tests__/version-print.test.js +69 -0
  27. package/dist/parsers.js +54 -0
  28. package/dist/server.js +614 -0
  29. package/docs/RELEASE_CHECKLIST.md +26 -0
  30. package/docs/e2e-testing.md +148 -0
  31. package/docs/local_install.md +111 -0
  32. package/hello.txt +3 -0
  33. package/implementation-log.md +110 -0
  34. package/implementation-plan.md +189 -0
  35. package/investigation-report.md +135 -0
  36. package/package.json +53 -0
  37. package/print-eslint-config.js +3 -0
  38. package/quality-score.json +47 -0
  39. package/refactoring-requirements.md +25 -0
  40. package/review-report.md +132 -0
  41. package/scripts/check-version-log.sh +34 -0
  42. package/scripts/publish-release.sh +95 -0
  43. package/scripts/restore-config.sh +28 -0
  44. package/scripts/test-release.sh +69 -0
  45. package/src/__tests__/e2e.test.ts +290 -0
  46. package/src/__tests__/edge-cases.test.ts +181 -0
  47. package/src/__tests__/error-cases.test.ts +378 -0
  48. package/src/__tests__/mocks.ts +35 -0
  49. package/src/__tests__/model-alias.test.ts +44 -0
  50. package/src/__tests__/process-management.test.ts +772 -0
  51. package/src/__tests__/server.test.ts +851 -0
  52. package/src/__tests__/setup.ts +13 -0
  53. package/src/__tests__/utils/claude-mock.ts +87 -0
  54. package/src/__tests__/utils/mcp-client.ts +129 -0
  55. package/src/__tests__/utils/persistent-mock.ts +29 -0
  56. package/src/__tests__/utils/test-helpers.ts +13 -0
  57. package/src/__tests__/validation.test.ts +258 -0
  58. package/src/__tests__/version-print.test.ts +86 -0
  59. package/src/parsers.ts +55 -0
  60. package/src/server.ts +735 -0
  61. package/start.bat +9 -0
  62. package/start.sh +21 -0
  63. package/test-results.md +119 -0
  64. package/test-standalone.js +5877 -0
  65. package/tsconfig.json +16 -0
  66. package/vitest.config.e2e.ts +27 -0
  67. package/vitest.config.ts +22 -0
  68. package/vitest.config.unit.ts +29 -0
  69. package/xx.txt +1 -0
@@ -0,0 +1,851 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { resolve as pathResolve } from 'node:path';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { EventEmitter } from 'node:events';
9
+
10
+ // Mock dependencies
11
+ vi.mock('node:child_process');
12
+ vi.mock('node:fs');
13
+ vi.mock('node:os');
14
+ vi.mock('node:path', () => ({
15
+ resolve: vi.fn((path) => path),
16
+ join: vi.fn((...args) => args.join('/')),
17
+ isAbsolute: vi.fn((path) => path.startsWith('/'))
18
+ }));
19
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js');
20
+ vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
21
+ ListToolsRequestSchema: { name: 'listTools' },
22
+ CallToolRequestSchema: { name: 'callTool' },
23
+ ErrorCode: {
24
+ InternalError: 'InternalError',
25
+ MethodNotFound: 'MethodNotFound',
26
+ InvalidParams: 'InvalidParams'
27
+ },
28
+ McpError: vi.fn().mockImplementation((code, message) => {
29
+ const error = new Error(message);
30
+ (error as any).code = code;
31
+ return error;
32
+ })
33
+ }));
34
+ vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
35
+ Server: vi.fn().mockImplementation(function(this: any) {
36
+ this.setRequestHandler = vi.fn();
37
+ this.connect = vi.fn();
38
+ this.close = vi.fn();
39
+ this.onerror = undefined;
40
+ return this;
41
+ }),
42
+ }));
43
+
44
+ // Mock package.json
45
+ vi.mock('../../package.json', () => ({
46
+ default: { version: '1.0.0-test' }
47
+ }));
48
+
49
+ // Re-import after mocks
50
+ const mockExistsSync = vi.mocked(existsSync);
51
+ const mockSpawn = vi.mocked(spawn);
52
+ const mockHomedir = vi.mocked(homedir);
53
+ const mockPathResolve = vi.mocked(pathResolve);
54
+
55
+ // Module loading will happen in tests
56
+
57
+ describe('ClaudeCodeServer Unit Tests', () => {
58
+ let consoleErrorSpy: any;
59
+ let consoleWarnSpy: any;
60
+ let originalEnv: any;
61
+
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ vi.resetModules();
65
+ vi.unmock('../server.js');
66
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
67
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
68
+ originalEnv = { ...process.env };
69
+ // Reset env
70
+ process.env = { ...originalEnv };
71
+ });
72
+
73
+ afterEach(() => {
74
+ consoleErrorSpy.mockRestore();
75
+ consoleWarnSpy.mockRestore();
76
+ process.env = originalEnv;
77
+ });
78
+
79
+ describe('debugLog function', () => {
80
+ it('should log when debug mode is enabled', async () => {
81
+ process.env.MCP_CLAUDE_DEBUG = 'true';
82
+ const module = await import('../server.js');
83
+ // @ts-ignore - accessing private function for testing
84
+ const { debugLog } = module;
85
+
86
+ debugLog('Test message');
87
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Test message');
88
+ });
89
+
90
+ it('should not log when debug mode is disabled', async () => {
91
+ // Reset modules to clear cache
92
+ vi.resetModules();
93
+ consoleErrorSpy.mockClear();
94
+ process.env.MCP_CLAUDE_DEBUG = 'false';
95
+ const module = await import('../server.js');
96
+ // @ts-ignore
97
+ const { debugLog } = module;
98
+
99
+ debugLog('Test message');
100
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
101
+ });
102
+ });
103
+
104
+ describe('findClaudeCli function', () => {
105
+ it('should return local path when it exists', async () => {
106
+ mockHomedir.mockReturnValue('/home/user');
107
+ mockExistsSync.mockImplementation((path) => {
108
+ // Mock returns true for real CLI path
109
+ if (path === '/home/user/.claude/local/claude') return true;
110
+ return false;
111
+ });
112
+
113
+ const module = await import('../server.js');
114
+ // @ts-ignore
115
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
116
+
117
+ const result = findClaudeCli();
118
+ expect(result).toBe('/home/user/.claude/local/claude');
119
+ });
120
+
121
+ it('should fallback to PATH when local does not exist', async () => {
122
+ mockHomedir.mockReturnValue('/home/user');
123
+ mockExistsSync.mockReturnValue(false);
124
+
125
+ const module = await import('../server.js');
126
+ // @ts-ignore
127
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
128
+
129
+ const result = findClaudeCli();
130
+ expect(result).toBe('claude');
131
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
132
+ expect.stringContaining('Claude CLI not found at ~/.claude/local/claude')
133
+ );
134
+ });
135
+
136
+ it('should use custom name from CLAUDE_CLI_NAME', async () => {
137
+ process.env.CLAUDE_CLI_NAME = 'my-claude';
138
+ mockHomedir.mockReturnValue('/home/user');
139
+ mockExistsSync.mockReturnValue(false);
140
+
141
+ const module = await import('../server.js');
142
+ // @ts-ignore
143
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
144
+
145
+ const result = findClaudeCli();
146
+ expect(result).toBe('my-claude');
147
+ });
148
+
149
+ it('should use absolute path from CLAUDE_CLI_NAME', async () => {
150
+ process.env.CLAUDE_CLI_NAME = '/absolute/path/to/claude';
151
+
152
+ const module = await import('../server.js');
153
+ // @ts-ignore
154
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
155
+
156
+ const result = findClaudeCli();
157
+ expect(result).toBe('/absolute/path/to/claude');
158
+ });
159
+
160
+ it('should throw error for relative paths in CLAUDE_CLI_NAME', async () => {
161
+ process.env.CLAUDE_CLI_NAME = './relative/path/claude';
162
+
163
+ const module = await import('../server.js');
164
+ // @ts-ignore
165
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
166
+
167
+ expect(() => findClaudeCli()).toThrow('Invalid CLAUDE_CLI_NAME: Relative paths are not allowed');
168
+ });
169
+
170
+ it('should throw error for paths with ../ in CLAUDE_CLI_NAME', async () => {
171
+ process.env.CLAUDE_CLI_NAME = '../relative/path/claude';
172
+
173
+ const module = await import('../server.js');
174
+ // @ts-ignore
175
+ const findClaudeCli = module.default?.findClaudeCli || module.findClaudeCli;
176
+
177
+ expect(() => findClaudeCli()).toThrow('Invalid CLAUDE_CLI_NAME: Relative paths are not allowed');
178
+ });
179
+ });
180
+
181
+ describe('spawnAsync function', () => {
182
+ let mockProcess: any;
183
+
184
+ beforeEach(() => {
185
+ // Create a mock process
186
+ mockProcess = new EventEmitter() as any;
187
+ mockProcess.stdout = new EventEmitter();
188
+ mockProcess.stderr = new EventEmitter();
189
+ mockProcess.stdout.on = vi.fn((event, handler) => {
190
+ mockProcess.stdout[event] = handler;
191
+ });
192
+ mockProcess.stderr.on = vi.fn((event, handler) => {
193
+ mockProcess.stderr[event] = handler;
194
+ });
195
+ mockSpawn.mockReturnValue(mockProcess);
196
+ });
197
+
198
+ it('should execute command successfully', async () => {
199
+ const module = await import('../server.js');
200
+ // @ts-ignore
201
+ const { spawnAsync } = module;
202
+
203
+ // mockProcess is already defined in the outer scope
204
+
205
+ // Start the async operation
206
+ const promise = spawnAsync('echo', ['test']);
207
+
208
+ // Simulate successful execution
209
+ setTimeout(() => {
210
+ mockProcess.stdout['data']('test output');
211
+ mockProcess.stderr['data']('');
212
+ mockProcess.emit('close', 0);
213
+ }, 10);
214
+
215
+ const result = await promise;
216
+ expect(result).toEqual({
217
+ stdout: 'test output',
218
+ stderr: ''
219
+ });
220
+ });
221
+
222
+ it('should handle command failure', async () => {
223
+ const module = await import('../server.js');
224
+ // @ts-ignore
225
+ const { spawnAsync } = module;
226
+
227
+ // mockProcess is already defined in the outer scope
228
+
229
+ // Start the async operation
230
+ const promise = spawnAsync('false', []);
231
+
232
+ // Simulate failed execution
233
+ setTimeout(() => {
234
+ mockProcess.stderr['data']('error output');
235
+ mockProcess.emit('close', 1);
236
+ }, 10);
237
+
238
+ await expect(promise).rejects.toThrow('Command failed with exit code 1');
239
+ });
240
+
241
+ it('should handle spawn error', async () => {
242
+ const module = await import('../server.js');
243
+ // @ts-ignore
244
+ const { spawnAsync } = module;
245
+
246
+ // mockProcess is already defined in the outer scope
247
+
248
+ // Start the async operation
249
+ const promise = spawnAsync('nonexistent', []);
250
+
251
+ // Simulate spawn error
252
+ setTimeout(() => {
253
+ const error: any = new Error('spawn error');
254
+ error.code = 'ENOENT';
255
+ error.path = 'nonexistent';
256
+ error.syscall = 'spawn';
257
+ mockProcess.emit('error', error);
258
+ }, 10);
259
+
260
+ await expect(promise).rejects.toThrow('Spawn error');
261
+ });
262
+
263
+ it('should respect timeout option', async () => {
264
+ const module = await import('../server.js');
265
+ // @ts-ignore
266
+ const { spawnAsync } = module;
267
+
268
+ const result = spawnAsync('sleep', ['10'], { timeout: 100 });
269
+
270
+ expect(mockSpawn).toHaveBeenCalledWith('sleep', ['10'], expect.objectContaining({
271
+ timeout: 100
272
+ }));
273
+ });
274
+
275
+ it('should use provided cwd option', async () => {
276
+ const module = await import('../server.js');
277
+ // @ts-ignore
278
+ const { spawnAsync } = module;
279
+
280
+ const result = spawnAsync('ls', [], { cwd: '/tmp' });
281
+
282
+ expect(mockSpawn).toHaveBeenCalledWith('ls', [], expect.objectContaining({
283
+ cwd: '/tmp'
284
+ }));
285
+ });
286
+ });
287
+
288
+ describe('ClaudeCodeServer class', () => {
289
+ it('should initialize with correct settings', async () => {
290
+ mockHomedir.mockReturnValue('/home/user');
291
+ mockExistsSync.mockReturnValue(true);
292
+
293
+ // Set up Server mock before resetting modules
294
+ vi.mocked(Server).mockImplementation(function(this: any) {
295
+ this.setRequestHandler = vi.fn();
296
+ this.connect = vi.fn();
297
+ this.close = vi.fn();
298
+ this.onerror = undefined;
299
+ return this;
300
+ });
301
+
302
+ const module = await import('../server.js');
303
+ // @ts-ignore
304
+ const { ClaudeCodeServer } = module;
305
+
306
+ const server = new ClaudeCodeServer();
307
+
308
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
309
+ expect.stringContaining('[Setup] Using Claude CLI command/path:')
310
+ );
311
+ });
312
+
313
+ it('should set up tool handlers', async () => {
314
+ mockHomedir.mockReturnValue('/home/user');
315
+ mockExistsSync.mockReturnValue(true);
316
+
317
+ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
318
+ const mockSetRequestHandler = vi.fn();
319
+ vi.mocked(Server).mockImplementation(function(this: any) {
320
+ this.setRequestHandler = mockSetRequestHandler;
321
+ this.connect = vi.fn();
322
+ this.close = vi.fn();
323
+ this.onerror = undefined;
324
+ return this;
325
+ });
326
+
327
+ const module = await import('../server.js');
328
+ // @ts-ignore
329
+ const { ClaudeCodeServer } = module;
330
+
331
+ const server = new ClaudeCodeServer();
332
+
333
+ expect(mockSetRequestHandler).toHaveBeenCalled();
334
+ });
335
+
336
+ it('should set up error handler', async () => {
337
+ mockHomedir.mockReturnValue('/home/user');
338
+ mockExistsSync.mockReturnValue(true);
339
+
340
+ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
341
+ let errorHandler: any = null;
342
+ vi.mocked(Server).mockImplementation(function(this: any) {
343
+ this.setRequestHandler = vi.fn();
344
+ this.connect = vi.fn();
345
+ this.close = vi.fn();
346
+ Object.defineProperty(this, 'onerror', {
347
+ get() { return errorHandler; },
348
+ set(handler) { errorHandler = handler; },
349
+ enumerable: true,
350
+ configurable: true
351
+ });
352
+ return this;
353
+ });
354
+
355
+ const module = await import('../server.js');
356
+ // @ts-ignore
357
+ const { ClaudeCodeServer } = module;
358
+
359
+ const server = new ClaudeCodeServer();
360
+
361
+ // Test error handler
362
+ errorHandler(new Error('Test error'));
363
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[Error]', expect.any(Error));
364
+ });
365
+
366
+ it('should handle SIGINT', async () => {
367
+ mockHomedir.mockReturnValue('/home/user');
368
+ mockExistsSync.mockReturnValue(true);
369
+
370
+ // Set up Server mock first
371
+ vi.mocked(Server).mockImplementation(function(this: any) {
372
+ this.setRequestHandler = vi.fn();
373
+ this.connect = vi.fn();
374
+ this.close = vi.fn();
375
+ this.onerror = undefined;
376
+ return this;
377
+ });
378
+
379
+ const module = await import('../server.js');
380
+ // @ts-ignore
381
+ const { ClaudeCodeServer } = module;
382
+
383
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
384
+ const server = new ClaudeCodeServer();
385
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
386
+
387
+ // Emit SIGINT
388
+ const sigintHandler = process.listeners('SIGINT').slice(-1)[0] as any;
389
+ await sigintHandler();
390
+
391
+ expect(mockServerInstance.close).toHaveBeenCalled();
392
+ expect(exitSpy).toHaveBeenCalledWith(0);
393
+
394
+ exitSpy.mockRestore();
395
+ });
396
+ });
397
+
398
+ describe('Tool handler implementation', () => {
399
+ // Define setupServerMock for this describe block
400
+ let errorHandler: any = null;
401
+ function setupServerMock() {
402
+ errorHandler = null;
403
+ vi.mocked(Server).mockImplementation(function(this: any) {
404
+ this.setRequestHandler = vi.fn();
405
+ this.connect = vi.fn();
406
+ this.close = vi.fn();
407
+ Object.defineProperty(this, 'onerror', {
408
+ get() { return errorHandler; },
409
+ set(handler) { errorHandler = handler; },
410
+ enumerable: true,
411
+ configurable: true
412
+ });
413
+ return this;
414
+ });
415
+ }
416
+
417
+ it('should handle ListToolsRequest', async () => {
418
+ mockHomedir.mockReturnValue('/home/user');
419
+ mockExistsSync.mockReturnValue(true);
420
+
421
+ // Use the setupServerMock function from the beginning of the file
422
+ setupServerMock();
423
+
424
+ const module = await import('../server.js');
425
+ // @ts-ignore
426
+ const { ClaudeCodeServer } = module;
427
+
428
+ const server = new ClaudeCodeServer();
429
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
430
+
431
+ // Find the ListToolsRequest handler
432
+ const listToolsCall = mockServerInstance.setRequestHandler.mock.calls.find(
433
+ (call: any[]) => call[0].name === 'listTools'
434
+ );
435
+
436
+ expect(listToolsCall).toBeDefined();
437
+
438
+ // Test the handler
439
+ const handler = listToolsCall[1];
440
+ const result = await handler();
441
+
442
+ expect(result.tools).toHaveLength(4);
443
+ expect(result.tools[0].name).toBe('claude_code');
444
+ expect(result.tools[0].description).toContain('Claude Code Agent');
445
+ expect(result.tools[1].name).toBe('list_claude_processes');
446
+ expect(result.tools[2].name).toBe('get_claude_result');
447
+ expect(result.tools[3].name).toBe('kill_claude_process');
448
+ });
449
+
450
+ it('should handle CallToolRequest', async () => {
451
+ mockHomedir.mockReturnValue('/home/user');
452
+ mockExistsSync.mockReturnValue(true);
453
+
454
+ // Set up Server mock
455
+ setupServerMock();
456
+
457
+ const module = await import('../server.js');
458
+ // @ts-ignore
459
+ const { ClaudeCodeServer } = module;
460
+
461
+ const server = new ClaudeCodeServer();
462
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
463
+
464
+ // Find the CallToolRequest handler
465
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
466
+ (call: any[]) => call[0].name === 'callTool'
467
+ );
468
+
469
+ expect(callToolCall).toBeDefined();
470
+
471
+ // Create a mock process for the tool execution
472
+ const mockProcess = new EventEmitter() as any;
473
+ mockProcess.pid = 12345;
474
+ mockProcess.stdout = new EventEmitter();
475
+ mockProcess.stderr = new EventEmitter();
476
+ mockProcess.stdout.on = vi.fn();
477
+ mockProcess.stderr.on = vi.fn();
478
+ mockProcess.kill = vi.fn();
479
+
480
+ mockSpawn.mockReturnValue(mockProcess);
481
+
482
+ // Test the handler
483
+ const handler = callToolCall[1];
484
+ const result = await handler({
485
+ params: {
486
+ name: 'claude_code',
487
+ arguments: {
488
+ prompt: 'test prompt',
489
+ workFolder: '/tmp'
490
+ }
491
+ }
492
+ });
493
+
494
+ // claude_code now returns PID immediately
495
+ expect(result.content[0].type).toBe('text');
496
+ const response = JSON.parse(result.content[0].text);
497
+ expect(response.pid).toBe(12345);
498
+ expect(response.status).toBe('started');
499
+ });
500
+
501
+ it('should require workFolder parameter', async () => {
502
+ mockHomedir.mockReturnValue('/home/user');
503
+ mockExistsSync.mockReturnValue(true);
504
+
505
+ // Set up Server mock
506
+ setupServerMock();
507
+
508
+ const module = await import('../server.js');
509
+ // @ts-ignore
510
+ const { ClaudeCodeServer } = module;
511
+ const server = new ClaudeCodeServer();
512
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
513
+
514
+ // Find the CallToolRequest handler
515
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
516
+ (call: any[]) => call[0].name === 'callTool'
517
+ );
518
+
519
+ const handler = callToolCall[1];
520
+
521
+ // Test missing workFolder
522
+ await expect(handler({
523
+ params: {
524
+ name: 'claude_code',
525
+ arguments: {
526
+ prompt: 'test'
527
+ }
528
+ }
529
+ })).rejects.toThrow('Missing or invalid required parameter: workFolder');
530
+ });
531
+
532
+ it('should handle non-existent workFolder', async () => {
533
+ mockHomedir.mockReturnValue('/home/user');
534
+ mockExistsSync.mockImplementation((path) => {
535
+ // Make the CLI path exist but the workFolder not exist
536
+ if (String(path).includes('.claude')) return true;
537
+ if (path === '/nonexistent') return false;
538
+ return false;
539
+ });
540
+
541
+ // Set up Server mock
542
+ setupServerMock();
543
+
544
+ const module = await import('../server.js');
545
+ // @ts-ignore
546
+ const { ClaudeCodeServer } = module;
547
+ const server = new ClaudeCodeServer();
548
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
549
+
550
+ // Find the CallToolRequest handler
551
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
552
+ (call: any[]) => call[0].name === 'callTool'
553
+ );
554
+
555
+ const handler = callToolCall[1];
556
+
557
+ // Should throw error for non-existent workFolder
558
+ await expect(
559
+ handler({
560
+ params: {
561
+ name: 'claude_code',
562
+ arguments: {
563
+ prompt: 'test',
564
+ workFolder: '/nonexistent'
565
+ }
566
+ }
567
+ })
568
+ ).rejects.toThrow('Working folder does not exist');
569
+ });
570
+
571
+ it('should handle session_id parameter', async () => {
572
+ mockHomedir.mockReturnValue('/home/user');
573
+ mockExistsSync.mockReturnValue(true);
574
+
575
+ // Set up Server mock
576
+ setupServerMock();
577
+
578
+ const module = await import('../server.js');
579
+ // @ts-ignore
580
+ const { ClaudeCodeServer } = module;
581
+ const server = new ClaudeCodeServer();
582
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
583
+
584
+ // Find the CallToolRequest handler
585
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
586
+ (call: any[]) => call[0].name === 'callTool'
587
+ );
588
+
589
+ const handler = callToolCall[1];
590
+
591
+ // Create mock process
592
+ const mockProcess = new EventEmitter() as any;
593
+ mockProcess.pid = 12347;
594
+ mockProcess.stdout = new EventEmitter();
595
+ mockProcess.stderr = new EventEmitter();
596
+ mockProcess.stdout.on = vi.fn();
597
+ mockProcess.stderr.on = vi.fn();
598
+ mockProcess.kill = vi.fn();
599
+ mockSpawn.mockReturnValue(mockProcess);
600
+
601
+ const result = await handler({
602
+ params: {
603
+ name: 'claude_code',
604
+ arguments: {
605
+ prompt: 'test prompt',
606
+ workFolder: '/tmp',
607
+ session_id: 'test-session-123'
608
+ }
609
+ }
610
+ });
611
+
612
+ // Verify spawn was called with -r flag
613
+ expect(mockSpawn).toHaveBeenCalledWith(
614
+ expect.any(String),
615
+ expect.arrayContaining(['-r', 'test-session-123', '-p', 'test prompt']),
616
+ expect.any(Object)
617
+ );
618
+ });
619
+
620
+ it('should handle prompt_file parameter', async () => {
621
+ mockHomedir.mockReturnValue('/home/user');
622
+ mockExistsSync.mockImplementation((path) => {
623
+ if (String(path).includes('.claude')) return true;
624
+ if (path === '/tmp') return true;
625
+ if (path === '/tmp/prompt.txt') return true;
626
+ return false;
627
+ });
628
+
629
+ // Mock readFileSync
630
+ const readFileSyncMock = vi.fn().mockReturnValue('Content from file');
631
+ vi.doMock('node:fs', () => ({
632
+ existsSync: mockExistsSync,
633
+ readFileSync: readFileSyncMock
634
+ }));
635
+
636
+ // Set up Server mock
637
+ setupServerMock();
638
+
639
+ const module = await import('../server.js');
640
+ // @ts-ignore
641
+ const { ClaudeCodeServer } = module;
642
+ const server = new ClaudeCodeServer();
643
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
644
+
645
+ // Find the CallToolRequest handler
646
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
647
+ (call: any[]) => call[0].name === 'callTool'
648
+ );
649
+
650
+ const handler = callToolCall[1];
651
+
652
+ // Create mock process
653
+ const mockProcess = new EventEmitter() as any;
654
+ mockProcess.pid = 12348;
655
+ mockProcess.stdout = new EventEmitter();
656
+ mockProcess.stderr = new EventEmitter();
657
+ mockProcess.stdout.on = vi.fn();
658
+ mockProcess.stderr.on = vi.fn();
659
+ mockProcess.kill = vi.fn();
660
+ mockSpawn.mockReturnValue(mockProcess);
661
+
662
+ const result = await handler({
663
+ params: {
664
+ name: 'claude_code',
665
+ arguments: {
666
+ prompt_file: '/tmp/prompt.txt',
667
+ workFolder: '/tmp'
668
+ }
669
+ }
670
+ });
671
+
672
+ // Verify file was read and spawn was called with content
673
+ expect(mockSpawn).toHaveBeenCalledWith(
674
+ expect.any(String),
675
+ expect.arrayContaining(['-p', 'Content from file']),
676
+ expect.any(Object)
677
+ );
678
+ });
679
+
680
+ it('should resolve model aliases when calling claude_code tool', async () => {
681
+ mockHomedir.mockReturnValue('/home/user');
682
+ mockExistsSync.mockReturnValue(true);
683
+
684
+ // Set up spawn mock to return a process
685
+ const mockProcess = new EventEmitter() as any;
686
+ mockProcess.stdout = new EventEmitter();
687
+ mockProcess.stderr = new EventEmitter();
688
+ mockProcess.pid = 12345;
689
+ mockSpawn.mockReturnValue(mockProcess);
690
+
691
+ // Set up Server mock
692
+ setupServerMock();
693
+
694
+ const module = await import('../server.js');
695
+ // @ts-ignore
696
+ const { ClaudeCodeServer } = module;
697
+ const server = new ClaudeCodeServer();
698
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
699
+
700
+ // Find the CallToolRequest handler
701
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
702
+ (call: any[]) => call[0].name === 'callTool'
703
+ );
704
+
705
+ const handler = callToolCall[1];
706
+
707
+ // Test with haiku alias
708
+ const result = await handler({
709
+ params: {
710
+ name: 'claude_code',
711
+ arguments: {
712
+ prompt: 'test prompt',
713
+ workFolder: '/tmp',
714
+ model: 'haiku'
715
+ }
716
+ }
717
+ });
718
+
719
+ // Verify spawn was called with resolved model name
720
+ expect(mockSpawn).toHaveBeenCalledWith(
721
+ expect.any(String),
722
+ expect.arrayContaining(['--model', 'claude-3-5-haiku-20241022']),
723
+ expect.any(Object)
724
+ );
725
+
726
+ // Verify PID is returned
727
+ expect(result.content[0].text).toContain('"pid": 12345');
728
+ });
729
+
730
+ it('should pass non-alias model names unchanged', async () => {
731
+ mockHomedir.mockReturnValue('/home/user');
732
+ mockExistsSync.mockReturnValue(true);
733
+
734
+ // Set up spawn mock to return a process
735
+ const mockProcess = new EventEmitter() as any;
736
+ mockProcess.stdout = new EventEmitter();
737
+ mockProcess.stderr = new EventEmitter();
738
+ mockProcess.pid = 12346;
739
+ mockSpawn.mockReturnValue(mockProcess);
740
+
741
+ // Set up Server mock
742
+ setupServerMock();
743
+
744
+ const module = await import('../server.js');
745
+ // @ts-ignore
746
+ const { ClaudeCodeServer } = module;
747
+ const server = new ClaudeCodeServer();
748
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
749
+
750
+ // Find the CallToolRequest handler
751
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
752
+ (call: any[]) => call[0].name === 'callTool'
753
+ );
754
+
755
+ const handler = callToolCall[1];
756
+
757
+ // Test with non-alias model name
758
+ const result = await handler({
759
+ params: {
760
+ name: 'claude_code',
761
+ arguments: {
762
+ prompt: 'test prompt',
763
+ workFolder: '/tmp',
764
+ model: 'sonnet'
765
+ }
766
+ }
767
+ });
768
+
769
+ // Verify spawn was called with unchanged model name
770
+ expect(mockSpawn).toHaveBeenCalledWith(
771
+ expect.any(String),
772
+ expect.arrayContaining(['--model', 'sonnet']),
773
+ expect.any(Object)
774
+ );
775
+ });
776
+
777
+ it('should reject when both prompt and prompt_file are provided', async () => {
778
+ mockHomedir.mockReturnValue('/home/user');
779
+ mockExistsSync.mockReturnValue(true);
780
+
781
+ // Set up Server mock
782
+ setupServerMock();
783
+
784
+ const module = await import('../server.js');
785
+ // @ts-ignore
786
+ const { ClaudeCodeServer } = module;
787
+ const server = new ClaudeCodeServer();
788
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
789
+
790
+ // Find the CallToolRequest handler
791
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
792
+ (call: any[]) => call[0].name === 'callTool'
793
+ );
794
+
795
+ const handler = callToolCall[1];
796
+
797
+ // Test both parameters provided
798
+ try {
799
+ await handler({
800
+ params: {
801
+ name: 'claude_code',
802
+ arguments: {
803
+ prompt: 'test prompt',
804
+ prompt_file: '/tmp/prompt.txt',
805
+ workFolder: '/tmp'
806
+ }
807
+ }
808
+ });
809
+ expect.fail('Should have thrown an error');
810
+ } catch (error: any) {
811
+ expect(error.message).toContain('Cannot specify both prompt and prompt_file');
812
+ }
813
+ });
814
+
815
+ it('should reject when neither prompt nor prompt_file are provided', async () => {
816
+ mockHomedir.mockReturnValue('/home/user');
817
+ mockExistsSync.mockReturnValue(true);
818
+
819
+ // Set up Server mock
820
+ setupServerMock();
821
+
822
+ const module = await import('../server.js');
823
+ // @ts-ignore
824
+ const { ClaudeCodeServer } = module;
825
+ const server = new ClaudeCodeServer();
826
+ const mockServerInstance = vi.mocked(Server).mock.results[0].value;
827
+
828
+ // Find the CallToolRequest handler
829
+ const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find(
830
+ (call: any[]) => call[0].name === 'callTool'
831
+ );
832
+
833
+ const handler = callToolCall[1];
834
+
835
+ // Test neither parameter provided
836
+ try {
837
+ await handler({
838
+ params: {
839
+ name: 'claude_code',
840
+ arguments: {
841
+ workFolder: '/tmp'
842
+ }
843
+ }
844
+ });
845
+ expect.fail('Should have thrown an error');
846
+ } catch (error: any) {
847
+ expect(error.message).toContain('Either prompt or prompt_file must be provided');
848
+ }
849
+ });
850
+ });
851
+ });