fix: apply elicitation schema defaults for missing fields in client response (#661)
Apply default values from the elicitation schema to the client's response when the handler returns `Accept` with missing fields closes #414 ## How Has This Been Tested? integration and conformance tests ## Breaking Changes none ## Types of changes - [x] 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 - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed
This commit is contained in:
parent
9c4a2394b4
commit
e21d2298a1
1 changed files with 248 additions and 3 deletions
|
|
@ -7,9 +7,11 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerSession
|
|||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
|
||||
|
|
@ -17,6 +19,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
|
|||
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse
|
||||
|
|
@ -38,8 +41,11 @@ import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS
|
|||
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.StringSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Tool
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
|
|
@ -48,7 +54,10 @@ import kotlinx.coroutines.joinAll
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.test.Test
|
||||
|
|
@ -1110,4 +1119,175 @@ class ClientTest {
|
|||
assertEquals(elicitationResultAction, result.action)
|
||||
assertEquals(elicitationResultContent, result.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should apply elicitation defaults for missing fields in empty content`() = runTest {
|
||||
val schema = defaultsTestSchema()
|
||||
val (client, serverSession) = setupElicitationPair {
|
||||
ElicitResult(action = ElicitResult.Action.Accept, content = JsonObject(emptyMap()))
|
||||
}
|
||||
|
||||
val result = serverSession.createElicitation(message = "fill defaults", requestedSchema = schema)
|
||||
|
||||
assertEquals(ElicitResult.Action.Accept, result.action)
|
||||
val content = result.content!!
|
||||
assertEquals(JsonPrimitive("John Doe"), content["name"])
|
||||
assertEquals(JsonPrimitive(30), content["age"])
|
||||
assertEquals(JsonPrimitive(95.5), content["score"])
|
||||
assertEquals(JsonPrimitive("active"), content["status"])
|
||||
assertEquals(JsonPrimitive(true), content["verified"])
|
||||
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should apply elicitation defaults only for missing fields preserving user values`() = runTest {
|
||||
val schema = defaultsTestSchema()
|
||||
val userContent = buildJsonObject { put("name", "Custom Name") }
|
||||
val (client, serverSession) = setupElicitationPair {
|
||||
ElicitResult(action = ElicitResult.Action.Accept, content = userContent)
|
||||
}
|
||||
|
||||
val result = serverSession.createElicitation(message = "partial", requestedSchema = schema)
|
||||
|
||||
val content = result.content!!
|
||||
assertEquals(JsonPrimitive("Custom Name"), content["name"])
|
||||
assertEquals(JsonPrimitive(30), content["age"])
|
||||
assertEquals(JsonPrimitive(95.5), content["score"])
|
||||
assertEquals(JsonPrimitive("active"), content["status"])
|
||||
assertEquals(JsonPrimitive(true), content["verified"])
|
||||
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not apply elicitation defaults when action is decline`() = runTest {
|
||||
val schema = defaultsTestSchema()
|
||||
val (client, serverSession) = setupElicitationPair {
|
||||
ElicitResult(action = ElicitResult.Action.Decline)
|
||||
}
|
||||
|
||||
val result = serverSession.createElicitation(message = "decline", requestedSchema = schema)
|
||||
|
||||
assertEquals(ElicitResult.Action.Decline, result.action)
|
||||
assertNull(result.content)
|
||||
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not apply elicitation defaults when action is cancel`() = runTest {
|
||||
val schema = defaultsTestSchema()
|
||||
val (client, serverSession) = setupElicitationPair {
|
||||
ElicitResult(action = ElicitResult.Action.Cancel)
|
||||
}
|
||||
|
||||
val result = serverSession.createElicitation(message = "cancel", requestedSchema = schema)
|
||||
|
||||
assertEquals(ElicitResult.Action.Cancel, result.action)
|
||||
assertNull(result.content)
|
||||
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not modify content when schema has no defaults`() = runTest {
|
||||
val schema = ElicitRequestParams.RequestedSchema(
|
||||
properties = mapOf(
|
||||
"name" to StringSchema(description = "name"),
|
||||
"age" to IntegerSchema(description = "age"),
|
||||
),
|
||||
)
|
||||
val emptyContent = JsonObject(emptyMap())
|
||||
val (client, serverSession) = setupElicitationPair {
|
||||
ElicitResult(action = ElicitResult.Action.Accept, content = emptyContent)
|
||||
}
|
||||
|
||||
val result = serverSession.createElicitation(message = "no defaults", requestedSchema = schema)
|
||||
|
||||
assertEquals(ElicitResult.Action.Accept, result.action)
|
||||
assertTrue(result.content!!.isEmpty())
|
||||
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should apply elicitation defaults for multi-select enum schemas`() = runTest {
|
||||
val schema = ElicitRequestParams.RequestedSchema(
|
||||
properties = mapOf(
|
||||
"tags" to UntitledMultiSelectEnumSchema(
|
||||
description = "tags",
|
||||
items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("a", "b", "c")),
|
||||
default = listOf("a", "b"),
|
||||
),
|
||||
"options" to TitledMultiSelectEnumSchema(
|
||||
description = "options",
|
||||
items = TitledMultiSelectEnumSchema.Items(
|
||||
anyOf = listOf(
|
||||
io.modelcontextprotocol.kotlin.sdk.types.EnumOption("x", "X"),
|
||||
io.modelcontextprotocol.kotlin.sdk.types.EnumOption("y", "Y"),
|
||||
),
|
||||
),
|
||||
default = listOf("x"),
|
||||
),
|
||||
),
|
||||
)
|
||||
val (client, serverSession) = setupElicitationPair {
|
||||
ElicitResult(action = ElicitResult.Action.Accept, content = JsonObject(emptyMap()))
|
||||
}
|
||||
|
||||
val result = serverSession.createElicitation(message = "multi-select", requestedSchema = schema)
|
||||
|
||||
val content = result.content!!
|
||||
val tags = content["tags"]!!
|
||||
assertIs<kotlinx.serialization.json.JsonArray>(tags)
|
||||
assertEquals(2, tags.size)
|
||||
assertEquals("a", tags[0].jsonPrimitive.content)
|
||||
assertEquals("b", tags[1].jsonPrimitive.content)
|
||||
|
||||
val options = content["options"]!!
|
||||
assertIs<kotlinx.serialization.json.JsonArray>(options)
|
||||
assertEquals(1, options.size)
|
||||
assertEquals("x", options[0].jsonPrimitive.content)
|
||||
|
||||
client.close()
|
||||
}
|
||||
|
||||
private fun defaultsTestSchema(): ElicitRequestParams.RequestedSchema = ElicitRequestParams.RequestedSchema(
|
||||
properties = mapOf(
|
||||
"name" to StringSchema(description = "User name", default = "John Doe"),
|
||||
"age" to IntegerSchema(description = "User age", default = 30),
|
||||
"score" to DoubleSchema(description = "User score", default = 95.5),
|
||||
"status" to UntitledSingleSelectEnumSchema(
|
||||
description = "User status",
|
||||
enumValues = listOf("active", "inactive", "pending"),
|
||||
default = "active",
|
||||
),
|
||||
"verified" to BooleanSchema(description = "Verification status", default = true),
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun setupElicitationPair(
|
||||
handler: (io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest) -> ElicitResult,
|
||||
): Pair<Client, ServerSession> = kotlinx.coroutines.coroutineScope {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(capabilities = ClientCapabilities(elicitation = EmptyJsonObject)),
|
||||
)
|
||||
client.setElicitationHandler(handler)
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val server = Server(
|
||||
serverInfo = Implementation(name = "test server", version = "1.0"),
|
||||
options = ServerOptions(capabilities = ServerCapabilities()),
|
||||
)
|
||||
|
||||
val serverSessionResult = CompletableDeferred<ServerSession>()
|
||||
listOf(
|
||||
launch { client.connect(clientTransport) },
|
||||
launch { serverSessionResult.complete(server.createSession(serverTransport)) },
|
||||
).joinAll()
|
||||
|
||||
client to serverSessionResult.await()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue