From 03fff840cb689d852e45488c2933c2eaa741a5ff Mon Sep 17 00:00:00 2001 From: jiwon Date: Thu, 30 Apr 2026 07:38:16 +0900 Subject: [PATCH] feat(server): expose client implementation and capabilities to handlers --- .../kotlin/sdk/server/ClientConnectionTest.kt | 11 +++++++++++ kotlin-sdk-server/api/kotlin-sdk-server.api | 2 ++ .../kotlin/sdk/server/ClientConnection.kt | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt index ee8953a7..5273c3d6 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt @@ -11,6 +11,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotificationParams import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel @@ -70,6 +71,8 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { val pingComplete = CompletableDeferred() val rootsResult = CompletableDeferred() val sessionId = CompletableDeferred() + val clientImplementation = CompletableDeferred() + val clientCapabilities = CompletableDeferred() val toolListChanged = onClientNotification(Method.Defined.NotificationsToolsListChanged) val resourceListChanged = @@ -120,6 +123,12 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { elicitationComplete.await().params.elicitationId shouldBe "elicit-123" } capturedSessionId.shouldNotBeBlank() + withClue("clientImplementation should expose the connected client's name and version") { + clientImplementation.await() shouldBe Implementation(name = "test client", version = "1.0") + } + withClue("clientCapabilities should expose what the client advertised at initialize") { + clientCapabilities.await() shouldBe getClientCapabilities() + } } } } @@ -142,6 +151,8 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = "elicit-123")), ) cap.sessionId.complete(sessionId) + cap.clientImplementation.complete(clientImplementation) + cap.clientCapabilities.complete(clientCapabilities) } @Test diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 80da5b53..04620895 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -7,6 +7,8 @@ public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/Client public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public abstract fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public abstract fun getClientImplementation ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; public abstract fun getSessionId ()Ljava/lang/String; public abstract fun listRoots (Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index 9b4b2f35..367c0420 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -2,6 +2,7 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest @@ -11,6 +12,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult @@ -41,6 +43,19 @@ public interface ClientConnection { */ public val sessionId: String + /** + * The client's reported [Implementation] (name and version) after initialization. + */ + public val clientImplementation: Implementation? + + /** + * The client's reported [ClientCapabilities] after initialization. + * + * Consult before invoking [createMessage], [listRoots], or [createElicitation] + * to fall back gracefully when a capability is not advertised. + */ + public val clientCapabilities: ClientCapabilities? + /** * Sends a server-side notification to the client. * @@ -179,6 +194,10 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client override val sessionId: String get() = session.sessionId + override val clientImplementation: Implementation? get() = session.clientVersion + + override val clientCapabilities: ClientCapabilities? get() = session.clientCapabilities + private suspend fun request(request: Request, options: RequestOptions? = null): T { logger.trace { "Sending request to client for session $sessionId: $request" } return session.request(request, options)