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:
parent
7ce665b931
commit
0cff2ca718
8 changed files with 705 additions and 478 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
kotlin.code.style=official
|
||||
|
||||
kotlin.daemon.jvmargs=-Xmx2G
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
151
samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt
Normal file
151
samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt
Normal 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)
|
||||
}
|
||||
50
samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt
Normal file
50
samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue