ai-cli-mcp 2.1.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 +158 -36
- 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 -671
package/src/server.ts
CHANGED
|
@@ -16,7 +16,7 @@ import * as path from 'path';
|
|
|
16
16
|
import { parseCodexOutput, parseClaudeOutput, parseGeminiOutput } from './parsers.js';
|
|
17
17
|
|
|
18
18
|
// Server version - update this when releasing new versions
|
|
19
|
-
const SERVER_VERSION = "2.
|
|
19
|
+
const SERVER_VERSION = "2.2.0";
|
|
20
20
|
|
|
21
21
|
// Model alias mappings for user-friendly model names
|
|
22
22
|
const MODEL_ALIASES: Record<string, string> = {
|
|
@@ -47,6 +47,13 @@ interface ClaudeProcess {
|
|
|
47
47
|
exitCode?: number;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Type definition for list_processes return value
|
|
51
|
+
interface ProcessListItem {
|
|
52
|
+
pid: number; // プロセスID
|
|
53
|
+
agent: 'claude' | 'codex' | 'gemini'; // エージェントタイプ
|
|
54
|
+
status: 'running' | 'completed' | 'failed'; // プロセスの状態
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
// Global process manager
|
|
51
58
|
const processManager = new Map<number, ClaudeProcess>();
|
|
52
59
|
|
|
@@ -332,7 +339,7 @@ export class ClaudeCodeServer {
|
|
|
332
339
|
**IMPORTANT**: This tool now returns immediately with a PID. Use other tools to check status and get results.
|
|
333
340
|
|
|
334
341
|
**Supported models**:
|
|
335
|
-
"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"
|
|
336
343
|
|
|
337
344
|
**Prompt input**: You must provide EITHER prompt (string) OR prompt_file (file path), but not both.
|
|
338
345
|
|
|
@@ -360,11 +367,11 @@ export class ClaudeCodeServer {
|
|
|
360
367
|
},
|
|
361
368
|
model: {
|
|
362
369
|
type: 'string',
|
|
363
|
-
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".',
|
|
364
371
|
},
|
|
365
372
|
session_id: {
|
|
366
373
|
type: 'string',
|
|
367
|
-
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.',
|
|
368
375
|
},
|
|
369
376
|
},
|
|
370
377
|
required: ['workFolder'],
|
|
@@ -372,7 +379,7 @@ export class ClaudeCodeServer {
|
|
|
372
379
|
},
|
|
373
380
|
{
|
|
374
381
|
name: 'list_processes',
|
|
375
|
-
description: 'List all running and completed AI agent processes
|
|
382
|
+
description: 'List all running and completed AI agent processes. Returns a simple list with PID, agent type, and status for each process.',
|
|
376
383
|
inputSchema: {
|
|
377
384
|
type: 'object',
|
|
378
385
|
properties: {},
|
|
@@ -392,6 +399,25 @@ export class ClaudeCodeServer {
|
|
|
392
399
|
required: ['pid'],
|
|
393
400
|
},
|
|
394
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
|
+
},
|
|
395
421
|
{
|
|
396
422
|
name: 'kill_process',
|
|
397
423
|
description: 'Terminate a running AI agent process by PID.',
|
|
@@ -405,6 +431,14 @@ export class ClaudeCodeServer {
|
|
|
405
431
|
},
|
|
406
432
|
required: ['pid'],
|
|
407
433
|
},
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'cleanup_processes',
|
|
437
|
+
description: 'Remove all completed and failed processes from the process list to free up memory.',
|
|
438
|
+
inputSchema: {
|
|
439
|
+
type: 'object',
|
|
440
|
+
properties: {},
|
|
441
|
+
},
|
|
408
442
|
}
|
|
409
443
|
],
|
|
410
444
|
}));
|
|
@@ -425,8 +459,12 @@ export class ClaudeCodeServer {
|
|
|
425
459
|
return this.handleListProcesses();
|
|
426
460
|
case 'get_result':
|
|
427
461
|
return this.handleGetResult(toolArguments);
|
|
462
|
+
case 'wait':
|
|
463
|
+
return this.handleWait(toolArguments);
|
|
428
464
|
case 'kill_process':
|
|
429
465
|
return this.handleKillProcess(toolArguments);
|
|
466
|
+
case 'cleanup_processes':
|
|
467
|
+
return this.handleCleanupProcesses();
|
|
430
468
|
default:
|
|
431
469
|
throw new McpError(ErrorCode.MethodNotFound, `Tool ${toolName} not found`);
|
|
432
470
|
}
|
|
@@ -525,6 +563,11 @@ export class ClaudeCodeServer {
|
|
|
525
563
|
cliPath = this.geminiCliPath;
|
|
526
564
|
processArgs = ['-y', '--output-format', 'json'];
|
|
527
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
|
+
|
|
528
571
|
// Add model if specified
|
|
529
572
|
if (toolArguments.model) {
|
|
530
573
|
processArgs.push('--model', toolArguments.model);
|
|
@@ -628,32 +671,15 @@ export class ClaudeCodeServer {
|
|
|
628
671
|
* Handle list_processes tool
|
|
629
672
|
*/
|
|
630
673
|
private async handleListProcesses(): Promise<ServerResult> {
|
|
631
|
-
const processes:
|
|
632
|
-
|
|
674
|
+
const processes: ProcessListItem[] = [];
|
|
675
|
+
|
|
633
676
|
for (const [pid, process] of processManager.entries()) {
|
|
634
|
-
const processInfo:
|
|
677
|
+
const processInfo: ProcessListItem = {
|
|
635
678
|
pid,
|
|
636
679
|
agent: process.toolType,
|
|
637
|
-
status: process.status
|
|
638
|
-
startTime: process.startTime,
|
|
639
|
-
prompt: process.prompt.substring(0, 100) + (process.prompt.length > 100 ? '...' : ''),
|
|
640
|
-
workFolder: process.workFolder,
|
|
641
|
-
model: process.model,
|
|
642
|
-
exitCode: process.exitCode
|
|
680
|
+
status: process.status
|
|
643
681
|
};
|
|
644
682
|
|
|
645
|
-
// Try to extract session_id from JSON output if available
|
|
646
|
-
if (process.stdout) {
|
|
647
|
-
try {
|
|
648
|
-
const claudeOutput = JSON.parse(process.stdout);
|
|
649
|
-
if (claudeOutput.session_id) {
|
|
650
|
-
processInfo.session_id = claudeOutput.session_id;
|
|
651
|
-
}
|
|
652
|
-
} catch (e) {
|
|
653
|
-
// Ignore parsing errors
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
683
|
processes.push(processInfo);
|
|
658
684
|
}
|
|
659
685
|
|
|
@@ -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');
|
|
@@ -773,6 +869,32 @@ export class ClaudeCodeServer {
|
|
|
773
869
|
}
|
|
774
870
|
}
|
|
775
871
|
|
|
872
|
+
/**
|
|
873
|
+
* Handle cleanup_processes tool
|
|
874
|
+
*/
|
|
875
|
+
private async handleCleanupProcesses(): Promise<ServerResult> {
|
|
876
|
+
const removedPids: number[] = [];
|
|
877
|
+
|
|
878
|
+
// Iterate through all processes and collect PIDs to remove
|
|
879
|
+
for (const [pid, process] of processManager.entries()) {
|
|
880
|
+
if (process.status === 'completed' || process.status === 'failed') {
|
|
881
|
+
removedPids.push(pid);
|
|
882
|
+
processManager.delete(pid);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
content: [{
|
|
888
|
+
type: 'text',
|
|
889
|
+
text: JSON.stringify({
|
|
890
|
+
removed: removedPids.length,
|
|
891
|
+
removedPids,
|
|
892
|
+
message: `Cleaned up ${removedPids.length} finished process(es)`
|
|
893
|
+
}, null, 2)
|
|
894
|
+
}]
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
776
898
|
/**
|
|
777
899
|
* Start the MCP server
|
|
778
900
|
*/
|
|
@@ -796,4 +918,4 @@ export class ClaudeCodeServer {
|
|
|
796
918
|
|
|
797
919
|
// Create and run the server if this is the main module
|
|
798
920
|
const server = new ClaudeCodeServer();
|
|
799
|
-
server.run().catch(console.error);
|
|
921
|
+
server.run().catch(console.error);
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { MCPTestClient } from './utils/mcp-client.js';
|
|
6
|
-
import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
|
|
7
|
-
describe('Claude Code MCP E2E Tests', () => {
|
|
8
|
-
let client;
|
|
9
|
-
let testDir;
|
|
10
|
-
const serverPath = 'dist/server.js';
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
// Ensure mock exists
|
|
13
|
-
await getSharedMock();
|
|
14
|
-
// Create a temporary directory for test files
|
|
15
|
-
testDir = mkdtempSync(join(tmpdir(), 'claude-code-test-'));
|
|
16
|
-
// Initialize MCP client with debug mode and custom binary name using absolute path
|
|
17
|
-
client = new MCPTestClient(serverPath, {
|
|
18
|
-
MCP_CLAUDE_DEBUG: 'true',
|
|
19
|
-
CLAUDE_CLI_NAME: '/tmp/claude-code-test-mock/claudeMocked',
|
|
20
|
-
});
|
|
21
|
-
await client.connect();
|
|
22
|
-
});
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
// Disconnect client
|
|
25
|
-
await client.disconnect();
|
|
26
|
-
// Clean up test directory
|
|
27
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
28
|
-
});
|
|
29
|
-
afterAll(async () => {
|
|
30
|
-
// Only cleanup mock at the very end
|
|
31
|
-
await cleanupSharedMock();
|
|
32
|
-
});
|
|
33
|
-
describe('Tool Registration', () => {
|
|
34
|
-
it('should register claude_code tool', async () => {
|
|
35
|
-
const tools = await client.listTools();
|
|
36
|
-
expect(tools).toHaveLength(4);
|
|
37
|
-
const claudeCodeTool = tools.find((t) => t.name === 'claude_code');
|
|
38
|
-
expect(claudeCodeTool).toEqual({
|
|
39
|
-
name: 'claude_code',
|
|
40
|
-
description: expect.stringContaining('Claude Code Agent'),
|
|
41
|
-
inputSchema: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
properties: {
|
|
44
|
-
prompt: {
|
|
45
|
-
type: 'string',
|
|
46
|
-
description: expect.stringContaining('Either this or prompt_file is required'),
|
|
47
|
-
},
|
|
48
|
-
prompt_file: {
|
|
49
|
-
type: 'string',
|
|
50
|
-
description: expect.stringContaining('Path to a file containing the prompt'),
|
|
51
|
-
},
|
|
52
|
-
workFolder: {
|
|
53
|
-
type: 'string',
|
|
54
|
-
description: expect.stringContaining('working directory'),
|
|
55
|
-
},
|
|
56
|
-
model: {
|
|
57
|
-
type: 'string',
|
|
58
|
-
description: expect.stringContaining('Claude model'),
|
|
59
|
-
},
|
|
60
|
-
session_id: {
|
|
61
|
-
type: 'string',
|
|
62
|
-
description: expect.stringContaining('session ID'),
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
required: ['workFolder'],
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
// Verify other tools exist
|
|
69
|
-
expect(tools.some((t) => t.name === 'list_claude_processes')).toBe(true);
|
|
70
|
-
expect(tools.some((t) => t.name === 'get_claude_result')).toBe(true);
|
|
71
|
-
expect(tools.some((t) => t.name === 'kill_claude_process')).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe('Basic Operations', () => {
|
|
75
|
-
it('should execute a simple prompt', async () => {
|
|
76
|
-
const response = await client.callTool('claude_code', {
|
|
77
|
-
prompt: 'create a file called test.txt with content "Hello World"',
|
|
78
|
-
workFolder: testDir,
|
|
79
|
-
});
|
|
80
|
-
expect(response).toEqual([{
|
|
81
|
-
type: 'text',
|
|
82
|
-
text: expect.stringContaining('successfully'),
|
|
83
|
-
}]);
|
|
84
|
-
});
|
|
85
|
-
it('should handle process management correctly', async () => {
|
|
86
|
-
// claude_code now returns a PID immediately
|
|
87
|
-
const response = await client.callTool('claude_code', {
|
|
88
|
-
prompt: 'error',
|
|
89
|
-
workFolder: testDir,
|
|
90
|
-
});
|
|
91
|
-
expect(response).toEqual([{
|
|
92
|
-
type: 'text',
|
|
93
|
-
text: expect.stringContaining('pid'),
|
|
94
|
-
}]);
|
|
95
|
-
// Extract PID from response
|
|
96
|
-
const responseText = response[0].text;
|
|
97
|
-
const pidMatch = responseText.match(/"pid":\s*(\d+)/);
|
|
98
|
-
expect(pidMatch).toBeTruthy();
|
|
99
|
-
});
|
|
100
|
-
it('should reject missing workFolder', async () => {
|
|
101
|
-
await expect(client.callTool('claude_code', {
|
|
102
|
-
prompt: 'List files in current directory',
|
|
103
|
-
})).rejects.toThrow(/workFolder/i);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
describe('Working Directory Handling', () => {
|
|
107
|
-
it('should respect custom working directory', async () => {
|
|
108
|
-
const response = await client.callTool('claude_code', {
|
|
109
|
-
prompt: 'Show current working directory',
|
|
110
|
-
workFolder: testDir,
|
|
111
|
-
});
|
|
112
|
-
expect(response).toBeTruthy();
|
|
113
|
-
});
|
|
114
|
-
it('should reject non-existent working directory', async () => {
|
|
115
|
-
const nonExistentDir = join(testDir, 'non-existent');
|
|
116
|
-
await expect(client.callTool('claude_code', {
|
|
117
|
-
prompt: 'Test prompt',
|
|
118
|
-
workFolder: nonExistentDir,
|
|
119
|
-
})).rejects.toThrow(/does not exist/i);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
describe('Timeout Handling', () => {
|
|
123
|
-
it('should respect timeout settings', async () => {
|
|
124
|
-
// This would require modifying the mock to simulate a long-running command
|
|
125
|
-
// Since we're testing locally, we'll skip the actual timeout test
|
|
126
|
-
expect(true).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
describe('Model Alias Handling', () => {
|
|
130
|
-
it('should resolve haiku alias when calling claude_code', async () => {
|
|
131
|
-
const response = await client.callTool('claude_code', {
|
|
132
|
-
prompt: 'Test with haiku model',
|
|
133
|
-
workFolder: testDir,
|
|
134
|
-
model: 'haiku'
|
|
135
|
-
});
|
|
136
|
-
expect(response).toEqual([{
|
|
137
|
-
type: 'text',
|
|
138
|
-
text: expect.stringContaining('pid'),
|
|
139
|
-
}]);
|
|
140
|
-
// Extract PID from response
|
|
141
|
-
const responseText = response[0].text;
|
|
142
|
-
const pidMatch = responseText.match(/"pid":\s*(\d+)/);
|
|
143
|
-
expect(pidMatch).toBeTruthy();
|
|
144
|
-
// Get the PID and check the process
|
|
145
|
-
const pid = parseInt(pidMatch[1]);
|
|
146
|
-
const processes = await client.callTool('list_claude_processes', {});
|
|
147
|
-
const processesText = processes[0].text;
|
|
148
|
-
const processData = JSON.parse(processesText);
|
|
149
|
-
// Find our process
|
|
150
|
-
const ourProcess = processData.find((p) => p.pid === pid);
|
|
151
|
-
expect(ourProcess).toBeTruthy();
|
|
152
|
-
// Verify that the model was set correctly
|
|
153
|
-
expect(ourProcess.model).toBe('haiku');
|
|
154
|
-
});
|
|
155
|
-
it('should pass non-alias model names unchanged', async () => {
|
|
156
|
-
const response = await client.callTool('claude_code', {
|
|
157
|
-
prompt: 'Test with sonnet model',
|
|
158
|
-
workFolder: testDir,
|
|
159
|
-
model: 'sonnet'
|
|
160
|
-
});
|
|
161
|
-
expect(response).toEqual([{
|
|
162
|
-
type: 'text',
|
|
163
|
-
text: expect.stringContaining('pid'),
|
|
164
|
-
}]);
|
|
165
|
-
// Extract PID
|
|
166
|
-
const responseText = response[0].text;
|
|
167
|
-
const pidMatch = responseText.match(/"pid":\s*(\d+)/);
|
|
168
|
-
const pid = parseInt(pidMatch[1]);
|
|
169
|
-
// Check the process
|
|
170
|
-
const processes = await client.callTool('list_claude_processes', {});
|
|
171
|
-
const processesText = processes[0].text;
|
|
172
|
-
const processData = JSON.parse(processesText);
|
|
173
|
-
// Find our process
|
|
174
|
-
const ourProcess = processData.find((p) => p.pid === pid);
|
|
175
|
-
expect(ourProcess).toBeTruthy();
|
|
176
|
-
// The model should be unchanged
|
|
177
|
-
expect(ourProcess.model).toBe('sonnet');
|
|
178
|
-
});
|
|
179
|
-
it('should work without specifying a model', async () => {
|
|
180
|
-
const response = await client.callTool('claude_code', {
|
|
181
|
-
prompt: 'Test without model parameter',
|
|
182
|
-
workFolder: testDir
|
|
183
|
-
});
|
|
184
|
-
expect(response).toEqual([{
|
|
185
|
-
type: 'text',
|
|
186
|
-
text: expect.stringContaining('pid'),
|
|
187
|
-
}]);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
describe('Debug Mode', () => {
|
|
191
|
-
it('should log debug information when enabled', async () => {
|
|
192
|
-
// Debug logs go to stderr, which we capture in the client
|
|
193
|
-
const response = await client.callTool('claude_code', {
|
|
194
|
-
prompt: 'Debug test prompt',
|
|
195
|
-
workFolder: testDir,
|
|
196
|
-
});
|
|
197
|
-
expect(response).toBeTruthy();
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
describe('Integration Tests (Local Only)', () => {
|
|
202
|
-
let client;
|
|
203
|
-
let testDir;
|
|
204
|
-
beforeEach(async () => {
|
|
205
|
-
testDir = mkdtempSync(join(tmpdir(), 'claude-code-integration-'));
|
|
206
|
-
// Initialize client without mocks for real Claude testing
|
|
207
|
-
client = new MCPTestClient('dist/server.js', {
|
|
208
|
-
MCP_CLAUDE_DEBUG: 'true',
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
afterEach(async () => {
|
|
212
|
-
if (client) {
|
|
213
|
-
await client.disconnect();
|
|
214
|
-
}
|
|
215
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
216
|
-
});
|
|
217
|
-
// These tests will only run locally when Claude is available
|
|
218
|
-
it.skip('should create a file with real Claude CLI', async () => {
|
|
219
|
-
await client.connect();
|
|
220
|
-
const response = await client.callTool('claude_code', {
|
|
221
|
-
prompt: 'Create a file called hello.txt with content "Hello from Claude"',
|
|
222
|
-
workFolder: testDir,
|
|
223
|
-
});
|
|
224
|
-
const filePath = join(testDir, 'hello.txt');
|
|
225
|
-
expect(existsSync(filePath)).toBe(true);
|
|
226
|
-
expect(readFileSync(filePath, 'utf-8')).toContain('Hello from Claude');
|
|
227
|
-
});
|
|
228
|
-
it.skip('should handle git operations with real Claude CLI', async () => {
|
|
229
|
-
await client.connect();
|
|
230
|
-
// Initialize git repo
|
|
231
|
-
const response = await client.callTool('claude_code', {
|
|
232
|
-
prompt: 'Initialize a git repository and create a README.md file',
|
|
233
|
-
workFolder: testDir,
|
|
234
|
-
});
|
|
235
|
-
expect(existsSync(join(testDir, '.git'))).toBe(true);
|
|
236
|
-
expect(existsSync(join(testDir, 'README.md'))).toBe(true);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { MCPTestClient } from './utils/mcp-client.js';
|
|
6
|
-
import { getSharedMock, cleanupSharedMock } from './utils/persistent-mock.js';
|
|
7
|
-
describe('Claude Code Edge Cases', () => {
|
|
8
|
-
let client;
|
|
9
|
-
let testDir;
|
|
10
|
-
const serverPath = 'dist/server.js';
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
// Ensure mock exists
|
|
13
|
-
await getSharedMock();
|
|
14
|
-
// Create test directory
|
|
15
|
-
testDir = mkdtempSync(join(tmpdir(), 'claude-code-edge-'));
|
|
16
|
-
// Initialize client with custom binary name using absolute path
|
|
17
|
-
client = new MCPTestClient(serverPath, {
|
|
18
|
-
MCP_CLAUDE_DEBUG: 'true',
|
|
19
|
-
CLAUDE_CLI_NAME: '/tmp/claude-code-test-mock/claudeMocked',
|
|
20
|
-
});
|
|
21
|
-
await client.connect();
|
|
22
|
-
});
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await client.disconnect();
|
|
25
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
afterAll(async () => {
|
|
28
|
-
// Cleanup mock only at the end
|
|
29
|
-
await cleanupSharedMock();
|
|
30
|
-
});
|
|
31
|
-
describe('Input Validation', () => {
|
|
32
|
-
it('should reject missing prompt', async () => {
|
|
33
|
-
await expect(client.callTool('claude_code', {
|
|
34
|
-
workFolder: testDir,
|
|
35
|
-
})).rejects.toThrow(/prompt/i);
|
|
36
|
-
});
|
|
37
|
-
it('should reject invalid prompt type', async () => {
|
|
38
|
-
await expect(client.callTool('claude_code', {
|
|
39
|
-
prompt: 123, // Should be string
|
|
40
|
-
workFolder: testDir,
|
|
41
|
-
})).rejects.toThrow();
|
|
42
|
-
});
|
|
43
|
-
it('should reject invalid workFolder type', async () => {
|
|
44
|
-
await expect(client.callTool('claude_code', {
|
|
45
|
-
prompt: 'Test prompt',
|
|
46
|
-
workFolder: 123, // Should be string
|
|
47
|
-
})).rejects.toThrow(/workFolder/i);
|
|
48
|
-
});
|
|
49
|
-
it('should reject empty prompt', async () => {
|
|
50
|
-
await expect(client.callTool('claude_code', {
|
|
51
|
-
prompt: '',
|
|
52
|
-
workFolder: testDir,
|
|
53
|
-
})).rejects.toThrow(/prompt/i);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe('Special Characters', () => {
|
|
57
|
-
it.skip('should handle prompts with quotes', async () => {
|
|
58
|
-
// Skipping: This test fails in CI when mock is not found at expected path
|
|
59
|
-
const response = await client.callTool('claude_code', {
|
|
60
|
-
prompt: 'Create a file with content "Hello \\"World\\""',
|
|
61
|
-
workFolder: testDir,
|
|
62
|
-
});
|
|
63
|
-
expect(response).toBeTruthy();
|
|
64
|
-
});
|
|
65
|
-
it('should handle prompts with newlines', async () => {
|
|
66
|
-
const response = await client.callTool('claude_code', {
|
|
67
|
-
prompt: 'Create a file with content:\\nLine 1\\nLine 2',
|
|
68
|
-
workFolder: testDir,
|
|
69
|
-
});
|
|
70
|
-
expect(response).toBeTruthy();
|
|
71
|
-
});
|
|
72
|
-
it('should handle prompts with shell special characters', async () => {
|
|
73
|
-
const response = await client.callTool('claude_code', {
|
|
74
|
-
prompt: 'Create a file named test$file.txt',
|
|
75
|
-
workFolder: testDir,
|
|
76
|
-
});
|
|
77
|
-
expect(response).toBeTruthy();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe('Error Recovery', () => {
|
|
81
|
-
it('should handle Claude CLI not found gracefully', async () => {
|
|
82
|
-
// Create a client with a different binary name that doesn't exist
|
|
83
|
-
const errorClient = new MCPTestClient(serverPath, {
|
|
84
|
-
MCP_CLAUDE_DEBUG: 'true',
|
|
85
|
-
CLAUDE_CLI_NAME: 'non-existent-claude',
|
|
86
|
-
});
|
|
87
|
-
await errorClient.connect();
|
|
88
|
-
await expect(errorClient.callTool('claude_code', {
|
|
89
|
-
prompt: 'Test prompt',
|
|
90
|
-
workFolder: testDir,
|
|
91
|
-
})).rejects.toThrow();
|
|
92
|
-
await errorClient.disconnect();
|
|
93
|
-
});
|
|
94
|
-
it('should handle permission denied errors', async () => {
|
|
95
|
-
const restrictedDir = '/root/restricted';
|
|
96
|
-
// Non-existent directories now throw an error
|
|
97
|
-
await expect(client.callTool('claude_code', {
|
|
98
|
-
prompt: 'Test prompt',
|
|
99
|
-
workFolder: restrictedDir,
|
|
100
|
-
})).rejects.toThrow(/does not exist/i);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('Concurrent Requests', () => {
|
|
104
|
-
it('should handle multiple simultaneous requests', async () => {
|
|
105
|
-
const promises = Array(5).fill(null).map((_, i) => client.callTool('claude_code', {
|
|
106
|
-
prompt: `Create file test${i}.txt`,
|
|
107
|
-
workFolder: testDir,
|
|
108
|
-
}));
|
|
109
|
-
const results = await Promise.allSettled(promises);
|
|
110
|
-
const successful = results.filter(r => r.status === 'fulfilled');
|
|
111
|
-
expect(successful.length).toBeGreaterThan(0);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
describe('Large Prompts', () => {
|
|
115
|
-
it('should handle very long prompts', async () => {
|
|
116
|
-
const longPrompt = 'Create a file with content: ' + 'x'.repeat(10000);
|
|
117
|
-
const response = await client.callTool('claude_code', {
|
|
118
|
-
prompt: longPrompt,
|
|
119
|
-
workFolder: testDir,
|
|
120
|
-
});
|
|
121
|
-
expect(response).toBeTruthy();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
describe('Path Traversal', () => {
|
|
125
|
-
it('should prevent path traversal attacks', async () => {
|
|
126
|
-
const maliciousPath = join(testDir, '..', '..', 'etc', 'passwd');
|
|
127
|
-
// Server resolves paths and checks existence
|
|
128
|
-
// The path /etc/passwd may exist but be a file, not a directory
|
|
129
|
-
await expect(client.callTool('claude_code', {
|
|
130
|
-
prompt: 'Read file',
|
|
131
|
-
workFolder: maliciousPath,
|
|
132
|
-
})).rejects.toThrow(/(does not exist|ENOTDIR)/i);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
});
|