Skip to content

Commit 458a49a

Browse files
authored
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
1 parent cae9130 commit 458a49a

File tree

10 files changed

+115
-144
lines changed

10 files changed

+115
-144
lines changed

samples/kotlin-mcp-client/gradle/libs.versions.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[versions]
22
anthropic = "2.9.0"
3-
kotlin = "2.2.20"
4-
ktor = "3.3.0"
5-
mcp-kotlin = "0.7.3"
3+
kotlin = "2.2.21"
4+
ktor = "3.2.3"
5+
mcp-kotlin = "0.7.4"
66
shadow = "9.2.2"
77
slf4j = "2.0.17"
88

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
# MCP Kotlin Server Sample
22

3-
A sample implementation of an MCP (Model Communication Protocol) server in Kotlin that demonstrates different server
4-
configurations and transport methods for both JVM and WASM targets.
3+
A sample implementation of an MCP (Model Context Protocol) server in Kotlin that demonstrates different server
4+
configurations and transport methods.
55

66
## Features
77

88
- Multiple server operation modes:
9-
- Standard I/O server (JVM only)
10-
- SSE (Server-Sent Events) server with plain configuration (JVM, WASM)
11-
- SSE server using Ktor plugin (JVM, WASM)
12-
- Multiplatform support
9+
- Standard I/O server
10+
- SSE (Server-Sent Events) server with plain configuration
11+
- SSE server using Ktor plugin
1312
- Built-in capabilities for:
1413
- Prompts management
1514
- Resources handling
@@ -19,31 +18,41 @@ configurations and transport methods for both JVM and WASM targets.
1918

2019
### Running the Server
2120

22-
You can run the server on the JVM or using Kotlin/WASM on Node.js.
21+
The server defaults to SSE mode with Ktor plugin on port 3001. You can customize the behavior using command-line arguments.
2322

23+
#### Default (SSE with Ktor plugin):
2424

25-
#### JVM:
25+
```bash
26+
./gradlew run
27+
```
2628

27-
To run the server on the JVM (defaults to SSE mode with Ktor plugin on port 3001):
29+
#### Standard I/O mode:
2830

2931
```bash
30-
./gradlew runJvm
32+
./gradlew run --args="--stdio"
3133
```
3234

33-
#### WASM:
35+
#### SSE with plain configuration:
36+
37+
```bash
38+
./gradlew run --args="--sse-server 3001"
39+
```
3440

35-
To run the server using Kotlin/WASM on Node.js (defaults to SSE mode with Ktor plugin on port 3001):
41+
#### SSE with Ktor plugin (custom port):
3642

3743
```bash
38-
./gradlew wasmJsNodeDevelopmentRun
44+
./gradlew run --args="--sse-server-ktor 3002"
3945
```
4046

4147
### Connecting to the Server
4248

43-
For servers on JVM or WASM:
49+
For SSE servers:
4450
1. Start the server
4551
2. Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) to connect to `http://localhost:<port>/sse`
4652

53+
For STDIO servers:
54+
- Connect using an MCP client that supports STDIO transport
55+
4756
## Server Capabilities
4857

4958
- **Prompts**: Supports prompt management with list change notifications
@@ -53,8 +62,14 @@ For servers on JVM or WASM:
5362
## Implementation Details
5463

5564
The server is implemented using:
56-
- Ktor for HTTP server functionality
65+
- Ktor for HTTP server functionality (SSE modes)
5766
- Kotlin coroutines for asynchronous operations
58-
- SSE for real-time communication
59-
- Standard I/O for command-line interface
60-
- Common Kotlin code shared between JVM and WASM targets
67+
- SSE for real-time communication in web contexts
68+
- Standard I/O for command-line interface and process-based communication
69+
70+
## Example Capabilities
71+
72+
The sample server demonstrates:
73+
- **Prompt**: "Kotlin Developer" - helps develop small Kotlin applications with a configurable project name
74+
- **Tool**: "kotlin-sdk-tool" - a simple test tool that returns a greeting
75+
- **Resource**: "Web Search" - a placeholder resource demonstrating resource handling
Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,26 @@
1-
@file:OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class)
2-
3-
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
4-
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
5-
61
plugins {
7-
alias(libs.plugins.kotlin.multiplatform)
2+
alias(libs.plugins.kotlin.jvm)
83
alias(libs.plugins.kotlin.serialization)
4+
application
95
}
106

