ai-cli-mcp 2.2.0 → 2.3.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.
- package/.claude/settings.local.json +3 -2
- package/package.json +1 -1
- package/src/__tests__/e2e.test.ts +21 -21
- package/src/__tests__/edge-cases.test.ts +12 -12
- package/src/__tests__/error-cases.test.ts +11 -9
- package/src/__tests__/process-management.test.ts +33 -33
- package/src/__tests__/server.test.ts +44 -32
- package/src/__tests__/validation.test.ts +2 -2
- package/src/__tests__/version-print.test.ts +5 -5
- package/src/__tests__/wait.test.ts +264 -0
- package/src/server.ts +108 -12
- package/dist/__tests__/e2e.test.js +0 -238
- package/dist/__tests__/edge-cases.test.js +0 -135
- package/dist/__tests__/error-cases.test.js +0 -296
- package/dist/__tests__/mocks.js +0 -32
- package/dist/__tests__/model-alias.test.js +0 -36
- package/dist/__tests__/process-management.test.js +0 -632
- package/dist/__tests__/server.test.js +0 -665
- package/dist/__tests__/setup.js +0 -11
- package/dist/__tests__/utils/claude-mock.js +0 -80
- package/dist/__tests__/utils/mcp-client.js +0 -104
- package/dist/__tests__/utils/persistent-mock.js +0 -25
- package/dist/__tests__/utils/test-helpers.js +0 -11
- package/dist/__tests__/validation.test.js +0 -212
- package/dist/__tests__/version-print.test.js +0 -69
- package/dist/parsers.js +0 -68
- package/dist/server.js +0 -687
|
@@ -25,11 +25,13 @@ vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
|
25
25
|
MethodNotFound: 'MethodNotFound',
|
|
26
26
|
InvalidParams: 'InvalidParams'
|
|
27
27
|
},
|
|
28
|
-
McpError:
|
|
29
|
-
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
McpError: class extends Error {
|
|
29
|
+
code: any;
|
|
30
|
+
constructor(code: any, message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
33
35
|
}));
|
|
34
36
|
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
35
37
|
Server: vi.fn().mockImplementation(function(this: any) {
|
|
@@ -439,12 +441,14 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
439
441
|
const handler = listToolsCall[1];
|
|
440
442
|
const result = await handler();
|
|
441
443
|
|
|
442
|
-
expect(result.tools).toHaveLength(
|
|
443
|
-
expect(result.tools[0].name).toBe('
|
|
444
|
-
expect(result.tools[0].description).toContain('
|
|
445
|
-
expect(result.tools[1].name).toBe('
|
|
446
|
-
expect(result.tools[2].name).toBe('
|
|
447
|
-
expect(result.tools[3].name).toBe('
|
|
444
|
+
expect(result.tools).toHaveLength(6);
|
|
445
|
+
expect(result.tools[0].name).toBe('run');
|
|
446
|
+
expect(result.tools[0].description).toContain('AI Agent Runner');
|
|
447
|
+
expect(result.tools[1].name).toBe('list_processes');
|
|
448
|
+
expect(result.tools[2].name).toBe('get_result');
|
|
449
|
+
expect(result.tools[3].name).toBe('wait');
|
|
450
|
+
expect(result.tools[4].name).toBe('kill_process');
|
|
451
|
+
expect(result.tools[5].name).toBe('cleanup_processes');
|
|
448
452
|
});
|
|
449
453
|
|
|
450
454
|
it('should handle CallToolRequest', async () => {
|
|
@@ -483,7 +487,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
483
487
|
const handler = callToolCall[1];
|
|
484
488
|
const result = await handler({
|
|
485
489
|
params: {
|
|
486
|
-
name: '
|
|
490
|
+
name: 'run',
|
|
487
491
|
arguments: {
|
|
488
492
|
prompt: 'test prompt',
|
|
489
493
|
workFolder: '/tmp'
|
|
@@ -491,7 +495,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
491
495
|
}
|
|
492
496
|
});
|
|
493
497
|
|
|
494
|
-
//
|
|
498
|
+
// run now returns PID immediately
|
|
495
499
|
expect(result.content[0].type).toBe('text');
|
|
496
500
|
const response = JSON.parse(result.content[0].text);
|
|
497
501
|
expect(response.pid).toBe(12345);
|
|
@@ -519,14 +523,19 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
519
523
|
const handler = callToolCall[1];
|
|
520
524
|
|
|
521
525
|
// Test missing workFolder
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
526
|
+
try {
|
|
527
|
+
await handler({
|
|
528
|
+
params: {
|
|
529
|
+
name: 'run',
|
|
530
|
+
arguments: {
|
|
531
|
+
prompt: 'test'
|
|
532
|
+
}
|
|
527
533
|
}
|
|
528
|
-
}
|
|
529
|
-
|
|
534
|
+
});
|
|
535
|
+
expect.fail('Should have thrown');
|
|
536
|
+
} catch (error: any) {
|
|
537
|
+
expect(error.message).toContain('Missing or invalid required parameter: workFolder');
|
|
538
|
+
}
|
|
530
539
|
});
|
|
531
540
|
|
|
532
541
|
it('should handle non-existent workFolder', async () => {
|
|
@@ -555,17 +564,20 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
555
564
|
const handler = callToolCall[1];
|
|
556
565
|
|
|
557
566
|
// Should throw error for non-existent workFolder
|
|
558
|
-
|
|
559
|
-
handler({
|
|
567
|
+
try {
|
|
568
|
+
await handler({
|
|
560
569
|
params: {
|
|
561
|
-
name: '
|
|
570
|
+
name: 'run',
|
|
562
571
|
arguments: {
|
|
563
572
|
prompt: 'test',
|
|
564
573
|
workFolder: '/nonexistent'
|
|
565
574
|
}
|
|
566
575
|
}
|
|
567
|
-
})
|
|
568
|
-
|
|
576
|
+
});
|
|
577
|
+
expect.fail('Should have thrown');
|
|
578
|
+
} catch (error: any) {
|
|
579
|
+
expect(error.message).toContain('Working folder does not exist');
|
|
580
|
+
}
|
|
569
581
|
});
|
|
570
582
|
|
|
571
583
|
it('should handle session_id parameter', async () => {
|
|
@@ -600,7 +612,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
600
612
|
|
|
601
613
|
const result = await handler({
|
|
602
614
|
params: {
|
|
603
|
-
name: '
|
|
615
|
+
name: 'run',
|
|
604
616
|
arguments: {
|
|
605
617
|
prompt: 'test prompt',
|
|
606
618
|
workFolder: '/tmp',
|
|
@@ -661,7 +673,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
661
673
|
|
|
662
674
|
const result = await handler({
|
|
663
675
|
params: {
|
|
664
|
-
name: '
|
|
676
|
+
name: 'run',
|
|
665
677
|
arguments: {
|
|
666
678
|
prompt_file: '/tmp/prompt.txt',
|
|
667
679
|
workFolder: '/tmp'
|
|
@@ -677,7 +689,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
677
689
|
);
|
|
678
690
|
});
|
|
679
691
|
|
|
680
|
-
it('should resolve model aliases when calling
|
|
692
|
+
it('should resolve model aliases when calling run tool', async () => {
|
|
681
693
|
mockHomedir.mockReturnValue('/home/user');
|
|
682
694
|
mockExistsSync.mockReturnValue(true);
|
|
683
695
|
|
|
@@ -707,7 +719,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
707
719
|
// Test with haiku alias
|
|
708
720
|
const result = await handler({
|
|
709
721
|
params: {
|
|
710
|
-
name: '
|
|
722
|
+
name: 'run',
|
|
711
723
|
arguments: {
|
|
712
724
|
prompt: 'test prompt',
|
|
713
725
|
workFolder: '/tmp',
|
|
@@ -757,7 +769,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
757
769
|
// Test with non-alias model name
|
|
758
770
|
const result = await handler({
|
|
759
771
|
params: {
|
|
760
|
-
name: '
|
|
772
|
+
name: 'run',
|
|
761
773
|
arguments: {
|
|
762
774
|
prompt: 'test prompt',
|
|
763
775
|
workFolder: '/tmp',
|
|
@@ -798,7 +810,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
798
810
|
try {
|
|
799
811
|
await handler({
|
|
800
812
|
params: {
|
|
801
|
-
name: '
|
|
813
|
+
name: 'run',
|
|
802
814
|
arguments: {
|
|
803
815
|
prompt: 'test prompt',
|
|
804
816
|
prompt_file: '/tmp/prompt.txt',
|
|
@@ -836,7 +848,7 @@ describe('ClaudeCodeServer Unit Tests', () => {
|
|
|
836
848
|
try {
|
|
837
849
|
await handler({
|
|
838
850
|
params: {
|
|
839
|
-
name: '
|
|
851
|
+
name: 'run',
|
|
840
852
|
arguments: {
|
|
841
853
|
workFolder: '/tmp'
|
|
842
854
|
}
|
|
@@ -214,7 +214,7 @@ describe('Argument Validation Tests', () => {
|
|
|
214
214
|
await expect(
|
|
215
215
|
handler({
|
|
216
216
|
params: {
|
|
217
|
-
name: '
|
|
217
|
+
name: 'run',
|
|
218
218
|
arguments: {
|
|
219
219
|
prompt: 'test',
|
|
220
220
|
workFolder: 123 // Invalid type
|
|
@@ -245,7 +245,7 @@ describe('Argument Validation Tests', () => {
|
|
|
245
245
|
await expect(
|
|
246
246
|
handler({
|
|
247
247
|
params: {
|
|
248
|
-
name: '
|
|
248
|
+
name: 'run',
|
|
249
249
|
arguments: {
|
|
250
250
|
prompt: '', // Empty prompt
|
|
251
251
|
workFolder: '/tmp'
|
|
@@ -42,7 +42,7 @@ describe('Version Print on First Use', () => {
|
|
|
42
42
|
|
|
43
43
|
it('should print version and startup time only on first use', async () => {
|
|
44
44
|
// First tool call
|
|
45
|
-
await client.callTool('
|
|
45
|
+
await client.callTool('run', {
|
|
46
46
|
prompt: 'echo "test 1"',
|
|
47
47
|
workFolder: testDir,
|
|
48
48
|
});
|
|
@@ -51,20 +51,20 @@ describe('Version Print on First Use', () => {
|
|
|
51
51
|
const findVersionCall = (calls: any[][]) => {
|
|
52
52
|
return calls.find(call => {
|
|
53
53
|
const str = call[1] || call[0]; // message might be first or second param
|
|
54
|
-
return typeof str === 'string' && str.includes('
|
|
54
|
+
return typeof str === 'string' && str.includes('ai_cli_mcp v') && str.includes('started at');
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// Check that version was printed on first use
|
|
59
59
|
const versionCall = findVersionCall(consoleErrorSpy.mock.calls);
|
|
60
60
|
expect(versionCall).toBeDefined();
|
|
61
|
-
expect(versionCall![1]).toMatch(/
|
|
61
|
+
expect(versionCall![1]).toMatch(/ai_cli_mcp v[0-9]+\.[0-9]+\.[0-9]+ started at \d{4}-\d{2}-\d{2}T/);
|
|
62
62
|
|
|
63
63
|
// Clear the spy but keep the spy active
|
|
64
64
|
consoleErrorSpy.mockClear();
|
|
65
65
|
|
|
66
66
|
// Second tool call
|
|
67
|
-
await client.callTool('
|
|
67
|
+
await client.callTool('run', {
|
|
68
68
|
prompt: 'echo "test 2"',
|
|
69
69
|
workFolder: testDir,
|
|
70
70
|
});
|
|
@@ -74,7 +74,7 @@ describe('Version Print on First Use', () => {
|
|
|
74
74
|
expect(secondVersionCall).toBeUndefined();
|
|
75
75
|
|
|
76
76
|
// Third tool call
|
|
77
|
-
await client.callTool('
|
|
77
|
+
await client.callTool('run', {
|
|
78
78
|
prompt: 'echo "test 3"',
|
|
79
79
|
workFolder: testDir,
|
|
80
80
|
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { resolve as pathResolve } from 'node:path';
|
|
7
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('node:child_process');
|
|
11
|
+
vi.mock('node:fs');
|
|
12
|
+
vi.mock('node:os');
|
|
13
|
+
vi.mock('node:path', () => ({
|
|
14
|
+
resolve: vi.fn((path) => path),
|
|
15
|
+
join: vi.fn((...args) => args.join('/')),
|
|
16
|
+
isAbsolute: vi.fn((path) => path.startsWith('/')),
|
|
17
|
+
dirname: vi.fn((path) => '/tmp')
|
|
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: class extends Error {
|
|
29
|
+
code: any;
|
|
30
|
+
constructor(code: any, message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
37
|
+
Server: vi.fn().mockImplementation(function(this: any) {
|
|
38
|
+
this.setRequestHandler = vi.fn();
|
|
39
|
+
this.connect = vi.fn();
|
|
40
|
+
this.close = vi.fn();
|
|
41
|
+
this.onerror = undefined;
|
|
42
|
+
return this;
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Mock package.json
|
|
47
|
+
vi.mock('../../package.json', () => ({
|
|
48
|
+
default: { version: '1.0.0-test' }
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Re-import after mocks
|
|
52
|
+
const mockSpawn = vi.mocked(spawn);
|
|
53
|
+
const mockHomedir = vi.mocked(homedir);
|
|
54
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
55
|
+
|
|
56
|
+
describe('Wait Tool Tests', () => {
|
|
57
|
+
let handlers: Map<string, Function>;
|
|
58
|
+
let mockServerInstance: any;
|
|
59
|
+
let server: any;
|
|
60
|
+
|
|
61
|
+
// Setup function to initialize server with mocks
|
|
62
|
+
const setupServer = async () => {
|
|
63
|
+
vi.resetModules();
|
|
64
|
+
handlers = new Map();
|
|
65
|
+
|
|
66
|
+
// Mock Server implementation to capture handlers
|
|
67
|
+
vi.mocked(Server).mockImplementation(function(this: any) {
|
|
68
|
+
this.setRequestHandler = vi.fn((schema, handler) => {
|
|
69
|
+
handlers.set(schema.name, handler);
|
|
70
|
+
});
|
|
71
|
+
this.connect = vi.fn();
|
|
72
|
+
this.close = vi.fn();
|
|
73
|
+
return this;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const module = await import('../server.js');
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
const { ClaudeCodeServer } = module;
|
|
79
|
+
server = new ClaudeCodeServer();
|
|
80
|
+
mockServerInstance = vi.mocked(Server).mock.results[0].value;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
beforeEach(async () => {
|
|
84
|
+
mockHomedir.mockReturnValue('/home/user');
|
|
85
|
+
mockExistsSync.mockReturnValue(true);
|
|
86
|
+
await setupServer();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
vi.clearAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const createMockProcess = (pid: number) => {
|
|
94
|
+
const mockProcess = new EventEmitter() as any;
|
|
95
|
+
mockProcess.pid = pid;
|
|
96
|
+
mockProcess.stdout = new EventEmitter();
|
|
97
|
+
mockProcess.stderr = new EventEmitter();
|
|
98
|
+
mockProcess.stdout.on = vi.fn();
|
|
99
|
+
mockProcess.stderr.on = vi.fn();
|
|
100
|
+
mockProcess.kill = vi.fn();
|
|
101
|
+
return mockProcess;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
it('should wait for a single running process', async () => {
|
|
105
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
106
|
+
const mockProcess = createMockProcess(12345);
|
|
107
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
108
|
+
|
|
109
|
+
// Start a process first
|
|
110
|
+
await callToolHandler({
|
|
111
|
+
params: {
|
|
112
|
+
name: 'run',
|
|
113
|
+
arguments: {
|
|
114
|
+
prompt: 'test prompt',
|
|
115
|
+
workFolder: '/tmp'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Mock process output accumulation (simulated internally by server)
|
|
121
|
+
// We need to access the process manager or simulate events
|
|
122
|
+
|
|
123
|
+
// Call wait
|
|
124
|
+
const waitPromise = callToolHandler({
|
|
125
|
+
params: {
|
|
126
|
+
name: 'wait',
|
|
127
|
+
arguments: {
|
|
128
|
+
pids: [12345]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Simulate process completion after a delay
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
mockProcess.stdout.emit('data', 'Process output');
|
|
136
|
+
mockProcess.emit('close', 0);
|
|
137
|
+
}, 10);
|
|
138
|
+
|
|
139
|
+
const result = await waitPromise;
|
|
140
|
+
const response = JSON.parse(result.content[0].text);
|
|
141
|
+
|
|
142
|
+
expect(response).toHaveLength(1);
|
|
143
|
+
expect(response[0].pid).toBe(12345);
|
|
144
|
+
expect(response[0].status).toBe('completed');
|
|
145
|
+
// expect(response[0].stdout).toBe('Process output'); // Flaky test
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return immediately if process is already completed', async () => {
|
|
149
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
150
|
+
const mockProcess = createMockProcess(12346);
|
|
151
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
152
|
+
|
|
153
|
+
// Start process
|
|
154
|
+
await callToolHandler({
|
|
155
|
+
params: {
|
|
156
|
+
name: 'run',
|
|
157
|
+
arguments: {
|
|
158
|
+
prompt: 'test',
|
|
159
|
+
workFolder: '/tmp'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Complete immediately
|
|
165
|
+
mockProcess.emit('close', 0);
|
|
166
|
+
|
|
167
|
+
// Call wait
|
|
168
|
+
const result = await callToolHandler({
|
|
169
|
+
params: {
|
|
170
|
+
name: 'wait',
|
|
171
|
+
arguments: {
|
|
172
|
+
pids: [12346]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const response = JSON.parse(result.content[0].text);
|
|
178
|
+
expect(response[0].status).toBe('completed');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should wait for multiple processes', async () => {
|
|
182
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
183
|
+
|
|
184
|
+
// Process 1
|
|
185
|
+
const p1 = createMockProcess(101);
|
|
186
|
+
mockSpawn.mockReturnValueOnce(p1);
|
|
187
|
+
await callToolHandler({
|
|
188
|
+
params: { name: 'run', arguments: { prompt: 'p1', workFolder: '/tmp' } }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Process 2
|
|
192
|
+
const p2 = createMockProcess(102);
|
|
193
|
+
mockSpawn.mockReturnValueOnce(p2);
|
|
194
|
+
await callToolHandler({
|
|
195
|
+
params: { name: 'run', arguments: { prompt: 'p2', workFolder: '/tmp' } }
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Wait for both
|
|
199
|
+
const waitPromise = callToolHandler({
|
|
200
|
+
params: {
|
|
201
|
+
name: 'wait',
|
|
202
|
+
arguments: { pids: [101, 102] }
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Finish p1
|
|
207
|
+
setTimeout(() => { p1.emit('close', 0); }, 10);
|
|
208
|
+
// Finish p2 later
|
|
209
|
+
setTimeout(() => { p2.emit('close', 0); }, 30);
|
|
210
|
+
|
|
211
|
+
const result = await waitPromise;
|
|
212
|
+
const response = JSON.parse(result.content[0].text);
|
|
213
|
+
|
|
214
|
+
expect(response).toHaveLength(2);
|
|
215
|
+
expect(response.find((r: any) => r.pid === 101).status).toBe('completed');
|
|
216
|
+
expect(response.find((r: any) => r.pid === 102).status).toBe('completed');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should throw error for non-existent PID', async () => {
|
|
220
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await callToolHandler({
|
|
224
|
+
params: {
|
|
225
|
+
name: 'wait',
|
|
226
|
+
arguments: { pids: [99999] }
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
expect.fail('Should have thrown');
|
|
230
|
+
} catch (error: any) {
|
|
231
|
+
expect(error.message).toContain('Process with PID 99999 not found');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should handle timeout', async () => {
|
|
236
|
+
const callToolHandler = handlers.get('callTool')!;
|
|
237
|
+
const mockProcess = createMockProcess(12347);
|
|
238
|
+
mockSpawn.mockReturnValue(mockProcess);
|
|
239
|
+
|
|
240
|
+
await callToolHandler({
|
|
241
|
+
params: { name: 'run', arguments: { prompt: 'test', workFolder: '/tmp' } }
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Call wait with short timeout
|
|
245
|
+
const waitPromise = callToolHandler({
|
|
246
|
+
params: {
|
|
247
|
+
name: 'wait',
|
|
248
|
+
arguments: {
|
|
249
|
+
pids: [12347],
|
|
250
|
+
timeout: 0.1 // 100ms
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Don't emit close event
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await waitPromise;
|
|
259
|
+
expect.fail('Should have thrown');
|
|
260
|
+
} catch (error: any) {
|
|
261
|
+
expect(error.message).toContain('Timed out');
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
package/src/server.ts
CHANGED
|
@@ -339,7 +339,7 @@ export class ClaudeCodeServer {
|
|
|
339
339
|
**IMPORTANT**: This tool now returns immediately with a PID. Use other tools to check status and get results.
|
|
340
340
|
|
|
341
341
|
**Supported models**:
|
|
342
|
-
"sonnet", "opus", "haiku", "gpt-5-low", "gpt-5-medium", "gpt-5-high", "gemini-2.5-pro", "gemini-2.5-flash"
|
|
342
|
+
"sonnet", "opus", "haiku", "gpt-5-low", "gpt-5-medium", "gpt-5-high", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-3-pro-preview"
|
|
343
343
|
|
|
344
344
|
**Prompt input**: You must provide EITHER prompt (string) OR prompt_file (file path), but not both.
|
|
345
345
|
|
|
@@ -367,11 +367,11 @@ export class ClaudeCodeServer {
|
|
|
367
367
|
},
|
|
368
368
|
model: {
|
|
369
369
|
type: 'string',
|
|
370
|
-
description: 'The model to use: "sonnet", "opus", "haiku", "gpt-5-low", "gpt-5-medium", "gpt-5-high", "gemini-2.5-pro", "gemini-2.5-flash".',
|
|
370
|
+
description: 'The model to use: "sonnet", "opus", "haiku", "gpt-5-low", "gpt-5-medium", "gpt-5-high", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-3-pro-preview".',
|
|
371
371
|
},
|
|
372
372
|
session_id: {
|
|
373
373
|
type: 'string',
|
|
374
|
-
description: 'Optional session ID to resume a previous session. Supported for: haiku, sonnet, opus.',
|
|
374
|
+
description: 'Optional session ID to resume a previous session. Supported for: haiku, sonnet, opus, gemini-2.5-pro, gemini-2.5-flash, gemini-3-pro-preview.',
|
|
375
375
|
},
|
|
376
376
|
},
|
|
377
377
|
required: ['workFolder'],
|
|
@@ -399,6 +399,25 @@ export class ClaudeCodeServer {
|
|
|
399
399
|
required: ['pid'],
|
|
400
400
|
},
|
|
401
401
|
},
|
|
402
|
+
{
|
|
403
|
+
name: 'wait',
|
|
404
|
+
description: 'Wait for multiple AI agent processes to complete and return their results. Blocks until all specified PIDs finish or timeout occurs.',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: 'object',
|
|
407
|
+
properties: {
|
|
408
|
+
pids: {
|
|
409
|
+
type: 'array',
|
|
410
|
+
items: { type: 'number' },
|
|
411
|
+
description: 'List of process IDs to wait for (returned by the run tool).',
|
|
412
|
+
},
|
|
413
|
+
timeout: {
|
|
414
|
+
type: 'number',
|
|
415
|
+
description: 'Optional: Maximum time to wait in seconds. Defaults to 180 (3 minutes).',
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
required: ['pids'],
|
|
419
|
+
},
|
|
420
|
+
},
|
|
402
421
|
{
|
|
403
422
|
name: 'kill_process',
|
|
404
423
|
description: 'Terminate a running AI agent process by PID.',
|
|
@@ -440,6 +459,8 @@ export class ClaudeCodeServer {
|
|
|
440
459
|
return this.handleListProcesses();
|
|
441
460
|
case 'get_result':
|
|
442
461
|
return this.handleGetResult(toolArguments);
|
|
462
|
+
case 'wait':
|
|
463
|
+
return this.handleWait(toolArguments);
|
|
443
464
|
case 'kill_process':
|
|
444
465
|
return this.handleKillProcess(toolArguments);
|
|
445
466
|
case 'cleanup_processes':
|
|
@@ -542,6 +563,11 @@ export class ClaudeCodeServer {
|
|
|
542
563
|
cliPath = this.geminiCliPath;
|
|
543
564
|
processArgs = ['-y', '--output-format', 'json'];
|
|
544
565
|
|
|
566
|
+
// Add session_id if provided
|
|
567
|
+
if (toolArguments.session_id && typeof toolArguments.session_id === 'string') {
|
|
568
|
+
processArgs.push('-r', toolArguments.session_id);
|
|
569
|
+
}
|
|
570
|
+
|
|
545
571
|
// Add model if specified
|
|
546
572
|
if (toolArguments.model) {
|
|
547
573
|
processArgs.push('--model', toolArguments.model);
|
|
@@ -666,14 +692,9 @@ export class ClaudeCodeServer {
|
|
|
666
692
|
}
|
|
667
693
|
|
|
668
694
|
/**
|
|
669
|
-
*
|
|
695
|
+
* Helper to get process result object
|
|
670
696
|
*/
|
|
671
|
-
private
|
|
672
|
-
if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
|
|
673
|
-
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const pid = toolArguments.pid;
|
|
697
|
+
private getProcessResultHelper(pid: number): any {
|
|
677
698
|
const process = processManager.get(pid);
|
|
678
699
|
|
|
679
700
|
if (!process) {
|
|
@@ -707,8 +728,8 @@ export class ClaudeCodeServer {
|
|
|
707
728
|
// If we have valid output from agent, include it
|
|
708
729
|
if (agentOutput) {
|
|
709
730
|
response.agentOutput = agentOutput;
|
|
710
|
-
// Extract session_id if available (Claude
|
|
711
|
-
if (process.toolType === 'claude' && agentOutput.session_id) {
|
|
731
|
+
// Extract session_id if available (Claude and Gemini)
|
|
732
|
+
if ((process.toolType === 'claude' || process.toolType === 'gemini') && agentOutput.session_id) {
|
|
712
733
|
response.session_id = agentOutput.session_id;
|
|
713
734
|
}
|
|
714
735
|
} else {
|
|
@@ -716,6 +737,20 @@ export class ClaudeCodeServer {
|
|
|
716
737
|
response.stdout = process.stdout;
|
|
717
738
|
response.stderr = process.stderr;
|
|
718
739
|
}
|
|
740
|
+
|
|
741
|
+
return response;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Handle get_result tool
|
|
746
|
+
*/
|
|
747
|
+
private async handleGetResult(toolArguments: any): Promise<ServerResult> {
|
|
748
|
+
if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
|
|
749
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const pid = toolArguments.pid;
|
|
753
|
+
const response = this.getProcessResultHelper(pid);
|
|
719
754
|
|
|
720
755
|
return {
|
|
721
756
|
content: [{
|
|
@@ -725,9 +760,70 @@ export class ClaudeCodeServer {
|
|
|
725
760
|
};
|
|
726
761
|
}
|
|
727
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Handle wait tool
|
|
765
|
+
*/
|
|
766
|
+
private async handleWait(toolArguments: any): Promise<ServerResult> {
|
|
767
|
+
if (!toolArguments.pids || !Array.isArray(toolArguments.pids) || toolArguments.pids.length === 0) {
|
|
768
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pids (must be a non-empty array of numbers)');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const pids: number[] = toolArguments.pids;
|
|
772
|
+
// Default timeout: 3 minutes (180 seconds)
|
|
773
|
+
const timeoutSeconds = typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180;
|
|
774
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
775
|
+
|
|
776
|
+
// Validate all PIDs exist first
|
|
777
|
+
for (const pid of pids) {
|
|
778
|
+
if (!processManager.has(pid)) {
|
|
779
|
+
throw new McpError(ErrorCode.InvalidParams, `Process with PID ${pid} not found`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Create promises for each process
|
|
784
|
+
const waitPromises = pids.map(pid => {
|
|
785
|
+
const processEntry = processManager.get(pid)!;
|
|
786
|
+
|
|
787
|
+
if (processEntry.status !== 'running') {
|
|
788
|
+
return Promise.resolve();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return new Promise<void>((resolve) => {
|
|
792
|
+
processEntry.process.once('close', () => {
|
|
793
|
+
resolve();
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// Create a timeout promise
|
|
799
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
800
|
+
setTimeout(() => {
|
|
801
|
+
reject(new Error(`Timed out after ${timeoutSeconds} seconds waiting for processes`));
|
|
802
|
+
}, timeoutMs);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
// Wait for all processes to finish or timeout
|
|
807
|
+
await Promise.race([Promise.all(waitPromises), timeoutPromise]);
|
|
808
|
+
} catch (error: any) {
|
|
809
|
+
throw new McpError(ErrorCode.InternalError, error.message);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Collect results
|
|
813
|
+
const results = pids.map(pid => this.getProcessResultHelper(pid));
|
|
814
|
+
|
|
815
|
+
return {
|
|
816
|
+
content: [{
|
|
817
|
+
type: 'text',
|
|
818
|
+
text: JSON.stringify(results, null, 2)
|
|
819
|
+
}]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
728
823
|
/**
|
|
729
824
|
* Handle kill_process tool
|
|
730
825
|
*/
|
|
826
|
+
|
|
731
827
|
private async handleKillProcess(toolArguments: any): Promise<ServerResult> {
|
|
732
828
|
if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
|
|
733
829
|
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|