diff --git a/anthropic-java-mcp/src/main/kotlin/com/anthropic/mcp/BetaMcp.kt b/anthropic-java-mcp/src/main/kotlin/com/anthropic/mcp/BetaMcp.kt index adaa1220f..a3ee73b5d 100644 --- a/anthropic-java-mcp/src/main/kotlin/com/anthropic/mcp/BetaMcp.kt +++ b/anthropic-java-mcp/src/main/kotlin/com/anthropic/mcp/BetaMcp.kt @@ -394,6 +394,9 @@ object BetaMcp { private fun betaInputSchema(schema: McpSchema.JsonSchema): BetaTool.InputSchema { val builder = BetaTool.InputSchema.builder() schema.required()?.let { builder.required(it) } + schema.additionalProperties()?.let { + builder.putAdditionalProperty("additionalProperties", JsonValue.from(it)) + } schema.properties()?.let { builder.putAdditionalProperty("properties", JsonValue.from(it)) } schema.defs()?.let { builder.putAdditionalProperty("\$defs", JsonValue.from(it)) } schema.definitions()?.let { diff --git a/anthropic-java-mcp/src/test/kotlin/com/anthropic/mcp/BetaMcpAdditionalPropertiesTest.kt b/anthropic-java-mcp/src/test/kotlin/com/anthropic/mcp/BetaMcpAdditionalPropertiesTest.kt new file mode 100644 index 000000000..058ed8b0f --- /dev/null +++ b/anthropic-java-mcp/src/test/kotlin/com/anthropic/mcp/BetaMcpAdditionalPropertiesTest.kt @@ -0,0 +1,82 @@ +package com.anthropic.mcp + +import com.fasterxml.jackson.databind.ObjectMapper +import io.modelcontextprotocol.client.McpSyncClient +import io.modelcontextprotocol.spec.McpSchema +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.kotlin.mock + +internal class BetaMcpAdditionalPropertiesTest { + private val client: McpSyncClient = mock() + private val mapper = ObjectMapper() + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun preservesExplicitAdditionalProperties(allowed: Boolean) { + val schema = + McpSchema.JsonSchema( + "object", + mapOf("name" to mapOf("type" to "string")), + listOf("name"), + allowed, + null, + null, + ) + val before = mapper.valueToTree(schema) + val tool = McpSchema.Tool("lookup", null, "Look up a name", schema, null, null, null) + val definition = BetaMcp.mcpTool(tool, client).definition + val wire = mapper.valueToTree(definition) + assertThat(wire.path("input_schema").has("additionalProperties")).isTrue() + assertThat(wire.path("input_schema").path("additionalProperties").booleanValue()) + .isEqualTo(allowed) + assertThat(wire.path("input_schema").path("required").get(0).textValue()).isEqualTo("name") + assertThat( + wire.path("input_schema").path("properties").path("name").path("type").textValue() + ) + .isEqualTo("string") + assertThat(mapper.valueToTree(schema)) + .isEqualTo(before) + } + + @Test + fun omittedAdditionalPropertiesRemainsAbsent() { + val schema = McpSchema.JsonSchema("object", emptyMap(), emptyList(), null, null, null) + val tool = McpSchema.Tool("lookup", null, null, schema, null, null, null) + val wire = + mapper.valueToTree( + BetaMcp.mcpTool(tool, client).definition + ) + assertThat(wire.path("input_schema").has("additionalProperties")).isFalse() + assertThat(wire.path("input_schema").path("type").textValue()).isEqualTo("object") + } + + @Test + fun closedSchemaAndDefinitionsSurviveListConversion() { + val defs = mapOf("Name" to mapOf("type" to "string")) + val schema = + McpSchema.JsonSchema( + "object", + mapOf("name" to mapOf("$" + "ref" to "#/$" + "defs/Name")), + listOf("name"), + false, + defs, + defs, + ) + val tool = McpSchema.Tool("lookup", null, null, schema, null, null, null) + val converted = BetaMcp.mcpTools(listOf(tool), client) + assertThat(converted).hasSize(1) + val wire = + mapper + .valueToTree(converted[0].definition) + .path("input_schema") + assertThat(wire.has("additionalProperties")).isTrue() + assertThat(wire.path("additionalProperties").booleanValue()).isFalse() + assertThat(wire.path("$" + "defs").path("Name").path("type").textValue()) + .isEqualTo("string") + assertThat(wire.path("definitions").path("Name").path("type").textValue()) + .isEqualTo("string") + } +}