Skip to content

Commit 93d544e

Browse files
authored
Add experimental mcpClient(...) factory method (#396)
- 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. ## Motivation and Context To improve the creation of the MCP Client experience. ## How Has This Been Tested? Integration test updated ## Breaking Changes No ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions -->
1 parent 8bdbd36 commit 93d544e

File tree

6 files changed

+118
-15
lines changed

6 files changed

+118
-15
lines changed

kotlin-sdk-client/api/kotlin-sdk-client.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp
4343
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;
4444
}
4545

46+
public final class io/modelcontextprotocol/kotlin/sdk/client/ClientKt {
47+
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;
48+
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;
49+
}
50+
4651
public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions {
4752
public fun <init> ()V
4853
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Z)V

kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.modelcontextprotocol.kotlin.sdk.client
22

33
import io.github.oshai.kotlinlogging.KotlinLogging
4+
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
45
import io.modelcontextprotocol.kotlin.sdk.shared.Protocol
56
import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions
67
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
@@ -69,6 +70,30 @@ public class ClientOptions(
6970
enforceStrictCapabilities: Boolean = true,
7071
) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities)
7172

73+
/**
74+
* Initializes and connects an MCP client using the provided clientInfo [Implementation], client options,
75+
* and transport mechanism.
76+
*
77+
* @param clientInfo The implementation details of the MCP client, including its name, version, and other metadata.
78+
* @param clientOptions Optional client configuration settings, such as supported capabilities
79+
* and strict enforcement options. Defaults to a new instance of [ClientOptions].
80+
* @param transport The transport mechanism used for communication.
81+
* @return An instance of [Client] that is connected and ready for use with the specified transport.
82+
*/
83+
@ExperimentalMcpApi
84+
public suspend fun mcpClient(
85+
clientInfo: Implementation,
86+
clientOptions: ClientOptions = ClientOptions(),
87+
transport: Transport,
88+
): Client {
89+
val client = Client(
90+
clientInfo = clientInfo,
91+
options = clientOptions,
92+
)
93+
client.connect(transport)
94+
return client
95+
}
96+
7297
/**
7398
* An MCP client on top of a pluggable transport.
7499
*

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$Companio
130130
public final fun serializer ()Lkotlinx/serialization/KSerializer;
131131
}
132132

133+
public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi : java/lang/annotation/Annotation {
134+
}
135+
133136
public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification {
134137
public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification;
135138
public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.modelcontextprotocol.kotlin.sdk
2+
3+
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
4+
import kotlin.annotation.AnnotationTarget.CLASS
5+
import kotlin.annotation.AnnotationTarget.CONSTRUCTOR
6+
import kotlin.annotation.AnnotationTarget.FIELD
7+
import kotlin.annotation.AnnotationTarget.FUNCTION
8+
import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE
9+
import kotlin.annotation.AnnotationTarget.PROPERTY
10+
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
11+
import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
12+
import kotlin.annotation.AnnotationTarget.TYPEALIAS
13+
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
14+
15+
/**
16+
* Annotation marking an API as experimental and subject to changes or removal in the future.
17+
*
18+
* This annotation is used to signal that a particular element, such as a class, function, or property,
19+
* is part of an experimental API. Such APIs may not be stable, and their usage requires opting in
20+
* explicitly.
21+
*
22+
* Users of the annotated API must explicitly accept the opt-in requirement to ensure they are aware
23+
* of the potential instability or unfinished nature of the API.
24+
*
25+
* Targets that can be annotated include:
26+
* - Classes
27+
* - Annotation classes
28+
* - Properties
29+
* - Fields
30+
* - Local variables
31+
* - Value parameters
32+
* - Constructors
33+
* - Functions
34+
* - Property getters
35+
* - Property setters
36+
* - Type aliases
37+
*/
38+
@RequiresOptIn
39+
@MustBeDocumented
40+
@Target(
41+
CLASS,
42+
ANNOTATION_CLASS,
43+
PROPERTY,
44+
FIELD,
45+
LOCAL_VARIABLE,
46+
VALUE_PARAMETER,
47+
CONSTRUCTOR,
48+
FUNCTION,
49+
PROPERTY_GETTER,
50+
PROPERTY_SETTER,
51+
TYPEALIAS,
52+
)
53+
@Retention(AnnotationRetention.BINARY)
54+
public annotation class ExperimentalMcpApi

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ class StdioClientTransportTest : BaseTransportTest() {
1717
val input = process.inputStream.asSource().buffered()
1818
val output = process.outputStream.asSink().buffered()
1919

20-
val client = StdioClientTransport(
20+
val transport = StdioClientTransport(
2121
input = input,
2222
output = output,
2323
)
2424

25-
testTransportOpenClose(client)
25+
testTransportOpenClose(transport)
2626

2727
process.destroy()
2828
}
@@ -35,12 +35,12 @@ class StdioClientTransportTest : BaseTransportTest() {
3535
val input = process.inputStream.asSource().buffered()
3636
val output = process.outputStream.asSink().buffered()
3737

38-
val client = StdioClientTransport(
38+
val transport = StdioClientTransport(
3939
input = input,
4040
output = output,
4141
)
4242

43-
testTransportRead(client)
43+
testTransportRead(transport)
4444

4545
process.waitFor()
4646
process.destroy()
@@ -55,12 +55,12 @@ class StdioClientTransportTest : BaseTransportTest() {
5555
val input = process.inputStream.asSource().buffered()
5656
val output = process.outputStream.asSink().buffered()
5757

58-
val client = StdioClientTransport(
58+
val transport = StdioClientTransport(
5959
input = input,
6060
output = output,
6161
)
6262

63-
testTransportRead(client)
63+
testTransportRead(transport)
6464

6565
process.waitFor()
6666
process.destroy()

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package io.modelcontextprotocol.kotlin.sdk.server
22

3+
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
34
import io.modelcontextprotocol.kotlin.sdk.Implementation
45
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
5-
import io.modelcontextprotocol.kotlin.sdk.client.Client
6+
import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions
7+
import io.modelcontextprotocol.kotlin.sdk.client.mcpClient
68
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
9+
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
710
import kotlinx.coroutines.test.runTest
811
import org.junit.jupiter.api.Test
912
import org.junit.jupiter.api.assertNull
1013
import kotlin.test.assertEquals
1114

15+
@OptIn(ExperimentalMcpApi::class)
1216
class ServerInstructionsTest {
1317

1418
@Test
@@ -22,10 +26,17 @@ class ServerInstructionsTest {
2226
// The instructions should be stored internally and used in handleInitialize
2327
// We can't directly access the private field, but we can test it through initialization
2428
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
25-
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
26-
2729
server.createSession(serverTransport)
28-
client.connect(clientTransport)
30+
31+
val client = mcpClient(
32+
clientInfo = Implementation(name = "test client", version = "1.0"),
33+
clientOptions = ClientOptions(
34+
capabilities = ClientCapabilities(
35+
roots = ClientCapabilities.Roots(listChanged = false),
36+
),
37+
),
38+
transport = clientTransport,
39+
)
2940

3041
assertEquals(instructions, client.serverInstructions)
3142
}
@@ -41,10 +52,12 @@ class ServerInstructionsTest {
4152
// The instructions should be stored internally and used in handleInitialize
4253
// We can't directly access the private field, but we can test it through initialization
4354
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
44-
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
45-
4655
server.createSession(serverTransport)
47-
client.connect(clientTransport)
56+
57+
val client = mcpClient(
58+
clientInfo = Implementation(name = "test client", version = "1.0"),
59+
transport = clientTransport,
60+
)
4861

4962
assertEquals(instructions, client.serverInstructions)
5063
}
@@ -58,10 +71,13 @@ class ServerInstructionsTest {
5871
val server = Server(serverInfo, serverOptions)
5972

6073
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
61-
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
6274

6375
server.createSession(serverTransport)
64-
client.connect(clientTransport)
76+
77+
val client = mcpClient(
78+
clientInfo = Implementation(name = "test client", version = "1.0"),
79+
transport = clientTransport,
80+
)
6581

6682
assertNull(client.serverInstructions)
6783
}

0 commit comments

Comments
 (0)