@thinkwell/conductor 0.2.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 (54) hide show
  1. package/README.md +140 -0
  2. package/dist/conductor.d.ts +219 -0
  3. package/dist/conductor.d.ts.map +1 -0
  4. package/dist/conductor.js +960 -0
  5. package/dist/conductor.js.map +1 -0
  6. package/dist/connectors/channel.d.ts +60 -0
  7. package/dist/connectors/channel.d.ts.map +1 -0
  8. package/dist/connectors/channel.js +155 -0
  9. package/dist/connectors/channel.js.map +1 -0
  10. package/dist/connectors/index.d.ts +6 -0
  11. package/dist/connectors/index.d.ts.map +1 -0
  12. package/dist/connectors/index.js +6 -0
  13. package/dist/connectors/index.js.map +1 -0
  14. package/dist/connectors/stdio.d.ts +36 -0
  15. package/dist/connectors/stdio.d.ts.map +1 -0
  16. package/dist/connectors/stdio.js +198 -0
  17. package/dist/connectors/stdio.js.map +1 -0
  18. package/dist/index.d.ts +72 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +76 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/instantiators.d.ts +129 -0
  23. package/dist/instantiators.d.ts.map +1 -0
  24. package/dist/instantiators.js +183 -0
  25. package/dist/instantiators.js.map +1 -0
  26. package/dist/logger.d.ts +121 -0
  27. package/dist/logger.d.ts.map +1 -0
  28. package/dist/logger.js +162 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/mcp-bridge/http-listener.d.ts +35 -0
  31. package/dist/mcp-bridge/http-listener.d.ts.map +1 -0
  32. package/dist/mcp-bridge/http-listener.js +204 -0
  33. package/dist/mcp-bridge/http-listener.js.map +1 -0
  34. package/dist/mcp-bridge/index.d.ts +9 -0
  35. package/dist/mcp-bridge/index.d.ts.map +1 -0
  36. package/dist/mcp-bridge/index.js +8 -0
  37. package/dist/mcp-bridge/index.js.map +1 -0
  38. package/dist/mcp-bridge/mcp-bridge.d.ts +80 -0
  39. package/dist/mcp-bridge/mcp-bridge.d.ts.map +1 -0
  40. package/dist/mcp-bridge/mcp-bridge.js +170 -0
  41. package/dist/mcp-bridge/mcp-bridge.js.map +1 -0
  42. package/dist/mcp-bridge/types.d.ts +69 -0
  43. package/dist/mcp-bridge/types.d.ts.map +1 -0
  44. package/dist/mcp-bridge/types.js +8 -0
  45. package/dist/mcp-bridge/types.js.map +1 -0
  46. package/dist/message-queue.d.ts +46 -0
  47. package/dist/message-queue.d.ts.map +1 -0
  48. package/dist/message-queue.js +90 -0
  49. package/dist/message-queue.js.map +1 -0
  50. package/dist/types.d.ts +129 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +13 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +40 -0
