Skip to content
Open
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 @@ -256,7 +256,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
JSONRPCResponse(
id = request.id,
error = JSONRPCError(
ErrorCode.Defined.MethodNotFound,
code = ErrorCode.Defined.MethodNotFound,
message = "Server does not support ${request.method}",
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,14 @@ public data class JSONRPCNotification(
*/
@Serializable
public class JSONRPCResponse(
public val id: RequestId,
public val id: RequestId?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public val id: RequestId?,
public val id: RequestId? = null,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the spec, the id is required field

public val jsonrpc: String = JSONRPC_VERSION,
public val result: RequestResult? = null,
public val error: JSONRPCError? = null,
) : JSONRPCMessage {

public fun copy(
id: RequestId = this.id,
id: RequestId? = this.id,
jsonrpc: String = this.jsonrpc,
result: RequestResult? = this.result,
error: JSONRPCError? = this.error,
Expand Down Expand Up @@ -292,8 +292,12 @@ public sealed interface ErrorCode {
* A response to a request that indicates an error occurred.
*/
@Serializable
public data class JSONRPCError(val code: ErrorCode, val message: String, val data: JsonObject = EmptyJsonObject) :
JSONRPCMessage
public data class JSONRPCError(
val id: RequestId? = null,
val code: ErrorCode,
val message: String,
val data: JsonObject = EmptyJsonObject,
) : JSONRPCMessage

/**
* Base interface for notification parameters with optional metadata.
Expand Down
27 changes: 27 additions & 0 deletions kotlin-sdk-server/api/kotlin-sdk-server.api
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt {
public static final field LIB_VERSION Ljava/lang/String;
}

public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/EventStore {
public abstract fun replayEventsAfter (Ljava/lang/String;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun storeEvent (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

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/application/Application;Lkotlin/jvm/functions/Function1;)V
public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function1;)V
public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;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;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;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;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
}

public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt {
Expand Down Expand Up @@ -115,6 +124,24 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTranspor
public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

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> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;)V
public synthetic fun <init> (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getSessionId ()Ljava/lang/String;
public final fun handleDeleteRequest (Lio/ktor/server/sse/ServerSSESession;Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun handleGetRequest (Lio/ktor/server/sse/ServerSSESession;Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun handlePostRequest (Lio/ktor/server/sse/ServerSSESession;Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun handleRequest (Lio/ktor/server/sse/ServerSSESession;Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun setOnSessionClosed (Lkotlin/jvm/functions/Function1;)V
public final fun setOnSessionInitialized (Lkotlin/jvm/functions/Function1;)V
public final fun setSessionIdGenerator (Lkotlin/jvm/functions/Function0;)V
public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt {
public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V
public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.request.header
import io.ktor.server.response.respond
import io.ktor.server.routing.Routing
import io.ktor.server.routing.RoutingContext
Expand All @@ -15,6 +16,7 @@ import io.ktor.server.sse.ServerSSESession
import io.ktor.server.sse.sse
import io.ktor.util.collections.ConcurrentMap
import io.ktor.utils.io.KtorDsl
import io.modelcontextprotocol.kotlin.sdk.ErrorCode

private val logger = KotlinLogging.logger {}

Expand Down Expand Up @@ -64,6 +66,51 @@ public fun Application.mcp(block: ServerSSESession.() -> Server) {
}
}

@KtorDsl
public fun Application.mcpStreamableHttp(
enableDnsRebindingProtection: Boolean = false,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
block: RoutingContext.() -> Server,
) {
val transports = ConcurrentMap<String, StreamableHttpServerTransport>()

routing {
post("/mcp") {
mcpStreamableHttpEndpoint(
transports,
enableDnsRebindingProtection,
allowedHosts,
allowedOrigins,
eventStore,
block,
)
}
}
}

@KtorDsl
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing KDoc

public fun Application.mcpStatelessStreamableHttp(
enableDnsRebindingProtection: Boolean = false,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
block: RoutingContext.() -> Server,
) {
routing {
post("/mcp") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get requests also need to be processed.

mcpStatelessStreamableHttpEndpoint(
enableDnsRebindingProtection,
allowedHosts,
allowedOrigins,
eventStore,
block,
)
}
}
}

private suspend fun ServerSSESession.mcpSseEndpoint(
postEndpoint: String,
transports: ConcurrentMap<String, SseServerTransport>,
Expand Down Expand Up @@ -94,6 +141,88 @@ internal fun ServerSSESession.mcpSseTransport(
return transport
}

private suspend fun RoutingContext.mcpStreamableHttpEndpoint(
transports: ConcurrentMap<String, StreamableHttpServerTransport>,
enableDnsRebindingProtection: Boolean = false,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
eventStore: EventStore? = null,
block: RoutingContext.() -> Server,
) {
val sessionId = this.call.request.header(MCP_SESSION_ID_HEADER)
val transport = if (sessionId != null && transports.containsKey(sessionId)) {
transports[sessionId]!!
} else if (sessionId == null) {
val transport = StreamableHttpServerTransport(
enableDnsRebindingProtection = enableDnsRebindingProtection,
allowedHosts = allowedHosts,
allowedOrigins = allowedOrigins,
eventStore = eventStore,
enableJsonResponse = true,
)

transport.setOnSessionInitialized { sessionId ->
transports[sessionId] = transport

logger.info { "New StreamableHttp connection established and stored with sessionId: $sessionId" }
}

val server = block()
server.onClose {
logger.info { "Server connection closed for sessionId: ${transport.sessionId}" }
}

server.connect(transport)

transport
} else {
null
}

if (transport == null) {
this.call.reject(
HttpStatusCode.BadRequest,
ErrorCode.Unknown(-32000),
"Bad Request: No valid session ID provided",
)
return
}

transport.handleRequest(null, this.call)
logger.debug { "Server connected to transport for sessionId: ${transport.sessionId}" }
}

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

logger.info { "New stateless StreamableHttp connection established without sessionId" }

val server = block()

server.onClose {
logger.info { "Server connection closed without sessionId" }
}

server.connect(transport)

transport.handleRequest(null, this.call)

logger.debug { "Server connected to transport without sessionId" }
}

internal suspend fun RoutingContext.mcpPostEndpoint(transports: ConcurrentMap<String, SseServerTransport>) {
val sessionId: String = call.request.queryParameters["sessionId"]
?: run {
Expand Down
Loading