feat: SSE heartbeat#761
Conversation
ae7b4af to
29365d6
Compare
29365d6 to
e52f4ad
Compare
devcrocod
left a comment
There was a problem hiding this comment.
This PR adds the ktor api to the transport layer, which we would like to avoid since we plan to move away from a tight coupling with ktor.
Please consider using heartbeat only in the ktor dsl
The problem is that we can't access
Maybe I should remove the reference of |
Maybe I'm missing something, but couldn't we just pass an additional parameter and use the heartbeat in serverSSESession? That way, we wouldn't have to expose the ktor api in core. |
I overthought this. I initially thought I needed to extract the mandatory SSE heartbeat parameters (like the interval) into a new configuration class to avoid a direct dependency on Ktor's config. However, since |
e52f4ad to
1f07367
Compare
|
To ensure binary compatibility for the API, I opted to add a function overload to introduce this parameter. @KtorDsl
public fun Application.mcpStreamableHttp(
path: String = "/mcp",
enableDnsRebindingProtection: Boolean = true,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
sseHeartbeatConfig: (Heartbeat.() -> Unit)?,
block: RoutingContext.() -> Server,
) { /* ... */ }I plan to mark the old function as deprecated later and set a default value Please let me know if a breaking change like this to the API would actually be preferable in this scenario: @KtorDsl
public fun Application.mcpStreamableHttp(
path: String = "/mcp",
enableDnsRebindingProtection: Boolean = true,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
sseHeartbeatConfig: (Heartbeat.() -> Unit)? = null,
block: RoutingContext.() -> Server,
) { /* ... */ } |
1f07367 to
f64ba1c
Compare
Summary
Adds optional SSE heartbeat configuration for streamable HTTP MCP server connections.
This lets
Application.mcpStreamableHttpcallers pass Ktor'sHeartbeatconfiguration 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.
Implementation Notes
Application.mcpStreamableHttpto introduce heartbeat parametersseHeartbeatConfig: (Heartbeat.() -> Unit)?.sse { ... }block before handling the existing streamable HTTP transport request.How Has This Been Tested?
Added
StreamableHttpHeartbeatIntegrationTestcovering:sseHeartbeatConfigis omitted.Verified locally with:
./gradlew :integration-test:jvmTest --tests "io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp.StreamableHttpHeartbeatIntegrationTest"Result:
BUILD SUCCESSFUL.Breaking Changes
None.
This change adds a new
Application.mcpStreamableHttpoverload that acceptssseHeartbeatConfig: (Heartbeat.() -> Unit)?while keeping the existing overloadunchanged. Existing callers continue to compile and keep the previous behavior:
SSE heartbeats are not sent unless the new overload is used with an explicit
heartbeat configuration.
Types of changes
Checklist
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