agent-gear 1.3.0 → 2.1.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.
Files changed (37) hide show
  1. package/dist/acp/bridge.d.ts +3 -1
  2. package/dist/acp/bridge.d.ts.map +1 -1
  3. package/dist/acp/bridge.js +13 -6
  4. package/dist/acp/bridge.js.map +1 -1
  5. package/dist/acp/bridge.test.js +26 -11
  6. package/dist/acp/bridge.test.js.map +1 -1
  7. package/dist/mcp/local-server.d.ts +11 -0
  8. package/dist/mcp/local-server.d.ts.map +1 -0
  9. package/dist/mcp/local-server.js +102 -0
  10. package/dist/mcp/local-server.js.map +1 -0
  11. package/dist/mcp/local-server.test.d.ts +2 -0
  12. package/dist/mcp/local-server.test.d.ts.map +1 -0
  13. package/dist/mcp/local-server.test.js +101 -0
  14. package/dist/mcp/local-server.test.js.map +1 -0
  15. package/dist/mcp/relay.d.ts +30 -0
  16. package/dist/mcp/relay.d.ts.map +1 -0
  17. package/dist/mcp/relay.js +65 -0
  18. package/dist/mcp/relay.js.map +1 -0
  19. package/dist/mcp/relay.test.d.ts +2 -0
  20. package/dist/mcp/relay.test.d.ts.map +1 -0
  21. package/dist/mcp/relay.test.js +76 -0
  22. package/dist/mcp/relay.test.js.map +1 -0
  23. package/dist/socketio-client.d.ts +25 -0
  24. package/dist/socketio-client.d.ts.map +1 -1
  25. package/dist/socketio-client.js +115 -4
  26. package/dist/socketio-client.js.map +1 -1
  27. package/package.json +2 -1
  28. package/src/acp/bridge.test.ts +30 -12
  29. package/src/acp/bridge.ts +22 -7
  30. package/src/mcp/local-server.test.ts +133 -0
  31. package/src/mcp/local-server.ts +106 -0
  32. package/src/mcp/relay.test.ts +91 -0
  33. package/src/mcp/relay.ts +94 -0
  34. package/src/socketio-client.ts +128 -7
  35. package/tests/acp-wire-mcp.test.ts +4 -4
  36. package/tests/endpoint-compat.test.ts +192 -0
  37. package/tests/socketio-client.test.ts +193 -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=relay.test.d.ts.map
@@ -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,4 +1,29 @@
1
1
  import { type Socket } from 'socket.io-client';
2
+ export interface ParsedEndpoint {
3
+ /** 服务源(协议 + host + port),不含 path/query,例如 `wss://cloud.example.com`。 */
4
+ origin: string;
5
+ /** Socket.IO namespace:旧 endpoint 为 `/`,带 pathname 的 endpoint 取其路径(如 `/worker`)。 */
6
+ namespace: string;
7
+ /** 握手 query 参数(如 `?token=xxx`);旧 endpoint 为空对象。 */
8
+ query: Record<string, string>;
9
+ }
10
+ /**
11
+ * 解析 controller endpoint,区分「旧 controller-api endpoint」与「带 path/query 的 cloud-controller endpoint」。
12
+ *
13
+ * - `http://host:4001` → namespace `/`、无 query(对 controller-api 旧行为零变化)
14
+ * - `wss://host/worker?token=x` → namespace `/worker`、query `{ token: 'x' }`
15
+ *
16
+ * socket.io-client v4 原生即按此规则解析 URL(pathname→namespace、search→handshake query),
17
+ * 因此 `connectToServer` 仍直接把 endpoint 透传给 `io()`。本函数用于单测锚定该解析口径,
18
+ * 并为日志输出提供不含凭据的连接目标。
19
+ */
20
+ export declare function parseEndpoint(endpoint: string): ParsedEndpoint;
21
+ export declare function extractHandshakeToken(endpoint: string): string | undefined;
22
+ /**
23
+ * 从 endpoint 移除一次性 token query(问题 11:token 不进 URL/access log,改由 Socket.IO
24
+ * 握手 `auth.token` 承载)。无 token 时原样返回。
25
+ */
26
+ export declare function stripTokenFromEndpoint(endpoint: string): string;
2
27
  export declare function connectToServer(endpoint: string, key: string, options?: {
3
28
  serverName?: string;
4
29
  }): Socket;
