diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index f8594469..48b58ac0 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -65,22 +65,37 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { public final fun addTools (Ljava/util/List;)V public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun createMessage (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun createSession (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; protected final fun getInstructionsProvider ()Lkotlin/jvm/functions/Function0; protected final fun getOptions ()Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions; public final fun getPrompts ()Ljava/util/Map; public final fun getResources ()Ljava/util/Map; protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + public final fun getSession (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession; + public final fun getSessionOrThrow (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession; + public final fun getSessions ()Ljava/util/Map; public final fun getTools ()Ljava/util/Map; + public final fun listRoots (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun onClose (Lkotlin/jvm/functions/Function0;)V public final fun onConnect (Lkotlin/jvm/functions/Function0;)V public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V + public final fun ping (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun removePrompt (Ljava/lang/String;)Z public final fun removePrompts (Ljava/util/List;)I public final fun removeResource (Ljava/lang/String;)Z public final fun removeResources (Ljava/util/List;)I public final fun removeTool (Ljava/lang/String;)Z public final fun removeTools (Ljava/util/List;)I + public final fun sendLoggingMessage (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendPromptListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceUpdated (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendToolListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { @@ -98,10 +113,13 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; protected final fun getInstructions ()Ljava/lang/String; protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + public final fun getSessionId ()Ljava/lang/String; + public fun hashCode ()I public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public fun onClose ()V diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index 99f7aa84..898cf349 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -3,7 +3,12 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest.RequestedSchema +import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult +import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest import io.modelcontextprotocol.kotlin.sdk.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.Implementation @@ -13,23 +18,27 @@ import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesRequest import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesResult import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest import io.modelcontextprotocol.kotlin.sdk.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.ListRootsResult import io.modelcontextprotocol.kotlin.sdk.ListToolsRequest import io.modelcontextprotocol.kotlin.sdk.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification import io.modelcontextprotocol.kotlin.sdk.Method import io.modelcontextprotocol.kotlin.sdk.Prompt import io.modelcontextprotocol.kotlin.sdk.PromptArgument import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult import io.modelcontextprotocol.kotlin.sdk.Resource +import io.modelcontextprotocol.kotlin.sdk.ResourceUpdatedNotification import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.Tool import io.modelcontextprotocol.kotlin.sdk.ToolAnnotations import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions +import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions import io.modelcontextprotocol.kotlin.sdk.shared.Transport import kotlinx.atomicfu.atomic import kotlinx.atomicfu.update -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.CancellationException import kotlinx.serialization.json.JsonObject @@ -45,7 +54,7 @@ public class ServerOptions(public val capabilities: ServerCapabilities, enforceS ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) /** - * An MCP server on top of a pluggable transport. + * An MCP server is responsible for storing features and handling new connections. * * This server automatically responds to the initialization flow as initiated by the client. * You can register tools, prompts, and resources using [addTool], [addPrompt], and [addResource]. @@ -79,7 +88,24 @@ public open class Server( block: Server.() -> Unit = {}, ) : this(serverInfo, options, { instructions }, block) - private val sessions = atomic(persistentListOf()) + private val sessionRegistry = atomic(persistentMapOf()) + + /** + * Returns a read-only view of the current server sessions. + */ + public val sessions: Map + get() = sessionRegistry.value + + /** + * Gets a server session by its ID. + */ + public fun getSession(sessionId: String): ServerSession? = sessions[sessionId] + + /** + * Gets a server session by its ID or throws an exception if the session doesn't exist. + */ + public fun getSessionOrThrow(sessionId: String): ServerSession = + sessions[sessionId] ?: throw IllegalArgumentException("Session not found: $sessionId") @Suppress("ktlint:standard:backing-property-naming") private var _onInitialized: (() -> Unit) = {} @@ -107,7 +133,10 @@ public open class Server( public suspend fun close() { logger.debug { "Closing MCP server" } - sessions.value.forEach { session -> session.close() } + sessions.forEach { (sessionId, session) -> + logger.info { "Closing session $sessionId" } + session.close() + } _onClose() } @@ -171,12 +200,12 @@ public open class Server( // Register cleanup handler to remove session from list when it closes session.onClose { logger.debug { "Removing closed session from active sessions list" } - sessions.update { list -> list.remove(session) } + sessionRegistry.update { sessions -> sessions.remove(session.sessionId) } } logger.debug { "Server session connecting to transport" } session.connect(transport) logger.debug { "Server session successfully connected to transport" } - sessions.update { sessions -> sessions.add(session) } + sessionRegistry.update { sessions -> sessions.put(session.sessionId, session) } _onConnect() return session @@ -535,4 +564,124 @@ public open class Server( // If you have resource templates, return them here. For now, return empty. return ListResourceTemplatesResult(listOf()) } + + // Start the ServerSession redirection section + + /** + * Triggers [ServerSession.ping] request for session by provided [sessionId]. + * @param sessionId The session ID to ping + */ + public suspend fun ping(sessionId: String): EmptyRequestResult { + val session = getSessionOrThrow(sessionId) + return session.ping() + } + + /** + * Triggers [ServerSession.createMessage] request for session by provided [sessionId]. + * + * @param sessionId The session ID to create a message. + * @param params The parameters for creating a message. + * @param options Optional request options. + * @return The created message result. + * @throws IllegalStateException If the server does not support sampling or if the request fails. + */ + public suspend fun createMessage( + sessionId: String, + params: CreateMessageRequest, + options: RequestOptions? = null, + ): CreateMessageResult { + val session = getSessionOrThrow(sessionId) + return session.request(params, options) + } + + /** + * Triggers [ServerSession.listRoots] request for session by provided [sessionId]. + * + * @param sessionId The session ID to list roots for. + * @param params JSON parameters for the request, usually empty. + * @param options Optional request options. + * @return The list of roots. + * @throws IllegalStateException If the server or client does not support roots. + */ + public suspend fun listRoots( + sessionId: String, + params: JsonObject = EmptyJsonObject, + options: RequestOptions? = null, + ): ListRootsResult { + val session = getSessionOrThrow(sessionId) + return session.listRoots(params, options) + } + + /** + * Triggers [ServerSession.createElicitation] request for session by provided [sessionId]. + * + * @param sessionId The session ID to create elicitation for. + * @param message The elicitation message. + * @param requestedSchema The requested schema for the elicitation. + * @param options Optional request options. + * @return The created elicitation result. + * @throws IllegalStateException If the server does not support elicitation or if the request fails. + */ + public suspend fun createElicitation( + sessionId: String, + message: String, + requestedSchema: RequestedSchema, + options: RequestOptions? = null, + ): CreateElicitationResult { + val session = getSessionOrThrow(sessionId) + return session.createElicitation(message, requestedSchema, options) + } + + /** + * Triggers [ServerSession.sendLoggingMessage] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the logging message to. + * @param notification The logging message notification. + */ + public suspend fun sendLoggingMessage(sessionId: String, notification: LoggingMessageNotification) { + val session = getSessionOrThrow(sessionId) + session.sendLoggingMessage(notification) + } + + /** + * Triggers [ServerSession.sendResourceUpdated] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the resource updated notification to. + * @param notification Details of the updated resource. + */ + public suspend fun sendResourceUpdated(sessionId: String, notification: ResourceUpdatedNotification) { + val session = getSessionOrThrow(sessionId) + session.sendResourceUpdated(notification) + } + + /** + * Triggers [ServerSession.sendResourceListChanged] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the resource list changed notification to. + */ + public suspend fun sendResourceListChanged(sessionId: String) { + val session = getSessionOrThrow(sessionId) + session.sendResourceListChanged() + } + + /** + * Triggers [ServerSession.sendToolListChanged] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the tool list changed notification to. + */ + public suspend fun sendToolListChanged(sessionId: String) { + val session = getSessionOrThrow(sessionId) + session.sendToolListChanged() + } + + /** + * Triggers [ServerSession.sendPromptListChanged] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the prompt list changed notification to. + */ + public suspend fun sendPromptListChanged(sessionId: String) { + val session = getSessionOrThrow(sessionId) + session.sendPromptListChanged() + } + // End the ServerSession redirection section } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index 887b57bd..4d508cec 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -32,14 +32,24 @@ import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic import kotlinx.coroutines.CompletableDeferred import kotlinx.serialization.json.JsonObject +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid private val logger = KotlinLogging.logger {} +/** + * Represents a server session. + */ +@Suppress("TooManyFunctions") public open class ServerSession( protected val serverInfo: Implementation, options: ServerOptions, protected val instructions: String?, ) : Protocol(options) { + + @OptIn(ExperimentalUuidApi::class) + public val sessionId: String = Uuid.random().toString() + @Suppress("ktlint:standard:backing-property-naming") private var _onInitialized: (() -> Unit) = {} @@ -428,4 +438,12 @@ public open class ServerSession( * @return true if the message should be accepted (not filtered out), false otherwise. */ private fun isMessageAccepted(level: LoggingLevel): Boolean = !isMessageIgnored(level) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ServerSession) return false + return sessionId == other.sessionId + } + + override fun hashCode(): Int = sessionId.hashCode() }