diff --git a/samples/kotlin-mcp-server/build.gradle.kts b/samples/kotlin-mcp-server/build.gradle.kts index 48789db2..11cfbc1a 100644 --- a/samples/kotlin-mcp-server/build.gradle.kts +++ b/samples/kotlin-mcp-server/build.gradle.kts @@ -16,6 +16,10 @@ dependencies { implementation(libs.mcp.kotlin.server) implementation(libs.ktor.server.cio) implementation(libs.slf4j.simple) + + testImplementation(libs.mcp.kotlin.client) + testImplementation(libs.ktor.client.cio) + testImplementation(kotlin("test")) } tasks.test { diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index c4c437a7..016241d0 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -3,12 +3,13 @@ kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.7.4" slf4j = "2.0.17" -serialization = "1.9.0" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } [plugins] diff --git a/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt index 45a70054..4cca92ca 100644 --- a/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt +++ b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt @@ -100,7 +100,7 @@ fun configureServer(): Server { return server } -fun runSseMcpServerWithPlainConfiguration(port: Int) { +fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true) { val serverSessions = ConcurrentMap() println("Starting SSE server on port $port") println("Use inspector to connect to http://localhost:$port/sse") @@ -136,7 +136,7 @@ fun runSseMcpServerWithPlainConfiguration(port: Int) { transport.handlePostMessage(call) } } - }.start(wait = true) + }.start(wait = wait) } /** @@ -147,7 +147,7 @@ fun runSseMcpServerWithPlainConfiguration(port: Int) { * * @param port The port number on which the SSE MCP server will listen for client connections. */ -fun runSseMcpServerUsingKtorPlugin(port: Int) { +fun runSseMcpServerUsingKtorPlugin(port: Int, wait: Boolean = true) { println("Starting SSE server on port $port") println("Use inspector to connect to http://localhost:$port/sse") @@ -155,7 +155,7 @@ fun runSseMcpServerUsingKtorPlugin(port: Int) { mcp { return@mcp configureServer() } - }.start(wait = true) + }.start(wait = wait) } /** diff --git a/samples/kotlin-mcp-server/src/test/kotlin/SseServerIntegrationTest.kt b/samples/kotlin-mcp-server/src/test/kotlin/SseServerIntegrationTest.kt new file mode 100644 index 00000000..2c0532c4 --- /dev/null +++ b/samples/kotlin-mcp-server/src/test/kotlin/SseServerIntegrationTest.kt @@ -0,0 +1,83 @@ +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport +import io.modelcontextprotocol.sample.server.runSseMcpServerUsingKtorPlugin +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SseServerIntegrationTest { + + companion object { + private const val PORT = 3002 + } + + private lateinit var client: Client + + private fun initClient(port: Int) { + client = Client( + Implementation(name = "test-client", version = "0.1.0"), + ) + + val httpClient = HttpClient(CIO) { + 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 + } + } + runBlocking { + client.connect(transport) + } + } + + @BeforeAll + fun setUp() { + runSseMcpServerUsingKtorPlugin(PORT, wait = false) + initClient(PORT) + } + + @Test + fun `should get tools`(): Unit = runBlocking { + val tools = client.listTools().tools + + assertEquals(expected = listOf("kotlin-sdk-tool"), actual = tools.map { it.name }) + } + + @Test + fun `should get prompts`(): Unit = runBlocking { + val prompts = client.listPrompts().prompts + + assertEquals(expected = listOf("Kotlin Developer"), actual = prompts.map { it.name }) + } + + @Test + fun `should get resources`(): Unit = runBlocking { + val resources = client.listResources().resources + + assertEquals(expected = listOf("Web Search"), actual = resources.map { it.name }) + } + + @Test + fun `should call tool`(): Unit = runBlocking { + val toolResult = client.callTool("kotlin-sdk-tool", EmptyJsonObject) + val content = toolResult?.content?.single() + assertIs(content, "Tool result should be a text content") + + assertEquals(expected = "Hello, world!", actual = content.text) + assertEquals(expected = "text", actual = content.type) + } +}