-
Notifications
You must be signed in to change notification settings - Fork 168
Description
Describe the bug
When McpJson.decodeFromString fails inside StreamableHttpClientTransport .send or .handleInlineSse, the Client/Protocol is left waiting on a CompletableDeferred that never completes. As a result, the program hangs indefinitely with no indication of the underlying error.
To Reproduce
Steps to reproduce the behavior:
- Create a
Client - Create a
StreamableHttpClientTransport - Have the MCP server respond with invalid JSON (or trigger another deserialization error, e.g. same as in Request deserilization is broken / throwing exception in version 0.6.0 #189).
- Call
client.connect(transport)
Observed behavior
The program hangs indefinitely without surfacing the decoding failure.
Expected behavior
client.connect should fail by throwing an exception, making the error visible to the caller.
Additional context
What's happening is that StreamableHttpClientTransport passes the exception to its onError callback without re-throwing it. Meanwhile, Protocol is still awaiting completion of its CompletableDeferred, which only resolves when an actual message is received. Since that never happens, the await call blocks forever.
Test to reproduce
@Test
fun testClientConnectWithInvalidJson() = runTest {
// Transport under test: respond with invalid JSON for the initialize request
val transport = createTransport { _ ->
respond(
"this is not valid json",
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
)
}
val client = Client(
clientInfo = io.modelcontextprotocol.kotlin.sdk.Implementation(
name = "test-client",
version = "1.0",
),
)
try {
// Real time-keeping is needed; otherwise Protocol will always throw TimeoutCancellationException in tests
withContext(Dispatchers.Default.limitedParallelism(1)) {
kotlinx.coroutines.withTimeout(5.seconds) {
client.connect(transport)
}
}
kotlin.test.fail("Expected client.connect to fail on invalid JSON response")
} catch (e: TimeoutCancellationException) {
// Current behavior: result.await() in Protocol is never resolved, leading to a hang.
kotlin.test.fail("Client connect caused a hang", e)
} catch (_: IllegalStateException) {
// Expected behavior: connect finishes and fails with an exception.
} catch (e: Throwable) {
kotlin.test.fail("Unexpected exception during client.connect", e)
} finally {
transport.close()
}
}