Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions kotlin-sdk-server/api/kotlin-sdk-server.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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].
Expand Down Expand Up @@ -79,7 +88,24 @@ public open class Server(
block: Server.() -> Unit = {},
) : this(serverInfo, options, { instructions }, block)

private val sessions = atomic(persistentListOf<ServerSession>())
private val sessionRegistry = atomic(persistentMapOf<String, ServerSession>())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create a class for a session registry with a Type alias for the sessionId type. To better comply with the Single Responsibility Principle and potentially implement atomic operations on the session


/**
* Returns a read-only view of the current server sessions.
*/
public val sessions: Map<String, ServerSession>
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) = {}
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Comment on lines +575 to +576
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see a pattern! It makes sense to make a method like withSession(sessionId) { session -> ... }

}

/**
* 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) = {}

Expand Down Expand Up @@ -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()
}
Loading