@@ -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;AA2M3E,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,MAAM,CAszBR;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/C"}
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,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAc9D;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM1E;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,MAAM,CAm3BR;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/C"}
@@ -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
- const CONTROLLER_VERSION = '1.1.1';
18
+ import { McpRelay } from './mcp/relay.js';
19
+ import { LocalMcpServer } from './mcp/local-server.js';
20
+ const CONTROLLER_VERSION = '2.1.0';
19
21
  const acpBridges = new Map();
20
22
  const permissionRequestBridge = new Map();
21
23
  const elicitationRequestBridge = new Map();
@@ -34,26 +36,92 @@ function describeMcpConfig(data) {
34
36
  parts.push(`mcpServers=${data.mcpServers.length}`);
35
37
  }
36
38
  if (data.mcpEndpoint) {
37
- parts.push(`mcpEndpoint(url=${data.mcpEndpoint.url}, serverNames=${JSON.stringify(data.mcpEndpoint.serverNames ?? null)})`);
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
  }
43
+ /**
44
+ * 解析 controller endpoint,区分「旧 controller-api endpoint」与「带 path/query 的 cloud-controller endpoint」。
45
+ *
46
+ * - `http://host:4001` → namespace `/`、无 query(对 controller-api 旧行为零变化)
47
+ * - `wss://host/worker?token=x` → namespace `/worker`、query `{ token: 'x' }`
48
+ *
49
+ * socket.io-client v4 原生即按此规则解析 URL(pathname→namespace、search→handshake query),
50
+ * 因此 `connectToServer` 仍直接把 endpoint 透传给 `io()`。本函数用于单测锚定该解析口径,
51
+ * 并为日志输出提供不含凭据的连接目标。
52
+ */
53
+ export function parseEndpoint(endpoint) {
54
+ const result = { origin: endpoint, namespace: '/', query: {} };
55
+ try {
56
+ const url = new URL(endpoint);
57
+ result.origin = url.origin;
58
+ const pathname = url.pathname.replace(/\/+$/, '');
59
+ result.namespace = pathname === '' ? '/' : pathname;
60
+ for (const [name, value] of url.searchParams) {
61
+ result.query[name] = value;
62
+ }
63
+ }
64
+ catch {
65
+ // 非法 URL:保持透传(origin=原始字符串、默认 namespace、无 query),与既有行为一致
66
+ }
67
+ return result;
68
+ }
69
+ export function extractHandshakeToken(endpoint) {
70
+ try {
71
+ return new URL(endpoint).searchParams.get('token') ?? undefined;
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ /**
78
+ * 从 endpoint 移除一次性 token query(问题 11:token 不进 URL/access log,改由 Socket.IO
79
+ * 握手 `auth.token` 承载)。无 token 时原样返回。
80
+ */
81
+ export function stripTokenFromEndpoint(endpoint) {
82
+ try {
83
+ const url = new URL(endpoint);
84
+ if (!url.searchParams.has('token'))
85
+ return endpoint;
86
+ url.searchParams.delete('token');
87
+ return url.toString();
88
+ }
89
+ catch {
90
+ return endpoint;
91
+ }
92
+ }
41
93
  export function connectToServer(endpoint, key, options) {
42
- const socket = createSocketIOClient(endpoint, {
94
+ // 一次性 token 优先取 endpoint query(兼容旧注入),否则用 key(cloud-controller 场景 key=token)。
95
+ // token 不再放进 URL,改经 Socket.IO 握手 auth 传递,避免进入 ingress/中间层访问日志。
96
+ const handshakeToken = extractHandshakeToken(endpoint) ?? (key || undefined);
97
+ const connectEndpoint = stripTokenFromEndpoint(endpoint);
98
+ const socket = createSocketIOClient(connectEndpoint, {
43
99
  transports: ['websocket'],
44
100
  reconnection: true,
45
101
  reconnectionDelay: 1000,
46
102
  reconnectionDelayMax: 60000,
103
+ auth: handshakeToken ? { token: handshakeToken } : {},
47
104
  });
48
105
  const heartbeat = new Heartbeat();
49
106
  const controllerName = options?.serverName || hostname();
107
+ const mcpRelay = new McpRelay((payload) => socket.emit('mcp:request', payload));
108
+ const localServer = new LocalMcpServer(mcpRelay);
109
+ // 回环 HTTP 服务在守护进程内只启动一次、进程退出前不停止(提案 Phase 2 要求全守护进程共享一个端口)。
110
+ // 断线快速失败由 mcpRelay.handleDisconnect()(503)承担,无需随 socket 连接启停 HTTP 服务。
111
+ // ready promise 保证 acp:start/acp:resume 读取 localServer.port 前服务已 listening(启动只 await 一次)。
112
+ const mcpServerReady = localServer.start();
113
+ mcpServerReady.catch((err) => {
114
+ console.error(`[MCP] failed to start local MCP endpoint: ${err.message}`);
115
+ });
50
116
  // Seed the fs:remove-dir whitelist with cwds of ACP processes that survived
51
117
  // a controller restart (their worktrees remain controller-managed).
52
118
  for (const entry of processRegistry.listEntries()) {
53
119
  fsHandler.registerAllowedPath(entry.cwd);
54
120
  }
55
121
  socket.on('connect', () => {
56
- console.log(`Connected to ${endpoint}`);
122
+ // 不直接打印 endpoint:cloud-controller 场景下 endpoint 含一次性 token,避免凭据进入日志
123
+ const { origin, namespace } = parseEndpoint(connectEndpoint);
124
+ console.log(`Connected to ${origin}${namespace === '/' ? '' : namespace}`);
57
125
  const resumeToken = getResumeTokenFromConfig();
58
126
  socket.emit('register', {
59
127
  key,
@@ -74,6 +142,13 @@ export function connectToServer(endpoint, key, options) {
74
142
  });
75
143
  socket.on('error', (data) => {
76
144
  console.error(`Server error: ${data.code} - ${data.message}`);
145
+ if (data.code === 'VERSION_MISMATCH') {
146
+ console.error('The agent-controller version is not supported by the server.');
147
+ console.error('Please upgrade agent-controller to 2.x (npm i -g agent-gear@2 or the matching release).');
148
+ socket.disconnect();
149
+ process.exit(1);
150
+ return;
151
+ }
77
152
  if (data.code === 'KEY_ALREADY_ONLINE') {
78
153
  console.log('Clearing resume token and retrying connection...');
79
154
  clearResumeTokenFromConfig();
@@ -142,11 +217,23 @@ export function connectToServer(endpoint, key, options) {
142
217
  await existingBridge.stop().catch(() => { });
143
218
  }
144
219
  const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
220
+ if (data.mcpEndpoint) {
221
+ // 确保回环服务已 listening 后再读取 port 并注入(启动失败时 fail loudly,不静默注入端口 0)
222
+ await mcpServerReady;
223
+ if (localServer.port <= 0) {
224
+ throw new Error(`MCP endpoint unavailable: local loopback server not listening (port=${localServer.port})`);
225
+ }
226
+ mcpRelay.registerSession(data.sessionId, {
227
+ token: data.mcpEndpoint.token,
228
+ serverNames: data.mcpEndpoint.serverNames,
229
+ });
230
+ }
145
231
  const bridge = new AcpSessionBridge(socket, {
146
232
  sessionId: data.sessionId,
147
233
  cwd: data.cwd,
148
234
  mcpServers: data.mcpServers,
149
235
  mcpEndpoint: data.mcpEndpoint,
236
+ mcpPort: localServer.port,
150
237
  additionalDirectories: data.additionalDirectories,
151
238
  initialModeId: data.modeId,
152
239
  initialModelId: data.modelId,
@@ -159,6 +246,7 @@ export function connectToServer(endpoint, key, options) {
159
246
  bridge.on('acp:stopped', (payload) => {
160
247
  if (payload.sessionId === data.sessionId) {
161
248
  processRegistry.deregister(data.sessionId);
249
+ mcpRelay.unregisterSession(data.sessionId);
162
250
  }
163
251
  });
164
252
  // Register PID in process registry after spawner starts
@@ -230,6 +318,7 @@ export function connectToServer(endpoint, key, options) {
230
318
  bridge.off('acp:stopped', onStopped);
231
319
  acpBridges.delete(data.sessionId);
232
320
  processRegistry.deregister(data.sessionId);
321
+ mcpRelay.unregisterSession(data.sessionId);
233
322
  console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
234
323
  socket.emit('acp:start:result', {
235
324
  requestId: data.requestId,
@@ -245,6 +334,7 @@ export function connectToServer(endpoint, key, options) {
245
334
  }
246
335
  catch (error) {
247
336
  console.error(`[ACP ctrl] bridge.start() failed: ${error.message}`);
337
+ mcpRelay.unregisterSession(data.sessionId);
248
338
  socket.emit('acp:start:result', {
249
339
  requestId: data.requestId,
250
340
  sessionId: data.sessionId,
@@ -264,10 +354,22 @@ export function connectToServer(endpoint, key, options) {
264
354
  await existingBridge.stop().catch(() => { });
265
355
  }
266
356
  const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
357
+ if (data.mcpEndpoint) {
358
+ // 确保回环服务已 listening 后再读取 port 并注入(启动失败时 fail loudly,不静默注入端口 0)
359
+ await mcpServerReady;
360
+ if (localServer.port <= 0) {
361
+ throw new Error(`MCP endpoint unavailable: local loopback server not listening (port=${localServer.port})`);
362
+ }
363
+ mcpRelay.registerSession(data.sessionId, {
364
+ token: data.mcpEndpoint.token,
365
+ serverNames: data.mcpEndpoint.serverNames,
366
+ });
367
+ }
267
368
  const bridge = new AcpSessionBridge(socket, {
268
369
  sessionId: data.sessionId,
269
370
  cwd: data.cwd,
270
371
  mcpEndpoint: data.mcpEndpoint,
372
+ mcpPort: localServer.port,
271
373
  additionalDirectories: data.additionalDirectories,
272
374
  // 初始 mode/model:resume 完成后、acp:initialized 之前应用(时序同 acp:start)
273
375
  initialModeId: data.modeId,
@@ -278,6 +380,7 @@ export function connectToServer(endpoint, key, options) {
278
380
  bridge.on('acp:stopped', (payload) => {
279
381
  if (payload.sessionId === data.sessionId) {
280
382
  processRegistry.deregister(data.sessionId);
383
+ mcpRelay.unregisterSession(data.sessionId);
281
384
  }
282
385
  });
283
386
  bridge.on('permission:sent', (requestId) => {
@@ -320,6 +423,7 @@ export function connectToServer(endpoint, key, options) {
320
423
  bridge.off('acp:stopped', onStopped);
321
424
  acpBridges.delete(data.sessionId);
322
425
  processRegistry.deregister(data.sessionId);
426
+ mcpRelay.unregisterSession(data.sessionId);
323
427
  console.log(`[ACP ctrl] emitting acp:resume:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
324
428
  socket.emit('acp:resume:result', {
325
429
  requestId: data.requestId,
@@ -334,6 +438,7 @@ export function connectToServer(endpoint, key, options) {
334
438
  }
335
439
  catch (error) {
336
440
  console.error(`[ACP ctrl] bridge.resume() failed: ${error.message}`);
441
+ mcpRelay.unregisterSession(data.sessionId);
337
442
  socket.emit('acp:resume:result', {
338
443
  requestId: data.requestId,
339
444
  sessionId: data.sessionId,
@@ -513,6 +618,7 @@ export function connectToServer(endpoint, key, options) {
513
618
  processRegistry.deregister(data.sessionId);
514
619
  }
515
620
  }
621
+ mcpRelay.unregisterSession(data.sessionId);
516
622
  if (data.ackRequired) {
517
623
  socket.emit('acp:stopped', {
518
624
  sessionId: data.sessionId,
@@ -778,6 +884,7 @@ export function connectToServer(endpoint, key, options) {
778
884
  }
779
885
  catch { /* process may already be dead */ }
780
886
  processRegistry.deregister(entry.sessionId);
887
+ mcpRelay.unregisterSession(entry.sessionId);
781
888
  // Also clean up bridge if present
782
889
  const bridge = acpBridges.get(entry.sessionId);
783
890
  if (bridge) {
@@ -812,12 +919,16 @@ export function connectToServer(endpoint, key, options) {
812
919
  bridge.handleElicitationResponse(data.requestId, data.action, data.content);
813
920
  }
814
921
  });
922
+ socket.on('mcp:response', (data) => {
923
+ mcpRelay.handleMcpResponse(data.requestId, data.status, data.body);
924
+ });
815
925
  socket.on('disconnect', (reason) => {
816
926
  console.log(`Disconnected: ${reason}`);
817
927
  heartbeat.stop();
818
928
  gatewayManager.closeAll();
819
929
  permissionRequestBridge.clear();
820
930
  elicitationRequestBridge.clear();
931
+ mcpRelay.handleDisconnect();
821
932
  });
822
933
  socket.on('connect_error', (err) => {
823
934
  console.error(`Connection error: ${err.message}`);