From d07cff9f58271f9e306956a4347fe50c9490dcaf Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:37:28 +0200 Subject: [PATCH] Add `mcpClient` function with experimental API support and update related tests - Introduced `@ExperimentalMcpApi` annotation to mark experimental APIs. - Added `mcpClient` function for initializing and connecting MCP clients. - Modified tests to utilize the new `mcpClient` function. - Refactored variable naming in transport tests for improved readability. --- kotlin-sdk-client/api/kotlin-sdk-client.api | 5 ++ .../kotlin/sdk/client/Client.kt | 25 +++++++++ kotlin-sdk-core/api/kotlin-sdk-core.api | 3 ++ .../kotlin/sdk/ExperimentalMcpApi.kt | 54 +++++++++++++++++++ .../sdk/client/StdioClientTransportTest.kt | 12 ++--- .../sdk/server/ServerInstructionsTest.kt | 34 ++++++++---- 6 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index 7a1e0fad..e071e81b 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -43,6 +43,11 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/client/ClientKt { + public static final fun mcpClient (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpClient$default (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { public fun ()V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Z)V diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index f0283c91..c86cad83 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.shared.Protocol import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions @@ -69,6 +70,30 @@ public class ClientOptions( enforceStrictCapabilities: Boolean = true, ) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) +/** + * Initializes and connects an MCP client using the provided clientInfo [Implementation], client options, + * and transport mechanism. + * + * @param clientInfo The implementation details of the MCP client, including its name, version, and other metadata. + * @param clientOptions Optional client configuration settings, such as supported capabilities + * and strict enforcement options. Defaults to a new instance of [ClientOptions]. + * @param transport The transport mechanism used for communication. + * @return An instance of [Client] that is connected and ready for use with the specified transport. + */ +@ExperimentalMcpApi +public suspend fun mcpClient( + clientInfo: Implementation, + clientOptions: ClientOptions = ClientOptions(), + transport: Transport, +): Client { + val client = Client( + clientInfo = clientInfo, + options = clientOptions, + ) + client.connect(transport) + return client +} + /** * An MCP client on top of a pluggable transport. * diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 3e8851ad..fa85476f 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -130,6 +130,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$Companio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi : java/lang/annotation/Annotation { +} + public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification { public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt new file mode 100644 index 00000000..7278f4e4 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt @@ -0,0 +1,54 @@ +package io.modelcontextprotocol.kotlin.sdk + +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.CONSTRUCTOR +import kotlin.annotation.AnnotationTarget.FIELD +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE +import kotlin.annotation.AnnotationTarget.PROPERTY +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER +import kotlin.annotation.AnnotationTarget.TYPEALIAS +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +/** + * Annotation marking an API as experimental and subject to changes or removal in the future. + * + * This annotation is used to signal that a particular element, such as a class, function, or property, + * is part of an experimental API. Such APIs may not be stable, and their usage requires opting in + * explicitly. + * + * Users of the annotated API must explicitly accept the opt-in requirement to ensure they are aware + * of the potential instability or unfinished nature of the API. + * + * Targets that can be annotated include: + * - Classes + * - Annotation classes + * - Properties + * - Fields + * - Local variables + * - Value parameters + * - Constructors + * - Functions + * - Property getters + * - Property setters + * - Type aliases + */ +@RequiresOptIn +@MustBeDocumented +@Target( + CLASS, + ANNOTATION_CLASS, + PROPERTY, + FIELD, + LOCAL_VARIABLE, + VALUE_PARAMETER, + CONSTRUCTOR, + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER, + TYPEALIAS, +) +@Retention(AnnotationRetention.BINARY) +public annotation class ExperimentalMcpApi diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt index 7d0fa360..f0c7aed2 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt @@ -17,12 +17,12 @@ class StdioClientTransportTest : BaseTransportTest() { val input = process.inputStream.asSource().buffered() val output = process.outputStream.asSink().buffered() - val client = StdioClientTransport( + val transport = StdioClientTransport( input = input, output = output, ) - testTransportOpenClose(client) + testTransportOpenClose(transport) process.destroy() } @@ -35,12 +35,12 @@ class StdioClientTransportTest : BaseTransportTest() { val input = process.inputStream.asSource().buffered() val output = process.outputStream.asSink().buffered() - val client = StdioClientTransport( + val transport = StdioClientTransport( input = input, output = output, ) - testTransportRead(client) + testTransportRead(transport) process.waitFor() process.destroy() @@ -55,12 +55,12 @@ class StdioClientTransportTest : BaseTransportTest() { val input = process.inputStream.asSource().buffered() val output = process.outputStream.asSink().buffered() - val client = StdioClientTransport( + val transport = StdioClientTransport( input = input, output = output, ) - testTransportRead(client) + testTransportRead(transport) process.waitFor() process.destroy() diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt index 89eeaef2..53d0fd1d 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt @@ -1,14 +1,18 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.mcpClient import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertNull import kotlin.test.assertEquals +@OptIn(ExperimentalMcpApi::class) class ServerInstructionsTest { @Test @@ -22,10 +26,17 @@ class ServerInstructionsTest { // The instructions should be stored internally and used in handleInitialize // We can't directly access the private field, but we can test it through initialization val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) - server.createSession(serverTransport) - client.connect(clientTransport) + + val client = mcpClient( + clientInfo = Implementation(name = "test client", version = "1.0"), + clientOptions = ClientOptions( + capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(listChanged = false), + ), + ), + transport = clientTransport, + ) assertEquals(instructions, client.serverInstructions) } @@ -41,10 +52,12 @@ class ServerInstructionsTest { // The instructions should be stored internally and used in handleInitialize // We can't directly access the private field, but we can test it through initialization val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) - server.createSession(serverTransport) - client.connect(clientTransport) + + val client = mcpClient( + clientInfo = Implementation(name = "test client", version = "1.0"), + transport = clientTransport, + ) assertEquals(instructions, client.serverInstructions) } @@ -58,10 +71,13 @@ class ServerInstructionsTest { val server = Server(serverInfo, serverOptions) val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) server.createSession(serverTransport) - client.connect(clientTransport) + + val client = mcpClient( + clientInfo = Implementation(name = "test client", version = "1.0"), + transport = clientTransport, + ) assertNull(client.serverInstructions) }