diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt index fea1de19..9968ceb1 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt @@ -17,7 +17,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.McpJson -import io.modelcontextprotocol.kotlin.sdk.types.RequestId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest @@ -50,7 +49,7 @@ class StreamableHttpClientTransportTest { @Test fun testSendJsonRpcMessage() = runTest { val message = JSONRPCRequest( - id = RequestId.StringId("test-id"), + id = "test-id", method = "test", params = buildJsonObject { }, ) @@ -78,7 +77,7 @@ class StreamableHttpClientTransportTest { @Test fun testStoreSessionId() = runTest { val initMessage = JSONRPCRequest( - id = RequestId.StringId("test-id"), + id = "test-id", method = "initialize", params = buildJsonObject { put( diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 28870834..8694e2d5 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -2118,8 +2118,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification$ public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest$Companion; + public fun (JLjava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (JLjava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;)V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Lkotlinx/serialization/json/JsonElement; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt index 66229001..8a580eb0 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -8,15 +8,14 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement -import kotlin.concurrent.atomics.AtomicLong import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.concurrent.atomics.incrementAndFetch import kotlin.jvm.JvmInline +import kotlin.jvm.JvmOverloads +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid public const val JSONRPC_VERSION: String = "2.0" -private val REQUEST_MESSAGE_ID: AtomicLong = AtomicLong(0L) - public fun RequestId(value: String): RequestId = RequestId.StringId(value) public fun RequestId(value: Long): RequestId = RequestId.NumberId(value) @@ -94,23 +93,50 @@ public sealed interface JSONRPCMessage { * A request that expects a response. * * Requests are identified by a unique [id] and specify a [method] to invoke. - * The server or client (depending on direction) must respond with either a + * The server or client (depending on a direction) must respond with either a * [JSONRPCResponse] or [JSONRPCError] that has the same [id]. * * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property id A unique identifier for this request. The response will include the same ID. - * Can be a string or number. + * Can be a string or number. UUID string is used by default. * @property method The name of the method to invoke (e.g., "tools/list", "resources/read"). * @property params Optional parameters for the method. Structure depends on the specific method. */ @Serializable -public data class JSONRPCRequest( - val id: RequestId = RequestId(REQUEST_MESSAGE_ID.incrementAndFetch()), +@OptIn(ExperimentalUuidApi::class) +public data class JSONRPCRequest @JvmOverloads public constructor( + val id: RequestId = RequestId(Uuid.random().toHexString()), val method: String, val params: JsonElement? = null, ) : JSONRPCMessage { @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION + + /** + * Secondary constructor for creating a `JSONRPCRequest` using a string-based ID. + * + * @param id The string ID for the request. + * @param method The method name for the request. + * @param params The parameters for the request as a JSON element. Defaults to `null`. + */ + public constructor( + id: String, + method: String, + params: JsonElement? = null, + ) : this(id = RequestId.StringId(id), method = method, params = params) + + /** + * Constructs a JSON-RPC request using a numerical ID, method name, and optional parameters. + * + * @param id The numerical ID for the request. + * @param method The method name for the request. + * @param params The parameters for the request as a JSON element. Defaults to `null`. + */ + public constructor( + id: Long, + method: String, + params: JsonElement? = null, + ) : this(id = RequestId.NumberId(id), method = method, params = params) } // ============================================================================ @@ -207,6 +233,7 @@ public data class RPCError(val code: Int, val message: String, val data: JsonEle public const val REQUEST_TIMEOUT: Int = -32001 // Standard JSON-RPC 2.0 error codes + /** Invalid JSON was received */ public const val PARSE_ERROR: Int = -32700 diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt index 9b970322..d8d20583 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt @@ -1,6 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeSameInstanceAs import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int @@ -398,4 +400,34 @@ class JsonRpcTest { } """.trimIndent() } + + @Test + fun `should create JSONRPCRequest with string ID`() { + val params = buildJsonObject { + put("foo", "bar") + } + val request = JSONRPCRequest( + id = "req-42", + method = "notifications/log", + params = params, + ) + request.id shouldBe RequestId("req-42") + request.method shouldBe "notifications/log" + request.params shouldBeSameInstanceAs params + } + + @Test + fun `should create JSONRPCRequest with numeric ID`() { + val params = buildJsonObject { + put("foo", "bar") + } + val request = JSONRPCRequest( + id = 42, + method = "notifications/log", + params = params, + ) + request.id shouldBe RequestId(42) + request.method shouldBe "notifications/log" + request.params shouldBeSameInstanceAs params + } }