Skip to content

fix: add SSE heartbeats and defer trace completion to prevent deadlock on MCP SSE streaming#3212

Merged
akshaydeo merged 3 commits intomainfrom
05-04-fix_mcp_sse_hang_fixes
May 6, 2026
Merged

fix: add SSE heartbeats and defer trace completion to prevent deadlock on MCP SSE streaming#3212
akshaydeo merged 3 commits intomainfrom
05-04-fix_mcp_sse_hang_fixes

Conversation

@Pratham-Mishra04
Copy link
Copy Markdown
Collaborator

Summary

Fixes a deadlock and connection reliability issue in the MCP server's SSE handler. Without this fix, post-hook middleware (transport-plugin and tracing) would attempt to materialize the SSE response body during cleanup, blocking indefinitely waiting for an EOF that never arrives while the streaming goroutine is still running. Additionally, idle SSE connections had no heartbeat mechanism, making it impossible to detect client disconnects since fasthttp never cancels the bifrost context on its own.

Changes

  • Sets BifrostContextKeyDeferTraceCompletion on the request context before streaming begins, signaling to transport-plugin and tracing middlewares to skip eager body materialization that would deadlock on the open SSE stream.
  • Adds X-Accel-Buffering: no response header to prevent nginx and similar reverse proxies from buffering SSE events.
  • Replaces the bare <-(*bifrostCtx).Done() wait with a 15-second ticker that sends SSE comment heartbeats (: ping\n\n). This keeps idle connections alive through proxies and detects client disconnects via reader.Send() returning false.
  • Guards the initial SSE init message send with a reader.Send() return check so a disconnected client causes an early exit rather than continuing the goroutine.

Type of change

  • Bug fix

Affected areas

  • Transports (HTTP)

How to test

Connect an SSE client to the MCP server endpoint and verify:

  1. Events are received without buffering through a reverse proxy.
  2. Disconnecting the client causes the server-side goroutine to exit cleanly within ~15 seconds (next heartbeat tick).
  3. Tracing and transport-plugin middleware complete without hanging after the SSE stream ends.
go test ./...

Breaking changes

  • No

Related issues

Security considerations

None.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Confidence Score: 5/5

Safe to merge — the fix correctly mirrors the deferred-completion pattern already proven in inference.go, and the defer ordering (runCompleter → cancel → reader.Done → traceCompleter) ensures post-hooks and spans are always finalised cleanly.

All three concerns raised in the previous review round (leaked trace span, magic number heartbeat constant, cancel-before-completer ordering) are fully addressed. The goroutine's cleanup sequence now matches inference.go. The 100ms atomic poll for the completer slot is a bounded busy-wait; in the absence of a TransportInterceptorMiddleware the slot stays nil and the wait fires on every disconnect, but this is a cosmetic latency concern rather than a correctness problem. No race conditions were found: completerSlot escapes to the heap, traceCompleter is captured before goroutine launch (tracing middleware sets it before calling next(ctx)), and ctx is not accessed inside the goroutine.

No files require special attention.

Important Files Changed

Filename Overview
transports/bifrost-http/handlers/mcpserver.go Adds deferred trace completion, transport post-hook completer slot, SSE heartbeat loop, and X-Accel-Buffering header to fix deadlock and connection reliability issues; structure mirrors inference.go and the implementation looks sound with one minor liveness concern around the 100ms completer poll

Reviews (8): Last reviewed commit: "fix: mcp sse hang fixes" | Re-trigger Greptile

Comment thread transports/bifrost-http/handlers/mcpserver.go
Comment thread transports/bifrost-http/handlers/mcpserver.go Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@Pratham-Mishra04 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 21 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ab0eaa7b-782e-44f7-ab6a-fbe51734dbdf

📥 Commits

Reviewing files that changed from the base of the PR and between e2bc0b5 and 83ff8a4.

📒 Files selected for processing (1)
  • transports/bifrost-http/handlers/mcpserver.go
📝 Walkthrough

Walkthrough

Signals streaming context, exposes an atomic transport post-hook completer slot, defers trace completion, sets X-Accel-Buffering: no, verifies initial SSE frame delivery, emits 15s SSE comment heartbeats to detect disconnects, and on stop runs the transport completer to collect plugin logs.

