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:
parent
6b71136224
commit
accba74c8f
6 changed files with 587 additions and 18 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2024-2024 the original author or authors.
|
* Copyright 2024-2026 the original author or authors.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
@ -15,6 +15,9 @@ import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
|
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
|
|
@ -30,16 +33,14 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||||
import io.modelcontextprotocol.util.ToolNameValidator;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
|
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
import io.modelcontextprotocol.util.ToolNameValidator;
|
||||||
import io.modelcontextprotocol.util.Utils;
|
import io.modelcontextprotocol.util.Utils;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
|
@ -171,6 +172,8 @@ public class McpAsyncClient {
|
||||||
*/
|
*/
|
||||||
private final boolean enableCallToolSchemaCaching;
|
private final boolean enableCallToolSchemaCaching;
|
||||||
|
|
||||||
|
private final boolean applyElicitationDefaults;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new McpAsyncClient with the given transport and session request-response
|
* Create a new McpAsyncClient with the given transport and session request-response
|
||||||
* timeout.
|
* timeout.
|
||||||
|
|
@ -195,6 +198,7 @@ public class McpAsyncClient {
|
||||||
this.jsonSchemaValidator = jsonSchemaValidator;
|
this.jsonSchemaValidator = jsonSchemaValidator;
|
||||||
this.toolsOutputSchemaCache = new ConcurrentHashMap<>();
|
this.toolsOutputSchemaCache = new ConcurrentHashMap<>();
|
||||||
this.enableCallToolSchemaCaching = features.enableCallToolSchemaCaching();
|
this.enableCallToolSchemaCaching = features.enableCallToolSchemaCaching();
|
||||||
|
this.applyElicitationDefaults = features.applyElicitationDefaults();
|
||||||
|
|
||||||
// Request Handlers
|
// Request Handlers
|
||||||
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
|
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||||
|
|
@ -561,10 +565,57 @@ public class McpAsyncClient {
|
||||||
ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
|
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
|
// Tools
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2024-2024 the original author or authors.
|
* Copyright 2024-2026 the original author or authors.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
@ -195,6 +195,8 @@ public interface McpClient {
|
||||||
|
|
||||||
private boolean enableCallToolSchemaCaching = false; // Default to false
|
private boolean enableCallToolSchemaCaching = false; // Default to false
|
||||||
|
|
||||||
|
private boolean applyElicitationDefaults = false; // Default to false
|
||||||
|
|
||||||
private SyncSpec(McpClientTransport transport) {
|
private SyncSpec(McpClientTransport transport) {
|
||||||
Assert.notNull(transport, "Transport must not be null");
|
Assert.notNull(transport, "Transport must not be null");
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
@ -479,6 +481,19 @@ public interface McpClient {
|
||||||
return this;
|
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
|
* Create an instance of {@link McpSyncClient} with the provided configurations or
|
||||||
* sensible defaults.
|
* sensible defaults.
|
||||||
|
|
@ -488,7 +503,7 @@ public interface McpClient {
|
||||||
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
||||||
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
|
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);
|
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
|
||||||
|
|
||||||
|
|
@ -549,6 +564,8 @@ public interface McpClient {
|
||||||
|
|
||||||
private boolean enableCallToolSchemaCaching = false; // Default to false
|
private boolean enableCallToolSchemaCaching = false; // Default to false
|
||||||
|
|
||||||
|
private boolean applyElicitationDefaults = false; // Default to false
|
||||||
|
|
||||||
private AsyncSpec(McpClientTransport transport) {
|
private AsyncSpec(McpClientTransport transport) {
|
||||||
Assert.notNull(transport, "Transport must not be null");
|
Assert.notNull(transport, "Transport must not be null");
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
@ -820,6 +837,19 @@ public interface McpClient {
|
||||||
return this;
|
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
|
* Create an instance of {@link McpAsyncClient} with the provided configurations
|
||||||
* or sensible defaults.
|
* or sensible defaults.
|
||||||
|
|
@ -833,7 +863,8 @@ public interface McpClient {
|
||||||
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
||||||
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
||||||
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
|
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching,
|
||||||
|
this.applyElicitationDefaults));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2024-2024 the original author or authors.
|
* Copyright 2024-2026 the original author or authors.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
@ -63,6 +63,9 @@ class McpClientFeatures {
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param elicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @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,
|
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
|
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
|
||||||
|
|
@ -73,7 +76,7 @@ class McpClientFeatures {
|
||||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance and validate the arguments.
|
* Create an instance and validate the arguments.
|
||||||
|
|
@ -87,6 +90,9 @@ class McpClientFeatures {
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param elicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @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,
|
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots,
|
Map<String, McpSchema.Root> roots,
|
||||||
|
|
@ -98,7 +104,7 @@ class McpClientFeatures {
|
||||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
Assert.notNull(clientInfo, "Client info must not be null");
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
this.clientInfo = clientInfo;
|
this.clientInfo = clientInfo;
|
||||||
|
|
@ -119,6 +125,7 @@ class McpClientFeatures {
|
||||||
this.samplingHandler = samplingHandler;
|
this.samplingHandler = samplingHandler;
|
||||||
this.elicitationHandler = elicitationHandler;
|
this.elicitationHandler = elicitationHandler;
|
||||||
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
||||||
|
this.applyElicitationDefaults = applyElicitationDefaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -135,7 +142,7 @@ class McpClientFeatures {
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
||||||
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
||||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
||||||
elicitationHandler, false);
|
elicitationHandler, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -194,7 +201,7 @@ class McpClientFeatures {
|
||||||
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
||||||
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
|
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
|
||||||
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
|
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
|
||||||
syncSpec.enableCallToolSchemaCaching);
|
syncSpec.enableCallToolSchemaCaching, syncSpec.applyElicitationDefaults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,6 +220,9 @@ class McpClientFeatures {
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param elicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @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,
|
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
|
|
@ -223,7 +233,7 @@ class McpClientFeatures {
|
||||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance and validate the arguments.
|
* Create an instance and validate the arguments.
|
||||||
|
|
@ -239,6 +249,9 @@ class McpClientFeatures {
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param elicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @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,
|
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
|
|
@ -249,7 +262,7 @@ class McpClientFeatures {
|
||||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
Assert.notNull(clientInfo, "Client info must not be null");
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
this.clientInfo = clientInfo;
|
this.clientInfo = clientInfo;
|
||||||
|
|
@ -270,6 +283,7 @@ class McpClientFeatures {
|
||||||
this.samplingHandler = samplingHandler;
|
this.samplingHandler = samplingHandler;
|
||||||
this.elicitationHandler = elicitationHandler;
|
this.elicitationHandler = elicitationHandler;
|
||||||
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
||||||
|
this.applyElicitationDefaults = applyElicitationDefaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -285,7 +299,7 @@ class McpClientFeatures {
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
||||||
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
||||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
||||||
elicitationHandler, false);
|
elicitationHandler, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 - 2024 the original author or authors.
|
* Copyright 2024 - 2026 the original author or authors.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol;
|
package io.modelcontextprotocol;
|
||||||
|
|
@ -11,6 +11,7 @@ import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
@ -468,6 +469,304 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testCreateElicitationWithApplyDefaults(String clientType) {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
// Client handler returns empty content — SDK should apply defaults
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
|
||||||
|
};
|
||||||
|
|
||||||
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AtomicReference<McpSchema.ElicitResult> elicitResultRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Provide your preferences")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties",
|
||||||
|
Map.of("nickname", 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")),
|
||||||
|
"required", List.of("nickname", "age", "subscribe", "color")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
.doOnNext(elicitResultRef::set)
|
||||||
|
.thenReturn(callResponse);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation(true, false).build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.applyElicitationDefaults(true)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertWith(elicitResultRef.get(), result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content()).containsEntry("nickname", "Guest");
|
||||||
|
assertThat(result.content()).containsEntry("age", 18);
|
||||||
|
assertThat(result.content()).containsEntry("subscribe", true);
|
||||||
|
assertThat(result.content()).containsEntry("color", "green");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testCreateElicitationWithApplyDefaultsAndUnmodifiableMap(String clientType) {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
// Client handler returns an unmodifiable map (Map.of()) — SDK must copy into a
|
||||||
|
// mutable map before applying defaults.
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
|
McpSchema.ElicitResult.Action.ACCEPT, Map.of());
|
||||||
|
|
||||||
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AtomicReference<McpSchema.ElicitResult> elicitResultRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Provide your preferences")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties",
|
||||||
|
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
||||||
|
Map.of("type", "integer", "default", 18)),
|
||||||
|
"required", List.of("nickname", "age")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
.doOnNext(elicitResultRef::set)
|
||||||
|
.thenReturn(callResponse);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation(true, false).build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.applyElicitationDefaults(true)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertWith(elicitResultRef.get(), result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content()).containsEntry("nickname", "Guest");
|
||||||
|
assertThat(result.content()).containsEntry("age", 18);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testCreateElicitationApplyDefaultsDisabledLeavesContentUntouched(String clientType) {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
|
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
|
||||||
|
|
||||||
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AtomicReference<McpSchema.ElicitResult> elicitResultRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Provide your preferences")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties",
|
||||||
|
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
.doOnNext(elicitResultRef::set)
|
||||||
|
.thenReturn(callResponse);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
// applyElicitationDefaults intentionally NOT called — default false.
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation(true, false).build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertWith(elicitResultRef.get(), result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content()).doesNotContainKey("nickname");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testCreateElicitationApplyDefaultsSkippedOnDecline(String clientType) {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
|
McpSchema.ElicitResult.Action.DECLINE, new HashMap<>());
|
||||||
|
|
||||||
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AtomicReference<McpSchema.ElicitResult> elicitResultRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Provide your preferences")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties",
|
||||||
|
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
.doOnNext(elicitResultRef::set)
|
||||||
|
.thenReturn(callResponse);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation(true, false).build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.applyElicitationDefaults(true)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertWith(elicitResultRef.get(), result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.DECLINE);
|
||||||
|
assertThat(result.content()).doesNotContainKey("nickname");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testCreateElicitationApplyDefaultsPreservesMeta(String clientType) {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
Map<String, Object> meta = Map.of("trace-id", "abc-123");
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
|
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>(), meta);
|
||||||
|
|
||||||
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AtomicReference<McpSchema.ElicitResult> elicitResultRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Provide your preferences")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties",
|
||||||
|
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
.doOnNext(elicitResultRef::set)
|
||||||
|
.thenReturn(callResponse);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation(true, false).build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.applyElicitationDefaults(true)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertWith(elicitResultRef.get(), result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content()).containsEntry("nickname", "Guest");
|
||||||
|
assertThat(result.meta()).containsEntry("trace-id", "abc-123");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@MethodSource("clientsForTesting")
|
@MethodSource("clientsForTesting")
|
||||||
void testCreateElicitationWithRequestTimeoutSuccess(String clientType) {
|
void testCreateElicitationWithRequestTimeoutSuccess(String clientType) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2025 - 2025 the original author or authors.
|
* Copyright 2025 - 2026 the original author or authors.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol.spec;
|
package io.modelcontextprotocol.spec;
|
||||||
|
|
@ -1886,6 +1886,29 @@ public class McpSchemaTests {
|
||||||
assertThat(json).doesNotContain("\"url\"");
|
assertThat(json).doesNotContain("\"url\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitRequestWithDefaultValues() throws Exception {
|
||||||
|
// Test that schemas with default values serialize correctly in an ElicitRequest
|
||||||
|
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Please provide your info")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties",
|
||||||
|
Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
|
||||||
|
Map.of("type", "integer", "default", 30), "score",
|
||||||
|
Map.of("type", "number", "default", 95.5), "status",
|
||||||
|
Map.of("type", "string", "enum", List.of("active", "inactive"), "default", "active"),
|
||||||
|
"verified", Map.of("type", "boolean", "default", true)),
|
||||||
|
"required", List.of("name")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String value = JSON_MAPPER.writeValueAsString(request);
|
||||||
|
|
||||||
|
assertThatJson(value).node("requestedSchema.properties.name.default").isEqualTo("John Doe");
|
||||||
|
assertThatJson(value).node("requestedSchema.properties.age.default").isEqualTo(30);
|
||||||
|
assertThatJson(value).node("requestedSchema.properties.score.default").isEqualTo(95.5);
|
||||||
|
assertThatJson(value).node("requestedSchema.properties.status.default").isEqualTo("active");
|
||||||
|
assertThatJson(value).node("requestedSchema.properties.verified.default").isEqualTo(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Progress Notification Tests
|
// Progress Notification Tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue