agent-gear 1.2.1 → 2.0.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/dist/acp/bridge.d.ts +3 -1
- package/dist/acp/bridge.d.ts.map +1 -1
- package/dist/acp/bridge.js +13 -6
- package/dist/acp/bridge.js.map +1 -1
- package/dist/acp/bridge.test.js +26 -11
- package/dist/acp/bridge.test.js.map +1 -1
- package/dist/mcp/local-server.d.ts +11 -0
- package/dist/mcp/local-server.d.ts.map +1 -0
- package/dist/mcp/local-server.js +99 -0
- package/dist/mcp/local-server.js.map +1 -0
- package/dist/mcp/local-server.test.d.ts +2 -0
- package/dist/mcp/local-server.test.d.ts.map +1 -0
- package/dist/mcp/local-server.test.js +101 -0
- package/dist/mcp/local-server.test.js.map +1 -0
- package/dist/mcp/relay.d.ts +30 -0
- package/dist/mcp/relay.d.ts.map +1 -0
- package/dist/mcp/relay.js +65 -0
- package/dist/mcp/relay.js.map +1 -0
- package/dist/mcp/relay.test.d.ts +2 -0
- package/dist/mcp/relay.test.d.ts.map +1 -0
- package/dist/mcp/relay.test.js +76 -0
- package/dist/mcp/relay.test.js.map +1 -0
- package/dist/socketio-client.d.ts.map +1 -1
- package/dist/socketio-client.js +58 -2
- package/dist/socketio-client.js.map +1 -1
- package/package.json +1 -1
- package/src/acp/bridge.test.ts +30 -12
- package/src/acp/bridge.ts +22 -7
- package/src/mcp/local-server.test.ts +133 -0
- package/src/mcp/local-server.ts +103 -0
- package/src/mcp/relay.test.ts +91 -0
- package/src/mcp/relay.ts +94 -0
- package/src/socketio-client.ts +62 -5
- package/tests/acp-wire-mcp.test.ts +4 -4
- package/tests/socketio-client.test.ts +196 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { LocalMcpServer } from './local-server.js';
|
|
4
|
+
function makeRelay(options) {
|
|
5
|
+
const session = options?.session ?? 'sess-1';
|
|
6
|
+
const server = options?.server ?? 'github-mcp';
|
|
7
|
+
return {
|
|
8
|
+
hasSession: (id) => id === session,
|
|
9
|
+
hasServer: (id, name) => id === session && name === server,
|
|
10
|
+
forward: vi.fn(async () => ({ status: 200, body: { result: 'ok' } })),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function startServer(relay) {
|
|
14
|
+
const server = new LocalMcpServer(relay);
|
|
15
|
+
await server.start();
|
|
16
|
+
return server;
|
|
17
|
+
}
|
|
18
|
+
function request(server, method, path, headers, body) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const req = http.request({ host: '127.0.0.1', port: server.port, path, method, headers }, (res) => {
|
|
21
|
+
let data = '';
|
|
22
|
+
res.on('data', (c) => (data += c));
|
|
23
|
+
res.on('end', () => resolve({ status: res.statusCode ?? 0, data }));
|
|
24
|
+
});
|
|
25
|
+
req.on('error', reject);
|
|
26
|
+
if (body !== undefined)
|
|
27
|
+
req.write(body);
|
|
28
|
+
req.end();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const AUTH = { authorization: 'Bearer tok-1' };
|
|
32
|
+
function httpAddress(server) {
|
|
33
|
+
return server.server.address();
|
|
34
|
+
}
|
|
35
|
+
describe('LocalMcpServer', () => {
|
|
36
|
+
it('T-C1 forwards a valid POST to the relay and writes back the response', async () => {
|
|
37
|
+
const relay = makeRelay();
|
|
38
|
+
const server = await startServer(relay);
|
|
39
|
+
const path = '/mcp/sess-1/github-mcp';
|
|
40
|
+
const res = await request(server, 'POST', path, AUTH, JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }));
|
|
41
|
+
expect(relay.forward).toHaveBeenCalledWith('sess-1', 'github-mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list' });
|
|
42
|
+
expect(res.status).toBe(200);
|
|
43
|
+
expect(JSON.parse(res.data)).toEqual({ result: 'ok' });
|
|
44
|
+
await server.stop();
|
|
45
|
+
});
|
|
46
|
+
it('T-C2 fast-fails unregistered session (404), out-of-snapshot server (404), missing Authorization (401)', async () => {
|
|
47
|
+
const relay = makeRelay({ session: 'sess-1', server: 'github-mcp' });
|
|
48
|
+
const server = await startServer(relay);
|
|
49
|
+
const body = JSON.stringify({ id: 1 });
|
|
50
|
+
const unregistered = await request(server, 'POST', '/mcp/ghost-mcp/anything', AUTH, body);
|
|
51
|
+
expect(unregistered.status).toBe(404);
|
|
52
|
+
const unknownServer = await request(server, 'POST', '/mcp/sess-1/unknown-server', AUTH, body);
|
|
53
|
+
expect(unknownServer.status).toBe(404);
|
|
54
|
+
const noAuth = await request(server, 'POST', '/mcp/sess-1/github-mcp', undefined, body);
|
|
55
|
+
expect(noAuth.status).toBe(401);
|
|
56
|
+
expect(relay.forward).not.toHaveBeenCalled();
|
|
57
|
+
await server.stop();
|
|
58
|
+
});
|
|
59
|
+
it('T-C3 returns 405 for GET', async () => {
|
|
60
|
+
const relay = makeRelay();
|
|
61
|
+
const server = await startServer(relay);
|
|
62
|
+
const res = await request(server, 'GET', '/mcp/sess-1/github-mcp');
|
|
63
|
+
expect(res.status).toBe(405);
|
|
64
|
+
expect(relay.forward).not.toHaveBeenCalled();
|
|
65
|
+
await server.stop();
|
|
66
|
+
});
|
|
67
|
+
it('T-C4 listens only on 127.0.0.1 with an ephemeral port > 0', async () => {
|
|
68
|
+
const relay = makeRelay();
|
|
69
|
+
const server = await startServer(relay);
|
|
70
|
+
const address = httpAddress(server);
|
|
71
|
+
expect(address.address).toBe('127.0.0.1');
|
|
72
|
+
expect(address.port).toBeGreaterThan(0);
|
|
73
|
+
expect(server.port).toBe(address.port);
|
|
74
|
+
await server.stop();
|
|
75
|
+
});
|
|
76
|
+
it('T-C5 concurrent requests resolve independently without interfering', async () => {
|
|
77
|
+
const resolvers = [];
|
|
78
|
+
const relay = {
|
|
79
|
+
hasSession: (id) => id === 'sess-1',
|
|
80
|
+
hasServer: (id, name) => id === 'sess-1' && name === 'github-mcp',
|
|
81
|
+
forward: vi.fn(() => new Promise((resolve) => {
|
|
82
|
+
resolvers.push(resolve);
|
|
83
|
+
})),
|
|
84
|
+
};
|
|
85
|
+
const server = await startServer(relay);
|
|
86
|
+
const p1 = request(server, 'POST', '/mcp/sess-1/github-mcp', AUTH, JSON.stringify({ id: 1 }));
|
|
87
|
+
const p2 = request(server, 'POST', '/mcp/sess-1/github-mcp', AUTH, JSON.stringify({ id: 2 }));
|
|
88
|
+
const p3 = request(server, 'POST', '/mcp/sess-1/github-mcp', AUTH, JSON.stringify({ id: 3 }));
|
|
89
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
90
|
+
expect(relay.forward).toHaveBeenCalledTimes(3);
|
|
91
|
+
expect(resolvers).toHaveLength(3);
|
|
92
|
+
resolvers[0]({ status: 200, body: { result: 'first' } });
|
|
93
|
+
resolvers[1]({ status: 504, body: { error: 'TIMEOUT' } });
|
|
94
|
+
resolvers[2]({ status: 200, body: { result: 'third' } });
|
|
95
|
+
expect((await p1).status).toBe(200);
|
|
96
|
+
expect((await p2).status).toBe(504);
|
|
97
|
+
expect((await p3).status).toBe(200);
|
|
98
|
+
await server.stop();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
//# sourceMappingURL=local-server.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-server.test.js","sourceRoot":"","sources":["../../src/mcp/local-server.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface SessionEndpoint {
|
|
2
|
+
token: string;
|
|
3
|
+
serverNames: string[];
|
|
4
|
+
}
|
|
5
|
+
export interface McpRelayResult {
|
|
6
|
+
status: number;
|
|
7
|
+
body: unknown;
|
|
8
|
+
}
|
|
9
|
+
interface McpRequestPayload {
|
|
10
|
+
requestId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
serverName: string;
|
|
13
|
+
token: string;
|
|
14
|
+
body: unknown;
|
|
15
|
+
}
|
|
16
|
+
export declare class McpRelay {
|
|
17
|
+
private pendingRequests;
|
|
18
|
+
private sessionEndpoints;
|
|
19
|
+
private emitMcpRequest;
|
|
20
|
+
constructor(emitMcpRequest: (payload: McpRequestPayload) => void);
|
|
21
|
+
registerSession(sessionId: string, endpoint: SessionEndpoint): void;
|
|
22
|
+
hasSession(sessionId: string): boolean;
|
|
23
|
+
hasServer(sessionId: string, serverName: string): boolean;
|
|
24
|
+
unregisterSession(sessionId: string): void;
|
|
25
|
+
forward(sessionId: string, serverName: string, body: unknown): Promise<McpRelayResult>;
|
|
26
|
+
handleMcpResponse(requestId: string, status: number, body: unknown): void;
|
|
27
|
+
handleDisconnect(): void;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/mcp/relay.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAUD,qBAAa,QAAQ;IACnB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,gBAAgB,CAAsC;IAC9D,OAAO,CAAC,cAAc,CAAuC;gBAEjD,cAAc,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI;IAIhE,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI;IAInE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAItC,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAKzD,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAUpC,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAgB5F,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IAYzE,gBAAgB,IAAI,IAAI;CAOzB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
2
|
+
export class McpRelay {
|
|
3
|
+
pendingRequests = new Map();
|
|
4
|
+
sessionEndpoints = new Map();
|
|
5
|
+
emitMcpRequest;
|
|
6
|
+
constructor(emitMcpRequest) {
|
|
7
|
+
this.emitMcpRequest = emitMcpRequest;
|
|
8
|
+
}
|
|
9
|
+
registerSession(sessionId, endpoint) {
|
|
10
|
+
this.sessionEndpoints.set(sessionId, endpoint);
|
|
11
|
+
}
|
|
12
|
+
hasSession(sessionId) {
|
|
13
|
+
return this.sessionEndpoints.has(sessionId);
|
|
14
|
+
}
|
|
15
|
+
hasServer(sessionId, serverName) {
|
|
16
|
+
const endpoint = this.sessionEndpoints.get(sessionId);
|
|
17
|
+
return endpoint ? endpoint.serverNames.includes(serverName) : false;
|
|
18
|
+
}
|
|
19
|
+
unregisterSession(sessionId) {
|
|
20
|
+
if (!this.sessionEndpoints.delete(sessionId))
|
|
21
|
+
return;
|
|
22
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
23
|
+
if (pending.sessionId !== sessionId)
|
|
24
|
+
continue;
|
|
25
|
+
clearTimeout(pending.timer);
|
|
26
|
+
this.pendingRequests.delete(requestId);
|
|
27
|
+
pending.resolve({ status: 410, body: { error: 'SESSION_ENDED', message: 'MCP session has ended' } });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async forward(sessionId, serverName, body) {
|
|
31
|
+
const endpoint = this.sessionEndpoints.get(sessionId);
|
|
32
|
+
if (!endpoint)
|
|
33
|
+
return { status: 404, body: { error: 'SESSION_NOT_FOUND' } };
|
|
34
|
+
const requestId = `mcpreq_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
35
|
+
console.log(`[MCP relay] forward requestId=${requestId} session=${sessionId} server=${serverName} method=${body?.method ?? '?'}`);
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
this.pendingRequests.delete(requestId);
|
|
39
|
+
console.log(`[MCP relay] requestId=${requestId} -> 504 (timeout after ${REQUEST_TIMEOUT_MS}ms, no mcp:response received)`);
|
|
40
|
+
resolve({ status: 504, body: { error: 'TIMEOUT', message: 'MCP request timed out' } });
|
|
41
|
+
}, REQUEST_TIMEOUT_MS);
|
|
42
|
+
this.pendingRequests.set(requestId, { sessionId, resolve, timer });
|
|
43
|
+
this.emitMcpRequest({ requestId, sessionId, serverName, token: endpoint.token, body });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
handleMcpResponse(requestId, status, body) {
|
|
47
|
+
const pending = this.pendingRequests.get(requestId);
|
|
48
|
+
if (!pending) {
|
|
49
|
+
console.log(`[MCP relay] response requestId=${requestId} status=${status} IGNORED (no pending request)`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.log(`[MCP relay] response requestId=${requestId} status=${status}`);
|
|
53
|
+
clearTimeout(pending.timer);
|
|
54
|
+
this.pendingRequests.delete(requestId);
|
|
55
|
+
pending.resolve({ status, body });
|
|
56
|
+
}
|
|
57
|
+
handleDisconnect() {
|
|
58
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
59
|
+
clearTimeout(pending.timer);
|
|
60
|
+
this.pendingRequests.delete(requestId);
|
|
61
|
+
pending.resolve({ status: 503, body: { error: 'CONTROLLER_DISCONNECTED', message: 'Controller disconnected' } });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=relay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay.js","sourceRoot":"","sources":["../../src/mcp/relay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay.test.d.ts","sourceRoot":"","sources":["../../src/mcp/relay.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { McpRelay } from './relay.js';
|
|
3
|
+
function createRelay() {
|
|
4
|
+
const emitted = [];
|
|
5
|
+
const relay = new McpRelay((payload) => emitted.push(payload));
|
|
6
|
+
return { relay, emitted };
|
|
7
|
+
}
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
describe('McpRelay', () => {
|
|
12
|
+
it('T-C6 resolves a pending request by requestId on mcp:response', async () => {
|
|
13
|
+
const { relay, emitted } = createRelay();
|
|
14
|
+
relay.registerSession('sess-1', { token: 'tok-1', serverNames: ['github-mcp'] });
|
|
15
|
+
const promise = relay.forward('sess-1', 'github-mcp', { method: 'tools/list' });
|
|
16
|
+
expect(emitted).toHaveLength(1);
|
|
17
|
+
expect(emitted[0].requestId).toMatch(/^mcpreq_/);
|
|
18
|
+
expect(emitted[0]).toMatchObject({
|
|
19
|
+
sessionId: 'sess-1',
|
|
20
|
+
serverName: 'github-mcp',
|
|
21
|
+
token: 'tok-1',
|
|
22
|
+
body: { method: 'tools/list' },
|
|
23
|
+
});
|
|
24
|
+
relay.handleMcpResponse(emitted[0].requestId, 200, { result: [] });
|
|
25
|
+
await expect(promise).resolves.toEqual({ status: 200, body: { result: [] } });
|
|
26
|
+
});
|
|
27
|
+
it('T-C7 times out after 60s with 504 and cleans up pending', async () => {
|
|
28
|
+
vi.useFakeTimers();
|
|
29
|
+
const { relay, emitted } = createRelay();
|
|
30
|
+
relay.registerSession('sess-1', { token: 'tok-1', serverNames: ['github-mcp'] });
|
|
31
|
+
const first = relay.forward('sess-1', 'github-mcp', { method: 'tools/list' });
|
|
32
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
33
|
+
await expect(first).resolves.toEqual(expect.objectContaining({ status: 504 }));
|
|
34
|
+
const second = relay.forward('sess-1', 'github-mcp', { method: 'tools/call' });
|
|
35
|
+
emitted[1] && relay.handleMcpResponse(emitted[1].requestId, 200, { ok: true });
|
|
36
|
+
await expect(second).resolves.toEqual({ status: 200, body: { ok: true } });
|
|
37
|
+
});
|
|
38
|
+
it('T-C8 fails all pending immediately with 503 on disconnect', async () => {
|
|
39
|
+
const { relay, emitted } = createRelay();
|
|
40
|
+
relay.registerSession('sess-1', { token: 'tok-1', serverNames: ['a-mcp'] });
|
|
41
|
+
const promises = [
|
|
42
|
+
relay.forward('sess-1', 'a-mcp', { id: 1 }),
|
|
43
|
+
relay.forward('sess-1', 'a-mcp', { id: 2 }),
|
|
44
|
+
];
|
|
45
|
+
expect(emitted).toHaveLength(2);
|
|
46
|
+
relay.handleDisconnect();
|
|
47
|
+
const results = await Promise.all(promises);
|
|
48
|
+
expect(results).toEqual([
|
|
49
|
+
expect.objectContaining({ status: 503 }),
|
|
50
|
+
expect.objectContaining({ status: 503 }),
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
it('T-C9 unregistering a session fails its pending with 410, other sessions unaffected', async () => {
|
|
54
|
+
const { relay, emitted } = createRelay();
|
|
55
|
+
relay.registerSession('sess-1', { token: 'tok-1', serverNames: ['a-mcp'] });
|
|
56
|
+
relay.registerSession('sess-2', { token: 'tok-2', serverNames: ['b-mcp'] });
|
|
57
|
+
const sess1 = relay.forward('sess-1', 'a-mcp', { id: 1 });
|
|
58
|
+
const sess2 = relay.forward('sess-2', 'b-mcp', { id: 2 });
|
|
59
|
+
relay.unregisterSession('sess-1');
|
|
60
|
+
await expect(sess1).resolves.toEqual(expect.objectContaining({ status: 410 }));
|
|
61
|
+
expect(relay.hasSession('sess-1')).toBe(false);
|
|
62
|
+
expect(relay.hasSession('sess-2')).toBe(true);
|
|
63
|
+
emitted[1] && relay.handleMcpResponse(emitted[1].requestId, 200, { ok: true });
|
|
64
|
+
await expect(sess2).resolves.toEqual({ status: 200, body: { ok: true } });
|
|
65
|
+
});
|
|
66
|
+
it('T-C10 ignores mcp:response for an unknown requestId', async () => {
|
|
67
|
+
const { relay } = createRelay();
|
|
68
|
+
expect(() => relay.handleMcpResponse('mcpreq_unknown', 200, {})).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
it('forward returns 404 for an unregistered session', async () => {
|
|
71
|
+
const { relay } = createRelay();
|
|
72
|
+
const result = await relay.forward('missing', 'a-mcp', {});
|
|
73
|
+
expect(result.status).toBe(404);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=relay.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay.test.js","sourceRoot":"","sources":["../../src/mcp/relay.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socketio-client.d.ts","sourceRoot":"","sources":["../src/socketio-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"socketio-client.d.ts","sourceRoot":"","sources":["../src/socketio-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA6M3E,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,MAAM,CA22BR;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/C"}
|
package/dist/socketio-client.js
CHANGED
|
@@ -15,7 +15,9 @@ import { worktreeHandler } from './worktree-handler.js';
|
|
|
15
15
|
import { fsHandler } from './fs-handler.js';
|
|
16
16
|
import { gatewayManager } from './gateway.js';
|
|
17
17
|
import { processRegistry, isProcessAlive } from './acp/process-registry.js';
|
|
18
|
-
|
|
18
|
+
import { McpRelay } from './mcp/relay.js';
|
|
19
|
+
import { LocalMcpServer } from './mcp/local-server.js';
|
|
20
|
+
const CONTROLLER_VERSION = '2.0.0';
|
|
19
21
|
const acpBridges = new Map();
|
|
20
22
|
const permissionRequestBridge = new Map();
|
|
21
23
|
const elicitationRequestBridge = new Map();
|
|
@@ -34,7 +36,7 @@ function describeMcpConfig(data) {
|
|
|
34
36
|
parts.push(`mcpServers=${data.mcpServers.length}`);
|
|
35
37
|
}
|
|
36
38
|
if (data.mcpEndpoint) {
|
|
37
|
-
parts.push(`mcpEndpoint(
|
|
39
|
+
parts.push(`mcpEndpoint(serverNames=${JSON.stringify(data.mcpEndpoint.serverNames ?? null)})`);
|
|
38
40
|
}
|
|
39
41
|
return parts.length > 0 ? `mcp=[${parts.join(' ')}]` : 'mcp=[none]';
|
|
40
42
|
}
|
|
@@ -47,6 +49,15 @@ export function connectToServer(endpoint, key, options) {
|
|
|
47
49
|
});
|
|
48
50
|
const heartbeat = new Heartbeat();
|
|
49
51
|
const controllerName = options?.serverName || hostname();
|
|
52
|
+
const mcpRelay = new McpRelay((payload) => socket.emit('mcp:request', payload));
|
|
53
|
+
const localServer = new LocalMcpServer(mcpRelay);
|
|
54
|
+
// 回环 HTTP 服务在守护进程内只启动一次、进程退出前不停止(提案 Phase 2 要求全守护进程共享一个端口)。
|
|
55
|
+
// 断线快速失败由 mcpRelay.handleDisconnect()(503)承担,无需随 socket 连接启停 HTTP 服务。
|
|
56
|
+
// ready promise 保证 acp:start/acp:resume 读取 localServer.port 前服务已 listening(启动只 await 一次)。
|
|
57
|
+
const mcpServerReady = localServer.start();
|
|
58
|
+
mcpServerReady.catch((err) => {
|
|
59
|
+
console.error(`[MCP] failed to start local MCP endpoint: ${err.message}`);
|
|
60
|
+
});
|
|
50
61
|
// Seed the fs:remove-dir whitelist with cwds of ACP processes that survived
|
|
51
62
|
// a controller restart (their worktrees remain controller-managed).
|
|
52
63
|
for (const entry of processRegistry.listEntries()) {
|
|
@@ -60,6 +71,8 @@ export function connectToServer(endpoint, key, options) {
|
|
|
60
71
|
hostname: controllerName,
|
|
61
72
|
version: CONTROLLER_VERSION,
|
|
62
73
|
resumeToken,
|
|
74
|
+
homeDir: homedir(),
|
|
75
|
+
platform: platform(),
|
|
63
76
|
});
|
|
64
77
|
});
|
|
65
78
|
socket.on('registered', (data) => {
|
|
@@ -72,6 +85,13 @@ export function connectToServer(endpoint, key, options) {
|
|
|
72
85
|
});
|
|
73
86
|
socket.on('error', (data) => {
|
|
74
87
|
console.error(`Server error: ${data.code} - ${data.message}`);
|
|
88
|
+
if (data.code === 'VERSION_MISMATCH') {
|
|
89
|
+
console.error('The agent-controller version is not supported by the server.');
|
|
90
|
+
console.error('Please upgrade agent-controller to 2.x (npm i -g agent-gear@2 or the matching release).');
|
|
91
|
+
socket.disconnect();
|
|
92
|
+
process.exit(1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
75
95
|
if (data.code === 'KEY_ALREADY_ONLINE') {
|
|
76
96
|
console.log('Clearing resume token and retrying connection...');
|
|
77
97
|
clearResumeTokenFromConfig();
|
|
@@ -140,11 +160,23 @@ export function connectToServer(endpoint, key, options) {
|
|
|
140
160
|
await existingBridge.stop().catch(() => { });
|
|
141
161
|
}
|
|
142
162
|
const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
|
|
163
|
+
if (data.mcpEndpoint) {
|
|
164
|
+
// 确保回环服务已 listening 后再读取 port 并注入(启动失败时 fail loudly,不静默注入端口 0)
|
|
165
|
+
await mcpServerReady;
|
|
166
|
+
if (localServer.port <= 0) {
|
|
167
|
+
throw new Error(`MCP endpoint unavailable: local loopback server not listening (port=${localServer.port})`);
|
|
168
|
+
}
|
|
169
|
+
mcpRelay.registerSession(data.sessionId, {
|
|
170
|
+
token: data.mcpEndpoint.token,
|
|
171
|
+
serverNames: data.mcpEndpoint.serverNames,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
143
174
|
const bridge = new AcpSessionBridge(socket, {
|
|
144
175
|
sessionId: data.sessionId,
|
|
145
176
|
cwd: data.cwd,
|
|
146
177
|
mcpServers: data.mcpServers,
|
|
147
178
|
mcpEndpoint: data.mcpEndpoint,
|
|
179
|
+
mcpPort: localServer.port,
|
|
148
180
|
additionalDirectories: data.additionalDirectories,
|
|
149
181
|
initialModeId: data.modeId,
|
|
150
182
|
initialModelId: data.modelId,
|
|
@@ -157,6 +189,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
157
189
|
bridge.on('acp:stopped', (payload) => {
|
|
158
190
|
if (payload.sessionId === data.sessionId) {
|
|
159
191
|
processRegistry.deregister(data.sessionId);
|
|
192
|
+
mcpRelay.unregisterSession(data.sessionId);
|
|
160
193
|
}
|
|
161
194
|
});
|
|
162
195
|
// Register PID in process registry after spawner starts
|
|
@@ -228,6 +261,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
228
261
|
bridge.off('acp:stopped', onStopped);
|
|
229
262
|
acpBridges.delete(data.sessionId);
|
|
230
263
|
processRegistry.deregister(data.sessionId);
|
|
264
|
+
mcpRelay.unregisterSession(data.sessionId);
|
|
231
265
|
console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
|
|
232
266
|
socket.emit('acp:start:result', {
|
|
233
267
|
requestId: data.requestId,
|
|
@@ -243,6 +277,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
243
277
|
}
|
|
244
278
|
catch (error) {
|
|
245
279
|
console.error(`[ACP ctrl] bridge.start() failed: ${error.message}`);
|
|
280
|
+
mcpRelay.unregisterSession(data.sessionId);
|
|
246
281
|
socket.emit('acp:start:result', {
|
|
247
282
|
requestId: data.requestId,
|
|
248
283
|
sessionId: data.sessionId,
|
|
@@ -262,10 +297,22 @@ export function connectToServer(endpoint, key, options) {
|
|
|
262
297
|
await existingBridge.stop().catch(() => { });
|
|
263
298
|
}
|
|
264
299
|
const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
|
|
300
|
+
if (data.mcpEndpoint) {
|
|
301
|
+
// 确保回环服务已 listening 后再读取 port 并注入(启动失败时 fail loudly,不静默注入端口 0)
|
|
302
|
+
await mcpServerReady;
|
|
303
|
+
if (localServer.port <= 0) {
|
|
304
|
+
throw new Error(`MCP endpoint unavailable: local loopback server not listening (port=${localServer.port})`);
|
|
305
|
+
}
|
|
306
|
+
mcpRelay.registerSession(data.sessionId, {
|
|
307
|
+
token: data.mcpEndpoint.token,
|
|
308
|
+
serverNames: data.mcpEndpoint.serverNames,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
265
311
|
const bridge = new AcpSessionBridge(socket, {
|
|
266
312
|
sessionId: data.sessionId,
|
|
267
313
|
cwd: data.cwd,
|
|
268
314
|
mcpEndpoint: data.mcpEndpoint,
|
|
315
|
+
mcpPort: localServer.port,
|
|
269
316
|
additionalDirectories: data.additionalDirectories,
|
|
270
317
|
// 初始 mode/model:resume 完成后、acp:initialized 之前应用(时序同 acp:start)
|
|
271
318
|
initialModeId: data.modeId,
|
|
@@ -276,6 +323,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
276
323
|
bridge.on('acp:stopped', (payload) => {
|
|
277
324
|
if (payload.sessionId === data.sessionId) {
|
|
278
325
|
processRegistry.deregister(data.sessionId);
|
|
326
|
+
mcpRelay.unregisterSession(data.sessionId);
|
|
279
327
|
}
|
|
280
328
|
});
|
|
281
329
|
bridge.on('permission:sent', (requestId) => {
|
|
@@ -318,6 +366,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
318
366
|
bridge.off('acp:stopped', onStopped);
|
|
319
367
|
acpBridges.delete(data.sessionId);
|
|
320
368
|
processRegistry.deregister(data.sessionId);
|
|
369
|
+
mcpRelay.unregisterSession(data.sessionId);
|
|
321
370
|
console.log(`[ACP ctrl] emitting acp:resume:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
|
|
322
371
|
socket.emit('acp:resume:result', {
|
|
323
372
|
requestId: data.requestId,
|
|
@@ -332,6 +381,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
332
381
|
}
|
|
333
382
|
catch (error) {
|
|
334
383
|
console.error(`[ACP ctrl] bridge.resume() failed: ${error.message}`);
|
|
384
|
+
mcpRelay.unregisterSession(data.sessionId);
|
|
335
385
|
socket.emit('acp:resume:result', {
|
|
336
386
|
requestId: data.requestId,
|
|
337
387
|
sessionId: data.sessionId,
|
|
@@ -511,6 +561,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
511
561
|
processRegistry.deregister(data.sessionId);
|
|
512
562
|
}
|
|
513
563
|
}
|
|
564
|
+
mcpRelay.unregisterSession(data.sessionId);
|
|
514
565
|
if (data.ackRequired) {
|
|
515
566
|
socket.emit('acp:stopped', {
|
|
516
567
|
sessionId: data.sessionId,
|
|
@@ -776,6 +827,7 @@ export function connectToServer(endpoint, key, options) {
|
|
|
776
827
|
}
|
|
777
828
|
catch { /* process may already be dead */ }
|
|
778
829
|
processRegistry.deregister(entry.sessionId);
|
|
830
|
+
mcpRelay.unregisterSession(entry.sessionId);
|
|
779
831
|
// Also clean up bridge if present
|
|
780
832
|
const bridge = acpBridges.get(entry.sessionId);
|
|
781
833
|
if (bridge) {
|
|
@@ -810,12 +862,16 @@ export function connectToServer(endpoint, key, options) {
|
|
|
810
862
|
bridge.handleElicitationResponse(data.requestId, data.action, data.content);
|
|
811
863
|
}
|
|
812
864
|
});
|
|
865
|
+
socket.on('mcp:response', (data) => {
|
|
866
|
+
mcpRelay.handleMcpResponse(data.requestId, data.status, data.body);
|
|
867
|
+
});
|
|
813
868
|
socket.on('disconnect', (reason) => {
|
|
814
869
|
console.log(`Disconnected: ${reason}`);
|
|
815
870
|
heartbeat.stop();
|
|
816
871
|
gatewayManager.closeAll();
|
|
817
872
|
permissionRequestBridge.clear();
|
|
818
873
|
elicitationRequestBridge.clear();
|
|
874
|
+
mcpRelay.handleDisconnect();
|
|
819
875
|
});
|
|
820
876
|
socket.on('connect_error', (err) => {
|
|
821
877
|
console.error(`Connection error: ${err.message}`);
|