From 3ad9f12b51be60e51652eb0ded17aceb4b367624 Mon Sep 17 00:00:00 2001 From: tpp-builder Date: Wed, 12 Feb 2025 10:03:20 +0800 Subject: [PATCH 1/3] fix SSEClientTransport endpoint process with none directory sse path --- .../kotlin/sdk/client/SSEClientTransport.kt | 16 ++++++-- .../kotlin/sdk/client/SseTransportTest.kt | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) 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 e7721ac1..42a96c90 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt @@ -55,7 +55,18 @@ public class SseClientTransport( private var job: Job? = null private val baseUrl by lazy { - session.call.request.url.toString().removeSuffix("/sse") + val requestUrl = session.call.request.url.toString() + val url = Url(requestUrl) + var path = url.encodedPath + if (path.isEmpty()) { + url.protocolWithAuthority + } else if (path.endsWith("/")) { + url.protocolWithAuthority + path.removeSuffix("/") + } else { + // the last item is not a directory, so will not be taken into account + path = path.substring(0, path.lastIndexOf("/")) + url.protocolWithAuthority + path + } } override suspend fun start() { @@ -95,8 +106,7 @@ public class SseClientTransport( val eventData = event.data ?: "" // check url correctness - val maybeEndpoint = Url(baseUrl + eventData) - + val maybeEndpoint = Url("$baseUrl/${if (eventData.startsWith("/")) eventData.substring(1) else eventData}") endpoint.complete(maybeEndpoint.toString()) } catch (e: Exception) { _onError(e) diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index fd4c3d69..9980cb91 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -82,4 +82,41 @@ class SseTransportTest : BaseTransportTest() { testClientRead(client) server.stopSuspend() } + + @Test + fun `test sse path not root path`() = runTest { + val server = embeddedServer(CIO, port = PORT) { + install(io.ktor.server.sse.SSE) + val transports = ConcurrentMap() + routing { + sse("/sse") { + mcpSseTransport("/messages", transports).apply { + onMessage { + send(it) + } + + start() + } + } + + post("/messages") { + + mcpPostEndpoint(transports) + } + } + }.start(wait = false) + + val client = HttpClient { + install(SSE) + }.mcpSseTransport { + url { + host = "localhost" + port = PORT + pathSegments = listOf("sse") + } + } + + testClientRead(client) + server.stop() + } } From 9a153e3935b6540123ed015a46b88fc1bc1b2536 Mon Sep 17 00:00:00 2001 From: tpp-builder Date: Wed, 12 Feb 2025 10:03:20 +0800 Subject: [PATCH 2/3] fix SSEClientTransport endpoint process with none directory sse path --- .../kotlin/sdk/client/SseTransportTest.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index 9980cb91..00c115fb 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -119,4 +119,31 @@ class SseTransportTest : BaseTransportTest() { testClientRead(client) server.stop() } + + @Test + fun `test sse path not root path`() = runTest { + val server = embeddedServer(CIO, port = PORT) { + install(io.ktor.server.sse.SSE) + routing { + mcpSseTransport(path = "/sse", incomingPath = "/messages") { + onMessage = { + send(it) + } + } + } + }.start(wait = false) + + val client = HttpClient { + install(SSE) + }.mcpSseTransport { + url { + host = "localhost" + port = PORT + pathSegments = listOf("sse") + } + } + + testClientRead(client) + server.stop() + } } From 8c0a6aac1cd0de1cb465aab67ed6c63dae0cb774 Mon Sep 17 00:00:00 2001 From: devcrocod Date: Thu, 3 Jul 2025 18:31:28 +0200 Subject: [PATCH 3/3] Refactor `SseTransportTest` to use `route` for better clarity and update server handling with suspend functions. --- .../kotlin/sdk/client/SSEClientTransport.kt | 2 +- .../kotlin/sdk/client/SseTransportTest.kt | 56 ++++++------------- 2 files changed, 17 insertions(+), 41 deletions(-) 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 42a96c90..168f3abd 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt @@ -12,6 +12,7 @@ import io.ktor.http.HttpHeaders import io.ktor.http.Url import io.ktor.http.append import io.ktor.http.isSuccess +import io.ktor.http.protocolWithAuthority import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.McpJson @@ -24,7 +25,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.properties.Delegates diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index 00c115fb..de61a079 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -6,6 +6,7 @@ import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer import io.ktor.server.routing.post +import io.ktor.server.routing.route import io.ktor.server.routing.routing import io.ktor.server.sse.sse import io.ktor.util.collections.ConcurrentMap @@ -85,65 +86,40 @@ class SseTransportTest : BaseTransportTest() { @Test fun `test sse path not root path`() = runTest { - val server = embeddedServer(CIO, port = PORT) { + val port = 3007 + val server = embeddedServer(CIO, port = port) { install(io.ktor.server.sse.SSE) val transports = ConcurrentMap() routing { - sse("/sse") { - mcpSseTransport("/messages", transports).apply { - onMessage { - send(it) + route("/sse") { + sse { + mcpSseTransport("", transports).apply { + onMessage { + send(it) + } + + start() } - - start() } - } - - post("/messages") { - - mcpPostEndpoint(transports) - } - } - }.start(wait = false) - - val client = HttpClient { - install(SSE) - }.mcpSseTransport { - url { - host = "localhost" - port = PORT - pathSegments = listOf("sse") - } - } - - testClientRead(client) - server.stop() - } - @Test - fun `test sse path not root path`() = runTest { - val server = embeddedServer(CIO, port = PORT) { - install(io.ktor.server.sse.SSE) - routing { - mcpSseTransport(path = "/sse", incomingPath = "/messages") { - onMessage = { - send(it) + post { + mcpPostEndpoint(transports) } } } - }.start(wait = false) + }.startSuspend(wait = false) val client = HttpClient { install(SSE) }.mcpSseTransport { url { host = "localhost" - port = PORT + this.port = port pathSegments = listOf("sse") } } testClientRead(client) - server.stop() + server.stopSuspend() } }