Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
8 changes: 8 additions & 0 deletions .idea/artifacts/kotlin_sdk_jvm_0_4_0.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 17 additions & 1 deletion api/kotlin-sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -2730,6 +2730,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt {
public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function0;)V
}

public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/server/McpParam : java/lang/annotation/Annotation {
public abstract fun description ()Ljava/lang/String;
public abstract fun required ()Z
public abstract fun type ()Ljava/lang/String;
}

public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/server/McpTool : java/lang/annotation/Annotation {
public abstract fun description ()Ljava/lang/String;
public abstract fun name ()Ljava/lang/String;
public abstract fun required ()[Ljava/lang/String;
}

public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt {
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt;
Expand Down Expand Up @@ -2792,7 +2804,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextp
public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public fun onClose ()V
public final fun onClose (Lkotlin/jvm/functions/Function0;)V
public final fun onInitalized (Lkotlin/jvm/functions/Function0;)V
public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V
public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand All @@ -2801,6 +2813,10 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextp
public final fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotationsKt {
public static final fun registerToolFromAnnotatedFunction (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/Object;Lkotlin/reflect/KFunction;Lio/modelcontextprotocol/kotlin/sdk/server/McpTool;)V
}

public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions {
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Z)V
public synthetic fun <init> (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
9 changes: 8 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jreleaser.model.Active
import org.gradle.jvm.toolchain.JavaLanguageVersion

plugins {
alias(libs.plugins.kotlin.multiplatform)
Expand Down Expand Up @@ -196,7 +197,9 @@ kotlin {

explicitApi = ExplicitApiMode.Strict

jvmToolchain(21)
jvmToolchain {
languageVersion = JavaLanguageVersion.of(17) // Downgrade to JDK 17 which is more likely to be available
}

sourceSets {
commonMain {
Expand All @@ -209,6 +212,7 @@ kotlin {
api(libs.ktor.server.websockets)

implementation(libs.kotlin.logging)
implementation(libs.kotlin.reflect)
}
}

Expand All @@ -219,6 +223,7 @@ kotlin {
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotlinx.coroutines.debug)
implementation(libs.kotest.assertions.json)
implementation(libs.kotlin.reflect)
}
}

Expand All @@ -230,3 +235,5 @@ kotlin {
}
}
}


1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ kotest = "5.9.1"
# Kotlinx libraries
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" }
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }

# Ktor
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
Expand Down
97 changes: 72 additions & 25 deletions samples/weather-stdio-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ java -jar build/libs/<your-jar-name>.jar

## Tool Implementation

The project registers two MCP tools using the Kotlin MCP SDK. Below is an overview of the core tool implementations:
The project provides two different approaches to register MCP tools using the Kotlin MCP SDK:

### 1. Weather Forecast Tool
### Traditional Approach

This tool fetches the weather forecast for a specific latitude and longitude using the `weather.gov` API.
The traditional approach uses the `addTool` method to register tools with explicit schema definitions.

Example tool registration in Kotlin:
#### 1. Weather Forecast Tool

```kotlin
server.addTool(
Expand All @@ -60,24 +60,22 @@ server.addTool(
Get weather forecast for a specific latitude/longitude
""".trimIndent(),
inputSchema = Tool.Input(
properties = JsonObject(
mapOf(
"latitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
"longitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
)
),
properties = buildJsonObject {
putJsonObject("latitude") {
put("type", "number")
}
putJsonObject("longitude") {
put("type", "number")
}
},
required = listOf("latitude", "longitude")
)
) { request ->
// Implementation tool
}
```

### 2. Weather Alerts Tool

This tool retrieves active weather alerts for a US state.

Example tool registration in Kotlin:
#### 2. Weather Alerts Tool

```kotlin
server.addTool(
Expand All @@ -86,23 +84,72 @@ server.addTool(
Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)
""".trimIndent(),
inputSchema = Tool.Input(
properties = JsonObject(
mapOf(
"state" to JsonObject(
mapOf(
"type" to JsonPrimitive("string"),
"description" to JsonPrimitive("Two-letter US state code (e.g. CA, NY)")
)
),
)
),
properties = buildJsonObject {
putJsonObject("state") {
put("type", "string")
put("description", "Two-letter US state code (e.g. CA, NY)")
}
},
required = listOf("state")
)
) { request ->
// Implementation tool
}
```

### Annotation-Based Approach

The project also demonstrates an alternative, more idiomatic approach using Kotlin annotations. This approach simplifies tool definition by leveraging Kotlin's type system and reflection.

To use the annotation-based approach, run the server with:
```shell
java -jar build/libs/<your-jar-name>.jar --use-annotations
```

#### Tool implementation with annotations:

```kotlin
class WeatherToolsAnnotated(private val httpClient: HttpClient) {

@McpTool(
name = "get_alerts",
description = "Get weather alerts for a US state"
)
suspend fun getAlerts(
@McpParam(
description = "Two-letter US state code (e.g. CA, NY)",
type = "string"
) state: String
): CallToolResult {
// Implementation
}

@McpTool(
name = "get_forecast",
description = "Get weather forecast for a specific latitude/longitude"
)
suspend fun getForecast(
@McpParam(description = "The latitude coordinate") latitude: Double,
@McpParam(description = "The longitude coordinate") longitude: Double
): CallToolResult {
// Implementation
}
}
```

Then register the tools using:

```kotlin
val weatherTools = WeatherToolsAnnotated(httpClient)
server.registerAnnotatedTools(weatherTools)
```

This approach provides several benefits:
- More idiomatic Kotlin code
- Parameter types are automatically inferred from Kotlin's type system
- Reduced boilerplate for tool registration
- Better IDE support with autocompletion and compile-time checking

## Client Integration

### Kotlin Client Example
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.modelcontextprotocol.sample.server

import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.modelcontextprotocol.kotlin.sdk.*
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
import io.modelcontextprotocol.kotlin.sdk.server.registerAnnotatedTools
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.io.asSink
import kotlinx.io.buffered
import kotlinx.serialization.json.*

/**
* Alternative implementation of the Weather MCP server using annotations.
* This demonstrates how to use @McpTool annotations to simplify tool registration.
*/
fun `run annotated mcp server`() {
// Base URL for the Weather API
val baseUrl = "https://api.weather.gov"

// Create an HTTP client with a default request configuration and JSON content negotiation
val httpClient = HttpClient {
defaultRequest {
url(baseUrl)
headers {
append("Accept", "application/geo+json")
append("User-Agent", "WeatherApiClient/1.0")
}
contentType(ContentType.Application.Json)
}
// Install content negotiation plugin for JSON serialization/deserialization
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
}

// Create the MCP Server instance
val server = Server(
Implementation(
name = "weather-annotated",
version = "1.0.0"
),
ServerOptions(
capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
)
)

// Create an instance of our annotated tools class
val weatherTools = WeatherToolsAnnotated(httpClient)

// Register all annotated tools from the weatherTools instance
server.registerAnnotatedTools(weatherTools)

// Create a transport using standard IO for server communication
val transport = StdioServerTransport(
System.`in`.asInput(),
System.out.asSink().buffered()
)

runBlocking {
server.connect(transport)
val done = Job()
server.onClose {
done.complete()
}
done.join()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.modelcontextprotocol.sample.server

import io.ktor.client.*
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.server.McpParam
import io.modelcontextprotocol.kotlin.sdk.server.McpTool
import io.modelcontextprotocol.kotlin.sdk.server.registerAnnotatedTools

/**
* Example class demonstrating the use of McpTool annotations.
*/
class WeatherToolsAnnotated(private val httpClient: HttpClient) {

/**
* Gets weather alerts for a specified US state using the @McpTool annotation.
*/
@McpTool(
name = "get_alerts",
description = "Get weather alerts for a US state"
)
suspend fun getAlerts(
@McpParam(
description = "Two-letter US state code (e.g. CA, NY)",
type = "string"
) state: String
): CallToolResult {
if (state.isEmpty()) {
return CallToolResult(
content = listOf(TextContent("The 'state' parameter is required."))
)
}

val alerts = httpClient.getAlerts(state)
return CallToolResult(content = alerts.map { TextContent(it) })
}

/**
* Gets weather forecast for specified coordinates using the @McpTool annotation.
*/
@McpTool(
name = "get_forecast",
description = "Get weather forecast for a specific latitude/longitude"
)
suspend fun getForecast(
@McpParam(description = "The latitude coordinate") latitude: Double,
@McpParam(description = "The longitude coordinate") longitude: Double
): CallToolResult {
val forecast = httpClient.getForecast(latitude, longitude)
return CallToolResult(content = forecast.map { TextContent(it) })
}

/**
* Gets brief weather summary using the @McpTool annotation with default name.
*/
@McpTool(
description = "Get a brief weather summary for a location"
)
suspend fun getWeatherSummary(
@McpParam(description = "City name") city: String,
@McpParam(description = "Temperature unit (celsius/fahrenheit)", required = false) unit: String = "celsius"
): String {
return "Weather summary for $city: Sunny, 25° $unit"
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
package io.modelcontextprotocol.sample.server

fun main() = `run mcp server`()
fun main(args: Array<String>) {
val useAnnotations = args.contains("--use-annotations")

if (useAnnotations) {
println("Starting annotated MCP Weather server...")
`run annotated mcp server`()
} else {
println("Starting traditional MCP Weather server...")
`run mcp server`()
}
}
Loading