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:
Maria Tigina 2025-09-29 09:10:07 +02:00 • committed by GitHub
commit d86813636e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1191 additions and 602 deletions

View file

@ -157,9 +157,11 @@ public open class Client(private val clientInfo: Implementation, options: Client
notification(InitializedNotification())
} catch (error: Throwable) {
logger.error(error) { "Failed to initialize client: ${error.message}" }
close()
if (error !is CancellationException) {
throw IllegalStateException("Error connecting to transport: ${error.message}")
throw IllegalStateException("Error connecting to transport: ${error.message}", error)
}
throw error

View file

@ -1,5 +1,6 @@
package io.modelcontextprotocol.kotlin.sdk.client
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import io.ktor.client.plugins.sse.ClientSSESession
import io.ktor.client.plugins.sse.sseSession
@ -46,6 +47,8 @@ public class SseClientTransport(
private val reconnectionTime: Duration? = null,
private val requestBuilder: HttpRequestBuilder.() -> Unit = {},
) : AbstractTransport() {
private val logger = KotlinLogging.logger {}
private val initialized: AtomicBoolean = AtomicBoolean(false)
private val endpoint = CompletableDeferred<String>()
@ -111,6 +114,8 @@ public class SseClientTransport(
val text = response.bodyAsText()
error("Error POSTing to endpoint (HTTP ${response.status}): $text")
}
logger.debug { "Client successfully sent message via SSE $endpoint" }
} catch (e: Throwable) {
_onError(e)
throw e
@ -158,6 +163,7 @@ public class SseClientTransport(
val path = if (eventData.startsWith("/")) eventData.substring(1) else eventData
val endpointUrl = Url("$baseUrl/$path")
endpoint.complete(endpointUrl.toString())
logger.debug { "Client connected to endpoint: $endpointUrl" }
} catch (e: Throwable) {
_onError(e)
endpoint.completeExceptionally(e)

View file

@ -1,5 +1,6 @@
package io.modelcontextprotocol.kotlin.sdk.client
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.HttpRequestBuilder
@ -10,6 +11,8 @@ import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL
import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport
import kotlin.properties.Delegates
private val logger = KotlinLogging.logger {}
/**
* Client transport for WebSocket: this will connect to a server over the WebSocket protocol.
*/
@ -21,6 +24,8 @@ public class WebSocketClientTransport(
override var session: WebSocketSession by Delegates.notNull()
override suspend fun initializeSession() {
logger.debug { "Websocket session initialization started..." }
session = urlString?.let {
client.webSocketSession(it) {
requestBuilder()
@ -32,5 +37,7 @@ public class WebSocketClientTransport(
header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL)
}
logger.debug { "Websocket session initialization finished" }
}
}

View file

@ -1,11 +1,14 @@
package io.modelcontextprotocol.kotlin.sdk.client
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import io.ktor.client.request.HttpRequestBuilder
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION
import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME
private val logger = KotlinLogging.logger {}
/**
* Returns a new WebSocket transport for the Model Context Protocol using the provided HttpClient.
*
@ -36,6 +39,8 @@ public suspend fun HttpClient.mcpWebSocket(
version = LIB_VERSION,
),
)
logger.debug { "Client started to connect to server" }
client.connect(transport)
logger.debug { "Client finished to connect to server" }
return client
}