Skip to content

Commit 0a3b4bd

Browse files
authored
feat: add SSE reconnection with retry support (#596)
Add configurable SSE reconnection with exponential backoff and server-driven retry delays to `StreamableHttpClientTransport` closes #590 closes #420 ## How Has This Been Tested? New unit tests and pass conformance test ## Breaking Changes old constructors are Deprecated `close` no longer calls `terminateSession` ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed
1 parent 4892b7c commit 0a3b4bd

10 files changed

Lines changed: 531 additions & 64 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ subprojects {
2424
apply(plugin = "org.jlleitschuh.gradle.ktlint")
2525
apply(plugin = "org.jetbrains.kotlinx.kover")
2626

27-
if (name != "conformance-test") {
27+
if (name != "conformance-test" && name != "docs") {
2828
apply(plugin = "dev.detekt")
2929

3030
detekt {

conformance-test/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Tests the conformance server against all server scenarios:
110110
111111
## Known SDK Limitations
112112

113-
9 scenarios are expected to fail due to current SDK limitations (tracked in [
113+
8 scenarios are expected to fail due to current SDK limitations (tracked in [
114114
`conformance-baseline.yml`](conformance-baseline.yml).
115115

116116
| Scenario | Suite | Root Cause |
@@ -123,6 +123,5 @@ Tests the conformance server against all server scenarios:
123123
| `elicitation-sep1330-enums` | server | *(same as above)* |
124124
| `resources-templates-read` | server | SDK does not implement `addResourceTemplate()` with URI pattern matching; resources are looked up by exact URI |
125125
| `elicitation-sep1034-client-defaults` | client | SDK does not fill in `default` values from the elicitation request schema before sending the response |
126-
| `sse-retry` | client | Transport does not respect the SSE `retry` field timing or send `Last-Event-ID` on reconnection |
127126

128127
These failures reveal SDK gaps and are intentionally not fixed in this module.

conformance-test/conformance-baseline.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,3 @@ server:
1111

1212
client:
1313
- elicitation-sep1034-client-defaults
14-
- sse-retry

kotlin-sdk-client/api/kotlin-sdk-client.api

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt {
6262
public static synthetic fun mcpSseTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport;
6363
}
6464

65+
public final class io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions {
66+
public synthetic fun <init> (JJDIILkotlin/jvm/internal/DefaultConstructorMarker;)V
67+
public synthetic fun <init> (JJDILkotlin/jvm/internal/DefaultConstructorMarker;)V
68+
public fun equals (Ljava/lang/Object;)Z
69+
public final fun getInitialReconnectionDelay-UwyO8pc ()J
70+
public final fun getMaxReconnectionDelay-UwyO8pc ()J
71+
public final fun getMaxRetries ()I
72+
public final fun getReconnectionDelayMultiplier ()D
73+
public fun hashCode ()I
74+
public fun toString ()Ljava/lang/String;
75+
}
76+
6577
public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport {
6678
public synthetic fun <init> (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
6779
public synthetic fun <init> (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -88,6 +100,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTranspor
88100
}
89101

90102
public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport {
103+
public fun <init> (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;)V
104+
public synthetic fun <init> (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
91105
public synthetic fun <init> (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
92106
public synthetic fun <init> (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
93107
public final fun getProtocolVersion ()Ljava/lang/String;
@@ -106,8 +120,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpError
106120
}
107121

108122
public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensionsKt {
123+
public static final fun mcpStreamableHttp (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
124+
public static synthetic fun mcpStreamableHttp$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
109125
public static final fun mcpStreamableHttp-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
110126
public static synthetic fun mcpStreamableHttp-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
127+
public static final fun mcpStreamableHttpTransport (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;
128+
public static synthetic fun mcpStreamableHttpTransport$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;
111129
public static final fun mcpStreamableHttpTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;
112130
public static synthetic fun mcpStreamableHttpTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;
113131
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.modelcontextprotocol.kotlin.sdk.client
2+
3+
import kotlin.time.Duration
4+
import kotlin.time.Duration.Companion.seconds
5+
6+
/**
7+
* Options for controlling SSE reconnection behavior.
8+
*
9+
* @property initialReconnectionDelay The initial delay before the first reconnection attempt.
10+
* @property maxReconnectionDelay The maximum delay between reconnection attempts.
11+
* @property reconnectionDelayMultiplier The factor by which the delay grows on each attempt.
12+
* @property maxRetries The maximum number of reconnection attempts per disconnect.
13+
*/
14+
public class ReconnectionOptions(
15+
public val initialReconnectionDelay: Duration = 1.seconds,
16+
public val maxReconnectionDelay: Duration = 30.seconds,
17+
public val reconnectionDelayMultiplier: Double = 1.5,
18+
public val maxRetries: Int = 2,
19+
) {
20+
override fun equals(other: Any?): Boolean {
21+
if (this === other) return true
22+
if (other == null || this::class != other::class) return false
23+
24+
other as ReconnectionOptions
25+
26+
if (reconnectionDelayMultiplier != other.reconnectionDelayMultiplier) return false
27+
if (maxRetries != other.maxRetries) return false
28+
if (initialReconnectionDelay != other.initialReconnectionDelay) return false
29+
if (maxReconnectionDelay != other.maxReconnectionDelay) return false
30+
31+
return true
32+
}
33+
34+
override fun hashCode(): Int {
35+
var result = reconnectionDelayMultiplier.hashCode()
36+
result = 31 * result + maxRetries
37+
result = 31 * result + initialReconnectionDelay.hashCode()
38+
result = 31 * result + maxReconnectionDelay.hashCode()
39+
return result
40+
}
41+
42+
override fun toString(): String =
43+
"ReconnectionOptions(initialReconnectionDelay=$initialReconnectionDelay, maxReconnectionDelay=$maxReconnectionDelay, reconnectionDelayMultiplier=$reconnectionDelayMultiplier, maxRetries=$maxRetries)"
44+
}

0 commit comments

Comments
 (0)