Skip to content

Commit e8c2f00

Browse files
devcrocodMr3zee
andauthored
Migrate project to Multiplatform (KMP) (#22)
* Migrate to Kotlin multiplatform * Fix publication * Switch to using `alias` for the AtomicFu plugin --------- Co-authored-by: Alexander Sysoev <Alexander.Sysoev@jetbrains.com>
1 parent 67d1f77 commit e8c2f00

33 files changed

+160
-125
lines changed

build.gradle.kts

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,35 @@
1+
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
2+
13
import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier
4+
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
25
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
6+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
37
import org.jreleaser.model.Active
48

59
plugins {
6-
alias(libs.plugins.kotlin.jvm)
10+
alias(libs.plugins.kotlin.multiplatform)
711
alias(libs.plugins.kotlin.serialization)
812
alias(libs.plugins.dokka)
913
alias(libs.plugins.jreleaser)
14+
alias(libs.plugins.atomicfu)
1015
`maven-publish`
1116
}
1217

1318
group = "io.modelcontextprotocol"
1419
version = "0.2.0"
1520

16-
repositories {
17-
mavenCentral()
18-
}
19-
20-
dependencies {
21-
api(libs.kotlinx.serialization.json)
22-
api(libs.ktor.client.cio)
23-
api(libs.ktor.server.cio)
24-
api(libs.ktor.server.sse)
25-
api(libs.ktor.server.websockets)
26-
27-
implementation(libs.kotlin.logging)
28-
29-
testImplementation(libs.kotlin.test)
30-
testImplementation(libs.mockk)
31-
testImplementation(libs.ktor.server.test.host)
32-
testImplementation(libs.kotlinx.coroutines.test)
33-
testImplementation(libs.kotlinx.coroutines.debug)
34-
}
35-
36-
val sources = tasks.create<Jar>("sourcesJar") {
37-
from(sourceSets["main"].allSource)
38-
archiveClassifier.set("sources")
21+
val mainSourcesJar = tasks.register<Jar>("mainSourcesJar") {
22+
archiveClassifier = "sources"
23+
from(kotlin.sourceSets.getByName("commonMain").kotlin)
3924
}
4025

4126
publishing {
42-
publications {
43-
create<MavenPublication>("maven") {
44-
groupId = project.group.toString()
45-
artifactId = project.name
46-
version = project.version.toString()
47-
48-
from(components["java"])
49-
}
50-
}
51-
5227
val javadocJar = configureEmptyJavadocArtifact()
5328

5429
publications.withType(MavenPublication::class).all {
5530
pom.configureMavenCentralMetadata()
5631
signPublicationIfKeyPresent()
5732
artifact(javadocJar)
58-
artifact(sources)
5933
}
6034

6135
repositories {
@@ -164,17 +138,7 @@ infix fun <T> Property<T>.by(value: T) {
164138
set(value)
165139
}
166140

167-
tasks.create<Jar>("localJar") {
168-
dependsOn(tasks.jar)
169-
170-
archiveFileName = "kotlin-sdk.jar"
171-
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
172-
from(configurations.runtimeClasspath.map {
173-
it.map { if (it.isDirectory) it else zipTree(it) }
174-
})
175-
}
176-
177-
tasks.test {
141+
tasks.withType<Test>().configureEach {
178142
useJUnitPlatform()
179143
}
180144

@@ -205,7 +169,7 @@ abstract class GenerateLibVersionTask @Inject constructor(
205169
dokka {
206170
moduleName.set("MCP Kotlin SDK")
207171

208-
dokkaSourceSets.main {
172+
dokkaSourceSets.configureEach {
209173
sourceLink {
210174
localDirectory.set(file("src/main/kotlin"))
211175
remoteUrl("https://github.com/modelcontextprotocol/kotlin-sdk")
@@ -224,13 +188,43 @@ val generateLibVersionTask =
224188
tasks.register<GenerateLibVersionTask>("generateLibVersion", version.toString(), sourcesDir)
225189

226190
kotlin {
191+
jvm {
192+
compilerOptions {
193+
jvmTarget = JvmTarget.JVM_17
194+
}
195+
}
196+
227197
explicitApi = ExplicitApiMode.Strict
228198

229199
jvmToolchain(21)
230200

231201
sourceSets {
232-
main {
202+
commonMain {
233203
kotlin.srcDir(generateLibVersionTask.map { it.sourcesDir })
204+
dependencies {
205+
api(libs.kotlinx.serialization.json)
206+
api(libs.ktor.client.cio)
207+
api(libs.ktor.server.cio)
208+
api(libs.ktor.server.sse)
209+
api(libs.ktor.server.websockets)
210+
211+
implementation(libs.kotlin.logging)
212+
}
213+
}
214+
215+
commonTest {
216+
dependencies {
217+
implementation(libs.kotlin.test)
218+
implementation(libs.ktor.server.test.host)
219+
implementation(libs.kotlinx.coroutines.test)
220+
implementation(libs.kotlinx.coroutines.debug)
221+
}
222+
}
223+
224+
jvmTest {
225+
dependencies {
226+
implementation(libs.mockk)
227+
}
234228
}
235229
}
236230
}

gradle/libs.versions.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[versions]
22
# plugins version
33
kotlin = "2.0.21"
4-
dokka = "2.0.0-Beta"
4+
dokka = "2.0.0"
5+
atomicfu = "0.26.1"
56

67
# libraries version
78
serialization = "1.7.3"
@@ -14,10 +15,9 @@ jreleaser = "1.15.0"
1415
[libraries]
1516
# Kotlinx libraries
1617
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
17-
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" }
18+
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" }
1819

1920
# Ktor
20-
ktor-client-apache = { group = "io.ktor", name = "ktor-client-apache", version.ref = "ktor" }
2121
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
2222
ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse", version.ref = "ktor" }
2323
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
@@ -33,7 +33,8 @@ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
3333

3434

3535
[plugins]
36-
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
36+
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
3737
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
3838
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
39-
jreleaser = { id = "org.jreleaser", version.ref = "jreleaser"}
39+
jreleaser = { id = "org.jreleaser", version.ref = "jreleaser"}
40+
atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }

settings.gradle.kts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1-
plugins {
2-
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
1+
pluginManagement {
2+
repositories {
3+
mavenCentral()
4+
gradlePluginPortal()
5+
}
6+
7+
plugins {
8+
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
9+
}
10+
}
11+
12+
dependencyResolutionManagement {
13+
repositories {
14+
mavenCentral()
15+
}
316
}
17+
418
rootProject.name = "kotlin-sdk"
519

src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt renamed to src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import io.ktor.http.*
88
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
99
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
1010
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
11+
import kotlinx.atomicfu.AtomicBoolean
12+
import kotlinx.atomicfu.atomic
1113
import kotlinx.coroutines.*
1214
import kotlinx.serialization.encodeToString
13-
import java.util.concurrent.atomic.AtomicBoolean
1415
import kotlin.properties.Delegates
1516
import kotlin.time.Duration
1617

@@ -28,7 +29,7 @@ public class SSEClientTransport(
2829
CoroutineScope(session.coroutineContext + SupervisorJob())
2930
}
3031

31-
private val initialized = AtomicBoolean(false)
32+
private val initialized: AtomicBoolean = atomic(false)
3233
private var session: ClientSSESession by Delegates.notNull()
3334
private val endpoint = CompletableDeferred<String>()
3435

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

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

src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt renamed to src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
55
import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer
66
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
77
import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage
8+
import kotlinx.atomicfu.AtomicBoolean
9+
import kotlinx.atomicfu.atomic
810
import kotlinx.coroutines.*
911
import kotlinx.coroutines.channels.Channel
1012
import kotlinx.coroutines.channels.consumeEach
11-
import java.io.InputStream
12-
import java.io.OutputStream
13-
import java.util.concurrent.atomic.AtomicBoolean
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
1419
import kotlin.coroutines.CoroutineContext
15-
import kotlin.text.Charsets.UTF_8
1620

1721
/**
1822
* A transport implementation for JSON-RPC communication that leverages standard input and output streams.
@@ -24,16 +28,16 @@ import kotlin.text.Charsets.UTF_8
2428
* @param output The output stream where messages are sent.
2529
*/
2630
public class StdioClientTransport(
27-
private val input: InputStream,
28-
private val output: OutputStream
31+
private val input: Source,
32+
private val output: Sink
2933
) : Transport {
3034
private val logger = KotlinLogging.logger {}
3135
private val ioCoroutineContext: CoroutineContext = Dispatchers.IO
3236
private val scope by lazy {
3337
CoroutineScope(ioCoroutineContext + SupervisorJob())
3438
}
3539
private var job: Job? = null
36-
private var initialized = AtomicBoolean(false)
40+
private val initialized: AtomicBoolean = atomic(false)
3741
private val sendChannel = Channel<JSONRPCMessage>(Channel.UNLIMITED)
3842
private val readBuffer = ReadBuffer()
3943

@@ -48,28 +52,26 @@ public class StdioClientTransport(
4852

4953
logger.debug { "Starting StdioClientTransport..." }
5054

51-
val outputStream = output.bufferedWriter(UTF_8)
55+
val outputStream = output.buffered()
5256

5357
job = scope.launch(CoroutineName("StdioClientTransport.IO#${hashCode()}")) {
5458
val readJob = launch {
5559
logger.debug { "Read coroutine started." }
5660
try {
57-
val buffer = ByteArray(8192)
58-
while (isActive) {
59-
val bytesRead = input.read(buffer)
60-
if (bytesRead == -1) break
61-
if (bytesRead > 0) {
62-
readBuffer.append(buffer.copyOf(bytesRead))
63-
processReadBuffer()
61+
input.use {
62+
while (isActive) {
63+
val buffer = Buffer()
64+
val bytesRead = input.readAtMostTo(buffer, 8192)
65+
if (bytesRead == -1L) break
66+
if (bytesRead > 0L) {
67+
readBuffer.append(buffer.readByteArray())
68+
processReadBuffer()
69+
}
6470
}
6571
}
66-
} catch (e: CancellationException) {
67-
throw e
6872
} catch (e: Exception) {
6973
onError?.invoke(e)
7074
logger.error(e) { "Error reading from input stream" }
71-
} finally {
72-
input.close()
7375
}
7476
}
7577

@@ -78,7 +80,7 @@ public class StdioClientTransport(
7880
try {
7981
sendChannel.consumeEach { message ->
8082
val json = serializeMessage(message)
81-
outputStream.write(json)
83+
outputStream.writeString(json)
8284
outputStream.flush()
8385
}
8486
} catch (e: Throwable) {
@@ -98,7 +100,7 @@ public class StdioClientTransport(
98100
}
99101

100102
override suspend fun send(message: JSONRPCMessage) {
101-
if (!initialized.get()) {
103+
if (!initialized.value) {
102104
error("Transport not started")
103105
}
104106

src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt renamed to src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public suspend fun HttpClient.mcpWebSocket(
3333
val client = Client(
3434
Implementation(
3535
name = IMPLEMENTATION_NAME,
36-
version = LIB_VERSION,
36+
version = LIB_VERSION
3737
)
3838
)
3939
client.connect(transport)

src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse.ktor.kt renamed to src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse.ktor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public suspend fun HttpClient.mcpSse(
3838
val client = Client(
3939
Implementation(
4040
name = IMPLEMENTATION_NAME,
41-
version = LIB_VERSION,
41+
version = LIB_VERSION
4242
)
4343
)
4444
client.connect(transport)

0 commit comments

Comments
 (0)