(deps: LocalHandshakeDeps)
| 212 | * never costs the old fall-back-to-direct robustness. |
| 213 | */ |
| 214 | export async function runLocalHandshakeProxy(deps: LocalHandshakeDeps): Promise<void> { |
| 215 | let daemonStatus: 'connecting' | 'ready' | 'failed' = 'connecting'; |
| 216 | let daemonSocket: net.Socket | null = null; |
| 217 | let clientInitId: unknown = undefined; // suppress the daemon's reply to the forwarded initialize |
| 218 | // Telemetry attribution for the in-process fallback only — calls routed to |
| 219 | // the daemon are counted by the daemon's own session (which receives the |
| 220 | // forwarded initialize, clientInfo included), never double-counted here. |
| 221 | let telemetryClient: ClientInfo | undefined; |
| 222 | const pending: string[] = []; // client lines buffered until the daemon resolves |
| 223 | let engine: MCPEngine | null = null; |
| 224 | let engineReady: Promise<void> | null = null; |
| 225 | let shuttingDown = false; |
| 226 | // Requests forwarded to the daemon and not yet answered, keyed by JSON-RPC id. |
| 227 | // If the daemon dies mid-session (#662 — e.g. an MCP host SIGTERM's it when a |
| 228 | // new session starts), these would otherwise hang forever; we re-serve them |
| 229 | // in-process so the host always gets a reply. |
| 230 | const inflight = new Map<unknown, string>(); |
| 231 | const trackInflight = (line: string): void => { |
| 232 | try { |
| 233 | const m = JSON.parse(line) as JsonRpc; |
| 234 | if (m && m.id !== undefined && typeof m.method === 'string' && m.method !== 'initialize') { |
| 235 | inflight.set(m.id, line); |
| 236 | } |
| 237 | } catch { /* unparseable — nothing we could re-serve anyway */ } |
| 238 | }; |
| 239 | |
| 240 | const writeClient = (obj: JsonRpc | string): void => { |
| 241 | try { process.stdout.write((typeof obj === 'string' ? obj : JSON.stringify(obj)) + '\n'); } catch { /* host gone */ } |
| 242 | }; |
| 243 | const shutdown = (): void => { |
| 244 | if (shuttingDown) return; shuttingDown = true; |
| 245 | try { daemonSocket?.destroy(); } catch { /* ignore */ } |
| 246 | try { engine?.stop(); } catch { /* ignore */ } |
| 247 | process.exit(0); |
| 248 | }; |
| 249 | const ensureEngine = (): Promise<void> => { |
| 250 | if (!engine) engine = deps.makeEngine(); |
| 251 | if (!engineReady) engineReady = engine.ensureInitialized(deps.root).catch(() => { /* degraded */ }); |
| 252 | return engineReady; |
| 253 | }; |
| 254 | // Daemon-unavailable fallback: serve a client message in-process. |
| 255 | const handleLocally = async (line: string): Promise<void> => { |
| 256 | let msg: JsonRpc; try { msg = JSON.parse(line) as JsonRpc; } catch { return; } |
| 257 | const id = msg.id; |
| 258 | if (msg.method === 'tools/call' && id !== undefined) { |
| 259 | try { |
| 260 | await ensureEngine(); |
| 261 | const params = (msg.params || {}) as { name: string; arguments?: Record<string, unknown> }; |
| 262 | const result = await engine!.getToolHandler().execute(params.name, params.arguments || {}); |
| 263 | writeClient({ jsonrpc: '2.0', id, result }); |
| 264 | getTelemetry().recordUsage('mcp_tool', params.name, !result.isError, telemetryClient); |
| 265 | } catch (err) { |
| 266 | writeClient({ jsonrpc: '2.0', id, error: { code: -32603, message: err instanceof Error ? err.message : String(err) } }); |
| 267 | } |
| 268 | } else if (msg.method === 'ping' && id !== undefined) { |
| 269 | writeClient({ jsonrpc: '2.0', id, result: {} }); |
| 270 | } else if (id !== undefined && msg.method !== 'initialize') { |
| 271 | // A request we can't serve in-process (and the daemon is gone) — answer |
no test coverage detected