@@ -0,0 +1,960 @@
1
+ /**
2
+ * Conductor - orchestrates ACP proxy chains
3
+ *
4
+ * The conductor sits between a client and an agent, managing message routing
5
+ * through a chain of proxy components. It:
6
+ *
7
+ * 1. Manages the message event loop
8
+ * 2. Routes messages left-to-right (client → proxies → agent)
9
+ * 3. Routes messages right-to-left (agent → proxies → client)
10
+ * 4. Handles `_proxy/successor/*` message wrapping/unwrapping
11
+ * 5. Manages proxy capability handshake during initialization
12
+ * 6. Correlates requests with responses via a pending request map
13
+ * 7. Bridges MCP-over-ACP for agents without native ACP transport support
14
+ */
15
+ import { isJsonRpcRequest, isJsonRpcNotification, isJsonRpcResponse, createSuccessResponse, createErrorResponse, createRequest, createNotification, createResponder, PROXY_SUCCESSOR_REQUEST, PROXY_SUCCESSOR_NOTIFICATION, unwrapProxySuccessorRequest, unwrapProxySuccessorNotification, wrapAsProxySuccessorRequest, wrapAsProxySuccessorNotification, } from "@thinkwell/protocol";
16
+ import { MessageQueue } from "./message-queue.js";
17
+ import { McpBridge } from "./mcp-bridge/index.js";
18
+ import { createLogger, createNoopLogger } from "./logger.js";
19
+ /**
20
+ * The Conductor orchestrates ACP proxy chains.
21
+ *
22
+ * It sits between a client and an agent, routing all messages through
23
+ * a central event loop to preserve message ordering.
24
+ *
25
+ * ## Message Flow with Proxies
26
+ *
27
+ * ### Left-to-Right (client → agent):
28
+ * 1. Client sends request to conductor
29
+ * 2. Conductor forwards to proxy[0] (normal ACP)
30
+ * 3. Proxy[0] sends `_proxy/successor/request` to conductor
31
+ * 4. Conductor unwraps and forwards to proxy[1] (normal ACP)
32
+ * 5. ... until agent receives normal ACP
33
+ *
34
+ * ### Right-to-Left (agent → client):
35
+ * 1. Agent sends notification/request to conductor
36
+ * 2. Conductor wraps in `_proxy/successor/request` and sends to proxy[n-1]
37
+ * 3. Proxy[n-1] processes and forwards to conductor
38
+ * 4. ... until client receives normal ACP
39
+ */
40
+ export class Conductor {
41
+ config;
42
+ messageQueue = new MessageQueue();
43
+ logger;
44
+ state = { type: "uninitialized" };
45
+ // Component connections (populated after initialization)
46
+ clientConnection = null;
47
+ proxies = [];
48
+ agentConnection = null;
49
+ // Request/response correlation
50
+ // Maps outgoing request IDs to pending request info
51
+ pendingRequests = new Map();
52
+ nextRequestId = 1;
53
+ // MCP Bridge for agents without native MCP-over-ACP support
54
+ mcpBridge = null;
55
+ // Track whether agent supports mcp_acp_transport capability
56
+ agentSupportsMcpAcpTransport = false;
57
+ // Pending session/new requests waiting for response (for MCP URL transformation)
58
+ pendingSessionRequests = new Map();
59
+ // Maps MCP connection IDs to their responders for _mcp/connect
60
+ mcpConnectResponders = new Map();
61
+ constructor(config) {
62
+ this.config = config;
63
+ // Initialize logger
64
+ if (config.logging) {
65
+ this.logger = createLogger({
66
+ ...config.logging,
67
+ name: config.logging.name ?? config.name ?? "conductor",
68
+ trace: config.trace ?? config.logging.trace,
69
+ });
70
+ }
71
+ else if (config.trace) {
72
+ this.logger = createLogger({
73
+ level: "info",
74
+ name: config.name ?? "conductor",
75
+ trace: config.trace,
76
+ });
77
+ }
78
+ else {
79
+ this.logger = createNoopLogger();
80
+ }
81
+ // Initialize MCP bridge if enabled
82
+ if (config.mcpBridgeMode !== "disabled") {
83
+ this.mcpBridge = new McpBridge({ messageQueue: this.messageQueue });
84
+ }
85
+ }
86
+ /**
87
+ * Connect to a client and run the conductor's message loop.
88
+ *
89
+ * This method blocks until the conductor shuts down.
90
+ */
91
+ async connect(clientConnector) {
92
+ if (this.state.type !== "uninitialized") {
93
+ throw new Error(`Conductor is already ${this.state.type}`);
94
+ }
95
+ this.logger.info("Connecting to client");
96
+ this.clientConnection = await clientConnector.connect();
97
+ this.logger.debug("Client connected, starting message pump");
98
+ // Pump client messages into the queue
99
+ this.pumpClientMessages(this.clientConnection);
100
+ // Run the main event loop
101
+ this.logger.debug("Starting event loop");
102
+ await this.runEventLoop();
103
+ this.logger.info("Event loop exited");
104
+ }
105
+ /**
106
+ * Shut down the conductor
107
+ */
108
+ async shutdown() {
109
+ if (this.state.type === "shutdown") {
110
+ return;
111
+ }
112
+ this.logger.info("Shutting down conductor");
113
+ this.state = { type: "shutdown" };
114
+ this.messageQueue.close();
115
+ // Close all connections
116
+ const closePromises = [];
117
+ if (this.clientConnection) {
118
+ closePromises.push(this.clientConnection.close());
119
+ }
120
+ for (const proxy of this.proxies) {
121
+ closePromises.push(proxy.close());
122
+ }
123
+ if (this.agentConnection) {
124
+ closePromises.push(this.agentConnection.close());
125
+ }
126
+ // Close MCP bridge
127
+ if (this.mcpBridge) {
128
+ closePromises.push(this.mcpBridge.close());
129
+ }
130
+ await Promise.all(closePromises);
131
+ await this.logger.close();
132
+ }
133
+ /**
134
+ * Pump messages from the client into the message queue
135
+ */
136
+ pumpClientMessages(client) {
137
+ (async () => {
138
+ try {
139
+ for await (const message of client.messages) {
140
+ // Responses from the client (to agent requests) need special handling
141
+ if (isJsonRpcResponse(message)) {
142
+ const dispatch = this.messageToDispatch(message, { type: "client" });
143
+ if (dispatch && dispatch.type === "response") {
144
+ // Route the response back via pendingRequests
145
+ this.handleResponse(dispatch);
146
+ }
147
+ continue;
148
+ }
149
+ const dispatch = this.messageToDispatch(message, { type: "client" });
150
+ if (dispatch) {
151
+ this.messageQueue.push({
152
+ type: "left-to-right",
153
+ targetIndex: 0, // First component (proxy[0] or agent if no proxies)
154
+ dispatch,
155
+ });
156
+ }
157
+ }
158
+ }
159
+ catch (error) {
160
+ this.logger.error("Error reading from client", { error: String(error) });
161
+ }
162
+ finally {
163
+ // Client disconnected - shut down
164
+ this.logger.debug("Client disconnected, shutting down");
165
+ this.shutdown();
166
+ }
167
+ })();
168
+ }
169
+ /**
170
+ * Pump messages from a proxy into the message queue
171
+ */
172
+ pumpProxyMessages(connection, proxyIndex) {
173
+ (async () => {
174
+ try {
175
+ for await (const message of connection.messages) {
176
+ // Check for `_proxy/successor/*` messages
177
+ if (isJsonRpcRequest(message)) {
178
+ if (message.method === PROXY_SUCCESSOR_REQUEST) {
179
+ // Proxy is forwarding a request to its successor
180
+ this.handleProxySuccessorRequest(proxyIndex, message);
181
+ continue;
182
+ }
183
+ }
184
+ if (isJsonRpcNotification(message)) {
185
+ if (message.method === PROXY_SUCCESSOR_NOTIFICATION) {
186
+ // Proxy is forwarding a notification to its successor
187
+ this.handleProxySuccessorNotification(proxyIndex, message);
188
+ continue;
189
+ }
190
+ }
191
+ // Non-successor messages go right-to-left (toward client)
192
+ const dispatch = this.messageToDispatch(message, {
193
+ type: "proxy",
194
+ index: proxyIndex,
195
+ });
196
+ if (dispatch) {
197
+ this.messageQueue.push({
198
+ type: "right-to-left",
199
+ sourceIndex: { type: "proxy", index: proxyIndex },
200
+ dispatch,
201
+ });
202
+ }
203
+ }
204
+ }
205
+ catch (error) {
206
+ this.logger.error("Error reading from proxy", { proxyIndex, error: String(error) });
207
+ }
208
+ finally {
209
+ // Component disconnected - shut down the whole chain
210
+ this.logger.debug("Proxy disconnected, shutting down", { proxyIndex });
211
+ this.shutdown();
212
+ }
213
+ })();
214
+ }
215
+ /**
216
+ * Pump messages from the agent into the message queue
217
+ */
218
+ pumpAgentMessages(connection) {
219
+ (async () => {
220
+ try {
221
+ for await (const message of connection.messages) {
222
+ const dispatch = this.messageToDispatch(message, {
223
+ type: "successor",
224
+ });
225
+ if (dispatch) {
226
+ this.messageQueue.push({
227
+ type: "right-to-left",
228
+ sourceIndex: { type: "successor" },
229
+ dispatch,
230
+ });
231
+ }
232
+ }
233
+ }
234
+ catch (error) {
235
+ this.logger.error("Error reading from agent", { error: String(error) });
236
+ }
237
+ finally {
238
+ // Component disconnected - shut down the whole chain
239
+ this.logger.debug("Agent disconnected, shutting down");
240
+ this.shutdown();
241
+ }
242
+ })();
243
+ }
244
+ /**
245
+ * Handle a `_proxy/successor/request` from a proxy
246
+ *
247
+ * The proxy is forwarding a request to its successor (next proxy or agent).
248
+ * We unwrap the inner request and forward it.
249
+ */
250
+ handleProxySuccessorRequest(proxyIndex, message) {
251
+ const params = message.params;
252
+ const inner = unwrapProxySuccessorRequest(params);
253
+ // Create a responder that wraps the response back to the proxy
254
+ const responder = createResponder((result) => {
255
+ // Send success response back to the proxy for the _proxy/successor/request
256
+ this.proxies[proxyIndex]?.send(createSuccessResponse(message.id, result));
257
+ }, (error) => {
258
+ // Send error response back to the proxy
259
+ this.proxies[proxyIndex]?.send(createErrorResponse(message.id, error));
260
+ });
261
+ const dispatch = {
262
+ type: "request",
263
+ id: message.id,
264
+ method: inner.method,
265
+ params: inner.params,
266
+ responder,
267
+ };
268
+ // Forward to the next component (proxy[proxyIndex+1] or agent)
269
+ const targetIndex = proxyIndex + 1;
270
+ this.messageQueue.push({
271
+ type: "left-to-right",
272
+ targetIndex,
273
+ dispatch,
274
+ });
275
+ }
276
+ /**
277
+ * Handle a `_proxy/successor/notification` from a proxy
278
+ *
279
+ * The proxy is forwarding a notification to its successor.
280
+ */
281
+ handleProxySuccessorNotification(proxyIndex, message) {
282
+ const params = message.params;
283
+ const inner = unwrapProxySuccessorNotification(params);
284
+ const dispatch = {
285
+ type: "notification",
286
+ method: inner.method,
287
+ params: inner.params,
288
+ };
289
+ // Forward to the next component
290
+ const targetIndex = proxyIndex + 1;
291
+ this.messageQueue.push({
292
+ type: "left-to-right",
293
+ targetIndex,
294
+ dispatch,
295
+ });
296
+ }
297
+ /**
298
+ * Convert a JSON-RPC message to a Dispatch
299
+ */
300
+ messageToDispatch(message, source) {
301
+ if (isJsonRpcRequest(message)) {
302
+ // For requests, we need to create a responder that routes back
303
+ const responder = this.createResponderForSource(source, message.id);
304
+ return {
305
+ type: "request",
306
+ id: message.id,
307
+ method: message.method,
308
+ params: message.params,
309
+ responder,
310
+ };
311
+ }
312
+ if (isJsonRpcNotification(message)) {
313
+ return {
314
+ type: "notification",
315
+ method: message.method,
316
+ params: message.params,
317
+ };
318
+ }
319
+ if (isJsonRpcResponse(message)) {
320
+ return {
321
+ type: "response",
322
+ id: message.id,
323
+ result: "result" in message ? message.result : undefined,
324
+ error: "error" in message ? message.error : undefined,
325
+ };
326
+ }
327
+ return null;
328
+ }
329
+ /**
330
+ * Create a responder that routes the response back to the appropriate destination
331
+ */
332
+ createResponderForSource(source, requestId) {
333
+ if (source.type === "client") {
334
+ // Response goes back to client
335
+ return createResponder((result) => {
336
+ this.clientConnection?.send(createSuccessResponse(requestId, result));
337
+ }, (error) => {
338
+ this.clientConnection?.send(createErrorResponse(requestId, error));
339
+ });
340
+ }
341
+ else if (source.type === "proxy") {
342
+ // Response goes back to the proxy (as response to a normal ACP request)
343
+ const proxyIndex = source.index;
344
+ return createResponder((result) => {
345
+ this.proxies[proxyIndex]?.send(createSuccessResponse(requestId, result));
346
+ }, (error) => {
347
+ this.proxies[proxyIndex]?.send(createErrorResponse(requestId, error));
348
+ });
349
+ }
350
+ else {
351
+ // Response goes back to the agent
352
+ return createResponder((result) => {
353
+ this.agentConnection?.send(createSuccessResponse(requestId, result));
354
+ }, (error) => {
355
+ this.agentConnection?.send(createErrorResponse(requestId, error));
356
+ });
357
+ }
358
+ }
359
+ /**
360
+ * Run the main event loop, processing messages from the queue
361
+ */
362
+ async runEventLoop() {
363
+ for await (const message of this.messageQueue) {
364
+ await this.handleMessage(message);
365
+ }
366
+ }
367
+ /**
368
+ * Handle a message from the queue
369
+ */
370
+ async handleMessage(message) {
371
+ // Trace message for JSONL output
372
+ this.logger.traceMessage({
373
+ direction: message.type === "left-to-right" ? "left-to-right"
374
+ : message.type === "right-to-left" ? "right-to-left"
375
+ : "internal",
376
+ source: this.getMessageSource(message),
377
+ target: this.getMessageTarget(message),
378
+ message,
379
+ });
380
+ switch (message.type) {
381
+ case "left-to-right":
382
+ this.logger.trace("Routing left-to-right", {
383
+ targetIndex: message.targetIndex,
384
+ dispatchType: message.dispatch.type,
385
+ method: "method" in message.dispatch ? message.dispatch.method : undefined,
386
+ });
387
+ await this.handleLeftToRight(message.targetIndex, message.dispatch);
388
+ break;
389
+ case "right-to-left":
390
+ this.logger.trace("Routing right-to-left", {
391
+ sourceIndex: message.sourceIndex,
392
+ dispatchType: message.dispatch.type,
393
+ method: "method" in message.dispatch ? message.dispatch.method : undefined,
394
+ });
395
+ await this.handleRightToLeft(message.sourceIndex, message.dispatch);
396
+ break;
397
+ case "shutdown":
398
+ this.logger.debug("Received shutdown message");
399
+ // Already handled by message queue closing
400
+ break;
401
+ // MCP bridge messages
402
+ case "mcp-connection-received":
403
+ this.logger.debug("MCP connection received", { acpUrl: message.acpUrl, connectionId: message.connectionId });
404
+ await this.handleMcpConnectionReceived(message.acpUrl, message.connectionId);
405
+ break;
406
+ case "mcp-connection-established":
407
+ this.logger.debug("MCP connection established", { connectionId: message.connectionId });
408
+ // Connection established successfully - nothing more to do
409
+ break;
410
+ case "mcp-client-to-server":
411
+ this.logger.trace("MCP client-to-server message", { connectionId: message.connectionId });
412
+ await this.handleMcpClientToServer(message.connectionId, message.dispatch);
413
+ break;
414
+ case "mcp-connection-disconnected":
415
+ this.logger.debug("MCP connection disconnected", { connectionId: message.connectionId });
416
+ await this.handleMcpConnectionDisconnected(message.connectionId);
417
+ break;
418
+ }
419
+ }
420
+ /**
421
+ * Get the source identifier for a message (for tracing)
422
+ */
423
+ getMessageSource(message) {
424
+ switch (message.type) {
425
+ case "left-to-right":
426
+ return message.targetIndex === 0 ? "client" : `proxy[${message.targetIndex - 1}]`;
427
+ case "right-to-left":
428
+ return message.sourceIndex.type === "successor" ? "agent" : `proxy[${message.sourceIndex.index}]`;
429
+ case "mcp-connection-received":
430
+ case "mcp-client-to-server":
431
+ case "mcp-connection-disconnected":
432
+ return "mcp-bridge";
433
+ default:
434
+ return "internal";
435
+ }
436
+ }
437
+ /**
438
+ * Get the target identifier for a message (for tracing)
439
+ */
440
+ getMessageTarget(message) {
441
+ switch (message.type) {
442
+ case "left-to-right":
443
+ return message.targetIndex < this.proxies.length
444
+ ? `proxy[${message.targetIndex}]`
445
+ : "agent";
446
+ case "right-to-left":
447
+ if (message.sourceIndex.type === "proxy" && message.sourceIndex.index === 0) {
448
+ return "client";
449
+ }
450
+ return message.sourceIndex.type === "successor"
451
+ ? `proxy[${this.proxies.length - 1}]`
452
+ : `proxy[${message.sourceIndex.index - 1}]`;
453
+ case "mcp-connection-received":
454
+ case "mcp-client-to-server":
455
+ case "mcp-connection-disconnected":
456
+ return "client";
457
+ default:
458
+ return "conductor";
459
+ }
460
+ }
461
+ /**
462
+ * Handle a left-to-right message (client → agent direction)
463
+ */
464
+ async handleLeftToRight(targetIndex, dispatch) {
465
+ // Check if this is an initialize request from the client and we need to set up components
466
+ if (targetIndex === 0 &&
467
+ dispatch.type === "request" &&
468
+ (dispatch.method === "initialize" || dispatch.method === "acp/initialize")) {
469
+ await this.handleInitialize(dispatch);
470
+ return;
471
+ }
472
+ // Check if this is a session/new request that needs MCP URL transformation
473
+ if (dispatch.type === "request" &&
474
+ dispatch.method === "session/new" &&
475
+ this.mcpBridge &&
476
+ !this.agentSupportsMcpAcpTransport) {
477
+ await this.handleSessionNew(targetIndex, dispatch);
478
+ return;
479
+ }
480
+ // Determine target connection
481
+ const target = this.getTargetConnection(targetIndex);
482
+ if (!target) {
483
+ if (dispatch.type === "request") {
484
+ dispatch.responder.respondWithError({
485
+ code: -32603,
486
+ message: `No target connection for index ${targetIndex}`,
487
+ });
488
+ }
489
+ return;
490
+ }
491
+ // Forward to the target
492
+ this.forwardToConnection(target, dispatch);
493
+ }
494
+ /**
495
+ * Handle a right-to-left message (agent/proxy → client direction)
496
+ */
497
+ async handleRightToLeft(sourceIndex, dispatch) {
498
+ // Responses need special handling - route via pending request map
499
+ if (dispatch.type === "response") {
500
+ this.handleResponse(dispatch);
501
+ return;
502
+ }
503
+ // Determine where to send this message
504
+ // - If from successor (agent) and we have proxies, wrap and send to last proxy
505
+ // - If from proxy[n], send to proxy[n-1] or client if n==0
506
+ // - If no proxies, send directly to client
507
+ if (this.proxies.length === 0) {
508
+ // No proxies - send directly to client
509
+ if (this.clientConnection) {
510
+ this.forwardToConnection(this.clientConnection, dispatch);
511
+ }
512
+ return;
513
+ }
514
+ if (sourceIndex.type === "successor") {
515
+ // Message from agent - wrap and send to last proxy
516
+ const lastProxyIndex = this.proxies.length - 1;
517
+ this.forwardWrappedToProxy(lastProxyIndex, dispatch);
518
+ }
519
+ else if (sourceIndex.type === "proxy") {
520
+ const proxyIndex = sourceIndex.index;
521
+ if (proxyIndex === 0) {
522
+ // First proxy - send to client (unwrapped)
523
+ if (this.clientConnection) {
524
+ this.forwardToConnection(this.clientConnection, dispatch);
525
+ }
526
+ }
527
+ else {
528
+ // Send to previous proxy (wrapped)
529
+ this.forwardWrappedToProxy(proxyIndex - 1, dispatch);
530
+ }
531
+ }
532
+ }
533
+ /**
534
+ * Forward a dispatch to a proxy, wrapped in `_proxy/successor/*`
535
+ *
536
+ * This is used when routing messages FROM a successor (agent or later proxy)
537
+ * TO an earlier proxy in the chain.
538
+ */
539
+ forwardWrappedToProxy(proxyIndex, dispatch) {
540
+ const proxy = this.proxies[proxyIndex];
541
+ if (!proxy)
542
+ return;
543
+ switch (dispatch.type) {
544
+ case "request": {
545
+ // Wrap as `_proxy/successor/request`
546
+ const wrappedParams = wrapAsProxySuccessorRequest(dispatch.method, dispatch.params);
547
+ const outgoingId = this.generateRequestId();
548
+ this.pendingRequests.set(String(outgoingId), {
549
+ originalId: dispatch.id,
550
+ responder: dispatch.responder,
551
+ source: proxyIndex,
552
+ });
553
+ proxy.send(createRequest(outgoingId, PROXY_SUCCESSOR_REQUEST, wrappedParams));
554
+ break;
555
+ }
556
+ case "notification": {
557
+ // Wrap as `_proxy/successor/notification`
558
+ const wrappedParams = wrapAsProxySuccessorNotification(dispatch.method, dispatch.params);
559
+ proxy.send(createNotification(PROXY_SUCCESSOR_NOTIFICATION, wrappedParams));
560
+ break;
561
+ }
562
+ case "response":
563
+ // Responses are handled via handleResponse, not here
564
+ break;
565
+ }
566
+ }
567
+ /**
568
+ * Handle the initialize request - instantiate components and perform initialization sequence
569
+ *
570
+ * The initialization follows this sequence:
571
+ * 1. Instantiate all components (connect to proxies and agent)
572
+ * 2. Send `initialize` with `_meta.proxy: true` to proxy[0]
573
+ * 3. Proxy[0] will use `_proxy/successor/request` to forward to proxy[1], etc.
574
+ * 4. Agent receives `initialize` without proxy capability
575
+ * 5. Responses flow back up the chain
576
+ * 6. Conductor verifies each proxy accepted the proxy capability
577
+ */
578
+ async handleInitialize(dispatch) {
579
+ if (this.state.type !== "uninitialized") {
580
+ dispatch.responder.respondWithError({
581
+ code: -32600,
582
+ message: "Conductor already initialized",
583
+ });
584
+ return;
585
+ }
586
+ this.state = { type: "initializing" };
587
+ this.logger.info("Starting initialization");
588
+ try {
589
+ // Build the initialize request structure
590
+ const initRequest = {
591
+ method: dispatch.method,
592
+ params: dispatch.params,
593
+ };
594
+ // Instantiate components
595
+ this.logger.debug("Instantiating components");
596
+ const { proxies, agent } = await this.config.instantiator.instantiate(initRequest);
597
+ this.logger.info("Components instantiated", { proxyCount: proxies.length });
598
+ // Connect to proxies
599
+ for (const proxyConnector of proxies) {
600
+ const proxyConnection = await proxyConnector.connect();
601
+ this.proxies.push(proxyConnection);
602
+ this.logger.debug("Proxy connected", { proxyIndex: this.proxies.length - 1 });
603
+ // Start pumping messages from this proxy
604
+ this.pumpProxyMessages(proxyConnection, this.proxies.length - 1);
605
+ }
606
+ // Connect to agent
607
+ this.logger.debug("Connecting to agent");
608
+ this.agentConnection = await agent.connect();
609
+ this.pumpAgentMessages(this.agentConnection);
610
+ this.logger.info("Agent connected");
611
+ this.state = { type: "running" };
612
+ this.logger.info("Conductor running");
613
+ if (this.proxies.length === 0) {
614
+ // No proxies - forward initialize directly to agent
615
+ const outgoingId = this.generateRequestId();
616
+ this.pendingRequests.set(String(outgoingId), {
617
+ originalId: dispatch.id,
618
+ responder: this.createAgentInitializeResponder(dispatch.responder),
619
+ source: "client",
620
+ });
621
+ this.agentConnection.send(createRequest(outgoingId, dispatch.method, dispatch.params));
622
+ }
623
+ else {
624
+ // With proxies - send initialize with proxy capability to first proxy
625
+ const paramsWithProxy = this.addProxyCapability(dispatch.params);
626
+ const outgoingId = this.generateRequestId();
627
+ this.pendingRequests.set(String(outgoingId), {
628
+ originalId: dispatch.id,
629
+ responder: this.createProxyInitializeResponder(this.createAgentInitializeResponder(dispatch.responder)),
630
+ source: "client",
631
+ });
632
+ this.proxies[0].send(createRequest(outgoingId, dispatch.method, paramsWithProxy));
633
+ }
634
+ }
635
+ catch (error) {
636
+ this.state = { type: "uninitialized" };
637
+ this.logger.error("Initialization failed", { error: String(error) });
638
+ dispatch.responder.respondWithError({
639
+ code: -32603,
640
+ message: `Failed to initialize: ${error}`,
641
+ });
642
+ }
643
+ }
644
+ /**
645
+ * Add proxy capability to initialize params
646
+ */
647
+ addProxyCapability(params) {
648
+ const p = (params ?? {});
649
+ const meta = (p._meta ?? {});
650
+ return {
651
+ ...p,
652
+ _meta: {
653
+ ...meta,
654
+ proxy: true,
655
+ },
656
+ };
657
+ }
658
+ /**
659
+ * Remove proxy capability from initialize params (for forwarding to agent)
660
+ */
661
+ removeProxyCapability(params) {
662
+ const p = (params ?? {});
663
+ const meta = (p._meta ?? {});
664
+ const { proxy: _, ...restMeta } = meta;
665
+ return {
666
+ ...p,
667
+ _meta: restMeta,
668
+ };
669
+ }
670
+ /**
671
+ * Create a responder that verifies the proxy accepted the capability
672
+ */
673
+ createProxyInitializeResponder(originalResponder) {
674
+ return createResponder((result) => {
675
+ // Verify the proxy accepted the proxy capability
676
+ const response = result;
677
+ if (!response?._meta?.proxy) {
678
+ originalResponder.respondWithError({
679
+ code: -32600,
680
+ message: "Proxy component did not accept proxy capability",
681
+ });
682
+ return;
683
+ }
684
+ originalResponder.respond(result);
685
+ }, (error) => {
686
+ originalResponder.respondWithError(error);
687
+ });
688
+ }
689
+ /**
690
+ * Create a responder that captures the agent's mcp_acp_transport capability
691
+ */
692
+ createAgentInitializeResponder(originalResponder) {
693
+ return createResponder((result) => {
694
+ // Capture the mcp_acp_transport capability
695
+ const response = result;
696
+ if (response?.capabilities?.mcp_acp_transport) {
697
+ this.agentSupportsMcpAcpTransport = true;
698
+ }
699
+ originalResponder.respond(result);
700
+ }, (error) => {
701
+ originalResponder.respondWithError(error);
702
+ });
703
+ }
704
+ /**
705
+ * Handle a response by routing it back to the original requester
706
+ */
707
+ handleResponse(dispatch) {
708
+ const pending = this.pendingRequests.get(String(dispatch.id));
709
+ if (!pending) {
710
+ // No pending request for this ID - might be a duplicate or error
711
+ this.logger.warn("No pending request for response ID", { id: dispatch.id });
712
+ return;
713
+ }
714
+ this.pendingRequests.delete(String(dispatch.id));
715
+ if (dispatch.error) {
716
+ pending.responder.respondWithError(dispatch.error);
717
+ }
718
+ else {
719
+ pending.responder.respond(dispatch.result);
720
+ }
721
+ }
722
+ /**
723
+ * Forward a dispatch to a connection, handling request ID rewriting
724
+ */
725
+ forwardToConnection(connection, dispatch) {
726
+ switch (dispatch.type) {
727
+ case "request": {
728
+ // Rewrite request ID and track for response routing
729
+ const outgoingId = this.generateRequestId();
730
+ this.pendingRequests.set(String(outgoingId), {
731
+ originalId: dispatch.id,
732
+ responder: dispatch.responder,
733
+ source: "client", // Default - actual source tracking is in PendingRequest
734
+ });
735
+ connection.send(createRequest(outgoingId, dispatch.method, dispatch.params));
736
+ break;
737
+ }
738
+ case "notification":
739
+ connection.send(createNotification(dispatch.method, dispatch.params));
740
+ break;
741
+ case "response":
742
+ // Responses are handled via handleResponse, not forwarded directly
743
+ if (dispatch.error) {
744
+ connection.send(createErrorResponse(dispatch.id, dispatch.error));
745
+ }
746
+ else {
747
+ connection.send(createSuccessResponse(dispatch.id, dispatch.result));
748
+ }
749
+ break;
750
+ }
751
+ }
752
+ /**
753
+ * Get the target connection for a given index
754
+ * Index 0..n-1 are proxies, index n is the agent
755
+ */
756
+ getTargetConnection(targetIndex) {
757
+ if (targetIndex < this.proxies.length) {
758
+ return this.proxies[targetIndex];
759
+ }
760
+ if (targetIndex === this.proxies.length) {
761
+ return this.agentConnection;
762
+ }
763
+ return null;
764
+ }
765
+ /**
766
+ * Handle a session/new request - transform acp: URLs to http: URLs
767
+ *
768
+ * If the request contains MCP servers with acp: URLs and the agent doesn't
769
+ * support mcp_acp_transport, we:
770
+ * 1. Spawn HTTP listeners for each acp: URL
771
+ * 2. Transform the URLs to http://localhost:$PORT
772
+ * 3. Forward the modified request to the agent
773
+ * 4. When the response comes back with session_id, deliver it to the listeners
774
+ */
775
+ async handleSessionNew(targetIndex, dispatch) {
776
+ const params = dispatch.params;
777
+ const servers = params?.mcpServers;
778
+ // Generate a session key to correlate the request with the response
779
+ const sessionKey = `session-${this.generateRequestId()}`;
780
+ // Transform MCP servers if needed
781
+ const { transformedServers, hasAcpServers } = await this.mcpBridge.transformMcpServers(servers, sessionKey);
782
+ if (!hasAcpServers) {
783
+ // No acp: servers - forward unchanged
784
+ const target = this.getTargetConnection(targetIndex);
785
+ if (target) {
786
+ this.forwardToConnection(target, dispatch);
787
+ }
788
+ return;
789
+ }
790
+ // Build the modified params with transformed servers
791
+ const modifiedParams = {
792
+ ...params,
793
+ mcpServers: transformedServers,
794
+ };
795
+ // Create a responder that captures the session_id and delivers it to listeners
796
+ const originalResponder = dispatch.responder;
797
+ const wrappedResponder = createResponder((result) => {
798
+ // Extract session_id from the response
799
+ const response = result;
800
+ if (response?.sessionId) {
801
+ this.mcpBridge.completeSession(sessionKey, response.sessionId);
802
+ }
803
+ originalResponder.respond(result);
804
+ }, async (error) => {
805
+ // Cancel the pending session on error
806
+ await this.mcpBridge.cancelSession(sessionKey);
807
+ originalResponder.respondWithError(error);
808
+ });
809
+ // Forward the modified request
810
+ const target = this.getTargetConnection(targetIndex);
811
+ if (!target) {
812
+ dispatch.responder.respondWithError({
813
+ code: -32603,
814
+ message: `No target connection for index ${targetIndex}`,
815
+ });
816
+ return;
817
+ }
818
+ const modifiedDispatch = {
819
+ type: "request",
820
+ id: dispatch.id,
821
+ method: dispatch.method,
822
+ params: modifiedParams,
823
+ responder: wrappedResponder,
824
+ };
825
+ this.forwardToConnection(target, modifiedDispatch);
826
+ }
827
+ /**
828
+ * Handle an MCP connection received from the HTTP bridge
829
+ *
830
+ * When an agent connects to our HTTP listener, we need to:
831
+ * 1. Send _mcp/connect to the proxy that owns this acp: URL
832
+ * 2. Wait for the connection_id in the response
833
+ */
834
+ async handleMcpConnectionReceived(acpUrl, connectionId) {
835
+ // The connection needs to be routed to the proxy that provides this MCP server
836
+ // For now, we route to the first proxy (or client if no proxies)
837
+ // In a more complete implementation, we'd track which proxy registered which URL
838
+ const mcpConnectParams = {
839
+ connectionId,
840
+ url: acpUrl,
841
+ };
842
+ // Create a responder for the _mcp/connect request
843
+ const responder = createResponder((result) => {
844
+ // Connection established - notify via message queue
845
+ const response = result;
846
+ this.messageQueue.push({
847
+ type: "mcp-connection-established",
848
+ connectionId: response.connectionId ?? connectionId,
849
+ serverInfo: response.serverInfo ?? { name: "unknown", version: "0.0.0" },
850
+ });
851
+ }, (error) => {
852
+ this.logger.error("MCP connect failed", { connectionId, acpUrl, error });
853
+ });
854
+ this.mcpConnectResponders.set(connectionId, responder);
855
+ // Route _mcp/connect request toward the client (right-to-left)
856
+ const dispatch = {
857
+ type: "request",
858
+ id: this.generateRequestId(),
859
+ method: "_mcp/connect",
860
+ params: mcpConnectParams,
861
+ responder,
862
+ };
863
+ // Send to client (or first proxy in the backward direction)
864
+ if (this.proxies.length === 0) {
865
+ // No proxies - send directly to client
866
+ if (this.clientConnection) {
867
+ this.forwardToConnection(this.clientConnection, dispatch);
868
+ }
869
+ }
870
+ else {
871
+ // With proxies - wrap and send to last proxy (backward direction)
872
+ this.forwardWrappedToProxy(this.proxies.length - 1, dispatch);
873
+ }
874
+ }
875
+ /**
876
+ * Handle an MCP message from a client (through the HTTP bridge)
877
+ *
878
+ * This routes MCP tool calls and other messages through the ACP chain.
879
+ */
880
+ async handleMcpClientToServer(connectionId, dispatch) {
881
+ // Wrap the MCP message in _mcp/message format
882
+ const mcpMessageParams = {
883
+ connectionId,
884
+ method: dispatch.type === "request" || dispatch.type === "notification"
885
+ ? dispatch.method
886
+ : undefined,
887
+ params: dispatch.type === "request" || dispatch.type === "notification"
888
+ ? dispatch.params
889
+ : undefined,
890
+ id: dispatch.type === "request" ? dispatch.id : undefined,
891
+ };
892
+ if (dispatch.type === "request") {
893
+ // Create _mcp/message request
894
+ const mcpDispatch = {
895
+ type: "request",
896
+ id: this.generateRequestId(),
897
+ method: "_mcp/message",
898
+ params: mcpMessageParams,
899
+ responder: dispatch.responder,
900
+ };
901
+ // Route toward client (right-to-left direction)
902
+ if (this.proxies.length === 0) {
903
+ if (this.clientConnection) {
904
+ this.forwardToConnection(this.clientConnection, mcpDispatch);
905
+ }
906
+ }
907
+ else {
908
+ this.forwardWrappedToProxy(this.proxies.length - 1, mcpDispatch);
909
+ }
910
+ }
911
+ else if (dispatch.type === "notification") {
912
+ // Create _mcp/message notification
913
+ const mcpDispatch = {
914
+ type: "notification",
915
+ method: "_mcp/message",
916
+ params: mcpMessageParams,
917
+ };
918
+ // Route toward client
919
+ if (this.proxies.length === 0) {
920
+ if (this.clientConnection) {
921
+ this.forwardToConnection(this.clientConnection, mcpDispatch);
922
+ }
923
+ }
924
+ else {
925
+ this.forwardWrappedToProxy(this.proxies.length - 1, mcpDispatch);
926
+ }
927
+ }
928
+ }
929
+ /**
930
+ * Handle an MCP connection being disconnected
931
+ */
932
+ async handleMcpConnectionDisconnected(connectionId) {
933
+ // Clean up the connect responder if still pending
934
+ this.mcpConnectResponders.delete(connectionId);
935
+ // Send _mcp/disconnect notification toward client
936
+ const dispatch = {
937
+ type: "notification",
938
+ method: "_mcp/disconnect",
939
+ params: { connectionId },
940
+ };
941
+ // Route toward client
942
+ if (this.proxies.length === 0) {
943
+ if (this.clientConnection) {
944
+ this.forwardToConnection(this.clientConnection, dispatch);
945
+ }
946
+ }
947
+ else {
948
+ this.forwardWrappedToProxy(this.proxies.length - 1, dispatch);
949
+ }
950
+ }
951
+ /**
952
+ * Generate a unique request ID for outgoing requests
953
+ */
954
+ generateRequestId() {
955
+ return this.nextRequestId++;
956
+ }
957
+ }
958
+ // Re-export instantiator helpers for convenience
959
+ export { fromCommands, fromConnectors, dynamic, staticInstantiator } from "./instantiators.js";
960
+ //# sourceMappingURL=conductor.js.map