Skip to content

Commit 8725d23

Browse files
committed
feat: Add JSON Schema integration with kotlinx-schema
- Introduce `asToolSchema` extension functions for seamless conversion of JSON Schema to `ToolSchema`. - Add support for `JsonObject`, `JsonSchema`, and `FunctionCallingSchema` conversions. - Integrate `kotlinx-schema` and `kotlinx-schema-generator` dependencies into the project. - Include multiple test cases to validate schema conversion functionality. - Extend documentation with examples and usage guidelines for JSON Schema integration.
1 parent ec53adc commit 8725d23

File tree

10 files changed

+675
-3
lines changed

10 files changed

+675
-3
lines changed

buildSrc/src/main/kotlin/mcp.dokka.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ dokka {
3131
packageListUrl("https://kotlinlang.org/api/kotlinx.coroutines/package-list")
3232
}
3333

34+
externalDocumentationLinks.register("kotlinx-schema") {
35+
url("https://kotlin.github.io/kotlinx-schema/")
36+
packageListUrl("https://kotlin.github.io/kotlinx-schema/package-list")
37+
}
38+
3439
externalDocumentationLinks.register("kotlinx-serialization") {
3540
url("https://kotlinlang.org/api/kotlinx.serialization/")
3641
packageListUrl("https://kotlinlang.org/api/kotlinx.serialization/package-list")

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ collections-immutable = "0.4.0"
1818
coroutines = "1.10.2"
1919
kotest = "6.1.3"
2020
kotlinx-io = "0.8.2"
21+
kotlinx-schema = "0.3.1"
2122
ktor = "3.2.3"
2223
logging = "7.0.14"
2324
mockk = "1.14.9"
@@ -36,6 +37,8 @@ maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version
3637

3738
# Kotlinx libraries
3839
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" }
40+
kotlinx-schema-generator-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-schema-generator-json", version.ref = "kotlinx-schema" }
41+
kotlinx-schema-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-schema-json", version.ref = "kotlinx-schema" }
3942
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" }
4043
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
4144
kotlinx-coroutines-core-wasm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-wasm-js", version.ref = "coroutines" }

kotlin-sdk-core/Module.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,78 @@ val request = CallToolRequest {
4242
val message: JSONRPCMessage = McpJson.decodeFromString(jsonString)
4343
```
4444

45+
## JSON Schema integration
46+
47+
The module integrates with [kotlinx-schema](https://kotlin.github.io/kotlinx-schema/) to enable type-safe schema
48+
generation from Kotlin data classes. Extension functions convert between kotlinx-schema types and MCP's [ToolSchema],
49+
allowing you to define tool schemas using `@Description` annotations and other schema metadata.
50+
51+
### Supported conversions
52+
53+
- [JsonObject.asToolSchema][asToolSchema] — converts a raw JSON Schema object
54+
- [JsonSchema.asToolSchema][asToolSchema] — converts kotlinx-schema's [JsonSchema]
55+
- [FunctionCallingSchema.asToolSchema][asToolSchema] — extracts parameters from [FunctionCallingSchema]
56+
57+
### Example: generating tool schema from a data class
58+
59+
```kotlin
60+
import kotlinx.schema.Description
61+
import kotlinx.schema.generator.core.SchemaGeneratorService
62+
import kotlinx.schema.json.JsonSchema
63+
64+
data class SearchParams(
65+
@property:Description("Search query")
66+
val query: String,
67+
@property:Description("Maximum number of results")
68+
val limit: Int = 10,
69+
)
70+
71+
// Generate schema from data class
72+
val generator = SchemaGeneratorService.getGenerator(KClass::class, JsonSchema::class)
73+
val schema = generator.generateSchema(SearchParams::class)
74+
75+
// Convert to MCP ToolSchema
76+
val toolSchema = schema.asToolSchema()
77+
78+
// Use in Tool definition
79+
val tool = Tool(
80+
name = "web-search",
81+
description = "Search the web",
82+
inputSchema = toolSchema,
83+
)
84+
```
85+
86+
### Example: using FunctionCallingSchema
87+
88+
```kotlin
89+
import kotlinx.schema.json.FunctionCallingSchema
90+
import kotlinx.schema.json.ObjectPropertyDefinition
91+
import kotlinx.schema.json.StringPropertyDefinition
92+
93+
val functionSchema = FunctionCallingSchema(
94+
name = "calculate",
95+
description = "Perform a calculation",
96+
parameters = ObjectPropertyDefinition(
97+
properties = mapOf(
98+
"expression" to StringPropertyDefinition(
99+
description = "Mathematical expression to evaluate"
100+
),
101+
),
102+
required = listOf("expression"),
103+
),
104+
)
105+
106+
val toolSchema = functionSchema.asToolSchema()
107+
```
108+
109+
### Schema validation
110+
111+
All conversion functions validate that the schema type is `"object"` (or omit validation if the type field is absent).
112+
Only object schemas are supported for MCP tool input/output schemas. Attempting to convert array, string, or other
113+
non-object schemas will throw [IllegalArgumentException].
114+
115+
---
116+
45117
Use this module when you need the raw building blocks of MCP—types, JSON config, and transport base classes—whether to
46118
embed in another runtime, author new transports, or contribute higher-level features in the client/server modules. The
47119
APIs are explicit to keep the shared surface stable for downstream users.

kotlin-sdk-core/api/kotlin-sdk-core.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4568,6 +4568,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Tools_dslKt {
45684568
public static final fun buildListToolsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest;
45694569
}
45704570

4571+
public final class io/modelcontextprotocol/kotlin/sdk/types/Tools_schemaKt {
4572+
public static final fun asToolSchema (Lkotlinx/schema/json/FunctionCallingSchema;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
4573+
public static final fun asToolSchema (Lkotlinx/schema/json/JsonSchema;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
4574+
public static final fun asToolSchema (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
4575+
}
4576+
45714577
public final class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents : io/modelcontextprotocol/kotlin/sdk/types/ResourceContents {
45724578
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents$Companion;
45734579
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V

kotlin-sdk-core/build.gradle.kts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,13 @@ kotlin {
110110
commonMain {
111111
kotlin.srcDir(generateLibVersion)
112112
dependencies {
113-
api(libs.kotlinx.serialization.json)
113+
api(libs.kotlinx.collections.immutable)
114114
api(libs.kotlinx.coroutines.core)
115115
api(libs.kotlinx.io.core)
116-
api(libs.kotlinx.collections.immutable)
117-
implementation(libs.ktor.server.websockets)
116+
api(libs.kotlinx.schema.json)
117+
api(libs.kotlinx.serialization.json)
118118
implementation(libs.kotlin.logging)
119+
implementation(libs.ktor.server.websockets)
119120
}
120121
}
121122

@@ -128,6 +129,7 @@ kotlin {
128129

129130
jvmTest {
130131
dependencies {
132+
implementation(libs.kotlinx.schema.generator.json)
131133
implementation(libs.junit.jupiter.params)
132134
implementation(libs.mockk)
133135
runtimeOnly(libs.slf4j.simple)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package io.modelcontextprotocol.kotlin.sdk.types
2+
3+
import kotlinx.schema.json.FunctionCallingSchema
4+
import kotlinx.schema.json.JsonSchema
5+
import kotlinx.serialization.json.JsonObject
6+
import kotlinx.serialization.json.encodeToJsonElement
7+
import kotlinx.serialization.json.jsonArray
8+
import kotlinx.serialization.json.jsonObject
9+
import kotlinx.serialization.json.jsonPrimitive
10+
11+
/**
12+
* Converts a JSON Schema [JsonObject] into a [ToolSchema] representation.
13+
*
14+
* Extracts `properties` and `required` fields from the JSON Schema object. The JSON object
15+
* should conform to JSON Schema Draft 2020-12 specification for object types.
16+
*
17+
* Sample:
18+
* ```kotlin
19+
* val jsonSchema = buildJsonObject {
20+
* put("type", "object")
21+
* putJsonObject("properties") {
22+
* putJsonObject("name") {
23+
* put("type", "string")
24+
* }
25+
* }
26+
* putJsonArray("required") { add("name") }
27+
* }
28+
* val toolSchema = jsonSchema.asToolSchema()
29+
* ```
30+
*
31+
* @return A [ToolSchema] instance with extracted schema metadata.
32+
* @throws IllegalArgumentException if the schema type is specified but is not "object". Missing type field is accepted.
33+
*/
34+
public fun JsonObject.asToolSchema(): ToolSchema {
35+
// Validate a schema type if present
36+
val schemaType = this["type"]?.jsonPrimitive?.content
37+
require(schemaType == null || schemaType == "object") {
38+
"Only object schemas are supported for ToolSchema conversion, got: $schemaType"
39+
}
40+
41+
return ToolSchema(
42+
properties = this["properties"]?.jsonObject,
43+
required = this["required"]?.jsonArray?.map { it.jsonPrimitive.content },
44+
)
45+
}
46+
47+
/**
48+
* Converts a [JsonSchema] from kotlinx-schema into a [ToolSchema] representation.
49+
*
50+
* This is useful when generating schemas from Kotlin data classes using kotlinx-schema's
51+
* schema generator. The schema is first serialized to JSON and then converted to [ToolSchema].
52+
*
53+
* @return A [ToolSchema] instance with the schema's properties and required fields.
54+
* @throws IllegalArgumentException if the schema type is not "object".
55+
* @sample
56+
* ```kotlin
57+
* import kotlinx.schema.generator.core.SchemaGeneratorService
58+
* import kotlinx.schema.Description
59+
*
60+
* @Serializable
61+
* data class SearchParams(
62+
* @property:Description("Search query")
63+
* val query: String,
64+
* val limit: Int = 10
65+
* )
66+
*
67+
* val generator = SchemaGeneratorService.getGenerator(KClass::class, JsonSchema::class)
68+
* val schema = generator.generateSchema(SearchParams::class)
69+
* val toolSchema = schema.asToolSchema()
70+
* ```
71+
*/
72+
public fun JsonSchema.asToolSchema(): ToolSchema = McpJson.encodeToJsonElement(this)
73+
.jsonObject.asToolSchema()
74+
75+
/**
76+
* Converts a [FunctionCallingSchema]'s parameters into a [ToolSchema] representation.
77+
*
78+
* Extracts and converts the `parameters` field from the function calling schema,
79+
* which is typically used in OpenAI-compatible function calling APIs.
80+
*
81+
* @return A [ToolSchema] object containing the function's parameter schema.
82+
* @throws IllegalArgumentException if the parameters schema type is not "object".
83+
* @sample
84+
* ```kotlin
85+
* val functionSchema = FunctionCallingSchema(
86+
* name = "search",
87+
* description = "Search the web",
88+
* parameters = ObjectPropertyDefinition(
89+
* properties = mapOf(
90+
* "query" to StringPropertyDefinition(description = "Search query")
91+
* ),
92+
* required = listOf("query")
93+
* )
94+
* )
95+
* val toolSchema = functionSchema.asToolSchema()
96+
* ```
97+
*/
98+
public fun FunctionCallingSchema.asToolSchema(): ToolSchema =
99+
McpJson.encodeToJsonElement(this.parameters).jsonObject.asToolSchema()
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package io.modelcontextprotocol.kotlin.sdk.types
2+
3+
import io.kotest.matchers.collections.shouldContainExactly
4+
import io.kotest.matchers.nulls.shouldNotBeNull
5+
import io.kotest.matchers.shouldBe
6+
import kotlinx.schema.json.ArrayPropertyDefinition
7+
import kotlinx.schema.json.FunctionCallingSchema
8+
import kotlinx.schema.json.ObjectPropertyDefinition
9+
import kotlinx.schema.json.StringPropertyDefinition
10+
import kotlin.test.Test
11+
12+
class FunctionCallingSchemaAsToolSchemaTest {
13+
14+
@Test
15+
fun `should convert function parameters`() {
16+
val functionSchema = FunctionCallingSchema(
17+
name = "calculate",
18+
description = "Perform calculation",
19+
parameters = ObjectPropertyDefinition(
20+
properties = mapOf(
21+
"operation" to StringPropertyDefinition(
22+
description = "Math operation",
23+
),
24+
"x" to StringPropertyDefinition(),
25+
"y" to StringPropertyDefinition(),
26+
),
27+
required = listOf("operation", "x", "y"),
28+
),
29+
)
30+
31+
val toolSchema = functionSchema.asToolSchema()
32+
33+
toolSchema.type shouldBe "object"
34+
toolSchema.properties.shouldNotBeNull {
35+
keys shouldContainExactly setOf("operation", "x", "y")
36+
}
37+
toolSchema.required shouldContainExactly listOf("operation", "x", "y")
38+
}
39+
40+
@Test
41+
fun `should handle required and optional parameters`() {
42+
// Test with some required, some optional
43+
val mixedSchema = FunctionCallingSchema(
44+
name = "search",
45+
parameters = ObjectPropertyDefinition(
46+
properties = mapOf(
47+
"query" to StringPropertyDefinition(description = "Search query"),
48+
"limit" to StringPropertyDefinition(description = "Result limit"),
49+
),
50+
required = listOf("query"),
51+
),
52+
)
53+
54+
val mixedToolSchema = mixedSchema.asToolSchema()
55+
mixedToolSchema.type shouldBe "object"
56+
mixedToolSchema.properties.shouldNotBeNull {
57+
keys shouldContainExactly setOf("query", "limit")
58+
}
59+
mixedToolSchema.required shouldContainExactly listOf("query")
60+
61+
// Test with all optional (empty required list)
62+
val allOptionalSchema = FunctionCallingSchema(
63+
name = "list",
64+
parameters = ObjectPropertyDefinition(
65+
properties = mapOf("filter" to StringPropertyDefinition()),
66+
required = emptyList(),
67+
),
68+
)
69+
70+
val allOptionalToolSchema = allOptionalSchema.asToolSchema()
71+
allOptionalToolSchema.required.shouldNotBeNull {
72+
this shouldContainExactly emptyList()
73+
}
74+
}
75+
76+
@Test
77+
fun `should handle complex nested parameter types`() {
78+
val functionSchema = FunctionCallingSchema(
79+
name = "process",
80+
parameters = ObjectPropertyDefinition(
81+
properties = mapOf(
82+
"items" to ArrayPropertyDefinition(
83+
items = StringPropertyDefinition(),
84+
description = "List of items to process",
85+
),
86+
"config" to ObjectPropertyDefinition(
87+
properties = mapOf(
88+
"timeout" to StringPropertyDefinition(description = "Timeout in seconds"),
89+
"retries" to StringPropertyDefinition(description = "Number of retries"),
90+
),
91+
required = listOf("timeout"),
92+
),
93+
),
94+
required = listOf("items", "config"),
95+
),
96+
)
97+
98+
val toolSchema = functionSchema.asToolSchema()
99+
100+
toolSchema.type shouldBe "object"
101+
toolSchema.properties.shouldNotBeNull {
102+
keys shouldContainExactly setOf("items", "config")
103+
}
104+
toolSchema.required shouldContainExactly listOf("items", "config")
105+
}
106+
107+
@Test
108+
fun `should handle parameters count variations`() {
109+
// Empty parameters
110+
val emptySchema = FunctionCallingSchema(
111+
name = "ping",
112+
parameters = ObjectPropertyDefinition(
113+
properties = emptyMap(),
114+
required = emptyList(),
115+
),
116+
)
117+
118+
val emptyToolSchema = emptySchema.asToolSchema()
119+
emptyToolSchema.type shouldBe "object"
120+
emptyToolSchema.properties.shouldNotBeNull {
121+
keys shouldContainExactly emptySet()
122+
}
123+
124+
// Single parameter
125+
val singleSchema = FunctionCallingSchema(
126+
name = "greet",
127+
parameters = ObjectPropertyDefinition(
128+
properties = mapOf(
129+
"name" to StringPropertyDefinition(description = "Name to greet"),
130+
),
131+
required = listOf("name"),
132+
),
133+
)
134+
135+
val singleToolSchema = singleSchema.asToolSchema()
136+
singleToolSchema.properties.shouldNotBeNull {
137+
keys shouldContainExactly setOf("name")
138+
}
139+
singleToolSchema.required shouldContainExactly listOf("name")
140+
}
141+
}

0 commit comments

Comments
 (0)