From 3cf02db38f8d10106ea28c3bf57ae84ba03c64ed Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 15 Sep 2025 18:56:32 +0200 Subject: [PATCH 01/18] Mark `testMultipleClientParallel` as ignored due to flakiness (#267) Ignore flaky `testMultipleClientParallel` --- .../TypeScriptClientKotlinServerTest.kt | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt 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 new file mode 100644 index 00000000..d25dbebb --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -0,0 +1,191 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.util.concurrent.TimeUnit +import kotlin.test.Ignore +import kotlin.test.assertTrue + +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") + } + println("Kotlin server started on port $port") + } + + @AfterEach + fun tearDown() { + try { + httpServer?.stop() + println("HTTP server stopped") + } catch (e: Exception) { + println("Error during server shutdown: ${e.message}") + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testToolCall() = runTest { + val testName = "TestUser" + val command = "npx tsx myClient.ts $serverUrl greet $testName" + val output = executeCommand(command, tsClientDir) + + assertTrue( + output.contains("Hello, $testName!"), + "Tool response should contain the greeting with the provided name", + ) + assertTrue(output.contains("Tool result:"), "Output should indicate a successful tool call") + assertTrue(output.contains("Text content:"), "Output should contain the text content section") + assertTrue(output.contains("Structured content:"), "Output should contain the structured content section") + assertTrue( + output.contains("\"greeting\": \"Hello, $testName!\""), + "Structured content should contain the greeting", + ) + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testNotifications() = runTest { + val name = "NotifUser" + val command = "npx tsx myClient.ts $serverUrl multi-greet $name" + val output = executeCommand(command, tsClientDir) + + assertTrue( + output.contains("Multiple greetings") || output.contains("greeting"), + "Tool response should contain greeting message", + ) + // verify that the server sent 3 notifications + assertTrue( + output.contains("\"notificationCount\": 3") || output.contains("notificationCount: 3"), + "Structured content should indicate that 3 notifications were emitted by the server.\nOutput:\n$output", + ) + } + + @Test + @Timeout(120, unit = TimeUnit.SECONDS) + fun testMultipleClientSequence() = runTest { + val testName1 = "FirstClient" + val command1 = "npx tsx myClient.ts $serverUrl greet $testName1" + val output1 = executeCommand(command1, tsClientDir) + + assertTrue(output1.contains("Connected to server"), "First client should connect to server") + assertTrue(output1.contains("Hello, $testName1!"), "Tool response should contain the greeting for first client") + assertTrue(output1.contains("Disconnected from server"), "First client should disconnect cleanly") + + val testName2 = "SecondClient" + val command2 = "npx tsx myClient.ts $serverUrl multi-greet $testName2" + val output2 = executeCommand(command2, tsClientDir) + + assertTrue(output2.contains("Connected to server"), "Second client should connect to server") + assertTrue( + output2.contains("Multiple greetings") || output2.contains("greeting"), + "Tool response should contain greeting message", + ) + assertTrue(output2.contains("Disconnected from server"), "Second client should disconnect cleanly") + + val command3 = "npx tsx myClient.ts $serverUrl" + val output3 = executeCommand(command3, tsClientDir) + + assertTrue(output3.contains("Connected to server"), "Third client should connect to server") + assertTrue(output3.contains("Available utils:"), "Third client should list available utils") + assertTrue(output3.contains("greet"), "Greet tool should be available to third client") + assertTrue(output3.contains("multi-greet"), "Multi-greet tool should be available to third client") + assertTrue(output3.contains("Disconnected from server"), "Third client should disconnect cleanly") + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + @Ignore // Ignored due to flaky, see issue https://github.com/modelcontextprotocol/kotlin-sdk/issues/262 + fun testMultipleClientParallel() = runTest { + val clientCount = 3 + val clients = listOf( + "FirstClient" to "greet", + "SecondClient" to "multi-greet", + "ThirdClient" to "", + ) + + val threads = mutableListOf() + val outputs = mutableListOf>() + val exceptions = mutableListOf() + + for (i in 0 until clientCount) { + val (clientName, toolName) = clients[i] + val thread = Thread { + try { + val command = if (toolName.isEmpty()) { + "npx tsx myClient.ts $serverUrl" + } else { + "npx tsx myClient.ts $serverUrl $toolName $clientName" + } + + val output = executeCommand(command, tsClientDir) + synchronized(outputs) { + outputs.add(i to output) + } + } catch (e: Exception) { + synchronized(exceptions) { + exceptions.add(e) + } + } + } + threads.add(thread) + thread.start() + Thread.sleep(500) + } + + threads.forEach { it.join() } + + if (exceptions.isNotEmpty()) { + println( + "Exceptions occurred in parallel clients: ${ + exceptions.joinToString { + it.message ?: it.toString() + } + }", + ) + } + + val sortedOutputs = outputs.sortedBy { it.first }.map { it.second } + + sortedOutputs.forEachIndexed { index, output -> + val clientName = clients[index].first + val toolName = clients[index].second + + when (toolName) { + "greet" -> { + val containsGreeting = output.contains("Hello, $clientName!") || + output.contains("\"greeting\": \"Hello, $clientName!\"") + assertTrue( + containsGreeting, + "Tool response should contain the greeting for $clientName", + ) + } + + "multi-greet" -> { + val containsGreeting = output.contains("Multiple greetings") || + output.contains("greeting") || + output.contains("greet") + assertTrue( + containsGreeting, + "Tool response should contain greeting message for $clientName", + ) + } + } + } + } +} From 82d5faf95449862150886f316180fc077dfd8132 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Tue, 16 Sep 2025 20:38:38 +0300 Subject: [PATCH 02/18] fixup! Add Stdio coverage for integration tests --- .../TypeScriptClientKotlinServerTest.kt | 191 ------------------ 1 file changed, 191 deletions(-) delete mode 100644 kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt 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 deleted file mode 100644 index d25dbebb..00000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ /dev/null @@ -1,191 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript - -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit -import kotlin.test.Ignore -import kotlin.test.assertTrue - -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") - } - println("Kotlin server started on port $port") - } - - @AfterEach - fun tearDown() { - try { - httpServer?.stop() - println("HTTP server stopped") - } catch (e: Exception) { - println("Error during server shutdown: ${e.message}") - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testToolCall() = runTest { - val testName = "TestUser" - val command = "npx tsx myClient.ts $serverUrl greet $testName" - val output = executeCommand(command, tsClientDir) - - assertTrue( - output.contains("Hello, $testName!"), - "Tool response should contain the greeting with the provided name", - ) - assertTrue(output.contains("Tool result:"), "Output should indicate a successful tool call") - assertTrue(output.contains("Text content:"), "Output should contain the text content section") - assertTrue(output.contains("Structured content:"), "Output should contain the structured content section") - assertTrue( - output.contains("\"greeting\": \"Hello, $testName!\""), - "Structured content should contain the greeting", - ) - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testNotifications() = runTest { - val name = "NotifUser" - val command = "npx tsx myClient.ts $serverUrl multi-greet $name" - val output = executeCommand(command, tsClientDir) - - assertTrue( - output.contains("Multiple greetings") || output.contains("greeting"), - "Tool response should contain greeting message", - ) - // verify that the server sent 3 notifications - assertTrue( - output.contains("\"notificationCount\": 3") || output.contains("notificationCount: 3"), - "Structured content should indicate that 3 notifications were emitted by the server.\nOutput:\n$output", - ) - } - - @Test - @Timeout(120, unit = TimeUnit.SECONDS) - fun testMultipleClientSequence() = runTest { - val testName1 = "FirstClient" - val command1 = "npx tsx myClient.ts $serverUrl greet $testName1" - val output1 = executeCommand(command1, tsClientDir) - - assertTrue(output1.contains("Connected to server"), "First client should connect to server") - assertTrue(output1.contains("Hello, $testName1!"), "Tool response should contain the greeting for first client") - assertTrue(output1.contains("Disconnected from server"), "First client should disconnect cleanly") - - val testName2 = "SecondClient" - val command2 = "npx tsx myClient.ts $serverUrl multi-greet $testName2" - val output2 = executeCommand(command2, tsClientDir) - - assertTrue(output2.contains("Connected to server"), "Second client should connect to server") - assertTrue( - output2.contains("Multiple greetings") || output2.contains("greeting"), - "Tool response should contain greeting message", - ) - assertTrue(output2.contains("Disconnected from server"), "Second client should disconnect cleanly") - - val command3 = "npx tsx myClient.ts $serverUrl" - val output3 = executeCommand(command3, tsClientDir) - - assertTrue(output3.contains("Connected to server"), "Third client should connect to server") - assertTrue(output3.contains("Available utils:"), "Third client should list available utils") - assertTrue(output3.contains("greet"), "Greet tool should be available to third client") - assertTrue(output3.contains("multi-greet"), "Multi-greet tool should be available to third client") - assertTrue(output3.contains("Disconnected from server"), "Third client should disconnect cleanly") - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - @Ignore // Ignored due to flaky, see issue https://github.com/modelcontextprotocol/kotlin-sdk/issues/262 - fun testMultipleClientParallel() = runTest { - val clientCount = 3 - val clients = listOf( - "FirstClient" to "greet", - "SecondClient" to "multi-greet", - "ThirdClient" to "", - ) - - val threads = mutableListOf() - val outputs = mutableListOf>() - val exceptions = mutableListOf() - - for (i in 0 until clientCount) { - val (clientName, toolName) = clients[i] - val thread = Thread { - try { - val command = if (toolName.isEmpty()) { - "npx tsx myClient.ts $serverUrl" - } else { - "npx tsx myClient.ts $serverUrl $toolName $clientName" - } - - val output = executeCommand(command, tsClientDir) - synchronized(outputs) { - outputs.add(i to output) - } - } catch (e: Exception) { - synchronized(exceptions) { - exceptions.add(e) - } - } - } - threads.add(thread) - thread.start() - Thread.sleep(500) - } - - threads.forEach { it.join() } - - if (exceptions.isNotEmpty()) { - println( - "Exceptions occurred in parallel clients: ${ - exceptions.joinToString { - it.message ?: it.toString() - } - }", - ) - } - - val sortedOutputs = outputs.sortedBy { it.first }.map { it.second } - - sortedOutputs.forEachIndexed { index, output -> - val clientName = clients[index].first - val toolName = clients[index].second - - when (toolName) { - "greet" -> { - val containsGreeting = output.contains("Hello, $clientName!") || - output.contains("\"greeting\": \"Hello, $clientName!\"") - assertTrue( - containsGreeting, - "Tool response should contain the greeting for $clientName", - ) - } - - "multi-greet" -> { - val containsGreeting = output.contains("Multiple greetings") || - output.contains("greeting") || - output.contains("greet") - assertTrue( - containsGreeting, - "Tool response should contain greeting message for $clientName", - ) - } - } - } - } -} From 6d0969d9ed8823f19c5e6769b26b87179f20c165 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Thu, 9 Oct 2025 16:07:19 +0300 Subject: [PATCH 03/18] Refactor integration tests to use Testcontainers for TypeScript client and server --- gradle/libs.versions.toml | 2 + kotlin-sdk-test/build.gradle.kts | 1 + .../sdk/integration/typescript/TsTestBase.kt | 572 ++++++++---------- .../KotlinClientTsServerEdgeCasesTestSse.kt | 6 +- .../sse/KotlinClientTsServerTestSse.kt | 6 +- .../sse/KotlinServerForTsClientSse.kt | 1 - .../sse/TsClientKotlinServerTestSse.kt | 12 +- .../typescript/sse/TsEdgeCasesTestSse.kt | 32 +- .../typescript/stdio/simpleStdio.ts | 4 +- .../kotlin/sdk/integration/utils/Dockerfile | 18 + 10 files changed, 287 insertions(+), 367 deletions(-) create mode 100644 kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bb80c86..bbdd8439 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ logging = "7.0.13" slf4j = "2.0.17" kotest = "6.0.3" awaitility = "4.3.0" +testcontainers = "1.21.3" # Samples mcp-kotlin = "0.7.2" @@ -54,6 +55,7 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +testcontainers = { group = "org.testcontainers", name = "testcontainers", version.ref = "testcontainers" } # Samples ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts index 012619b9..82df0f1a 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/kotlin-sdk-test/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { dependencies { implementation(kotlin("test-junit5")) implementation(libs.awaitility) + implementation(libs.testcontainers) runtimeOnly(libs.slf4j.simple) } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index 801f5820..3ef631c6 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -2,453 +2,353 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.get import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp import io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse.KotlinServerForTsClient import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry -import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout -import kotlinx.io.Sink -import kotlinx.io.Source import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered import org.awaitility.kotlin.await -import org.junit.jupiter.api.BeforeAll -import java.io.BufferedReader -import java.io.File -import java.io.InputStreamReader +import org.testcontainers.containers.BindMode +import org.testcontainers.containers.GenericContainer +import org.testcontainers.images.PullPolicy +import java.io.ByteArrayInputStream +import java.io.OutputStream import java.net.ServerSocket import java.net.Socket import java.util.concurrent.TimeUnit -import kotlin.io.path.createTempDirectory import kotlin.time.Duration.Companion.seconds enum class TransportKind { SSE, STDIO, DEFAULT } @Retry(times = 3) abstract class TsTestBase { - protected open val transportKind: TransportKind = TransportKind.DEFAULT - protected val projectRoot: File get() = File(System.getProperty("user.dir")) - protected val tsClientDir: File - get() { - val base = File( - projectRoot, - "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript", - ) - - // Allow override via system property for CI: -Dts.transport=stdio|sse - val fromProp = System.getProperty("ts.transport")?.lowercase() - val overrideSubDir = when (fromProp) { - "stdio" -> "stdio" - "sse" -> "sse" - else -> null - } - - val subDirName = overrideSubDir ?: when (transportKind) { - TransportKind.STDIO -> "stdio" - TransportKind.SSE -> "sse" - TransportKind.DEFAULT -> null - } - if (subDirName != null) { - val sub = File(base, subDirName) - if (sub.exists()) return sub - } - return base - } - companion object { - @JvmStatic - private val tempRootDir: File = createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() } - - @JvmStatic - protected val sdkDir: File = File(tempRootDir, "typescript-sdk") - - @JvmStatic - @BeforeAll - fun setupTypeScriptSdk() { - println("Cloning TypeScript SDK repository") - - if (!sdkDir.exists()) { - val process = ProcessBuilder( - "git", - "clone", - "--depth", - "1", - "https://github.com/modelcontextprotocol/typescript-sdk.git", - sdkDir.absolutePath, - ) - .redirectErrorStream(true) - .start() - val exitCode = process.waitFor() - if (exitCode != 0) { - throw RuntimeException("Failed to clone TypeScript SDK repository: exit code $exitCode") - } - } + private val isWindows = System.getProperty("os.name").lowercase().contains("windows") - println("Installing TypeScript SDK dependencies") - executeCommand("npm install", sdkDir, allowFailure = false, timeoutSeconds = null) - } + private fun tsDockerImage() = System.getenv("TS_SDK_IMAGE") + ?: throw IllegalStateException("TS_SDK_IMAGE environment variable is not set") - @JvmStatic - protected fun killProcessOnPort(port: Int) { - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val killCommand = if (isWindows) { - "netstat -ano | findstr :$port | for /f \"tokens=5\" %a in ('more')" + - " do taskkill /F /PID %a 2>nul || echo No process found" - } else { - "lsof -ti:$port | xargs kill -9 2>/dev/null || true" - } - executeCommand(killCommand, File("."), allowFailure = true, timeoutSeconds = null) + private fun getTsFilesPath(subdir: String): String { + val userDir = System.getProperty("user.dir") + return "$userDir/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/$subdir" } - @JvmStatic - protected fun findFreePort(): Int { - ServerSocket(0).use { socket -> - return socket.localPort - } - } + fun findFreePort() = ServerSocket(0).use { it.localPort } - @JvmStatic - protected fun executeCommand( - command: String, - workingDir: File, - allowFailure: Boolean = false, - timeoutSeconds: Long? = null, - ): String { - if (!workingDir.exists()) { - if (!workingDir.mkdirs()) { - throw RuntimeException("Failed to create working directory: ${workingDir.absolutePath}") - } - } - - if (!workingDir.isDirectory || !workingDir.canRead()) { - throw RuntimeException("Working directory is not accessible: ${workingDir.absolutePath}") - } - - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val processBuilder = if (isWindows) { - ProcessBuilder() - .command("cmd.exe", "/c", "set TYPESCRIPT_SDK_DIR=${sdkDir.absolutePath} && $command") + fun killProcessOnPort(port: Int) { + val command = if (isWindows) { + listOf( + "cmd.exe", + "/c", + "netstat -ano | findstr :$port | for /f \"tokens=5\" %a in ('more') do taskkill /F /PID %a 2>nul || echo No process found", + ) } else { - ProcessBuilder() - .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") + listOf("bash", "-c", "lsof -ti:$port | xargs kill -9 2>/dev/null || true") } - val process = processBuilder - .directory(workingDir) + val process = ProcessBuilder(command) .redirectErrorStream(true) .start() - val output = StringBuilder() - BufferedReader(InputStreamReader(process.inputStream)).use { reader -> - var line: String? - while (reader.readLine().also { line = it } != null) { - println(line) - output.append(line).append("\n") - } - } - - if (timeoutSeconds == null) { - val exitCode = process.waitFor() - if (!allowFailure && exitCode != 0) { - throw RuntimeException( - "Command execution failed with exit code $exitCode: $command\n" + - "Working dir: ${workingDir.absolutePath}\nOutput:\n$output", - ) - } - } else { - process.waitFor(timeoutSeconds, TimeUnit.SECONDS) - } - - return output.toString() - } - } - - private fun waitForProcessTermination(process: Process, timeoutSeconds: Long): Boolean { - if (process.isAlive && !process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { - process.destroyForcibly() - process.waitFor(2, TimeUnit.SECONDS) - return false + process.inputStream.bufferedReader().use { it.readText() } + process.waitFor() } - return true - } - private fun createProcessOutputReader(process: Process, prefix: String = "TS-SERVER"): Thread { - val outputReader = Thread { - try { - process.inputStream.bufferedReader().useLines { lines -> - for (line in lines) { - println("[$prefix] $line") - } + fun runDockerCommand( + image: String = tsDockerImage(), + command: List, + interactive: Boolean = true, + environmentVariables: Map = emptyMap(), + extraArgs: List = emptyList(), + allowFailure: Boolean = false, + ): Process { + val dockerArgs = buildList { + add("docker") + add("run") + add("--rm") + if (interactive) add("-i") + add("-v") + add("${getTsFilesPath("sse")}:/app/sse") + add("-v") + add("${getTsFilesPath("stdio")}:/app/stdio") + addAll(extraArgs) + environmentVariables.forEach { (key, value) -> + add("-e") + add("$key=$value") } - } catch (e: Exception) { - println("Warning: Error reading process output: ${e.message}") + add(image) + addAll(command) } + + return ProcessBuilder(dockerArgs) + .redirectErrorStream(false) + .start() + .also { if (!allowFailure) it.startErrorLogger() } } - outputReader.isDaemon = true - return outputReader - } - private fun createProcessErrorReader(process: Process, prefix: String = "TS-SERVER"): Thread { - val errorReader = Thread { - try { - process.errorStream.bufferedReader().useLines { lines -> - for (line in lines) { - println("[$prefix][err] $line") - } - } - } catch (e: Exception) { - println("Warning: Error reading process error stream: ${e.message}") + private fun Process.startErrorLogger() = Thread { + errorStream.bufferedReader().useLines { lines -> + lines.forEach { println("[ERR] $it") } } + }.apply { + isDaemon = true + start() } - errorReader.isDaemon = true - return errorReader } - protected fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10): Boolean = try { + // ===== Wait utilities ===== + protected fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10) = runCatching { await.atMost(timeoutSeconds, TimeUnit.SECONDS) .pollDelay(200, TimeUnit.MILLISECONDS) .pollInterval(100, TimeUnit.MILLISECONDS) .until { - try { - Socket(host, port).use { true } - } catch (_: Exception) { - false - } + runCatching { Socket(host, port).close() }.isSuccess } true - } catch (_: Exception) { - false - } - - protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String = - executeCommand(command, workingDir, allowFailure = true, timeoutSeconds = timeoutSeconds) - - protected fun startTypeScriptServer(port: Int): Process { - killProcessOnPort(port) + }.getOrDefault(false) - if (!sdkDir.exists() || !sdkDir.isDirectory) { - throw IllegalStateException( - "TypeScript SDK directory does not exist or is not accessible: ${sdkDir.absolutePath}", - ) + protected fun waitForHttpReady(url: String, timeoutSeconds: Long = 10) = runCatching { + await.atMost(timeoutSeconds, TimeUnit.SECONDS) + .pollDelay(200, TimeUnit.MILLISECONDS) + .pollInterval(150, TimeUnit.MILLISECONDS) + .until { + runCatching { + HttpClient(CIO) { + install(HttpTimeout) { + requestTimeoutMillis = 1000 + connectTimeoutMillis = 1000 + socketTimeoutMillis = 1000 + } + followRedirects = false + }.use { client -> + runBlocking { client.get(url).status.value in 200..499 } + } + }.getOrDefault(false) + } + true + }.getOrDefault(false) + + // ===== TypeScript Server (SSE/HTTP) ===== + protected fun startTypeScriptServer(port: Int): ContainerProcess { + val container = GenericContainer(tsDockerImage()).apply { + withImagePullPolicy(PullPolicy.defaultPolicy()) + withExposedPorts(port) + mapOf( + "MCP_HOST" to "0.0.0.0", + "MCP_PORT" to port.toString(), + "MCP_PATH" to "/mcp", + ).forEach { (k, v) -> withEnv(k, v) } + withFileSystemBind(getTsFilesPath("sse"), "/app/sse", BindMode.READ_ONLY) + withFileSystemBind(getTsFilesPath("stdio"), "/app/stdio", BindMode.READ_ONLY) + withCommand("npx", "--prefix", "/opt/typescript-sdk", "tsx", "/app/sse/simpleStreamableHttp.ts") + withReuse(false) } - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val localServerPath = File(tsClientDir, "simpleStreamableHttp.ts").absolutePath - val processBuilder = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - "set MCP_PORT=$port && set NODE_PATH=${sdkDir.absolutePath}\\node_modules && npx --prefix \"${sdkDir.absolutePath}\" tsx \"$localServerPath\"", - ) - } else { - ProcessBuilder() - .command( - "bash", - "-c", - "MCP_PORT=$port NODE_PATH='${sdkDir.absolutePath}/node_modules' npx --prefix '${sdkDir.absolutePath}' tsx \"$localServerPath\"", - ) + runCatching { container.start() }.onFailure { + runCatching { container.stop() } + throw it } - processBuilder.environment()["TYPESCRIPT_SDK_DIR"] = sdkDir.absolutePath + val host = container.host + val mappedPort = container.getMappedPort(port) + require(waitForPort(host, mappedPort, 60)) { + runCatching { container.stop() } + "TypeScript server did not become ready on $host:$mappedPort" + } + require(waitForHttpReady("http://$host:$mappedPort/mcp")) { + runCatching { container.stop() } + "TypeScript server HTTP endpoint /mcp not ready on $host:$mappedPort" + } - val process = processBuilder - .directory(tsClientDir) - .redirectErrorStream(true) - .start() + println("TypeScript server started on $host:$mappedPort (container port: $port)") + Thread.sleep(300) - createProcessOutputReader(process).start() + return ContainerProcess(container, mappedPort) + } - if (!waitForPort(port = port, timeoutSeconds = 20)) { - throw IllegalStateException("TypeScript server did not become ready on localhost:$port within timeout") + protected class ContainerProcess(private val container: GenericContainer<*>, val mappedPort: Int) : Process() { + override fun destroy() = runCatching { container.stop() }.let { } + override fun destroyForcibly() = apply { destroy() } + override fun exitValue() = if (container.isRunning) throw IllegalThreadStateException() else 0 + override fun isAlive() = container.isRunning + override fun waitFor(): Int { + while (container.isRunning) { + runCatching { Thread.sleep(50) } + } + return 0 + } + override fun getInputStream() = ByteArrayInputStream(ByteArray(0)) + override fun getErrorStream() = ByteArrayInputStream(ByteArray(0)) + override fun getOutputStream() = object : OutputStream() { + override fun write(b: Int) {} } - return process } - protected fun stopProcess(process: Process, waitSeconds: Long = 3, name: String = "TypeScript server") { + protected fun stopProcess(process: Process, waitSeconds: Long = 3, name: String = "Process") { process.destroy() - if (waitForProcessTermination(process, waitSeconds)) { - println("$name stopped gracefully") - } else { - println("$name did not stop gracefully, forced termination") - } + val stopped = process.waitFor(waitSeconds, TimeUnit.SECONDS) + if (!stopped) process.destroyForcibly() + println("$name stopped ${if (stopped) "gracefully" else "forcibly"}") } - // ===== SSE client helpers ===== - protected suspend fun newClient(serverUrl: String): Client = - HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl) + // ===== SSE Client ===== + protected suspend fun newClient(serverUrl: String) = HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl) protected suspend fun withClient(serverUrl: String, block: suspend (Client) -> T): T { val client = newClient(serverUrl) return try { withTimeout(20.seconds) { block(client) } } finally { - try { - withTimeout(3.seconds) { client.close() } - } catch (_: Exception) { - // ignore errors - } + runCatching { withTimeout(3.seconds) { client.close() } } } } - // ===== STDIO client + server helpers ===== + // ===== STDIO Client ===== protected fun startTypeScriptServerStdio(): Process { - if (!sdkDir.exists() || !sdkDir.isDirectory) { - throw IllegalStateException( - "TypeScript SDK directory does not exist or is not accessible: ${sdkDir.absolutePath}", - ) - } - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val localServerPath = File(tsClientDir, "simpleStdio.ts").absolutePath - val processBuilder = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - "set NODE_PATH=${sdkDir.absolutePath}\\node_modules && npx --prefix \"${sdkDir.absolutePath}\" tsx \"$localServerPath\"", - ) - } else { - ProcessBuilder() - .command( - "bash", - "-c", - "NODE_PATH='${sdkDir.absolutePath}/node_modules' npx --prefix '${sdkDir.absolutePath}' tsx \"$localServerPath\"", - ) - } - processBuilder.environment()["TYPESCRIPT_SDK_DIR"] = sdkDir.absolutePath - val process = processBuilder - .directory(tsClientDir) - .redirectErrorStream(false) - .start() - // For stdio transports, do NOT read from stdout (it's used for protocol). Read stderr for logs only. - createProcessErrorReader(process, prefix = "TS-SERVER-STDIO").start() - // Give the process a moment to start + val process = runDockerCommand( + command = listOf("npx", "--prefix", "/opt/typescript-sdk", "tsx", "/app/stdio/simpleStdio.ts"), + ) + await.atMost(2, TimeUnit.SECONDS) .pollDelay(200, TimeUnit.MILLISECONDS) .pollInterval(100, TimeUnit.MILLISECONDS) .until { process.isAlive } + return process } protected suspend fun newClientStdio(process: Process): Client { - val input: Source = process.inputStream.asSource().buffered() - val output: Sink = process.outputStream.asSink().buffered() - val transport = StdioClientTransport(input = input, output = output) - val client = Client(Implementation("test", "1.0")) - client.connect(transport) - return client + val transport = StdioClientTransport( + input = process.inputStream.asSource().buffered(), + output = process.outputStream.asSink().buffered(), + ) + return Client(Implementation("test", "1.0")).apply { connect(transport) } } protected suspend fun withClientStdio(block: suspend (Client, Process) -> T): T { val proc = startTypeScriptServerStdio() val client = newClientStdio(proc) return try { - withTimeout(20.seconds) { block(client, proc) } + withTimeout(5.seconds) { block(client, proc) } } finally { - try { - withTimeout(3.seconds) { client.close() } - } catch (_: Exception) { - } - try { - stopProcess(proc, name = "TypeScript stdio server") - } catch (_: Exception) { - } + runCatching { withTimeout(3.seconds) { client.close() } } + runCatching { stopProcess(proc, name = "TypeScript stdio server") } } } - // ===== Helpers to run TypeScript client over STDIO against Kotlin server over STDIO ===== - protected fun runStdioClient(vararg args: String): String { - // Start Node stdio client (it will speak MCP over its stdout/stdin) - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val clientPath = File(tsClientDir, "myClient.ts").absolutePath + // ===== HTTP Client (TypeScript → Kotlin Server) ===== + protected fun runHttpClient(serverUrl: String, vararg args: String) = runTsClient(serverUrl, args.toList()) + + protected fun runHttpClientAllowingFailure(serverUrl: String, vararg args: String) = + runTsClient(serverUrl, args.toList(), allowFailure = true) + + private fun runTsClient(serverUrl: String, args: List, allowFailure: Boolean = false): String { + val containerUrl = serverUrl.replace("localhost", "host.docker.internal") + val command = buildList { + add("npx") + add("--prefix") + add("/opt/typescript-sdk") + add("tsx") + add("/app/sse/myClient.ts") + add(containerUrl) + addAll(args) + } - val process = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - ( - "set TYPESCRIPT_SDK_DIR=${sdkDir.absolutePath} && " + - "set NODE_PATH=${sdkDir.absolutePath}\\node_modules && " + - "npx --prefix \"${sdkDir.absolutePath}\" tsx \"$clientPath\" " + - args.joinToString(" ") - ), - ) - .directory(tsClientDir) - .redirectErrorStream(false) - .start() + val process = runDockerCommand( + command = command, + environmentVariables = mapOf("NODE_PATH" to "/opt/typescript-sdk/node_modules"), + extraArgs = listOf("--add-host=host.docker.internal:host-gateway"), + allowFailure = allowFailure, + ) + + // Capture both stdout and stderr from the TS client to ensure error messages are returned to tests + val output = StringBuffer() + + fun captureStream(stream: java.io.InputStream, prefix: String = ""): Thread = Thread { + stream.bufferedReader().useLines { lines -> + lines.forEach { line -> + val msg = if (prefix.isEmpty()) line else "$prefix$line" + println(msg) + output.append(line).append('\n') + } + } + }.apply { + isDaemon = true + start() + } + + val stdoutThread = captureStream(process.inputStream) + val stderrThread = captureStream(process.errorStream, "[TS-CLIENT][err] ") + + val finished = if (allowFailure) { + process.waitFor(25, TimeUnit.SECONDS) } else { - ProcessBuilder() - .command( - "bash", - "-c", - ( - "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' " + - "NODE_PATH='${sdkDir.absolutePath}/node_modules' " + - "npx --prefix '${sdkDir.absolutePath}' tsx \"$clientPath\" " + - args.joinToString(" ") - ), - ) - .directory(tsClientDir) - .redirectErrorStream(false) - .start() + process.waitFor() + true } - // Create Kotlin server and attach stdio transport to the process streams - val server: Server = KotlinServerForTsClient().createMcpServer() + if (!finished) { + process.destroyForcibly() + } + + stdoutThread.join(1000) + stderrThread.join(1000) + + return output.toString() + } + + // ===== STDIO Client (TypeScript → Kotlin Server) ===== + protected fun runStdioClient(vararg args: String): String { + val process = runDockerCommand( + command = listOf("npx", "--prefix", "/opt/typescript-sdk", "tsx", "/app/stdio/myClient.ts") + args, + allowFailure = true, + ) + + val server = KotlinServerForTsClient().createMcpServer() val transport = StdioServerTransport( inputStream = process.inputStream.asSource().buffered(), outputStream = process.outputStream.asSink().buffered(), ) - // Connect server in a background thread to avoid blocking - val serverThread = Thread { - try { - kotlinx.coroutines.runBlocking { server.connect(transport) } - } catch (e: Exception) { - println("[STDIO-SERVER] Error connecting: ${e.message}") - } + Thread { + runCatching { runBlocking { server.connect(transport) } } + .onFailure { println("[STDIO-SERVER] Error: ${it.message}") } + }.apply { + isDaemon = true + start() } - serverThread.isDaemon = true - serverThread.start() - - // Read ONLY stderr from client for human-readable output - val output = StringBuilder() - val errReader = Thread { - try { - process.errorStream.bufferedReader().useLines { lines -> - lines.forEach { line -> - println("[TS-CLIENT-STDIO][err] $line") - output.append(line).append('\n') - } + + val output = StringBuffer() + Thread { + process.errorStream.bufferedReader().useLines { lines -> + lines.forEach { + println("[TS-CLIENT-STDIO][err] $it") + output.append(it).append('\n') } - } catch (e: Exception) { - println("Warning: Error reading stdio client stderr: ${e.message}") } + }.apply { + isDaemon = true + start() } - errReader.isDaemon = true - errReader.start() - // Wait up to 25s for client to exit - val finished = process.waitFor(25, TimeUnit.SECONDS) - if (!finished) { - println("Stdio client did not finish in time; destroying") + if (!process.waitFor(25, TimeUnit.SECONDS)) { + println("Stdio client timeout; destroying") process.destroyForcibly() } - try { - kotlinx.coroutines.runBlocking { transport.close() } - } catch (_: Exception) { - } - + runCatching { runBlocking { transport.close() } } return output.toString() } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt index 1585aa5e..57b9c507 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt @@ -33,14 +33,14 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { private lateinit var serverUrl: String private lateinit var client: Client - private lateinit var tsServerProcess: Process + private lateinit var tsServerProcess: ContainerProcess @BeforeEach fun setUp() { port = findFreePort() - serverUrl = "http://$host:$port/mcp" tsServerProcess = startTypeScriptServer(port) - println("TypeScript server started on port $port") + serverUrl = "http://$host:${tsServerProcess.mappedPort}/mcp" + println("TypeScript server started on port ${tsServerProcess.mappedPort}") } @AfterEach diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt index 95a80f0b..aa650245 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt @@ -15,14 +15,14 @@ class KotlinClientTsServerTestSse : AbstractKotlinClientTsServerTest() { private var port: Int = 0 private val host = "localhost" private lateinit var serverUrl: String - private lateinit var tsServerProcess: Process + private lateinit var tsServerProcess: ContainerProcess @BeforeEach fun setUpSse() { port = findFreePort() - serverUrl = "http://$host:$port/mcp" tsServerProcess = startTypeScriptServer(port) - println("TypeScript server started on port $port") + serverUrl = "http://$host:${tsServerProcess.mappedPort}/mcp" + println("TypeScript server started on port ${tsServerProcess.mappedPort}") } @AfterEach diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt index 5cb0f61b..d9cc5ace 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt @@ -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 diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt index edee4279..5355ecca 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt @@ -36,15 +36,5 @@ class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { override fun beforeServer() {} override fun afterServer() {} - override fun runClient(vararg args: String): String { - val cmd = buildString { - append("npx tsx myClient.ts ") - append(serverUrl) - if (args.isNotEmpty()) { - append(' ') - append(args.joinToString(" ")) - } - } - return executeCommand(cmd, tsClientDir) - } + override fun runClient(vararg args: String): String = runHttpClient(serverUrl, *args) } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt index 65b003df..603f1560 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt @@ -52,16 +52,15 @@ class TsEdgeCasesTestSse : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testInvalidURL() = runTest { - val nonExistentToolCommand = "npx tsx myClient.ts $serverUrl non-existent-tool" - val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, tsClientDir) + val nonExistentToolOutput = runHttpClientAllowingFailure(serverUrl, "non-existent-tool") assertTrue( nonExistentToolOutput.contains("Tool \"non-existent-tool\" not found"), "Client should handle non-existent tool gracefully", ) - val invalidUrlCommand = "npx tsx myClient.ts http://localhost:${port + 1000}/mcp greet TestUser" - val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, tsClientDir) + val invalidUrl = "http://localhost:${port + 1000}/mcp" + val invalidUrlOutput = runHttpClientAllowingFailure(invalidUrl, "greet", "TestUser") assertTrue( invalidUrlOutput.contains("Invalid URL") || @@ -81,8 +80,7 @@ class TsEdgeCasesTestSse : TsTestBase() { tempFile.deleteOnExit() val specialCharsContent = tempFile.readText() - val specialCharsCommand = "npx tsx myClient.ts $serverUrl greet \"$specialCharsContent\"" - val specialCharsOutput = executeCommand(specialCharsCommand, tsClientDir) + val specialCharsOutput = runHttpClient(serverUrl, "greet", specialCharsContent) assertTrue( specialCharsOutput.contains("Hello, $specialChars!"), @@ -106,8 +104,7 @@ class TsEdgeCasesTestSse : TsTestBase() { tempFile.deleteOnExit() val largeNameContent = tempFile.readText() - val largePayloadCommand = "npx tsx myClient.ts $serverUrl greet \"$largeNameContent\"" - val largePayloadOutput = executeCommand(largePayloadCommand, tsClientDir) + val largePayloadOutput = runHttpClient(serverUrl, "greet", largeNameContent) tempFile.delete() @@ -165,7 +162,21 @@ class TsEdgeCasesTestSse : TsTestBase() { val jobs = commands.mapIndexed { index, command -> async(kotlinx.coroutines.Dispatchers.IO) { println("Starting client $index") - val output = executeCommand(command, tsClientDir) + val output = when { + command.contains("greet \"Client1\"") -> runHttpClient(serverUrl, "greet", "Client1") + + command.contains( + "multi-greet \"Client2\"", + ) -> runHttpClient(serverUrl, "multi-greet", "Client2") + + command.contains("greet \"Client3\"") -> runHttpClient(serverUrl, "greet", "Client3") + + command.contains( + "multi-greet \"Client5\"", + ) -> runHttpClient(serverUrl, "multi-greet", "Client5") + + else -> runHttpClient(serverUrl) + } println("Client $index completed") assertContains( @@ -239,8 +250,7 @@ class TsEdgeCasesTestSse : TsTestBase() { @Timeout(120, unit = TimeUnit.SECONDS) fun testRapidSequentialRequests() = runTest { val outputs = (1..10).map { i -> - val command = "npx tsx myClient.ts $serverUrl greet \"RapidClient$i\"" - val output = executeCommand(command, tsClientDir) + val output = runHttpClient(serverUrl, "greet", "RapidClient$i") assertTrue( output.contains("Connected to server"), diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts index 29863091..8e5fc799 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts @@ -1,7 +1,7 @@ // @ts-nocheck -import { z } from 'zod'; +import {z} from 'zod'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import {pathToFileURL} from 'node:url'; const SDK_DIR = process.env.TYPESCRIPT_SDK_DIR; if (!SDK_DIR) { diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile new file mode 100644 index 00000000..bf986d67 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile @@ -0,0 +1,18 @@ +# Docker image for running TypeScript SDK test servers/clients with SSE over HTTP and STDIO + +FROM node:20-alpine AS base + +# 1) System deps +RUN apk add --no-cache git + +# 2) Fetch the official MCP TypeScript SDK and install its dependencies +WORKDIR /opt/typescript-sdk +RUN git clone --depth 1 https://github.com/modelcontextprotocol/typescript-sdk.git . \ + && npm ci || npm install + +# 3) Set working directory +WORKDIR /app + +# 4) Runtime configuration +ENV NODE_PATH=/opt/typescript-sdk/node_modules +ENV TYPESCRIPT_SDK_DIR=/opt/typescript-sdk From 2db79db368461fedac8a1699f60a8bec353e224e Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 12:31:51 +0300 Subject: [PATCH 04/18] fixup! Refactor integration tests to use Testcontainers for TypeScript client and server --- .../kotlin/sdk/integration/typescript/TsTestBase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index 3ef631c6..00dc3f04 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -66,7 +66,7 @@ abstract class TsTestBase { process.waitFor() } - fun runDockerCommand( + private fun runDockerCommand( image: String = tsDockerImage(), command: List, interactive: Boolean = true, From c20316cc209fe27849d87fea86eb1be415a9184e Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 13:41:15 +0300 Subject: [PATCH 05/18] fixup! Refactor integration tests to use Testcontainers for TypeScript client and server --- .../kotlin/sdk/integration/typescript/TsTestBase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index 00dc3f04..65cda1b1 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -274,7 +274,7 @@ abstract class TsTestBase { ) // Capture both stdout and stderr from the TS client to ensure error messages are returned to tests - val output = StringBuffer() + val output = StringBuilder() fun captureStream(stream: java.io.InputStream, prefix: String = ""): Thread = Thread { stream.bufferedReader().useLines { lines -> @@ -330,7 +330,7 @@ abstract class TsTestBase { start() } - val output = StringBuffer() + val output = StringBuilder() Thread { process.errorStream.bufferedReader().useLines { lines -> lines.forEach { From 8027e8625e0eab01daaa0b1e0b719faf7ede17ae Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 13:54:51 +0300 Subject: [PATCH 06/18] Add env var for docker registry --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2d28699..0977eed1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ jobs: name: Validate PR env: JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" + TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp:585bd19d0f2c" steps: - uses: actions/checkout@v5 From 2c4d6273015d15e2c033470a55e6c93692d99d96 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 14:03:03 +0300 Subject: [PATCH 07/18] fixup! Add env var for docker registry --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0977eed1..757a3172 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: name: Validate PR env: JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" - TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp:585bd19d0f2c" + TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:585bd19d0f2c3e46250da4d128cf3f2e8c155dc387026927e319f556a4fd559f" steps: - uses: actions/checkout@v5 From 5ca1d4f91c49ee8bafc624598d4f933a9299704e Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 17:39:18 +0300 Subject: [PATCH 08/18] Revert "fixup! Add env var for docker registry" This reverts commit 2c4d6273015d15e2c033470a55e6c93692d99d96. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 757a3172..0977eed1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: name: Validate PR env: JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" - TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:585bd19d0f2c3e46250da4d128cf3f2e8c155dc387026927e319f556a4fd559f" + TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp:585bd19d0f2c" steps: - uses: actions/checkout@v5 From 1e93fa7a471219314b02e6fe8e40bfa7bdc7739e Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 17:39:21 +0300 Subject: [PATCH 09/18] Revert "Add env var for docker registry" This reverts commit 8027e8625e0eab01daaa0b1e0b719faf7ede17ae. --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0977eed1..e2d28699 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,6 @@ jobs: name: Validate PR env: JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" - TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp:585bd19d0f2c" steps: - uses: actions/checkout@v5 From 71481f2a530b9e39ce3ddecf89aa34033a614239 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 17:41:59 +0300 Subject: [PATCH 10/18] Run PR validation on multiple OS --- .github/workflows/build.yml | 37 +++++++++++++++++++++++++------- kotlin-sdk-test/build.gradle.kts | 6 +++++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2d28699..d9d1a039 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,11 +13,26 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: - validate-pr: - runs-on: macos-latest-xlarge - name: Validate PR + build: + runs-on: ${{ matrix.os }} + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest-xlarge + name: "macOS" + java-opts: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g -DexcludeDockerTests=true" + gradle-task: "clean ktlintCheck assemble macosArm64Test macosX64Test jvmTest koverLog koverHtmlReport" + test-type: "macos" + - os: ubuntu-latest + name: "Ubuntu" + java-opts: "-Xmx4g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx3g" + gradle-task: "clean :kotlin-sdk-test:jvmTest" + test-type: "ubuntu" env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" + JAVA_OPTS: "${{ matrix.java-opts }}" + TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:585bd19d0f2c3e46250da4d128cf3f2e8c155dc387026927e319f556a4fd559f" steps: - uses: actions/checkout@v5 @@ -34,19 +49,24 @@ jobs: cache-read-only: true - name: Build with Gradle - run: |- - ./gradlew clean ktlintCheck build koverLog koverHtmlReport - ./gradlew :kotlin-sdk-core:publishToMavenLocal :kotlin-sdk-client:publishToMavenLocal :kotlin-sdk-server:publishToMavenLocal + run: ./gradlew ${{ matrix.gradle-task }} + + - name: Publish to Maven Local (macOS only) + if: matrix.test-type == 'macos' + run: ./gradlew :kotlin-sdk-core:publishToMavenLocal :kotlin-sdk-client:publishToMavenLocal :kotlin-sdk-server:publishToMavenLocal - name: Build Kotlin-MCP-Client Sample + if: matrix.test-type == 'macos' working-directory: ./samples/kotlin-mcp-client run: ./../../gradlew clean build - name: Build Kotlin-MCP-Server Sample + if: matrix.test-type == 'macos' working-directory: ./samples/kotlin-mcp-server run: ./../../gradlew clean build - name: Build Weather-Stdio-Server Sample + if: matrix.test-type == 'macos' working-directory: ./samples/weather-stdio-server run: ./../../gradlew clean build @@ -54,7 +74,7 @@ jobs: if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: reports + name: reports-${{ matrix.test-type }} path: | **/build/reports/ @@ -68,6 +88,7 @@ jobs: include_empty_in_summary: false include_time_in_summary: true annotate_only: true + check_name: Test Report (${{ matrix.test-type }}) - name: Disable Auto-Merge on Fail if: failure() && github.event_name == 'pull_request' diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts index 82df0f1a..cf92ae39 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/kotlin-sdk-test/build.gradle.kts @@ -6,6 +6,11 @@ kotlin { jvm { testRuns["test"].executionTask.configure { useJUnitPlatform() + if (System.getProperty("excludeDockerTests") == "true") { + filter { + excludeTestsMatching("*.typescript.*") + } + } } } sourceSets { @@ -25,7 +30,6 @@ kotlin { dependencies { implementation(kotlin("test-junit5")) implementation(libs.awaitility) - implementation(libs.testcontainers) runtimeOnly(libs.slf4j.simple) } } From 6b19476f89b001ae385289aaf3e7476c059e1a2a Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 17:49:13 +0300 Subject: [PATCH 11/18] fixup! Run PR validation on multiple OS --- kotlin-sdk-test/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts index cf92ae39..72d9308c 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/kotlin-sdk-test/build.gradle.kts @@ -30,6 +30,7 @@ kotlin { dependencies { implementation(kotlin("test-junit5")) implementation(libs.awaitility) + implementation(libs.testcontainers) runtimeOnly(libs.slf4j.simple) } } From 73dba749a55217bf393490167a18040d8ac15c55 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 18:01:27 +0300 Subject: [PATCH 12/18] fixup! Run PR validation on multiple OS --- .../kotlin/sdk/integration/typescript/TsTestBase.kt | 2 +- .../kotlin/sdk/integration/utils/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index 65cda1b1..ae045160 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -143,7 +143,7 @@ abstract class TsTestBase { // ===== TypeScript Server (SSE/HTTP) ===== protected fun startTypeScriptServer(port: Int): ContainerProcess { val container = GenericContainer(tsDockerImage()).apply { - withImagePullPolicy(PullPolicy.defaultPolicy()) + withImagePullPolicy(PullPolicy.alwaysPull()) withExposedPorts(port) mapOf( "MCP_HOST" to "0.0.0.0", diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile index bf986d67..c979326f 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile @@ -1,6 +1,6 @@ # Docker image for running TypeScript SDK test servers/clients with SSE over HTTP and STDIO -FROM node:20-alpine AS base +FROM --platform=linux/amd64 node:20-alpine AS base # 1) System deps RUN apk add --no-cache git From 327e62e95384413edcaa3131eb7dc95dcf6077c9 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 18:12:14 +0300 Subject: [PATCH 13/18] fixup! Run PR validation on multiple OS --- .github/workflows/build.yml | 1 + .../kotlin/sdk/integration/typescript/TsTestBase.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9d1a039..b8b74ec0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,7 @@ jobs: env: JAVA_OPTS: "${{ matrix.java-opts }}" TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:585bd19d0f2c3e46250da4d128cf3f2e8c155dc387026927e319f556a4fd559f" + DOCKER_DEFAULT_PLATFORM: "linux/amd64" steps: - uses: actions/checkout@v5 diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index ae045160..0d7485e2 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -144,6 +144,7 @@ abstract class TsTestBase { protected fun startTypeScriptServer(port: Int): ContainerProcess { val container = GenericContainer(tsDockerImage()).apply { withImagePullPolicy(PullPolicy.alwaysPull()) + withCreateContainerCmdModifier { cmd -> cmd.withPlatform("linux/amd64") } withExposedPorts(port) mapOf( "MCP_HOST" to "0.0.0.0", From f335da225c56f122f92451126ee89d48fe157d9f Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 18:26:54 +0300 Subject: [PATCH 14/18] fixup! Run PR validation on multiple OS --- .../kotlin/sdk/integration/typescript/TsTestBase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index 0d7485e2..1734c2d4 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -143,8 +143,8 @@ abstract class TsTestBase { // ===== TypeScript Server (SSE/HTTP) ===== protected fun startTypeScriptServer(port: Int): ContainerProcess { val container = GenericContainer(tsDockerImage()).apply { - withImagePullPolicy(PullPolicy.alwaysPull()) withCreateContainerCmdModifier { cmd -> cmd.withPlatform("linux/amd64") } + withImagePullPolicy(PullPolicy.alwaysPull()) withExposedPorts(port) mapOf( "MCP_HOST" to "0.0.0.0", From 61b70e914af98d554f122f6d8c206a70db33d3e6 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Fri, 10 Oct 2025 23:18:31 +0300 Subject: [PATCH 15/18] fixup! Run PR validation on multiple OS --- kotlin-sdk-test/build.gradle.kts | 4 ++++ .../kotlin/sdk/integration/typescript/TsTestBase.kt | 2 ++ 2 files changed, 6 insertions(+) diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts index 72d9308c..03bed378 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/kotlin-sdk-test/build.gradle.kts @@ -11,6 +11,10 @@ kotlin { excludeTestsMatching("*.typescript.*") } } + // Pass DOCKER_DEFAULT_PLATFORM to test process for Testcontainers + System.getenv("DOCKER_DEFAULT_PLATFORM")?.let { platform -> + environment("DOCKER_DEFAULT_PLATFORM", platform) + } } } sourceSets { diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index 1734c2d4..b2bb8502 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -78,6 +78,8 @@ abstract class TsTestBase { add("docker") add("run") add("--rm") + add("--platform") + add("linux/amd64") if (interactive) add("-i") add("-v") add("${getTsFilesPath("sse")}:/app/sse") From 19a099e0efa41b44267045240f5332835e34dd84 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Sat, 11 Oct 2025 00:24:03 +0300 Subject: [PATCH 16/18] fixup! Run PR validation on multiple OS --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8b74ec0..f17c76da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: test-type: "ubuntu" env: JAVA_OPTS: "${{ matrix.java-opts }}" - TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:585bd19d0f2c3e46250da4d128cf3f2e8c155dc387026927e319f556a4fd559f" + TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:97db9969e988c566c87c1c1b64605c6ce333a6db84d702339c8670987e8e8064" DOCKER_DEFAULT_PLATFORM: "linux/amd64" steps: - uses: actions/checkout@v5 From 7b20cdabec1c162d582ecf4d38b266fdbe8d9122 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Sat, 11 Oct 2025 13:13:16 +0300 Subject: [PATCH 17/18] fixup! Run PR validation on multiple OS --- .github/workflows/build.yml | 5 ++--- kotlin-sdk-test/build.gradle.kts | 4 ---- .../kotlin/sdk/integration/utils/Dockerfile | 4 +++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f17c76da..550d04fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: include: - os: macos-latest-xlarge name: "macOS" - java-opts: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g -DexcludeDockerTests=true" + java-opts: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" gradle-task: "clean ktlintCheck assemble macosArm64Test macosX64Test jvmTest koverLog koverHtmlReport" test-type: "macos" - os: ubuntu-latest @@ -32,8 +32,7 @@ jobs: test-type: "ubuntu" env: JAVA_OPTS: "${{ matrix.java-opts }}" - TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:97db9969e988c566c87c1c1b64605c6ce333a6db84d702339c8670987e8e8064" - DOCKER_DEFAULT_PLATFORM: "linux/amd64" + TS_SDK_IMAGE: "registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp@sha256:69f7762ec271b768b10e2d383e1dbc135c4df38a314a75c1ef35e9ff42276cb4" steps: - uses: actions/checkout@v5 diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts index 03bed378..72d9308c 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/kotlin-sdk-test/build.gradle.kts @@ -11,10 +11,6 @@ kotlin { excludeTestsMatching("*.typescript.*") } } - // Pass DOCKER_DEFAULT_PLATFORM to test process for Testcontainers - System.getenv("DOCKER_DEFAULT_PLATFORM")?.let { platform -> - environment("DOCKER_DEFAULT_PLATFORM", platform) - } } } sourceSets { diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile index c979326f..e40492fa 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Dockerfile @@ -1,6 +1,8 @@ # Docker image for running TypeScript SDK test servers/clients with SSE over HTTP and STDIO +# Be sure to build it for amd64 architecture with the following command: +# `docker buildx build --platform linux/amd64 -t registry.jetbrains.team/p/grazi/grazie-infra-public/typescript-sdk-mcp:latest --push .` -FROM --platform=linux/amd64 node:20-alpine AS base +FROM node:20-alpine AS base # 1) System deps RUN apk add --no-cache git From 35cb309f82781f78948c5d1eccb1684177f59434 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Sat, 11 Oct 2025 13:23:48 +0300 Subject: [PATCH 18/18] fixup! Run PR validation on multiple OS --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 550d04fb..f4642604 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: include: - os: macos-latest-xlarge name: "macOS" - java-opts: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" + java-opts: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g -DexcludeDockerTests=true" gradle-task: "clean ktlintCheck assemble macosArm64Test macosX64Test jvmTest koverLog koverHtmlReport" test-type: "macos" - os: ubuntu-latest