add ios and wasm targets (#81)

* add wasm and ios targets

* add yarn.lock file to gitignore

* clean up code

* fix typo in method name

* add wasm server example

* update jrelease publishing for klibs

* update gradle-publish workflow to use macos runner

* update README to reflect multiplatform support and clarify server setup for JVM and WASM

* remove the old main file from the kotlin-mcp-server example

* update api dump

* Refactor `build.gradle.kts`

* clean up imports and remove unnecessary charset specification in tests

* add JS target configuration and update API dump
This commit is contained in:
Pavel Gorgulov 2025-05-29 07:01:50 +02:00 • committed by GitHub
commit 0cff2ca718
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 705 additions and 478 deletions

View file

@ -1,13 +1,15 @@
# MCP Kotlin Server Sample
A sample implementation of an MCP (Model Communication Protocol) server in Kotlin that demonstrates different server configurations and transport methods.
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.
## Features
- Multiple server operation modes:
- Standard I/O server
- SSE (Server-Sent Events) server with plain configuration
- SSE server using Ktor plugin
- 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
- Built-in capabilities for:
- Prompts management
- Resources handling
@ -17,17 +19,30 @@ A sample implementation of an MCP (Model Communication Protocol) server in Kotli
### Running the Server
To run the server in SSE mode on the port 3001, run:
You can run the server on the JVM or using Kotlin/WASM on Node.js.
#### JVM:
To run the server on the JVM (defaults to SSE mode with Ktor plugin on port 3001):
```bash
./gradlew run
./gradlew runJvm
```
#### WASM:
To run the server using Kotlin/WASM on Node.js (defaults to SSE mode with Ktor plugin on port 3001):
```bash
./gradlew wasmJsNodeDevelopmentRun
```
### Connecting to the Server
For SSE servers (both plain and Ktor plugin versions):
For servers on JVM or WASM:
1. Start the server
2. Use the MCP inspector to connect to `http://localhost:<port>/sse`
2. Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) to connect to `http://localhost:<port>/sse`
## Server Capabilities
@ -42,3 +57,4 @@ The server is implemented using:
- 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

View file

@ -1,35 +1,53 @@
@file:OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class)
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("jvm") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
application
kotlin("multiplatform") version "2.1.20"
kotlin("plugin.serialization") version "2.1.20"
}
application {
mainClass.set("MainKt")
}
group = "org.example"
version = "0.1.0"
dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.5.0")
implementation("org.slf4j:slf4j-nop:2.0.9")
repositories {
mavenCentral()
}
tasks.test {
useJUnitPlatform()
}
val jvmMainClass = "Main_jvmKt"
kotlin {
jvmToolchain(21)
}
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
}
tasks.jar {
manifest {
attributes["Main-Class"] = "MainKt"
from(configurations["jvmRuntimeClasspath"].map { if (it.isDirectory) it else zipTree(it) })
}
}
}
wasmJs {
nodejs()
binaries.executable()
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
sourceSets {
commonMain.dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.5.0")
}
jvmMain.dependencies {
implementation("org.slf4j:slf4j-nop:2.0.9")
}
wasmJsMain.dependencies {}
}
}

View file

@ -1 +1,3 @@
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx2G

View file

@ -2,11 +2,3 @@ plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "kotlin-mcp-server"
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/kotlin-mcp-sdk/sdk")
}
}

View file

@ -0,0 +1,151 @@
package shared
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.SseServerTransport
import io.modelcontextprotocol.kotlin.sdk.server.mcp
import kotlin.collections.set
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()
) { request ->
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
}
suspend fun runSseMcpServerWithPlainConfiguration(port: Int): Unit {
val servers = ConcurrentMap<String, Server>()
println("Starting sse server on port $port. ")
println("Use inspector to connect to the http://localhost:$port/sse")
embeddedServer(CIO, host = "0.0.0.0", port = port) {
install(SSE)
routing {
sse("/sse") {
val transport = SseServerTransport("/message", this)
val server = configureServer()
// For SSE, you can also add prompts/tools/resources if needed:
// server.addTool(...), server.addPrompt(...), server.addResource(...)
servers[transport.sessionId] = server
server.onClose {
println("Server closed")
servers.remove(transport.sessionId)
}
server.connect(transport)
}
post("/message") {
println("Received Message")
val sessionId: String = call.request.queryParameters["sessionId"]!!
val transport = servers[sessionId]?.transport as? SseServerTransport
if (transport == null) {
call.respond(HttpStatusCode.NotFound, "Session not found")
return@post
}
transport.handlePostMessage(call)
}
}
}.startSuspend(wait = true)
}
/**
* Starts an SSE (Server Sent Events) MCP server using the Ktor framework and the specified port.
*
* The url can be accessed in the MCP inspector at [http://localhost:$port]
*
* @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): Unit {
println("Starting sse server on port $port")
println("Use inspector to connect to the http://localhost:$port/sse")
embeddedServer(CIO, host = "0.0.0.0", port = port) {
mcp {
return@mcp configureServer()
}
}.startSuspend(wait = true)
}

View file

@ -0,0 +1,50 @@
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,21 @@
import shared.runSseMcpServerUsingKtorPlugin
import shared.runSseMcpServerWithPlainConfiguration
/**
* Start sse-server mcp on port 3001.
*
* @param args
* - "--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>) {
val command = args.firstOrNull() ?: "--sse-server-ktor"
val port = args.getOrNull(1)?.toIntOrNull() ?: 3001
when (command) {
"--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port)
"--sse-server" -> runSseMcpServerWithPlainConfiguration(port)
else -> {
error("Unknown command: $command")
}
}
}

View file

@ -67,12 +67,9 @@ fun `run mcp server`() {
required = listOf("state")
)
) { request ->
val state = request.arguments["state"]?.jsonPrimitive?.content
if (state == null) {
return@addTool CallToolResult(
content = listOf(TextContent("The 'state' parameter is required."))
)
}
val state = request.arguments["state"]?.jsonPrimitive?.content ?: return@addTool CallToolResult(
content = listOf(TextContent("The 'state' parameter is required."))
)
val alerts = httpClient.getAlerts(state)