Skip to content

mcpProxy: transport errors on server send are swallowed — client awaiter never receives a response #293

Description

@sahilm-ti

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions