Skip to content

Commit 63a50c1

Browse files
authored
Update to Kotlin 2.1.20 and minor refactoring (#78)
* update kotlin version to 2.1.20 * change atomics from atomicfu to kotlin.concurrent * refactor to use the writechannel * cancel job and close channel
1 parent 63288ea commit 63a50c1

File tree

8 files changed

+63
-66
lines changed

8 files changed

+63
-66
lines changed

build.gradle.kts

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ plugins {
1111
alias(libs.plugins.kotlin.serialization)
1212
alias(libs.plugins.dokka)
1313
alias(libs.plugins.jreleaser)
14-
alias(libs.plugins.atomicfu)
1514
`maven-publish`
1615
alias(libs.plugins.kotlinx.binary.compatibility.validator)
1716
}

gradle/libs.versions.toml

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
[versions]
22
# plugins version
3-
kotlin = "2.0.21"
3+
kotlin = "2.1.20"
44
dokka = "2.0.0"
5-
atomicfu = "0.26.1"
65

76
# libraries version
87
serialization = "1.7.3"
@@ -40,5 +39,4 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref
4039
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
4140
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
4241
jreleaser = { id = "org.jreleaser", version.ref = "jreleaser"}
43-
atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }
4442
kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" }

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import io.ktor.http.*
88
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
99
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
1010
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
11-
import kotlinx.atomicfu.AtomicBoolean
12-
import kotlinx.atomicfu.atomic
1311
import kotlinx.coroutines.*
1412
import kotlinx.serialization.encodeToString
13+
import kotlin.concurrent.atomics.AtomicBoolean
14+
import kotlin.concurrent.atomics.ExperimentalAtomicApi
1515
import kotlin.properties.Delegates
1616
import kotlin.time.Duration
1717

@@ -22,6 +22,7 @@ public typealias SSEClientTransport = SseClientTransport
2222
* Client transport for SSE: this will connect to a server using Server-Sent Events for receiving
2323
* messages and make separate POST requests for sending messages.
2424
*/
25+
@OptIn(ExperimentalAtomicApi::class)
2526
public class SseClientTransport(
2627
private val client: HttpClient,
2728
private val urlString: String?,
@@ -32,7 +33,7 @@ public class SseClientTransport(
3233
CoroutineScope(session.coroutineContext + SupervisorJob())
3334
}
3435

35-
private val initialized: AtomicBoolean = atomic(false)
36+
private val initialized: AtomicBoolean = AtomicBoolean(false)
3637
private var session: ClientSSESession by Delegates.notNull()
3738
private val endpoint = CompletableDeferred<String>()
3839

@@ -127,7 +128,7 @@ public class SseClientTransport(
127128
}
128129

129130
override suspend fun close() {
130-
if (!initialized.value) {
131+
if (!initialized.load()) {
131132
error("SSEClientTransport is not initialized!")
132133
}
133134

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt

+6-10
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,12 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
55
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
66
import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer
77
import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage
8-
import kotlinx.atomicfu.AtomicBoolean
9-
import kotlinx.atomicfu.atomic
108
import kotlinx.coroutines.*
119
import kotlinx.coroutines.channels.Channel
1210
import kotlinx.coroutines.channels.consumeEach
13-
import kotlinx.io.Buffer
14-
import kotlinx.io.Sink
15-
import kotlinx.io.Source
16-
import kotlinx.io.buffered
17-
import kotlinx.io.readByteArray
18-
import kotlinx.io.writeString
11+
import kotlinx.io.*
12+
import kotlin.concurrent.atomics.AtomicBoolean
13+
import kotlin.concurrent.atomics.ExperimentalAtomicApi
1914
import kotlin.coroutines.CoroutineContext
2015

2116
/**
@@ -27,6 +22,7 @@ import kotlin.coroutines.CoroutineContext
2722
* @param input The input stream where messages are received.
2823
* @param output The output stream where messages are sent.
2924
*/
25+
@OptIn(ExperimentalAtomicApi::class)
3026
public class StdioClientTransport(
3127
private val input: Source,
3228
private val output: Sink
@@ -37,7 +33,7 @@ public class StdioClientTransport(
3733
CoroutineScope(ioCoroutineContext + SupervisorJob())
3834
}
3935
private var job: Job? = null
40-
private val initialized: AtomicBoolean = atomic(false)
36+
private val initialized: AtomicBoolean = AtomicBoolean(false)
4137
private val sendChannel = Channel<JSONRPCMessage>(Channel.UNLIMITED)
4238
private val readBuffer = ReadBuffer()
4339

@@ -96,7 +92,7 @@ public class StdioClientTransport(
9692
}
9793

9894
override suspend fun send(message: JSONRPCMessage) {
99-
if (!initialized.value) {
95+
if (!initialized.load()) {
10096
error("Transport not started")
10197
}
10298

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import io.ktor.server.sse.*
88
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
99
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
1010
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
11-
import kotlinx.atomicfu.AtomicBoolean
12-
import kotlinx.atomicfu.atomic
1311
import kotlinx.coroutines.job
1412
import kotlinx.serialization.encodeToString
13+
import kotlin.concurrent.atomics.AtomicBoolean
14+
import kotlin.concurrent.atomics.ExperimentalAtomicApi
1515
import kotlin.uuid.ExperimentalUuidApi
1616
import kotlin.uuid.Uuid
1717

@@ -25,11 +25,12 @@ public typealias SSEServerTransport = SseServerTransport
2525
*
2626
* Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`.
2727
*/
28+
@OptIn(ExperimentalAtomicApi::class)
2829
public class SseServerTransport(
2930
private val endpoint: String,
3031
private val session: ServerSSESession,
3132
) : AbstractTransport() {
32-
private val initialized: AtomicBoolean = atomic(false)
33+
private val initialized: AtomicBoolean = AtomicBoolean(false)
3334

3435
@OptIn(ExperimentalUuidApi::class)
3536
public val sessionId: String = Uuid.random().toString()
@@ -63,7 +64,7 @@ public class SseServerTransport(
6364
* This should be called when a POST request is made to send a message to the server.
6465
*/
6566
public suspend fun handlePostMessage(call: ApplicationCall) {
66-
if (!initialized.value) {
67+
if (!initialized.load()) {
6768
val message = "SSE connection not established"
6869
call.respondText(message, status = HttpStatusCode.InternalServerError)
6970
_onError.invoke(IllegalStateException(message))
@@ -112,7 +113,7 @@ public class SseServerTransport(
112113
}
113114

114115
override suspend fun send(message: JSONRPCMessage) {
115-
if (!initialized.value) {
116+
if (!initialized.load()) {
116117
throw error("Not connected")
117118
}
118119

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt

+29-22
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,38 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
55
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
66
import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer
77
import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage
8-
import kotlinx.atomicfu.AtomicBoolean
9-
import kotlinx.atomicfu.atomic
10-
import kotlinx.atomicfu.locks.ReentrantLock
11-
import kotlinx.atomicfu.locks.withLock
128
import kotlinx.coroutines.*
139
import kotlinx.coroutines.channels.Channel
14-
import kotlinx.io.Buffer
15-
import kotlinx.io.Sink
16-
import kotlinx.io.Source
17-
import kotlinx.io.buffered
18-
import kotlinx.io.readByteArray
19-
import kotlinx.io.writeString
10+
import kotlinx.io.*
11+
import kotlin.concurrent.atomics.AtomicBoolean
12+
import kotlin.concurrent.atomics.ExperimentalAtomicApi
2013
import kotlin.coroutines.CoroutineContext
2114

2215
/**
2316
* A server transport that communicates with a client via standard I/O.
2417
*
2518
* Reads from System.in and writes to System.out.
2619
*/
20+
@OptIn(ExperimentalAtomicApi::class)
2721
public class StdioServerTransport(
28-
private val inputStream: Source, //BufferedInputStream = BufferedInputStream(System.`in`),
29-
outputStream: Sink //PrintStream = System.out
22+
private val inputStream: Source,
23+
outputStream: Sink
3024
) : AbstractTransport() {
3125
private val logger = KotlinLogging.logger {}
3226

3327
private val readBuffer = ReadBuffer()
34-
private val initialized: AtomicBoolean = atomic(false)
28+
private val initialized: AtomicBoolean = AtomicBoolean(false)
3529
private var readingJob: Job? = null
30+
private var sendingJob: Job? = null
3631

3732
private val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob()
3833
private val scope = CoroutineScope(coroutineContext)
3934
private val readChannel = Channel<ByteArray>(Channel.UNLIMITED)
35+
private val writeChannel = Channel<JSONRPCMessage>(Channel.UNLIMITED)
4036
private val outputWriter = outputStream.buffered()
41-
private val lock = ReentrantLock()
4237

4338
override suspend fun start() {
44-
if (!initialized.compareAndSet(false, true)) {
39+
if (!initialized.compareAndSet(expectedValue = false, newValue = true)) {
4540
error("StdioServerTransport already started!")
4641
}
4742

@@ -80,6 +75,20 @@ public class StdioServerTransport(
8075
_onError.invoke(e)
8176
}
8277
}
78+
79+
// Launch a coroutine to handle message sending
80+
sendingJob = scope.launch {
81+
try {
82+
for (message in writeChannel) {
83+
val json = serializeMessage(message)
84+
outputWriter.writeString(json)
85+
outputWriter.flush()
86+
}
87+
} catch (e: Throwable) {
88+
logger.error(e) { "Error writing to stdout" }
89+
_onError.invoke(e)
90+
}
91+
}
8392
}
8493

8594
private suspend fun processReadBuffer() {
@@ -102,22 +111,20 @@ public class StdioServerTransport(
102111
}
103112

104113
override suspend fun close() {
105-
if (!initialized.compareAndSet(true, false)) return
114+
if (!initialized.compareAndSet(expectedValue = true, newValue = false)) return
106115

107116
// Cancel reading job and close channel
108117
readingJob?.cancel() // ToDO("was cancel and join")
118+
sendingJob?.cancel()
119+
109120
readChannel.close()
121+
writeChannel.close()
110122
readBuffer.clear()
111123

112124
_onClose.invoke()
113125
}
114126

115127
override suspend fun send(message: JSONRPCMessage) {
116-
val json = serializeMessage(message)
117-
lock.withLock {
118-
// You may need to add Content-Length headers before the message if using the LSP framing protocol
119-
outputWriter.writeString(json)
120-
outputWriter.flush()
121-
}
128+
writeChannel.send(message)
122129
}
123130
}

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt

+8-15
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
11
package io.modelcontextprotocol.kotlin.sdk.shared
22

3-
import io.ktor.websocket.Frame
4-
import io.ktor.websocket.WebSocketSession
5-
import io.ktor.websocket.close
6-
import io.ktor.websocket.readText
3+
import io.ktor.websocket.*
74
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
8-
import kotlinx.atomicfu.AtomicBoolean
9-
import kotlinx.atomicfu.atomic
10-
import kotlinx.coroutines.CoroutineName
11-
import kotlinx.coroutines.CoroutineScope
12-
import kotlinx.coroutines.InternalCoroutinesApi
13-
import kotlinx.coroutines.SupervisorJob
5+
import kotlinx.coroutines.*
146
import kotlinx.coroutines.channels.ClosedReceiveChannelException
15-
import kotlinx.coroutines.job
16-
import kotlinx.coroutines.launch
177
import kotlinx.serialization.encodeToString
8+
import kotlin.concurrent.atomics.AtomicBoolean
9+
import kotlin.concurrent.atomics.ExperimentalAtomicApi
1810

1911
internal const val MCP_SUBPROTOCOL = "mcp"
2012

2113
/**
2214
* Abstract class representing a WebSocket transport for the Model Context Protocol (MCP).
2315
* Handles communication over a WebSocket session.
2416
*/
17+
@OptIn(ExperimentalAtomicApi::class)
2518
public abstract class WebSocketMcpTransport : AbstractTransport() {
2619
private val scope by lazy {
2720
CoroutineScope(session.coroutineContext + SupervisorJob())
2821
}
2922

30-
private val initialized: AtomicBoolean = atomic(false)
23+
private val initialized: AtomicBoolean = AtomicBoolean(false)
3124
/**
3225
* The WebSocket session used for communication.
3326
*/
@@ -83,15 +76,15 @@ public abstract class WebSocketMcpTransport : AbstractTransport() {
8376
}
8477

8578
override suspend fun send(message: JSONRPCMessage) {
86-
if (!initialized.value) {
79+
if (!initialized.load()) {
8780
error("Not connected")
8881
}
8982

9083
session.outgoing.send(Frame.Text(McpJson.encodeToString(message)))
9184
}
9285

9386
override suspend fun close() {
94-
if (!initialized.value) {
87+
if (!initialized.load()) {
9588
error("Not connected")
9689
}
9790

src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
package io.modelcontextprotocol.kotlin.sdk
44

55
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
6-
import kotlinx.atomicfu.AtomicLong
7-
import kotlinx.atomicfu.atomic
86
import kotlinx.serialization.Serializable
97
import kotlinx.serialization.json.JsonElement
108
import kotlinx.serialization.json.JsonObject
119
import kotlinx.serialization.json.decodeFromJsonElement
1210
import kotlinx.serialization.json.encodeToJsonElement
13-
import kotlin.jvm.JvmInline
11+
import kotlin.concurrent.atomics.AtomicLong
12+
import kotlin.concurrent.atomics.ExperimentalAtomicApi
13+
import kotlin.concurrent.atomics.incrementAndFetch
1414

1515
public const val LATEST_PROTOCOL_VERSION: String = "2024-11-05"
1616

@@ -21,7 +21,8 @@ public val SUPPORTED_PROTOCOL_VERSIONS: Array<String> = arrayOf(
2121

2222
public const val JSONRPC_VERSION: String = "2.0"
2323

24-
private val REQUEST_MESSAGE_ID: AtomicLong = atomic(0L)
24+
@OptIn(ExperimentalAtomicApi::class)
25+
private val REQUEST_MESSAGE_ID: AtomicLong = AtomicLong(0L)
2526

2627
/**
2728
* A progress token, used to associate progress notifications with the original request.
@@ -132,7 +133,7 @@ internal fun Request.toJSON(): JSONRPCRequest {
132133
*/
133134
internal fun JSONRPCRequest.fromJSON(): Request? {
134135
val serializer = selectRequestDeserializer(method)
135-
val params = params ?: return null
136+
val params = params
136137
return McpJson.decodeFromJsonElement<Request>(serializer, params)
137138
}
138139

@@ -211,9 +212,10 @@ public sealed interface JSONRPCMessage
211212
/**
212213
* A request that expects a response.
213214
*/
215+
@OptIn(ExperimentalAtomicApi::class)
214216
@Serializable
215217
public data class JSONRPCRequest(
216-
val id: RequestId = RequestId.NumberId(REQUEST_MESSAGE_ID.incrementAndGet()),
218+
val id: RequestId = RequestId.NumberId(REQUEST_MESSAGE_ID.incrementAndFetch()),
217219
val method: String,
218220
val params: JsonElement = EmptyJsonObject,
219221
val jsonrpc: String = JSONRPC_VERSION,

0 commit comments

Comments
 (0)