Skip to content

Support SSE protocol with fixes of mcp client#211

Open
dIvYaNshhh wants to merge 12 commits intomainfrom
feature/gateway-sse-transport
Open

Support SSE protocol with fixes of mcp client#211
dIvYaNshhh wants to merge 12 commits intomainfrom
feature/gateway-sse-transport

Conversation

@dIvYaNshhh
Copy link
Copy Markdown
Collaborator

@dIvYaNshhh dIvYaNshhh commented Apr 9, 2026

Add SSE server transport and fix critical thread-safety crashes

Summary

Adds SSE server transport, fixes critical thread-safety crashes in McpClient/McpServer, and replaces fragile raw socket HTTP POST with libcurl.

Changes

New Features

  • SSE server transport — Server now accepts GET /sse to open a long-lived stream and POST /callback/{session_id} to receive JSON-RPC requests, with responses routed back through the SSE stream via a session registry. Supports external_url config for absolute callback URLs behind reverse proxies (Traefik, nginx).
  • Configurable SSE/RPC pathshttp_sse_path and http_rpc_path are now wired through McpServerConfig to the filter chain factory.
  • libcurl HTTP POST — Replaces raw socket POST in McpConnectionManager::sendHttpPost(). The raw socket approach was failing due to EAGAIN on SSL handshake and HTTP/1.1 vs HTTP/2 mismatches.

Bug Fixes

Crashes / use-after-free

  • Defer connection manager destruction during reconnect (src/client/mcp_client.cc) — reconnectInternal() was destroying the old McpConnectionManager immediately while libevent callbacks were still queued in the current event-loop iteration. Now moves it to a dead_connection_managers_ list cleared on the next iteration.
  • Move closed connections to dead list (src/mcp_connection_manager.cc) — Replaces deferred-via-dispatcher destruction with a closed_connections_ vector. Calls readDisable(true) first to unregister libevent fd events before retiring the connection, preventing stale callbacks from firing.
  • Session cleanup on connection close (src/server/mcp_server.cc) — Fixed map erase race in onConnectionEvent() by removing redundant connection_sessions_ erase that duplicated SessionManager::removeSessionByConnection().

Timer lifetime / resource leaks

  • Background task timers (src/server/mcp_server.cc) — startBackgroundTasks() was assigning timers to local unique_ptr variables that went out of scope immediately, so session cleanup never ran. Timers are now stored in session_cleanup_timer_ and resource_update_timer_ member variables. stopBackgroundTasks() explicitly disables and resets them.
  • Disable HTTP body timeout for client mode (src/filter/http_codec_filter.cc) — The 60s body timeout was firing on client SSE connections (where the body is an infinite stream), then crashing in HttpCodecStateMachine::executeTransition(). Body timeout is now 0 (disabled) for client mode.

Routing / proxy compatibility

  • SSE callback URL matching (src/filter/http_sse_filter_chain_factory.cc) — Old code required path.find("/callback/") == 0, which failed when behind a reverse proxy that forwarded a prefixed path like /v1/mcp/gateways/{id}/callback/client_1. Now uses path.rfind("/callback/") to match anywhere in the path.
  • Removed Connection hop-by-hop header — Should not be set by the application layer.
  • Send notifications/initialized after protocol init as required by MCP spec.

Files Changed

File Purpose
include/mcp/client/mcp_client.h Add dead_connection_managers_ member
src/client/mcp_client.cc Defer connection manager destruction; send notifications/initialized
include/mcp/mcp_connection_manager.h Add closed_connections_ member
src/mcp_connection_manager.cc libcurl HTTP POST; dead-list connection cleanup
include/mcp/server/mcp_server.h Add background task timer members; SSE/RPC path config
src/server/mcp_server.cc Fix timer lifetime; session cleanup race; wire config paths
include/mcp/filter/http_sse_filter_chain_factory.h Add SSE path/external_url params
src/filter/http_sse_filter_chain_factory.cc SSE server transport + session registry; proxy-prefix-aware callback routing
src/filter/http_codec_filter.cc Disable client-mode body timeout

Testing

Verified locally with Docker against production MCP backends:

  • Both POST /mcp (Streamable HTTP) and GET /sse + POST /callback/{id} transports work
  • Tool calls (Gmail, Apollo) return responses in 0.8s–2s
  • No SIGSEGV after extended runtime
  • Clean shutdown
  • Works behind reverse proxy with external_url config

The MCP spec requires sending notifications/initialized before
any subsequent requests like tools/list. Without this, some
servers reject the tools/list request.
The Connection header is prohibited in HTTP/2 (RFC 9113 Section
8.2.2) and causes Traefik v3 to fail when translating HTTP/1.1
backend responses to HTTP/2 for external clients. HTTP/1.1
defaults to keep-alive without an explicit header.
The raw socket approach for sendHttpPost failed due to SSL
handshake issues (EAGAIN dropping Client Hello) and HTTP/1.1
vs HTTP/2 mismatch with servers that prefer h2. Using libcurl
handles TLS negotiation and protocol selection correctly.
Runs POST in a detached thread to avoid blocking the event loop.
Rename http_sse_path default from /events to /sse for clarity.
Add external_url field to McpServerConfig for constructing absolute
SSE callback URLs when running behind a reverse proxy.
Pass http_sse_path, http_rpc_path, and external_url from
McpServerConfig to the HttpSseFilterChainFactory so the filter
chain uses the configured endpoint paths instead of hardcoded
defaults.

Defer active_connections_ removal to the next event loop iteration
via dispatcher post to prevent use-after-free when a connection
is destroyed during its own close callback.

Add response logging for debugging request/response flow.
Extend HttpSseFilterChainFactory constructor to accept configurable
sse_path, rpc_path, and external_url parameters. These are passed
through to the protocol filter for SSE server transport support.
Add full MCP SSE server transport support:

- SseSessionRegistry tracks active SSE connections by session ID
- GET /sse opens SSE stream, registers session, sends endpoint event
  with absolute callback URL (supports reverse proxy deployments)
- POST /callback/{session_id} sends 202 Accepted and routes the
  JSON-RPC response through the corresponding SSE stream
- POST /mcp continues to work as Streamable HTTP (direct response)
- Configurable SSE/RPC paths passed from server config
- SSE session cleanup on connection close via filter destructor
- Updated /info endpoint to reflect configured paths
- CORS registration for configurable endpoint paths
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

❌ Code Formatting Check Failed

Some files in this PR are not properly formatted according to the project's clang-format rules.

To fix this issue:

make format

Then commit and push the changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants