Add URL elicitation support (SEP-1036) (#993)

Add URL-type elicitation schema support allowing servers to request URL
input from users during tool execution. This enables out-of-band
interactions like payment processing or API key entry.

Breaking changes:

- `ElicitRequest` changed from a `record` to an `interface`.
- The original `ElicitRequest` record was renamed to `ElicitFormRequest`.
- `McpClient` builder `elicitation()` methods now accept `ElicitFormRequest` instead of `ElicitRequest`.

New APIs:

- `ElicitUrlRequest` record for URL-mode elicitation.
- `urlElicitation()` builder methods in `McpClient`.
- `elicitationCompleteConsumer()` and `elicitationCompleteConsumers()` builder methods in `McpClient`.
- `sendElicitationComplete()` methods in `McpAsyncServer` and `McpSyncServer`.
- `McpError.URL_ELICITATION_REQUIRED` and `McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED`.
- `ElicitationCompleteNotification` record and `METHOD_NOTIFICATION_ELICITATION_COMPLETE` constant.

Co-authored-by: Daniel Garnier-Moiroux <git@garnier.wf>
Co-authored-by: Sainath Reddy Bobbala <bsnr@amazon.com>
This commit is contained in:
Daniel Garnier-Moiroux 2026-06-03 16:24:29 +02:00 • committed by GitHub
commit dbb9bdad51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1413 additions and 196 deletions

View file

@ -270,20 +270,28 @@ This capability allows:
Elicitation enables servers to request additional information or user input through the client. This is useful when a server needs clarification or confirmation during an operation: Elicitation enables servers to request additional information or user input through the client. This is useful when a server needs clarification or confirmation during an operation:
```java ```java
// Configure elicitation handler // Configure form elicitation handler
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> { Function<ElicitFormRequest, ElicitResult> formElicitationHandler = request -> {
// Present the request to the user and collect their response // Present the request to the user and collect their response
// The request contains a message and a schema describing the expected input // The request contains a message and a schema describing the expected input
Map<String, Object> userResponse = collectUserInput(request.message(), request.requestedSchema()); Map<String, Object> userResponse = collectUserInput(request.message(), request.requestedSchema());
return new ElicitResult(ElicitResult.Action.ACCEPT, userResponse); return new ElicitResult(ElicitResult.Action.ACCEPT, userResponse);
}; };
// Configure URL elicitation handler
Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler = request -> {
// Prompt the user to visit the URL
// e.g. openBrowser(request.url());
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of());
};
// Create client with elicitation support // Create client with elicitation support
var client = McpClient.sync(transport) var client = McpClient.sync(transport)
.capabilities(ClientCapabilities.builder() .capabilities(ClientCapabilities.builder()
.elicitation() .elicitation(true, true) // enables both form and URL elicitation
.build()) .build())
.elicitation(elicitationHandler) .elicitation(formElicitationHandler)
.urlElicitation(urlElicitationHandler)
.build(); .build();
``` ```
@ -293,6 +301,28 @@ The `ElicitResult` supports three actions:
- `DECLINE` - The user declined to provide the information - `DECLINE` - The user declined to provide the information
- `CANCEL` - The operation was cancelled - `CANCEL` - The operation was cancelled
#### URL Elicitation Required Handling
When a server requires out-of-band URL elicitation but the client has not negotiated support for it (or the server strictly requires out-of-band handling), the server may return a `URL_ELICITATION_REQUIRED` error during tool execution or prompt retrieval.
```java
try {
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
} catch (McpError e) {
if (e.getJsonRpcError().code() == McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED) {
// Extract elicitation requests from the error data
Map<String, Object> data = (Map<String, Object>) e.getJsonRpcError().data();
TypeRef<List<McpSchema.ElicitUrlRequest>> typeRef = new TypeRef<>() {};
var requests = McpJsonDefaults.getMapper()
.convertValue(data.get("elicitations"), typeRef);
for (var req : requests) {
// handle elicitation requests
}
}
}
```
### Logging Support ### Logging Support
The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages: The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages:

View file

@ -715,9 +715,7 @@ var tool = SyncToolSpecification.builder()
} }
// Request user confirmation // Request user confirmation
ElicitRequest elicitRequest = ElicitRequest.builder() ElicitRequest elicitRequest = ElicitFormRequest.builder("Do you want to proceed with this action?", Map.of(
.message("Do you want to proceed with this action?")
.requestedSchema(Map.of(
"type", "object", "type", "object",
"properties", Map.of("confirmed", Map.of("type", "boolean")) "properties", Map.of("confirmed", Map.of("type", "boolean"))
)) ))
@ -739,6 +737,40 @@ var tool = SyncToolSpecification.builder()
.build(); .build();
``` ```
To request out-of-band URL elicitation, such as a user authorizing an OAuth flow:
```java
var urlTool = SyncToolSpecification.builder()
.tool(Tool.builder()
.name("oauth-auth")
.description("Authenticates via OAuth")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Request URL elicitation from client
if (
exchange.getClientCapabilities().elicitation() != null
&& exchange.getClientCapabilities().elicitation().url() != null
) {
ElicitRequest urlRequest = McpSchema.ElicitUrlRequest
.builder("Please authenticate", "https://example.com/oauth", "oauth-123").build();
ElicitResult result = exchange.elicit(urlRequest);
// handle result.action == CANCELLED or DENIED
if (result.action() != ElicitResult.Action.ACCEPT) {
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Authentication failed or cancelled")))
.build();
}
}
// wait for user to visit the URL
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Authentication successful")))
.build();
})
.build();
```
### Logging Support ### Logging Support
The server provides structured logging capabilities that allow sending log messages to clients with different severity levels. The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.

View file

@ -15,9 +15,6 @@ 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;
@ -29,8 +26,10 @@ import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult; import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
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.spec.McpSchema.ListPromptsResult; import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
@ -41,6 +40,8 @@ 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.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;
@ -108,6 +109,9 @@ public class McpAsyncClient {
public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() { public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() {
}; };
public static final TypeRef<McpSchema.ElicitationCompleteNotification> ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF = new TypeRef<>() {
};
public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version"; public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";
/** /**
@ -145,7 +149,14 @@ public class McpAsyncClient {
* necessary information dynamically. Servers can request structured data from users * necessary information dynamically. Servers can request structured data from users
* with optional JSON schemas to validate responses. * with optional JSON schemas to validate responses.
*/ */
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler; private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;
/**
* MCP provides a standardized way for servers to request additional information from
* users out-of-band during interactions. This flow allows users to share information
* with the server without sharing it with the client.
*/
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;
/** /**
* Client transport implementation. * Client transport implementation.
@ -226,11 +237,21 @@ public class McpAsyncClient {
// Elicitation Handler // Elicitation Handler
if (this.clientCapabilities.elicitation() != null) { if (this.clientCapabilities.elicitation() != null) {
if (features.elicitationHandler() == null) { // elicitation: {} is equivalent to elicitation: { form: {} } for
// backwards-compatiblity
var supportsForm = this.clientCapabilities.elicitation().form() != null
|| this.clientCapabilities.elicitation().url() == null;
var supportsUrl = this.clientCapabilities.elicitation().url() != null;
if (supportsForm && features.formElicitationHandler() == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Elicitation handler must not be null when client capabilities include elicitation"); "Form elicitation handler must not be null when client capabilities include form elicitation");
} }
this.elicitationHandler = features.elicitationHandler(); if (supportsUrl && features.urlElicitationHandler() == null) {
throw new IllegalArgumentException(
"URL elicitation handler must not be null when client capabilities include URL elicitation");
}
this.formElicitationHandler = features.formElicitationHandler();
this.urlElicitationHandler = features.urlElicitationHandler();
requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler()); requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
} }
@ -301,6 +322,16 @@ public class McpAsyncClient {
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS, notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
asyncProgressNotificationHandler(progressConsumersFinal)); asyncProgressNotificationHandler(progressConsumersFinal));
// Elicitation Complete Notification
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumersFinal = new ArrayList<>();
elicitationCompleteConsumersFinal
.add((notification) -> Mono.fromRunnable(() -> logger.debug("Elicitation complete: {}", notification)));
if (!Utils.isEmpty(features.elicitationCompleteConsumers())) {
elicitationCompleteConsumersFinal.addAll(features.elicitationCompleteConsumers());
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
asyncElicitationCompleteNotificationHandler(elicitationCompleteConsumersFinal));
Function<Initialization, Mono<Void>> postInitializationHook = init -> { Function<Initialization, Mono<Void>> postInitializationHook = init -> {
if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) { if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
@ -552,23 +583,47 @@ public class McpAsyncClient {
}; };
} }
// --------------------------
// Elicitation
// --------------------------
private RequestHandler<ElicitResult> elicitationCreateHandler() { private RequestHandler<ElicitResult> elicitationCreateHandler() {
return params -> { return params -> {
ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() { McpSchema.ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
}); });
return this.elicitationHandler.apply(request).map(result -> { if (request instanceof ElicitUrlRequest urlRequest) {
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT if (this.urlElicitationHandler == null) {
&& result.content() != null) { return Mono.error(new IllegalStateException(
Map<String, Object> merged = new HashMap<>(result.content()); "Received URL elicitation request, but urlElicitation handler is null"));
applyElicitationDefaults(request.requestedSchema(), merged);
return new ElicitResult(result.action(), merged, result.meta());
} }
return result; return this.urlElicitationHandler.apply(urlRequest);
}); }
else if (request instanceof ElicitFormRequest formRequest) {
if (this.formElicitationHandler == null) {
return Mono.error(new IllegalStateException(
"Received FORM elicitation request, but formElicitationHandler handler is null"));
}
return this.formElicitationHandler.apply(formRequest).map(result -> {
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
&& result.content() != null) {
Map<String, Object> merged = new HashMap<>(result.content());
applyElicitationDefaults(formRequest.requestedSchema(), merged);
return new ElicitResult(result.action(), merged, result.meta());
}
return result;
});
}
return Mono.error(new IllegalStateException("Unknown elictation type deserialized"));
};
}
private NotificationHandler asyncElicitationCompleteNotificationHandler(
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
return params -> {
McpSchema.ElicitationCompleteNotification notification = transport.unmarshalFrom(params,
ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF);
return Flux.fromIterable(elicitationCompleteConsumers)
.flatMap(consumer -> consumer.apply(notification))
.then();
}; };
} }

View file

@ -4,22 +4,6 @@
package io.modelcontextprotocol.client; package io.modelcontextprotocol.client;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpTransport;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -29,6 +13,23 @@ import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpTransport;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono;
/** /**
* Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that * Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that
* enables AI models to interact with external tools and resources through a standardized * enables AI models to interact with external tools and resources through a standardized
@ -185,9 +186,13 @@ public interface McpClient {
private final List<Consumer<McpSchema.ProgressNotification>> progressConsumers = new ArrayList<>(); private final List<Consumer<McpSchema.ProgressNotification>> progressConsumers = new ArrayList<>();
private final List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers = new ArrayList<>();
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler; private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
private Function<ElicitRequest, ElicitResult> elicitationHandler; private Function<ElicitFormRequest, ElicitResult> formElicitationHandler;
private Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler;
private Supplier<McpTransportContext> contextProvider = () -> McpTransportContext.EMPTY; private Supplier<McpTransportContext> contextProvider = () -> McpTransportContext.EMPTY;
@ -314,9 +319,24 @@ public interface McpClient {
* @return This builder instance for method chaining * @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationHandler is null * @throws IllegalArgumentException if elicitationHandler is null
*/ */
public SyncSpec elicitation(Function<ElicitRequest, ElicitResult> elicitationHandler) { public SyncSpec elicitation(Function<ElicitFormRequest, ElicitResult> elicitationHandler) {
Assert.notNull(elicitationHandler, "Elicitation handler must not be null"); Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
this.elicitationHandler = elicitationHandler; this.formElicitationHandler = elicitationHandler;
return this;
}
/**
* Sets a custom elicitation handler for processing URL-mode elicitation message
* requests. The elicitation handler can modify or validate messages before they
* are sent to the server, enabling custom processing logic.
* @param elicitationHandler A function that processes elicitation requests and
* returns results. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationHandler is null
*/
public SyncSpec urlElicitation(Function<ElicitUrlRequest, ElicitResult> elicitationHandler) {
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
this.urlElicitationHandler = elicitationHandler;
return this; return this;
} }
@ -439,6 +459,36 @@ public interface McpClient {
return this; return this;
} }
/**
* Adds a consumer to be notified by the server when an URL elicitation is
* complete.
* @param elicitationCompleteConsumer A consumer that receives elicitation
* complete notifications. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
*/
public SyncSpec elicitationCompleteConsumer(
Consumer<McpSchema.ElicitationCompleteNotification> elicitationCompleteConsumer) {
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
return this;
}
/**
* Adds multiple consumers to be notified by the server when an URL elicitation is
* complete.
* @param elicitationCompleteConsumers A list of consumers that receives
* elicitation complete notifications. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationCompleteConsumers is null
*/
public SyncSpec elicitationCompleteConsumers(
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers) {
Assert.notNull(elicitationCompleteConsumers, "Elicitation complete consumers must not be null");
this.elicitationCompleteConsumers.addAll(elicitationCompleteConsumers);
return this;
}
/** /**
* Add a provider of {@link McpTransportContext}, providing a context before * Add a provider of {@link McpTransportContext}, providing a context before
* calling any client operation. This allows to extract thread-locals and hand * calling any client operation. This allows to extract thread-locals and hand
@ -502,8 +552,9 @@ public interface McpClient {
public McpSyncClient build() { public McpSyncClient build() {
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.elicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults); this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
this.urlElicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures); McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
@ -556,9 +607,13 @@ public interface McpClient {
private final List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers = new ArrayList<>(); private final List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers = new ArrayList<>();
private final List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler; private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler; private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;
private JsonSchemaValidator jsonSchemaValidator; private JsonSchemaValidator jsonSchemaValidator;
@ -683,9 +738,24 @@ public interface McpClient {
* @return This builder instance for method chaining * @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationHandler is null * @throws IllegalArgumentException if elicitationHandler is null
*/ */
public AsyncSpec elicitation(Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler) { public AsyncSpec elicitation(Function<ElicitFormRequest, Mono<ElicitResult>> elicitationHandler) {
Assert.notNull(elicitationHandler, "Elicitation handler must not be null"); Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
this.elicitationHandler = elicitationHandler; this.formElicitationHandler = elicitationHandler;
return this;
}
/**
* Sets a custom elicitation handler for processing elicitation message requests.
* The elicitation handler can modify or validate messages before they are sent to
* the server, enabling custom processing logic.
* @param elicitationHandler A function that processes elicitation requests and
* returns results. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationHandler is null
*/
public AsyncSpec urlElicitation(Function<ElicitUrlRequest, Mono<ElicitResult>> elicitationHandler) {
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
this.urlElicitationHandler = elicitationHandler;
return this; return this;
} }
@ -812,6 +882,36 @@ public interface McpClient {
return this; return this;
} }
/**
* Adds a consumer to be notified by the server when an URL elicitation is
* complete.
* @param elicitationCompleteConsumer A consumer that receives elicitation
* complete notifications. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
*/
public AsyncSpec elicitationCompleteConsumer(
Function<McpSchema.ElicitationCompleteNotification, Mono<Void>> elicitationCompleteConsumer) {
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
return this;
}
/**
* Adds multiple consumers to be notified by the server when an URL elicitation is
* complete.
* @param elicitationCompleteConsumers A list of consumers that receives
* elicitation complete notifications. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if elicitationCompleteConsumers is null
*/
public AsyncSpec elicitationCompleteConsumers(
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
Assert.notNull(elicitationCompleteConsumers, "Elicitation complete consumers must not be null");
this.elicitationCompleteConsumers.addAll(elicitationCompleteConsumers);
return this;
}
/** /**
* Sets the JSON schema validator to use for validating tool responses against * Sets the JSON schema validator to use for validating tool responses against
* output schemas. * output schemas.
@ -863,7 +963,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.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
this.urlElicitationHandler, this.enableCallToolSchemaCaching,
this.applyElicitationDefaults)); this.applyElicitationDefaults));
} }

View file

@ -61,7 +61,7 @@ class McpClientFeatures {
* @param loggingConsumers the logging consumers. * @param loggingConsumers the logging consumers.
* @param progressConsumers the progress consumers. * @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler. * @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler. * @param formElicitationHandler 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 * @param applyElicitationDefaults whether the client should fill in missing fields of
* an accepted {@code ElicitResult.content} with the {@code default} values declared * an accepted {@code ElicitResult.content} with the {@code default} values declared
@ -74,8 +74,10 @@ class McpClientFeatures {
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers, List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers, List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers, List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler, Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler, Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> formElicitationHandler,
Function<McpSchema.ElicitUrlRequest, Mono<McpSchema.ElicitResult>> urlElicitationHandler,
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) { boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
/** /**
@ -88,7 +90,7 @@ class McpClientFeatures {
* @param loggingConsumers the logging consumers. * @param loggingConsumers the logging consumers.
* @param progressConsumers the progress consumers. * @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler. * @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler. * @param formElicitationHandler 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 * @param applyElicitationDefaults whether the client should fill in missing
* fields of an accepted {@code ElicitResult.content} with the {@code default} * fields of an accepted {@code ElicitResult.content} with the {@code default}
@ -102,8 +104,10 @@ class McpClientFeatures {
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers, List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers, List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers, List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler, Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler, Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> formElicitationHandler,
Function<McpSchema.ElicitUrlRequest, Mono<McpSchema.ElicitResult>> urlElicitationHandler,
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) { boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
Assert.notNull(clientInfo, "Client info must not be null"); Assert.notNull(clientInfo, "Client info must not be null");
@ -112,8 +116,7 @@ class McpClientFeatures {
: new McpSchema.ClientCapabilities(null, : new McpSchema.ClientCapabilities(null,
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null, !Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null, samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build() elicitationCapabilities(formElicitationHandler, urlElicitationHandler));
: null);
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>(); this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of(); this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
@ -122,8 +125,11 @@ class McpClientFeatures {
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of(); this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of(); this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of(); this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
: List.of();
this.samplingHandler = samplingHandler; this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler; this.formElicitationHandler = formElicitationHandler;
this.urlElicitationHandler = urlElicitationHandler;
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching; this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
this.applyElicitationDefaults = applyElicitationDefaults; this.applyElicitationDefaults = applyElicitationDefaults;
} }
@ -139,10 +145,10 @@ class McpClientFeatures {
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers, List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers, List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler, Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) { Function<McpSchema.ElicitFormRequest, 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(), List.of(),
elicitationHandler, false, false); samplingHandler, elicitationHandler, null, false, false);
} }
/** /**
@ -190,19 +196,36 @@ class McpClientFeatures {
.subscribeOn(Schedulers.boundedElastic())); .subscribeOn(Schedulers.boundedElastic()));
} }
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
for (Consumer<McpSchema.ElicitationCompleteNotification> consumer : syncSpec
.elicitationCompleteConsumers()) {
elicitationCompleteConsumers.add(l -> Mono.<Void>fromRunnable(() -> consumer.accept(l))
.subscribeOn(Schedulers.boundedElastic()));
}
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
.fromCallable(() -> syncSpec.samplingHandler().apply(r)) .fromCallable(() -> syncSpec.samplingHandler().apply(r))
.subscribeOn(Schedulers.boundedElastic()); .subscribeOn(Schedulers.boundedElastic());
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = r -> Mono Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> formElicitationHandler = syncSpec
.fromCallable(() -> syncSpec.elicitationHandler().apply(r)) .formElicitationHandler() != null
.subscribeOn(Schedulers.boundedElastic()); ? r -> Mono.fromCallable(() -> syncSpec.formElicitationHandler().apply(r))
.subscribeOn(Schedulers.boundedElastic())
: null;
Function<McpSchema.ElicitUrlRequest, Mono<McpSchema.ElicitResult>> urlElicitationHandler = syncSpec
.urlElicitationHandler() != null
? r -> Mono.fromCallable(() -> syncSpec.urlElicitationHandler().apply(r))
.subscribeOn(Schedulers.boundedElastic())
: null;
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, elicitationCompleteConsumers, samplingHandler,
syncSpec.enableCallToolSchemaCaching, syncSpec.applyElicitationDefaults); formElicitationHandler, urlElicitationHandler, syncSpec.enableCallToolSchemaCaching,
syncSpec.applyElicitationDefaults);
} }
} }
/** /**
@ -218,7 +241,7 @@ class McpClientFeatures {
* @param loggingConsumers the logging consumers. * @param loggingConsumers the logging consumers.
* @param progressConsumers the progress consumers. * @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler. * @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler. * @param formElicitationHandler 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 * @param applyElicitationDefaults whether the client should fill in missing fields of
* an accepted {@code ElicitResult.content} with the {@code default} values declared * an accepted {@code ElicitResult.content} with the {@code default} values declared
@ -231,8 +254,10 @@ class McpClientFeatures {
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers, List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers, List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
List<Consumer<McpSchema.ProgressNotification>> progressConsumers, List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler, Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler, Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler,
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler,
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) { boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
/** /**
@ -247,7 +272,7 @@ class McpClientFeatures {
* @param loggingConsumers the logging consumers. * @param loggingConsumers the logging consumers.
* @param progressConsumers the progress consumers. * @param progressConsumers the progress consumers.
* @param samplingHandler the sampling handler. * @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler. * @param formElicitationHandler 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 * @param applyElicitationDefaults whether the client should fill in missing
* fields of an accepted {@code ElicitResult.content} with the {@code default} * fields of an accepted {@code ElicitResult.content} with the {@code default}
@ -260,8 +285,10 @@ class McpClientFeatures {
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers, List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers, List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
List<Consumer<McpSchema.ProgressNotification>> progressConsumers, List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler, Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler, Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler,
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler,
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) { boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
Assert.notNull(clientInfo, "Client info must not be null"); Assert.notNull(clientInfo, "Client info must not be null");
@ -270,8 +297,7 @@ class McpClientFeatures {
: new McpSchema.ClientCapabilities(null, : new McpSchema.ClientCapabilities(null,
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null, !Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null, samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build() elicitationCapabilities(formElicitationHandler, urlElicitationHandler));
: null);
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>(); this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of(); this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
@ -280,8 +306,11 @@ class McpClientFeatures {
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of(); this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of(); this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of(); this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
: List.of();
this.samplingHandler = samplingHandler; this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler; this.formElicitationHandler = formElicitationHandler;
this.urlElicitationHandler = urlElicitationHandler;
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching; this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
this.applyElicitationDefaults = applyElicitationDefaults; this.applyElicitationDefaults = applyElicitationDefaults;
} }
@ -296,11 +325,29 @@ class McpClientFeatures {
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers, List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers, List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler, Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) { Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler,
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler) {
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers, this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler, resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), List.of(),
elicitationHandler, false, false); samplingHandler, formElicitationHandler, urlElicitationHandler, false, false);
} }
} }
private static McpSchema.ClientCapabilities.Elicitation elicitationCapabilities(
Function<McpSchema.ElicitFormRequest, ?> formElicitationHandler,
Function<McpSchema.ElicitUrlRequest, ?> urlElicitationHandler) {
McpSchema.ClientCapabilities.Elicitation elicitationCapabilities = null;
if (formElicitationHandler != null || urlElicitationHandler != null) {
var elicitationCapabilitiesBuilder = McpSchema.ClientCapabilities.Elicitation.builder();
if (formElicitationHandler != null) {
elicitationCapabilitiesBuilder.form(new McpSchema.ClientCapabilities.Elicitation.Form());
}
if (urlElicitationHandler != null) {
elicitationCapabilitiesBuilder.url(new McpSchema.ClientCapabilities.Elicitation.Url());
}
elicitationCapabilities = elicitationCapabilitiesBuilder.build();
}
return elicitationCapabilities;
}
} }

View file

@ -920,6 +920,25 @@ public class McpAsyncServer {
return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null); return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
} }
/**
* Sends an elicitation complete notification to a specific client session, indicating
* that an out-of-band URL elicitation interaction has completed.
* @param sessionId The ID of the session to notify
* @param notification The notification containing the elicitation ID
* @return A Mono that completes when the notification has been sent
*/
public Mono<Void> sendElicitationComplete(String sessionId,
McpSchema.ElicitationCompleteNotification notification) {
if (sessionId == null) {
return Mono.error(new IllegalArgumentException("Session ID must not be null"));
}
if (notification == null) {
return Mono.error(new IllegalArgumentException("Notification must not be null"));
}
return this.mcpTransportProvider.notifyClient(sessionId, McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
notification);
}
private McpRequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() { private McpRequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
return (exchange, params) -> { return (exchange, params) -> {
// TODO: Implement pagination // TODO: Implement pagination

View file

@ -4,18 +4,16 @@
package io.modelcontextprotocol.server; package io.modelcontextprotocol.server;
import io.modelcontextprotocol.common.McpTransportContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpLoggableSession; import io.modelcontextprotocol.spec.McpLoggableSession;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
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.McpSession;
import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -171,13 +169,27 @@ public class McpAsyncServerExchange {
return Mono return Mono
.error(new IllegalStateException("Client must be initialized. Call the initialize method first!")); .error(new IllegalStateException("Client must be initialized. Call the initialize method first!"));
} }
if (this.clientCapabilities.elicitation() == null) { McpSchema.ClientCapabilities.Elicitation elicitation = this.clientCapabilities.elicitation();
if (elicitation == null) {
return Mono.error(new IllegalStateException("Client must be configured with elicitation capabilities")); return Mono.error(new IllegalStateException("Client must be configured with elicitation capabilities"));
} }
if (this.jsonSchemaValidator != null) {
// elicitation: {} is equivalent to elicitation: { form: {} }
boolean supportsForm = elicitation.form() != null || elicitation.url() == null;
boolean supportsUrl = elicitation.url() != null;
if (elicitRequest instanceof McpSchema.ElicitFormRequest && !supportsForm) {
return Mono
.error(new IllegalStateException("Client must be configured with form elicitation capabilities"));
}
if (elicitRequest instanceof McpSchema.ElicitUrlRequest && !supportsUrl) {
return Mono.error(new IllegalStateException("Client must be configured with URL elicitation capabilities"));
}
if (this.jsonSchemaValidator != null && elicitRequest instanceof McpSchema.ElicitFormRequest formRequest) {
try { try {
this.jsonSchemaValidator.assertConforms("ElicitRequest requestedSchema", this.jsonSchemaValidator.assertConforms("ElicitRequest requestedSchema", formRequest.requestedSchema());
elicitRequest.requestedSchema());
} }
catch (IllegalArgumentException e) { catch (IllegalArgumentException e) {
return Mono.error(e); return Mono.error(e);

View file

@ -230,6 +230,16 @@ public class McpSyncServer {
this.asyncServer.notifyPromptsListChanged().block(); this.asyncServer.notifyPromptsListChanged().block();
} }
/**
* Sends an elicitation complete notification to a specific client session, indicating
* that an out-of-band URL elicitation interaction has completed.
* @param sessionId The ID of the session to notify
* @param notification The notification containing the elicitation ID
*/
public void sendElicitationComplete(String sessionId, McpSchema.ElicitationCompleteNotification notification) {
this.asyncServer.sendElicitationComplete(sessionId, notification).block();
}
/** /**
* Close the server gracefully. * Close the server gracefully.
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2024 - 2024 the original author or authors. * Copyright 2024 - 2026 the original author or authors.
*/ */
package io.modelcontextprotocol.spec; package io.modelcontextprotocol.spec;
@ -20,6 +20,15 @@ public class McpError extends RuntimeException {
public static final Function<String, McpError> RESOURCE_NOT_FOUND = resourceUri -> new McpError(new JSONRPCError( public static final Function<String, McpError> RESOURCE_NOT_FOUND = resourceUri -> new McpError(new JSONRPCError(
McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found", Map.of("uri", resourceUri))); McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found", Map.of("uri", resourceUri)));
/**
* <a href=
* "https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-elicitation-required-error">URL
* Elicitation Required</a>
*/
public static final Function<java.util.List<McpSchema.ElicitUrlRequest>, McpError> URL_ELICITATION_REQUIRED = elicitations -> new McpError(
new JSONRPCError(McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED, "URL elicitation required",
Map.of("elicitations", elicitations)));
private JSONRPCError jsonRpcError; private JSONRPCError jsonRpcError;
public McpError(JSONRPCError jsonRpcError) { public McpError(JSONRPCError jsonRpcError) {

View file

@ -10,9 +10,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@ -20,10 +17,11 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Based on the <a href="http://www.jsonrpc.org/specification">JSON-RPC 2.0 * Based on the <a href="http://www.jsonrpc.org/specification">JSON-RPC 2.0
@ -114,6 +112,8 @@ public final class McpSchema {
// Elicitation Methods // Elicitation Methods
public static final String METHOD_ELICITATION_CREATE = "elicitation/create"; public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
public static final String METHOD_NOTIFICATION_ELICITATION_COMPLETE = "notifications/elicitation/complete";
// --------------------------- // ---------------------------
// JSON-RPC Error Codes // JSON-RPC Error Codes
// --------------------------- // ---------------------------
@ -152,6 +152,11 @@ public final class McpSchema {
*/ */
public static final int RESOURCE_NOT_FOUND = -32002; public static final int RESOURCE_NOT_FOUND = -32002;
/**
* URL elicitation is required before the request can proceed.
*/
public static final int URL_ELICITATION_REQUIRED = -32042;
} }
/** /**
@ -3883,9 +3888,48 @@ public final class McpSchema {
} }
// Elicitation // Elicitation
/**
* A request from the server to elicit additional information from the user, either
* through the client or out-of-band.
*
* @see ElicitFormRequest
* @see ElicitUrlRequest
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "mode",
defaultImpl = ElicitFormRequest.class)
@JsonSubTypes({ @JsonSubTypes.Type(value = ElicitFormRequest.class, name = ElicitFormRequest.MODE),
@JsonSubTypes.Type(value = ElicitUrlRequest.class, name = ElicitUrlRequest.MODE) })
public interface ElicitRequest extends Request {
String message();
Map<String, Object> meta();
String mode();
/**
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} instead.
*/
@Deprecated
static ElicitFormRequest.Builder builder() {
return new ElicitFormRequest.Builder();
}
/**
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} instead.
*/
@Deprecated
static ElicitFormRequest.Builder builder(String message, Map<String, Object> requestedSchema) {
return new ElicitFormRequest.Builder(message, requestedSchema);
}
}
/** /**
* A request from the server to elicit additional information from the user via the * A request from the server to elicit additional information from the user via the
* client. * client, using {@code form} mode.
* *
* @param message The message to present to the user * @param message The message to present to the user
* @param requestedSchema A restricted subset of JSON Schema. Only top-level * @param requestedSchema A restricted subset of JSON Schema. Only top-level
@ -3901,18 +3945,26 @@ public final class McpSchema {
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public record ElicitRequest( // @formatter:off public record ElicitFormRequest( // @formatter:off
@JsonProperty("message") String message, @JsonProperty("message") String message,
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema, @JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
@JsonProperty("_meta") Map<String, Object> meta) implements Request { // @formatter:on @JsonProperty("_meta") Map<String, Object> meta) implements ElicitRequest { // @formatter:on
public ElicitRequest { public static final String MODE = "form";
public ElicitFormRequest {
Assert.notNull(message, "message must not be null"); Assert.notNull(message, "message must not be null");
Assert.notNull(requestedSchema, "requestedSchema must not be null"); Assert.notNull(requestedSchema, "requestedSchema must not be null");
} }
@Override
@JsonProperty("mode")
public String mode() {
return MODE;
}
@JsonCreator @JsonCreator
static ElicitRequest fromJson(@JsonProperty("message") String message, static ElicitFormRequest fromJson(@JsonProperty("message") String message,
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema, @JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
@JsonProperty("_meta") Map<String, Object> meta) { @JsonProperty("_meta") Map<String, Object> meta) {
if (message == null || requestedSchema == null) { if (message == null || requestedSchema == null) {
@ -3925,23 +3977,10 @@ public final class McpSchema {
missing.add("requestedSchema -> {}"); missing.add("requestedSchema -> {}");
requestedSchema = Map.of(); requestedSchema = Map.of();
} }
logger.warn("ElicitRequest: missing required fields during deserialization: {}", logger.warn("ElicitFormRequest: missing required fields during deserialization: {}",
String.join(", ", missing)); String.join(", ", missing));
} }
return new ElicitRequest(message, requestedSchema, meta); return new ElicitFormRequest(message, requestedSchema, meta);
}
// backwards compatibility constructor
public ElicitRequest(String message, Map<String, Object> requestedSchema) {
this(message, requestedSchema, null);
}
/**
* @deprecated Use {@link #builder(String, Map)} instead.
*/
@Deprecated
public static Builder builder() {
return new Builder();
} }
public static Builder builder(String message, Map<String, Object> requestedSchema) { public static Builder builder(String message, Map<String, Object> requestedSchema) {
@ -3957,11 +3996,11 @@ public final class McpSchema {
private Map<String, Object> meta; private Map<String, Object> meta;
/** /**
* @deprecated Use {@link ElicitRequest#builder(String, Map)} factory method * @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
* instead. * method instead.
*/ */
@Deprecated @Deprecated
public Builder() { private Builder() {
} }
private Builder(String message, Map<String, Object> requestedSchema) { private Builder(String message, Map<String, Object> requestedSchema) {
@ -3971,12 +4010,22 @@ public final class McpSchema {
this.requestedSchema = requestedSchema; this.requestedSchema = requestedSchema;
} }
/**
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
* method instead.
*/
@Deprecated
public Builder message(String message) { public Builder message(String message) {
Assert.notNull(message, "message must not be null"); Assert.notNull(message, "message must not be null");
this.message = message; this.message = message;
return this; return this;
} }
/**
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
* method instead.
*/
@Deprecated
public Builder requestedSchema(Map<String, Object> requestedSchema) { public Builder requestedSchema(Map<String, Object> requestedSchema) {
Assert.notNull(requestedSchema, "requestedSchema must not be null"); Assert.notNull(requestedSchema, "requestedSchema must not be null");
this.requestedSchema = requestedSchema; this.requestedSchema = requestedSchema;
@ -3996,10 +4045,114 @@ public final class McpSchema {
return this; return this;
} }
public ElicitRequest build() { public ElicitFormRequest build() {
Assert.notNull(message, "message must not be null"); Assert.notNull(message, "message must not be null");
Assert.notNull(requestedSchema, "requestedSchema must not be null"); Assert.notNull(requestedSchema, "requestedSchema must not be null");
return new ElicitRequest(message, requestedSchema, meta); return new ElicitFormRequest(message, requestedSchema, meta);
}
}
}
/**
* A request from the server to elicit additional information from the user out of
* band, using {@code url} mode.
*
* @param message The message to present to the user
* @param url The URL the user must navigate to.
* @param elicitationId The elicitation ID of the elicitations reques.t
* @param meta See specification for notes on _meta usage
* <p>
* Note: {@code message}, {@code url} and {@code elicitationId} are required by the
* MCP specification. Deserialization accepts missing values and substitutes defaults
* to avoid breaking existing integrations that may omit these fields.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ElicitUrlRequest( // @formatter:off
@JsonProperty("message") String message,
@JsonProperty("url") String url,
@JsonProperty("elicitationId") String elicitationId,
@JsonProperty("_meta") Map<String, Object> meta) implements ElicitRequest { // @formatter:on
public static final String MODE = "url";
public ElicitUrlRequest {
Assert.notNull(message, "message must not be null");
Assert.notNull(url, "url must not be null");
Assert.notNull(elicitationId, "elicitationId must not be null");
}
@Override
@JsonProperty("mode")
public String mode() {
return MODE;
}
@JsonCreator
static ElicitUrlRequest fromJson(@JsonProperty("message") String message, @JsonProperty("url") String url,
@JsonProperty("elicitationId") String elicitationId, @JsonProperty("_meta") Map<String, Object> meta) {
if (message == null || url == null || elicitationId == null) {
List<String> missing = new ArrayList<>();
if (message == null) {
missing.add("message -> ''");
message = "";
}
if (url == null) {
missing.add("url -> ''");
url = "";
}
if (elicitationId == null) {
missing.add("elicitationId -> ''");
elicitationId = "";
}
logger.warn("ElicitUrlRequest: missing required fields during deserialization: {}",
String.join(", ", missing));
}
return new ElicitUrlRequest(message, url, elicitationId, meta);
}
public static Builder builder(String message, String url, String elicitationId) {
return new Builder(message, url, elicitationId);
}
public static class Builder {
private final String message;
private final String url;
private final String elicitationId;
private Map<String, Object> meta;
private Builder(String message, String url, String elicitationId) {
Assert.notNull(message, "message must not be null");
Assert.notNull(url, "url must not be null");
Assert.notNull(elicitationId, "elicitationId must not be null");
this.message = message;
this.url = url;
this.elicitationId = elicitationId;
}
public Builder meta(Map<String, Object> meta) {
this.meta = meta;
return this;
}
public Builder progressToken(Object progressToken) {
if (this.meta == null) {
this.meta = new HashMap<>();
}
this.meta.put("progressToken", progressToken);
return this;
}
public ElicitUrlRequest build() {
Assert.notNull(message, "message must not be null");
Assert.notNull(url, "url must not be null");
Assert.notNull(elicitationId, "elicitationId must not be null");
return new ElicitUrlRequest(message, url, elicitationId, meta);
} }
} }
@ -4103,6 +4256,39 @@ public final class McpSchema {
} }
} }
/**
* A notification from the server to the client indicating that an out-of-band URL
* elicitation interaction has completed.
*
* @param elicitationId The unique identifier of the completed elicitation
* @param meta See specification for notes on _meta usage
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ElicitationCompleteNotification( // @formatter:off
@JsonProperty("elicitationId") String elicitationId,
@JsonProperty("_meta") Map<String, Object> meta) implements Notification { // @formatter:on
public ElicitationCompleteNotification {
Assert.notNull(elicitationId, "elicitationId must not be null");
}
@JsonCreator
static ElicitationCompleteNotification fromJson(@JsonProperty("elicitationId") String elicitationId,
@JsonProperty("_meta") Map<String, Object> meta) {
if (elicitationId == null || elicitationId.isBlank()) {
logger.warn(
"ElicitationCompleteNotification: missing required field 'elicitationId' during deserialization, using default ''");
elicitationId = "";
}
return new ElicitationCompleteNotification(elicitationId, meta);
}
public ElicitationCompleteNotification(String elicitationId) {
this(elicitationId, null);
}
}
// --------------------------- // ---------------------------
// Pagination Interfaces // Pagination Interfaces
// --------------------------- // ---------------------------

View file

@ -0,0 +1,182 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.client;
import java.util.List;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Daniel Garnier-Moiroux
*/
class McpAsyncClientTest {
@Nested
class ClientBuilder {
@Nested
class ElicitationHandlers {
@Test
void formElicitationMissingHandler() {
McpClientTransport transport = mock(McpClientTransport.class);
var clientBuilder = McpClient.async(transport)
.capabilities(McpSchema.ClientCapabilities.builder().elicitation().build())
.jsonSchemaValidator(mock(JsonSchemaValidator.class));
var clientBuilderExplicitFormElicitation = McpClient.async(transport)
.capabilities(McpSchema.ClientCapabilities.builder().elicitation(true, false).build())
.jsonSchemaValidator(mock(JsonSchemaValidator.class));
var clientBuilderUrlElicitation = McpClient.async(transport)
.capabilities(McpSchema.ClientCapabilities.builder().elicitation(true, true).build())
.urlElicitation(req -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class));
assertThatThrownBy(clientBuilder::build).isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Form elicitation handler must not be null when client capabilities include form elicitation");
assertThatThrownBy(clientBuilderExplicitFormElicitation::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Form elicitation handler must not be null when client capabilities include form elicitation");
assertThatThrownBy(clientBuilderUrlElicitation::build).isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Form elicitation handler must not be null when client capabilities include form elicitation");
}
@Test
void formElicitationHandlerPresent() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
McpClient.AsyncSpec asyncSpec = McpClient.async(transport)
.capabilities(McpSchema.ClientCapabilities.builder().elicitation(true, false).build());
var clientBuilder = asyncSpec.elicitation(request -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class));
assertThatCode(clientBuilder::build).doesNotThrowAnyException();
}
@Test
void urlElicitationMissingHandler() {
var clientBuilder = McpClient.async(mock(McpClientTransport.class))
.capabilities(McpSchema.ClientCapabilities.builder().elicitation(false, true).build())
.jsonSchemaValidator(mock(JsonSchemaValidator.class));
assertThatThrownBy(clientBuilder::build).isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"URL elicitation handler must not be null when client capabilities include URL elicitation");
}
@Test
void urlElicitationHandlerPresent() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var clientBuilder = McpClient.async(transport)
.capabilities(McpSchema.ClientCapabilities.builder().elicitation(false, true).build())
.urlElicitation(request -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class));
assertThatCode(clientBuilder::build).doesNotThrowAnyException();
}
@Test
void bothHandlersPresent() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
McpClient.AsyncSpec asyncSpec = McpClient.async(transport)
.capabilities(McpSchema.ClientCapabilities.builder().elicitation().build());
var clientBuilder = asyncSpec.elicitation(request1 -> Mono.empty())
.urlElicitation(request -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class));
assertThatCode(clientBuilder::build).doesNotThrowAnyException();
}
}
@Nested
class ClientCapabilities {
@Test
void noElicitation() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var client = McpClient.async(transport).jsonSchemaValidator(mock(JsonSchemaValidator.class)).build();
assertThat(client.getClientCapabilities().elicitation()).isNull();
}
@Test
void formElicitationFromHandler() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
McpClient.AsyncSpec asyncSpec = McpClient.async(transport);
var client = asyncSpec.elicitation(req -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().form()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().url()).isNull();
}
@Test
void urlElicitationFromHandler() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var client = McpClient.async(transport)
.urlElicitation(req -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().form()).isNull();
assertThat(client.getClientCapabilities().elicitation().url()).isNotNull();
}
@Test
void elicitationFromHandlers() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
McpClient.AsyncSpec asyncSpec = McpClient.async(transport);
var client = asyncSpec.elicitation(req -> Mono.empty())
.urlElicitation(req -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().form()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().url()).isNotNull();
}
@Test
void noElicitationFromCapabilities() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
McpClient.AsyncSpec asyncSpec = McpClient.async(transport)
.capabilities(McpSchema.ClientCapabilities.builder().build());
var client = asyncSpec.elicitation(req -> Mono.empty())
.urlElicitation(req -> Mono.empty())
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNull();
}
}
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.client;
import java.util.List;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Daniel Garnier-Moiroux
*/
class McpSyncClientTest {
@Nested
class ClientCapabilities {
@Test
void noElicitation() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var client = McpClient.sync(transport).jsonSchemaValidator(mock(JsonSchemaValidator.class)).build();
assertThat(client.getClientCapabilities().elicitation()).isNull();
}
@Test
void formElicitationFromHandler() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var asyncSpec = McpClient.sync(transport);
var client = asyncSpec.elicitation(req -> null)
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().form()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().url()).isNull();
}
@Test
void urlElicitationFromHandler() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var client = McpClient.sync(transport)
.urlElicitation(req -> null)
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().form()).isNull();
assertThat(client.getClientCapabilities().elicitation().url()).isNotNull();
}
@Test
void elicitationFromHandlers() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var asyncSpec = McpClient.sync(transport);
var client = asyncSpec.elicitation(req -> null)
.urlElicitation(req -> null)
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().form()).isNotNull();
assertThat(client.getClientCapabilities().elicitation().url()).isNotNull();
}
@Test
void noElicitationFromCapabilities() {
McpClientTransport transport = mock(McpClientTransport.class);
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
var asyncSpec = McpClient.sync(transport).capabilities(McpSchema.ClientCapabilities.builder().build());
var client = asyncSpec.elicitation(req -> null)
.urlElicitation(req -> null)
.jsonSchemaValidator(mock(JsonSchemaValidator.class))
.build();
assertThat(client.getClientCapabilities().elicitation()).isNull();
}
}
}

