Introduce server sessions (#198)
Introduce a `ServerSession` to manage multiple connections to the mcp server. `Protocall` logic of the `Server` was moved to `ServerSession`, while MCP resources logic remained in `Server` ## Motivation and Context 1. Fix the multiple connection issues reported for SSE transport 2. Avoid server duplications during configurations: currently every connection creates a session, but the server stays single ## How Has This Been Tested? Added `SseIntegrationTest` and `WebSocketIntegrationTest` integration tests to check connection with single and multiple clients ## Breaking Changes This change caused several depreciations: 1. The `Server` -> `Protocol` inheritance (which caused single connection issue) should be removed in future releases, now, all the methods inherited from `Protocall` marked as `Deprecated` with a `Warning` 2. Websocket Ktor server initialization changes in order not to create server instance per connection ## Types of changes - [ ] 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 - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> --------- Co-authored-by: devcrocod <devcrocod@gmail.com>
This commit is contained in:
parent
0ed92f5b03
commit
d86813636e
5 changed files with 1191 additions and 602 deletions
|
|
@ -13,11 +13,29 @@ import io.ktor.server.routing.routing
|
|||
import io.ktor.server.sse.SSE
|
||||
import io.ktor.server.sse.ServerSSESession
|
||||
import io.ktor.server.sse.sse
|
||||
import io.ktor.util.collections.ConcurrentMap
|
||||
import io.ktor.utils.io.KtorDsl
|
||||
import kotlinx.atomicfu.AtomicRef
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.update
|
||||
import kotlinx.collections.immutable.PersistentMap
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
internal class SseTransportManager(transports: Map<String, SseServerTransport> = emptyMap()) {
|
||||
private val transports: AtomicRef<PersistentMap<String, SseServerTransport>> = atomic(transports.toPersistentMap())
|
||||
|
||||
fun getTransport(sessionId: String): SseServerTransport? = transports.value[sessionId]
|
||||
|
||||
fun addTransport(transport: SseServerTransport) {
|
||||
transports.update { it.put(transport.sessionId, transport) }
|
||||
}
|
||||
|
||||
fun removeTransport(sessionId: String) {
|
||||
transports.update { it.remove(sessionId) }
|
||||
}
|
||||
}
|
||||
|
||||
@KtorDsl
|
||||
public fun Routing.mcp(path: String, block: ServerSSESession.() -> Server) {
|
||||
route(path) {
|
||||
|
|
@ -25,85 +43,76 @@ public fun Routing.mcp(path: String, block: ServerSSESession.() -> Server) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE).
|
||||
*/
|
||||
/*
|
||||
* Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE).
|
||||
*/
|
||||
@KtorDsl
|
||||
public fun Routing.mcp(block: ServerSSESession.() -> Server) {
|
||||
val transports = ConcurrentMap<String, SseServerTransport>()
|
||||
val sseTransportManager = SseTransportManager()
|
||||
|
||||
sse {
|
||||
mcpSseEndpoint("", transports, block)
|
||||
mcpSseEndpoint("", sseTransportManager, block)
|
||||
}
|
||||
|
||||
post {
|
||||
mcpPostEndpoint(transports)
|
||||
mcpPostEndpoint(sseTransportManager)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@Deprecated("Use mcp() instead", ReplaceWith("mcp(block)"), DeprecationLevel.WARNING)
|
||||
@Deprecated("Use mcp() instead", ReplaceWith("mcp(block)"), DeprecationLevel.ERROR)
|
||||
public fun Application.MCP(block: ServerSSESession.() -> Server) {
|
||||
mcp(block)
|
||||
}
|
||||
|
||||
@KtorDsl
|
||||
public fun Application.mcp(block: ServerSSESession.() -> Server) {
|
||||
val transports = ConcurrentMap<String, SseServerTransport>()
|
||||
|
||||
install(SSE)
|
||||
|
||||
routing {
|
||||
sse("/sse") {
|
||||
mcpSseEndpoint("/message", transports, block)
|
||||
}
|
||||
|
||||
post("/message") {
|
||||
mcpPostEndpoint(transports)
|
||||
}
|
||||
mcp(block)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ServerSSESession.mcpSseEndpoint(
|
||||
internal suspend fun ServerSSESession.mcpSseEndpoint(
|
||||
postEndpoint: String,
|
||||
transports: ConcurrentMap<String, SseServerTransport>,
|
||||
sseTransportManager: SseTransportManager,
|
||||
block: ServerSSESession.() -> Server,
|
||||
) {
|
||||
val transport = mcpSseTransport(postEndpoint, transports)
|
||||
val transport = mcpSseTransport(postEndpoint, sseTransportManager)
|
||||
|
||||
val server = block()
|
||||
|
||||
server.onClose {
|
||||
logger.info { "Server connection closed for sessionId: ${transport.sessionId}" }
|
||||
transports.remove(transport.sessionId)
|
||||
sseTransportManager.removeTransport(transport.sessionId)
|
||||
}
|
||||
|
||||
server.connect(transport)
|
||||
|
||||
logger.debug { "Server connected to transport for sessionId: ${transport.sessionId}" }
|
||||
}
|
||||
|
||||
internal fun ServerSSESession.mcpSseTransport(
|
||||
postEndpoint: String,
|
||||
transports: ConcurrentMap<String, SseServerTransport>,
|
||||
sseTransportManager: SseTransportManager,
|
||||
): SseServerTransport {
|
||||
val transport = SseServerTransport(postEndpoint, this)
|
||||
transports[transport.sessionId] = transport
|
||||
|
||||
sseTransportManager.addTransport(transport)
|
||||
logger.info { "New SSE connection established and stored with sessionId: ${transport.sessionId}" }
|
||||
|
||||
return transport
|
||||
}
|
||||
|
||||
internal suspend fun RoutingContext.mcpPostEndpoint(transports: ConcurrentMap<String, SseServerTransport>) {
|
||||
val sessionId: String = call.request.queryParameters["sessionId"]
|
||||
?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, "sessionId query parameter is not provided")
|
||||
return
|
||||
}
|
||||
internal suspend fun RoutingContext.mcpPostEndpoint(sseTransportManager: SseTransportManager) {
|
||||
val sessionId: String = call.request.queryParameters["sessionId"] ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, "sessionId query parameter is not provided")
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug { "Received message for sessionId: $sessionId" }
|
||||
|
||||
val transport = transports[sessionId]
|
||||
val transport = sseTransportManager.getTransport(sessionId)
|
||||
if (transport == null) {
|
||||
logger.warn { "Session not found for sessionId: $sessionId" }
|
||||
call.respond(HttpStatusCode.NotFound, "Session not found")
|
||||
|
|
|
|||
|
|
@ -3,58 +3,35 @@ 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.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest
|
||||
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
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListPromptsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListPromptsResult
|
||||
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.ListRootsRequest
|
||||
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.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.Prompt
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.Resource
|
||||
import io.modelcontextprotocol.kotlin.sdk.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ResourceUpdatedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.Tool
|
||||
import io.modelcontextprotocol.kotlin.sdk.ToolAnnotations
|
||||
import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.Protocol
|
||||
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.getAndUpdate
|
||||
import kotlinx.atomicfu.update
|
||||
import kotlinx.collections.immutable.minus
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
|
@ -77,27 +54,18 @@ public class ServerOptions(public val capabilities: ServerCapabilities, enforceS
|
|||
* @param serverInfo Information about this server implementation (name, version).
|
||||
* @param options Configuration options for the server.
|
||||
*/
|
||||
public open class Server(private val serverInfo: Implementation, options: ServerOptions) : Protocol(options) {
|
||||
public open class Server(private val serverInfo: Implementation, private val options: ServerOptions) {
|
||||
private val sessions = atomic(persistentListOf<ServerSession>())
|
||||
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private var _onInitialized: (() -> Unit) = {}
|
||||
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private var _onConnect: (() -> Unit) = {}
|
||||
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private var _onClose: () -> Unit = {}
|
||||
|
||||
/**
|
||||
* The client's reported capabilities after initialization.
|
||||
*/
|
||||
public var clientCapabilities: ClientCapabilities? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The client's version information after initialization.
|
||||
*/
|
||||
public var clientVersion: Implementation? = null
|
||||
private set
|
||||
|
||||
private val capabilities: ServerCapabilities = options.capabilities
|
||||
|
||||
private val _tools = atomic(persistentMapOf<String, RegisteredTool>())
|
||||
private val _prompts = atomic(persistentMapOf<String, RegisteredPrompt>())
|
||||
private val _resources = atomic(persistentMapOf<String, RegisteredResource>())
|
||||
|
|
@ -108,55 +76,83 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
public val resources: Map<String, RegisteredResource>
|
||||
get() = _resources.value
|
||||
|
||||
init {
|
||||
logger.debug { "Initializing MCP server with capabilities: $capabilities" }
|
||||
public suspend fun close() {
|
||||
logger.debug { "Closing MCP server" }
|
||||
sessions.value.forEach { it.close() }
|
||||
_onClose()
|
||||
}
|
||||
|
||||
// Core protocol handlers
|
||||
setRequestHandler<InitializeRequest>(Method.Defined.Initialize) { request, _ ->
|
||||
handleInitialize(request)
|
||||
}
|
||||
setNotificationHandler<InitializedNotification>(Method.Defined.NotificationsInitialized) {
|
||||
_onInitialized()
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
/**
|
||||
* Starts a new server session with the given transport and initializes
|
||||
* internal request handlers based on the server's capabilities.
|
||||
*
|
||||
* @param transport The transport layer to connect the session with.
|
||||
* @return The initialized and connected server session.
|
||||
*/
|
||||
public suspend fun connect(transport: Transport): ServerSession {
|
||||
val session = ServerSession(serverInfo, options)
|
||||
|
||||
// Internal handlers for tools
|
||||
if (capabilities.tools != null) {
|
||||
setRequestHandler<ListToolsRequest>(Method.Defined.ToolsList) { _, _ ->
|
||||
if (options.capabilities.tools != null) {
|
||||
session.setRequestHandler<ListToolsRequest>(Method.Defined.ToolsList) { _, _ ->
|
||||
handleListTools()
|
||||
}
|
||||
setRequestHandler<CallToolRequest>(Method.Defined.ToolsCall) { request, _ ->
|
||||
session.setRequestHandler<CallToolRequest>(Method.Defined.ToolsCall) { request, _ ->
|
||||
handleCallTool(request)
|
||||
}
|
||||
}
|
||||
|
||||
// Internal handlers for prompts
|
||||
if (capabilities.prompts != null) {
|
||||
setRequestHandler<ListPromptsRequest>(Method.Defined.PromptsList) { _, _ ->
|
||||
if (options.capabilities.prompts != null) {
|
||||
session.setRequestHandler<ListPromptsRequest>(Method.Defined.PromptsList) { _, _ ->
|
||||
handleListPrompts()
|
||||
}
|
||||
setRequestHandler<GetPromptRequest>(Method.Defined.PromptsGet) { request, _ ->
|
||||
session.setRequestHandler<GetPromptRequest>(Method.Defined.PromptsGet) { request, _ ->
|
||||
handleGetPrompt(request)
|
||||
}
|
||||
}
|
||||
|
||||
// Internal handlers for resources
|
||||
if (capabilities.resources != null) {
|
||||
setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||
if (options.capabilities.resources != null) {
|
||||
session.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||
handleListResources()
|
||||
}
|
||||
setRequestHandler<ReadResourceRequest>(Method.Defined.ResourcesRead) { request, _ ->
|
||||
session.setRequestHandler<ReadResourceRequest>(Method.Defined.ResourcesRead) { request, _ ->
|
||||
handleReadResource(request)
|
||||
}
|
||||
setRequestHandler<ListResourceTemplatesRequest>(Method.Defined.ResourcesTemplatesList) { _, _ ->
|
||||
session.setRequestHandler<ListResourceTemplatesRequest>(Method.Defined.ResourcesTemplatesList) { _, _ ->
|
||||
handleListResourceTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug { "Server session connecting to transport" }
|
||||
session.connect(transport)
|
||||
logger.debug { "Server session successfully connected to transport" }
|
||||
sessions.update { it.add(session) }
|
||||
|
||||
_onConnect()
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked when the new server session connected.
|
||||
*/
|
||||
public fun onConnect(block: () -> Unit) {
|
||||
val old = _onConnect
|
||||
_onConnect = {
|
||||
old()
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked when the server has completed initialization.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Initialization moved to ServerSession, use ServerSession.onInitialized instead.",
|
||||
ReplaceWith("ServerSession.onInitialized"),
|
||||
DeprecationLevel.WARNING,
|
||||
)
|
||||
public fun onInitialized(block: () -> Unit) {
|
||||
val old = _onInitialized
|
||||
_onInitialized = {
|
||||
|
|
@ -176,14 +172,6 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the server connection is closing.
|
||||
*/
|
||||
override fun onClose() {
|
||||
logger.info { "Server connection closing" }
|
||||
_onClose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a single tool. The client can then call this tool.
|
||||
*
|
||||
|
|
@ -192,7 +180,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support tools.
|
||||
*/
|
||||
public fun addTool(tool: Tool, handler: suspend (CallToolRequest) -> CallToolResult) {
|
||||
if (capabilities.tools == null) {
|
||||
if (options.capabilities.tools == null) {
|
||||
logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" }
|
||||
throw IllegalStateException("Server does not support tools capability. Enable it in ServerOptions.")
|
||||
}
|
||||
|
|
@ -232,7 +220,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support tools.
|
||||
*/
|
||||
public fun addTools(toolsToAdd: List<RegisteredTool>) {
|
||||
if (capabilities.tools == null) {
|
||||
if (options.capabilities.tools == null) {
|
||||
logger.error { "Failed to add tools: Server does not support tools capability" }
|
||||
throw IllegalStateException("Server does not support tools capability.")
|
||||
}
|
||||
|
|
@ -248,7 +236,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support tools.
|
||||
*/
|
||||
public fun removeTool(name: String): Boolean {
|
||||
if (capabilities.tools == null) {
|
||||
if (options.capabilities.tools == null) {
|
||||
logger.error { "Failed to remove tool '$name': Server does not support tools capability" }
|
||||
throw IllegalStateException("Server does not support tools capability.")
|
||||
}
|
||||
|
|
@ -275,7 +263,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support tools.
|
||||
*/
|
||||
public fun removeTools(toolNames: List<String>): Int {
|
||||
if (capabilities.tools == null) {
|
||||
if (options.capabilities.tools == null) {
|
||||
logger.error { "Failed to remove tools: Server does not support tools capability" }
|
||||
throw IllegalStateException("Server does not support tools capability.")
|
||||
}
|
||||
|
|
@ -302,7 +290,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support prompts.
|
||||
*/
|
||||
public fun addPrompt(prompt: Prompt, promptProvider: suspend (GetPromptRequest) -> GetPromptResult) {
|
||||
if (capabilities.prompts == null) {
|
||||
if (options.capabilities.prompts == null) {
|
||||
logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" }
|
||||
throw IllegalStateException("Server does not support prompts capability.")
|
||||
}
|
||||
|
|
@ -336,7 +324,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support prompts.
|
||||
*/
|
||||
public fun addPrompts(promptsToAdd: List<RegisteredPrompt>) {
|
||||
if (capabilities.prompts == null) {
|
||||
if (options.capabilities.prompts == null) {
|
||||
logger.error { "Failed to add prompts: Server does not support prompts capability" }
|
||||
throw IllegalStateException("Server does not support prompts capability.")
|
||||
}
|
||||
|
|
@ -352,7 +340,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support prompts.
|
||||
*/
|
||||
public fun removePrompt(name: String): Boolean {
|
||||
if (capabilities.prompts == null) {
|
||||
if (options.capabilities.prompts == null) {
|
||||
logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" }
|
||||
throw IllegalStateException("Server does not support prompts capability.")
|
||||
}
|
||||
|
|
@ -379,7 +367,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support prompts.
|
||||
*/
|
||||
public fun removePrompts(promptNames: List<String>): Int {
|
||||
if (capabilities.prompts == null) {
|
||||
if (options.capabilities.prompts == null) {
|
||||
logger.error { "Failed to remove prompts: Server does not support prompts capability" }
|
||||
throw IllegalStateException("Server does not support prompts capability.")
|
||||
}
|
||||
|
|
@ -416,7 +404,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
mimeType: String = "text/html",
|
||||
readHandler: suspend (ReadResourceRequest) -> ReadResourceResult,
|
||||
) {
|
||||
if (capabilities.resources == null) {
|
||||
if (options.capabilities.resources == null) {
|
||||
logger.error { "Failed to add resource '$name': Server does not support resources capability" }
|
||||
throw IllegalStateException("Server does not support resources capability.")
|
||||
}
|
||||
|
|
@ -436,7 +424,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support resources.
|
||||
*/
|
||||
public fun addResources(resourcesToAdd: List<RegisteredResource>) {
|
||||
if (capabilities.resources == null) {
|
||||
if (options.capabilities.resources == null) {
|
||||
logger.error { "Failed to add resources: Server does not support resources capability" }
|
||||
throw IllegalStateException("Server does not support resources capability.")
|
||||
}
|
||||
|
|
@ -452,7 +440,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support resources.
|
||||
*/
|
||||
public fun removeResource(uri: String): Boolean {
|
||||
if (capabilities.resources == null) {
|
||||
if (options.capabilities.resources == null) {
|
||||
logger.error { "Failed to remove resource '$uri': Server does not support resources capability" }
|
||||
throw IllegalStateException("Server does not support resources capability.")
|
||||
}
|
||||
|
|
@ -479,7 +467,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
* @throws IllegalStateException If the server does not support resources.
|
||||
*/
|
||||
public fun removeResources(uris: List<String>): Int {
|
||||
if (capabilities.resources == null) {
|
||||
if (options.capabilities.resources == null) {
|
||||
logger.error { "Failed to remove resources: Server does not support resources capability" }
|
||||
throw IllegalStateException("Server does not support resources capability.")
|
||||
}
|
||||
|
|
@ -499,123 +487,7 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
return removedCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client to check connectivity.
|
||||
*
|
||||
* @return The result of the ping request.
|
||||
* @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
|
||||
*/
|
||||
public suspend fun ping(): EmptyRequestResult = request<EmptyRequestResult>(PingRequest())
|
||||
|
||||
/**
|
||||
* Creates a message using the server's sampling capability.
|
||||
*
|
||||
* @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(
|
||||
params: CreateMessageRequest,
|
||||
options: RequestOptions? = null,
|
||||
): CreateMessageResult {
|
||||
logger.debug { "Creating message with params: $params" }
|
||||
return request<CreateMessageResult>(params, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available "roots" from the client's perspective (if supported).
|
||||
*
|
||||
* @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(
|
||||
params: JsonObject = EmptyJsonObject,
|
||||
options: RequestOptions? = null,
|
||||
): ListRootsResult {
|
||||
logger.debug { "Listing roots with params: $params" }
|
||||
return request<ListRootsResult>(ListRootsRequest(params), options)
|
||||
}
|
||||
|
||||
public suspend fun createElicitation(
|
||||
message: String,
|
||||
requestedSchema: RequestedSchema,
|
||||
options: RequestOptions? = null,
|
||||
): CreateElicitationResult {
|
||||
logger.debug { "Creating elicitation with message: $message" }
|
||||
return request(CreateElicitationRequest(message, requestedSchema), options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a logging message notification to the client.
|
||||
*
|
||||
* @param params The logging message notification parameters.
|
||||
*/
|
||||
public suspend fun sendLoggingMessage(params: LoggingMessageNotification) {
|
||||
logger.trace { "Sending logging message: ${params.params.data}" }
|
||||
notification(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a resource-updated notification to the client, indicating that a specific resource has changed.
|
||||
*
|
||||
* @param params Details of the updated resource.
|
||||
*/
|
||||
public suspend fun sendResourceUpdated(params: ResourceUpdatedNotification) {
|
||||
logger.debug { "Sending resource updated notification for: ${params.params.uri}" }
|
||||
notification(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of resources has changed.
|
||||
*/
|
||||
public suspend fun sendResourceListChanged() {
|
||||
logger.debug { "Sending resource list changed notification" }
|
||||
notification(ResourceListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of tools has changed.
|
||||
*/
|
||||
public suspend fun sendToolListChanged() {
|
||||
logger.debug { "Sending tool list changed notification" }
|
||||
notification(ToolListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of prompts has changed.
|
||||
*/
|
||||
public suspend fun sendPromptListChanged() {
|
||||
logger.debug { "Sending prompt list changed notification" }
|
||||
notification(PromptListChangedNotification())
|
||||
}
|
||||
|
||||
// --- Internal Handlers ---
|
||||
|
||||
private suspend fun handleInitialize(request: InitializeRequest): InitializeResult {
|
||||
logger.info { "Handling initialize request from client ${request.clientInfo}" }
|
||||
clientCapabilities = request.capabilities
|
||||
clientVersion = request.clientInfo
|
||||
|
||||
val requestedVersion = request.protocolVersion
|
||||
val protocolVersion = if (SUPPORTED_PROTOCOL_VERSIONS.contains(requestedVersion)) {
|
||||
requestedVersion
|
||||
} else {
|
||||
logger.warn {
|
||||
"Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
|
||||
}
|
||||
LATEST_PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
return InitializeResult(
|
||||
protocolVersion = protocolVersion,
|
||||
capabilities = capabilities,
|
||||
serverInfo = serverInfo,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleListTools(): ListToolsResult {
|
||||
val toolList = tools.values.map { it.tool }
|
||||
return ListToolsResult(tools = toolList, nextCursor = null)
|
||||
|
|
@ -666,147 +538,6 @@ public open class Server(private val serverInfo: Implementation, options: Server
|
|||
// If you have resource templates, return them here. For now, return empty.
|
||||
return ListResourceTemplatesResult(listOf())
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the client supports the capability required for the given [method].
|
||||
*
|
||||
* This method is automatically called by the [Protocol] framework before handling requests.
|
||||
* Throws [IllegalStateException] if the capability is not supported.
|
||||
*
|
||||
* @param method The method for which we are asserting capability.
|
||||
*/
|
||||
override fun assertCapabilityForMethod(method: Method) {
|
||||
logger.trace { "Asserting capability for method: ${method.value}" }
|
||||
when (method.value) {
|
||||
"sampling/createMessage" -> {
|
||||
if (clientCapabilities?.sampling == null) {
|
||||
logger.error { "Client capability assertion failed: sampling not supported" }
|
||||
throw IllegalStateException("Client does not support sampling (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
"roots/list" -> {
|
||||
if (clientCapabilities?.roots == null) {
|
||||
throw IllegalStateException("Client does not support listing roots (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
"elicitation/create" -> {
|
||||
if (clientCapabilities?.elicitation == null) {
|
||||
throw IllegalStateException("Client does not support elicitation (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
"ping" -> {
|
||||
// No specific capability required
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the server can handle the specified notification method.
|
||||
*
|
||||
* Throws [IllegalStateException] if the server does not have the capabilities required to handle this notification.
|
||||
*
|
||||
* @param method The notification method.
|
||||
*/
|
||||
override fun assertNotificationCapability(method: Method) {
|
||||
logger.trace { "Asserting notification capability for method: ${method.value}" }
|
||||
when (method.value) {
|
||||
"notifications/message" -> {
|
||||
if (capabilities.logging == null) {
|
||||
logger.error { "Server capability assertion failed: logging not supported" }
|
||||
throw IllegalStateException("Server does not support logging (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
"notifications/resources/updated",
|
||||
"notifications/resources/list_changed",
|
||||
-> {
|
||||
if (capabilities.resources == null) {
|
||||
throw IllegalStateException(
|
||||
"Server does not support notifying about resources (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"notifications/tools/list_changed" -> {
|
||||
if (capabilities.tools == null) {
|
||||
throw IllegalStateException(
|
||||
"Server does not support notifying of tool list changes (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"notifications/prompts/list_changed" -> {
|
||||
if (capabilities.prompts == null) {
|
||||
throw IllegalStateException(
|
||||
"Server does not support notifying of prompt list changes (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"notifications/cancelled",
|
||||
"notifications/progress",
|
||||
-> {
|
||||
// Always allowed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the server can handle the specified request method.
|
||||
*
|
||||
* Throws [IllegalStateException] if the server does not have the capabilities required to handle this request.
|
||||
*
|
||||
* @param method The request method.
|
||||
*/
|
||||
override fun assertRequestHandlerCapability(method: Method) {
|
||||
logger.trace { "Asserting request handler capability for method: ${method.value}" }
|
||||
when (method.value) {
|
||||
"sampling/createMessage" -> {
|
||||
if (capabilities.sampling == null) {
|
||||
logger.error { "Server capability assertion failed: sampling not supported" }
|
||||
throw IllegalStateException("Server does not support sampling (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
"logging/setLevel" -> {
|
||||
if (capabilities.logging == null) {
|
||||
throw IllegalStateException("Server does not support logging (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
"prompts/get",
|
||||
"prompts/list",
|
||||
-> {
|
||||
if (capabilities.prompts == null) {
|
||||
throw IllegalStateException("Server does not support prompts (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
"resources/list",
|
||||
"resources/templates/list",
|
||||
"resources/read",
|
||||
-> {
|
||||
if (capabilities.resources == null) {
|
||||
throw IllegalStateException("Server does not support resources (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
"tools/call",
|
||||
"tools/list",
|
||||
-> {
|
||||
if (capabilities.tools == null) {
|
||||
throw IllegalStateException("Server does not support tools (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
"ping", "initialize" -> {
|
||||
// No capability required
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,371 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest
|
||||
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.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListRootsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListRootsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.Method.Defined
|
||||
import io.modelcontextprotocol.kotlin.sdk.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ResourceUpdatedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS
|
||||
import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.Protocol
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
public open class ServerSession(private val serverInfo: Implementation, options: ServerOptions) : Protocol(options) {
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private var _onInitialized: (() -> Unit) = {}
|
||||
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private var _onClose: () -> Unit = {}
|
||||
|
||||
init {
|
||||
// Core protocol handlers
|
||||
setRequestHandler<InitializeRequest>(Method.Defined.Initialize) { request, _ ->
|
||||
handleInitialize(request)
|
||||
}
|
||||
setNotificationHandler<InitializedNotification>(Method.Defined.NotificationsInitialized) {
|
||||
_onInitialized()
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The capabilities supported by the server, related to the session.
|
||||
*/
|
||||
private val serverCapabilities = options.capabilities
|
||||
|
||||
/**
|
||||
* The client's reported capabilities after initialization.
|
||||
*/
|
||||
public var clientCapabilities: ClientCapabilities? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The client's version information after initialization.
|
||||
*/
|
||||
public var clientVersion: Implementation? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked when the server has completed initialization.
|
||||
*/
|
||||
public fun onInitialized(block: () -> Unit) {
|
||||
val old = _onInitialized
|
||||
_onInitialized = {
|
||||
old()
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked when the server session is closing.
|
||||
*/
|
||||
public fun onClose(block: () -> Unit) {
|
||||
val old = _onClose
|
||||
_onClose = {
|
||||
old()
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the server session is closing.
|
||||
*/
|
||||
override fun onClose() {
|
||||
logger.debug { "Server connection closing" }
|
||||
_onClose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client to check connectivity.
|
||||
*
|
||||
* @return The result of the ping request.
|
||||
* @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
|
||||
*/
|
||||
public suspend fun ping(): EmptyRequestResult = request<EmptyRequestResult>(PingRequest())
|
||||
|
||||
/**
|
||||
* Creates a message using the server's sampling capability.
|
||||
*
|
||||
* @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(
|
||||
params: CreateMessageRequest,
|
||||
options: RequestOptions? = null,
|
||||
): CreateMessageResult {
|
||||
logger.debug { "Creating message with params: $params" }
|
||||
return request<CreateMessageResult>(params, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available "roots" from the client's perspective (if supported).
|
||||
*
|
||||
* @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(
|
||||
params: JsonObject = EmptyJsonObject,
|
||||
options: RequestOptions? = null,
|
||||
): ListRootsResult {
|
||||
logger.debug { "Listing roots with params: $params" }
|
||||
return request<ListRootsResult>(ListRootsRequest(params), options)
|
||||
}
|
||||
|
||||
public suspend fun createElicitation(
|
||||
message: String,
|
||||
requestedSchema: RequestedSchema,
|
||||
options: RequestOptions? = null,
|
||||
): CreateElicitationResult {
|
||||
logger.debug { "Creating elicitation with message: $message" }
|
||||
return request(CreateElicitationRequest(message, requestedSchema), options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a logging message notification to the client.
|
||||
*
|
||||
* @param notification The logging message notification.
|
||||
*/
|
||||
public suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
|
||||
logger.trace { "Sending logging message: ${notification.params.data}" }
|
||||
notification(notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a resource-updated notification to the client, indicating that a specific resource has changed.
|
||||
*
|
||||
* @param notification Details of the updated resource.
|
||||
*/
|
||||
public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification) {
|
||||
logger.debug { "Sending resource updated notification for: ${notification.params.uri}" }
|
||||
notification(notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of resources has changed.
|
||||
*/
|
||||
public suspend fun sendResourceListChanged() {
|
||||
logger.debug { "Sending resource list changed notification" }
|
||||
notification(ResourceListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of tools has changed.
|
||||
*/
|
||||
public suspend fun sendToolListChanged() {
|
||||
logger.debug { "Sending tool list changed notification" }
|
||||
notification(ToolListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client indicating that the list of prompts has changed.
|
||||
*/
|
||||
public suspend fun sendPromptListChanged() {
|
||||
logger.debug { "Sending prompt list changed notification" }
|
||||
notification(PromptListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the client supports the capability required for the given [method].
|
||||
*
|
||||
* This method is automatically called by the [Protocol] framework before handling requests.
|
||||
* Throws [IllegalStateException] if the capability is not supported.
|
||||
*
|
||||
* @param method The method for which we are asserting capability.
|
||||
*/
|
||||
override fun assertCapabilityForMethod(method: Method) {
|
||||
logger.trace { "Asserting capability for method: ${method.value}" }
|
||||
when (method) {
|
||||
Defined.SamplingCreateMessage -> {
|
||||
if (clientCapabilities?.sampling == null) {
|
||||
logger.error { "Client capability assertion failed: sampling not supported" }
|
||||
throw IllegalStateException("Client does not support sampling (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.RootsList -> {
|
||||
if (clientCapabilities?.roots == null) {
|
||||
logger.error { "Client capability assertion failed: listing roots not supported" }
|
||||
throw IllegalStateException("Client does not support listing roots (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.ElicitationCreate -> {
|
||||
if (clientCapabilities?.elicitation == null) {
|
||||
logger.error { "Client capability assertion failed: elicitation not supported" }
|
||||
throw IllegalStateException("Client does not support elicitation (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.Ping -> {
|
||||
// No specific capability required
|
||||
}
|
||||
|
||||
else -> {
|
||||
// For notifications not specifically listed, no assertion by default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the server can handle the specified notification method.
|
||||
*
|
||||
* Throws [IllegalStateException] if the server does not have the capabilities required to handle this notification.
|
||||
*
|
||||
* @param method The notification method.
|
||||
*/
|
||||
override fun assertNotificationCapability(method: Method) {
|
||||
logger.trace { "Asserting notification capability for method: ${method.value}" }
|
||||
when (method) {
|
||||
Defined.NotificationsMessage -> {
|
||||
if (serverCapabilities.logging == null) {
|
||||
logger.error { "Server capability assertion failed: logging not supported" }
|
||||
throw IllegalStateException("Server does not support logging (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.NotificationsResourcesUpdated,
|
||||
Defined.NotificationsResourcesListChanged,
|
||||
-> {
|
||||
if (serverCapabilities.resources == null) {
|
||||
throw IllegalStateException(
|
||||
"Server does not support notifying about resources (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Defined.NotificationsToolsListChanged -> {
|
||||
if (serverCapabilities.tools == null) {
|
||||
throw IllegalStateException(
|
||||
"Server does not support notifying of tool list changes (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Defined.NotificationsPromptsListChanged -> {
|
||||
if (serverCapabilities.prompts == null) {
|
||||
throw IllegalStateException(
|
||||
"Server does not support notifying of prompt list changes (required for ${method.value})",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Defined.NotificationsCancelled,
|
||||
Defined.NotificationsProgress,
|
||||
-> {
|
||||
// Always allowed
|
||||
}
|
||||
|
||||
else -> {
|
||||
// For notifications not specifically listed, no assertion by default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the server can handle the specified request method.
|
||||
*
|
||||
* Throws [IllegalStateException] if the server does not have the capabilities required to handle this request.
|
||||
*
|
||||
* @param method The request method.
|
||||
*/
|
||||
override fun assertRequestHandlerCapability(method: Method) {
|
||||
logger.trace { "Asserting request handler capability for method: ${method.value}" }
|
||||
when (method) {
|
||||
Defined.SamplingCreateMessage -> {
|
||||
if (serverCapabilities.sampling == null) {
|
||||
logger.error { "Server capability assertion failed: sampling not supported" }
|
||||
throw IllegalStateException("Server does not support sampling (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.LoggingSetLevel -> {
|
||||
if (serverCapabilities.logging == null) {
|
||||
throw IllegalStateException("Server does not support logging (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.PromptsGet,
|
||||
Defined.PromptsList,
|
||||
-> {
|
||||
if (serverCapabilities.prompts == null) {
|
||||
throw IllegalStateException("Server does not support prompts (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.ResourcesList,
|
||||
Defined.ResourcesTemplatesList,
|
||||
Defined.ResourcesRead,
|
||||
Defined.ResourcesSubscribe,
|
||||
Defined.ResourcesUnsubscribe,
|
||||
-> {
|
||||
if (serverCapabilities.resources == null) {
|
||||
throw IllegalStateException("Server does not support resources (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.ToolsCall,
|
||||
Defined.ToolsList,
|
||||
-> {
|
||||
if (serverCapabilities.tools == null) {
|
||||
throw IllegalStateException("Server does not support tools (required for $method)")
|
||||
}
|
||||
}
|
||||
|
||||
Defined.Ping, Defined.Initialize -> {
|
||||
// No capability required
|
||||
}
|
||||
|
||||
else -> {
|
||||
// For notifications not specifically listed, no assertion by default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleInitialize(request: InitializeRequest): InitializeResult {
|
||||
logger.debug { "Handling initialization request from client" }
|
||||
clientCapabilities = request.capabilities
|
||||
clientVersion = request.clientInfo
|
||||
|
||||
val requestedVersion = request.protocolVersion
|
||||
val protocolVersion = if (SUPPORTED_PROTOCOL_VERSIONS.contains(requestedVersion)) {
|
||||
requestedVersion
|
||||
} else {
|
||||
logger.warn {
|
||||
"Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
|
||||
}
|
||||
LATEST_PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
return InitializeResult(
|
||||
protocolVersion = protocolVersion,
|
||||
capabilities = serverCapabilities,
|
||||
serverInfo = serverInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,83 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.routing
|
||||
import io.ktor.server.websocket.WebSocketServerSession
|
||||
import io.ktor.server.websocket.WebSockets
|
||||
import io.ktor.server.websocket.webSocket
|
||||
import io.ktor.utils.io.CancellationException
|
||||
import io.ktor.utils.io.KtorDsl
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket.
|
||||
*/
|
||||
@KtorDsl
|
||||
public fun Routing.mcpWebSocket(block: () -> Server) {
|
||||
webSocket {
|
||||
mcpWebSocketEndpoint(block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket.
|
||||
*/
|
||||
@KtorDsl
|
||||
public fun Routing.mcpWebSocket(path: String, block: () -> Server) {
|
||||
webSocket(path) {
|
||||
mcpWebSocketEndpoint(block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket.
|
||||
*/
|
||||
@KtorDsl
|
||||
public fun Application.mcpWebSocket(block: () -> Server) {
|
||||
install(WebSockets)
|
||||
|
||||
routing {
|
||||
mcpWebSocket(block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket at the specified path.
|
||||
*/
|
||||
@KtorDsl
|
||||
public fun Application.mcpWebSocket(path: String, block: () -> Server) {
|
||||
install(WebSockets)
|
||||
|
||||
routing {
|
||||
mcpWebSocket(path, block)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun WebSocketServerSession.mcpWebSocketEndpoint(block: () -> Server) {
|
||||
logger.info { "Ktor Server establishing new connection" }
|
||||
val transport = createMcpTransport(this)
|
||||
val server = block()
|
||||
var session: ServerSession? = null
|
||||
try {
|
||||
session = server.connect(transport)
|
||||
awaitCancellation()
|
||||
} catch (e: CancellationException) {
|
||||
session?.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMcpTransport(webSocketSession: WebSocketServerSession): WebSocketMcpServerTransport =
|
||||
WebSocketMcpServerTransport(webSocketSession)
|
||||
|
||||
/**
|
||||
* Registers a WebSocket route that establishes an MCP (Model Context Protocol) server session.
|
||||
|
|
@ -14,12 +85,28 @@ import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME
|
|||
* @param options Optional server configuration settings for the MCP server.
|
||||
* @param handler A suspend function that defines the server's behavior.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use mcpWebSocket with a lambda that returns a Server instance instead",
|
||||
ReplaceWith("Routing.mcpWebSocket"),
|
||||
DeprecationLevel.WARNING,
|
||||
)
|
||||
public fun Route.mcpWebSocket(options: ServerOptions? = null, handler: suspend Server.() -> Unit = {}) {
|
||||
webSocket {
|
||||
createMcpServer(this, options, handler)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use mcpWebSocket with a lambda that returns a Server instance instead",
|
||||
ReplaceWith("Routing.mcpWebSocket"),
|
||||
DeprecationLevel.WARNING,
|
||||
)
|
||||
public fun Route.mcpWebSocket(block: () -> Server) {
|
||||
webSocket {
|
||||
block().connect(createMcpTransport(this))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a WebSocket route at the specified [path] that establishes an MCP server session.
|
||||
*
|
||||
|
|
@ -27,6 +114,11 @@ public fun Route.mcpWebSocket(options: ServerOptions? = null, handler: suspend S
|
|||
* @param options Optional server configuration settings for the MCP server.
|
||||
* @param handler A suspend function that defines the server's behavior.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use mcpWebSocket with a path and a lambda that returns a Server instance instead",
|
||||
ReplaceWith("Routing.mcpWebSocket"),
|
||||
DeprecationLevel.WARNING,
|
||||
)
|
||||
public fun Route.mcpWebSocket(path: String, options: ServerOptions? = null, handler: suspend Server.() -> Unit = {}) {
|
||||
webSocket(path) {
|
||||
createMcpServer(this, options, handler)
|
||||
|
|
@ -38,6 +130,11 @@ public fun Route.mcpWebSocket(path: String, options: ServerOptions? = null, hand
|
|||
*
|
||||
* @param handler A suspend function that defines the behavior of the transport layer.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use mcpWebSocket with a lambda that returns a Server instance instead",
|
||||
ReplaceWith("Routing.mcpWebSocket"),
|
||||
DeprecationLevel.WARNING,
|
||||
)
|
||||
public fun Route.mcpWebSocketTransport(handler: suspend WebSocketMcpServerTransport.() -> Unit = {}) {
|
||||
webSocket {
|
||||
val transport = createMcpTransport(this)
|
||||
|
|
@ -53,6 +150,11 @@ public fun Route.mcpWebSocketTransport(handler: suspend WebSocketMcpServerTransp
|
|||
* @param path The URL path at which to register the WebSocket route.
|
||||
* @param handler A suspend function that defines the behavior of the transport layer.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use mcpWebSocket with a path and a lambda that returns a Server instance instead",
|
||||
ReplaceWith("Routing.mcpWebSocket"),
|
||||
DeprecationLevel.WARNING,
|
||||
)
|
||||
public fun Route.mcpWebSocketTransport(path: String, handler: suspend WebSocketMcpServerTransport.() -> Unit = {}) {
|
||||
webSocket(path) {
|
||||
val transport = createMcpTransport(this)
|
||||
|
|
@ -62,6 +164,11 @@ public fun Route.mcpWebSocketTransport(path: String, handler: suspend WebSocketM
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use mcpWebSocket with a lambda that returns a Server instance instead",
|
||||
ReplaceWith("mcpWebSocket"),
|
||||
DeprecationLevel.WARNING,
|
||||
)
|
||||
private suspend fun Route.createMcpServer(
|
||||
session: WebSocketServerSession,
|
||||
options: ServerOptions?,
|
||||
|
|
@ -87,6 +194,3 @@ private suspend fun Route.createMcpServer(
|
|||
handler(server)
|
||||
server.close()
|
||||
}
|
||||
|
||||
private fun createMcpTransport(session: WebSocketServerSession): WebSocketMcpServerTransport =
|
||||
WebSocketMcpServerTransport(session)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.server.websocket.WebSocketServerSession
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Server-side implementation of the MCP (Model Context Protocol) transport over WebSocket.
|
||||
*
|
||||
|
|
@ -12,6 +15,7 @@ import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport
|
|||
*/
|
||||
public class WebSocketMcpServerTransport(override val session: WebSocketServerSession) : WebSocketMcpTransport() {
|
||||
override suspend fun initializeSession() {
|
||||
logger.debug { "Checking session headers" }
|
||||
val subprotocol = session.call.request.headers[HttpHeaders.SecWebSocketProtocol]
|
||||
if (subprotocol != MCP_SUBPROTOCOL) {
|
||||
error("Invalid subprotocol: $subprotocol, expected $MCP_SUBPROTOCOL")
|
||||
|
|
|
|||
Loading…
Reference in a new issue