Support SSE protocol with fixes of mcp client#211
Open
dIvYaNshhh wants to merge 12 commits intomainfrom
Open
Conversation
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
❌ Code Formatting Check FailedSome files in this PR are not properly formatted according to the project's clang-format rules. To fix this issue: make formatThen commit and push the changes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
GET /sseto open a long-lived stream andPOST /callback/{session_id}to receive JSON-RPC requests, with responses routed back through the SSE stream via a session registry. Supportsexternal_urlconfig for absolute callback URLs behind reverse proxies (Traefik, nginx).http_sse_pathandhttp_rpc_pathare now wired throughMcpServerConfigto the filter chain factory.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
src/client/mcp_client.cc) —reconnectInternal()was destroying the oldMcpConnectionManagerimmediately while libevent callbacks were still queued in the current event-loop iteration. Now moves it to adead_connection_managers_list cleared on the next iteration.src/mcp_connection_manager.cc) — Replaces deferred-via-dispatcher destruction with aclosed_connections_vector. CallsreadDisable(true)first to unregister libevent fd events before retiring the connection, preventing stale callbacks from firing.src/server/mcp_server.cc) — Fixed map erase race inonConnectionEvent()by removing redundantconnection_sessions_erase that duplicatedSessionManager::removeSessionByConnection().Timer lifetime / resource leaks
src/server/mcp_server.cc) —startBackgroundTasks()was assigning timers to localunique_ptrvariables that went out of scope immediately, so session cleanup never ran. Timers are now stored insession_cleanup_timer_andresource_update_timer_member variables.stopBackgroundTasks()explicitly disables and resets them.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 inHttpCodecStateMachine::executeTransition(). Body timeout is now0(disabled) for client mode.Routing / proxy compatibility
src/filter/http_sse_filter_chain_factory.cc) — Old code requiredpath.find("/callback/") == 0, which failed when behind a reverse proxy that forwarded a prefixed path like/v1/mcp/gateways/{id}/callback/client_1. Now usespath.rfind("/callback/")to match anywhere in the path.Connectionhop-by-hop header — Should not be set by the application layer.notifications/initializedafter protocol init as required by MCP spec.Files Changed
include/mcp/client/mcp_client.hdead_connection_managers_membersrc/client/mcp_client.ccnotifications/initializedinclude/mcp/mcp_connection_manager.hclosed_connections_membersrc/mcp_connection_manager.ccinclude/mcp/server/mcp_server.hsrc/server/mcp_server.ccinclude/mcp/filter/http_sse_filter_chain_factory.hsrc/filter/http_sse_filter_chain_factory.ccsrc/filter/http_codec_filter.ccTesting
Verified locally with Docker against production MCP backends:
POST /mcp(Streamable HTTP) andGET /sse+POST /callback/{id}transports workexternal_urlconfig