Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 16 additions & 5 deletions kotlin-sdk-server/api/kotlin-sdk-server.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -194,7 +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 <init> ()V
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;)V
public fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;)V
public synthetic fun <init> (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;
Expand All @@ -211,6 +211,17 @@ 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 synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (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/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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
configuration: StreamableHttpServerTransport.Configuration = StreamableHttpServerTransport.Configuration(),
block: RoutingContext.() -> Server,
) {
install(SSE)
Expand All @@ -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
Expand All @@ -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<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
configuration: StreamableHttpServerTransport.Configuration = StreamableHttpServerTransport.Configuration(),
block: RoutingContext.() -> Server,
) {
install(SSE)
Expand All @@ -160,10 +173,7 @@ public fun Application.mcpStatelessStreamableHttp(
route(path) {
post {
mcpStatelessStreamableHttpEndpoint(
enableDnsRebindingProtection = enableDnsRebindingProtection,
allowedHosts = allowedHosts,
allowedOrigins = allowedOrigins,
eventStore = eventStore,
configuration = configuration,
block = block,
)
}
Expand Down Expand Up @@ -218,18 +228,11 @@ private fun ServerSSESession.mcpSseTransport(
}

private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint(
enableDnsRebindingProtection: Boolean = false,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
configuration: StreamableHttpServerTransport.Configuration = StreamableHttpServerTransport.Configuration(),
block: RoutingContext.() -> Server,
) {
val transport = StreamableHttpServerTransport(
enableDnsRebindingProtection = enableDnsRebindingProtection,
allowedHosts = allowedHosts,
allowedOrigins = allowedOrigins,
eventStore = eventStore,
enableJsonResponse = true,
configuration,
).also { it.setSessionIdGenerator(null) }

logger.info { "New stateless StreamableHttp connection established without sessionId" }
Expand Down Expand Up @@ -292,10 +295,7 @@ private suspend fun existingStreamableTransport(

private suspend fun RoutingContext.streamableTransport(
transportManager: TransportManager,
enableDnsRebindingProtection: Boolean,
allowedHosts: List<String>?,
allowedOrigins: List<String>?,
eventStore: EventStore?,
configuration: StreamableHttpServerTransport.Configuration,
block: RoutingContext.() -> Server,
): StreamableHttpServerTransport? {
val sessionId = call.request.sessionId()
Expand All @@ -304,13 +304,7 @@ private suspend fun RoutingContext.streamableTransport(
return transport ?: existingStreamableTransport(call, transportManager)
}

val transport = StreamableHttpServerTransport(
enableDnsRebindingProtection = enableDnsRebindingProtection,
allowedHosts = allowedHosts,
allowedOrigins = allowedOrigins,
eventStore = eventStore,
enableJsonResponse = true,
)
val transport = StreamableHttpServerTransport(configuration)

transport.setOnSessionInitialized { initializedSessionId ->
transportManager.addTransport(initializedSessionId, transport)
Expand Down
Loading
Loading