Skip to content

fix(mcpproxy): skip non-response SSE events during initialize (#2219) - #2267

Merged
nacx merged 3 commits into
envoyproxy:mainfrom
pjdurden:fix/2219-mcp-sse-skip-keepalive
Jul 10, 2026
Merged

fix(mcpproxy): skip non-response SSE events during initialize (#2219)#2267
nacx merged 3 commits into
envoyproxy:mainfrom
pjdurden:fix/2219-mcp-sse-skip-keepalive

Conversation

@pjdurden

Copy link
Copy Markdown
Contributor

Description

Fixes #2219.

The MCP proxy fails initialize with 500 failed to create MCP session to any backend and logs MCP message is not a response: <nil> when a backend's initialize SSE response begins with a non-response event (an empty/keep-alive data: line) before the JSON-RPC result. Some FastMCP backends do this depending on the requested protocolVersion (e.g. firecrawl with 2025-11-25).

Two layers caused it:

  1. sseEventParser.parseEvent (internal/mcpproxy/sse.go) called jsonrpc.DecodeMessage on an empty data: line, which errors, so the whole leading event was returned as a parse error.
  2. The initialize reader loop (internal/mcpproxy/mcpproxy.go) treated that non-EOF error as fatal and broke with rawMsg still nil, then reported it as "not a response".

This change:

  • Skips empty data: lines in the SSE parser (keep-alive/heartbeat events carry no message to decode). Non-empty but malformed data still errors as before.
  • Makes the initialize reader skip non-response events and keep reading until it finds the JSON-RPC *Response, instead of requiring the first event to be the response.

Testing

  • TestSSEEventParser_EmptyDataLineSkipped — a leading event with an empty data line yields an event with no messages and no error; the following event still decodes.
  • TestNewSession_SSEWithLeadingKeepAlive — a backend whose initialize SSE response starts with an empty keep-alive event before the real response now initializes successfully (this test reproduced the MCP message is not a response: <nil> failure before the fix).
go test ./internal/mcpproxy/
go vet ./internal/mcpproxy/
go build ./...

Done with AI assistance; I have reviewed every line and understand the change.

…roxy#2219)

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
@pjdurden
pjdurden requested a review from a team as a code owner June 21, 2026 19:07
Copilot AI review requested due to automatic review settings June 21, 2026 19:07
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Jun 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes MCP proxy session initialization failures when a backend’s initialize SSE stream begins with a non-response keep-alive event (e.g., an empty data: line) before the actual JSON-RPC response, as reported in #2219.

Changes:

  • Skip empty data: lines in the SSE event parser so keep-alive/heartbeat events don’t trigger JSON-RPC decode errors.
  • Update the initialize SSE reader loop to ignore non-response events and keep reading until a JSON-RPC *Response is encountered.
  • Add targeted regression tests covering both the parser behavior and end-to-end session creation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
internal/mcpproxy/sse.go Skips empty SSE data: lines to avoid decode failures on keep-alives.
internal/mcpproxy/sse_test.go Adds a regression test ensuring empty data: lines are skipped without error.
internal/mcpproxy/mcpproxy.go Initialize loop now skips non-response events until it finds a JSON-RPC response.
internal/mcpproxy/mcpproxy_test.go Adds a regression test reproducing the leading keep-alive initialize stream scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/mcpproxy/mcpproxy.go Outdated
Comment on lines 369 to 370
if parseErr != nil {
if errors.Is(parseErr, io.EOF) || strings.Contains(parseErr.Error(), "context deadline exceeded") {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 115b66d. The initialize reader now tracks the terminating read error and, when the SSE stream ends before any JSON-RPC response arrives, returns a dedicated error instead of falling through to the misleading "MCP message is not a response: ": a wrapped read error for non-EOF/deadline cases, and "MCP initialize stream from backend ... ended before a JSON-RPC response was received" for clean EOF/deadline. Added TestInitializeSession_SSEEndsBeforeResponse to cover it.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Jun 21, 2026
…esponse

Address review feedback: the initialize reader broke on EOF/deadline with
rawMsg still nil, producing the misleading "MCP message is not a response:
<nil>". Track the read error and surface a dedicated error instead, and add
a regression test for an initialize stream that closes before any JSON-RPC
response arrives.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
@pjdurden
pjdurden force-pushed the fix/2219-mcp-sse-skip-keepalive branch from 115b66d to e7c7446 Compare June 24, 2026 00:52

@nacx nacx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! LGTM

@nacx
nacx enabled auto-merge (squash) July 10, 2026 10:36
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.87%. Comparing base (4850070) to head (c0dd9b9).

Files with missing lines Patch % Lines
internal/mcpproxy/mcpproxy.go 71.42% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2267   +/-   ##
=======================================
  Coverage   84.86%   84.87%           
=======================================
  Files         144      144           
  Lines       21308    21317    +9     
=======================================
+ Hits        18084    18093    +9     
+ Misses       2139     2138    -1     
- Partials     1085     1086    +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nacx
nacx merged commit f5f83b2 into envoyproxy:main Jul 10, 2026
34 checks passed
DanielChrn pushed a commit to DanielChrn/ai-gateway that referenced this pull request Jul 16, 2026
…roxy#2219) (envoyproxy#2267)

**Description**

Fixes envoyproxy#2219.

The MCP proxy fails `initialize` with `500 failed to create MCP session
to any backend` and logs `MCP message is not a response: <nil>` when a
backend's `initialize` SSE response begins with a non-response event (an
empty/keep-alive `data:` line) before the JSON-RPC result. Some FastMCP
backends do this depending on the requested `protocolVersion` (e.g.
firecrawl with `2025-11-25`).

Two layers caused it:

1. `sseEventParser.parseEvent` (`internal/mcpproxy/sse.go`) called
`jsonrpc.DecodeMessage` on an empty `data:` line, which errors, so the
whole leading event was returned as a parse error.
2. The initialize reader loop (`internal/mcpproxy/mcpproxy.go`) treated
that non-EOF error as fatal and broke with `rawMsg` still nil, then
reported it as "not a response".

This change:

- Skips empty `data:` lines in the SSE parser (keep-alive/heartbeat
events carry no message to decode). Non-empty but malformed data still
errors as before.
- Makes the initialize reader skip non-response events and keep reading
until it finds the JSON-RPC `*Response`, instead of requiring the first
event to be the response.

**Testing**

- `TestSSEEventParser_EmptyDataLineSkipped` — a leading event with an
empty data line yields an event with no messages and no error; the
following event still decodes.
- `TestNewSession_SSEWithLeadingKeepAlive` — a backend whose initialize
SSE response starts with an empty keep-alive event before the real
response now initializes successfully (this test reproduced the `MCP
message is not a response: <nil>` failure before the fix).

```
go test ./internal/mcpproxy/
go vet ./internal/mcpproxy/
go build ./...
```

Done with AI assistance; I have reviewed every line and understand the
change.

---------

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
Co-authored-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Daniel Chernovsky <daniel.chernovsky@doubleverify.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP proxy: initialize 500s when the backend SSE response starts with a non-response event (empty/keep-alive) — "MCP message is not a response: <nil>"

4 participants