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.
@@ -1,296 +0,0 @@
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 { EventEmitter } from 'node:events';
6
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
- // Mock dependencies
8
- vi.mock('node:child_process');
9
- vi.mock('node:fs');
10
- vi.mock('node:os');
11
- vi.mock('node:path', () => ({
12
- resolve: vi.fn((path) => path),
13
- join: vi.fn((...args) => args.join('/')),
14
- isAbsolute: vi.fn((path) => path.startsWith('/'))
15
- }));
16
- vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
17
- Server: vi.fn().mockImplementation(function () {
18
- this.setRequestHandler = vi.fn();
19
- this.connect = vi.fn();
20
- this.close = vi.fn();
21
- this.onerror = undefined;
22
- return this;
23
- }),
24
- }));
25
- vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
26
- ListToolsRequestSchema: { name: 'listTools' },
27
- CallToolRequestSchema: { name: 'callTool' },
28
- ErrorCode: {
29
- InternalError: 'InternalError',
30
- MethodNotFound: 'MethodNotFound',
31
- InvalidParams: 'InvalidParams'
32
- },
33
- McpError: vi.fn().mockImplementation((code, message) => {
34
- const error = new Error(message);
35
- error.code = code;
36
- return error;
37
- })
38
- }));
39
- const mockExistsSync = vi.mocked(existsSync);
40
- const mockSpawn = vi.mocked(spawn);
41
- const mockHomedir = vi.mocked(homedir);
42
- describe('Error Handling Tests', () => {
43
- let consoleErrorSpy;
44
- let originalEnv;
45
- let errorHandler = null;
46
- function setupServerMock() {
47
- errorHandler = null;
48
- vi.mocked(Server).mockImplementation(function () {
49
- this.setRequestHandler = vi.fn();
50
- this.connect = vi.fn();
51
- this.close = vi.fn();
52
- Object.defineProperty(this, 'onerror', {
53
- get() { return errorHandler; },
54
- set(handler) { errorHandler = handler; },
55
- enumerable: true,
56
- configurable: true
57
- });
58
- return this;
59
- });
60
- }
61
- beforeEach(() => {
62
- vi.clearAllMocks();
63
- vi.resetModules();
64
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
65
- originalEnv = { ...process.env };
66
- process.env = { ...originalEnv };
67
- });
68
- afterEach(() => {
69
- consoleErrorSpy.mockRestore();
70
- process.env = originalEnv;
71
- });
72
- describe('CallToolRequest Error Cases', () => {
73
- it('should throw error for unknown tool name', async () => {
74
- mockHomedir.mockReturnValue('/home/user');
75
- mockExistsSync.mockReturnValue(true);
76
- // Set up Server mock before importing the module
77
- setupServerMock();
78
- const module = await import('../server.js');
79
- // @ts-ignore
80
- const { ClaudeCodeServer } = module;
81
- const server = new ClaudeCodeServer();
82
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
83
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
84
- const handler = callToolCall[1];
85
- await expect(handler({
86
- params: {
87
- name: 'unknown_tool',
88
- arguments: {}
89
- }
90
- })).rejects.toThrow('Tool unknown_tool not found');
91
- });
92
- it('should handle timeout errors', async () => {
93
- mockHomedir.mockReturnValue('/home/user');
94
- mockExistsSync.mockReturnValue(true);
95
- setupServerMock();
96
- const module = await import('../server.js');
97
- // @ts-ignore
98
- const { ClaudeCodeServer } = module;
99
- const { McpError } = await import('@modelcontextprotocol/sdk/types.js');
100
- const server = new ClaudeCodeServer();
101
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
102
- // Find the callTool handler
103
- let callToolHandler;
104
- for (const call of mockServerInstance.setRequestHandler.mock.calls) {
105
- if (call[0].name === 'callTool') {
106
- callToolHandler = call[1];
107
- break;
108
- }
109
- }
110
- // Mock spawn to return process without PID
111
- mockSpawn.mockImplementation(() => {
112
- const mockProcess = new EventEmitter();
113
- mockProcess.stdout = new EventEmitter();
114
- mockProcess.stderr = new EventEmitter();
115
- mockProcess.stdout.on = vi.fn();
116
- mockProcess.stderr.on = vi.fn();
117
- mockProcess.pid = undefined; // No PID to simulate process start failure
118
- return mockProcess;
119
- });
120
- // Call handler
121
- try {
122
- await callToolHandler({
123
- params: {
124
- name: 'claude_code',
125
- arguments: {
126
- prompt: 'test',
127
- workFolder: '/tmp'
128
- }
129
- }
130
- });
131
- expect.fail('Should have thrown');
132
- }
133
- catch (err) {
134
- // Check if McpError was called with the process start failure message
135
- expect(McpError).toHaveBeenCalledWith('InternalError', 'Failed to start Claude CLI process');
136
- }
137
- });
138
- it('should handle invalid argument types', async () => {
139
- mockHomedir.mockReturnValue('/home/user');
140
- mockExistsSync.mockReturnValue(true);
141
- setupServerMock();
142
- const module = await import('../server.js');
143
- // @ts-ignore
144
- const { ClaudeCodeServer } = module;
145
- const server = new ClaudeCodeServer();
146
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
147
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
148
- const handler = callToolCall[1];
149
- await expect(handler({
150
- params: {
151
- name: 'claude_code',
152
- arguments: 'invalid-should-be-object'
153
- }
154
- })).rejects.toThrow();
155
- });
156
- it('should include CLI error details in error message', async () => {
157
- mockHomedir.mockReturnValue('/home/user');
158
- mockExistsSync.mockReturnValue(true);
159
- setupServerMock();
160
- const module = await import('../server.js');
161
- // @ts-ignore
162
- const { ClaudeCodeServer } = module;
163
- const server = new ClaudeCodeServer();
164
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
165
- const callToolCall = mockServerInstance.setRequestHandler.mock.calls.find((call) => call[0].name === 'callTool');
166
- const handler = callToolCall[1];
167
- // Create a simple mock process
168
- mockSpawn.mockImplementation(() => {
169
- const mockProcess = Object.create(EventEmitter.prototype);
170
- EventEmitter.call(mockProcess);
171
- mockProcess.stdout = Object.create(EventEmitter.prototype);
172
- EventEmitter.call(mockProcess.stdout);
173
- mockProcess.stderr = Object.create(EventEmitter.prototype);
174
- EventEmitter.call(mockProcess.stderr);
175
- mockProcess.stdout.on = vi.fn((event, callback) => {
176
- if (event === 'data') {
177
- // Send some stdout data
178
- process.nextTick(() => callback('stdout content'));
179
- }
180
- });
181
- mockProcess.stderr.on = vi.fn((event, callback) => {
182
- if (event === 'data') {
183
- // Send some stderr data
184
- process.nextTick(() => callback('stderr content'));
185
- }
186
- });
187
- // Emit error/close event after data is sent
188
- setTimeout(() => {
189
- mockProcess.emit('close', 1);
190
- }, 1);
191
- return mockProcess;
192
- });
193
- await expect(handler({
194
- params: {
195
- name: 'claude_code',
196
- arguments: {
197
- prompt: 'test',
198
- workFolder: '/tmp'
199
- }
200
- }
201
- })).rejects.toThrow();
202
- });
203
- });
204
- describe('Process Spawn Error Cases', () => {
205
- it('should handle spawn ENOENT error', async () => {
206
- const module = await import('../server.js');
207
- // @ts-ignore
208
- const { spawnAsync } = module;
209
- const mockProcess = new EventEmitter();
210
- mockProcess.stdout = new EventEmitter();
211
- mockProcess.stderr = new EventEmitter();
212
- mockProcess.stdout.on = vi.fn();
213
- mockProcess.stderr.on = vi.fn();
214
- mockSpawn.mockReturnValue(mockProcess);
215
- const promise = spawnAsync('nonexistent-command', []);
216
- // Simulate ENOENT error
217
- setTimeout(() => {
218
- const error = new Error('spawn ENOENT');
219
- error.code = 'ENOENT';
220
- error.path = 'nonexistent-command';
221
- error.syscall = 'spawn';
222
- mockProcess.emit('error', error);
223
- }, 10);
224
- await expect(promise).rejects.toThrow('Spawn error');
225
- await expect(promise).rejects.toThrow('nonexistent-command');
226
- });
227
- it('should handle generic spawn errors', async () => {
228
- const module = await import('../server.js');
229
- // @ts-ignore
230
- const { spawnAsync } = module;
231
- const mockProcess = new EventEmitter();
232
- mockProcess.stdout = new EventEmitter();
233
- mockProcess.stderr = new EventEmitter();
234
- mockProcess.stdout.on = vi.fn();
235
- mockProcess.stderr.on = vi.fn();
236
- mockSpawn.mockReturnValue(mockProcess);
237
- const promise = spawnAsync('test', []);
238
- // Simulate generic error
239
- setTimeout(() => {
240
- mockProcess.emit('error', new Error('Generic spawn error'));
241
- }, 10);
242
- await expect(promise).rejects.toThrow('Generic spawn error');
243
- });
244
- it('should accumulate stderr output before error', async () => {
245
- const module = await import('../server.js');
246
- // @ts-ignore
247
- const { spawnAsync } = module;
248
- const mockProcess = new EventEmitter();
249
- mockProcess.stdout = new EventEmitter();
250
- mockProcess.stderr = new EventEmitter();
251
- let stderrHandler;
252
- mockProcess.stdout.on = vi.fn();
253
- mockProcess.stderr.on = vi.fn((event, handler) => {
254
- if (event === 'data')
255
- stderrHandler = handler;
256
- });
257
- mockSpawn.mockReturnValue(mockProcess);
258
- const promise = spawnAsync('test', []);
259
- // Simulate stderr data then error
260
- setTimeout(() => {
261
- stderrHandler('error line 1\n');
262
- stderrHandler('error line 2\n');
263
- mockProcess.emit('error', new Error('Command failed'));
264
- }, 10);
265
- await expect(promise).rejects.toThrow('error line 1\nerror line 2');
266
- });
267
- });
268
- describe('Server Initialization Errors', () => {
269
- it('should handle CLI path not found gracefully', async () => {
270
- // Mock no CLI found anywhere
271
- mockHomedir.mockReturnValue('/home/user');
272
- mockExistsSync.mockReturnValue(false);
273
- const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
274
- setupServerMock();
275
- const module = await import('../server.js');
276
- // @ts-ignore
277
- const { ClaudeCodeServer } = module;
278
- const server = new ClaudeCodeServer();
279
- expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Claude CLI not found'));
280
- consoleWarnSpy.mockRestore();
281
- });
282
- it('should handle server connection errors', async () => {
283
- mockHomedir.mockReturnValue('/home/user');
284
- mockExistsSync.mockReturnValue(true);
285
- setupServerMock();
286
- const module = await import('../server.js');
287
- // @ts-ignore
288
- const { ClaudeCodeServer } = module;
289
- const server = new ClaudeCodeServer();
290
- // Mock connection failure
291
- const mockServerInstance = vi.mocked(Server).mock.results[0].value;
292
- mockServerInstance.connect.mockRejectedValue(new Error('Connection failed'));
293
- await expect(server.run()).rejects.toThrow('Connection failed');
294
- });
295
- });
296
- });
@@ -1,32 +0,0 @@
1
- import { vi } from 'vitest';
2
- // Mock Claude CLI responses
3
- export const mockClaudeResponse = (stdout, stderr = '', exitCode = 0) => {
4
- return {
5
- stdout: { on: vi.fn((event, cb) => event === 'data' && cb(stdout)) },
6
- stderr: { on: vi.fn((event, cb) => event === 'data' && cb(stderr)) },
7
- on: vi.fn((event, cb) => {
8
- if (event === 'exit')
9
- setTimeout(() => cb(exitCode), 10);
10
- }),
11
- };
12
- };
13
- // Mock MCP request builder
14
- export const createMCPRequest = (tool, args, id = 1) => ({
15
- jsonrpc: '2.0',
16
- method: 'tools/call',
17
- params: {
18
- name: tool,
19
- arguments: args,
20
- },
21
- id,
22
- });
23
- // Mock file system operations
24
- export const setupTestEnvironment = () => {
25
- const testFiles = new Map();
26
- return {
27
- writeFile: (path, content) => testFiles.set(path, content),
28
- readFile: (path) => testFiles.get(path),
29
- exists: (path) => testFiles.has(path),
30
- cleanup: () => testFiles.clear(),
31
- };
32
- };
@@ -1,36 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- // Test the model alias resolution logic directly
3
- describe('Model Alias Resolution', () => {
4
- // Define the same MODEL_ALIASES as in server.ts
5
- const MODEL_ALIASES = {
6
- 'haiku': 'claude-3-5-haiku-20241022'
7
- };
8
- // Replicate the resolveModelAlias function
9
- function resolveModelAlias(model) {
10
- return MODEL_ALIASES[model] || model;
11
- }
12
- it('should resolve haiku alias to full model name', () => {
13
- expect(resolveModelAlias('haiku')).toBe('claude-3-5-haiku-20241022');
14
- });
15
- it('should pass through non-alias model names unchanged', () => {
16
- expect(resolveModelAlias('sonnet')).toBe('sonnet');
17
- expect(resolveModelAlias('opus')).toBe('opus');
18
- expect(resolveModelAlias('claude-3-opus-20240229')).toBe('claude-3-opus-20240229');
19
- });
20
- it('should pass through empty strings', () => {
21
- expect(resolveModelAlias('')).toBe('');
22
- });
23
- it('should be case-sensitive', () => {
24
- // Should not resolve uppercase version
25
- expect(resolveModelAlias('Haiku')).toBe('Haiku');
26
- expect(resolveModelAlias('HAIKU')).toBe('HAIKU');
27
- });
28
- it('should handle undefined input gracefully', () => {
29
- // TypeScript would normally prevent this, but testing for runtime safety
30
- expect(resolveModelAlias(undefined)).toBe(undefined);
31
- });
32
- it('should handle null input gracefully', () => {
33
- // TypeScript would normally prevent this, but testing for runtime safety
34
- expect(resolveModelAlias(null)).toBe(null);
35
- });
36
- });