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
6 changes: 3 additions & 3 deletions samples/kotlin-mcp-client/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
51 changes: 33 additions & 18 deletions samples/kotlin-mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.

#### Default (SSE with Ktor plugin):

#### JVM:
```bash
./gradlew run
```

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

```bash
./gradlew runJvm
./gradlew run --args="--stdio"
```

#### WASM:
#### SSE with plain configuration:

```bash
./gradlew run --args="--sse-server 3001"
```

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

```bash
./gradlew wasmJsNodeDevelopmentRun
./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
Expand All @@ -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
54 changes: 15 additions & 39 deletions samples/kotlin-mcp-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}

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
}
dependencies {
implementation(libs.mcp.kotlin.server)
implementation(libs.ktor.server.cio)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use Netty as server engine, because CIO does not support Http2 and Netty is de-facto a standard for high-performance servers for decades.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is essentially crossplatform
Therefore, I think it’s better to keep cio as the default ktor engine, since ktor itself uses it in its own examples

implementation(libs.slf4j.simple)
}

from(configurations["jvmRuntimeClasspath"].map { if (it.isDirectory) it else zipTree(it) })
}
}
}
wasmJs {
nodejs()
binaries.executable()
}
tasks.test {
useJUnitPlatform()
}

sourceSets {
commonMain.dependencies {
implementation(libs.mcp.kotlin.server)
implementation(libs.ktor.server.cio)
}
jvmMain.dependencies {
implementation(libs.slf4j.simple)
}
wasmJsMain.dependencies {}
}
kotlin {
jvmToolchain(17)
}
8 changes: 4 additions & 4 deletions samples/kotlin-mcp-server/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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" }
50 changes: 0 additions & 50 deletions samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import shared.runSseMcpServerUsingKtorPlugin
import shared.runSseMcpServerWithPlainConfiguration
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.
*/
suspend fun main(args: Array<String>) {
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")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package shared
package io.modelcontextprotocol.sample.server
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
Expand All @@ -25,7 +25,13 @@ 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(
Expand Down Expand Up @@ -71,7 +77,7 @@ fun configureServer(): Server {
name = "kotlin-sdk-tool",
description = "A test tool",
inputSchema = Tool.Input(),
) { request ->
) { _ ->
CallToolResult(
content = listOf(TextContent("Hello, world!")),
)
Expand All @@ -94,10 +100,10 @@ fun configureServer(): Server {
return server
}

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

val server = configureServer()

Expand All @@ -106,21 +112,21 @@ suspend fun runSseMcpServerWithPlainConfiguration(port: Int) {
routing {
sse("/sse") {
val transport = SseServerTransport("/message", this)

// For SSE, you can also add prompts/tools/resources if needed:
// server.addTool(...), server.addPrompt(...), server.addResource(...)

val serverSession = server.connect(transport)
serverSessions[transport.sessionId] = server.connect(transport)
serverSessions[transport.sessionId] = serverSession

serverSession.onClose {
println("Server closed")
println("Server session closed for: ${transport.sessionId}")
serverSessions.remove(transport.sessionId)
}
}
post("/message") {
println("Received Message")
val sessionId: String = call.request.queryParameters["sessionId"]!!
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")
Expand All @@ -130,24 +136,47 @@ suspend fun runSseMcpServerWithPlainConfiguration(port: Int) {
transport.handlePostMessage(call)
}
}
}.startSuspend(wait = true)
}.start(wait = true)
}

/**
* Starts an SSE (Server Sent Events) MCP server using the Ktor framework and the specified port.
* Starts an SSE (Server-Sent Events) MCP server using the Ktor plugin.
*
* The url can be accessed in the MCP inspector at [http://localhost:$port]
* 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.
* @return Unit This method does not return a value.
*/
suspend fun runSseMcpServerUsingKtorPlugin(port: Int) {
println("Starting sse server on port $port")
println("Use inspector to connect to the http://localhost:$port/sse")
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()
}
}.startSuspend(wait = true)
}.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()
}
}
Loading
Loading