Changes

SSE Streaming Robustness & Tracing Coordination

Layer / File(s) Summary
Constants / Config
transports/bifrost-http/handlers/mcpserver.go
Adds const sseHeartbeatInterval = 15 * time.Second.
Imports
transports/bifrost-http/handlers/mcpserver.go
Adds sync/atomic for atomic.Value usage.
Context Decoration
transports/bifrost-http/handlers/mcpserver.go
Sets schemas.BifrostContextKeyDeferTraceCompletion, stores an atomic.Value under schemas.BifrostContextKeyTransportPostHookCompleter, and reads schemas.BifrostContextKeyTraceCompleter from request context.
Response Headers
transports/bifrost-http/handlers/mcpserver.go
Adds X-Accel-Buffering: no header in SSE responses.
Initial Frame Delivery
transports/bifrost-http/handlers/mcpserver.go
Sends initial connection/opened SSE frame and aborts streaming if reader.Send(buf) fails.
Heartbeat & Streaming Loop
transports/bifrost-http/handlers/mcpserver.go
Adds active loop with ticker sending SSE comment heartbeats (: ping\n\n) every 15s; exits when send fails or bifrost context is done.
Shutdown / Completer Invocation
transports/bifrost-http/handlers/mcpserver.go
Adds runCompleter closure that waits briefly (~100ms) for a transport post-hook completer in the atomic slot, invokes it once to collect transport/plugin logs, defers cancel() and reader.Done(), and calls the trace completer with captured logs.
Manifests
go.mod
Manifest touched; no tests changed in this diff.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant SSEHandler as Bifrost HTTP handler
participant TransportPostHook as Transport Post-hook (atomic.Value)
participant TraceCompleter as Trace completer (ctx.UserValue)

Client->>SSEHandler: Open SSE connection
SSEHandler->>Client: Send "connection/opened" SSE frame
alt send fails
  SSEHandler-->>Client: Abort streaming / exit
