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
4 changes: 4 additions & 0 deletions samples/kotlin-mcp-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion samples/kotlin-mcp-server/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ fun configureServer(): Server {
return server
}

fun runSseMcpServerWithPlainConfiguration(port: Int) {
fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true) {
val serverSessions = ConcurrentMap<String, ServerSession>()
println("Starting SSE server on port $port")
println("Use inspector to connect to http://localhost:$port/sse")
Expand Down Expand Up @@ -136,7 +136,7 @@ fun runSseMcpServerWithPlainConfiguration(port: Int) {
transport.handlePostMessage(call)
}
}
}.start(wait = true)
}.start(wait = wait)
}

/**
Expand All @@ -147,15 +147,15 @@ 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")

embeddedServer(CIO, host = "127.0.0.1", port = port) {
mcp {
return@mcp configureServer()
}
}.start(wait = true)
}.start(wait = wait)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TextContent>(content, "Tool result should be a text content")

assertEquals(expected = "Hello, world!", actual = content.text)
assertEquals(expected = "text", actual = content.type)
}
}
Loading