agent-gear 1.1.1 → 1.1.4
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 +33 -0
- package/dist/acp/bridge.js +2 -2
- package/dist/acp/bridge.js.map +1 -1
- package/dist/acp/detector.d.ts +3 -0
- package/dist/acp/detector.d.ts.map +1 -1
- package/dist/acp/detector.js +41 -2
- package/dist/acp/detector.js.map +1 -1
- 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/{port-forward.d.ts → gateway.d.ts} +7 -3
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +158 -0
- package/dist/gateway.js.map +1 -0
- package/dist/gateway.test.d.ts +2 -0
- package/dist/gateway.test.d.ts.map +1 -0
- package/dist/gateway.test.js +241 -0
- package/dist/gateway.test.js.map +1 -0
- 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 +20 -20
- package/dist/socketio-client.js.map +1 -1
- package/package.json +1 -1
- package/src/acp/bridge.ts +4 -4
- package/src/acp/detector.ts +41 -2
- package/src/commands/mcp-proxy.ts +128 -0
- package/src/commands/mcp.ts +41 -0
- package/src/gateway.test.ts +301 -0
- package/src/gateway.ts +195 -0
- package/src/index.ts +28 -0
- package/src/socketio-client.ts +22 -16
- package/tests/acp-bridge.test.ts +6 -6
- package/tests/acp-detector.test.ts +68 -26
- package/tests/commands/mcp-proxy.test.ts +111 -0
- package/tests/commands/mcp.test.ts +43 -0
- package/dist/port-forward.d.ts.map +0 -1
- package/dist/port-forward.js +0 -68
- package/dist/port-forward.js.map +0 -1
- package/dist/port-forward.test.d.ts +0 -2
- package/dist/port-forward.test.d.ts.map +0 -1
- package/dist/port-forward.test.js +0 -163
- package/dist/port-forward.test.js.map +0 -1
- package/src/port-forward.test.ts +0 -202
- package/src/port-forward.ts +0 -88
package/src/socketio-client.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { AcpSyncBridge } from './acp/sync-bridge.js';
|
|
|
13
13
|
import { gitHandler } from './git-handler.js';
|
|
14
14
|
import { worktreeHandler } from './worktree-handler.js';
|
|
15
15
|
import { fsHandler } from './fs-handler.js';
|
|
16
|
-
import {
|
|
16
|
+
import { gatewayManager } from './gateway.js';
|
|
17
17
|
|
|
18
18
|
const CONTROLLER_VERSION = '1.1.1';
|
|
19
19
|
const acpBridges = new Map<string, AcpSessionBridge>();
|
|
@@ -83,12 +83,14 @@ interface AcpResumePayload {
|
|
|
83
83
|
|
|
84
84
|
interface AcpListSessionsPayload {
|
|
85
85
|
requestId: string;
|
|
86
|
+
serverType: string;
|
|
86
87
|
cwd: string;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
interface AcpLoadSessionPayload {
|
|
90
91
|
requestId: string;
|
|
91
92
|
sessionId: string;
|
|
93
|
+
serverType: string;
|
|
92
94
|
cwd: string;
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -243,10 +245,10 @@ export function connectToServer(
|
|
|
243
245
|
success: true,
|
|
244
246
|
externalSessionId: payload.acpSessionId,
|
|
245
247
|
};
|
|
246
|
-
|
|
248
|
+
result.modes = payload.modes ?? [];
|
|
247
249
|
if (payload.currentModeId) result.currentModeId = payload.currentModeId;
|
|
248
250
|
if (payload.currentModel) result.currentModel = payload.currentModel;
|
|
249
|
-
|
|
251
|
+
result.availableModels = payload.availableModels ?? [];
|
|
250
252
|
if (payload.currentModelId) result.currentModelId = payload.currentModelId;
|
|
251
253
|
socket.emit('acp:start:result', result);
|
|
252
254
|
};
|
|
@@ -306,10 +308,10 @@ export function connectToServer(
|
|
|
306
308
|
sessionId: data.sessionId,
|
|
307
309
|
success: true,
|
|
308
310
|
};
|
|
309
|
-
|
|
311
|
+
result.modes = payload.modes ?? [];
|
|
310
312
|
if (payload.currentModeId) result.currentModeId = payload.currentModeId;
|
|
311
313
|
if (payload.currentModel) result.currentModel = payload.currentModel;
|
|
312
|
-
|
|
314
|
+
result.availableModels = payload.availableModels ?? [];
|
|
313
315
|
if (payload.currentModelId) result.currentModelId = payload.currentModelId;
|
|
314
316
|
socket.emit('acp:resume:result', result);
|
|
315
317
|
};
|
|
@@ -343,8 +345,10 @@ export function connectToServer(
|
|
|
343
345
|
});
|
|
344
346
|
|
|
345
347
|
socket.on('acp:list-sessions', async (data: AcpListSessionsPayload) => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
+
const serverType = data.serverType ?? 'opencode';
|
|
349
|
+
console.log(`[ACP sync] acp:list-sessions received: requestId=${data.requestId} serverType=${serverType} cwd=${data.cwd}`);
|
|
350
|
+
const spawner = createSpawner(serverType, `sync_${Date.now()}`, data.cwd);
|
|
351
|
+
const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
|
|
348
352
|
try {
|
|
349
353
|
await bridge.connect();
|
|
350
354
|
const result = await bridge.listSessions(data.cwd);
|
|
@@ -371,8 +375,10 @@ export function connectToServer(
|
|
|
371
375
|
});
|
|
372
376
|
|
|
373
377
|
socket.on('acp:load-session', async (data: AcpLoadSessionPayload) => {
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
const serverType = data.serverType ?? 'opencode';
|
|
379
|
+
console.log(`[ACP] load-session received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${serverType} cwd=${data.cwd}`);
|
|
380
|
+
const spawner = createSpawner(serverType, `sync_${Date.now()}`, data.cwd);
|
|
381
|
+
const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
|
|
376
382
|
try {
|
|
377
383
|
await bridge.connect();
|
|
378
384
|
const { config, updates } = await bridge.loadSession(data.sessionId, data.cwd);
|
|
@@ -585,19 +591,19 @@ export function connectToServer(
|
|
|
585
591
|
socket.emit('git:commit:result', { requestId: data.requestId, ...result });
|
|
586
592
|
});
|
|
587
593
|
|
|
588
|
-
// Port Forward events
|
|
594
|
+
// Port Forward events (event names kept unchanged for backward compatibility)
|
|
589
595
|
socket.on('port-forward:create', async (data: { tunnelId: string; port: number }) => {
|
|
590
|
-
console.log(`
|
|
591
|
-
await
|
|
596
|
+
console.log(`Gateway tunnel create requested: tunnelId=${data.tunnelId} port=${data.port}`);
|
|
597
|
+
await gatewayManager.createTunnel(data.tunnelId, data.port, socket);
|
|
592
598
|
});
|
|
593
599
|
|
|
594
600
|
socket.on('port-forward:data', (data: { tunnelId: string; data: string }) => {
|
|
595
|
-
|
|
601
|
+
gatewayManager.handleTunnelData(data.tunnelId, data.data);
|
|
596
602
|
});
|
|
597
603
|
|
|
598
604
|
socket.on('port-forward:close', (data: { tunnelId: string }) => {
|
|
599
|
-
console.log(`
|
|
600
|
-
|
|
605
|
+
console.log(`Gateway tunnel close: tunnelId=${data.tunnelId}`);
|
|
606
|
+
gatewayManager.closeTunnel(data.tunnelId);
|
|
601
607
|
});
|
|
602
608
|
|
|
603
609
|
socket.on('acp:permission:response', (data: {
|
|
@@ -614,7 +620,7 @@ export function connectToServer(
|
|
|
614
620
|
socket.on('disconnect', (reason) => {
|
|
615
621
|
console.log(`Disconnected: ${reason}`);
|
|
616
622
|
heartbeat.stop();
|
|
617
|
-
|
|
623
|
+
gatewayManager.closeAll();
|
|
618
624
|
permissionRequestBridge.clear();
|
|
619
625
|
});
|
|
620
626
|
|
package/tests/acp-bridge.test.ts
CHANGED
|
@@ -536,26 +536,26 @@ describe('AcpSessionBridge', () => {
|
|
|
536
536
|
});
|
|
537
537
|
|
|
538
538
|
describe('extractAvailableModels', () => {
|
|
539
|
-
it('should return
|
|
539
|
+
it('should return empty array when models is null', () => {
|
|
540
540
|
const bridge = createBridge();
|
|
541
541
|
|
|
542
542
|
// Access private method via bracket notation
|
|
543
543
|
const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels(null);
|
|
544
|
-
expect(result).
|
|
544
|
+
expect(result).toEqual([]);
|
|
545
545
|
});
|
|
546
546
|
|
|
547
|
-
it('should return
|
|
547
|
+
it('should return empty array when models is undefined', () => {
|
|
548
548
|
const bridge = createBridge();
|
|
549
549
|
|
|
550
550
|
const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels(undefined);
|
|
551
|
-
expect(result).
|
|
551
|
+
expect(result).toEqual([]);
|
|
552
552
|
});
|
|
553
553
|
|
|
554
|
-
it('should return
|
|
554
|
+
it('should return empty array when availableModels is missing', () => {
|
|
555
555
|
const bridge = createBridge();
|
|
556
556
|
|
|
557
557
|
const result = (bridge as unknown as Record<string, unknown>).extractAvailableModels({});
|
|
558
|
-
expect(result).
|
|
558
|
+
expect(result).toEqual([]);
|
|
559
559
|
});
|
|
560
560
|
});
|
|
561
561
|
});
|
|
@@ -32,71 +32,98 @@ describe('AcpDetector', () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
describe('detect', () => {
|
|
35
|
-
it('should detect
|
|
35
|
+
it('should detect all 5 servers when all available', async () => {
|
|
36
36
|
mockExecSuccessOnce('opencode v1.2.3\n');
|
|
37
37
|
mockExecSuccessOnce('copilot 1.0.0\n');
|
|
38
|
+
mockExecSuccessOnce('claude-agent-acp 0.1.0\n');
|
|
39
|
+
mockExecSuccessOnce('gemini 2.0.0\n');
|
|
40
|
+
mockExecSuccessOnce('codex-acp 0.5.0\n');
|
|
38
41
|
|
|
39
42
|
const result = await detector.detect();
|
|
40
43
|
|
|
41
|
-
expect(result).toHaveLength(
|
|
44
|
+
expect(result).toHaveLength(5);
|
|
42
45
|
expect(result[0]).toEqual({ type: 'opencode', available: true, version: '1.2.3' });
|
|
43
46
|
expect(result[1]).toEqual({ type: 'github_copilot', available: true, version: '1.0.0' });
|
|
47
|
+
expect(result[2]).toEqual({ type: 'claude_code', available: true, version: '0.1.0' });
|
|
48
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: true, version: '2.0.0' });
|
|
49
|
+
expect(result[4]).toEqual({ type: 'codex', available: true, version: '0.5.0' });
|
|
44
50
|
});
|
|
45
51
|
|
|
46
52
|
it('should detect as unavailable when command fails', async () => {
|
|
47
53
|
mockExecFailureOnce();
|
|
48
54
|
mockExecFailureOnce();
|
|
55
|
+
mockExecFailureOnce();
|
|
56
|
+
mockExecFailureOnce();
|
|
57
|
+
mockExecFailureOnce();
|
|
49
58
|
|
|
50
59
|
const result = await detector.detect();
|
|
51
60
|
|
|
52
|
-
expect(result).toHaveLength(
|
|
61
|
+
expect(result).toHaveLength(5);
|
|
53
62
|
expect(result[0]).toEqual({ type: 'opencode', available: false });
|
|
54
63
|
expect(result[1]).toEqual({ type: 'github_copilot', available: false });
|
|
64
|
+
expect(result[2]).toEqual({ type: 'claude_code', available: false });
|
|
65
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: false });
|
|
66
|
+
expect(result[4]).toEqual({ type: 'codex', available: false });
|
|
55
67
|
});
|
|
56
68
|
|
|
57
|
-
it('should
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
it('should detect claude_code only when other servers fail', async () => {
|
|
70
|
+
mockExecFailureOnce();
|
|
71
|
+
mockExecFailureOnce();
|
|
72
|
+
mockExecSuccessOnce('claude-agent-acp 0.1.0\n');
|
|
73
|
+
mockExecFailureOnce();
|
|
74
|
+
mockExecFailureOnce();
|
|
60
75
|
|
|
61
76
|
const result = await detector.detect();
|
|
62
77
|
|
|
63
|
-
expect(result
|
|
64
|
-
expect(result[
|
|
78
|
+
expect(result).toHaveLength(5);
|
|
79
|
+
expect(result[0]).toEqual({ type: 'opencode', available: false });
|
|
80
|
+
expect(result[1]).toEqual({ type: 'github_copilot', available: false });
|
|
81
|
+
expect(result[2]).toEqual({ type: 'claude_code', available: true, version: '0.1.0' });
|
|
82
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: false });
|
|
83
|
+
expect(result[4]).toEqual({ type: 'codex', available: false });
|
|
65
84
|
});
|
|
66
85
|
|
|
67
|
-
it('should
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
it('should detect gemini_cli only when other servers fail', async () => {
|
|
87
|
+
mockExecFailureOnce();
|
|
88
|
+
mockExecFailureOnce();
|
|
89
|
+
mockExecFailureOnce();
|
|
90
|
+
mockExecSuccessOnce('gemini 2.0.0\n');
|
|
91
|
+
mockExecFailureOnce();
|
|
70
92
|
|
|
71
93
|
const result = await detector.detect();
|
|
72
94
|
|
|
73
|
-
expect(result
|
|
74
|
-
expect(result[
|
|
95
|
+
expect(result).toHaveLength(5);
|
|
96
|
+
expect(result[3]).toEqual({ type: 'gemini_cli', available: true, version: '2.0.0' });
|
|
75
97
|
});
|
|
76
98
|
|
|
77
|
-
it('should detect only
|
|
78
|
-
|
|
99
|
+
it('should detect codex only when other servers fail', async () => {
|
|
100
|
+
mockExecFailureOnce();
|
|
101
|
+
mockExecFailureOnce();
|
|
79
102
|
mockExecFailureOnce();
|
|
103
|
+
mockExecFailureOnce();
|
|
104
|
+
mockExecSuccessOnce('codex-acp 0.5.0\n');
|
|
80
105
|
|
|
81
106
|
const result = await detector.detect();
|
|
82
107
|
|
|
83
|
-
expect(result).toHaveLength(
|
|
84
|
-
expect(result[
|
|
85
|
-
expect(result[1]).toEqual({ type: 'github_copilot', available: false });
|
|
108
|
+
expect(result).toHaveLength(5);
|
|
109
|
+
expect(result[4]).toEqual({ type: 'codex', available: true, version: '0.5.0' });
|
|
86
110
|
});
|
|
87
111
|
|
|
88
|
-
it('should
|
|
89
|
-
|
|
90
|
-
mockExecSuccessOnce('copilot
|
|
112
|
+
it('should parse version with different formats', async () => {
|
|
113
|
+
mockExecSuccessOnce('OpenCode version 2.0.1\n');
|
|
114
|
+
mockExecSuccessOnce('copilot 3.0.0\n');
|
|
115
|
+
mockExecSuccessOnce('v0.1.0\n');
|
|
116
|
+
mockExecSuccessOnce('Gemini 2.0.0\n');
|
|
117
|
+
mockExecSuccessOnce('codex-acp version 0.5.0\n');
|
|
91
118
|
|
|
92
119
|
const result = await detector.detect();
|
|
93
120
|
|
|
94
|
-
expect(result).
|
|
95
|
-
expect(result[
|
|
96
|
-
expect(result[
|
|
121
|
+
expect(result[0].version).toBe('2.0.1');
|
|
122
|
+
expect(result[1].version).toBe('3.0.0');
|
|
123
|
+
expect(result[2].version).toBe('0.1.0');
|
|
124
|
+
expect(result[3].version).toBe('2.0.0');
|
|
125
|
+
expect(result[4].version).toBe('0.5.0');
|
|
97
126
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
127
|
});
|
|
101
128
|
|
|
102
129
|
describe('getSpawnerConfig', () => {
|
|
@@ -110,6 +137,21 @@ describe('AcpDetector', () => {
|
|
|
110
137
|
expect(config).toEqual({ command: 'copilot', args: ['--acp', '--stdio'] });
|
|
111
138
|
});
|
|
112
139
|
|
|
140
|
+
it('should return claude_code spawner config', () => {
|
|
141
|
+
const config = detector.getSpawnerConfig('claude_code');
|
|
142
|
+
expect(config).toEqual({ command: 'claude-agent-acp', args: [] });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return gemini_cli spawner config', () => {
|
|
146
|
+
const config = detector.getSpawnerConfig('gemini_cli');
|
|
147
|
+
expect(config).toEqual({ command: 'gemini', args: ['--acp'] });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should return codex spawner config', () => {
|
|
151
|
+
const config = detector.getSpawnerConfig('codex');
|
|
152
|
+
expect(config).toEqual({ command: 'codex-acp', args: [] });
|
|
153
|
+
});
|
|
154
|
+
|
|
113
155
|
it('should return opencode as default for unknown serverType', () => {
|
|
114
156
|
const config = detector.getSpawnerConfig('unknown_server');
|
|
115
157
|
expect(config).toEqual({ command: 'opencode', args: ['acp'] });
|
|
@@ -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
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"port-forward.d.ts","sourceRoot":"","sources":["../src/port-forward.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAQ/C,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,OAAO,CAAkC;IAE3C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDvF,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAStD,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQnC,QAAQ,IAAI,IAAI;CAOjB;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAC"}
|
package/dist/port-forward.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import net from 'node:net';
|
|
2
|
-
export class PortForwardManager {
|
|
3
|
-
tunnels = new Map();
|
|
4
|
-
async createTunnel(tunnelId, port, serverSocket) {
|
|
5
|
-
const localSocket = net.connect({ host: '127.0.0.1', port });
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
const onError = (err) => {
|
|
8
|
-
console.error(`Port forward tunnel error: tunnelId=${tunnelId} error=${err.message}`);
|
|
9
|
-
serverSocket.emit('port-forward:created', {
|
|
10
|
-
tunnelId,
|
|
11
|
-
success: false,
|
|
12
|
-
error: err.message,
|
|
13
|
-
});
|
|
14
|
-
localSocket.destroy();
|
|
15
|
-
resolve();
|
|
16
|
-
};
|
|
17
|
-
localSocket.once('error', onError);
|
|
18
|
-
localSocket.once('connect', () => {
|
|
19
|
-
localSocket.removeListener('error', onError);
|
|
20
|
-
this.tunnels.set(tunnelId, { id: tunnelId, port, localSocket });
|
|
21
|
-
console.log(`Port forward tunnel created: tunnelId=${tunnelId} port=${port}`);
|
|
22
|
-
serverSocket.emit('port-forward:created', { tunnelId, success: true });
|
|
23
|
-
localSocket.on('data', (data) => {
|
|
24
|
-
serverSocket.emit('port-forward:data', {
|
|
25
|
-
tunnelId,
|
|
26
|
-
data: data.toString('base64'),
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
localSocket.on('close', () => {
|
|
30
|
-
console.log(`Port forward local socket closed: tunnelId=${tunnelId}`);
|
|
31
|
-
this.tunnels.delete(tunnelId);
|
|
32
|
-
serverSocket.emit('port-forward:close', { tunnelId });
|
|
33
|
-
});
|
|
34
|
-
localSocket.on('error', (err) => {
|
|
35
|
-
console.error(`Port forward local socket error: tunnelId=${tunnelId} error=${err.message}`);
|
|
36
|
-
this.tunnels.delete(tunnelId);
|
|
37
|
-
serverSocket.emit('port-forward:error', { tunnelId, error: err.message });
|
|
38
|
-
});
|
|
39
|
-
resolve();
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
handleTunnelData(tunnelId, data) {
|
|
44
|
-
const tunnel = this.tunnels.get(tunnelId);
|
|
45
|
-
if (!tunnel) {
|
|
46
|
-
console.warn(`Port forward data for unknown tunnel: tunnelId=${tunnelId}`);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
tunnel.localSocket.write(Buffer.from(data, 'base64'));
|
|
50
|
-
}
|
|
51
|
-
closeTunnel(tunnelId) {
|
|
52
|
-
const tunnel = this.tunnels.get(tunnelId);
|
|
53
|
-
if (!tunnel)
|
|
54
|
-
return;
|
|
55
|
-
console.log(`Port forward closing tunnel: tunnelId=${tunnelId}`);
|
|
56
|
-
tunnel.localSocket.destroy();
|
|
57
|
-
this.tunnels.delete(tunnelId);
|
|
58
|
-
}
|
|
59
|
-
closeAll() {
|
|
60
|
-
console.log(`Port forward closing all tunnels: count=${this.tunnels.size}`);
|
|
61
|
-
for (const tunnel of this.tunnels.values()) {
|
|
62
|
-
tunnel.localSocket.destroy();
|
|
63
|
-
}
|
|
64
|
-
this.tunnels.clear();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
export const portForwardManager = new PortForwardManager();
|
|
68
|
-
//# sourceMappingURL=port-forward.js.map
|
package/dist/port-forward.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"port-forward.js","sourceRoot":"","sources":["../src/port-forward.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"port-forward.test.d.ts","sourceRoot":"","sources":["../src/port-forward.test.ts"],"names":[],"mappings":""}
|