View file

@ -4,17 +4,17 @@
package io.modelcontextprotocol.server; package io.modelcontextprotocol.server;
import io.modelcontextprotocol.common.McpTransportContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.json.TypeRef;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mock; import org.mockito.Mock;
@ -531,6 +531,149 @@ class McpAsyncServerExchangeTests {
any(TypeRef.class)); any(TypeRef.class));
} }
@Test
void testCreateElicitationWithUrlRequest() {
McpSchema.ClientCapabilities capabilitiesWithUrlElicitation = McpSchema.ClientCapabilities.builder()
.elicitation(false, true)
.build();
McpAsyncServerExchange exchangeWithElicitation = new McpAsyncServerExchange("testSessionId", mockSession,
capabilitiesWithUrlElicitation, clientInfo, McpTransportContext.EMPTY);
McpSchema.ElicitUrlRequest elicitUrlRequest = McpSchema.ElicitUrlRequest
.builder("Please authenticate via URL", "https://example.com/auth", "elicit-url-123")
.build();
McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT)
.build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitUrlRequest), any(TypeRef.class)))
.thenReturn(Mono.just(expectedResult));
StepVerifier.create(exchangeWithElicitation.createElicitation(elicitUrlRequest)).assertNext(result -> {
assertThat(result).isEqualTo(expectedResult);
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
}).verifyComplete();
}
@Test
void testCreateElicitationWithUrlRequestBypassesValidator() {
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
.elicitation(false, true)
.build();
JsonSchemaValidator rejectingValidator = new JsonSchemaValidator() {
@Override
public ValidationResponse validate(Map<String, Object> schema, Object content) {
return ValidationResponse.asInvalid("should not be called");
}
@Override
public ValidationResponse validateSchema(Map<String, Object> schema) {
return ValidationResponse.asInvalid("should not be called");
}
};
McpAsyncServerExchange exchangeWithValidator = new McpAsyncServerExchange("testSessionId", mockSession,
capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY, rejectingValidator);
McpSchema.ElicitUrlRequest elicitUrlRequest = McpSchema.ElicitUrlRequest
.builder("Please visit the URL", "https://example.com/oauth", "elicit-oauth-123")
.build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitUrlRequest), any(TypeRef.class)))
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
StepVerifier.create(exchangeWithValidator.createElicitation(elicitUrlRequest)).assertNext(result -> {
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
}).verifyComplete();
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitUrlRequest),
any(TypeRef.class));
}
@Test
void testElicitationCapabilitiesEmptyObject() {
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
McpAsyncServerExchange exchangeEmpty = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
clientInfo, McpTransportContext.EMPTY);
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
.build();
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(formRequest), any(TypeRef.class)))
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
StepVerifier.create(exchangeEmpty.createElicitation(formRequest)).expectNextCount(1).verifyComplete();
StepVerifier.create(exchangeEmpty.createElicitation(urlRequest))
.verifyErrorSatisfies(e -> assertThat(e).isInstanceOf(IllegalStateException.class)
.hasMessage("Client must be configured with URL elicitation capabilities"));
}
@Test
void testElicitationCapabilitiesFormOnly() {
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
.elicitation(true, false)
.build();
McpAsyncServerExchange exchangeForm = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
clientInfo, McpTransportContext.EMPTY);
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
.build();
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(formRequest), any(TypeRef.class)))
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
StepVerifier.create(exchangeForm.createElicitation(formRequest)).expectNextCount(1).verifyComplete();
StepVerifier.create(exchangeForm.createElicitation(urlRequest))
.verifyErrorSatisfies(e -> assertThat(e).isInstanceOf(IllegalStateException.class)
.hasMessage("Client must be configured with URL elicitation capabilities"));
}
@Test
void testElicitationCapabilitiesUrlOnly() {
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
.elicitation(false, true)
.build();
McpAsyncServerExchange exchangeUrl = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
clientInfo, McpTransportContext.EMPTY);
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
.build();
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(urlRequest), any(TypeRef.class)))
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
StepVerifier.create(exchangeUrl.createElicitation(urlRequest)).expectNextCount(1).verifyComplete();
StepVerifier.create(exchangeUrl.createElicitation(formRequest))
.verifyErrorSatisfies(e -> assertThat(e).isInstanceOf(IllegalStateException.class)
.hasMessage("Client must be configured with form elicitation capabilities"));
}
@Test
void testElicitationCapabilitiesBoth() {
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
.elicitation(true, true)
.build();
McpAsyncServerExchange exchangeBoth = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
clientInfo, McpTransportContext.EMPTY);
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
.build();
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(formRequest), any(TypeRef.class)))
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(urlRequest), any(TypeRef.class)))
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
StepVerifier.create(exchangeBoth.createElicitation(formRequest)).expectNextCount(1).verifyComplete();
StepVerifier.create(exchangeBoth.createElicitation(urlRequest)).expectNextCount(1).verifyComplete();
}
// --------------------------------------- // ---------------------------------------
// Create Message Tests // Create Message Tests
// --------------------------------------- // ---------------------------------------

