Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.runBlocking
import kotlinx.io.asSink
import kotlinx.io.asSource
Expand Down Expand Up @@ -102,13 +103,13 @@ fun configureServer(): Server {
return server
}

fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true) {
fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true): EmbeddedServer<*, *> {
printBanner(port = port, path = "/sse")
val serverSessions = ConcurrentMap<String, ServerSession>()

val server = configureServer()

embeddedServer(CIO, host = "127.0.0.1", port = port) {
val ktorServer = embeddedServer(CIO, host = "127.0.0.1", port = port) {
installCors()
install(SSE)
routing {
Expand All @@ -121,6 +122,7 @@ fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true) {
println("Server session closed for: ${transport.sessionId}")
serverSessions.remove(transport.sessionId)
}
awaitCancellation()
}
post("/message") {
val sessionId: String? = call.request.queryParameters["sessionId"]
Expand All @@ -139,6 +141,8 @@ fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true) {
}
}
}.start(wait = wait)

return ktorServer
}

/**
Expand Down
17 changes: 17 additions & 0 deletions samples/kotlin-mcp-server/src/test/kotlin/McpServerType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import io.ktor.server.engine.EmbeddedServer
import io.modelcontextprotocol.sample.server.runSseMcpServerUsingKtorPlugin
import io.modelcontextprotocol.sample.server.runSseMcpServerWithPlainConfiguration

enum class McpServerType(
val sseEndpoint: String,
val serverFactory: (port: Int) -> EmbeddedServer<*, *>
) {
KTOR_PLUGIN(
sseEndpoint = "",
serverFactory = { port -> runSseMcpServerUsingKtorPlugin(port, wait = false) }
),
PLAIN_CONFIGURATION(
sseEndpoint = "/sse",
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sseEndpoint should be "sse" instead of "/sse" to avoid creating a double-slash URL (//sse) when concatenated in TestEnvironment.kt line 40. The leading slash is already provided in the URL construction.

Suggested change
sseEndpoint = "/sse",
sseEndpoint = "sse",

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, but can be addressed separately

serverFactory = { port -> runSseMcpServerWithPlainConfiguration(port, wait = false) }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class SseServerIntegrationTest {
abstract class SseServerIntegrationTestBase {

private val client: Client = TestEnvironment.client
abstract val client: Client

@Test
fun `should list tools`(): Unit = runBlocking {
Expand Down Expand Up @@ -88,3 +88,13 @@ class SseServerIntegrationTest {
assertEquals(expected = "text", actual = "${content.type}".lowercase())
}
}

class SseServerKtorPluginIntegrationTest : SseServerIntegrationTestBase() {
private val testEnvironment = TestEnvironment(McpServerType.KTOR_PLUGIN)
override val client: Client = testEnvironment.client
}

class SseServerPlainConfigurationIntegrationTest : SseServerIntegrationTestBase() {
private val testEnvironment = TestEnvironment(McpServerType.PLAIN_CONFIGURATION)
override val client: Client = testEnvironment.client
}
22 changes: 8 additions & 14 deletions samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.sse.SSE
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.ktor.server.engine.EmbeddedServer
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport
import io.modelcontextprotocol.sample.server.runSseMcpServerUsingKtorPlugin
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit

object TestEnvironment {
class TestEnvironment(private val serverConfig: McpServerType) {

val server = runSseMcpServerUsingKtorPlugin(0, wait = false)
val server: EmbeddedServer<*, *> = serverConfig.serverFactory(0)
val client: Client

init {
client = runBlocking {
val port = server.engine.resolvedConnectors().single().port
initClient(port)
initClient(port, serverConfig)
}

Runtime.getRuntime().addShutdownHook(
Thread {
println("🏁 Shutting down server")
println("🏁 Shutting down server (${serverConfig.name})")
server.stop(500, 700, TimeUnit.MILLISECONDS)
println("☑️ Shutdown complete")
},
)
}

private suspend fun initClient(port: Int): Client {
private suspend fun initClient(port: Int, config: McpServerType): Client {
val client = Client(
Implementation(name = "test-client", version = "0.1.0"),
)
Expand All @@ -37,13 +37,7 @@ object TestEnvironment {
install(SSE)
}

// Create a transport wrapper that captures the session ID and received messages
val transport = httpClient.mcpSseTransport {
url {
this.host = "127.0.0.1"
this.port = port
}
}
val transport = httpClient.mcpSseTransport("http://127.0.0.1:$port/${config.sseEndpoint}")
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL construction "http://127.0.0.1:$port/${config.sseEndpoint}" creates incorrect URLs:

  • When sseEndpoint = "/sse", it produces "http://127.0.0.1:$port//sse" (double slash)
  • When sseEndpoint = "", it produces "http://127.0.0.1:$port/" (trailing slash)

The sseEndpoint in McpServerType should not include the leading slash. Change it to:

PLAIN_CONFIGURATION(
    sseEndpoint = "sse",
    ...
)

Then this URL construction will work correctly for both cases.

Copilot uses AI. Check for mistakes.
client.connect(transport)
return client
}
Expand Down
Loading