@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.
- package/README.md +140 -0
- package/dist/conductor.d.ts +219 -0
- package/dist/conductor.d.ts.map +1 -0
- package/dist/conductor.js +960 -0
- package/dist/conductor.js.map +1 -0
- package/dist/connectors/channel.d.ts +60 -0
- package/dist/connectors/channel.d.ts.map +1 -0
- package/dist/connectors/channel.js +155 -0
- package/dist/connectors/channel.js.map +1 -0
- package/dist/connectors/index.d.ts +6 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +6 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/stdio.d.ts +36 -0
- package/dist/connectors/stdio.d.ts.map +1 -0
- package/dist/connectors/stdio.js +198 -0
- package/dist/connectors/stdio.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/instantiators.d.ts +129 -0
- package/dist/instantiators.d.ts.map +1 -0
- package/dist/instantiators.js +183 -0
- package/dist/instantiators.js.map +1 -0
- package/dist/logger.d.ts +121 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +162 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp-bridge/http-listener.d.ts +35 -0
- package/dist/mcp-bridge/http-listener.d.ts.map +1 -0
- package/dist/mcp-bridge/http-listener.js +204 -0
- package/dist/mcp-bridge/http-listener.js.map +1 -0
- package/dist/mcp-bridge/index.d.ts +9 -0
- package/dist/mcp-bridge/index.d.ts.map +1 -0
- package/dist/mcp-bridge/index.js +8 -0
- package/dist/mcp-bridge/index.js.map +1 -0
- package/dist/mcp-bridge/mcp-bridge.d.ts +80 -0
- package/dist/mcp-bridge/mcp-bridge.d.ts.map +1 -0
- package/dist/mcp-bridge/mcp-bridge.js +170 -0
- package/dist/mcp-bridge/mcp-bridge.js.map +1 -0
- package/dist/mcp-bridge/types.d.ts +69 -0
- package/dist/mcp-bridge/types.d.ts.map +1 -0
- package/dist/mcp-bridge/types.js +8 -0
- package/dist/mcp-bridge/types.js.map +1 -0
- package/dist/message-queue.d.ts +46 -0
- package/dist/message-queue.d.ts.map +1 -0
- package/dist/message-queue.js +90 -0
- package/dist/message-queue.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- 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
|