Skip to content

Commit 1f046c8

Browse files
authored
Merge branch 'main' into kpavlov/kotlinx-schema
2 parents dce9ff1 + efed85a commit 1f046c8

File tree

56 files changed

+1657
-568
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1657
-568
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ standardized protocol interface.
4646
* [Streamable HTTP Transport](#streamable-http-transport)
4747
* [SSE Transport](#sse-transport)
4848
* [WebSocket Transport](#websocket-transport)
49+
* [ChannelTransport (testing)](#channeltransport-testing)
4950
* [Connecting your server](#connecting-your-server)
5051
* [Examples](#examples)
5152
* [Documentation](#documentation)
@@ -774,6 +775,12 @@ SSE Ktor plugin when you need drop-in compatibility, but prefer Streamable HTTP
774775
`WebSocketClientTransport` plus the matching server utilities provide full-duplex, low-latency connections—useful when
775776
you expect lots of notifications or long-running sessions behind a reverse proxy that already terminates WebSockets.
776777

778+
### ChannelTransport (testing)
779+
780+
`ChannelTransport` provides a simple, non-networked transport for testing and local development.
781+
It uses Kotlin coroutines channels to provide a full-duplex connection between a client and server,
782+
allowing for easy testing of MCP functionality without the need for network setup.
783+
777784
## Connecting your server
778785

779786
1. Start a sample HTTP server on port 3000:

build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ dependencies {
1111
dokka(project(":kotlin-sdk-core"))
1212
dokka(project(":kotlin-sdk-client"))
1313
dokka(project(":kotlin-sdk-server"))
14+
dokka(project(":kotlin-sdk-testing"))
1415

1516
kover(project(":kotlin-sdk-core"))
1617
kover(project(":kotlin-sdk-client"))
1718
kover(project(":kotlin-sdk-server"))
18-
"kover"(project(":integration-test"))
19+
kover(project(":kotlin-sdk-testing"))
20+
kover(project(":integration-test"))
1921
}
2022

2123
subprojects {
@@ -59,7 +61,7 @@ kover {
5961
}
6062
verify {
6163
rule {
62-
minBound(65)
64+
minBound(73)
6365
}
6466
}
6567
}

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

integration-test/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ kotlin {
3030
jvmTest {
3131
dependencies {
3232
implementation(project(":test-utils"))
33+
implementation(project(":kotlin-sdk-testing"))
3334
}
3435
}
3536
}

integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.modelcontextprotocol.kotlin.test.utils.createTeeProcessBuilder
99
import kotlinx.coroutines.Dispatchers
1010
import kotlinx.coroutines.runBlocking
1111
import kotlinx.coroutines.test.runTest
12+
import kotlinx.coroutines.withTimeout
1213
import kotlinx.io.asSink
1314
import kotlinx.io.asSource
1415
import kotlinx.io.buffered
@@ -56,8 +57,11 @@ class StdioClientTransportTest : BaseTransportTest() {
5657
)
5758

5859
// The error in stderr should cause connecting to fail
60+
// Use explicit timeout to avoid blocking indefinitely due to kotlinx.io cancellation issue (#514)
5961
assertThrows<McpException> {
60-
client.connect(transport)
62+
withTimeout(5.seconds) {
63+
client.connect(transport)
64+
}
6165
}
6266

6367
process.destroyForcibly()
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.modelcontextprotocol.kotlin.sdk.testing
2+
3+
import io.kotest.matchers.collections.shouldHaveSize
4+
import io.kotest.matchers.nulls.shouldNotBeNull
5+
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
6+
import io.modelcontextprotocol.kotlin.sdk.client.Client
7+
import io.modelcontextprotocol.kotlin.sdk.server.Server
8+
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
9+
import io.modelcontextprotocol.kotlin.sdk.server.ServerSession
10+
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
11+
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest
12+
import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult
13+
import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION
14+
import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest
15+
import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult
16+
import io.modelcontextprotocol.kotlin.sdk.types.Method
17+
import io.modelcontextprotocol.kotlin.sdk.types.Resource
18+
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
19+
import kotlinx.coroutines.CompletableDeferred
20+
import kotlinx.coroutines.joinAll
21+
import kotlinx.coroutines.launch
22+
import kotlinx.coroutines.runBlocking
23+
import kotlin.test.Test
24+
25+
@OptIn(ExperimentalMcpApi::class)
26+
class ChannelTransportTest {
27+
28+
@Test
29+
fun `should connect and list resources`(): Unit = runBlocking {
30+
val serverOptions = ServerOptions(
31+
capabilities = ServerCapabilities(
32+
resources = ServerCapabilities.Resources(),
33+
),
34+
)
35+
val server = Server(
36+
Implementation(name = "test server", version = "1.0"),
37+
serverOptions,
38+
)
39+
40+
val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair()
41+
42+
val client = Client(
43+
clientInfo = Implementation(name = "test client", version = "1.0"),
44+
)
45+
46+
val serverSessionResult = CompletableDeferred<ServerSession>()
47+
48+
listOf(
49+
launch {
50+
client.connect(clientTransport)
51+
},
52+
launch {
53+
serverSessionResult.complete(server.createSession(serverTransport))
54+
},
55+
).joinAll()
56+
57+
val serverSession = serverSessionResult.await()
58+
serverSession.setRequestHandler<InitializeRequest>(Method.Defined.Initialize) { _, _ ->
59+
InitializeResult(
60+
protocolVersion = LATEST_PROTOCOL_VERSION,
61+
capabilities = ServerCapabilities(
62+
resources = ServerCapabilities.Resources(null, null),
63+
tools = ServerCapabilities.Tools(null),
64+
),
65+
serverInfo = Implementation(name = "test", version = "1.0"),
66+
)
67+
}
68+
69+
serverSession.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
70+
ListResourcesResult(
71+
resources = listOf(
72+
Resource(
73+
uri = "/foo/bar",
74+
name = "foo-bar-resource",
75+
),
76+
),
77+
)
78+
}
79+
80+
// These should not throw
81+
client.listResources() shouldNotBeNull {
82+
this.resources shouldHaveSize 1
83+
}
84+
85+
client.close()
86+
server.close()
87+
}
88+
}

kotlin-sdk-client/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ kotlin {
4141
commonTest {
4242
dependencies {
4343
implementation(kotlin("test"))
44+
implementation(project(":kotlin-sdk-testing"))
4445
implementation(libs.kotest.assertions.core)
4546
implementation(libs.kotlinx.coroutines.test)
4647
implementation(libs.ktor.client.logging)

kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import io.kotest.assertions.nondeterministic.eventually
44
import io.kotest.assertions.throwables.shouldThrow
55
import io.kotest.matchers.shouldBe
66
import io.kotest.matchers.string.shouldContain
7-
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
7+
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport
88
import io.modelcontextprotocol.kotlin.sdk.types.McpException
99
import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
1010
import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED
@@ -16,7 +16,7 @@ import kotlin.test.Test
1616
import kotlin.time.Duration.Companion.milliseconds
1717
import kotlin.time.Duration.Companion.seconds
1818

19-
abstract class AbstractClientTransportLifecycleTest<T : AbstractTransport> {
19+
abstract class AbstractClientTransportLifecycleTest<T : AbstractClientTransport> {
2020

2121
protected lateinit var transport: T
2222

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.modelcontextprotocol.kotlin.sdk.client.channel
2+
3+
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
4+
import io.modelcontextprotocol.kotlin.sdk.client.AbstractClientTransportLifecycleTest
5+
import io.modelcontextprotocol.kotlin.sdk.testing.ChannelTransport
6+
import kotlin.test.Ignore
7+
import kotlin.test.Test
8+
9+
@OptIn(ExperimentalMcpApi::class)
10+
class ChannelClientTransportLifecycleTest : AbstractClientTransportLifecycleTest<ChannelTransport>() {
11+
12+
/**
13+
* Dummy method to make IDE treat this class as a test
14+
*/
15+
@Test
16+
@Ignore
17+
fun dummyTest() = Unit
18+
19+
override fun createTransport(): ChannelTransport = ChannelTransport()
20+
}

kotlin-sdk-core/api/kotlin-sdk-core.api

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4525,13 +4525,15 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotif
45254525
public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema {
45264526
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion;
45274527
public fun <init> ()V
4528-
public fun <init> (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)V
4529-
public synthetic fun <init> (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
4528+
public fun <init> (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V
4529+
public synthetic fun <init> (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
45304530
public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
45314531
public final fun component2 ()Ljava/util/List;
4532-
public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
4533-
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
4532+
public final fun component3 ()Lkotlinx/serialization/json/JsonObject;
4533+
public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
4534+
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
45344535
public fun equals (Ljava/lang/Object;)Z
4536+
public final fun getDefs ()Lkotlinx/serialization/json/JsonObject;
45354537
public final fun getProperties ()Lkotlinx/serialization/json/JsonObject;
45364538
public final fun getRequired ()Ljava/util/List;
45374539
public final fun getType ()Ljava/lang/String;

0 commit comments

Comments
 (0)