-
Notifications
You must be signed in to change notification settings - Fork 70
Add support for testing JSON serialization of the mcp API #45
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
Add support for testing JSON serialization of the mcp API #45
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @morisil, thank you for the PR! Nicely done, lgtm!
|
||
// see https://docs.anthropic.com/en/docs/build-with-claude/tool-use | ||
/* language=json */ | ||
private val getWeatherToolJson = """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use buildJsonObject
here: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/build-json-object.html
…xtprotocol#45) * Add kotest-assertions-json dependency + ToolSerializationTest * ToolSerializationTest update
Here's my full serialization code: package io.modelcontextprotocol.kotlin.sdk.shared
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.json.*
@OptIn(ExperimentalSerializationApi::class)
@SerialInfo
@Target(AnnotationTarget.PROPERTY)
public annotation class SchemaDescription(val why: String)
@OptIn(ExperimentalSerializationApi::class)
public fun buildParametersSchema(serializer: KSerializer<*>): JsonObject {
val descriptor = serializer.descriptor
val properties = buildJsonObject {
repeat(descriptor.elementsCount) { index ->
val propName = descriptor.getElementName(index)
val propDescriptor = descriptor.getElementDescriptor(index)
val annotations = descriptor.getElementAnnotations(index)
val description = annotations.filterIsInstance<SchemaDescription>().firstOrNull()?.why ?: ""
val propSchema = buildJsonObject {
addSchemaForDescriptor(propDescriptor)
if (description.isNotEmpty()) {
put("description", JsonPrimitive(description))
}
}
put(propName, propSchema)
}
}
val required = buildJsonArray {
repeat(descriptor.elementsCount) { index ->
if (!descriptor.isElementOptional(index)) {
add(descriptor.getElementName(index))
}
}
}
return buildJsonObject {
put("type", JsonPrimitive("object"))
put("properties", properties)
if (required.size > 0) {
put("required", required)
}
}
}
@OptIn(ExperimentalSerializationApi::class)
private fun JsonObjectBuilder.addSchemaForDescriptor(descriptor: SerialDescriptor) {
when (val kind = descriptor.kind) {
is PrimitiveKind -> handlePrimitiveKind(kind)
is SerialKind.ENUM -> handleEnumType(descriptor)
is StructureKind.LIST -> handleListType(descriptor)
is StructureKind.CLASS -> handleClassType(descriptor)
else -> throw IllegalArgumentException("Unsupported type kind: $kind")
}
}
private fun JsonObjectBuilder.handlePrimitiveKind(kind: PrimitiveKind) {
val type = when (kind) {
PrimitiveKind.BOOLEAN -> "boolean"
PrimitiveKind.INT, PrimitiveKind.LONG, PrimitiveKind.SHORT -> "integer"
PrimitiveKind.FLOAT, PrimitiveKind.DOUBLE -> "number"
PrimitiveKind.STRING -> "string"
else -> throw IllegalArgumentException("Unsupported primitive type: $kind")
}
put("type", JsonPrimitive(type))
}
@OptIn(ExperimentalSerializationApi::class)
private fun JsonObjectBuilder.handleEnumType(descriptor: SerialDescriptor) {
put("type", JsonPrimitive("string"))
val enumClass: Class<out Any>? = Class.forName(descriptor.serialName)
if (enumClass != null && enumClass.isEnum) {
val enumValues = enumClass.enumConstants.map { (it as Enum<*>).name }
put("enum", JsonArray(enumValues.map { JsonPrimitive(it) }))
}
}
@OptIn(ExperimentalSerializationApi::class)
private fun JsonObjectBuilder.handleListType(descriptor: SerialDescriptor) {
put("type", JsonPrimitive("array"))
val elementDescriptor = descriptor.getElementDescriptor(0)
put("items", buildJsonObject { addSchemaForDescriptor(elementDescriptor) })
}
@OptIn(ExperimentalSerializationApi::class)
private fun JsonObjectBuilder.handleClassType(descriptor: SerialDescriptor) {
put("type", JsonPrimitive("object"))
val classProperties = buildJsonObject {
repeat(descriptor.elementsCount) { index ->
val propName = descriptor.getElementName(index)
val propDescriptor = descriptor.getElementDescriptor(index)
put(propName, buildJsonObject { addSchemaForDescriptor(propDescriptor) })
}
}
put("properties", classProperties)
val required = buildJsonArray {
repeat(descriptor.elementsCount) { index ->
if (!descriptor.isElementOptional(index)) {
add(descriptor.getElementName(index))
}
}
}
if (required.size > 0) {
put("required", required)
}
} Test it with following classes: enum class TempType {
celsius,
fahrenheit,
}
@Serializable
class Prop (
@SchemaDescription("The city and state, e.g. San Francisco, CA")
val location: String,
@SchemaDescription("The temperature unit to use. Infer this from the users location.")
val format: TempType,
@SchemaDescription("The number of days to forecast")
@SerialName("num_days")
val nunDays: Int
) |
I added:
kotest-assertions-json
dependencyToolSerializationTest
to verify correctness of tool serializationMotivation and Context
This change does not provide any feature, except for improving test coverage. It paves the way for my subsequent PRs presenting JSON schema inputs which are valid and might appear in MCP, but cannot be expressed by the current
Tool.Input
.See also:
How Has This Been Tested?
It comes with the test itself.
Breaking Changes
No
Types of changes
Checklist