Skip to content

Support for MCP definations using kotlin annotation #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
languageVersion = JavaLanguageVersion.of(17) // Downgrade to JDK 17 which is more likely to be available
languageVersion = JavaLanguageVersion.of(17)

}

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