mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-230494 [MCP Server] Fix output schema generation for optional properties
(cherry picked from commit 65a37e82ef552a95515dbeedececbeb0e2696d38) IJ-MR-188464 Space-RevId: 78cc3d1451493ac2d4bd905157d255b5e356ac48 GitOrigin-RevId: 28fa1bf89d4ac06fffe9a0cf99470b56792af3aa
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f3ad3193e0
commit
fa75590de3
@@ -19,6 +19,7 @@ import io.github.smiley4.schemakenerator.jsonschema.jsonDsl.JsonArray
|
||||
import io.github.smiley4.schemakenerator.jsonschema.jsonDsl.JsonObject
|
||||
import io.github.smiley4.schemakenerator.serialization.SerializationSteps.analyzeTypeUsingKotlinxSerialization
|
||||
import io.github.smiley4.schemakenerator.serialization.analyzer.AnnotationAnalyzer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.serializerOrNull
|
||||
import kotlin.reflect.KAnnotatedElement
|
||||
@@ -86,7 +87,7 @@ fun KCallable<*>.returnTypeSchema(): McpToolSchema? {
|
||||
if (type.isSubtypeOf(typeOf<Enum<*>>())) return null
|
||||
if (type.isSubtypeOf(typeOf<McpToolCallResult>())) return null
|
||||
if (type.isSubtypeOf(typeOf<McpToolCallResultContent>())) return null
|
||||
if (serializerOrNull(type) == null) return null
|
||||
val serializer = serializerOrNull(type) ?: return null
|
||||
|
||||
val intermediateJsonSchemaData = initial(type)
|
||||
.analyzeTypeUsingKotlinxSerialization()
|
||||
@@ -99,7 +100,9 @@ fun KCallable<*>.returnTypeSchema(): McpToolSchema? {
|
||||
val jsonSchema = schema.json.toKt() as? kotlinx.serialization.json.JsonObject ?: error("Non-primitive type is expected in return type: ${type.classifier} in $this")
|
||||
val properties = jsonSchema["properties"] as? kotlinx.serialization.json.JsonObject ?: error("Properties are expected in return type: ${type.classifier} in $this")
|
||||
val required = jsonSchema["required"] as? kotlinx.serialization.json.JsonArray ?: error("Required is expected in return type: ${type.classifier} in $this")
|
||||
return McpToolSchema.ofPropertiesSchema(properties = properties, requiredProperties = required.map { it.jsonPrimitive.content }.toSet(), definitions = emptyMap(), definitionsPath = McpToolSchema.DEFAULT_DEFINITIONS_PATH)
|
||||
val requiredProperties = required.map { it.jsonPrimitive.content }.toSet()
|
||||
val adjustedRequired = removeRequiredForDefaultValues(requiredProperties, serializer)
|
||||
return McpToolSchema.ofPropertiesSchema(properties = properties, requiredProperties = adjustedRequired, definitions = emptyMap(), definitionsPath = McpToolSchema.DEFAULT_DEFINITIONS_PATH)
|
||||
}
|
||||
|
||||
private fun JsonNode.toKt(): JsonElement {
|
||||
@@ -144,6 +147,24 @@ private fun IntermediateJsonSchemaData.removeNumericBounds(): IntermediateJsonSc
|
||||
return this
|
||||
}
|
||||
|
||||
// to mark properties as optional when they have default values
|
||||
// fixes problem with EncodeDefault.Never case
|
||||
// see https://youtrack.jetbrains.com/issue/IJPL-230494
|
||||
private fun removeRequiredForDefaultValues(requiredProperties: Set<String>, serializer: KSerializer<*>): Set<String> {
|
||||
val result = mutableSetOf(*requiredProperties.toTypedArray())
|
||||
|
||||
val descriptor = serializer.descriptor
|
||||
if (descriptor.elementsCount == 0) return result
|
||||
|
||||
for (i in 0 until descriptor.elementsCount) {
|
||||
if (descriptor.isElementOptional(i)) {
|
||||
result.remove(descriptor.getElementName(i))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private const val descriptionPropertyNameInschema = "description"
|
||||
|
||||
private class JsonSchemaCoreAnnotationMcpDescriptionStep() {
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
package com.intellij.mcpserver
|
||||
|
||||
import com.intellij.mcpserver.annotations.McpDescription
|
||||
import com.intellij.mcpserver.impl.util.asTool
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.Arguments
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
@Serializable
|
||||
class Object2(
|
||||
@property:McpDescription("Int field") val intField: Int,
|
||||
@property:McpDescription("Int nullable") val intNullable: Int? = null,
|
||||
@property:McpDescription("String field") val stringField: String,
|
||||
@property:McpDescription("String nullable") val stringNullable: String? = null,
|
||||
@property:McpDescription("Boolean field") val booleanField: Boolean,
|
||||
@property:McpDescription("Boolean nullable") val booleanNullable: Boolean? = null,
|
||||
@property:McpDescription("Double field") val doubleField: Double,
|
||||
@property:McpDescription("Double nullable") val doubleNullable: Double? = null,
|
||||
@property:McpDescription("List field") val listField: List<String>,
|
||||
@property:McpDescription("List nullable") val listNullable: List<String>? = null,
|
||||
@property:McpDescription("Nested field") val nestedField: Inner,
|
||||
@property:McpDescription("Nested nullable") val nestedNullable: Inner? = null,
|
||||
@property:McpDescription("Enum field") val enumField: Kind,
|
||||
@property:McpDescription("Enum nullable") val enumNullable: Kind? = null,
|
||||
@property:McpDescription("EncodeDefault ALWAYS") @EncodeDefault(EncodeDefault.Mode.ALWAYS) val encodeDefaultAlways: String = "default",
|
||||
@property:McpDescription("EncodeDefault NEVER") @EncodeDefault(EncodeDefault.Mode.NEVER) val encodeDefaultNever: String = "default",
|
||||
@property:McpDescription("EncodeDefault ALWAYS nullable") @EncodeDefault(EncodeDefault.Mode.ALWAYS) val encodeDefaultAlwaysNullable: String? = null,
|
||||
@property:McpDescription("EncodeDefault NEVER nullable") @EncodeDefault(EncodeDefault.Mode.NEVER) val encodeDefaultNeverNullable: String? = null,
|
||||
@property:McpDescription("Default value") val defaultValue: Int = 42,
|
||||
@property:McpDescription("Bool with default false") val boolDefaultFalse: Boolean = false,
|
||||
@property:McpDescription("Bool with default true") val boolDefaultTrue: Boolean = true,
|
||||
@property:McpDescription("Bool EncodeDefault NEVER with default false") @EncodeDefault(EncodeDefault.Mode.NEVER) val boolEncodeNeverDefaultFalse: Boolean = false,
|
||||
@property:McpDescription("Bool EncodeDefault NEVER with default true") @EncodeDefault(EncodeDefault.Mode.NEVER) val boolEncodeNeverDefaultTrue: Boolean = true,
|
||||
@property:McpDescription("Bool EncodeDefault ALWAYS with default false") @EncodeDefault(EncodeDefault.Mode.ALWAYS) val boolEncodeAlwaysDefaultFalse: Boolean = false,
|
||||
@property:McpDescription("Bool EncodeDefault ALWAYS with default true") @EncodeDefault(EncodeDefault.Mode.ALWAYS) val boolEncodeAlwaysDefaultTrue: Boolean = true,
|
||||
@property:McpDescription("Bool nullable EncodeDefault NEVER") @EncodeDefault(EncodeDefault.Mode.NEVER) val boolNullableEncodeNever: Boolean? = false,
|
||||
@property:McpDescription("Bool nullable EncodeDefault ALWAYS") @EncodeDefault(EncodeDefault.Mode.ALWAYS) val boolNullableEncodeAlways: Boolean? = null,
|
||||
)
|
||||
|
||||
fun output_fun1(): Object { TODO() }
|
||||
fun output_fun2(): String { TODO() }
|
||||
@@ -19,6 +52,7 @@ fun output_fun8(): Char { TODO() }
|
||||
fun output_fun9(): McpToolCallResult { TODO() }
|
||||
fun output_fun10(): McpToolCallResultContent { TODO() }
|
||||
fun output_fun11(): Kind { TODO() }
|
||||
fun output_fun12(): Object2 { TODO() }
|
||||
|
||||
class OutputSchemaTest {
|
||||
companion object {
|
||||
@@ -94,8 +128,7 @@ class OutputSchemaTest {
|
||||
"b",
|
||||
"b1",
|
||||
"c",
|
||||
"d",
|
||||
"eEncodeDefault"
|
||||
"d"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}"""),
|
||||
@@ -109,6 +142,189 @@ class OutputSchemaTest {
|
||||
Arguments.of(::output_fun9, null),
|
||||
Arguments.of(::output_fun10, null),
|
||||
Arguments.of(::output_fun11, null),
|
||||
Arguments.of(::output_fun12, /**language=JSON*/ """{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intField": {
|
||||
"type": "integer",
|
||||
"description": "Int field"
|
||||
},
|
||||
"intNullable": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"description": "Int nullable"
|
||||
},
|
||||
"stringField": {
|
||||
"type": "string",
|
||||
"description": "String field"
|
||||
},
|
||||
"stringNullable": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "String nullable"
|
||||
},
|
||||
"booleanField": {
|
||||
"type": "boolean",
|
||||
"description": "Boolean field"
|
||||
},
|
||||
"booleanNullable": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "Boolean nullable"
|
||||
},
|
||||
"doubleField": {
|
||||
"type": "number",
|
||||
"description": "Double field"
|
||||
},
|
||||
"doubleNullable": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
],
|
||||
"description": "Double nullable"
|
||||
},
|
||||
"listField": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List field"
|
||||
},
|
||||
"listNullable": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List nullable"
|
||||
},
|
||||
"nestedField": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"e"
|
||||
],
|
||||
"properties": {
|
||||
"e": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"description": "Nested field"
|
||||
},
|
||||
"nestedNullable": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"required": [
|
||||
"e"
|
||||
],
|
||||
"properties": {
|
||||
"e": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"description": "Nested nullable"
|
||||
},
|
||||
"enumField": {
|
||||
"enum": [
|
||||
"K1",
|
||||
"K2",
|
||||
"K3"
|
||||
],
|
||||
"description": "Enum field"
|
||||
},
|
||||
"enumNullable": {
|
||||
"enum": [
|
||||
"K1",
|
||||
"K2",
|
||||
"K3"
|
||||
],
|
||||
"description": "Enum nullable"
|
||||
},
|
||||
"encodeDefaultAlways": {
|
||||
"type": "string",
|
||||
"description": "EncodeDefault ALWAYS"
|
||||
},
|
||||
"encodeDefaultNever": {
|
||||
"type": "string",
|
||||
"description": "EncodeDefault NEVER"
|
||||
},
|
||||
"encodeDefaultAlwaysNullable": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "EncodeDefault ALWAYS nullable"
|
||||
},
|
||||
"encodeDefaultNeverNullable": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "EncodeDefault NEVER nullable"
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "integer",
|
||||
"description": "Default value"
|
||||
},
|
||||
"boolDefaultFalse": {
|
||||
"type": "boolean",
|
||||
"description": "Bool with default false"
|
||||
},
|
||||
"boolDefaultTrue": {
|
||||
"type": "boolean",
|
||||
"description": "Bool with default true"
|
||||
},
|
||||
"boolEncodeNeverDefaultFalse": {
|
||||
"type": "boolean",
|
||||
"description": "Bool EncodeDefault NEVER with default false"
|
||||
},
|
||||
"boolEncodeNeverDefaultTrue": {
|
||||
"type": "boolean",
|
||||
"description": "Bool EncodeDefault NEVER with default true"
|
||||
},
|
||||
"boolEncodeAlwaysDefaultFalse": {
|
||||
"type": "boolean",
|
||||
"description": "Bool EncodeDefault ALWAYS with default false"
|
||||
},
|
||||
"boolEncodeAlwaysDefaultTrue": {
|
||||
"type": "boolean",
|
||||
"description": "Bool EncodeDefault ALWAYS with default true"
|
||||
},
|
||||
"boolNullableEncodeNever": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "Bool nullable EncodeDefault NEVER"
|
||||
},
|
||||
"boolNullableEncodeAlways": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "Bool nullable EncodeDefault ALWAYS"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"intField",
|
||||
"stringField",
|
||||
"booleanField",
|
||||
"doubleField",
|
||||
"listField",
|
||||
"nestedField",
|
||||
"enumField"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}"""),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user