diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 0ad4874..496bfa0 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -989,7 +989,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1824,7 +1823,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt index 66d43f7..0b774a6 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt @@ -230,8 +230,18 @@ public class StdioClientTransport @JvmOverloads public constructor( } override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { - check(initialized.load()) { "Transport is not started" } - check(!onCloseCalled.load()) { "Transport is closed" } + if (!initialized.load()) { + throw McpException( + code = CONNECTION_CLOSED, + message = "Transport is not started", + ) + } + if (onCloseCalled.load()) { + throw McpException( + code = CONNECTION_CLOSED, + message = "Transport is closed", + ) + } @Suppress("TooGenericExceptionCaught", "SwallowedException") try { sendChannel.send(message) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index a171217..c8f250a 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -27,7 +27,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED import io.modelcontextprotocol.kotlin.sdk.types.RequestId import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName @@ -105,7 +107,12 @@ public class StreamableHttpClientTransport( resumptionToken: String?, onResumptionToken: ((String) -> Unit)? = null, ) { - check(initialized.load()) { "Transport is not started" } + if (!initialized.load()) { + throw McpException( + code = CONNECTION_CLOSED, + message = "Transport is not started", + ) + } logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" } // If we have a resumption token, reconnect the SSE stream with it diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt index 8afc81f..02b6d25 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt @@ -4,7 +4,9 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED import io.modelcontextprotocol.kotlin.sdk.types.toJSON import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest @@ -46,10 +48,11 @@ abstract class AbstractClientTransportLifecycleTest { fun `should throw when sending before start`() = runTest { val transport = createTransport() - val exception = shouldThrow { + val exception = shouldThrow { transport.send(PingRequest().toJSON()) } exception.message shouldContain "not started" + exception.code shouldBe CONNECTION_CLOSED } @Test diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportErrorHandlingTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportErrorHandlingTest.kt index da18741..b227d5c 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportErrorHandlingTest.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportErrorHandlingTest.kt @@ -6,6 +6,7 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import io.kotest.matchers.types.shouldBeSameInstanceAs import io.mockk.coEvery +import io.mockk.every import io.mockk.mockk import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage @@ -151,7 +152,16 @@ class StdioClientTransportErrorHandlingTest { fun `Send should handle exceptions`(throwable: Throwable, shouldWrap: Boolean, expectedCode: Int?) = runTest { val sendChannel: Channel = mockk(relaxed = true) - val stdin = createNonEmptyBuffer { "id: 1\ndata:\n" } + // Create a stdin Source that never returns EOF to prevent transport from closing + // Use mockk since Source is a sealed interface + val stdin: kotlinx.io.Source = mockk(relaxed = true) + every { stdin.readAtMostTo(any(), any()) } coAnswers { + // Return 0 to indicate no data available (but not EOF) + // This keeps the transport alive without blocking + delay(10) // Small delay to prevent busy-waiting + 0L + } + transport = StdioClientTransport( input = stdin, output = Buffer(),