agent-gear 1.1.0 → 1.1.2
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/AGENTS.md +10 -0
- package/dist/acp/bridge.d.ts +2 -0
- package/dist/acp/bridge.d.ts.map +1 -1
- package/dist/acp/bridge.js +28 -8
- package/dist/acp/bridge.js.map +1 -1
- package/dist/acp/bridge.test.d.ts +2 -0
- package/dist/acp/bridge.test.d.ts.map +1 -0
- package/dist/acp/bridge.test.js +149 -0
- package/dist/acp/bridge.test.js.map +1 -0
- package/dist/commands/mcp-proxy.d.ts +8 -0
- package/dist/commands/mcp-proxy.d.ts.map +1 -0
- package/dist/commands/mcp-proxy.js +105 -0
- package/dist/commands/mcp-proxy.js.map +1 -0
- package/dist/commands/mcp.d.ts +7 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +33 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/socketio-client.d.ts.map +1 -1
- package/dist/socketio-client.js +32 -11
- package/dist/socketio-client.js.map +1 -1
- package/package.json +1 -1
- package/src/acp/bridge.test.ts +196 -0
- package/src/acp/bridge.ts +39 -10
- package/src/commands/mcp-proxy.ts +128 -0
- package/src/commands/mcp.ts +41 -0
- package/src/config.ts +18 -0
- package/src/index.ts +28 -0
- package/src/socketio-client.ts +40 -7
- package/tests/acp-bridge.test.ts +42 -15
- package/tests/commands/mcp-proxy.test.ts +111 -0
- package/tests/commands/mcp.test.ts +43 -0
- package/tests/socketio-client.test.ts +7 -2
package/src/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface ConfigData {
|
|
|
10
10
|
key?: { value: string };
|
|
11
11
|
server?: { endpoint: string };
|
|
12
12
|
controller?: { name: string };
|
|
13
|
+
resumeToken?: { value: string };
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function readConfig(): ConfigData {
|
|
@@ -73,3 +74,20 @@ export function resolveEndpoint(cliEndpoint?: string): string | undefined {
|
|
|
73
74
|
if (envEndpoint) return envEndpoint;
|
|
74
75
|
return getEndpointFromConfig();
|
|
75
76
|
}
|
|
77
|
+
|
|
78
|
+
export function getResumeTokenFromConfig(): string | undefined {
|
|
79
|
+
const config = readConfig();
|
|
80
|
+
return config.resumeToken?.value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function saveResumeTokenToConfig(token: string): void {
|
|
84
|
+
const config = readConfig();
|
|
85
|
+
config.resumeToken = { value: token };
|
|
86
|
+
writeConfig(config);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function clearResumeTokenFromConfig(): void {
|
|
90
|
+
const config = readConfig();
|
|
91
|
+
delete config.resumeToken;
|
|
92
|
+
writeConfig(config);
|
|
93
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { hostname } from 'os';
|
|
|
4
4
|
import { resolveKey, resolveEndpoint, saveKeyToConfig, saveEndpointToConfig } from './config.js';
|
|
5
5
|
import { connectToServer } from './socketio-client.js';
|
|
6
6
|
import { writePid, removePid, isRunning } from './daemon.js';
|
|
7
|
+
import { startMcpRegistration } from './commands/mcp-proxy.js';
|
|
8
|
+
import { startMcpProxy } from './commands/mcp.js';
|
|
7
9
|
|
|
8
10
|
const program = new Command();
|
|
9
11
|
|
|
@@ -56,4 +58,30 @@ program
|
|
|
56
58
|
});
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
program
|
|
62
|
+
.command('mcp-proxy')
|
|
63
|
+
.description('Register a local MCP command as a proxy')
|
|
64
|
+
.requiredOption('--mcp-server <endpoint>', 'Server endpoint URL')
|
|
65
|
+
.requiredOption('--mcp-key <key>', 'MCP Proxy Key')
|
|
66
|
+
.requiredOption('--command <command>', 'Local MCP command to proxy')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
await startMcpRegistration({
|
|
69
|
+
server: options.mcpServer,
|
|
70
|
+
key: options.mcpKey,
|
|
71
|
+
command: options.command,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
program
|
|
76
|
+
.command('mcp')
|
|
77
|
+
.description('Start stdio MCP proxy (HTTP to stdio)')
|
|
78
|
+
.requiredOption('--endpoint <url>', 'MCP forwarder endpoint URL')
|
|
79
|
+
.requiredOption('--token <token>', 'Bearer token for authentication')
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
await startMcpProxy({
|
|
82
|
+
endpoint: options.endpoint,
|
|
83
|
+
token: options.token,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
59
87
|
program.parse();
|
package/src/socketio-client.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { io as createSocketIOClient, type Socket } from 'socket.io-client';
|
|
2
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
2
3
|
import { hostname, platform } from 'os';
|
|
3
4
|
import { readdir, access } from 'fs/promises';
|
|
4
5
|
import { join } from 'path';
|
|
5
6
|
import { homedir } from 'os';
|
|
6
7
|
import { Heartbeat } from './heartbeat.js';
|
|
7
|
-
import { clearConfigKey, saveKeyToConfig, saveEndpointToConfig } from './config.js';
|
|
8
|
+
import { clearConfigKey, saveKeyToConfig, saveEndpointToConfig, getResumeTokenFromConfig, saveResumeTokenToConfig, clearResumeTokenFromConfig } from './config.js';
|
|
8
9
|
import { acpDetector } from './acp/detector.js';
|
|
9
10
|
import { AcpSpawner } from './acp/spawner.js';
|
|
10
11
|
import { AcpSessionBridge } from './acp/bridge.js';
|
|
@@ -14,8 +15,9 @@ import { worktreeHandler } from './worktree-handler.js';
|
|
|
14
15
|
import { fsHandler } from './fs-handler.js';
|
|
15
16
|
import { portForwardManager } from './port-forward.js';
|
|
16
17
|
|
|
17
|
-
const CONTROLLER_VERSION = '1.
|
|
18
|
+
const CONTROLLER_VERSION = '1.1.1';
|
|
18
19
|
const acpBridges = new Map<string, AcpSessionBridge>();
|
|
20
|
+
const permissionRequestBridge = new Map<string, AcpSessionBridge>();
|
|
19
21
|
|
|
20
22
|
function createSpawner(serverType: string, sessionId: string, cwd: string): AcpSpawner {
|
|
21
23
|
const config = acpDetector.getSpawnerConfig(serverType);
|
|
@@ -24,6 +26,7 @@ function createSpawner(serverType: string, sessionId: string, cwd: string): AcpS
|
|
|
24
26
|
|
|
25
27
|
interface RegisteredPayload {
|
|
26
28
|
controllerId: string;
|
|
29
|
+
resumeToken?: string;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
interface ErrorPayload {
|
|
@@ -155,22 +158,32 @@ export function connectToServer(
|
|
|
155
158
|
|
|
156
159
|
socket.on('connect', () => {
|
|
157
160
|
console.log(`Connected to ${endpoint}`);
|
|
161
|
+
const resumeToken = getResumeTokenFromConfig();
|
|
158
162
|
socket.emit('register', {
|
|
159
163
|
key,
|
|
160
164
|
hostname: controllerName,
|
|
161
165
|
version: CONTROLLER_VERSION,
|
|
166
|
+
resumeToken,
|
|
162
167
|
});
|
|
163
168
|
});
|
|
164
169
|
|
|
165
170
|
socket.on('registered', (data: RegisteredPayload) => {
|
|
166
171
|
console.log(`Controller registered: ${data.controllerId}`);
|
|
172
|
+
if (data.resumeToken) {
|
|
173
|
+
saveResumeTokenToConfig(data.resumeToken);
|
|
174
|
+
console.log('Resume token saved to config');
|
|
175
|
+
}
|
|
167
176
|
heartbeat.start(() => socket.emit('heartbeat'));
|
|
168
177
|
});
|
|
169
178
|
|
|
170
179
|
socket.on('error', (data: ErrorPayload) => {
|
|
171
180
|
console.error(`Server error: ${data.code} - ${data.message}`);
|
|
172
181
|
if (data.code === 'KEY_ALREADY_ONLINE') {
|
|
173
|
-
console.
|
|
182
|
+
console.log('Clearing resume token and retrying connection...');
|
|
183
|
+
clearResumeTokenFromConfig();
|
|
184
|
+
// Don't exit, let the reconnection logic handle it
|
|
185
|
+
socket.disconnect();
|
|
186
|
+
return;
|
|
174
187
|
}
|
|
175
188
|
socket.disconnect();
|
|
176
189
|
process.exit(1);
|
|
@@ -215,6 +228,10 @@ export function connectToServer(
|
|
|
215
228
|
|
|
216
229
|
acpBridges.set(data.sessionId, bridge);
|
|
217
230
|
|
|
231
|
+
bridge.on('permission:sent', (requestId: string) => {
|
|
232
|
+
permissionRequestBridge.set(requestId, bridge);
|
|
233
|
+
});
|
|
234
|
+
|
|
218
235
|
const onInitialized = (payload: Record<string, unknown>) => {
|
|
219
236
|
if (payload.sessionId !== data.sessionId) return;
|
|
220
237
|
bridge.off('acp:initialized', onInitialized);
|
|
@@ -226,10 +243,10 @@ export function connectToServer(
|
|
|
226
243
|
success: true,
|
|
227
244
|
externalSessionId: payload.acpSessionId,
|
|
228
245
|
};
|
|
229
|
-
|
|
246
|
+
result.modes = payload.modes ?? [];
|
|
230
247
|
if (payload.currentModeId) result.currentModeId = payload.currentModeId;
|
|
231
248
|
if (payload.currentModel) result.currentModel = payload.currentModel;
|
|
232
|
-
|
|
249
|
+
result.availableModels = payload.availableModels ?? [];
|
|
233
250
|
if (payload.currentModelId) result.currentModelId = payload.currentModelId;
|
|
234
251
|
socket.emit('acp:start:result', result);
|
|
235
252
|
};
|
|
@@ -276,6 +293,10 @@ export function connectToServer(
|
|
|
276
293
|
|
|
277
294
|
acpBridges.set(data.sessionId, bridge);
|
|
278
295
|
|
|
296
|
+
bridge.on('permission:sent', (requestId: string) => {
|
|
297
|
+
permissionRequestBridge.set(requestId, bridge);
|
|
298
|
+
});
|
|
299
|
+
|
|
279
300
|
const onInitialized = (payload: Record<string, unknown>) => {
|
|
280
301
|
if (payload.sessionId !== data.sessionId) return;
|
|
281
302
|
bridge.off('acp:initialized', onInitialized);
|
|
@@ -285,10 +306,10 @@ export function connectToServer(
|
|
|
285
306
|
sessionId: data.sessionId,
|
|
286
307
|
success: true,
|
|
287
308
|
};
|
|
288
|
-
|
|
309
|
+
result.modes = payload.modes ?? [];
|
|
289
310
|
if (payload.currentModeId) result.currentModeId = payload.currentModeId;
|
|
290
311
|
if (payload.currentModel) result.currentModel = payload.currentModel;
|
|
291
|
-
|
|
312
|
+
result.availableModels = payload.availableModels ?? [];
|
|
292
313
|
if (payload.currentModelId) result.currentModelId = payload.currentModelId;
|
|
293
314
|
socket.emit('acp:resume:result', result);
|
|
294
315
|
};
|
|
@@ -579,10 +600,22 @@ export function connectToServer(
|
|
|
579
600
|
portForwardManager.closeTunnel(data.tunnelId);
|
|
580
601
|
});
|
|
581
602
|
|
|
603
|
+
socket.on('acp:permission:response', (data: {
|
|
604
|
+
requestId: string;
|
|
605
|
+
outcome: { outcome: string; optionId?: string };
|
|
606
|
+
}) => {
|
|
607
|
+
const bridge = permissionRequestBridge.get(data.requestId);
|
|
608
|
+
if (bridge) {
|
|
609
|
+
permissionRequestBridge.delete(data.requestId);
|
|
610
|
+
bridge.handlePermissionResponse(data.requestId, data.outcome as acp.RequestPermissionOutcome);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
582
614
|
socket.on('disconnect', (reason) => {
|
|
583
615
|
console.log(`Disconnected: ${reason}`);
|
|
584
616
|
heartbeat.stop();
|
|
585
617
|
portForwardManager.closeAll();
|
|
618
|
+
permissionRequestBridge.clear();
|
|
586
619
|
});
|
|
587
620
|
|
|
588
621
|
socket.on('connect_error', (err) => {
|
package/tests/acp-bridge.test.ts
CHANGED
|
@@ -449,18 +449,45 @@ describe('AcpSessionBridge', () => {
|
|
|
449
449
|
});
|
|
450
450
|
|
|
451
451
|
describe('requestPermission', () => {
|
|
452
|
-
it('should
|
|
452
|
+
it('should emit acp:permission and wait for response', async () => {
|
|
453
453
|
const bridge = createBridge();
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
454
|
+
const options = [
|
|
455
|
+
{ optionId: 'opt-1', kind: 'allow_once', label: 'Allow' },
|
|
456
|
+
{ optionId: 'opt-2', kind: 'reject', label: 'Reject' },
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
const promise = bridge.requestPermission({
|
|
460
|
+
options,
|
|
461
|
+
toolCall: { toolCallId: 'tc-1' },
|
|
459
462
|
} as never);
|
|
460
463
|
|
|
461
|
-
expect(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
+
expect(mockSocket.emit).toHaveBeenCalledWith('acp:permission', expect.objectContaining({
|
|
465
|
+
sessionId: 'sess-1',
|
|
466
|
+
toolCall: { toolCallId: 'tc-1' },
|
|
467
|
+
options,
|
|
468
|
+
}));
|
|
469
|
+
|
|
470
|
+
const emitCall = mockSocket.emit.mock.calls.find((c: unknown[]) => c[0] === 'acp:permission');
|
|
471
|
+
const requestId = (emitCall![1] as { requestId: string }).requestId;
|
|
472
|
+
|
|
473
|
+
bridge.handlePermissionResponse(requestId, { outcome: 'selected', optionId: 'opt-1' });
|
|
474
|
+
|
|
475
|
+
const result = await promise;
|
|
476
|
+
expect(result).toEqual({ outcome: { outcome: 'selected', optionId: 'opt-1' } });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should resolve with cancelled on 65s timeout', async () => {
|
|
480
|
+
const bridge = createBridge();
|
|
481
|
+
|
|
482
|
+
const promise = bridge.requestPermission({
|
|
483
|
+
options: [{ optionId: 'opt-1', kind: 'allow_once', label: 'Allow' }],
|
|
484
|
+
toolCall: { toolCallId: 'tc-1' },
|
|
485
|
+
} as never);
|
|
486
|
+
|
|
487
|
+
await vi.advanceTimersByTimeAsync(65_000);
|
|
488
|
+
|
|
489
|
+
const result = await promise;
|
|
490
|
+
expect(result).toEqual({ outcome: { outcome: 'cancelled' } });
|
|
464
491
|
});
|
|
465
492
|
});
|
|
466
493
|
|
|
@@ -509,26 +536,26 @@ describe('AcpSessionBridge', () => {
|
|
|
509
536
|
});
|
|
510
537
|
|
|
511
538
|
describe('extractAvailableModels', () => {
|
|
512
|
-
it('should return
|
|
539
|
+
it('should return empty array when models is null', () => {
|
|
513
540
|
const bridge = createBridge();
|
|
514
541
|
|
|
515
542
|
// Access private method via bracket notation
|
|
516
543
|
const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels(null);
|
|
517
|
-
expect(result).
|
|
544
|
+
expect(result).toEqual([]);
|
|
518
545
|
});
|
|
519
546
|
|
|
520
|
-
it('should return
|
|
547
|
+
it('should return empty array when models is undefined', () => {
|
|
521
548
|
const bridge = createBridge();
|
|
522
549
|
|
|
523
550
|
const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels(undefined);
|
|
524
|
-
expect(result).
|
|
551
|
+
expect(result).toEqual([]);
|
|
525
552
|
});
|
|
526
553
|
|
|
527
|
-
it('should return
|
|
554
|
+
it('should return empty array when availableModels is missing', () => {
|
|
528
555
|
const bridge = createBridge();
|
|
529
556
|
|
|
530
557
|
const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels({});
|
|
531
|
-
expect(result).
|
|
558
|
+
expect(result).toEqual([]);
|
|
532
559
|
});
|
|
533
560
|
});
|
|
534
561
|
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('child_process', () => ({
|
|
4
|
+
spawn: vi.fn().mockReturnValue({
|
|
5
|
+
stdin: { write: vi.fn() },
|
|
6
|
+
stdout: { on: vi.fn(), removeListener: vi.fn() },
|
|
7
|
+
on: vi.fn(),
|
|
8
|
+
kill: vi.fn(),
|
|
9
|
+
}),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('socket.io-client', () => ({
|
|
13
|
+
io: vi.fn().mockReturnValue({
|
|
14
|
+
on: vi.fn(),
|
|
15
|
+
emit: vi.fn(),
|
|
16
|
+
disconnect: vi.fn(),
|
|
17
|
+
connected: true,
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('readline', () => ({
|
|
22
|
+
createInterface: vi.fn().mockReturnValue({
|
|
23
|
+
on: vi.fn(),
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { startMcpRegistration } from '../../src/commands/mcp-proxy.js';
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
32
|
+
json: () => Promise.resolve({ success: true, proxyId: 'proxy_1', serverId: 'server_1' }),
|
|
33
|
+
}));
|
|
34
|
+
vi.stubGlobal('process', {
|
|
35
|
+
...process,
|
|
36
|
+
env: { HOSTNAME: 'test-host' },
|
|
37
|
+
on: vi.fn(),
|
|
38
|
+
exit: vi.fn(),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('mcp-proxy command', () => {
|
|
43
|
+
it('should register with server and spawn child process', async () => {
|
|
44
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
45
|
+
|
|
46
|
+
await startMcpRegistration({
|
|
47
|
+
server: 'http://localhost:4001',
|
|
48
|
+
key: 'mcp_test-key',
|
|
49
|
+
command: 'npx @modelcontextprotocol/server-github',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(fetch).toHaveBeenCalledWith('http://localhost:4001/api/mcp-proxy/register', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
key: 'mcp_test-key',
|
|
57
|
+
hostname: 'test-host',
|
|
58
|
+
command: 'npx @modelcontextprotocol/server-github',
|
|
59
|
+
serverName: '@modelcontextprotocol/server-github',
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
consoleSpy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should exit if registration fails', async () => {
|
|
67
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
68
|
+
json: () => Promise.resolve({ success: false, error: 'Invalid key' }),
|
|
69
|
+
}));
|
|
70
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
71
|
+
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
|
72
|
+
|
|
73
|
+
await startMcpRegistration({
|
|
74
|
+
server: 'http://localhost:4001',
|
|
75
|
+
key: 'invalid-key',
|
|
76
|
+
command: 'npx @modelcontextprotocol/server-github',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
80
|
+
|
|
81
|
+
consoleErrorSpy.mockRestore();
|
|
82
|
+
processExitSpy.mockRestore();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should use correct option names for CLI parsing', () => {
|
|
86
|
+
// This test verifies that the CLI options are correctly named
|
|
87
|
+
// to avoid conflicts with global options
|
|
88
|
+
const { Command } = require('commander');
|
|
89
|
+
const program = new Command();
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command('mcp-proxy')
|
|
93
|
+
.description('Register a local MCP command as a proxy')
|
|
94
|
+
.requiredOption('--mcp-server <endpoint>', 'Server endpoint URL')
|
|
95
|
+
.requiredOption('--mcp-key <key>', 'MCP Proxy Key')
|
|
96
|
+
.requiredOption('--command <command>', 'Local MCP command to proxy');
|
|
97
|
+
|
|
98
|
+
// Verify the command has the correct options
|
|
99
|
+
const cmd = program.commands.find((c: any) => c.name() === 'mcp-proxy');
|
|
100
|
+
expect(cmd).toBeDefined();
|
|
101
|
+
|
|
102
|
+
const options = cmd.options.map((o: any) => o.long);
|
|
103
|
+
expect(options).toContain('--mcp-server');
|
|
104
|
+
expect(options).toContain('--mcp-key');
|
|
105
|
+
expect(options).toContain('--command');
|
|
106
|
+
|
|
107
|
+
// Verify these don't conflict with global options
|
|
108
|
+
expect(options).not.toContain('--key');
|
|
109
|
+
expect(options).not.toContain('--server');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('readline', () => ({
|
|
4
|
+
createInterface: vi.fn().mockReturnValue({
|
|
5
|
+
on: vi.fn(),
|
|
6
|
+
}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { startMcpProxy } from '../../src/commands/mcp.js';
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
14
|
+
json: () => Promise.resolve({ jsonrpc: '2.0', id: 1, result: { tools: [] } }),
|
|
15
|
+
}));
|
|
16
|
+
vi.stubGlobal('process', {
|
|
17
|
+
...process,
|
|
18
|
+
stdout: { write: vi.fn() },
|
|
19
|
+
exit: vi.fn(),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('mcp command', () => {
|
|
24
|
+
it('should start stdio proxy with endpoint and token', async () => {
|
|
25
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
26
|
+
const readline = await import('readline');
|
|
27
|
+
const mockRl = {
|
|
28
|
+
on: vi.fn(),
|
|
29
|
+
};
|
|
30
|
+
vi.mocked(readline.createInterface).mockReturnValue(mockRl as never);
|
|
31
|
+
|
|
32
|
+
await startMcpProxy({
|
|
33
|
+
endpoint: 'http://localhost:3002/mcp/session-1',
|
|
34
|
+
token: 'test-token',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(readline.createInterface).toHaveBeenCalledWith({ input: process.stdin });
|
|
38
|
+
expect(mockRl.on).toHaveBeenCalledWith('line', expect.any(Function));
|
|
39
|
+
expect(mockRl.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
40
|
+
|
|
41
|
+
consoleSpy.mockRestore();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -21,6 +21,9 @@ vi.mock('../src/config.js', () => ({
|
|
|
21
21
|
clearConfigKey: vi.fn(),
|
|
22
22
|
saveKeyToConfig: vi.fn(),
|
|
23
23
|
saveEndpointToConfig: vi.fn(),
|
|
24
|
+
getResumeTokenFromConfig: vi.fn(),
|
|
25
|
+
saveResumeTokenToConfig: vi.fn(),
|
|
26
|
+
clearResumeTokenFromConfig: vi.fn(),
|
|
24
27
|
}));
|
|
25
28
|
|
|
26
29
|
vi.mock('os', () => ({
|
|
@@ -101,7 +104,8 @@ describe('SocketIO Client', () => {
|
|
|
101
104
|
expect(mockSocket.emit).toHaveBeenCalledWith('register', {
|
|
102
105
|
key: 'zag_k_1_abc123',
|
|
103
106
|
hostname: 'my-machine',
|
|
104
|
-
version: '1.
|
|
107
|
+
version: '1.1.1',
|
|
108
|
+
resumeToken: undefined,
|
|
105
109
|
});
|
|
106
110
|
});
|
|
107
111
|
|
|
@@ -116,7 +120,8 @@ describe('SocketIO Client', () => {
|
|
|
116
120
|
expect(mockSocket.emit).toHaveBeenCalledWith('register', {
|
|
117
121
|
key: 'zag_k_1_abc123',
|
|
118
122
|
hostname: 'test-machine',
|
|
119
|
-
version: '1.
|
|
123
|
+
version: '1.1.1',
|
|
124
|
+
resumeToken: undefined,
|
|
120
125
|
});
|
|
121
126
|
});
|
|
122
127
|
|