else send succeeds
  loop every 15s
    SSEHandler->>Client: ": ping" SSE comment heartbeat
  end
  alt client disconnect or bifrostCtx.Done
    SSEHandler->>TransportPostHook: wait ~100ms for completer
    TransportPostHook-->>SSEHandler: provide completer/logs (if set)
    SSEHandler->>TraceCompleter: invoke with collected logs
    SSEHandler-->>Client: cleanup (cancel, reader.Done)
  end
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I’m a rabbit by the wire, I ping and softly hum,
I wait a beat for post-hooks, then gather what they’ve done.
I tuck the trace in ribbons, append the plugin logs,
Then bow and hop away as quiet goes the logs. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: adding SSE heartbeats and deferring trace completion to fix a deadlock in MCP SSE streaming.
Description check ✅ Passed The description comprehensively covers the problem, changes, testing approach, and affected areas following the template structure with all critical sections completed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 05-04-fix_mcp_sse_hang_fixes

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@transports/bifrost-http/handlers/mcpserver.go`:
- Around line 150-155: The SSE handler sets
BifrostContextKeyDeferTraceCompletion but does not seed the deferred post-hook
completer, so middlewares.go skips snapshot/completion and pooled objects leak;
before returning the stream in the handler in mcpserver.go, create and
SetUserValue for BifrostContextKeyTransportPostHookCompleter with the same
completer type used by other streaming handlers, and ensure the stream teardown
path drains/invokes that completer (mirror the logic used in the other streaming
handlers) so the deferred transport/tracing path runs and pooled objects are
released.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eab98ee8-e58b-4652-981f-71a6cf38d843

📥 Commits

Reviewing files that changed from the base of the PR and between 09c4f02 and 08b9a17.

📒 Files selected for processing (1)
  • transports/bifrost-http/handlers/mcpserver.go

Comment thread transports/bifrost-http/handlers/mcpserver.go
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-feat_add_refresh_token_expiry_reauth_flow_for_per_user_mcp_oauth branch from 09c4f02 to bc19ce7 Compare May 5, 2026 12:20
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-fix_mcp_sse_hang_fixes branch from 08b9a17 to 0132257 Compare May 5, 2026 12:20
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 5, 2026
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-fix_mcp_sse_hang_fixes branch from 0132257 to e3d6e25 Compare May 5, 2026 15:29
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-feat_add_refresh_token_expiry_reauth_flow_for_per_user_mcp_oauth branch 2 times, most recently from c092d13 to e34323a Compare May 6, 2026 07:32
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-fix_mcp_sse_hang_fixes branch from e3d6e25 to 3dcc042 Compare May 6, 2026 07:32
Comment thread transports/bifrost-http/handlers/mcpserver.go
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-fix_mcp_sse_hang_fixes branch from 3dcc042 to e2bc0b5 Compare May 6, 2026 07:53
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-feat_add_refresh_token_expiry_reauth_flow_for_per_user_mcp_oauth branch from e34323a to 553a797 Compare May 6, 2026 07:53
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
transports/bifrost-http/handlers/mcpserver.go (1)

71-75: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate tool filter registration.

The same WithToolFilter call is registered twice on globalMCPServer with identical comments. This appears to be an accidental duplication that could cause the filter to run twice per request.

🔧 Proposed fix
 	// Register per-request tool filter so x-bf-mcp-include-clients and x-bf-mcp-include-tools are respected on tools/list
 	server.WithToolFilter(handler.makeIncludeClientsFilter())(handler.globalMCPServer)
-
-	// Register per-request tool filter so x-bf-mcp-include-clients and x-bf-mcp-include-tools are respected on tools/list
-	server.WithToolFilter(handler.makeIncludeClientsFilter())(handler.globalMCPServer)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transports/bifrost-http/handlers/mcpserver.go` around lines 71 - 75, The
WithToolFilter registration for handler.makeIncludeClientsFilter() is duplicated
on handler.globalMCPServer; remove the redundant call so the filter is only
registered once (keep a single
server.WithToolFilter(handler.makeIncludeClientsFilter())(handler.globalMCPServer)
and delete the duplicate) to prevent the filter executing twice per request.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@transports/bifrost-http/handlers/mcpserver.go`:
- Around line 71-75: The WithToolFilter registration for
handler.makeIncludeClientsFilter() is duplicated on handler.globalMCPServer;
remove the redundant call so the filter is only registered once (keep a single
server.WithToolFilter(handler.makeIncludeClientsFilter())(handler.globalMCPServer)
and delete the duplicate) to prevent the filter executing twice per request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21380091-38fb-49a8-b202-2559e1566838

📥 Commits

Reviewing files that changed from the base of the PR and between 3dcc042 and e2bc0b5.

📒 Files selected for processing (1)
  • transports/bifrost-http/handlers/mcpserver.go

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-fix_mcp_sse_hang_fixes branch from e2bc0b5 to 83ff8a4 Compare May 6, 2026 08:11
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 05-04-feat_add_refresh_token_expiry_reauth_flow_for_per_user_mcp_oauth branch from 553a797 to 825ea3b Compare May 6, 2026 08:11
Copy link
Copy Markdown
Contributor

akshaydeo commented May 6, 2026

Merge activity

  • May 6, 8:41 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • May 6, 8:43 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo akshaydeo changed the base branch from 05-04-feat_add_refresh_token_expiry_reauth_flow_for_per_user_mcp_oauth to graphite-base/3212 May 6, 2026 08:42
@akshaydeo akshaydeo changed the base branch from graphite-base/3212 to main May 6, 2026 08:42
@akshaydeo akshaydeo dismissed coderabbitai[bot]’s stale review May 6, 2026 08:42

The base branch was changed.

@akshaydeo akshaydeo requested a review from a team as a code owner May 6, 2026 08:42
@akshaydeo akshaydeo merged commit beff6d0 into main May 6, 2026
11 of 13 checks passed
@akshaydeo akshaydeo deleted the 05-04-fix_mcp_sse_hang_fixes branch May 6, 2026 08:43
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.

[Bug]: Authenticated GET /mcp SSE hangs without response headers

3 participants