Skip to content
Draft
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
5 changes: 5 additions & 0 deletions buildSrc/src/main/kotlin/mcp.dokka.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ dokka {
packageListUrl("https://kotlinlang.org/api/kotlinx.coroutines/package-list")
}

externalDocumentationLinks.register("kotlinx-schema") {
url("https://kotlin.github.io/kotlinx-schema/")
packageListUrl("https://kotlin.github.io/kotlinx-schema/package-list")
}

externalDocumentationLinks.register("kotlinx-serialization") {
url("https://kotlinlang.org/api/kotlinx.serialization/")
packageListUrl("https://kotlinlang.org/api/kotlinx.serialization/package-list")
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ collections-immutable = "0.4.0"
coroutines = "1.10.2"
kotest = "6.1.3"
kotlinx-io = "0.8.2"
kotlinx-schema = "0.3.1"
ktor = "3.2.3"
logging = "7.0.14"
mockk = "1.14.9"
Expand All @@ -36,6 +37,8 @@ maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version

# Kotlinx libraries
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" }
kotlinx-schema-generator-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-schema-generator-json", version.ref = "kotlinx-schema" }
kotlinx-schema-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-schema-json", version.ref = "kotlinx-schema" }
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-core-wasm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-wasm-js", version.ref = "coroutines" }
Expand Down
72 changes: 72 additions & 0 deletions kotlin-sdk-core/Module.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,78 @@ val request = CallToolRequest {
val message: JSONRPCMessage = McpJson.decodeFromString(jsonString)
```

## JSON Schema integration

The module integrates with [kotlinx-schema](https://kotlin.github.io/kotlinx-schema/) to enable type-safe schema
generation from Kotlin data classes. Extension functions convert between kotlinx-schema types and MCP's [ToolSchema],
allowing you to define tool schemas using `@Description` annotations and other schema metadata.

### Supported conversions

- [JsonObject.asToolSchema][asToolSchema] — converts a raw JSON Schema object
- [JsonSchema.asToolSchema][asToolSchema] — converts kotlinx-schema's [JsonSchema]
- [FunctionCallingSchema.asToolSchema][asToolSchema] — extracts parameters from [FunctionCallingSchema]

### Example: generating tool schema from a data class

```kotlin
import kotlinx.schema.Description
import kotlinx.schema.generator.core.SchemaGeneratorService
import kotlinx.schema.json.JsonSchema

data class SearchParams(
@property:Description("Search query")
val query: String,
@property:Description("Maximum number of results")
val limit: Int = 10,
)

// Generate schema from data class
val generator = SchemaGeneratorService.getGenerator(KClass::class, JsonSchema::class)
val schema = generator.generateSchema(SearchParams::class)

// Convert to MCP ToolSchema
val toolSchema = schema.asToolSchema()

// Use in Tool definition
val tool = Tool(
name = "web-search",
description = "Search the web",
inputSchema = toolSchema,
)
```

### Example: using FunctionCallingSchema

```kotlin
import kotlinx.schema.json.FunctionCallingSchema
import kotlinx.schema.json.ObjectPropertyDefinition
import kotlinx.schema.json.StringPropertyDefinition

val functionSchema = FunctionCallingSchema(
name = "calculate",
description = "Perform a calculation",
parameters = ObjectPropertyDefinition(
properties = mapOf(
"expression" to StringPropertyDefinition(
description = "Mathematical expression to evaluate"
),
),
required = listOf("expression"),
),
)

val toolSchema = functionSchema.asToolSchema()
```

### Schema validation

All conversion functions validate that the schema type is `"object"` (or omit validation if the type field is absent).
Only object schemas are supported for MCP tool input/output schemas. Attempting to convert array, string, or other
non-object schemas will throw [IllegalArgumentException].

---

Use this module when you need the raw building blocks of MCP—types, JSON config, and transport base classes—whether to
embed in another runtime, author new transports, or contribute higher-level features in the client/server modules. The
APIs are explicit to keep the shared surface stable for downstream users.
6 changes: 6 additions & 0 deletions kotlin-sdk-core/api/kotlin-sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -4613,6 +4613,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Tools_dslKt {
public static final fun buildListToolsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest;
}

public final class io/modelcontextprotocol/kotlin/sdk/types/Tools_schemaKt {
public static final fun asToolSchema (Lkotlinx/schema/json/FunctionCallingSchema;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
public static final fun asToolSchema (Lkotlinx/schema/json/JsonSchema;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
public static final fun asToolSchema (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
}

public final class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents : io/modelcontextprotocol/kotlin/sdk/types/ResourceContents {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V
Expand Down
8 changes: 5 additions & 3 deletions kotlin-sdk-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,13 @@ kotlin {
commonMain {
kotlin.srcDir(generateLibVersion)
dependencies {
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.collections.immutable)
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.io.core)
api(libs.kotlinx.collections.immutable)
implementation(libs.ktor.server.websockets)
api(libs.kotlinx.schema.json)
api(libs.kotlinx.serialization.json)
implementation(libs.kotlin.logging)
implementation(libs.ktor.server.websockets)
}
}

Expand All @@ -128,6 +129,7 @@ kotlin {

jvmTest {
dependencies {
implementation(libs.kotlinx.schema.generator.json)
implementation(libs.junit.jupiter.params)
implementation(libs.mockk)
runtimeOnly(libs.slf4j.simple)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.modelcontextprotocol.kotlin.sdk.types

import kotlinx.schema.json.FunctionCallingSchema
import kotlinx.schema.json.JsonSchema
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

/**
* Converts a JSON Schema [JsonObject] into a [ToolSchema] representation.
*
* Extracts `properties` and `required` fields from the JSON Schema object. The JSON object
* should conform to JSON Schema Draft 2020-12 specification for object types.
*
* Sample:
* ```kotlin
* val jsonSchema = buildJsonObject {
* put("type", "object")
* putJsonObject("properties") {
* putJsonObject("name") {
* put("type", "string")
* }
* }
* putJsonArray("required") { add("name") }
* }
* val toolSchema = jsonSchema.asToolSchema()
* ```
*
* @return A [ToolSchema] instance with extracted schema metadata.
* @throws IllegalArgumentException if the schema type is specified but is not "object". Missing type field is accepted.
*/
public fun JsonObject.asToolSchema(): ToolSchema {
// Validate a schema type if present
val schemaType = this["type"]?.jsonPrimitive?.content
require(schemaType == null || schemaType == "object") {
"Only object schemas are supported for ToolSchema conversion, got: $schemaType"
}

return ToolSchema(
properties = this["properties"]?.jsonObject,
required = this["required"]?.jsonArray?.map { it.jsonPrimitive.content },
)
}

/**
* Converts a [JsonSchema] from kotlinx-schema into a [ToolSchema] representation.
*
* This is useful when generating schemas from Kotlin data classes using kotlinx-schema's
* schema generator. The schema is first serialized to JSON and then converted to [ToolSchema].
*
* @return A [ToolSchema] instance with the schema's properties and required fields.
* @throws IllegalArgumentException if the schema type is not "object".
* @sample
* ```kotlin
* import kotlinx.schema.generator.core.SchemaGeneratorService
* import kotlinx.schema.Description
*
* @Serializable
* data class SearchParams(
* @property:Description("Search query")
* val query: String,
* val limit: Int = 10
* )
*
* val generator = SchemaGeneratorService.getGenerator(KClass::class, JsonSchema::class)
* val schema = generator.generateSchema(SearchParams::class)
* val toolSchema = schema.asToolSchema()
* ```
*/
public fun JsonSchema.asToolSchema(): ToolSchema = McpJson.encodeToJsonElement(this)
.jsonObject.asToolSchema()

/**
* Converts a [FunctionCallingSchema]'s parameters into a [ToolSchema] representation.
*
* Extracts and converts the `parameters` field from the function calling schema,
* which is typically used in OpenAI-compatible function calling APIs.
*
* @return A [ToolSchema] object containing the function's parameter schema.
* @throws IllegalArgumentException if the parameters schema type is not "object".
* @sample
* ```kotlin
* val functionSchema = FunctionCallingSchema(
* name = "search",
* description = "Search the web",
* parameters = ObjectPropertyDefinition(
* properties = mapOf(
* "query" to StringPropertyDefinition(description = "Search query")
* ),
* required = listOf("query")
* )
* )
* val toolSchema = functionSchema.asToolSchema()
* ```
*/
public fun FunctionCallingSchema.asToolSchema(): ToolSchema =
McpJson.encodeToJsonElement(this.parameters).jsonObject.asToolSchema()
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package io.modelcontextprotocol.kotlin.sdk.types

import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import kotlinx.schema.json.ArrayPropertyDefinition
import kotlinx.schema.json.FunctionCallingSchema
import kotlinx.schema.json.ObjectPropertyDefinition
import kotlinx.schema.json.StringPropertyDefinition
import kotlin.test.Test

class FunctionCallingSchemaAsToolSchemaTest {

@Test
fun `should convert function parameters`() {
val functionSchema = FunctionCallingSchema(
name = "calculate",
description = "Perform calculation",
parameters = ObjectPropertyDefinition(
properties = mapOf(
"operation" to StringPropertyDefinition(
description = "Math operation",
),
"x" to StringPropertyDefinition(),
"y" to StringPropertyDefinition(),
),
required = listOf("operation", "x", "y"),
),
)

val toolSchema = functionSchema.asToolSchema()

toolSchema.type shouldBe "object"
toolSchema.properties.shouldNotBeNull {
keys shouldContainExactly setOf("operation", "x", "y")
}
toolSchema.required shouldContainExactly listOf("operation", "x", "y")
}

@Test
fun `should handle required and optional parameters`() {
// Test with some required, some optional
val mixedSchema = FunctionCallingSchema(
name = "search",
parameters = ObjectPropertyDefinition(
properties = mapOf(
"query" to StringPropertyDefinition(description = "Search query"),
"limit" to StringPropertyDefinition(description = "Result limit"),
),
required = listOf("query"),
),
)

val mixedToolSchema = mixedSchema.asToolSchema()
mixedToolSchema.type shouldBe "object"
mixedToolSchema.properties.shouldNotBeNull {
keys shouldContainExactly setOf("query", "limit")
}
mixedToolSchema.required shouldContainExactly listOf("query")

// Test with all optional (empty required list)
val allOptionalSchema = FunctionCallingSchema(
name = "list",
parameters = ObjectPropertyDefinition(
properties = mapOf("filter" to StringPropertyDefinition()),
required = emptyList(),
),
)

val allOptionalToolSchema = allOptionalSchema.asToolSchema()
allOptionalToolSchema.required.shouldNotBeNull {
this shouldContainExactly emptyList()
}
}

@Test
fun `should handle complex nested parameter types`() {
val functionSchema = FunctionCallingSchema(
name = "process",
parameters = ObjectPropertyDefinition(
properties = mapOf(
"items" to ArrayPropertyDefinition(
items = StringPropertyDefinition(),
description = "List of items to process",
),
"config" to ObjectPropertyDefinition(
properties = mapOf(
"timeout" to StringPropertyDefinition(description = "Timeout in seconds"),
"retries" to StringPropertyDefinition(description = "Number of retries"),
),
required = listOf("timeout"),
),
),
required = listOf("items", "config"),
),
)

val toolSchema = functionSchema.asToolSchema()

toolSchema.type shouldBe "object"
toolSchema.properties.shouldNotBeNull {
keys shouldContainExactly setOf("items", "config")
}
toolSchema.required shouldContainExactly listOf("items", "config")
}

@Test
fun `should handle parameters count variations`() {
// Empty parameters
val emptySchema = FunctionCallingSchema(
name = "ping",
parameters = ObjectPropertyDefinition(
properties = emptyMap(),
required = emptyList(),
),
)

val emptyToolSchema = emptySchema.asToolSchema()
emptyToolSchema.type shouldBe "object"
emptyToolSchema.properties.shouldNotBeNull {
keys shouldContainExactly emptySet()
}

// Single parameter
val singleSchema = FunctionCallingSchema(
name = "greet",
parameters = ObjectPropertyDefinition(
properties = mapOf(
"name" to StringPropertyDefinition(description = "Name to greet"),
),
required = listOf("name"),
),
)

val singleToolSchema = singleSchema.asToolSchema()
singleToolSchema.properties.shouldNotBeNull {
keys shouldContainExactly setOf("name")
}
singleToolSchema.required shouldContainExactly listOf("name")
}
}
Loading
Loading