View file

@ -4,8 +4,6 @@
package io.modelcontextprotocol; package io.modelcontextprotocol;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@ -21,6 +19,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -39,7 +38,7 @@ import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
import io.modelcontextprotocol.spec.McpSchema.CompleteResult; import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult; import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.ModelPreferences; import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
@ -51,7 +50,6 @@ import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.util.McpJsonMapperUtils;
import io.modelcontextprotocol.util.Utils; import io.modelcontextprotocol.util.Utils;
import net.javacrumbs.jsonunit.core.Option; import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@ -59,11 +57,16 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertWith; import static org.assertj.core.api.Assertions.assertWith;
import static org.assertj.core.api.InstanceOfAssertFactories.LIST;
import static org.assertj.core.api.InstanceOfAssertFactories.MAP;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -381,7 +384,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
.callHandler((exchange, request) -> exchange.createElicitation(mock(ElicitRequest.class)) .callHandler((exchange, request) -> exchange.createElicitation(mock(McpSchema.ElicitFormRequest.class))
.then(Mono.just(mock(CallToolResult.class)))) .then(Mono.just(mock(CallToolResult.class))))
.build(); .build();
@ -412,7 +415,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> { Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler = request -> {
assertThat(request.message()).isNotEmpty(); assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull(); assertThat(request.requestedSchema()).isNotNull();
@ -431,7 +434,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = McpSchema.ElicitRequest var elicitationRequest = McpSchema.ElicitFormRequest
.builder("Test message", .builder("Test message",
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build(); .build();
@ -447,7 +450,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
try (var mcpClient = clientBuilder try (var mcpClient = clientBuilder
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
.capabilities(ClientCapabilities.builder().elicitation().build()) .capabilities(ClientCapabilities.builder().elicitation().build())
.elicitation(elicitationHandler) .elicitation(formElicitationHandler)
.build()) { .build()) {
InitializeResult initResult = mcpClient.initialize(); InitializeResult initResult = mcpClient.initialize();
@ -476,7 +479,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
// Client handler returns empty content — SDK should apply defaults // Client handler returns empty content — SDK should apply defaults
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> { Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty(); assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull(); assertThat(request.requestedSchema()).isNotNull();
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>()); return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
@ -491,15 +494,13 @@ public abstract class AbstractMcpClientServerIntegrationTests {
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences",
var elicitationRequest = McpSchema.ElicitRequest.builder() Map.of("type", "object", "properties",
.message("Provide your preferences") Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
.requestedSchema(Map.of("type", "object", "properties", Map.of("type", "integer", "default", 18), "subscribe",
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age", Map.of("type", "boolean", "default", true), "color",
Map.of("type", "integer", "default", 18), "subscribe", Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")),
Map.of("type", "boolean", "default", true), "color", "required", List.of("nickname", "age", "subscribe", "color")))
Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")),
"required", List.of("nickname", "age", "subscribe", "color")))
.build(); .build();
return exchange.createElicitation(elicitationRequest) return exchange.createElicitation(elicitationRequest)
@ -544,7 +545,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
// Client handler returns an unmodifiable map (Map.of()) — SDK must copy into a // Client handler returns an unmodifiable map (Map.of()) — SDK must copy into a
// mutable map before applying defaults. // mutable map before applying defaults.
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult( Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
McpSchema.ElicitResult.Action.ACCEPT, Map.of()); McpSchema.ElicitResult.Action.ACCEPT, Map.of());
CallToolResult callResponse = McpSchema.CallToolResult.builder() CallToolResult callResponse = McpSchema.CallToolResult.builder()
@ -557,9 +558,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = McpSchema.ElicitRequest.builder() var elicitationRequest = McpSchema.ElicitFormRequest
.message("Provide your preferences") .builder("Provide your preferences", Map.of("type", "object", "properties",
.requestedSchema(Map.of("type", "object", "properties",
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age", Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
Map.of("type", "integer", "default", 18)), Map.of("type", "integer", "default", 18)),
"required", List.of("nickname", "age"))) "required", List.of("nickname", "age")))
@ -603,7 +603,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult( Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>()); McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
CallToolResult callResponse = McpSchema.CallToolResult.builder() CallToolResult callResponse = McpSchema.CallToolResult.builder()
@ -616,10 +616,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = McpSchema.ElicitRequest.builder() var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
.message("Provide your preferences") "object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
.requestedSchema(Map.of("type", "object", "properties",
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
.build(); .build();
return exchange.createElicitation(elicitationRequest) return exchange.createElicitation(elicitationRequest)
@ -659,7 +657,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult( Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
McpSchema.ElicitResult.Action.DECLINE, new HashMap<>()); McpSchema.ElicitResult.Action.DECLINE, new HashMap<>());
CallToolResult callResponse = McpSchema.CallToolResult.builder() CallToolResult callResponse = McpSchema.CallToolResult.builder()
@ -672,10 +670,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = McpSchema.ElicitRequest.builder() var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
.message("Provide your preferences") "object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
.requestedSchema(Map.of("type", "object", "properties",
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
.build(); .build();
return exchange.createElicitation(elicitationRequest) return exchange.createElicitation(elicitationRequest)
@ -716,7 +712,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
Map<String, Object> meta = Map.of("trace-id", "abc-123"); Map<String, Object> meta = Map.of("trace-id", "abc-123");
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult( Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>(), meta); McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>(), meta);
CallToolResult callResponse = McpSchema.CallToolResult.builder() CallToolResult callResponse = McpSchema.CallToolResult.builder()
@ -729,10 +725,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = McpSchema.ElicitRequest.builder() var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
.message("Provide your preferences") "object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
.requestedSchema(Map.of("type", "object", "properties",
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
.build(); .build();
return exchange.createElicitation(elicitationRequest) return exchange.createElicitation(elicitationRequest)
@ -773,9 +767,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> { Function<ElicitFormRequest, ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty(); assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull(); assertThat(((McpSchema.ElicitFormRequest) request).requestedSchema()).isNotNull();
return ElicitResult.builder(ElicitResult.Action.ACCEPT) return ElicitResult.builder(ElicitResult.Action.ACCEPT)
.content(Map.of("message", request.message())) .content(Map.of("message", request.message()))
.build(); .build();
@ -791,7 +785,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = McpSchema.ElicitRequest var elicitationRequest = McpSchema.ElicitFormRequest
.builder("Test message", .builder("Test message",
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build(); .build();
@ -840,7 +834,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> { Function<ElicitFormRequest, ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty(); assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull(); assertThat(request.requestedSchema()).isNotNull();
@ -867,7 +861,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
var elicitationRequest = ElicitRequest var elicitationRequest = ElicitFormRequest
.builder("Test message", .builder("Test message",
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build(); .build();
@ -904,6 +898,165 @@ public abstract class AbstractMcpClientServerIntegrationTests {
} }
} }
@ParameterizedTest(name = "{0} : {displayName} ")
@MethodSource("clientsForTesting")
void testCreateUrlElicitationSuccess(String clientType) {
var elicitationRequest = McpSchema.ElicitUrlRequest
.builder("Test message", "https://example.com/auth", "elicitation-123")
.build();
var clientBuilder = clientBuilders.get(clientType);
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler = request -> {
assertThat(request.message()).isEqualTo("Test message");
assertThat(request.url()).isEqualTo("https://example.com/auth");
assertThat(request.elicitationId()).isEqualTo("elicitation-123");
return McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build();
};
CallToolResult callResponse = McpSchema.CallToolResult.builder()
.addContent(McpSchema.TextContent.builder("CALL RESPONSE").build())
.build();
AtomicReference<McpSchema.ElicitResult> elicitResultRef = new AtomicReference<>();
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
.callHandler((exchange, request) -> 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(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
.capabilities(ClientCapabilities.builder().elicitation(false, true).build())
.urlElicitation(urlElicitationHandler)
.build()) {
CallToolResult response = mcpClient
.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build());
assertThat(response).isNotNull();
assertThat(response).isEqualTo(callResponse);
var elicitResult = elicitResultRef.get();
assertThat(elicitResult).isNotNull();
assertThat(elicitResult.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
}
finally {
mcpServer.closeGracefully().block();
}
}
@ParameterizedTest(name = "{0} : {displayName} ")
@MethodSource("clientsForTesting")
void testElicitationCompleteNotification(String clientType) throws InterruptedException {
var clientBuilder = clientBuilders.get(clientType);
CountDownLatch notificationLatch = new CountDownLatch(1);
AtomicReference<McpSchema.ElicitationCompleteNotification> notificationRef = new AtomicReference<>();
AtomicReference<String> sessionId = new AtomicReference<>();
Consumer<McpSchema.ElicitationCompleteNotification> elicitationCompleteConsumer = notification -> {
notificationRef.set(notification);
notificationLatch.countDown();
};
CallToolResult callResponse = McpSchema.CallToolResult.builder()
.addContent(McpSchema.TextContent.builder("CALL RESPONSE").build())
.build();
// Capture the session ID so we can trigger an "elicitation complete" notification
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
.callHandler((exchange, request) -> {
sessionId.set(exchange.sessionId());
return Mono.just(callResponse);
})
.build();
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
try (var mcpClient = clientBuilder
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
.elicitationCompleteConsumer(elicitationCompleteConsumer)
// enable elicitation so that we can register an elicitation complete consumer
.urlElicitation(request -> McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build())
.build()) {
var response = mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build());
var capturedSessionId = sessionId.get();
assertThat(response).isNotNull();
assertThat(capturedSessionId).isNotNull();
mcpServer
.sendElicitationComplete(capturedSessionId,
new McpSchema.ElicitationCompleteNotification("elicitation-123"))
.block();
assertThat(notificationLatch.await(5, TimeUnit.SECONDS)).isTrue();
var notification = notificationRef.get();
assertThat(notification).isNotNull();
assertThat(notification.elicitationId()).isEqualTo("elicitation-123");
}
finally {
mcpServer.closeGracefully().block();
}
}
@ParameterizedTest(name = "{0} : {displayName} ")
@MethodSource("clientsForTesting")
void testElicitationRequiredError(String clientType) throws InterruptedException {
var clientBuilder = clientBuilders.get(clientType);
// Capture the session ID so we can trigger an "elicitation complete" notification
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
.callHandler((exchange, request) -> {
return Mono.error(McpError.URL_ELICITATION_REQUIRED.apply(List
.of(McpSchema.ElicitUrlRequest.builder("do the thing", "https://example.com", "elicitation-1234")
.build())));
})
.build();
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
Function<McpSchema.ElicitUrlRequest, ElicitResult> elicitationHandler = request -> ElicitResult
.builder(ElicitResult.Action.ACCEPT)
.build();
try (var mcpClient = clientBuilder
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
.urlElicitation(elicitationHandler)
.build()) {
assertThatThrownBy(
() -> mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build()))
.isInstanceOf(McpError.class)
.extracting("jsonRpcError")
.asInstanceOf(type(McpSchema.JSONRPCResponse.JSONRPCError.class))
.satisfies(error -> {
assertThat(error.code()).isEqualTo(McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED);
assertThat(error.data()).asInstanceOf(MAP)
.hasSize(1)
.extracting("elicitations")
.asInstanceOf(LIST)
.hasSize(1)
.first()
.asInstanceOf(MAP)
.containsEntry("mode", "url")
.containsEntry("message", "do the thing")
.containsEntry("url", "https://example.com")
.containsEntry("elicitationId", "elicitation-1234");
});
}
finally {
mcpServer.closeGracefully().block();
}
}
// --------------------------------------- // ---------------------------------------
// Roots Tests // Roots Tests
// --------------------------------------- // ---------------------------------------

View file

@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
@ -685,11 +686,13 @@ public abstract class AbstractMcpAsyncClientTests {
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
.just(CreateMessageResult.builder(McpSchema.Role.ASSISTANT, "test", "test-model").build()); .just(CreateMessageResult.builder(McpSchema.Role.ASSISTANT, "test", "test-model").build());
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler = request -> Mono Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler = request -> Mono
.just(ElicitResult.builder(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build()); .just(ElicitResult.builder(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build());
withClient(createMcpTransport(), withClient(createMcpTransport(),
builder -> builder.capabilities(capabilities).sampling(samplingHandler).elicitation(elicitationHandler), builder -> builder.capabilities(capabilities)
.sampling(samplingHandler)
.elicitation(formElicitationHandler),
client -> client ->
StepVerifier.create(client.initialize()).assertNext(result -> { StepVerifier.create(client.initialize()).assertNext(result -> {

View file

@ -402,7 +402,7 @@ class McpAsyncClientResponseHandlerTests {
MockMcpClientTransport transport = initializationEnabledTransport(); MockMcpClientTransport transport = initializationEnabledTransport();
// Create a test elicitation handler that echoes back the input // Create a test elicitation handler that echoes back the input
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> { Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty(); assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isInstanceOf(Map.class); assertThat(request.requestedSchema()).isInstanceOf(Map.class);
assertThat(request.requestedSchema().get("type")).isEqualTo("object"); assertThat(request.requestedSchema().get("type")).isEqualTo("object");
@ -458,7 +458,7 @@ class McpAsyncClientResponseHandlerTests {
MockMcpClientTransport transport = initializationEnabledTransport(); MockMcpClientTransport transport = initializationEnabledTransport();
// Create a test elicitation handler to decline the request // Create a test elicitation handler to decline the request
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> Mono Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> Mono
.just(McpSchema.ElicitResult.builder(action).build()); .just(McpSchema.ElicitResult.builder(action).build());
// Create client with elicitation capability and handler // Create client with elicitation capability and handler
@ -534,17 +534,6 @@ class McpAsyncClientResponseHandlerTests {
asyncMcpClient.closeGracefully(); asyncMcpClient.closeGracefully();
} }
@Test
void testElicitationCreateRequestHandlingWithNullHandler() {
MockMcpClientTransport transport = new MockMcpClientTransport();
// Create client with elicitation capability but null handler
assertThatThrownBy(() -> McpClient.async(transport)
.capabilities(ClientCapabilities.builder().elicitation().build())
.build()).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Elicitation handler must not be null when client capabilities include elicitation");
}
@Test @Test
void testPingMessageRequestHandling() { void testPingMessageRequestHandling() {
MockMcpClientTransport transport = initializationEnabledTransport(); MockMcpClientTransport transport = initializationEnabledTransport();

View file

@ -0,0 +1,30 @@
/*
* Copyright 2026 - 2026 the original author or authors.
*/
package io.modelcontextprotocol.spec;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class McpErrorTests {
@Test
void testUrlElicitationRequired() {
McpSchema.ElicitUrlRequest elicitation = McpSchema.ElicitUrlRequest
.builder("Please auth", "https://example.com", "123")
.build();
McpError error = McpError.URL_ELICITATION_REQUIRED.apply(List.of(elicitation));
assertThat(error.getJsonRpcError().code()).isEqualTo(McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED);
assertThat(error.getJsonRpcError().message()).isEqualTo("URL elicitation required");
assertThat(error.getJsonRpcError().data()).isInstanceOf(Map.class);
Map<String, Object> data = (Map<String, Object>) error.getJsonRpcError().data();
assertThat(data).containsEntry("elicitations", List.of(elicitation));
}
}

View file

@ -1,15 +1,9 @@
/* /*
* Copyright 2025 - 2026 the original author or authors. * Copyright 2025 - 2026 the original author or authors.
*/ */
package io.modelcontextprotocol.spec; package io.modelcontextprotocol.spec;
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -17,12 +11,17 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import net.javacrumbs.jsonunit.core.Option;
import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import io.modelcontextprotocol.json.TypeRef; import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
import net.javacrumbs.jsonunit.core.Option; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** /**
* @author Christian Tzolov * @author Christian Tzolov
@ -1557,7 +1556,7 @@ public class McpSchemaTests {
@Test @Test
void testCreateElicitationRequest() throws Exception { void testCreateElicitationRequest() throws Exception {
McpSchema.ElicitRequest request = McpSchema.ElicitRequest McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest
.builder("Please provide additional information", Map.of("type", "object", "required", List.of("a"), .builder("Please provide additional information", Map.of("type", "object", "required", List.of("a"),
"properties", Map.of("foo", Map.of("type", "string")))) "properties", Map.of("foo", Map.of("type", "string"))))
.build(); .build();
@ -1567,9 +1566,43 @@ public class McpSchemaTests {
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject() .isObject()
.isEqualTo( .isEqualTo(json("""
json(""" {
{"message":"Please provide additional information","requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}""")); "mode": "form",
"message": "Please provide additional information",
"requestedSchema": {
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"a"
],
"type": "object"
}
}"""));
}
@Test
void testCreateElicitationUrlRequest() throws Exception {
McpSchema.ElicitRequest request = McpSchema.ElicitUrlRequest
.builder("Please visit the URL", "https://example.com/oauth", "elicit-oauth-123")
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{
"mode": "url",
"message": "Please visit the URL",
"url": "https://example.com/oauth",
"elicitationId": "elicit-oauth-123"
}
"""));
} }
@Test @Test
@ -1588,12 +1621,57 @@ public class McpSchemaTests {
} }
@Test @Test
void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception { void testElicitRequestDeserializationDefaultsToForm() throws Exception {
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{}", McpSchema.ElicitRequest.class); var request = JSON_MAPPER.readValue("{\"message\":\"do the thing\"}", McpSchema.ElicitRequest.class);
assertThat(request).isNotNull(); assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitFormRequest.class);
assertThat(request.message()).isEqualTo("do the thing");
assertThat(request.mode()).isEqualTo("form");
var formRequest = (McpSchema.ElicitFormRequest) request;
assertThat(formRequest.requestedSchema()).isEmpty();
}
@Test
void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception {
var request = JSON_MAPPER.readValue("{\"mode\":\"form\"}", McpSchema.ElicitRequest.class);
assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitFormRequest.class);
assertThat(request.message()).isEmpty(); assertThat(request.message()).isEmpty();
assertThat(request.requestedSchema()).isEmpty(); assertThat(request.mode()).isEqualTo("form");
var formRequest = (McpSchema.ElicitFormRequest) request;
assertThat(formRequest.requestedSchema()).isEmpty();
}
@Test
void testElicitUrlRequestDeserializationWithMissingRequiredFields() throws Exception {
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{\"mode\":\"url\"}", McpSchema.ElicitRequest.class);
assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitUrlRequest.class);
assertThat(request.message()).isEmpty();
assertThat(request.mode()).isEqualTo("url");
var urlRequest = (McpSchema.ElicitUrlRequest) request;
assertThat(urlRequest.url()).isEmpty();
assertThat(urlRequest.elicitationId()).isEmpty();
}
@Test
void testElicitUrlDeserialization() throws Exception {
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("""
{
"mode": "url",
"message": "Please visit the URL",
"url": "https://example.com/oauth",
"elicitationId": "elicit-oauth-123"
}
""", McpSchema.ElicitRequest.class);
assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitUrlRequest.class);
assertThat(request.message()).isEqualTo("Please visit the URL");
assertThat(request.mode()).isEqualTo("url");
var urlRequest = (McpSchema.ElicitUrlRequest) request;
assertThat(urlRequest.url()).isEqualTo("https://example.com/oauth");
assertThat(urlRequest.elicitationId()).isEqualTo("elicit-oauth-123");
} }
@Test @Test
@ -1604,7 +1682,8 @@ public class McpSchemaTests {
Map<String, Object> meta = new HashMap<>(); Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "elicit-token-789"); meta.put("progressToken", "elicit-token-789");
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder("Please provide your name", requestedSchema) McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest
.builder("Please provide your name", requestedSchema)
.meta(meta) .meta(meta)
.build(); .build();
@ -1612,7 +1691,8 @@ public class McpSchemaTests {
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject() .isObject()
.containsEntry("_meta", Map.of("progressToken", "elicit-token-789")); .containsEntry("_meta", Map.of("progressToken", "elicit-token-789"))
.containsEntry("mode", "form");
// Test Request interface methods // Test Request interface methods
assertThat(request.meta()).isEqualTo(meta); assertThat(request.meta()).isEqualTo(meta);
@ -1627,16 +1707,25 @@ public class McpSchemaTests {
requestedSchema.put("properties", Map.of("name", Map.of("type", "string"))); requestedSchema.put("properties", Map.of("name", Map.of("type", "string")));
requestedSchema.put("required", List.of("name")); requestedSchema.put("required", List.of("name"));
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder("Please provide name", requestedSchema) McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest.builder("Please provide name", requestedSchema)
.build(); .build();
String json = JSON_MAPPER.writeValueAsString(request); String json = JSON_MAPPER.writeValueAsString(request);
assertThatJson(json).inPath("$.requestedSchema.$schema").isEqualTo(McpSchema.JSON_SCHEMA_DIALECT_2020_12); assertThatJson(json).inPath("$.requestedSchema.$schema").isEqualTo(McpSchema.JSON_SCHEMA_DIALECT_2020_12);
McpSchema.ElicitRequest parsed = JSON_MAPPER.readValue(json, McpSchema.ElicitRequest.class); McpSchema.ElicitFormRequest parsed = (McpSchema.ElicitFormRequest) JSON_MAPPER.readValue(json,
McpSchema.ElicitRequest.class);
assertThat(parsed.requestedSchema()).containsEntry("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12); assertThat(parsed.requestedSchema()).containsEntry("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
} }
@Test
void testElicitRequestToleratesUnknownFields() throws Exception {
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("""
{"message":"hello","requestedSchema":{"type":"object"},"futureField":42}""",
McpSchema.ElicitRequest.class);
assertThat(request.message()).isEqualTo("hello");
}
// Pagination Tests // Pagination Tests
@Test @Test
@ -1889,15 +1978,13 @@ public class McpSchemaTests {
@Test @Test
void testElicitRequestWithDefaultValues() throws Exception { void testElicitRequestWithDefaultValues() throws Exception {
// Test that schemas with default values serialize correctly in an ElicitRequest // Test that schemas with default values serialize correctly in an ElicitRequest
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder() McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest.builder("Please provide your info", Map.of("type",
.message("Please provide your info") "object", "properties",
.requestedSchema(Map.of("type", "object", "properties", Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
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),
Map.of("type", "integer", "default", 30), "score", "status", Map.of("type", "string", "enum", List.of("active", "inactive"), "default", "active"),
Map.of("type", "number", "default", 95.5), "status", "verified", Map.of("type", "boolean", "default", true)),
Map.of("type", "string", "enum", List.of("active", "inactive"), "default", "active"), "required", List.of("name")))
"verified", Map.of("type", "boolean", "default", true)),
"required", List.of("name")))
.build(); .build();
String value = JSON_MAPPER.writeValueAsString(request); String value = JSON_MAPPER.writeValueAsString(request);
@ -1909,6 +1996,41 @@ public class McpSchemaTests {
assertThatJson(value).node("requestedSchema.properties.verified.default").isEqualTo(true); assertThatJson(value).node("requestedSchema.properties.verified.default").isEqualTo(true);
} }
// Elicitation Complete Notification Tests (SEP-1036)
@Test
void testElicitationCompleteNotification() throws Exception {
McpSchema.ElicitationCompleteNotification notification = new McpSchema.ElicitationCompleteNotification(
"elicit-789");
String json = JSON_MAPPER.writeValueAsString(notification);
assertThatJson(json).isObject().containsEntry("elicitationId", "elicit-789");
McpSchema.ElicitationCompleteNotification deserialized = JSON_MAPPER.readValue(json,
McpSchema.ElicitationCompleteNotification.class);
assertThat(deserialized.elicitationId()).isEqualTo("elicit-789");
}
@Test
void testElicitationCompleteNotificationNullElicitationIdThrows() {
assertThatThrownBy(() -> new McpSchema.ElicitationCompleteNotification(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void testElicitationCompleteNotificationDeserializesWithoutElicitationId() throws Exception {
McpSchema.ElicitationCompleteNotification notification = JSON_MAPPER.readValue("""
{}""", McpSchema.ElicitationCompleteNotification.class);
assertThat(notification.elicitationId()).isEqualTo("");
}
@Test
void testElicitationCompleteNotificationToleratesUnknownFields() throws Exception {
McpSchema.ElicitationCompleteNotification notification = JSON_MAPPER.readValue("""
{"elicitationId":"abc","futureField":"ignored"}""", McpSchema.ElicitationCompleteNotification.class);
assertThat(notification.elicitationId()).isEqualTo("abc");
}
// Progress Notification Tests // Progress Notification Tests
@Test @Test