Skip to content

fix(client): invoke onClose when SSE session disconnects (#437)#738

Open
jskjw157 wants to merge 1 commit intomodelcontextprotocol:mainfrom
jskjw157:feature/issue-437-sse-close-detection
Open

fix(client): invoke onClose when SSE session disconnects (#437)#738
jskjw157 wants to merge 1 commit intomodelcontextprotocol:mainfrom
jskjw157:feature/issue-437-sse-close-detection

Conversation

@jskjw157
Copy link
Copy Markdown
Contributor

@jskjw157 jskjw157 commented May 1, 2026

Fixes the SSE client transport so that registered onClose callbacks fire when the underlying SSE session ends — whether the server closes the stream gracefully or an error event terminates it.

closes #437

Motivation and Context

SseClientTransport.collectMessages already runs closeResources() in its finally block when session.incoming completes, but it never invoked the onClose callback chain. As a result, the MCP Client (and any user code calling transport.onClose { ... }) had no way to detect that the server-side SSE connection had dropped.

The other client transports already do this correctly:

  • StdioClientTransport calls invokeOnCloseCallback() from its read-loop finally (kotlin-sdk-client/src/.../StdioClientTransport.kt:224).
  • The protected AbstractTransport.invokeOnCloseCallback() helper guards against duplicate firing with an AtomicBoolean, so it is safe to call from both the read-loop finally and the user-initiated close() path.

The fix mirrors that established pattern in one line.

How Has This Been Tested?

Added onClose callback fires when the SSE stream is disconnected by the server to SseClientTransportTest. The test:

  1. Starts the transport against a mock SSE engine.
  2. Closes the SSE byte channel server-side via a new MockSseClientEngine.disconnectSseStream() helper.
  3. Asserts the registered onClose callback fires (using Kotest eventually, since the close propagates on a real-time dispatcher).

I also verified the test fails before the fix and passes after.

./gradlew :kotlin-sdk-client:jvmTest
./gradlew apiCheck ktlintCheck detekt

Breaking Changes

None. invokeOnCloseCallback() is protected on AbstractTransport; this PR only adds a call site inside SseClientTransport. No public API surface changes.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

…tprotocol#437)

The SSE client transport's `collectMessages` coroutine releases resources
when the underlying SSE stream completes, but never invoked the registered
`onClose` callbacks. As a result, code listening for transport closure (e.g.
the MCP `Client`) was not notified when a server closed the SSE connection,
leaving callers unable to detect disconnects.

Mirror the pattern already used by `StdioClientTransport`: after
`closeResources()` in the `finally` block, call the protected
`invokeOnCloseCallback()` helper. The helper is idempotent via an
`AtomicBoolean`, so the user-initiated `close()` path remains safe.
@jskjw157
Copy link
Copy Markdown
Contributor Author

jskjw157 commented May 1, 2026

@devcrocod when you have a moment — small fix for the SSE disconnect-detection bug from #437. The change is one line plus a regression test; the existing StdioClientTransport already does this, so this aligns SSE with the established pattern. Happy to adjust the test setup if you prefer a different approach to simulating the server-side disconnect.

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.

SseClientTransport isn't closed when underlying sseSession is disconnected

1 participant