diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt index e0ccd39b..df420728 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt @@ -12,12 +12,12 @@ import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry +import io.modelcontextprotocol.kotlin.sdk.integration.utils.port import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcp import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout -import org.awaitility.kotlin.await import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import kotlin.time.Duration.Companion.seconds @@ -40,12 +40,7 @@ abstract class KotlinTestBase { @BeforeEach fun setUp() { setupServer() - await - .ignoreExceptions() - .until { - port = runBlocking { serverEngine.engine.resolvedConnectors().first().port } - port != 0 - } + port = serverEngine.port() runBlocking { setupClient() } @@ -74,7 +69,7 @@ abstract class KotlinTestBase { configureServer() - serverEngine = embeddedServer(ServerCIO, host = host, port = port) { + serverEngine = embeddedServer(ServerCIO, host = host, port = 0) { install(ServerSSE) routing { mcp { server } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinServerForTypeScriptClient.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinServerForTypeScriptClient.kt index 5757fcbc..9128e987 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinServerForTypeScriptClient.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinServerForTypeScriptClient.kt @@ -37,6 +37,7 @@ import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.TextResourceContents import io.modelcontextprotocol.kotlin.sdk.Tool +import io.modelcontextprotocol.kotlin.sdk.integration.utils.port import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport @@ -65,10 +66,10 @@ class KotlinServerForTypeScriptClient { private val jsonFormat = Json { ignoreUnknownKeys = true } private var server: EmbeddedServer<*, *>? = null - fun start(port: Int = 3000) { - logger.info { "Starting HTTP server on port $port" } + fun start(): Int { + logger.info { "Starting HTTP server on random port" } - server = embeddedServer(CIO, port = port) { + server = embeddedServer(CIO, port = 0) { routing { get("/mcp") { val sessionId = call.request.header("mcp-session-id") @@ -186,7 +187,9 @@ class KotlinServerForTypeScriptClient { } } - server?.start(wait = false) + val theServer = requireNotNull(server) { "Server must be created" } + theServer.start(wait = false) + return theServer.port() } fun stop() { @@ -357,6 +360,8 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { write("\n\n") flush() } + } catch (e: CancellationException) { + logger.info(e) { e.message } } catch (e: Exception) { logger.warn(e) { "SSE stream terminated for session: $sessionId" } } finally { diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt index d25dbebb..03d496e4 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript import kotlinx.coroutines.test.runTest +import org.awaitility.kotlin.await import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -8,23 +9,20 @@ import org.junit.jupiter.api.Timeout import java.util.concurrent.TimeUnit import kotlin.test.Ignore import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { - private var port: Int = 0 private lateinit var serverUrl: String private var httpServer: KotlinServerForTypeScriptClient? = null @BeforeEach fun setUp() { - port = findFreePort() - serverUrl = "http://localhost:$port/mcp" - killProcessOnPort(port) httpServer = KotlinServerForTypeScriptClient() - httpServer?.start(port) - if (!waitForPort(port = port)) { - throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") - } + val port = httpServer!!.start() + serverUrl = "http://localhost:$port/mcp" println("Kotlin server started on port $port") } @@ -145,10 +143,14 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { } threads.add(thread) thread.start() - Thread.sleep(500) } - threads.forEach { it.join() } + await + .pollInterval(100.milliseconds.toJavaDuration()) + .atMost(30.seconds.toJavaDuration()) + .until { + outputs.size == clientCount + } if (exceptions.isNotEmpty()) { println( diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt index 6504b49e..917871a6 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -20,14 +20,9 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @BeforeEach fun setUp() { - port = findFreePort() - serverUrl = "http://localhost:$port/mcp" - killProcessOnPort(port) httpServer = KotlinServerForTypeScriptClient() - httpServer?.start(port) - if (!waitForPort(port = port)) { - throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") - } + port = httpServer!!.start() + serverUrl = "http://localhost:$port/mcp" println("Kotlin server started on port $port") } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt index a19f00ec..d62171b2 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt @@ -109,7 +109,7 @@ abstract class TypeScriptTestBase { .redirectErrorStream(true) .start() - val output = StringBuilder() + val output = StringBuffer() BufferedReader(InputStreamReader(process.inputStream)).use { reader -> var line: String? while (reader.readLine().also { line = it } != null) { diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/EmbeddedServerExtensions.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/EmbeddedServerExtensions.kt new file mode 100644 index 00000000..51e15de8 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/EmbeddedServerExtensions.kt @@ -0,0 +1,17 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.utils + +import io.ktor.server.engine.EmbeddedServer +import kotlinx.coroutines.runBlocking +import org.awaitility.kotlin.await + +internal fun EmbeddedServer<*, *>.port(): Int { + var port = 0 + val server = this + await + .ignoreExceptions() + .until { + port = runBlocking { server.engine.resolvedConnectors().first().port } + port != 0 + } + return port +}