@@ -3,6 +3,7 @@ package io.modelcontextprotocol.kotlin.sdk.client
33import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest
44import io.modelcontextprotocol.kotlin.sdk.types.Implementation
55import io.modelcontextprotocol.kotlin.sdk.types.McpException
6+ import kotlinx.coroutines.delay
67import kotlinx.coroutines.runBlocking
78import kotlinx.coroutines.test.runTest
89import kotlinx.io.asSink
@@ -12,12 +13,18 @@ import org.junit.jupiter.api.Test
1213import org.junit.jupiter.api.Timeout
1314import org.junit.jupiter.api.assertThrows
1415import java.util.concurrent.TimeUnit
15-
16- @Timeout(20 , unit = TimeUnit .SECONDS )
16+ import kotlin.concurrent.atomics.AtomicBoolean
17+ import kotlin.concurrent.atomics.ExperimentalAtomicApi
18+ import kotlin.test.assertFalse
19+ import kotlin.test.assertTrue
20+ import kotlin.test.fail
21+ import kotlin.time.Duration.Companion.milliseconds
22+ import kotlin.time.Duration.Companion.seconds
23+
24+ @Timeout(30 , unit = TimeUnit .SECONDS )
1725class StdioClientTransportTest : BaseTransportTest () {
1826
1927 @Test
20- @Timeout(30 , unit = TimeUnit .SECONDS )
2128 fun `handle stdio error` (): Unit = runBlocking {
2229 val processBuilder = if (System .getProperty(" os.name" ).lowercase().contains(" win" )) {
2330 ProcessBuilder (" cmd" , " /c" , " pause 1 && echo simulated error 1>&2 && exit 1" )
@@ -55,6 +62,7 @@ class StdioClientTransportTest : BaseTransportTest() {
5562 process.destroyForcibly()
5663 }
5764
65+ @OptIn(ExperimentalAtomicApi ::class )
5866 @Test
5967 fun `should start then close cleanly` () = runTest {
6068 // Run process "/usr/bin/tee"
@@ -63,15 +71,33 @@ class StdioClientTransportTest : BaseTransportTest() {
6371
6472 val input = process.inputStream.asSource().buffered()
6573 val output = process.outputStream.asSink().buffered()
74+ val error = process.errorStream.asSource().buffered()
6675
6776 val transport = StdioClientTransport (
6877 input = input,
6978 output = output,
79+ error = error,
7080 )
7181
72- testTransportOpenClose(transport)
82+ transport.onError { error ->
83+ fail(" Unexpected error: $error " )
84+ }
85+
86+ val didClose = AtomicBoolean (false )
87+ transport.onClose { didClose.store(true ) }
88+
89+ transport.start()
90+ delay(1 .seconds)
7391
92+ assertFalse(didClose.load(), " Transport should not be closed immediately after start" )
93+
94+ // Destroy process BEFORE close() to unblock stdin reader
7495 process.destroyForcibly()
96+ delay(100 .milliseconds) // Give time for EOF to propagate
97+
98+ transport.close()
99+
100+ assertTrue(didClose.load(), " Transport should be closed after close() call" )
75101 }
76102
77103 @Test
0 commit comments