Update Kotlin-MCP-Server with STDIO support, logging & CORS (#371)
Update Kotlin-MCP-Server with STDIO support, logging & CORS ### Summary - Added support for STDIO transport mode (default behavior). - Configured CORS and migrated SSE server to Ktor's Netty engine. - Redirected logs to `./build/stdout.log` and updated log levels. - New `TestEnvironment` setup for integration testing. - Enhanced documentation with examples for server execution and MCP Inspector usage. - Introduced shadow JAR plugin for building runnable artifacts. ### Motivation and Context Improves server versatility and developer experience by enabling STDIO and refining setup. ## How Has This Been Tested? Unit tests, tested locally (see README.md in sample) **NB! sse plain configuration is not working at the moment** ## Breaking Changes No ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [ ] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions -->
This commit is contained in:
parent
e4bd866fdd
commit
dc4386f0f1
10 changed files with 172 additions and 69 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.shadow)
|
||||
application
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -28,4 +30,10 @@ tasks.test {
|
|||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
javaParameters = true
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xdebug",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
samples/kotlin-mcp-server/mcp-inspector-config.json
Normal file
25
samples/kotlin-mcp-server/mcp-inspector-config.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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") {
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
50
samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt
Normal file
50
samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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…
Reference in a new issue