diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 8652aca3..7490ed61 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -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" diff --git a/samples/kotlin-mcp-server/README.md b/samples/kotlin-mcp-server/README.md index ee87ab76..13a2976e 100644 --- a/samples/kotlin-mcp-server/README.md +++ b/samples/kotlin-mcp-server/README.md @@ -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. +#### 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:/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 diff --git a/samples/kotlin-mcp-server/build.gradle.kts b/samples/kotlin-mcp-server/build.gradle.kts index e85100d7..0bf45d86 100644 --- a/samples/kotlin-mcp-server/build.gradle.kts +++ b/samples/kotlin-mcp-server/build.gradle.kts @@ -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) + 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) } diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index caccfdf9..37eb1609 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -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" } diff --git a/samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt b/samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt deleted file mode 100644 index 509aa569..00000000 --- a/samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt +++ /dev/null @@ -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 ": Runs an SSE MCP server using Ktor plugin (default if no argument is provided). - * - "--sse-server ": Runs an SSE MCP server with a plain configuration. - */ -fun main(args: Array): 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") - } -} \ No newline at end of file diff --git a/samples/kotlin-mcp-server/src/wasmJsMain/kotlin/main.wasmJs.kt b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt similarity index 71% rename from samples/kotlin-mcp-server/src/wasmJsMain/kotlin/main.wasmJs.kt rename to samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt index 7aa12190..d01def55 100644 --- a/samples/kotlin-mcp-server/src/wasmJsMain/kotlin/main.wasmJs.kt +++ b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt @@ -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 ": Runs an SSE MCP server using Ktor plugin (default if no argument is provided). * - "--sse-server ": Runs an SSE MCP server with a plain configuration. */ -suspend fun main(args: Array) { +fun main(args: Array): 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") } } -} \ No newline at end of file +} diff --git a/samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt similarity index 69% rename from samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt rename to samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt index 227c83d9..45a70054 100644 --- a/samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt +++ b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt @@ -1,4 +1,4 @@ -package shared +package io.modelcontextprotocol.sample.server import io.ktor.http.HttpStatusCode import io.ktor.server.application.install @@ -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( @@ -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!")), ) @@ -94,10 +100,10 @@ fun configureServer(): Server { return server } -suspend fun runSseMcpServerWithPlainConfiguration(port: Int) { +fun runSseMcpServerWithPlainConfiguration(port: Int) { val serverSessions = ConcurrentMap() - 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() @@ -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") @@ -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() + } } diff --git a/samples/kotlin-mcp-server/src/jvmMain/resources/simplelogger.properties b/samples/kotlin-mcp-server/src/main/resources/simplelogger.properties similarity index 100% rename from samples/kotlin-mcp-server/src/jvmMain/resources/simplelogger.properties rename to samples/kotlin-mcp-server/src/main/resources/simplelogger.properties diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 89217240..87efc887 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -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" diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt index c92ebec4..eb921a92 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt @@ -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