From f649ff0a7e3d258329d2dcb9dc62a2b05224cc39 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:42:12 +0200 Subject: [PATCH 1/4] feat: introduce Configuration class for StreamableHttpServerTransport Replace the six-parameter flat constructor of StreamableHttpServerTransport with a typed Configuration class. This improves API ergonomics and provides a stable extension point for future options without further breaking the constructor signature. Changes: - Add `Configuration` as a public class nested directly on `StreamableHttpServerTransport`, with `enableJsonResponse` as the first parameter (most commonly set) - Change the primary constructor to accept `Configuration` - Rename `retryIntervalMillis: Long?` to `retryInterval: Duration?` in Configuration, aligning with Kotlin's type-safe time API - Deprecate the old flat constructor with a compatibility bridge - Update KotlinTestBase integration test to use the new constructor --- .../kotlin/AbstractResourceIntegrationTest.kt | 2 - .../sdk/integration/kotlin/KotlinTestBase.kt | 4 +- kotlin-sdk-server/api/kotlin-sdk-server.api | 18 +++ .../kotlin/sdk/server/KtorServer.kt | 24 ++-- .../server/StreamableHttpServerTransport.kt | 136 +++++++++++++----- 5 files changed, 133 insertions(+), 51 deletions(-) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt index fc0881036..40b955204 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.concurrent.atomic.AtomicBoolean -import kotlin.test.Ignore import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -165,7 +164,6 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { assertEquals(testResourceContent, content.text, "Resource content should match") } - @Ignore("Blocked by https://github.com/modelcontextprotocol/kotlin-sdk/issues/249") @Test fun testSubscribeAndUnsubscribe() { runBlocking(Dispatchers.IO) { diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt index f8e8740e2..2f8b3872f 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt @@ -148,7 +148,9 @@ abstract class KotlinTestBase { // Create StreamableHTTP server transport // Using JSON response mode for simpler testing (no SSE session required) val transport = StreamableHttpServerTransport( - enableJsonResponse = true, // Use JSON response mode for testing + StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, // Use JSON response mode for testing + ), ) // Use stateless mode to skip session validation for simpler testing transport.setSessionIdGenerator(null) diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 15bbe5ee1..d6f55c174 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -195,6 +195,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTranspor public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public static final field STANDALONE_SSE_STREAM_ID Ljava/lang/String; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;)V public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -211,6 +213,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration$Companion; + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAllowedHosts ()Ljava/util/List; + public final fun getAllowedOrigins ()Ljava/util/List; + public final fun getEnableDnsRebindingProtection ()Z + public final fun getEnableJsonResponse ()Z + public final fun getEventStore ()Lio/modelcontextprotocol/kotlin/sdk/server/EventStore; + public final fun getRetryInterval-FghU774 ()Lkotlin/time/Duration; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration$Companion { + public final fun getDefault ()Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration; +} + public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index 5a750a5d6..d3cbf4019 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -225,11 +225,13 @@ private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( block: RoutingContext.() -> Server, ) { val transport = StreamableHttpServerTransport( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, + StreamableHttpServerTransport.Configuration( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + enableJsonResponse = true, + ), ).also { it.setSessionIdGenerator(null) } logger.info { "New stateless StreamableHttp connection established without sessionId" } @@ -305,11 +307,13 @@ private suspend fun RoutingContext.streamableTransport( } val transport = StreamableHttpServerTransport( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, + StreamableHttpServerTransport.Configuration( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + enableJsonResponse = true, + ), ) transport.setOnSessionInitialized { initializedSessionId -> diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 1894cdc45..744126573 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -36,6 +36,8 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -46,8 +48,8 @@ private const val MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB /** * A holder for an active request call. - * If enableJsonResponse is true, session is null. - * Otherwise, session is not null. + * If [StreamableHttpServerTransport.Configuration.enableJsonResponse] is true, the session is null. + * Otherwise, the session is not null. */ private data class SessionContext(val session: ServerSSESession?, val call: ApplicationCall) @@ -66,32 +68,86 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl * - No Session ID is included in any responses * - No session validation is performed * - * @param enableJsonResponse If true, the server will return JSON responses instead of starting an SSE stream. - * This can be useful for simple request/response scenarios without streaming. - * Default is false (SSE streams are preferred). - * @param enableDnsRebindingProtection Enable DNS rebinding protection - * (requires allowedHosts and/or allowedOrigins to be configured). - * Default is false for backwards compatibility. - * @param allowedHosts List of allowed host header values for DNS rebinding protection. - * If not specified, host validation is disabled. - * @param allowedOrigins List of allowed origin header values for DNS rebinding protection. - * If not specified, origin validation is disabled. - * @param eventStore Event store for resumability support - * If provided, resumability will be enabled, allowing clients to reconnect and resume messages - * @param retryIntervalMillis Retry interval (in milliseconds) advertised via SSE priming events - * to hint the client when to reconnect. Applies only when an [eventStore] is configured. - * Defaults to `null` (no retry hint). + * @param configuration Transport configuration. See [Configuration] for available options. */ @OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) @Suppress("TooManyFunctions") -public class StreamableHttpServerTransport( - private val enableJsonResponse: Boolean = false, - private val enableDnsRebindingProtection: Boolean = false, - private val allowedHosts: List? = null, - private val allowedOrigins: List? = null, - private val eventStore: EventStore? = null, - private val retryIntervalMillis: Long? = null, -) : AbstractTransport() { +public class StreamableHttpServerTransport(private val configuration: Configuration = Configuration.Default) : + AbstractTransport() { + + /** + * Secondary constructor for `StreamableHttpServerTransport` that simplifies initialization by directly taking the + * configurable parameters without requiring a `Configuration` instance. + * + * @param enableJsonResponse Determines whether the server should return JSON responses. + * Defaults to `false`. + * @param enableDnsRebindingProtection Enables DNS rebinding protection. + * Defaults to `false`. + * @param allowedHosts A list of hosts allowed for server communication. + * Defaults to `null`, allowing all hosts. + * @param allowedOrigins A list of allowed origins for CORS (Cross-Origin Resource Sharing). + * Defaults to `null`, allowing all origins. + * @param eventStore The `EventStore` instance for handling resumable events. + * Defaults to `null`, disabling resumability. + * @param retryIntervalMillis Retry interval in milliseconds for event handling or reconnection attempts. + * Defaults to `null`. + */ + @Deprecated( + "Use constructor with Configuration: StreamableHttpServerTransport(Configuration(enableJsonResponse = ...))", + level = DeprecationLevel.WARNING, + ) + public constructor( + enableJsonResponse: Boolean = false, + enableDnsRebindingProtection: Boolean = false, + allowedHosts: List? = null, + allowedOrigins: List? = null, + eventStore: EventStore? = null, + retryIntervalMillis: Long? = null, + ) : this( + Configuration( + enableJsonResponse = enableJsonResponse, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + retryInterval = retryIntervalMillis?.milliseconds, + ), + ) + + /** + * Configuration for managing various aspects of the StreamableHttpServerTransport. + * + * @property enableJsonResponse Determines whether the server should return JSON responses. + * Defaults to `false`. + * + * @property enableDnsRebindingProtection Enables DNS rebinding protection. + * Defaults to `false`. + * + * @property allowedHosts A list of hosts allowed for server communication. + * Defaults to `null`, allowing all hosts. + * + * @property allowedOrigins A list of allowed origins for CORS (Cross-Origin Resource Sharing). + * Defaults to `null`, allowing all origins. + * + * @property eventStore The `EventStore` instance for handling resumable events. + * Defaults to `null`, disabling resumability. + * + * @property retryInterval Retry interval for event handling or reconnection attempts. + * Defaults to `null`. + */ + public class Configuration( + public val enableJsonResponse: Boolean = false, + public val enableDnsRebindingProtection: Boolean = false, + public val allowedHosts: List? = null, + public val allowedOrigins: List? = null, + public val eventStore: EventStore? = null, + public val retryInterval: Duration? = null, + ) { + public companion object { + public val Default: Configuration = Configuration() + } + } + public var sessionId: String? = null private set @@ -177,7 +233,7 @@ public class StreamableHttpServerTransport( ?: error("No connection established for request id $routingRequestId") val activeStream = streamsMapping[streamId] - if (!enableJsonResponse) { + if (!configuration.enableJsonResponse) { activeStream?.let { stream -> emitOnStream(streamId, stream.session, message) } @@ -194,7 +250,7 @@ public class StreamableHttpServerTransport( streamMutex.withLock { if (activeStream == null) error("No connection established for request ID: $routingRequestId") - if (enableJsonResponse) { + if (configuration.enableJsonResponse) { activeStream.call.response.header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) sessionId?.let { activeStream.call.response.header(MCP_SESSION_ID_HEADER, it) } val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } @@ -261,7 +317,7 @@ public class StreamableHttpServerTransport( @Suppress("CyclomaticComplexMethod", "LongMethod", "ReturnCount", "TooGenericExceptionCaught") public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) { try { - if (!enableJsonResponse && session == null) { + if (!configuration.enableJsonResponse && session == null) { error("Server session can't be null for SSE responses") } @@ -328,7 +384,7 @@ public class StreamableHttpServerTransport( } val streamId = Uuid.random().toString() - if (!enableJsonResponse) { + if (!configuration.enableJsonResponse) { call.appendSseHeaders() flushSse(session) // flush headers immediately maybeSendPrimingEvent(streamId, session) @@ -353,7 +409,7 @@ public class StreamableHttpServerTransport( @Suppress("ReturnCount") public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) { - if (enableJsonResponse) { + if (configuration.enableJsonResponse) { call.reject( HttpStatusCode.MethodNotAllowed, RPCError.ErrorCode.CONNECTION_CLOSED, @@ -375,7 +431,7 @@ public class StreamableHttpServerTransport( if (!validateSession(call) || !validateProtocolVersion(call)) return - eventStore?.let { store -> + configuration.eventStore?.let { store -> call.request.header(MCP_RESUMPTION_TOKEN_HEADER)?.let { lastEventId -> replayEvents(store, lastEventId, sseSession) return @@ -413,7 +469,7 @@ public class StreamableHttpServerTransport( */ @Suppress("ReturnCount", "TooGenericExceptionCaught") public suspend fun closeSseStream(requestId: RequestId) { - if (enableJsonResponse) return + if (configuration.enableJsonResponse) return val streamId = requestToStreamMapping[requestId] ?: return val sessionContext = streamsMapping[streamId] ?: return @@ -562,9 +618,9 @@ public class StreamableHttpServerTransport( @Suppress("ReturnCount") private fun validateHeaders(call: ApplicationCall): String? { - if (!enableDnsRebindingProtection) return null + if (!configuration.enableDnsRebindingProtection) return null - allowedHosts?.let { hosts -> + configuration.allowedHosts?.let { hosts -> val hostHeader = call.request.headers[HttpHeaders.Host]?.lowercase() val allowedHostsLowercase = hosts.map { it.lowercase() } @@ -573,7 +629,7 @@ public class StreamableHttpServerTransport( } } - allowedOrigins?.let { origins -> + configuration.allowedOrigins?.let { origins -> val originHeader = call.request.headers[HttpHeaders.Origin]?.lowercase() val allowedOriginsLowercase = origins.map { it.lowercase() } @@ -636,7 +692,7 @@ public class StreamableHttpServerTransport( this?.lowercase()?.contains(mime.toString().lowercase()) == true private suspend fun emitOnStream(streamId: String, session: ServerSSESession?, message: JSONRPCMessage) { - val eventId = eventStore?.storeEvent(streamId, message) + val eventId = configuration.eventStore?.storeEvent(streamId, message) try { session?.send(event = "message", id = eventId, data = McpJson.encodeToString(message)) } catch (_: Exception) { @@ -646,11 +702,15 @@ public class StreamableHttpServerTransport( @Suppress("TooGenericExceptionCaught") private suspend fun maybeSendPrimingEvent(streamId: String, session: ServerSSESession?) { - val store = eventStore ?: return + val store = configuration.eventStore ?: return val sseSession = session ?: return try { val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage) - sseSession.send(id = primingEventId, retry = retryIntervalMillis, data = "") + sseSession.send( + id = primingEventId, + retry = configuration.retryInterval?.inWholeMilliseconds, + data = "", + ) } catch (e: Exception) { _onError(e) } From 0df06f56712dc140c4b015f6f050ae10e5fdd7d5 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:46:20 +0200 Subject: [PATCH 2/4] refactor: remove `Default` companion object from `Configuration` and simplify `StreamableHttpServerTransport` constructor --- kotlin-sdk-server/api/kotlin-sdk-server.api | 7 ------- .../kotlin/sdk/server/StreamableHttpServerTransport.kt | 9 ++------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index d6f55c174..424dc7bb5 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -194,9 +194,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTranspor public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public static final field STANDALONE_SSE_STREAM_ID Ljava/lang/String; - public fun ()V public fun (Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;)V public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -214,7 +212,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe } public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration$Companion; public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getAllowedHosts ()Ljava/util/List; @@ -225,10 +222,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe public final fun getRetryInterval-FghU774 ()Lkotlin/time/Duration; } -public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration$Companion { - public final fun getDefault ()Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration; -} - public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 744126573..e30ece053 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -72,8 +72,7 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl */ @OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) @Suppress("TooManyFunctions") -public class StreamableHttpServerTransport(private val configuration: Configuration = Configuration.Default) : - AbstractTransport() { +public class StreamableHttpServerTransport(private val configuration: Configuration) : AbstractTransport() { /** * Secondary constructor for `StreamableHttpServerTransport` that simplifies initialization by directly taking the @@ -142,11 +141,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat public val allowedOrigins: List? = null, public val eventStore: EventStore? = null, public val retryInterval: Duration? = null, - ) { - public companion object { - public val Default: Configuration = Configuration() - } - } + ) public var sessionId: String? = null private set From bc3c7508c49808c306f43f6179160d20f610545c Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:54:01 +0200 Subject: [PATCH 3/4] refactor: replace flat parameter definitions with `StreamableHttpServerTransport.Configuration` in Application Ktor extensions Simplified `Application.mcp*StreamableHttp` functions by replacing individual parameters with the typed `StreamableHttpServerTransport.Configuration` for improved extensibility and performance. --- kotlin-sdk-server/api/kotlin-sdk-server.api | 8 +-- .../kotlin/sdk/server/KtorServer.kt | 72 ++++++++----------- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 424dc7bb5..1b7198e08 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -38,10 +38,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V public static final fun mcp (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public static final fun mcp (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function1;)V - public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun mcpStatelessStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static final fun mcpStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun mcpStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun mcpStatelessStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun mcpStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun mcpStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V } public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt : io/modelcontextprotocol/kotlin/sdk/server/Feature { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index d3cbf4019..e6b12287e 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -92,6 +92,11 @@ public fun Route.mcp(block: ServerSSESession.() -> Server) { } } +/** + * Configures the application to use Server-Sent Events (SSE) and sets up routing for the provided server logic. + * + * @param block A lambda function that defines the server logic within the context of a [ServerSSESession]. + */ @KtorDsl public fun Application.mcp(block: ServerSSESession.() -> Server) { install(SSE) @@ -101,14 +106,20 @@ public fun Application.mcp(block: ServerSSESession.() -> Server) { } } +/** + * Sets up HTTP endpoints for an application to support MCP streamable interactions + * using the Server-Sent Events (SSE) protocol and other HTTP methods. + * + * @param path The base URL path for the MCP streamable HTTP routes. Defaults to "/mcp". + * @param configuration An instance of `StreamableHttpServerTransport.Configuration` used to configure + * the behavior of the transport layer. + * @param block A lambda with a `RoutingContext` receiver, allowing the user to define server logic + * for handling streamable transport. + */ @KtorDsl -@Suppress("LongParameterList") public fun Application.mcpStreamableHttp( path: String = "/mcp", - enableDnsRebindingProtection: Boolean = false, - allowedHosts: List? = null, - allowedOrigins: List? = null, - eventStore: EventStore? = null, + configuration: StreamableHttpServerTransport.Configuration = StreamableHttpServerTransport.Configuration(), block: RoutingContext.() -> Server, ) { install(SSE) @@ -125,10 +136,7 @@ public fun Application.mcpStreamableHttp( post { val transport = streamableTransport( transportManager = transportManager, - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, + configuration = configuration, block = block, ) ?: return@post @@ -144,14 +152,19 @@ public fun Application.mcpStreamableHttp( } } +/** + * Sets up a stateless and streamable HTTP endpoint within the application using the specified path and configuration. + * This method installs the SSE feature and defines specific routing behavior for HTTP methods. + * + * @param path The URL path where the endpoint will be accessible. Defaults to "/mcp". + * @param configuration The configuration object used to customize the behavior of the streamable HTTP server transport. + * @param block A lambda function that provides the routing context to define the server behavior. + */ @KtorDsl @Suppress("LongParameterList") public fun Application.mcpStatelessStreamableHttp( path: String = "/mcp", - enableDnsRebindingProtection: Boolean = false, - allowedHosts: List? = null, - allowedOrigins: List? = null, - eventStore: EventStore? = null, + configuration: StreamableHttpServerTransport.Configuration = StreamableHttpServerTransport.Configuration(), block: RoutingContext.() -> Server, ) { install(SSE) @@ -160,10 +173,7 @@ public fun Application.mcpStatelessStreamableHttp( route(path) { post { mcpStatelessStreamableHttpEndpoint( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, + configuration = configuration, block = block, ) } @@ -218,20 +228,11 @@ private fun ServerSSESession.mcpSseTransport( } private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( - enableDnsRebindingProtection: Boolean = false, - allowedHosts: List? = null, - allowedOrigins: List? = null, - eventStore: EventStore? = null, + configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ) { val transport = StreamableHttpServerTransport( - StreamableHttpServerTransport.Configuration( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, - ), + configuration, ).also { it.setSessionIdGenerator(null) } logger.info { "New stateless StreamableHttp connection established without sessionId" } @@ -294,10 +295,7 @@ private suspend fun existingStreamableTransport( private suspend fun RoutingContext.streamableTransport( transportManager: TransportManager, - enableDnsRebindingProtection: Boolean, - allowedHosts: List?, - allowedOrigins: List?, - eventStore: EventStore?, + configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ): StreamableHttpServerTransport? { val sessionId = call.request.sessionId() @@ -306,15 +304,7 @@ private suspend fun RoutingContext.streamableTransport( return transport ?: existingStreamableTransport(call, transportManager) } - val transport = StreamableHttpServerTransport( - StreamableHttpServerTransport.Configuration( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, - ), - ) + val transport = StreamableHttpServerTransport(configuration) transport.setOnSessionInitialized { initializedSessionId -> transportManager.addTransport(initializedSessionId, transport) From a682a43c6aaffc553abc878e4ca82e1408b38667 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:30:52 +0200 Subject: [PATCH 4/4] refactor: replace flat parameter definitions with `StreamableHttpServerTransport.Configuration` in Application Ktor extensions Simplified `Application.mcp*StreamableHttp` functions by replacing individual parameters with the typed `StreamableHttpServerTransport.Configuration` with default value for improved extensibility and performance. --- .../io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index e6b12287e..a6533170e 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -228,7 +228,7 @@ private fun ServerSSESession.mcpSseTransport( } private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( - configuration: StreamableHttpServerTransport.Configuration, + configuration: StreamableHttpServerTransport.Configuration = StreamableHttpServerTransport.Configuration(), block: RoutingContext.() -> Server, ) { val transport = StreamableHttpServerTransport(