Refactor mcp server, rename connect to (#364)

- Rename `connect` to `createSession` in `Server.kt `to make naming more
clear
- Add block to configure build to `Server.kt`
- Move sse test to jvm to be able to run, small test refactoring
- Introduce base integration test for sse server / client
This commit is contained in:
Maria Tigina 2025-11-03 15:32:25 +01:00 • committed by GitHub
commit f27fe9a41d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 298 additions and 258 deletions

View file

@ -45,9 +45,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool {
}
public class io/modelcontextprotocol/kotlin/sdk/server/Server {
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;)V
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;)V
public synthetic fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V
public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
@ -61,6 +62,7 @@ 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 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;

View file

@ -88,7 +88,7 @@ internal suspend fun ServerSSESession.mcpSseEndpoint(
sseTransportManager.removeTransport(transport.sessionId)
}
server.connect(transport)
server.createSession(transport)
logger.debug { "Server connected to transport for sessionId: ${transport.sessionId}" }
}

View file

@ -57,12 +57,14 @@ public class ServerOptions(public val capabilities: ServerCapabilities, enforceS
* @param options Configuration options for the server.
* @param instructionsProvider Optional provider for instructions from the server to the client about how to use
* this server. The provider is called each time a new session is started to support dynamic instructions.
* @param block A block to configure the mcp server.
*/
public open class Server(
protected val serverInfo: Implementation,
protected val options: ServerOptions,
protected val instructionsProvider: (() -> String)? = null,
block: Server.() -> Unit = {},
) {
/**
* Alternative constructor that provides the instructions directly as a string.
@ -70,12 +72,14 @@ public open class Server(
* @param serverInfo Information about this server implementation (name, version).
* @param options Configuration options for the server.
* @param instructions Instructions from the server to the client about how to use this server.
* @param block A block to configure the mcp server.
*/
public constructor(
serverInfo: Implementation,
options: ServerOptions,
instructions: String,
) : this(serverInfo, options, { instructions })
block: Server.() -> Unit = {},
) : this(serverInfo, options, { instructions }, block)
private val sessions = atomic(persistentListOf<ServerSession>())
@ -98,6 +102,10 @@ public open class Server(
public val resources: Map<String, RegisteredResource>
get() = _resources.value
init {
block(this)
}
public suspend fun close() {
logger.debug { "Closing MCP server" }
sessions.value.forEach { it.close() }
@ -111,7 +119,21 @@ public open class Server(
* @param transport The transport layer to connect the session with.
* @return The initialized and connected server session.
*/
public suspend fun connect(transport: Transport): ServerSession {
@Deprecated(
"Use createSession(transport) instead.",
ReplaceWith("createSession(transport)"),
DeprecationLevel.WARNING,
)
public suspend fun connect(transport: Transport): ServerSession = createSession(transport)
/**
* 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 createSession(transport: Transport): ServerSession {
val session = ServerSession(serverInfo, options, instructionsProvider?.invoke())
// Internal handlers for tools

View file

@ -69,7 +69,7 @@ internal suspend fun WebSocketServerSession.mcpWebSocketEndpoint(block: () -> Se
val server = block()
var session: ServerSession? = null
try {
session = server.connect(transport)
session = server.createSession(transport)
awaitCancellation()
} catch (e: CancellationException) {
session?.close()
@ -103,7 +103,7 @@ public fun Route.mcpWebSocket(options: ServerOptions? = null, handler: suspend S
)
public fun Route.mcpWebSocket(block: () -> Server) {
webSocket {
block().connect(createMcpTransport(this))
block().createSession(createMcpTransport(this))
}
}
@ -190,7 +190,7 @@ private suspend fun Route.createMcpServer(
),
)
server.connect(transport)
server.createSession(transport)
handler(server)
server.close()
}

View file

@ -24,7 +24,6 @@ kotlin {
}
jvmTest {
dependencies {
implementation(kotlin("test-junit5"))
implementation(libs.awaitility)
runtimeOnly(libs.slf4j.simple)
}

View file

@ -258,7 +258,7 @@ class ClientTest {
client.connect(clientTransport)
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
},
).joinAll()
@ -322,7 +322,7 @@ class ClientTest {
println("Client connected")
},
launch {
server.connect(serverTransport)
server.createSession(serverTransport)
println("Server connected")
},
).joinAll()
@ -379,7 +379,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()
@ -439,7 +439,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()
@ -502,7 +502,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()
@ -594,7 +594,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()
@ -680,7 +680,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()
@ -812,7 +812,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()
@ -859,7 +859,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()
@ -939,7 +939,7 @@ class ClientTest {
println("Client connected")
},
launch {
serverSessionResult.complete(server.connect(serverTransport))
serverSessionResult.complete(server.createSession(serverTransport))
println("Server connected")
},
).joinAll()

View file

@ -48,7 +48,7 @@ class SseTransportTest : BaseTransportTest() {
val actualPort = server.actualPort()
val client = HttpClient {
val transport = HttpClient {
install(ClientSSE)
}.mcpSseTransport {
url {
@ -58,7 +58,7 @@ class SseTransportTest : BaseTransportTest() {
}
try {
testClientOpenClose(client)
testTransportRead(transport)
} finally {
server.stopSuspend()
}
@ -76,7 +76,7 @@ class SseTransportTest : BaseTransportTest() {
val actualPort = server.actualPort()
val client = HttpClient {
val transport = HttpClient {
install(ClientSSE)
}.mcpSseTransport {
url {
@ -86,7 +86,7 @@ class SseTransportTest : BaseTransportTest() {
}
try {
testClientRead(client)
testTransportRead(transport)
} finally {
server.stopSuspend()
}
@ -104,7 +104,7 @@ class SseTransportTest : BaseTransportTest() {
val actualPort = server.actualPort()
val client = HttpClient {
val transport = HttpClient {
install(ClientSSE)
}.mcpSseTransport {
url {
@ -115,7 +115,7 @@ class SseTransportTest : BaseTransportTest() {
}
try {
testClientRead(client)
testTransportRead(transport)
} finally {
server.stopSuspend()
}

View file

@ -22,7 +22,7 @@ class WebSocketTransportTest : BaseTransportTest() {
install(io.ktor.client.plugins.websocket.WebSockets)
}.mcpWebSocketTransport()
testClientOpenClose(client)
testTransportOpenClose(client)
}
@Test
@ -41,11 +41,11 @@ class WebSocketTransportTest : BaseTransportTest() {
}
}
val client = createClient {
val transport = createClient {
install(io.ktor.client.plugins.websocket.WebSockets)
}.mcpWebSocketTransport()
testClientRead(client)
testTransportRead(transport)
clientFinished.complete(Unit)
}

View file

@ -1,199 +0,0 @@
package io.modelcontextprotocol.kotlin.sdk.integration
import io.ktor.client.HttpClient
import io.ktor.client.plugins.sse.SSE
import io.ktor.server.application.install
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.routing.routing
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
import io.modelcontextprotocol.kotlin.sdk.PromptMessage
import io.modelcontextprotocol.kotlin.sdk.Role
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.mcp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
import io.ktor.client.engine.cio.CIO as ClientCIO
import io.ktor.server.cio.CIO as ServerCIO
import io.ktor.server.sse.SSE as ServerSSE
class SseIntegrationTest {
@Test
@Ignore // Ignored because it doesn’t work with wasm/js in Ktor 3.2.3
fun `client should be able to connect to sse server`() = runTest(timeout = 5.seconds) {
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
var client: Client? = null
try {
withContext(Dispatchers.Default) {
server = initServer()
val port = server.engine.resolvedConnectors().first().port
client = initClient(serverPort = port)
}
} finally {
client?.close()
server?.stopSuspend(1000, 2000)
}
}
/**
* Test Case #1: One opened connection, a client gets a prompt
*
* 1. Open SSE from Client A.
* 2. Send a POST request from Client A to POST /prompts/get.
* 3. Observe that Client A receives a response related to it.
*/
@Test
@Ignore // Ignored because it doesn’t work with wasm/js in Ktor 3.2.3
fun `single sse connection`() = runTest(timeout = 5.seconds) {
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
var client: Client? = null
try {
withContext(Dispatchers.Default) {
server = initServer()
val port = server.engine.resolvedConnectors().first().port
client = initClient("Client A", port)
val promptA = getPrompt(client, "Client A")
assertTrue { "Client A" in promptA }
}
} finally {
client?.close()
server?.stopSuspend(1000, 2000)
}
}
/**
* Test Case #1: Two open connections, each client gets a client-specific prompt
*
* 1. Open SSE connection #1 from Client A and note the sessionId=<sessionId#1> value.
* 2. Open SSE connection #2 from Client B and note the sessionId=<sessionId#2> value.
* 3. Send a POST request to POST /message with the corresponding sessionId#1.
* 4. Observe that Client B (connection #2) receives a response related to sessionId#1.
*/
@Test
@Ignore // Ignored because it doesn’t work with wasm/js in Ktor 3.2.3
fun `multiple sse connections`() = runTest(timeout = 5.seconds) {
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
var clientA: Client? = null
var clientB: Client? = null
try {
withContext(Dispatchers.Default) {
server = initServer()
val port = server.engine.resolvedConnectors().first().port
clientA = initClient("Client A", port)
clientB = initClient("Client B", port)
// Step 3: Send a prompt request from Client A
val promptA = getPrompt(clientA, "Client A")
// Step 4: Send a prompt request from Client B
val promptB = getPrompt(clientB, "Client B")
assertTrue { "Client A" in promptA }
assertTrue { "Client B" in promptB }
}
} finally {
clientA?.close()
clientB?.close()
server?.stopSuspend(1000, 2000)
}
}
private suspend fun initClient(name: String = "", serverPort: Int): Client {
val client = Client(
Implementation(name = name, version = "1.0.0"),
)
val httpClient = HttpClient(ClientCIO) {
install(SSE)
}
// Create a transport wrapper that captures the session ID and received messages
val transport = httpClient.mcpSseTransport {
url {
host = URL
port = serverPort
}
}
client.connect(transport)
return client
}
private suspend fun initServer(): EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration> {
val server = Server(
Implementation(name = "sse-server", version = "1.0.0"),
ServerOptions(
capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)),
),
)
server.addPrompt(
name = "prompt",
description = "Prompt description",
arguments = listOf(
PromptArgument(
name = "client",
description = "Client name who requested a prompt",
required = true,
),
),
) { request ->
GetPromptResult(
"Prompt for ${request.name}",
messages = listOf(
PromptMessage(
role = Role.user,
content = TextContent("Prompt for client ${request.arguments?.get("client")}"),
),
),
)
}
val ktorServer = embeddedServer(ServerCIO, host = URL, port = PORT) {
install(ServerSSE)
routing {
mcp { server }
}
}
return ktorServer.startSuspend(wait = false)
}
/**
* Retrieves a prompt result using the provided client and client name.
*/
private suspend fun getPrompt(client: Client, clientName: String): String {
val response = client.getPrompt(
GetPromptRequest(
"prompt",
arguments = mapOf("client" to clientName),
),
)
return (response.messages.first().content as? TextContent)?.text
?: error("Failed to receive prompt for Client $clientName")
}
companion object {
private const val URL = "127.0.0.1"
private const val PORT = 0
}
}

View file

@ -11,23 +11,24 @@ import kotlin.test.assertTrue
import kotlin.test.fail
abstract class BaseTransportTest {
protected suspend fun testClientOpenClose(client: Transport) {
client.onError { error ->
protected suspend fun testTransportOpenClose(transport: Transport) {
transport.onError { error ->
fail("Unexpected error: $error")
}
var didClose = false
client.onClose { didClose = true }
transport.onClose { didClose = true }
client.start()
transport.start()
assertFalse(didClose, "Transport should not be closed immediately after start")
client.close()
transport.close()
assertTrue(didClose, "Transport should be closed after close() call")
}
protected suspend fun testClientRead(client: Transport) {
client.onError { error ->
protected suspend fun testTransportRead(transport: Transport) {
transport.onError { error ->
error.printStackTrace()
fail("Unexpected error: $error")
}
@ -40,23 +41,23 @@ abstract class BaseTransportTest {
val readMessages = mutableListOf<JSONRPCMessage>()
val finished = CompletableDeferred<Unit>()
client.onMessage { message ->
transport.onMessage { message ->
readMessages.add(message)
if (message == messages.last()) {
finished.complete(Unit)
}
}
client.start()
transport.start()
for (message in messages) {
client.send(message)
transport.send(message)
}
finished.await()
assertEquals(messages, readMessages, "Assert messages received")
client.close()
transport.close()
}
}

View file

@ -22,7 +22,7 @@ class StdioClientTransportTest : BaseTransportTest() {
output = output,
)
testClientOpenClose(client)
testTransportOpenClose(client)
process.destroy()
}
@ -40,7 +40,7 @@ class StdioClientTransportTest : BaseTransportTest() {
output = output,
)
testClientRead(client)
testTransportRead(client)
process.waitFor()
process.destroy()
@ -60,7 +60,7 @@ class StdioClientTransportTest : BaseTransportTest() {
output = output,
)
testClientRead(client)
testTransportRead(client)
process.waitFor()
process.destroy()

View file

@ -139,7 +139,7 @@ abstract class KotlinTestBase {
// Start server transport by connecting the server
runBlocking {
server.connect(serverTransport)
server.createSession(serverTransport)
}
}
}

View file

@ -0,0 +1,105 @@
package io.modelcontextprotocol.kotlin.sdk.integration.sse
import io.ktor.client.HttpClient
import io.ktor.client.plugins.sse.SSE
import io.ktor.server.application.install
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.routing.routing
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
import io.modelcontextprotocol.kotlin.sdk.PromptMessage
import io.modelcontextprotocol.kotlin.sdk.Role
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.mcp
import io.ktor.client.engine.cio.CIO as ClientCIO
import io.ktor.server.cio.CIO as ServerCIO
import io.ktor.server.sse.SSE as ServerSSE
open class AbstractSseIntegrationTest {
suspend fun EmbeddedServer<*, *>.actualPort() = engine.resolvedConnectors().single().port
suspend fun initTestClient(serverPort: Int, name: String? = null): Client {
val client = Client(
Implementation(name = name ?: DEFAULT_CLIENT_NAME, version = VERSION),
)
val httpClient = HttpClient(ClientCIO) {
install(SSE)
}
// Create a transport wrapper that captures the session ID and received messages
val transport = httpClient.mcpSseTransport {
url {
host = URL
port = serverPort
}
}
client.connect(transport)
return client
}
suspend fun initTestServer(
name: String? = null,
): EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration> {
val server = Server(
Implementation(name = name ?: DEFAULT_SERVER_NAME, version = VERSION),
ServerOptions(
capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)),
),
) {
addPrompt(
name = "prompt",
description = "Prompt description",
arguments = listOf(
PromptArgument(
name = "client",
description = "Client name who requested a prompt",
required = true,
),
),
) { request ->
GetPromptResult(
"Prompt for ${request.name}",
messages = listOf(
PromptMessage(
role = Role.user,
content = TextContent("Prompt for client ${request.arguments?.get("client")}"),
),
),
)
}
}
val ktorServer = embeddedServer(
ServerCIO,
host = URL,
port = PORT,
) {
install(ServerSSE)
routing {
mcp { server }
}
}
return ktorServer.startSuspend(wait = false)
}
companion object {
private const val DEFAULT_CLIENT_NAME = "sse-test-client"
private const val DEFAULT_SERVER_NAME = "sse-test-server"
private const val VERSION = "1.0.0"
private const val URL = "127.0.0.1"
private const val PORT = 0
}
}

View file

@ -0,0 +1,111 @@
package io.modelcontextprotocol.kotlin.sdk.integration.sse
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.EmbeddedServer
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.client.Client
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlin.test.Test
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SSeIntegrationTest : AbstractSseIntegrationTest() {
@Test
fun `client should be able to connect to sse server`() = runTest(timeout = 5.seconds) {
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
var client: Client? = null
try {
withContext(Dispatchers.Default) {
server = initTestServer()
val port = server.engine.resolvedConnectors().single().port
client = initTestClient(serverPort = port)
}
} finally {
client?.close()
server?.stopSuspend(1000, 2000)
}
}
/**
* Test Case #1: One opened connection, a client gets a prompt
*
* 1. Open SSE from Client A.
* 2. Send a POST request from Client A to POST /prompts/get.
* 3. Observe that Client A receives a response related to it.
*/
@Test
fun `single sse connection`() = runTest(timeout = 5.seconds) {
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
var client: Client? = null
try {
withContext(Dispatchers.Default) {
server = initTestServer()
val port = server.engine.resolvedConnectors().single().port
client = initTestClient(port, "Client A")
val promptA = getPrompt(client, "Client A")
assertTrue { "Client A" in promptA }
}
} finally {
client?.close()
server?.stopSuspend(1000, 2000)
}
}
/**
* Test Case #1: Two open connections, each client gets a client-specific prompt
*
* 1. Open SSE connection #1 from Client A and note the sessionId=<sessionId#1> value.
* 2. Open SSE connection #2 from Client B and note the sessionId=<sessionId#2> value.
* 3. Send a POST request to POST /message with the corresponding sessionId#1.
* 4. Observe that Client B (connection #2) receives a response related to sessionId#1.
*/
@Test
fun `multiple sse connections`() = runTest(timeout = 5.seconds) {
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
var clientA: Client? = null
var clientB: Client? = null
try {
withContext(Dispatchers.Default) {
server = initTestServer()
val port = server.engine.resolvedConnectors().first().port
clientA = initTestClient(port, "Client A")
clientB = initTestClient(port, "Client B")
// Step 3: Send a prompt request from Client A
val promptA = getPrompt(clientA, "Client A")
// Step 4: Send a prompt request from Client B
val promptB = getPrompt(clientB, "Client B")
assertTrue { "Client A" in promptA }
assertTrue { "Client B" in promptB }
}
} finally {
clientA?.close()
clientB?.close()
server?.stopSuspend(1000, 2000)
}
}
/**
* Retrieves a prompt result using the provided client and client name.
*/
private suspend fun getPrompt(client: Client, clientName: String): String {
val response = client.getPrompt(
GetPromptRequest(
"prompt",
arguments = mapOf("client" to clientName),
),
)
return (response.messages.first().content as? TextContent)?.text
?: error("Failed to receive prompt for Client $clientName")
}
}

View file

@ -412,7 +412,7 @@ abstract class TsTestBase {
// Connect server in a background thread to avoid blocking
val serverThread = Thread {
try {
kotlinx.coroutines.runBlocking { server.connect(transport) }
kotlinx.coroutines.runBlocking { server.createSession(transport) }
} catch (e: Exception) {
println("[STDIO-SERVER] Error connecting: ${e.message}")
}

View file

@ -53,7 +53,6 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonPrimitive
import org.awaitility.Awaitility.await
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
@ -134,7 +133,7 @@ class KotlinServerForTsClient {
val serverThread = Thread {
runBlocking {
mcpServer.connect(transport)
mcpServer.createSession(transport)
}
}
serverThread.start()

View file

@ -53,7 +53,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Remove the tool
val result = server.removeTool("test-tool")
@ -90,7 +90,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Try to remove a non-existent tool
val result = server.removeTool("non-existent-tool")
@ -147,7 +147,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Remove the tools
val result = server.removeTools(listOf("test-tool-1", "test-tool-2"))
@ -186,7 +186,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Remove the prompt
val result = server.removePrompt(testPrompt.name)
@ -232,7 +232,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Remove the prompts
val result = server.removePrompts(listOf(testPrompt1.name, testPrompt2.name))
@ -281,7 +281,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Remove the resource
val result = server.removeResource(testResourceUri)
@ -347,7 +347,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Remove the resources
val result = server.removeResources(listOf(testResourceUri1, testResourceUri2))
@ -384,7 +384,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Try to remove a non-existent prompt
val result = server.removePrompt("non-existent-prompt")
@ -442,7 +442,7 @@ class ServerTest {
// Connect client and server
launch { client.connect(clientTransport) }
launch { server.connect(serverTransport) }
launch { server.createSession(serverTransport) }
// Try to remove a non-existent resource
val result = server.removeResource("non-existent-resource")
@ -486,7 +486,7 @@ class ServerTest {
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
server.connect(serverTransport)
server.createSession(serverTransport)
client.connect(clientTransport)
assertEquals(instructions, client.serverInstructions)
@ -505,7 +505,7 @@ class ServerTest {
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
server.connect(serverTransport)
server.createSession(serverTransport)
client.connect(clientTransport)
assertEquals(instructions, client.serverInstructions)
@ -522,7 +522,7 @@ class ServerTest {
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
val client = Client(clientInfo = Implementation(name = "test client", version = "1.0"))
server.connect(serverTransport)
server.createSession(serverTransport)
client.connect(clientTransport)
assertNull(client.serverInstructions)