Skip to content

Commit accba74

Browse files
chemicLSainath Reddy Bobbala
andauthored
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>
1 parent 6b71136 commit accba74

6 files changed

Lines changed: 587 additions & 18 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2026 the original author or authors.
33
*/
44

55
package io.modelcontextprotocol.client;
@@ -15,6 +15,9 @@
1515
import java.util.concurrent.ConcurrentHashMap;
1616
import java.util.function.Function;
1717

18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
1821
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
1922
import io.modelcontextprotocol.json.TypeRef;
2023
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
@@ -30,16 +33,14 @@
3033
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
3134
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
3235
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
33-
import io.modelcontextprotocol.util.ToolNameValidator;
3436
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
3537
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
3638
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
3739
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
3840
import io.modelcontextprotocol.spec.McpSchema.Root;
3941
import io.modelcontextprotocol.util.Assert;
42+
import io.modelcontextprotocol.util.ToolNameValidator;
4043
import io.modelcontextprotocol.util.Utils;
41-
import org.slf4j.Logger;
42-
import org.slf4j.LoggerFactory;
4344
import reactor.core.publisher.Flux;
4445
import reactor.core.publisher.Mono;
4546

@@ -171,6 +172,8 @@ public class McpAsyncClient {
171172
*/
172173
private final boolean enableCallToolSchemaCaching;
173174

175+
private final boolean applyElicitationDefaults;
176+
174177
/**
175178
* Create a new McpAsyncClient with the given transport and session request-response
176179
* timeout.
@@ -195,6 +198,7 @@ public class McpAsyncClient {
195198
this.jsonSchemaValidator = jsonSchemaValidator;
196199
this.toolsOutputSchemaCache = new ConcurrentHashMap<>();
197200
this.enableCallToolSchemaCaching = features.enableCallToolSchemaCaching();
201+
this.applyElicitationDefaults = features.applyElicitationDefaults();
198202

199203
// Request Handlers
200204
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
@@ -561,10 +565,57 @@ private RequestHandler<ElicitResult> elicitationCreateHandler() {
561565
ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
562566
});
563567

564-
return this.elicitationHandler.apply(request);
568+
return this.elicitationHandler.apply(request).map(result -> {
569+
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
570+
&& result.content() != null) {
571+
Map<String, Object> merged = new HashMap<>(result.content());
572+
applyElicitationDefaults(request.requestedSchema(), merged);
573+
return new ElicitResult(result.action(), merged, result.meta());
574+
}
575+
return result;
576+
});
565577
};
566578
}
567579

580+
/**
581+
* Applies default values from the elicitation schema into a result-content map: for
582+
* each top-level property in {@code schema.properties} that declares a
583+
* {@code "default"}, the value is inserted into {@code content} when the key is
584+
* absent.
585+
* <p>
586+
* Only top-level properties are visited; nested objects and {@code anyOf}/
587+
* {@code oneOf} branches are not traversed. This is sufficient for SEP-1034's flat
588+
* elicitation primitive schemas (string, number, boolean, enum).
589+
* @param schema the {@code requestedSchema} from the {@link ElicitRequest}
590+
* @param content the mutable content map to update
591+
*/
592+
@SuppressWarnings("unchecked")
593+
static void applyElicitationDefaults(Map<String, Object> schema, Map<String, Object> content) {
594+
if (schema == null || content == null) {
595+
return;
596+
}
597+
598+
Object propertiesObj = schema.get("properties");
599+
if (!(propertiesObj instanceof Map)) {
600+
return;
601+
}
602+
603+
Map<String, Object> properties = (Map<String, Object>) propertiesObj;
604+
for (Map.Entry<String, Object> entry : properties.entrySet()) {
605+
String key = entry.getKey();
606+
Object propDef = entry.getValue();
607+
608+
if (!(propDef instanceof Map)) {
609+
continue;
610+
}
611+
612+
Map<String, Object> propMap = (Map<String, Object>) propDef;
613+
if (!content.containsKey(key) && propMap.containsKey("default")) {
614+
content.put(key, propMap.get("default"));
615+
}
616+
}
617+
}
618+
568619
// --------------------------
569620
// Tools
570621
// --------------------------

mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2026 the original author or authors.
33
*/
44

55
package io.modelcontextprotocol.client;
@@ -195,6 +195,8 @@ class SyncSpec {
195195

196196
private boolean enableCallToolSchemaCaching = false; // Default to false
197197

198+
private boolean applyElicitationDefaults = false; // Default to false
199+
198200
private SyncSpec(McpClientTransport transport) {
199201
Assert.notNull(transport, "Transport must not be null");
200202
this.transport = transport;
@@ -479,6 +481,19 @@ public SyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching)
479481
return this;
480482
}
481483

484+
/**
485+
* Enables SDK-side merging of elicitation schema defaults into an accepted
486+
* {@link ElicitResult}'s {@code content} for fields the elicitation handler left
487+
* unset. This is a client-local behavior and is NOT serialized as part of the MCP
488+
* capability handshake.
489+
* @param applyElicitationDefaults true to enable, false to disable
490+
* @return This builder instance for method chaining
491+
*/
492+
public SyncSpec applyElicitationDefaults(boolean applyElicitationDefaults) {
493+
this.applyElicitationDefaults = applyElicitationDefaults;
494+
return this;
495+
}
496+
482497
/**
483498
* Create an instance of {@link McpSyncClient} with the provided configurations or
484499
* sensible defaults.
@@ -488,7 +503,7 @@ public McpSyncClient build() {
488503
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
489504
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
490505
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
491-
this.elicitationHandler, this.enableCallToolSchemaCaching);
506+
this.elicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
492507

493508
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
494509

@@ -549,6 +564,8 @@ class AsyncSpec {
549564

550565
private boolean enableCallToolSchemaCaching = false; // Default to false
551566

567+
private boolean applyElicitationDefaults = false; // Default to false
568+
552569
private AsyncSpec(McpClientTransport transport) {
553570
Assert.notNull(transport, "Transport must not be null");
554571
this.transport = transport;
@@ -820,6 +837,19 @@ public AsyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching
820837
return this;
821838
}
822839

840+
/**
841+
* Enables SDK-side merging of elicitation schema defaults into an accepted
842+
* {@link ElicitResult}'s {@code content} for fields the elicitation handler left
843+
* unset. This is a client-local behavior and is NOT serialized as part of the MCP
844+
* capability handshake.
845+
* @param applyElicitationDefaults true to enable, false to disable
846+
* @return This builder instance for method chaining
847+
*/
848+
public AsyncSpec applyElicitationDefaults(boolean applyElicitationDefaults) {
849+
this.applyElicitationDefaults = applyElicitationDefaults;
850+
return this;
851+
}
852+
823853
/**
824854
* Create an instance of {@link McpAsyncClient} with the provided configurations
825855
* or sensible defaults.
@@ -833,7 +863,8 @@ public McpAsyncClient build() {
833863
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
834864
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
835865
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
836-
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
866+
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching,
867+
this.applyElicitationDefaults));
837868
}
838869

839870
}

mcp-core/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2026 the original author or authors.
33
*/
44

55
package io.modelcontextprotocol.client;
@@ -63,6 +63,9 @@ class McpClientFeatures {
6363
* @param samplingHandler the sampling handler.
6464
* @param elicitationHandler the elicitation handler.
6565
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
66+
* @param applyElicitationDefaults whether the client should fill in missing fields of
67+
* an accepted {@code ElicitResult.content} with the {@code default} values declared
68+
* in the {@code requestedSchema}.
6669
*/
6770
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
6871
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
@@ -73,7 +76,7 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
7376
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
7477
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
7578
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
76-
boolean enableCallToolSchemaCaching) {
79+
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
7780

7881
/**
7982
* Create an instance and validate the arguments.
@@ -87,6 +90,9 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
8790
* @param samplingHandler the sampling handler.
8891
* @param elicitationHandler the elicitation handler.
8992
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
93+
* @param applyElicitationDefaults whether the client should fill in missing
94+
* fields of an accepted {@code ElicitResult.content} with the {@code default}
95+
* values declared in the {@code requestedSchema}.
9096
*/
9197
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
9298
Map<String, McpSchema.Root> roots,
@@ -98,7 +104,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
98104
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
99105
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
100106
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
101-
boolean enableCallToolSchemaCaching) {
107+
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
102108

103109
Assert.notNull(clientInfo, "Client info must not be null");
104110
this.clientInfo = clientInfo;
@@ -119,6 +125,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
119125
this.samplingHandler = samplingHandler;
120126
this.elicitationHandler = elicitationHandler;
121127
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
128+
this.applyElicitationDefaults = applyElicitationDefaults;
122129
}
123130

124131
/**
@@ -135,7 +142,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
135142
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
136143
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
137144
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
138-
elicitationHandler, false);
145+
elicitationHandler, false, false);
139146
}
140147

141148
/**
@@ -194,7 +201,7 @@ public static Async fromSync(Sync syncSpec) {
194201
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
195202
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
196203
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
197-
syncSpec.enableCallToolSchemaCaching);
204+
syncSpec.enableCallToolSchemaCaching, syncSpec.applyElicitationDefaults);
198205
}
199206
}
200207

@@ -213,6 +220,9 @@ public static Async fromSync(Sync syncSpec) {
213220
* @param samplingHandler the sampling handler.
214221
* @param elicitationHandler the elicitation handler.
215222
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
223+
* @param applyElicitationDefaults whether the client should fill in missing fields of
224+
* an accepted {@code ElicitResult.content} with the {@code default} values declared
225+
* in the {@code requestedSchema}.
216226
*/
217227
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
218228
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
@@ -223,7 +233,7 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
223233
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
224234
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
225235
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
226-
boolean enableCallToolSchemaCaching) {
236+
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
227237

228238
/**
229239
* Create an instance and validate the arguments.
@@ -239,6 +249,9 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
239249
* @param samplingHandler the sampling handler.
240250
* @param elicitationHandler the elicitation handler.
241251
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
252+
* @param applyElicitationDefaults whether the client should fill in missing
253+
* fields of an accepted {@code ElicitResult.content} with the {@code default}
254+
* values declared in the {@code requestedSchema}.
242255
*/
243256
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
244257
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
@@ -249,7 +262,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
249262
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
250263
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
251264
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
252-
boolean enableCallToolSchemaCaching) {
265+
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
253266

254267
Assert.notNull(clientInfo, "Client info must not be null");
255268
this.clientInfo = clientInfo;
@@ -270,6 +283,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
270283
this.samplingHandler = samplingHandler;
271284
this.elicitationHandler = elicitationHandler;
272285
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
286+
this.applyElicitationDefaults = applyElicitationDefaults;
273287
}
274288

275289
/**
@@ -285,7 +299,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
285299
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
286300
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
287301
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
288-
elicitationHandler, false);
302+
elicitationHandler, false, false);
289303
}
290304
}
291305

0 commit comments

Comments
 (0)