Fix kotlin-mcp-server example and update samples (#345)

- updated the sdk version in all examples
- fixed `kotlin-mcp-server` example (#331)

## Motivation and Context
Removed the WASM example, as it duplicates the JVM example and only adds
unnecessary complexity

## How Has This Been Tested?
Locally

## Breaking Changes
No

## Types of changes
- [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
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [x] I have added or updated documentation as needed
This commit is contained in:
Pavel Gorgulov 2025-10-31 12:21:46 +01:00 • committed by GitHub
commit 458a49a5fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 115 additions and 144 deletions

View file

@ -1,8 +1,8 @@
[versions]
anthropic = "2.9.0"
kotlin = "2.2.20"
ktor = "3.3.0"
mcp-kotlin = "0.7.3"
kotlin = "2.2.21"
ktor = "3.2.3"
mcp-kotlin = "0.7.4"
shadow = "9.2.2"
slf4j = "2.0.17"

View file

@ -1,15 +1,14 @@
# MCP Kotlin Server Sample
A sample implementation of an MCP (Model Communication Protocol) server in Kotlin that demonstrates different server
configurations and transport methods for both JVM and WASM targets.
A sample implementation of an MCP (Model Context Protocol) server in Kotlin that demonstrates different server
configurations and transport methods.
## Features
- Multiple server operation modes:
- Standard I/O server (JVM only)
- SSE (Server-Sent Events) server with plain configuration (JVM, WASM)
- SSE server using Ktor plugin (JVM, WASM)
- Multiplatform support
- Standard I/O server
- SSE (Server-Sent Events) server with plain configuration
- SSE server using Ktor plugin
- Built-in capabilities for:
- Prompts management
- Resources handling
@ -19,31 +18,41 @@ configurations and transport methods for both JVM and WASM targets.
### Running the Server
You can run the server on the JVM or using Kotlin/WASM on Node.js.
The server defaults to SSE mode with Ktor plugin on port 3001. You can customize the behavior using command-line arguments.
#### JVM:
To run the server on the JVM (defaults to SSE mode with Ktor plugin on port 3001):
#### Default (SSE with Ktor plugin):
```bash
./gradlew runJvm
./gradlew run
```
#### WASM:
To run the server using Kotlin/WASM on Node.js (defaults to SSE mode with Ktor plugin on port 3001):
#### Standard I/O mode:
```bash
./gradlew wasmJsNodeDevelopmentRun
./gradlew run --args="--stdio"
```
#### SSE with plain configuration:
```bash
./gradlew run --args="--sse-server 3001"
```
#### SSE with Ktor plugin (custom port):
```bash
./gradlew run --args="--sse-server-ktor 3002"
```
### Connecting to the Server
For servers on JVM or WASM:
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
## Server Capabilities
- **Prompts**: Supports prompt management with list change notifications
@ -53,8 +62,14 @@ For servers on JVM or WASM:
## Implementation Details
The server is implemented using:
- Ktor for HTTP server functionality
- Ktor for HTTP server functionality (SSE modes)
- Kotlin coroutines for asynchronous operations
- SSE for real-time communication
- Standard I/O for command-line interface
- Common Kotlin code shared between JVM and WASM targets
- SSE for real-time communication in web contexts
- Standard I/O for command-line interface and process-based communication
## Example Capabilities
The sample server demonstrates:
- **Prompt**: "Kotlin Developer" - helps develop small Kotlin applications with a configurable project name
- **Tool**: "kotlin-sdk-tool" - a simple test tool that returns a greeting
- **Resource**: "Web Search" - a placeholder resource demonstrating resource handling

View file

@ -1,50 +1,26 @@
@file:OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
application
}
group = "org.example"
version = "0.1.0"
val jvmMainClass = "Main_jvmKt"
application {
mainClass.set("io.modelcontextprotocol.sample.server.MainKt")
}
dependencies {
implementation(libs.mcp.kotlin.server)
implementation(libs.ktor.server.cio)
implementation(libs.slf4j.simple)
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(17)
jvm {
binaries {
executable {
mainClass.set(jvmMainClass)
}
}
val jvmJar by tasks.getting(org.gradle.jvm.tasks.Jar::class) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
doFirst {
manifest {
attributes["Main-Class"] = jvmMainClass
}
from(configurations["jvmRuntimeClasspath"].map { if (it.isDirectory) it else zipTree(it) })
}
}
}
wasmJs {
nodejs()
binaries.executable()
}
sourceSets {
commonMain.dependencies {
implementation(libs.mcp.kotlin.server)
implementation(libs.ktor.server.cio)
}
jvmMain.dependencies {
implementation(libs.slf4j.simple)
}
wasmJsMain.dependencies {}
}
}

View file

@ -1,7 +1,7 @@
[versions]
kotlin = "2.2.20"
ktor = "3.3.1"
mcp-kotlin = "0.7.3"
kotlin = "2.2.21"
ktor = "3.2.3"
mcp-kotlin = "0.7.4"
slf4j = "2.0.17"
serialization = "1.9.0"
@ -11,5 +11,5 @@ mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-serv
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

View file

@ -1,50 +0,0 @@
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.io.asSink
import kotlinx.io.asSource
import kotlinx.io.buffered
import shared.configureServer
import shared.runSseMcpServerUsingKtorPlugin
import shared.runSseMcpServerWithPlainConfiguration
/**
* Start sse-server mcp on port 3001.
*
* @param args
* - "--stdio": Runs an MCP server using standard input/output.
* - "--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"
val port = args.getOrNull(1)?.toIntOrNull() ?: 3001
when (command) {
"--stdio" -> runMcpServerUsingStdio()
"--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port)
"--sse-server" -> runSseMcpServerWithPlainConfiguration(port)
else -> {
System.err.println("Unknown command: $command")
}
}
}
fun runMcpServerUsingStdio() {
// Note: The server will handle listing prompts, tools, and resources automatically.
// The handleListResourceTemplates will return empty as defined in the Server code.
val server = configureServer()
val transport = StdioServerTransport(
inputStream = System.`in`.asSource().buffered(),
outputStream = System.out.asSink().buffered()
)
runBlocking {
server.connect(transport)
val done = Job()
server.onClose {
done.complete()
}
done.join()
println("Server closed")
}
}

View file

@ -0,0 +1,24 @@
package io.modelcontextprotocol.sample.server
import kotlinx.coroutines.runBlocking
/**
* Start sse-server mcp on port 3001.
*
* @param args
* - "--stdio": Runs an MCP server using standard input/output.
* - "--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"
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")
}
}
}

View file

@ -0,0 +1,182 @@
package io.modelcontextprotocol.sample.server
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.response.respond
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import io.ktor.server.sse.SSE
import io.ktor.server.sse.sse
import io.ktor.util.collections.ConcurrentMap
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
import io.modelcontextprotocol.kotlin.sdk.PromptMessage
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
import io.modelcontextprotocol.kotlin.sdk.Role
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.TextResourceContents
import io.modelcontextprotocol.kotlin.sdk.Tool
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.ServerSession
import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
import io.modelcontextprotocol.kotlin.sdk.server.mcp
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.io.asSink
import kotlinx.io.asSource
import kotlinx.io.buffered
fun configureServer(): Server {
val server = Server(
Implementation(
name = "mcp-kotlin test server",
version = "0.1.0",
),
ServerOptions(
capabilities = ServerCapabilities(
prompts = ServerCapabilities.Prompts(listChanged = true),
resources = ServerCapabilities.Resources(subscribe = true, listChanged = true),
tools = ServerCapabilities.Tools(listChanged = true),
),
),
)
server.addPrompt(
name = "Kotlin Developer",
description = "Develop small kotlin applications",
arguments = listOf(
PromptArgument(
name = "Project Name",
description = "Project name for the new project",
required = true,
),
),
) { request ->
GetPromptResult(
"Description for ${request.name}",
messages = listOf(
PromptMessage(
role = Role.user,
content = TextContent(
"Develop a kotlin project named <name>${request.arguments?.get("Project Name")}</name>",
),
),
),
)
}
// Add a tool
server.addTool(
name = "kotlin-sdk-tool",
description = "A test tool",
inputSchema = Tool.Input(),
) { _ ->
CallToolResult(
content = listOf(TextContent("Hello, world!")),
)
}
// Add a resource
server.addResource(
uri = "https://search.com/",
name = "Web Search",
description = "Web search engine",
mimeType = "text/html",
) { request ->
ReadResourceResult(
contents = listOf(
TextResourceContents("Placeholder content for ${request.uri}", request.uri, "text/html"),
),
)
}
return server
}
fun runSseMcpServerWithPlainConfiguration(port: Int) {
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) {
install(SSE)
routing {
sse("/sse") {
val transport = SseServerTransport("/message", this)
val serverSession = server.connect(transport)
serverSessions[transport.sessionId] = serverSession
serverSession.onClose {
println("Server session closed for: ${transport.sessionId}")
serverSessions.remove(transport.sessionId)
}
}
post("/message") {
val sessionId: String? = call.request.queryParameters["sessionId"]
if (sessionId == null) {
call.respond(HttpStatusCode.BadRequest, "Missing sessionId parameter")
return@post
}
val transport = serverSessions[sessionId]?.transport as? SseServerTransport
if (transport == null) {
call.respond(HttpStatusCode.NotFound, "Session not found")
return@post
}
transport.handlePostMessage(call)
}
}
}.start(wait = true)
}
/**
* Starts an SSE (Server-Sent Events) MCP server using the Ktor plugin.
*
* This is the recommended approach for SSE servers as it simplifies configuration.
* The URL can be accessed in the MCP inspector at http://localhost:[port]/sse
*
* @param port The port number on which the SSE MCP server will listen for client connections.
*/
fun runSseMcpServerUsingKtorPlugin(port: Int) {
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)
}
/**
* Starts an MCP server using Standard I/O transport.
*
* This mode is useful for process-based communication where the server
* communicates via stdin/stdout with a parent process or client.
*/
fun runMcpServerUsingStdio() {
val server = configureServer()
val transport = StdioServerTransport(
inputStream = System.`in`.asSource().buffered(),
outputStream = System.out.asSink().buffered()
)
runBlocking {
server.connect(transport)
val done = Job()
server.onClose {
done.complete()
}
done.join()
}
}

View file

@ -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

View file

@ -1,10 +1,10 @@
[versions]
collections-immutable = "0.4.0"
coroutines = "1.10.2"
kotlin = "2.2.20"
ktor = "3.3.0"
kotlin = "2.2.21"
ktor = "3.2.3"
logging = "7.0.13"
mcp-kotlin = "0.7.3"
mcp-kotlin = "0.7.4"
shadow = "9.2.2"
slf4j = "2.0.17"

View file

@ -7,7 +7,6 @@ import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.headers
import io.ktor.serialization.kotlinx.json.json
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.streams.asInput
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.Implementation
@ -19,7 +18,6 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.io.Sink
import kotlinx.io.asSink
import kotlinx.io.buffered
import kotlinx.serialization.json.Json