Skip to content

feat: SSE heartbeat#761

Open
UnscientificJsZhai wants to merge 2 commits into
modelcontextprotocol:mainfrom
UnscientificJsZhai:feature/sse_heartbeat
Open

feat: SSE heartbeat#761
UnscientificJsZhai wants to merge 2 commits into
modelcontextprotocol:mainfrom
UnscientificJsZhai:feature/sse_heartbeat

Conversation

@UnscientificJsZhai
Copy link
Copy Markdown

Summary

Adds optional SSE heartbeat configuration for streamable HTTP MCP server connections.

This lets Application.mcpStreamableHttp callers pass Ktor's Heartbeat configuration for SSE streams, including heartbeat period and event payload. Heartbeats remain disabled by default, preserving the existing stream behavior unless the new option is explicitly provided.

Motivation and Context

Some MCP clients need heartbeat messages on SSE connections to keep the connection alive. Without those heartbeats, the client may proactively disconnect from the MCP server. MCP server developers need a way to configure an SSE heartbeat mechanism so they can avoid unexpected client disconnects.

In my case, my MCP server was consistently disconnected by Gemini CLI. I worked around the issue with some fallback approaches, but I believe the best implementation is to add heartbeat support by using Ktor's built-in API.

My solution:

    embeddedServer(/* ... */) {
        /* ... */
        launch {
            while (true) {
                delay(30.seconds)
                mcpServer?.sessions?.forEach { (_, session) ->
                    try {
                        session.transport?.send(
                            JSONRPCNotification(method = Method.Custom("heartbeat").value, params = null)
                        )
                    } catch (e: Exception) {
                        logger.warn("Sending heartbeat error", e)
                    }
                }
            }
        }
    }

Implementation Notes

  • Added an optional sseHeartbeatConfig: (Heartbeat.() -> Unit)? = null parameter to Application.mcpStreamableHttp.
  • Stored the heartbeat configuration in StreamableHttpServerTransport.Configuration.
  • Applied the configuration inside the Ktor sse { ... } block before handling the existing streamable HTTP transport request.
  • Updated the server API dump for the new public API surface.

How Has This Been Tested?

Added StreamableHttpHeartbeatTest covering:

  • Configured GET SSE streams apply the provided heartbeat configuration.
  • GET SSE streams do not send default heartbeats when sseHeartbeatConfig is omitted.

Verified locally with:

./gradlew :kotlin-sdk-server:jvmTest --tests "io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpHeartbeatTest"

Result: BUILD SUCCESSFUL.

Breaking Changes

No source-level breaking changes are expected. The new heartbeat option is nullable and defaults to null, so existing mcpStreamableHttp calls continue to use the previous behavior with no heartbeat.

This PR does update the public API surface by adding an optional parameter and configuration property.

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

Additional context

Heartbeat behavior is delegated to Ktor's SSE heartbeat support, so applications can use the same configuration semantics they would use in native Ktor SSE routes.

Related issue: SSE needs a heartbeat #344

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.

1 participant