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:
Pavel Gorgulov 2026-04-01 16:31:34 +02:00 • committed by GitHub
commit e21d2298a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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()
}
}