From be4f9c37e0fb02fe0f51dd92df2b2c92c33083c5 Mon Sep 17 00:00:00 2001 From: Abdullatif Ghajar <62988232+AbdullatifGhajar@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:29:48 +0100 Subject: [PATCH] Test and fix #52 (#53) * Add test to reproduce #52 * Solve issue #52 by improving url handling in SseClientTransport * Remove unnecessary url processing --------- Co-authored-by: Abdullatif.Ghajar --- .../kotlin/sdk/client/SSEClientTransport.kt | 6 +-- .../kotlin/integration/SseIntegrationTest.kt | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/jvmTest/kotlin/integration/SseIntegrationTest.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt index 9e21347..91a9f85 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt @@ -39,7 +39,7 @@ public class SseClientTransport( private var job: Job? = null private val baseUrl by lazy { - session.call.request.url.toString().removeSuffix("/") + session.call.request.url.toString().removeSuffix("/sse") } override suspend fun start() { @@ -52,7 +52,7 @@ public class SseClientTransport( session = urlString?.let { client.sseSession( - urlString = it, + urlString = "$it/sse", reconnectionTime = reconnectionTime, block = requestBuilder, ) @@ -79,7 +79,7 @@ public class SseClientTransport( val eventData = event.data ?: "" // check url correctness - val maybeEndpoint = Url("$baseUrl/$eventData") + val maybeEndpoint = Url(baseUrl + eventData) endpoint.complete(maybeEndpoint.toString()) } catch (e: Exception) { diff --git a/src/jvmTest/kotlin/integration/SseIntegrationTest.kt b/src/jvmTest/kotlin/integration/SseIntegrationTest.kt new file mode 100644 index 0000000..741ea14 --- /dev/null +++ b/src/jvmTest/kotlin/integration/SseIntegrationTest.kt @@ -0,0 +1,51 @@ +package integration + +import io.ktor.client.* +import io.ktor.client.plugins.sse.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpSse +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.server.cio.CIO as ServerCIO + +class SseIntegrationTest { + @Test + fun `client should be able to connect to sse server`() { // runTest will cause network timeout issues + runBlocking { + val serverEngine = initServer() + try { + assertDoesNotThrow { initClient() } + } finally { + // Make sure to stop the server + serverEngine.stop(1000, 2000) + } + } + } + + private suspend fun initClient(): Client { + return HttpClient(ClientCIO) { install(SSE) }.mcpSse("http://$URL:$PORT") + } + + private fun initServer(): EmbeddedServer { + val server = Server( + Implementation(name = "sse-e2e-test", version = "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + + return embeddedServer(ServerCIO, host = URL, port = PORT) { mcp { server } }.start(wait = false) + } + + companion object { + private const val PORT = 3001 + private const val URL = "localhost" + } +} \ No newline at end of file