diff --git a/.idea/artifacts/kotlin_sdk_jvm_0_4_0.xml b/.idea/artifacts/kotlin_sdk_jvm_0_4_0.xml
new file mode 100644
index 0000000..b99eca2
--- /dev/null
+++ b/.idea/artifacts/kotlin_sdk_jvm_0_4_0.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/api/kotlin-sdk.api b/api/kotlin-sdk.api
index 4f0b20c..7f80350 100644
--- a/api/kotlin-sdk.api
+++ b/api/kotlin-sdk.api
@@ -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 (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt;
@@ -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;
@@ -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 (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Z)V
public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
diff --git a/build.gradle.kts b/build.gradle.kts
index 18f8f94..964e3d5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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)
@@ -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 {
@@ -209,6 +212,7 @@ kotlin {
api(libs.ktor.server.websockets)
implementation(libs.kotlin.logging)
+ implementation(libs.kotlin.reflect)
}
}
@@ -219,6 +223,7 @@ kotlin {
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotlinx.coroutines.debug)
implementation(libs.kotest.assertions.json)
+ implementation(libs.kotlin.reflect)
}
}
@@ -230,3 +235,5 @@ kotlin {
}
}
}
+
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bbb4098..38c79ba 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }
diff --git a/samples/weather-stdio-server/README.md b/samples/weather-stdio-server/README.md
index d1edd7f..e89b30f 100644
--- a/samples/weather-stdio-server/README.md
+++ b/samples/weather-stdio-server/README.md
@@ -45,13 +45,13 @@ java -jar build/libs/.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(
@@ -60,12 +60,14 @@ 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 ->
@@ -73,11 +75,7 @@ server.addTool(
}
```
-### 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(
@@ -86,16 +84,12 @@ 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 ->
@@ -103,6 +97,59 @@ server.addTool(
}
```
+### 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/.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
diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedMcpWeatherServer.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedMcpWeatherServer.kt
new file mode 100644
index 0000000..4a32cc1
--- /dev/null
+++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedMcpWeatherServer.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedToolsExample.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedToolsExample.kt
new file mode 100644
index 0000000..f398319
--- /dev/null
+++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/AnnotatedToolsExample.kt
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt
index 21100dd..74cd566 100644
--- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt
+++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt
@@ -1,3 +1,13 @@
package io.modelcontextprotocol.sample.server
-fun main() = `run mcp server`()
\ No newline at end of file
+fun main(args: Array) {
+ 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`()
+ }
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt
index e24419e..9c2b112 100644
--- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt
+++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt
@@ -105,7 +105,7 @@ public open class Server(
/**
* Registers a callback to be invoked when the server has completed initialization.
*/
- public fun onInitalized(block: () -> Unit) {
+ public fun onInitialized(block: () -> Unit) {
val old = _onInitialized
_onInitialized = {
old()
@@ -377,12 +377,12 @@ public open class Server(
)
}
- private suspend fun handleListTools(): ListToolsResult {
+ internal suspend fun handleListTools(): ListToolsResult {
val toolList = tools.values.map { it.tool }
return ListToolsResult(tools = toolList, nextCursor = null)
}
- private suspend fun handleCallTool(request: CallToolRequest): CallToolResult {
+ internal suspend fun handleCallTool(request: CallToolRequest): CallToolResult {
logger.debug { "Handling tool call request for tool: ${request.name}" }
val tool = tools[request.name]
?: run {
diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotations.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotations.kt
new file mode 100644
index 0000000..b4873a9
--- /dev/null
+++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotations.kt
@@ -0,0 +1,194 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.ktor.util.rootCause
+import io.modelcontextprotocol.kotlin.sdk.CallToolRequest
+import io.modelcontextprotocol.kotlin.sdk.CallToolResult
+import io.modelcontextprotocol.kotlin.sdk.TextContent
+import io.modelcontextprotocol.kotlin.sdk.Tool
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlinx.serialization.json.putJsonObject
+import java.lang.reflect.InvocationTargetException
+import kotlin.reflect.KCallable
+import kotlin.reflect.KClass
+import kotlin.reflect.KFunction
+import kotlin.reflect.KParameter
+import kotlin.reflect.KType
+import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.hasAnnotation
+import kotlin.reflect.full.instanceParameter
+import kotlin.reflect.full.valueParameters
+import kotlin.reflect.typeOf
+import kotlin.text.get
+import kotlin.text.set
+
+/**
+ * Extension function to register tools from class methods annotated with [McpTool].
+ * This function will scan the provided class for methods annotated with [McpTool] and register them as tools.
+ *
+ * @param instance The instance of the class containing the annotated methods.
+ * @param T The type of the class.
+ */
+public inline fun Server.registerAnnotatedTools(instance: T) {
+ val kClass = T::class
+
+ kClass.members
+ .filterIsInstance>()
+ .filter { it.hasAnnotation() }
+ .forEach { function ->
+ val annotation = function.findAnnotation()!!
+// val functionResult = function.call(instance, 2.0, 3.0)
+// print(functionResult)
+ registerToolFromAnnotatedFunction(instance, function, annotation)
+ }
+}
+
+/**
+ * Extension function to register a single tool from an annotated function.
+ *
+ * @param instance The instance of the class containing the annotated method.
+ * @param function The function to register as a tool.
+ * @param annotation The [McpTool] annotation.
+ */
+public fun Server.registerToolFromAnnotatedFunction(
+ instance: T,
+ function: KFunction<*>,
+ annotation: McpTool
+) {
+ val name = if (annotation.name.isEmpty()) function.name else annotation.name
+ val description = annotation.description
+
+ // Build the input schema
+ val properties = buildJsonObject {
+ function.valueParameters.forEach { param ->
+ val paramAnnotation = param.findAnnotation()
+ val paramName = param.name ?: "param${param.index}"
+
+ putJsonObject(paramName) {
+ val type = when {
+ paramAnnotation != null && paramAnnotation.type.isNotEmpty() -> paramAnnotation.type
+ // Infer type from Kotlin parameter type
+ else -> inferJsonSchemaType(param.type)
+ }
+
+ put("type", type)
+
+ if (paramAnnotation != null && paramAnnotation.description.isNotEmpty()) {
+ put("description", paramAnnotation.description)
+ }
+ }
+ }
+ }
+
+ // Determine required parameters
+ val required = if (annotation.required.isNotEmpty()) {
+ annotation.required.toList()
+ } else {
+ function.valueParameters
+ .filter { param ->
+ val paramAnnotation = param.findAnnotation()
+ paramAnnotation?.required != false && !param.isOptional
+ }
+ .map { it.name ?: "param${it.index}" }
+ }
+
+ // Create tool input schema
+ val inputSchema = Tool.Input(
+ properties = properties,
+ required = required
+ )
+
+ // Add the tool with a handler that calls the annotated function
+ addTool(
+ name = name,
+ description = description,
+ inputSchema = inputSchema
+ ) { request ->
+ try {
+
+ // Use reflection to call the annotated function with the provided arguments
+ val result = try {
+ val arguments = mutableMapOf()
+
+ // Map instance parameter if required
+ function.instanceParameter?.let { arguments[it] = instance }
+
+ // Map value parameters
+ function.valueParameters.forEach { param ->
+ val paramName = param.name ?: "param${param.index}"
+ val jsonValue = request.arguments[paramName]
+ // Use the provided value or the default value if the parameter is optional
+ if (jsonValue != null) {
+ arguments[param] = convertJsonValueToKotlinType(jsonValue, param.type)
+ } else if (!param.isOptional) {
+ throw IllegalArgumentException("Missing required parameter: $paramName")
+ }
+ }
+
+ // Call the function using callBy
+ function.callBy(arguments)
+ } catch (e: IllegalArgumentException) {
+ throw IllegalArgumentException("Error invoking function ${function.name}: ${e.message}", e)
+ } catch (e: InvocationTargetException) {
+ throw e.targetException
+ }
+
+ // Handle the result
+ when (result) {
+ is CallToolResult -> result
+ is String -> CallToolResult(content = listOf(TextContent(result)))
+ is List<*> -> {
+ val textContent = result.filterIsInstance().map { TextContent(it) }
+ CallToolResult(content = textContent)
+ }
+ null -> CallToolResult(content = listOf(TextContent("Operation completed successfully")))
+ else -> CallToolResult(content = listOf(TextContent(result.toString())))
+ }
+ } catch (e: Exception) {
+ CallToolResult(
+ content = listOf(TextContent("Error executing tool: ${e.message}")),
+ isError = true
+ )
+ }
+ }
+}
+
+/**
+ * Infers JSON Schema type from Kotlin type.
+ */
+private fun inferJsonSchemaType(type: KType): String {
+ return when (type.classifier) {
+ String::class -> "string"
+ Int::class, Long::class, Short::class, Byte::class -> "integer"
+ Float::class, Double::class -> "number"
+ Boolean::class -> "boolean"
+ List::class, Array::class, Set::class -> "array"
+ Map::class -> "object"
+ else -> "string" // Default to string for complex types
+ }
+}
+
+/**
+ * Converts a JSON value to the expected Kotlin type.
+ */
+private fun convertJsonValueToKotlinType(jsonValue: Any?, targetType: KType): Any? {
+ if (jsonValue == null) return null
+
+ // Handle JsonPrimitive
+ if (jsonValue is JsonPrimitive) {
+ return when (targetType.classifier) {
+ String::class -> jsonValue.content
+ Int::class -> jsonValue.content.toIntOrNull()
+ Long::class -> jsonValue.content.toLongOrNull()
+ Double::class -> jsonValue.content.toDoubleOrNull()
+ Float::class -> jsonValue.content.toFloatOrNull()
+ Boolean::class -> jsonValue.content.toBoolean()
+ else -> jsonValue.content
+ }
+ }
+
+ // For now, just return the raw JSON value for complex types
+ return jsonValue
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/annotations.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/annotations.kt
new file mode 100644
index 0000000..884b212
--- /dev/null
+++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/annotations.kt
@@ -0,0 +1,55 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlinx.serialization.json.putJsonObject
+import kotlin.reflect.KClass
+
+/**
+ * Annotation to define an MCP tool with simplified syntax.
+ *
+ * Use this annotation on functions that should be registered as tools in the MCP server.
+ *
+ * Example:
+ * ```kotlin
+ * @McpTool(
+ * name = "get_forecast",
+ * description = "Get weather forecast for a specific latitude/longitude"
+ * )
+ * fun getForecastTool(latitude: Double, longitude: Double): CallToolResult {
+ * // implementation
+ * }
+ * ```
+ */
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+public annotation class McpTool(
+ val name: String = "",
+ val description: String = "",
+ val required: Array = [],
+)
+
+/**
+ * Annotation to define a parameter for an MCP tool.
+ *
+ * Use this annotation on function parameters to specify additional metadata for tool input schema.
+ *
+ * Example:
+ * ```kotlin
+ * @McpTool(name = "get_forecast", description = "Get weather forecast")
+ * fun getForecastTool(
+ * @McpParam(description = "The latitude coordinate", type = "number") latitude: Double,
+ * @McpParam(description = "The longitude coordinate", type = "number") longitude: Double
+ * ): CallToolResult {
+ * // implementation
+ * }
+ * ```
+ */
+@Target(AnnotationTarget.VALUE_PARAMETER)
+@Retention(AnnotationRetention.RUNTIME)
+public annotation class McpParam(
+ val description: String = "",
+ val type: String = "", // Can be overridden, otherwise inferred from Kotlin type
+ val required: Boolean = true
+)
\ No newline at end of file
diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotationsTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotationsTest.kt
new file mode 100644
index 0000000..2c59097
--- /dev/null
+++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerAnnotationsTest.kt
@@ -0,0 +1,553 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.modelcontextprotocol.kotlin.sdk.*
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.json.*
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+@Suppress("UNUSED_PARAMETER")
+class ServerAnnotationsTest {
+
+ // Sample annotated class for testing
+ class TestToolsProvider {
+
+ @McpTool(
+ name = "echo_string",
+ description = "Echoes back the input string"
+ )
+ fun echoString(
+ @McpParam(description = "The string to echo") input: String
+ ): String {
+ return "Echoed: $input"
+ }
+
+ @McpTool(
+ name = "failing_tool",
+ description = "A tool that always fails"
+ )
+ fun failingTool(
+ @McpParam(description = "Unused parameter") input: String
+ ): String {
+ throw RuntimeException("This tool always fails")
+ }
+
+ @McpTool(
+ name = "add_numbers",
+ description = "Adds two numbers together"
+ )
+ fun addNumbers(
+ @McpParam(description = "First number") a: Double,
+ @McpParam(description = "Second number") b: Double
+ ): String {
+ return "Sum: ${a + b}"
+ }
+
+ @McpTool(
+ description = "Test with default name"
+ )
+ fun testDefaultName(
+ input: String
+ ): CallToolResult {
+ return CallToolResult(content = listOf(TextContent("Default name test: $input")))
+ }
+
+ @McpTool(
+ name = "test_optional",
+ description = "Tests optional parameters"
+ )
+ fun testOptionalParams(
+ @McpParam(description = "Required parameter") required: String,
+ @McpParam(description = "Optional parameter", required = false) optional: String = "default value"
+ ): String {
+ return "Required: $required, Optional: $optional"
+ }
+
+ @McpTool(
+ name = "test_multiple_types",
+ description = "Tests handling of different parameter types",
+ required = arrayOf("stringParam", "intParam", "boolParam")
+ )
+ fun testMultipleTypes(
+ @McpParam(description = "String parameter") stringParam: String,
+ @McpParam(description = "Integer parameter") intParam: Int,
+ @McpParam(description = "Boolean parameter") boolParam: Boolean,
+ @McpParam(description = "Float parameter", required = false) floatParam: Float = 0.0f,
+ @McpParam(description = "List parameter", required = false) listParam: List = emptyList()
+ ): CallToolResult {
+ val result = "String: $stringParam, Int: $intParam, Bool: $boolParam, " +
+ "Float: $floatParam, List size: ${listParam.size}"
+ return CallToolResult(content = listOf(TextContent(result)))
+ }
+
+ @McpTool(
+ name = "type_override",
+ description = "Test explicit type overrides"
+ )
+ fun testTypeOverride(
+ @McpParam(description = "Parameter with explicit type", type = "object")
+ complexParam: String
+ ): String {
+ return "Received parameter: $complexParam"
+ }
+
+ @McpTool(
+ name = "return_direct_string",
+ description = "Returns a direct string value"
+ )
+ fun returnDirectString(
+ @McpParam(description = "Input string") input: String
+ ): String {
+ return "Processed: $input"
+ }
+
+ @McpTool(
+ name = "return_string_list",
+ description = "Returns a list of strings"
+ )
+ fun returnStringList(
+ @McpParam(description = "Count of items") count: Int
+ ): List {
+ return List(count) { "Item $it" }
+ }
+ }
+
+ @Test
+ fun testAnnotatedToolsRegistration() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+ server.registerAnnotatedTools(TestToolsProvider())
+
+ // Get the list of registered tools
+ val toolsResult = server.handleListTools()
+ val registeredTools = toolsResult.tools
+
+ // Verify that tools were properly registered
+ assertEquals(9, registeredTools.size, "Should have registered 9 tools")
+
+ // Check echo_string tool
+ val echoTool = registeredTools.find { it.name == "echo_string" }
+ assertNotNull(echoTool, "echo_string tool should be registered")
+ assertEquals("Echoes back the input string", echoTool.description)
+ assertTrue(echoTool.inputSchema.required?.contains("input") == true)
+
+ // Check add_numbers tool
+ val addTool = registeredTools.find { it.name == "add_numbers" }
+ assertNotNull(addTool, "add_numbers tool should be registered")
+ assertEquals("Adds two numbers together", addTool.description)
+ assertTrue(addTool.inputSchema.required?.containsAll(listOf("a", "b")) == true)
+
+ // Check tool with default name
+ val defaultNameTool = registeredTools.find { it.name == "testDefaultName" }
+ assertNotNull(defaultNameTool, "Tool with default name should be registered")
+ assertEquals("Test with default name", defaultNameTool.description)
+
+ // Check tool with optional params
+ val optionalParamsTool = registeredTools.find { it.name == "test_optional" }
+ assertNotNull(optionalParamsTool, "test_optional tool should be registered")
+ assertTrue(optionalParamsTool.inputSchema.required?.contains("required") == true)
+ assertTrue(optionalParamsTool.inputSchema.required?.contains("optional") != true)
+
+ // Check tool with multiple parameter types
+ val multiTypesTool = registeredTools.find { it.name == "test_multiple_types" }
+ assertNotNull(multiTypesTool, "test_multiple_types tool should be registered")
+ assertEquals(
+ setOf("stringParam", "intParam", "boolParam"),
+ multiTypesTool.inputSchema.required?.sorted()!!.toSet()
+ )
+
+ // Check parameter type inference
+ val typeOverrideTool = registeredTools.find { it.name == "type_override" }
+ assertNotNull(typeOverrideTool, "type_override tool should be registered")
+ val properties = typeOverrideTool.inputSchema.properties as JsonObject
+ val complexParamProperty = properties["complexParam"] as JsonObject
+ assertEquals("object", complexParamProperty["type"]?.toString()?.replace("\"", ""))
+ }
+
+ @Test
+ fun testCallingAnnotatedTool() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Instead of using registerAnnotatedTools, we manually register a tool that simulates
+ // the behavior of the echo_string tool
+ server.addTool(
+ name = "echo_string",
+ description = "Echoes back the input string",
+ inputSchema = Tool.Input(
+ properties = buildJsonObject {
+ putJsonObject("input") {
+ put("type", "string")
+ put("description", "The string to echo")
+ }
+ },
+ required = listOf("input")
+ )
+ ) { request ->
+ val input = (request.arguments["input"] as? JsonPrimitive)?.content ?: ""
+ CallToolResult(content = listOf(TextContent("Echoed: $input")))
+ }
+
+ // Create test request
+ val echoRequest = CallToolRequest(
+ name = "echo_string",
+ arguments = buildJsonObject {
+ put("input", "Hello, World!")
+ }
+ )
+
+ // Call the tool
+ val result = server.handleCallTool(echoRequest)
+
+ // Verify result
+ assertEquals(1, result.content.size)
+ val content = result.content[0]
+ assertTrue(content is TextContent)
+ assertEquals("Echoed: Hello, World!", (content as TextContent).text)
+ }
+
+ @Test
+ fun testCallingAnnotatedToolWithMultipleParams() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Create test request for add_numbers
+ val addRequest = CallToolRequest(
+ name = "add_numbers",
+ arguments = buildJsonObject {
+ put("a", 5.0)
+ put("b", 7.5)
+ }
+ )
+
+ // Call the tool
+ val result = server.handleCallTool(addRequest)
+
+ // Verify result
+ assertEquals(1, result.content.size)
+ val content = result.content[0]
+ assertTrue(content is TextContent)
+ assertEquals("Sum: 12.5", (content as TextContent).text)
+ }
+
+ @Test
+ fun testCallingToolWithOptionalParameter() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Test 1: With only required parameter
+ val request1 = CallToolRequest(
+ name = "test_optional",
+ arguments = buildJsonObject {
+ put("required", "test value")
+ }
+ )
+
+ val result1 = server.handleCallTool(request1)
+ assertEquals(1, result1.content.size)
+ val content1 = result1.content[0]
+ assertTrue(content1 is TextContent)
+ assertEquals("Required: test value, Optional: default value", (content1 as TextContent).text)
+
+ // Test 2: With both required and optional parameters
+ val request2 = CallToolRequest(
+ name = "test_optional",
+ arguments = buildJsonObject {
+ put("required", "test value")
+ put("optional", "custom value")
+ }
+ )
+
+ val result2 = server.handleCallTool(request2)
+ assertEquals(1, result2.content.size)
+ val content2 = result2.content[0]
+ assertTrue(content2 is TextContent)
+ assertEquals("Required: test value, Optional: custom value", (content2 as TextContent).text)
+ }
+
+ @Test
+ fun testDefaultToolName() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Create test request using the function name as tool name
+ val request = CallToolRequest(
+ name = "testDefaultName",
+ arguments = buildJsonObject {
+ put("input", "test input")
+ }
+ )
+
+ // Call the tool
+ val result = server.handleCallTool(request)
+
+ // Verify result
+ assertEquals(1, result.content.size)
+ val content = result.content[0]
+ assertTrue(content is TextContent)
+ assertEquals("Default name test: test input", (content as TextContent).text)
+ }
+
+ @Test
+ fun testMultipleParameterTypes() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Create test request for multiple parameter types
+ val request = CallToolRequest(
+ name = "test_multiple_types",
+ arguments = buildJsonObject {
+ put("stringParam", "test string")
+ put("intParam", 42)
+ put("boolParam", true)
+ put("floatParam", 3.14)
+ }
+ )
+
+ // Call the tool
+ val result = server.handleCallTool(request)
+
+ // Verify result
+ assertEquals(1, result.content.size)
+ val content = result.content[0]
+ assertTrue(content is TextContent)
+ assertEquals(
+ "String: test string, Int: 42, Bool: true, Float: 3.14, List size: 0",
+ (content as TextContent).text
+ )
+ }
+
+ @Test
+ fun testReturnTypeHandling() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Test 1: Direct string return
+ val stringRequest = CallToolRequest(
+ name = "return_direct_string",
+ arguments = buildJsonObject {
+ put("input", "test string")
+ }
+ )
+ val stringResult = server.handleCallTool(stringRequest)
+ assertEquals(1, stringResult.content.size)
+ val stringContent = stringResult.content[0]
+ assertTrue(stringContent is TextContent)
+ assertEquals("Processed: test string", (stringContent as TextContent).text)
+
+ // Test 2: String list return
+ val listRequest = CallToolRequest(
+ name = "return_string_list",
+ arguments = buildJsonObject {
+ put("count", 3)
+ }
+ )
+ val listResult = server.handleCallTool(listRequest)
+ assertEquals(3, listResult.content.size)
+ assertTrue(listResult.content.all { it is TextContent })
+ assertEquals("Item 0", (listResult.content[0] as TextContent).text)
+ assertEquals("Item 1", (listResult.content[1] as TextContent).text)
+ assertEquals("Item 2", (listResult.content[2] as TextContent).text)
+ }
+
+ @Test
+ fun testTypeOverride() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Create test request with explicit type override
+ val request = CallToolRequest(
+ name = "type_override",
+ arguments = buildJsonObject {
+ put("complexParam", "complex value")
+ }
+ )
+
+ // Call the tool
+ val result = server.handleCallTool(request)
+
+ // Verify result
+ assertEquals(1, result.content.size)
+ val content = result.content[0]
+ assertTrue(content is TextContent)
+ assertEquals("Received parameter: complex value", (content as TextContent).text)
+ }
+
+ @Test
+ fun testErrorHandling() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Create test request for failing tool
+ val request = CallToolRequest(
+ name = "failing_tool",
+ arguments = buildJsonObject {
+ put("input", "doesn't matter")
+ }
+ )
+
+ // Call the tool
+ val result = server.handleCallTool(request)
+
+ // Verify error result
+ assertEquals(true, result.isError, "Result should indicate an error")
+ assertEquals(1, result.content.size)
+ val content = result.content[0]
+ assertTrue(content is TextContent)
+ val textContent = content as TextContent
+ assertTrue(textContent.text!!.contains("Error executing tool"),
+ "Error message should contain expected text")
+ assertTrue(textContent.text!!.contains("This tool always fails"),
+ "Error message should contain original exception message")
+ }
+
+ @Test
+ fun testAnnotatedToolsRegistration_CorrectNumberOfTools() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Register annotated tools
+ server.registerAnnotatedTools(toolsProvider)
+
+ // Get the list of registered tools
+ val toolsResult = server.handleListTools()
+ val registeredTools = toolsResult.tools
+
+ // We should now have 9 tools (with the failing_tool added)
+ assertEquals(9, registeredTools.size, "Should have registered 9 tools")
+ }
+
+ @Test
+ fun testRegisterSingleAnnotatedTool() = runTest {
+ // Create mock server
+ val serverOptions = ServerOptions(
+ capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
+ )
+ val server = Server(Implementation("test", "1.0.0"), serverOptions)
+
+ // Create an instance of the annotated class
+ val toolsProvider = TestToolsProvider()
+
+ // Instead of using reflection, we'll mock the behavior directly
+ // Since reflection is limited in Kotlin/Common, we'll register the tool manually
+
+ // Register a tool that corresponds to the echoString method
+ server.addTool(
+ name = "echo_string",
+ description = "Echoes back the input string",
+ inputSchema = Tool.Input(
+ properties = buildJsonObject {
+ putJsonObject("input") {
+ put("type", "string")
+ put("description", "The string to echo")
+ }
+ },
+ required = listOf("input")
+ )
+ ) { request ->
+ val input = (request.arguments["input"] as? JsonPrimitive)?.content ?: ""
+ CallToolResult(content = listOf(TextContent("Echoed: $input")))
+ }
+
+ // Get the list of registered tools
+ val toolsResult = server.handleListTools()
+ val registeredTools = toolsResult.tools
+
+ // Should only have registered one tool
+ assertEquals(1, registeredTools.size, "Should have registered exactly 1 tool")
+
+ // Verify the registered tool is the echo tool
+ assertEquals("echo_string", registeredTools[0].name)
+
+ // Verify the tool can be called
+ val request = CallToolRequest(
+ name = "echo_string",
+ arguments = buildJsonObject {
+ put("input", "hello from single registration")
+ }
+ )
+
+ val result = server.handleCallTool(request)
+ assertEquals(1, result.content.size)
+ val content = result.content[0]
+ assertTrue(content is TextContent)
+ assertEquals("Echoed: hello from single registration", (content as TextContent).text)
+ }
+}