fix(mcp): Fix legacy MCP SSE connections#2301
Conversation
There was a problem hiding this comment.
Code Review
This pull request transitions MCP JSON-RPC request IDs from numeric values to strings to improve compatibility with various gateways, while maintaining support for numeric echoes. It also routes legacy SSE endpoint URLs directly to SseTransport instead of HttpTransport. The review feedback highlights a potential issue where custom headers (such as authentication) are ignored when bypassing HttpTransport for legacy SSE connections. Additionally, several performance optimizations are suggested to pass references instead of cloning string IDs when constructing JSON payloads.
| if is_legacy_sse_endpoint_url(url) { | ||
| Box::new( | ||
| SseTransport::connect( | ||
| client, | ||
| url.clone(), | ||
| cancel_token.clone(), | ||
| Duration::from_secs(connect_timeout_secs), | ||
| ) | ||
| .await?, | ||
| ) |
There was a problem hiding this comment.
By bypassing HttpTransport and directly connecting via SseTransport::connect for legacy SSE URLs, any custom headers configured in config.headers (such as Authorization or API keys) will be completely ignored. SseTransport currently does not accept or apply custom headers in its connection loop or message sending. If a legacy SSE server is behind an API gateway or requires authentication headers, the connection will fail. Consider updating SseTransport to accept and apply these custom headers.
| self.send(serde_json::json!({ | ||
| "jsonrpc": "2.0", | ||
| "id": init_id, | ||
| "id": init_id.clone(), |
There was a problem hiding this comment.
You can avoid cloning init_id here by passing a reference &init_id to the serde_json::json! macro. Since serde_json::json! only needs to serialize the value, a reference is sufficient and avoids an unnecessary heap allocation. This also leaves init_id owned and available to be passed to self.recv(init_id) later without cloning.
| "id": init_id.clone(), | |
| "id": &init_id, |
| self.send(serde_json::json!({ | ||
| "jsonrpc": "2.0", | ||
| "id": list_id, | ||
| "id": list_id.clone(), |
| self.send(serde_json::json!({ | ||
| "jsonrpc": "2.0", | ||
| "id": list_id, | ||
| "id": list_id.clone(), |
| self.send(serde_json::json!({ | ||
| "jsonrpc": "2.0", | ||
| "id": list_id, | ||
| "id": list_id.clone(), |
| self.send(serde_json::json!({ | ||
| "jsonrpc": "2.0", | ||
| "id": list_id, | ||
| "id": list_id.clone(), |
| self.send(serde_json::json!({ | ||
| "jsonrpc": "2.0", | ||
| "id": call_id, | ||
| "id": call_id.clone(), |
|
Addressed the review feedback in
Verification:
|
Summary
Bugfix
This fixes legacy SSE MCP servers that expose an initial GET /sse stream with an endpoint event, but reject direct Streamable HTTP POSTs to /sse. One observed failure was a 403 api-key not found response from the wrong POST path even though the correct SSE flow does not require an API key.
Verification
Greptile Summary
This PR fixes legacy MCP SSE server compatibility by replacing a URL-path heuristic (
/ssesuffix detection) with an explicittransport: "sse"config field, propagating custom headers to both the initial SSEGETand the subsequentPOST, and switching JSON-RPC request IDs fromu64toStringwhile accepting numeric echoes via the newresponse_id_matcheshelper.SseTransport::connectnow receivesheadersdirectly and applies them via the refactoredapply_safe_custom_headershelper; the connection path branches onis_legacy_sse_transportinstead of guessing from the URL path.next_id()returns aString;response_id_matchesaccepts either a string or unsigned-integer echo from the server, preserving interoperability with older MCP servers.--transportflag is added tomcp add, and/mcp add sse <name> <url>is split into its own match arm in the TUI command parser, with corresponding tests and template updates.Confidence Score: 5/5
Safe to merge — transport routing, header propagation, and ID-compatibility changes are all well-scoped and backed by new regression tests.
All changed code paths are exercised by targeted new tests. The only findings are stale help-text strings that do not affect runtime behavior.
crates/tui/src/commands/mcp.rs — three user-facing error/usage strings were not updated to mention the new
ssesubcommand.Important Files Changed
transportfield toMcpServerConfig, refactors header application intoapply_safe_custom_headers, addsheaderspropagation toSseTransport, changes JSON-RPC IDs to strings, and addsresponse_id_matchesfor backward-compatible numeric echoes. Logic looks correct and well-tested.--transportCLI flag with inline validation; propagates the field through template generation and config insertion. Transport validation is duplicated inline rather than reusingvalidate_mcp_transport, but both paths produce identical errors.transport: Option<String>toMcpUiAction::AddHttpstruct field — a straightforward, mechanical change.add_server_configcall sites to pass the newtransportparameter; no logic changes beyond wiring the field through.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[McpConnection::connect] --> B{is_legacy_sse_transport?} B -- yes --> C[SseTransport::connect\nGET /sse + custom headers] B -- no --> D[HttpTransport::new\nStreamable HTTP] C --> E[run_sse_loop\napply_safe_custom_headers on GET] E --> F[Receive endpoint event] F --> G[SseTransport::send\napply_safe_custom_headers on POST] D --> H[try_establish_session GET] H --> I[StreamableHttpTransport::send\nPOST with headers + session-id] J[next_id to String] --> K[JSON-RPC request with string id] K --> L[response_id_matches\naccept string OR u64 echo]Reviews (4): Last reviewed commit: "Improve MCP SSE error diagnostics" | Re-trigger Greptile