fix(stdio-client)!: Stdio- and StreamableHttpClientTransport should throw McpException on send when not ready (#487)
This commit is contained in:
parent
9f16ae45d2
commit
4d505025b4
5 changed files with 35 additions and 7 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<T : AbstractTransport> {
|
|||
fun `should throw when sending before start`() = runTest {
|
||||
val transport = createTransport()
|
||||
|
||||
val exception = shouldThrow<IllegalStateException> {
|
||||
val exception = shouldThrow<McpException> {
|
||||
transport.send(PingRequest().toJSON())
|
||||
}
|
||||
exception.message shouldContain "not started"
|
||||
exception.code shouldBe CONNECTION_CLOSED
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -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<JSONRPCMessage> = 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<Buffer>(), 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(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue