Summary
When transportToServer.send(message) rejects (e.g. due to a transport-level HTTP error from the remote MCP endpoint), mcpProxy catches the error via onServerError, logs it to stderr, and returns — without sending any response back to the local client. The client's awaiting request is left with no resolution, so any caller blocking on that request id parks indefinitely until an external timeout fires (if one exists at all).
Root cause
In src/lib/utils.ts, the relevant line is:
transportToServer.send(message).catch(onServerError)
onServerError only logs:
function onServerError(error: Error) {
log('Error from remote server:', error)
debugLog('Error from remote server', { stack: error.stack })
// returns — no response sent back to transportToClient
}
Because message.id is not in scope inside onServerError, it cannot construct a JSON-RPC error response even if it wanted to.
For contrast, the ignoredTools path in the same function correctly handles the error case by sending a JSON-RPC error back to the client (because request.id is in scope there).
Impact
- Any request that triggers a transport-level error (HTTP 4xx/5xx, connection refused, etc.) on the remote side leaves the local client waiting indefinitely
- The remote process stays alive (no
process.exit), so the hang is silent — the only signal is the stderr log
- This affects
initialize, tools/call, tools/list, and any other request forwarded through mcpProxy
Related issues with the same root cause: #138 (ECONNREFUSED logged, process lives), #226 (SSE hangs on long-running requests).
Expected behaviour
When the server send fails, the client should receive a JSON-RPC error response for the in-flight request id so it can surface the error immediately rather than hanging.
Proposed fix
Thread message.id into the catch handler and send a JSON-RPC error back to the client:
const requestId = 'id' in message ? (message as any).id : undefined
transportToServer.send(message).catch((error: Error) => {
onServerError(error)
if (requestId !== undefined) {
const errorResponse = {
jsonrpc: '2.0' as const,
id: requestId,
error: {
code: -32603,
message: error.message ?? 'Internal error forwarding request to remote server',
},
}
transportToClient.send(errorResponse).catch(onClientError)
}
})
Notifications (no id) are unaffected. Existing stderr logging is preserved.
Summary
When
transportToServer.send(message)rejects (e.g. due to a transport-level HTTP error from the remote MCP endpoint),mcpProxycatches the error viaonServerError, logs it to stderr, and returns — without sending any response back to the local client. The client's awaiting request is left with no resolution, so any caller blocking on that request id parks indefinitely until an external timeout fires (if one exists at all).Root cause
In
src/lib/utils.ts, the relevant line is:onServerErroronly logs:Because
message.idis not in scope insideonServerError, it cannot construct a JSON-RPC error response even if it wanted to.For contrast, the
ignoredToolspath in the same function correctly handles the error case by sending a JSON-RPC error back to the client (becauserequest.idis in scope there).Impact
process.exit), so the hang is silent — the only signal is the stderr loginitialize,tools/call,tools/list, and any other request forwarded throughmcpProxyRelated issues with the same root cause: #138 (ECONNREFUSED logged, process lives), #226 (SSE hangs on long-running requests).
Expected behaviour
When the server send fails, the client should receive a JSON-RPC error response for the in-flight request id so it can surface the error immediately rather than hanging.
Proposed fix
Thread
message.idinto the catch handler and send a JSON-RPC error back to the client:Notifications (no
id) are unaffected. Existing stderr logging is preserved.