Skip to content

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

Conversation

morisil
Copy link
Contributor

@morisil morisil commented Feb 19, 2025

I added:

  • kotest-assertions-json dependency
  • ToolSerializationTest to verify correctness of tool serialization

Motivation 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Copy link
Contributor

@e5l e5l left a 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 = """
Copy link
Contributor

@e5l e5l Feb 21, 2025

Choose a reason for hiding this comment

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

@e5l e5l enabled auto-merge (squash) February 21, 2025 17:19
@e5l e5l merged commit 0d6fa86 into modelcontextprotocol:main Feb 21, 2025
1 check passed
@morisil morisil deleted the feature/addSupportForTestingSerializedJson branch February 21, 2025 18:04
tinycrops pushed a commit to tinycrops/kotlin-sdk that referenced this pull request Mar 8, 2025
…xtprotocol#45)

* Add kotest-assertions-json dependency + ToolSerializationTest

* ToolSerializationTest update
@ssuukk
Copy link

ssuukk commented Mar 29, 2025

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
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants