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
50 changes: 34 additions & 16 deletions samples/kotlin-mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,58 @@ configurations and transport methods.

### Running the Server

The server defaults to SSE mode with Ktor plugin on port 3001. You can customize the behavior using command-line arguments.
The server defaults [STDIO transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio).

#### Default (SSE with Ktor plugin):
You can customize the behavior using command-line arguments.
Logs are printed to [./build/stdout.log](./build/stdout.log)

#### Standard I/O mode (STDIO):

```bash
./gradlew run
./gradlew clean build
```
Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector)
to connect to MCP via STDIO (Click the "▶️ Connect" button):

#### Standard I/O mode:

```bash
./gradlew run --args="--stdio"
```shell
npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server stdio-server
```

#### SSE with plain configuration:

**NB!: 🐞 This configuration may not work ATM**

```bash
./gradlew run --args="--sse-server 3001"
```
or
```shell
./gradlew clean build
java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server 3001
```

#### SSE with Ktor plugin (custom port):
Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector)
to connect to `http://localhost:3002/` via SSE Transport (Click the "▶️ Connect" button):
```shell
npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-server
```

#### SSE with Ktor plugin:

```bash
./gradlew run --args="--sse-server-ktor 3002"
```
or
```shell
./gradlew clean build
java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server-ktor 3002
```

### Connecting to the Server

For SSE servers:
1. Start the server
2. Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) to connect to `http://localhost:<port>/sse`

For STDIO servers:
- Connect using an MCP client that supports STDIO transport
Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector)
to connect to `http://localhost:3002/` via SSE transport (Click the "▶️ Connect" button):
```shell
npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-ktor-server
```

## Server Capabilities

Expand Down
8 changes: 8 additions & 0 deletions samples/kotlin-mcp-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.shadow)
application
}

Expand All @@ -15,6 +16,7 @@ dependencies {
implementation(dependencies.platform(libs.ktor.bom))
implementation(libs.mcp.kotlin.server)
implementation(libs.ktor.server.cio)
implementation(libs.ktor.server.cors)
implementation(libs.slf4j.simple)

testImplementation(libs.mcp.kotlin.client)
Expand All @@ -28,4 +30,10 @@ tasks.test {

kotlin {
jvmToolchain(17)
compilerOptions {
javaParameters = true
freeCompilerArgs.addAll(
"-Xdebug",
)
}
}
7 changes: 5 additions & 2 deletions samples/kotlin-mcp-server/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ kotlin = "2.2.21"
ktor = "3.2.3"
mcp-kotlin = "0.7.4"
slf4j = "2.0.17"
shadow = "9.2.2"

[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" }
ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio" }
ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" }
mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" }
mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" }
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
25 changes: 25 additions & 0 deletions samples/kotlin-mcp-server/mcp-inspector-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"mcpServers": {
"stdio-server": {
"command": "java",
"args": [
"-Dorg.slf4j.simpleLogger.defaultLogLevel=off",
"-jar",
"./build/libs/kotlin-mcp-server-0.1.0-all.jar"
],
"env": {
},
"note": "For SSE connections, add this URL directly in your MCP Client"
},
"sse-server": {
"type": "sse",
"url": "http://127.0.0.1:3001/sse",
"note": "SSE with plain configuration"
},
"sse-ktor-server": {
"type": "sse",
"url": "http://127.0.0.1:3002/",
"note": "SSE with Ktor plugin"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import kotlinx.coroutines.runBlocking
* - "--sse-server-ktor <port>": Runs an SSE MCP server using Ktor plugin (default if no argument is provided).
* - "--sse-server <port>": Runs an SSE MCP server with a plain configuration.
*/
fun main(args: Array<String>): Unit = runBlocking {
val command = args.firstOrNull() ?: "--sse-server-ktor"
fun main(vararg args: String): Unit = runBlocking {
val command = args.firstOrNull() ?: "--stdio"
val port = args.getOrNull(1)?.toIntOrNull() ?: 3001
when (command) {
"--stdio" -> runMcpServerUsingStdio()

"--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port)

"--sse-server" -> runSseMcpServerWithPlainConfiguration(port)

else -> {
error("Unknown command: $command")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.modelcontextprotocol.sample.server

import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.response.respond
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
Expand Down Expand Up @@ -101,13 +105,13 @@ fun configureServer(): Server {
}

fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true) {
printBanner(port = port, path = "/sse")
val serverSessions = ConcurrentMap<String, ServerSession>()
println("Starting SSE server on port $port")
println("Use inspector to connect to http://localhost:$port/sse")

val server = configureServer()

embeddedServer(CIO, host = "127.0.0.1", port = port) {
installCors()
install(SSE)
routing {
sse("/sse") {
Expand Down Expand Up @@ -147,15 +151,36 @@ fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true) {
*
* @param port The port number on which the SSE MCP server will listen for client connections.
*/
fun runSseMcpServerUsingKtorPlugin(port: Int, wait: Boolean = true) {
println("Starting SSE server on port $port")
println("Use inspector to connect to http://localhost:$port/sse")
fun runSseMcpServerUsingKtorPlugin(port: Int, wait: Boolean = true): EmbeddedServer<*, *> {
printBanner(port)

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

private fun printBanner(port: Int, path: String = "") {
if (port == 0) {
println("🎬 Starting SSE server on random port")
} else {
println("🎬 Starting SSE server on ${if (port > 0) "port $port" else "random port"}")
println("🔍 Use MCP inspector to connect to http://localhost:$port$path")
}
}

private fun Application.installCors() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Delete)
allowNonSimpleContentTypes = true
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
}

/**
Expand All @@ -168,7 +193,7 @@ fun runMcpServerUsingStdio() {
val server = configureServer()
val transport = StdioServerTransport(
inputStream = System.`in`.asSource().buffered(),
outputStream = System.out.asSink().buffered()
outputStream = System.out.asSink().buffered(),
)

runBlocking {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ org.slf4j.simpleLogger.defaultLogLevel=INFO
org.slf4j.simpleLogger.showThreadName=true
org.slf4j.simpleLogger.showDateTime=false

org.slf4j.simpleLogger.logFile=./build/stdout.log

# Log level for specific packages or classes
org.slf4j.simpleLogger.log.io.modelcontextprotocol=TRACE
org.slf4j.simpleLogger.log.io.ktor=TRACE
org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG
org.slf4j.simpleLogger.log.io.ktor=DEBUG
Original file line number Diff line number Diff line change
@@ -1,54 +1,15 @@
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)
}
private val client: Client = TestEnvironment.client

@Test
fun `should get tools`(): Unit = runBlocking {
Expand Down
50 changes: 50 additions & 0 deletions samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport
import io.modelcontextprotocol.sample.server.runSseMcpServerUsingKtorPlugin
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit

object TestEnvironment {

val server = runSseMcpServerUsingKtorPlugin(0, wait = false)
val client: Client

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

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

private suspend fun initClient(port: Int): Client {
val 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
}
}
client.connect(transport)
return client
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO)
org.slf4j.simpleLogger.defaultLogLevel=INFO
org.slf4j.simpleLogger.showThreadName=true
org.slf4j.simpleLogger.showDateTime=false

# Log level for specific packages or classes
org.slf4j.simpleLogger.log.io.modelcontextprotocol=TRACE
org.slf4j.simpleLogger.log.io.ktor=TRACE
Loading