117
group = "org.example"
128
version = "0.1.0"
139

14-
val jvmMainClass = "Main_jvmKt"
10+
application {
11+
mainClass.set("io.modelcontextprotocol.sample.server.MainKt")
12+
}
1513

16-
kotlin {
17-
jvmToolchain(17)
18-
jvm {
19-
binaries {
20-
executable {
21-
mainClass.set(jvmMainClass)
22-
}
23-
}
24-
val jvmJar by tasks.getting(org.gradle.jvm.tasks.Jar::class) {
25-
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
26-
doFirst {
27-
manifest {
28-
attributes["Main-Class"] = jvmMainClass
29-
}
14+
dependencies {
15+
implementation(libs.mcp.kotlin.server)
16+
implementation(libs.ktor.server.cio)
17+
implementation(libs.slf4j.simple)
18+
}
3019

31-
from(configurations["jvmRuntimeClasspath"].map { if (it.isDirectory) it else zipTree(it) })
32-
}
33-
}
34-
}
35-
wasmJs {
36-
nodejs()
37-
binaries.executable()
38-
}
20+
tasks.test {
21+
useJUnitPlatform()
22+
}
3923

40-
sourceSets {
41-
commonMain.dependencies {
42-
implementation(libs.mcp.kotlin.server)
43-
implementation(libs.ktor.server.cio)
44-
}
45-
jvmMain.dependencies {
46-
implementation(libs.slf4j.simple)
47-
}
48-
wasmJsMain.dependencies {}
49-
}
24+
kotlin {
25+
jvmToolchain(17)
5026
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[versions]
2-
kotlin = "2.2.20"
3-
ktor = "3.3.1"
4-
mcp-kotlin = "0.7.3"
2+
kotlin = "2.2.21"
3+
ktor = "3.2.3"
4+
mcp-kotlin = "0.7.4"
55
slf4j = "2.0.17"
66
serialization = "1.9.0"
77

@@ -11,5 +11,5 @@ mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-serv
1111
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
1212

1313
[plugins]
14-
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
14+
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
1515
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt

Lines changed: 0 additions & 50 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1-
import shared.runSseMcpServerUsingKtorPlugin
2-
import shared.runSseMcpServerWithPlainConfiguration
1+
package io.modelcontextprotocol.sample.server
2+
3+
import kotlinx.coroutines.runBlocking
34

45
/**
56
* Start sse-server mcp on port 3001.
67
*
78
* @param args
9+
* - "--stdio": Runs an MCP server using standard input/output.
810
* - "--sse-server-ktor <port>": Runs an SSE MCP server using Ktor plugin (default if no argument is provided).
911
* - "--sse-server <port>": Runs an SSE MCP server with a plain configuration.
1012
*/
11-
suspend fun main(args: Array<String>) {
13+
fun main(args: Array<String>): Unit = runBlocking {
1214
val command = args.firstOrNull() ?: "--sse-server-ktor"
1315
val port = args.getOrNull(1)?.toIntOrNull() ?: 3001
1416
when (command) {
17+
"--stdio" -> runMcpServerUsingStdio()
1518
"--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port)
1619
"--sse-server" -> runSseMcpServerWithPlainConfiguration(port)
1720
else -> {
1821
error("Unknown command: $command")
1922
}
2023
}
21-
}
24+
}

samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt renamed to samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package shared
1+
package io.modelcontextprotocol.sample.server
22

33
import io.ktor.http.HttpStatusCode
44
import io.ktor.server.application.install
@@ -25,7 +25,13 @@ import io.modelcontextprotocol.kotlin.sdk.server.Server
2525
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
2626
import io.modelcontextprotocol.kotlin.sdk.server.ServerSession
2727
import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport
28+
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
2829
import io.modelcontextprotocol.kotlin.sdk.server.mcp
30+
import kotlinx.coroutines.Job
31+
import kotlinx.coroutines.runBlocking
32+
import kotlinx.io.asSink
33+
import kotlinx.io.asSource
34+
import kotlinx.io.buffered
2935

3036
fun configureServer(): Server {
3137
val server = Server(
@@ -71,7 +77,7 @@ fun configureServer(): Server {
7177
name = "kotlin-sdk-tool",
7278
description = "A test tool",
7379
inputSchema = Tool.Input(),
74-
) { request ->
80+
) { _ ->
7581
CallToolResult(
7682
content = listOf(TextContent("Hello, world!")),
7783
)
@@ -94,10 +100,10 @@ fun configureServer(): Server {
94100
return server
95101
}
96102

97-
suspend fun runSseMcpServerWithPlainConfiguration(port: Int) {
103+
fun runSseMcpServerWithPlainConfiguration(port: Int) {
98104
val serverSessions = ConcurrentMap<String, ServerSession>()
99-
println("Starting sse server on port $port. ")
100-
println("Use inspector to connect to the http://localhost:$port/sse")
105+
println("Starting SSE server on port $port")
106+
println("Use inspector to connect to http://localhost:$port/sse")
101107

102108
val server = configureServer()
103109

@@ -106,21 +112,21 @@ suspend fun runSseMcpServerWithPlainConfiguration(port: Int) {
106112
routing {
107113
sse("/sse") {
108114
val transport = SseServerTransport("/message", this)
109-
110-
// For SSE, you can also add prompts/tools/resources if needed:
111-
// server.addTool(...), server.addPrompt(...), server.addResource(...)
112-
113115
val serverSession = server.connect(transport)
114-
serverSessions[transport.sessionId] = server.connect(transport)
116+
serverSessions[transport.sessionId] = serverSession
115117

116118
serverSession.onClose {
117-
println("Server closed")
119+
println("Server session closed for: ${transport.sessionId}")
118120
serverSessions.remove(transport.sessionId)
119121
}
120122
}
121123
post("/message") {
122-
println("Received Message")
123-
val sessionId: String = call.request.queryParameters["sessionId"]!!
124+
val sessionId: String? = call.request.queryParameters["sessionId"]
125+
if (sessionId == null) {
126+
call.respond(HttpStatusCode.BadRequest, "Missing sessionId parameter")
127+
return@post
128+
}
129+
124130
val transport = serverSessions[sessionId]?.transport as? SseServerTransport
125131
if (transport == null) {
126132
call.respond(HttpStatusCode.NotFound, "Session not found")
@@ -130,24 +136,47 @@ suspend fun runSseMcpServerWithPlainConfiguration(port: Int) {
130136
transport.handlePostMessage(call)
131137
}
132138
}
133-
}.startSuspend(wait = true)
139+
}.start(wait = true)
134140
}
135141

136142
/**
137-
* Starts an SSE (Server Sent Events) MCP server using the Ktor framework and the specified port.
143+
* Starts an SSE (Server-Sent Events) MCP server using the Ktor plugin.
138144
*
139-
* The url can be accessed in the MCP inspector at [http://localhost:$port]
145+
* This is the recommended approach for SSE servers as it simplifies configuration.
146+
* The URL can be accessed in the MCP inspector at http://localhost:[port]/sse
140147
*
141148
* @param port The port number on which the SSE MCP server will listen for client connections.
142-
* @return Unit This method does not return a value.
143149
*/
144-
suspend fun runSseMcpServerUsingKtorPlugin(port: Int) {
145-
println("Starting sse server on port $port")
146-
println("Use inspector to connect to the http://localhost:$port/sse")
150+
fun runSseMcpServerUsingKtorPlugin(port: Int) {
151+
println("Starting SSE server on port $port")
152+
println("Use inspector to connect to http://localhost:$port/sse")
147153

148154
embeddedServer(CIO, host = "127.0.0.1", port = port) {
149155
mcp {
150156
return@mcp configureServer()
151157
}
152-
}.startSuspend(wait = true)
158+
}.start(wait = true)
159+
}
160+
161+
/**
162+
* Starts an MCP server using Standard I/O transport.
163+
*
164+
* This mode is useful for process-based communication where the server
165+
* communicates via stdin/stdout with a parent process or client.
166+
*/
167+
fun runMcpServerUsingStdio() {
168+
val server = configureServer()
169+
val transport = StdioServerTransport(
170+
inputStream = System.`in`.asSource().buffered(),
171+
outputStream = System.out.asSink().buffered()
172+
)
173+
174+
runBlocking {
175+
server.connect(transport)
176+
val done = Job()
177+
server.onClose {
178+
done.complete()
179+
}
180+
done.join()
181+
}
153182
}

0 commit comments

Comments
 (0)