Skip to content
Merged
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
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-
ktor-client-apache5 = { group = "io.ktor", name = "ktor-client-apache5", version.ref = "ktor" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" }
ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse", version.ref = "ktor" }
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
Expand Down
10 changes: 10 additions & 0 deletions kotlin-sdk-core/api/kotlin-sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientResult$Default
}

public final class io/modelcontextprotocol/kotlin/sdk/types/CommonKt {
public static final field DEFAULT_NEGOTIATED_PROTOCOL_VERSION Ljava/lang/String;
public static final field LATEST_PROTOCOL_VERSION Ljava/lang/String;
public static final fun ProgressToken (J)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;
public static final fun ProgressToken (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;
Expand Down Expand Up @@ -2046,6 +2047,15 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializedNotificat
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCEmptyMessage : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage {
public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCEmptyMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getJsonrpc ()Ljava/lang/String;
public fun hashCode ()I
public final fun serializer ()Lkotlinx/serialization/KSerializer;
public fun toString ()Ljava/lang/String;
}

public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCError : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError$Companion;
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RPCError;)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.modelcontextprotocol.kotlin.sdk.types.CancelledNotification
import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams
import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCEmptyMessage
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest
Expand Down Expand Up @@ -249,6 +250,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
is JSONRPCRequest -> onRequest(message)
is JSONRPCNotification -> onNotification(message)
is JSONRPCError -> onResponse(null, message)
is JSONRPCEmptyMessage -> Unit
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.modelcontextprotocol.kotlin.sdk.types

import io.modelcontextprotocol.kotlin.sdk.types.Icon.Theme.Dark
import io.modelcontextprotocol.kotlin.sdk.types.Icon.Theme.Light
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
Expand All @@ -12,6 +10,8 @@ import kotlinx.serialization.json.JsonObject

public const val LATEST_PROTOCOL_VERSION: String = "2025-06-18"

public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-03-26"

public val SUPPORTED_PROTOCOL_VERSIONS: List<String> = listOf(
LATEST_PROTOCOL_VERSION,
"2025-03-26",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public sealed interface JSONRPCMessage {
public val jsonrpc: String
}

@Serializable
public data object JSONRPCEmptyMessage : JSONRPCMessage {
override val jsonrpc: String = JSONRPC_VERSION
}

// ============================================================================
// JSONRPCRequest
// ============================================================================
Expand Down Expand Up @@ -197,7 +202,7 @@ public data class JSONRPCResponse(val id: RequestId, val result: RequestResult =
* @property error Details about the error that occurred, including error code and message.
*/
@Serializable
public data class JSONRPCError(val id: RequestId, val error: RPCError) : JSONRPCMessage {
public data class JSONRPCError(val id: RequestId?, val error: RPCError) : JSONRPCMessage {
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The id field of JSONRPCError is now nullable, but this could be a breaking API change. According to the JSON-RPC 2.0 specification, the id member is required in error responses except when there was an error in detecting the id in the request object (e.g., parse error). Consider documenting when null is appropriate, or ensure this aligns with how the codebase handles error scenarios where the request ID cannot be determined.

Copilot uses AI. Check for mistakes.
@EncodeDefault
override val jsonrpc: String = JSONRPC_VERSION
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ internal object JSONRPCMessagePolymorphicSerializer :
"result" in jsonObject -> JSONRPCResponse.serializer()
"method" in jsonObject && "id" in jsonObject -> JSONRPCRequest.serializer()
"method" in jsonObject -> JSONRPCNotification.serializer()
jsonObject.isEmpty() || jsonObject.keys == setOf("jsonrpc") -> JSONRPCEmptyMessage.serializer()
else -> throw SerializationException("Invalid JSONRPCMessage type: ${jsonObject.keys}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,16 @@ class JsonRpcTest {
request.method shouldBe "notifications/log"
request.params shouldBeSameInstanceAs params
}

@Test
fun `should deserialize JSONRPCEmptyMessage`() {
val json = """
{
"jsonrpc": "2.0"
}
""".trimIndent()

val message = McpJson.decodeFromString<JSONRPCMessage>(json)
message shouldBeSameInstanceAs JSONRPCEmptyMessage
}
}
25 changes: 25 additions & 0 deletions kotlin-sdk-server/api/kotlin-sdk-server.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/EventStore {
public abstract fun getStreamIdForEventId (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
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/types/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/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
Expand Down Expand Up @@ -151,6 +157,25 @@ 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;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;
public final fun closeSseStream (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getSessionId ()Ljava/lang/String;
public final fun handleDeleteRequest (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/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;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/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
7 changes: 7 additions & 0 deletions kotlin-sdk-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ kotlin {

jvmTest {
dependencies {
implementation(libs.ktor.client.logging)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization)
implementation(libs.ktor.server.test.host)
implementation(libs.kotest.assertions.core)
implementation(libs.kotest.assertions.json)
runtimeOnly(libs.slf4j.simple)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.modelcontextprotocol.kotlin.sdk.server

import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage

/**
* Interface for resumability support via event storage
*/
public interface EventStore {
/**
* Stores an event for later retrieval
* @param streamId ID of the stream the event belongs to
* @param message The JSON-RPC message to store
* @returns The generated event ID for the stored event
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The comment style uses @returns instead of the Kotlin standard @return. KDoc convention uses @return (singular) for documenting return values.

Suggested change
* @returns The generated event ID for the stored event
* @return The generated event ID for the stored event

Copilot uses AI. Check for mistakes.
*/
public suspend fun storeEvent(streamId: String, message: JSONRPCMessage): String

/**
* Replays events after the specified event ID
* @param lastEventId The last event ID that was received
* @param sender Function to send events
* @return The stream ID for the replayed events
*/
public suspend fun replayEventsAfter(
lastEventId: String,
sender: suspend (eventId: String, message: JSONRPCMessage) -> Unit,
): String

/**
* Returns the stream ID associated with [eventId], or null if the event is unknown.
* Default implementation is a no-op which disables extra validation during replay.
Comment on lines +29 to +30
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Missing KDoc documentation for the getStreamIdForEventId method. The inline comment on line 30 describes it as a "no-op" default implementation, but there's no actual default implementation provided (it's an interface method). Consider either providing a default implementation or clarifying the documentation to explain that implementers may throw NotImplementedError or UnsupportedOperationException to disable this validation.

Suggested change
* Returns the stream ID associated with [eventId], or null if the event is unknown.
* Default implementation is a no-op which disables extra validation during replay.
* Returns the stream ID associated with the given [eventId], or null if the event is unknown.
*
* Implementers may throw [NotImplementedError] or [UnsupportedOperationException] to disable
* extra validation during replay, as there is no default implementation provided.
*
* @param eventId The event ID to look up.
* @return The stream ID associated with the event, or null if unknown.

Copilot uses AI. Check for mistakes.
*/
public suspend fun getStreamIdForEventId(eventId: String): String?
}
Loading
Loading