Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2026 the original author or authors.
*/

package io.modelcontextprotocol.client;
Expand All @@ -15,6 +15,9 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
Expand All @@ -30,16 +33,14 @@
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.util.ToolNameValidator;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.ToolNameValidator;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

Expand Down Expand Up @@ -171,6 +172,8 @@ public class McpAsyncClient {
*/
private final boolean enableCallToolSchemaCaching;

private final boolean applyElicitationDefaults;

/**
* Create a new McpAsyncClient with the given transport and session request-response
* timeout.
Expand All @@ -195,6 +198,7 @@ public class McpAsyncClient {
this.jsonSchemaValidator = jsonSchemaValidator;
this.toolsOutputSchemaCache = new ConcurrentHashMap<>();
this.enableCallToolSchemaCaching = features.enableCallToolSchemaCaching();
this.applyElicitationDefaults = features.applyElicitationDefaults();

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

return this.elicitationHandler.apply(request);
return this.elicitationHandler.apply(request).map(result -> {
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
&& result.content() != null) {
Map<String, Object> merged = new HashMap<>(result.content());
applyElicitationDefaults(request.requestedSchema(), merged);
return new ElicitResult(result.action(), merged, result.meta());
}
return result;
});
};
}

/**
* Applies default values from the elicitation schema into a result-content map: for
* each top-level property in {@code schema.properties} that declares a
* {@code "default"}, the value is inserted into {@code content} when the key is
* absent.
* <p>
* Only top-level properties are visited; nested objects and {@code anyOf}/
* {@code oneOf} branches are not traversed. This is sufficient for SEP-1034's flat
* elicitation primitive schemas (string, number, boolean, enum).
* @param schema the {@code requestedSchema} from the {@link ElicitRequest}
* @param content the mutable content map to update
*/
@SuppressWarnings("unchecked")
static void applyElicitationDefaults(Map<String, Object> schema, Map<String, Object> content) {
if (schema == null || content == null) {
return;
}

Object propertiesObj = schema.get("properties");
if (!(propertiesObj instanceof Map)) {
return;
}

Map<String, Object> properties = (Map<String, Object>) propertiesObj;
for (Map.Entry<String, Object> entry : properties.entrySet()) {
String key = entry.getKey();
Object propDef = entry.getValue();

if (!(propDef instanceof Map)) {
continue;
}

Map<String, Object> propMap = (Map<String, Object>) propDef;
if (!content.containsKey(key) && propMap.containsKey("default")) {
content.put(key, propMap.get("default"));
}
}
}

// --------------------------
// Tools
// --------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2026 the original author or authors.
*/

package io.modelcontextprotocol.client;
Expand Down Expand Up @@ -195,6 +195,8 @@ class SyncSpec {

private boolean enableCallToolSchemaCaching = false; // Default to false

private boolean applyElicitationDefaults = false; // Default to false

private SyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
Expand Down Expand Up @@ -479,6 +481,19 @@ public SyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching)
return this;
}

/**
* Enables SDK-side merging of elicitation schema defaults into an accepted
* {@link ElicitResult}'s {@code content} for fields the elicitation handler left
* unset. This is a client-local behavior and is NOT serialized as part of the MCP
* capability handshake.
* @param applyElicitationDefaults true to enable, false to disable
* @return This builder instance for method chaining
*/
public SyncSpec applyElicitationDefaults(boolean applyElicitationDefaults) {
this.applyElicitationDefaults = applyElicitationDefaults;
return this;
}

/**
* Create an instance of {@link McpSyncClient} with the provided configurations or
* sensible defaults.
Expand All @@ -488,7 +503,7 @@ public McpSyncClient build() {
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
this.elicitationHandler, this.enableCallToolSchemaCaching);
this.elicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);

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

Expand Down Expand Up @@ -549,6 +564,8 @@ class AsyncSpec {

private boolean enableCallToolSchemaCaching = false; // Default to false

private boolean applyElicitationDefaults = false; // Default to false

private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
Expand Down Expand Up @@ -820,6 +837,19 @@ public AsyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching
return this;
}

/**
* Enables SDK-side merging of elicitation schema defaults into an accepted
* {@link ElicitResult}'s {@code content} for fields the elicitation handler left
* unset. This is a client-local behavior and is NOT serialized as part of the MCP
* capability handshake.
* @param applyElicitationDefaults true to enable, false to disable
* @return This builder instance for method chaining
*/
public AsyncSpec applyElicitationDefaults(boolean applyElicitationDefaults) {
this.applyElicitationDefaults = applyElicitationDefaults;
return this;
}

/**
* Create an instance of {@link McpAsyncClient} with the provided configurations
* or sensible defaults.
Expand All @@ -833,7 +863,8 @@ public McpAsyncClient build() {
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching,
this.applyElicitationDefaults));
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2026 the original author or authors.
*/

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

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

Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
Expand All @@ -119,6 +125,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler;
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
this.applyElicitationDefaults = applyElicitationDefaults;
}

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

/**
Expand Down Expand Up @@ -194,7 +201,7 @@ public static Async fromSync(Sync syncSpec) {
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
syncSpec.enableCallToolSchemaCaching);
syncSpec.enableCallToolSchemaCaching, syncSpec.applyElicitationDefaults);
}
}

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

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

Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
Expand All @@ -270,6 +283,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler;
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
this.applyElicitationDefaults = applyElicitationDefaults;
}

/**
Expand All @@ -285,7 +299,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
elicitationHandler, false);
elicitationHandler, false, false);
}
}

Expand Down
Loading
Loading