feat: client-side application of elicitation schema defaults (SEP-1034) (#976)

Adds an opt-in McpClient builder option `applyElicitationDefaults(boolean)`
that, when enabled, fills missing keys of an accepted ElicitResult.content
with `default` values declared in the requestedSchema before returning the
result to the server. Mirrors the TypeScript SDK's applyElicitationDefaults
behavior, but exposed as a local client config rather than a wire capability.

Co-authored-by: Sainath Reddy Bobbala <bsnr@amazon.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
This commit is contained in:
Dariusz Jędrzejczyk 2026-05-22 19:51:54 +02:00 • committed by GitHub
commit accba74c8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,151 @@
/*
* Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.client;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link McpAsyncClient#applyElicitationDefaults(Map, Map)}.
*
* Verifies that the client-side default application logic correctly fills in missing
* fields from schema defaults, matching the behavior specified in SEP-1034.
*/
class McpAsyncClientElicitationDefaultsTests {
@Test
void appliesStringDefault() {
Map<String, Object> schema = Map.of("properties", Map.of("name", Map.of("type", "string", "default", "Guest")));
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("name", "Guest");
}
@Test
void appliesNumberDefault() {
Map<String, Object> schema = Map.of("properties", Map.of("age", Map.of("type", "integer", "default", 18)));
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("age", 18);
}
@Test
void appliesBooleanDefault() {
Map<String, Object> schema = Map.of("properties",
Map.of("subscribe", Map.of("type", "boolean", "default", true)));
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("subscribe", true);
}
@Test
void appliesEnumDefault() {
Map<String, Object> schema = Map.of("properties",
Map.of("color", Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")));
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("color", "green");
}
@Test
void doesNotOverrideExistingValues() {
Map<String, Object> schema = Map.of("properties", Map.of("name", Map.of("type", "string", "default", "Guest")));
Map<String, Object> content = new HashMap<>();
content.put("name", "Alice");
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("name", "Alice");
}
@Test
void skipsPropertiesWithoutDefault() {
Map<String, Object> schema = Map.of("properties", Map.of("email", Map.of("type", "string")));
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).doesNotContainKey("email");
}
@Test
void appliesMultipleDefaults() {
Map<String, Object> schema = Map.of("properties",
Map.of("name", Map.of("type", "string", "default", "Guest"), "age",
Map.of("type", "integer", "default", 18), "subscribe",
Map.of("type", "boolean", "default", true), "color",
Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")));
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("name", "Guest")
.containsEntry("age", 18)
.containsEntry("subscribe", true)
.containsEntry("color", "green");
}
@Test
void handlesNullSchema() {
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(null, content);
assertThat(content).isEmpty();
}
@Test
void handlesNullContent() {
Map<String, Object> schema = Map.of("properties", Map.of("name", Map.of("type", "string", "default", "Guest")));
// Should not throw
McpAsyncClient.applyElicitationDefaults(schema, null);
}
@Test
void handlesSchemaWithoutProperties() {
Map<String, Object> schema = Map.of("type", "object");
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).isEmpty();
}
@Test
void appliesDefaultsOnlyToMissingFields() {
Map<String, Object> schema = Map.of("properties", Map.of("name", Map.of("type", "string", "default", "Guest"),
"age", Map.of("type", "integer", "default", 18)));
Map<String, Object> content = new HashMap<>();
content.put("name", "John");
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("name", "John").containsEntry("age", 18);
}
@Test
void appliesFloatingPointDefault() {
Map<String, Object> schema = Map.of("properties", Map.of("score", Map.of("type", "number", "default", 95.5)));
Map<String, Object> content = new HashMap<>();
McpAsyncClient.applyElicitationDefaults(schema, content);
assertThat(content).containsEntry("score", 95.5);
}
}