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:
parent
c49a994463
commit
dbb9bdad51
18 changed files with 1413 additions and 196 deletions
|
|
@ -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:
|
||||
|
||||
```java
|
||||
// Configure elicitation handler
|
||||
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||
// Configure form elicitation handler
|
||||
Function<ElicitFormRequest, ElicitResult> formElicitationHandler = request -> {
|
||||
// Present the request to the user and collect their response
|
||||
// The request contains a message and a schema describing the expected input
|
||||
Map<String, Object> userResponse = collectUserInput(request.message(), request.requestedSchema());
|
||||
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
|
||||
var client = McpClient.sync(transport)
|
||||
.capabilities(ClientCapabilities.builder()
|
||||
.elicitation()
|
||||
.elicitation(true, true) // enables both form and URL elicitation
|
||||
.build())
|
||||
.elicitation(elicitationHandler)
|
||||
.elicitation(formElicitationHandler)
|
||||
.urlElicitation(urlElicitationHandler)
|
||||
.build();
|
||||
```
|
||||
|
||||
|
|
@ -293,6 +301,28 @@ The `ElicitResult` supports three actions:
|
|||
- `DECLINE` - The user declined to provide the information
|
||||
- `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
|
||||
|
||||
The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages:
|
||||
|
|
|
|||
|
|
@ -715,9 +715,7 @@ var tool = SyncToolSpecification.builder()
|
|||
}
|
||||
|
||||
// Request user confirmation
|
||||
ElicitRequest elicitRequest = ElicitRequest.builder()
|
||||
.message("Do you want to proceed with this action?")
|
||||
.requestedSchema(Map.of(
|
||||
ElicitRequest elicitRequest = ElicitFormRequest.builder("Do you want to proceed with this action?", Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of("confirmed", Map.of("type", "boolean"))
|
||||
))
|
||||
|
|
@ -739,6 +737,40 @@ var tool = SyncToolSpecification.builder()
|
|||
.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
|
||||
|
||||
The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ import java.util.Map;
|
|||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||
|
|
@ -29,8 +26,10 @@ 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.ElicitRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||
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.ToolNameValidator;
|
||||
import io.modelcontextprotocol.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
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.ElicitationCompleteNotification> ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF = new TypeRef<>() {
|
||||
};
|
||||
|
||||
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
|
||||
* 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.
|
||||
|
|
@ -226,11 +237,21 @@ public class McpAsyncClient {
|
|||
|
||||
// Elicitation Handler
|
||||
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(
|
||||
"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());
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +322,16 @@ public class McpAsyncClient {
|
|||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
|
||||
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 -> {
|
||||
|
||||
if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
|
||||
|
|
@ -552,23 +583,47 @@ public class McpAsyncClient {
|
|||
};
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// Elicitation
|
||||
// --------------------------
|
||||
private RequestHandler<ElicitResult> elicitationCreateHandler() {
|
||||
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 (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
|
||||
&& result.content() != null) {
|
||||
Map<String, Object> merged = new HashMap<>(result.content());
|
||||
applyElicitationDefaults(request.requestedSchema(), merged);
|
||||
return new ElicitResult(result.action(), merged, result.meta());
|
||||
if (request instanceof ElicitUrlRequest urlRequest) {
|
||||
if (this.urlElicitationHandler == null) {
|
||||
return Mono.error(new IllegalStateException(
|
||||
"Received URL elicitation request, but urlElicitation handler is null"));
|
||||
}
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,6 @@
|
|||
|
||||
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.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
|
@ -29,6 +13,23 @@ import java.util.function.Consumer;
|
|||
import java.util.function.Function;
|
||||
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
|
||||
* 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.ElicitationCompleteNotification>> elicitationCompleteConsumers = new ArrayList<>();
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -314,9 +319,24 @@ public interface McpClient {
|
|||
* @return This builder instance for method chaining
|
||||
* @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");
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -439,6 +459,36 @@ public interface McpClient {
|
|||
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
|
||||
* calling any client operation. This allows to extract thread-locals and hand
|
||||
|
|
@ -502,8 +552,9 @@ public interface McpClient {
|
|||
public McpSyncClient build() {
|
||||
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
||||
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
|
||||
this.elicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
|
||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
||||
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
|
||||
this.urlElicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
|
||||
|
||||
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.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -683,9 +738,24 @@ public interface McpClient {
|
|||
* @return This builder instance for method chaining
|
||||
* @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");
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -812,6 +882,36 @@ public interface McpClient {
|
|||
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
|
||||
* output schemas.
|
||||
|
|
@ -863,7 +963,8 @@ public interface McpClient {
|
|||
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
||||
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
||||
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching,
|
||||
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
|
||||
this.urlElicitationHandler, this.enableCallToolSchemaCaching,
|
||||
this.applyElicitationDefaults));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class McpClientFeatures {
|
|||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @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 applyElicitationDefaults whether the client should fill in missing fields of
|
||||
* 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<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
|
||||
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) {
|
||||
|
||||
/**
|
||||
|
|
@ -88,7 +90,7 @@ class McpClientFeatures {
|
|||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @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 applyElicitationDefaults whether the client should fill in missing
|
||||
* 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<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
|
||||
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) {
|
||||
|
||||
Assert.notNull(clientInfo, "Client info must not be null");
|
||||
|
|
@ -112,8 +116,7 @@ class McpClientFeatures {
|
|||
: new McpSchema.ClientCapabilities(null,
|
||||
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
||||
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build()
|
||||
: null);
|
||||
elicitationCapabilities(formElicitationHandler, urlElicitationHandler));
|
||||
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
||||
|
||||
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||
|
|
@ -122,8 +125,11 @@ class McpClientFeatures {
|
|||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
||||
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
|
||||
: List.of();
|
||||
this.samplingHandler = samplingHandler;
|
||||
this.elicitationHandler = elicitationHandler;
|
||||
this.formElicitationHandler = formElicitationHandler;
|
||||
this.urlElicitationHandler = urlElicitationHandler;
|
||||
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
||||
this.applyElicitationDefaults = applyElicitationDefaults;
|
||||
}
|
||||
|
|
@ -139,10 +145,10 @@ class McpClientFeatures {
|
|||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||
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,
|
||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
||||
elicitationHandler, false, false);
|
||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), List.of(),
|
||||
samplingHandler, elicitationHandler, null, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -190,19 +196,36 @@ class McpClientFeatures {
|
|||
.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
|
||||
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
|
||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = r -> Mono
|
||||
.fromCallable(() -> syncSpec.elicitationHandler().apply(r))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> formElicitationHandler = syncSpec
|
||||
.formElicitationHandler() != null
|
||||
? 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(),
|
||||
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
|
||||
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
|
||||
syncSpec.enableCallToolSchemaCaching, syncSpec.applyElicitationDefaults);
|
||||
loggingConsumers, progressConsumers, elicitationCompleteConsumers, samplingHandler,
|
||||
formElicitationHandler, urlElicitationHandler, syncSpec.enableCallToolSchemaCaching,
|
||||
syncSpec.applyElicitationDefaults);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -218,7 +241,7 @@ class McpClientFeatures {
|
|||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @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 applyElicitationDefaults whether the client should fill in missing fields of
|
||||
* 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<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
|
||||
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) {
|
||||
|
||||
/**
|
||||
|
|
@ -247,7 +272,7 @@ class McpClientFeatures {
|
|||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @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 applyElicitationDefaults whether the client should fill in missing
|
||||
* 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<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
|
||||
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) {
|
||||
|
||||
Assert.notNull(clientInfo, "Client info must not be null");
|
||||
|
|
@ -270,8 +297,7 @@ class McpClientFeatures {
|
|||
: new McpSchema.ClientCapabilities(null,
|
||||
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
||||
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build()
|
||||
: null);
|
||||
elicitationCapabilities(formElicitationHandler, urlElicitationHandler));
|
||||
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
|
||||
|
||||
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||
|
|
@ -280,8 +306,11 @@ class McpClientFeatures {
|
|||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
||||
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
|
||||
: List.of();
|
||||
this.samplingHandler = samplingHandler;
|
||||
this.elicitationHandler = elicitationHandler;
|
||||
this.formElicitationHandler = formElicitationHandler;
|
||||
this.urlElicitationHandler = urlElicitationHandler;
|
||||
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
||||
this.applyElicitationDefaults = applyElicitationDefaults;
|
||||
}
|
||||
|
|
@ -296,11 +325,29 @@ class McpClientFeatures {
|
|||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||
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,
|
||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
||||
elicitationHandler, false, false);
|
||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), List.of(),
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -920,6 +920,25 @@ public class McpAsyncServer {
|
|||
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() {
|
||||
return (exchange, params) -> {
|
||||
// TODO: Implement pagination
|
||||
|
|
|
|||
|
|
@ -4,18 +4,16 @@
|
|||
|
||||
package io.modelcontextprotocol.server;
|
||||
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpLoggableSession;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||
import io.modelcontextprotocol.spec.McpSession;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
|
|
@ -171,13 +169,27 @@ public class McpAsyncServerExchange {
|
|||
return Mono
|
||||
.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"));
|
||||
}
|
||||
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 {
|
||||
this.jsonSchemaValidator.assertConforms("ElicitRequest requestedSchema",
|
||||
elicitRequest.requestedSchema());
|
||||
this.jsonSchemaValidator.assertConforms("ElicitRequest requestedSchema", formRequest.requestedSchema());
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return Mono.error(e);
|
||||
|
|
|
|||
|
|
@ -230,6 +230,16 @@ public class McpSyncServer {
|
|||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2024 - 2024 the original author or authors.
|
||||
* Copyright 2024 - 2026 the original author or authors.
|
||||
*/
|
||||
|
||||
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(
|
||||
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;
|
||||
|
||||
public McpError(JSONRPCError jsonRpcError) {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
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.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
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
|
||||
|
|
@ -114,6 +112,8 @@ public final class McpSchema {
|
|||
// Elicitation Methods
|
||||
public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_ELICITATION_COMPLETE = "notifications/elicitation/complete";
|
||||
|
||||
// ---------------------------
|
||||
// JSON-RPC Error Codes
|
||||
// ---------------------------
|
||||
|
|
@ -152,6 +152,11 @@ public final class McpSchema {
|
|||
*/
|
||||
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
|
||||
/**
|
||||
* 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
|
||||
* client.
|
||||
* client, using {@code form} mode.
|
||||
*
|
||||
* @param message The message to present to the user
|
||||
* @param requestedSchema A restricted subset of JSON Schema. Only top-level
|
||||
|
|
@ -3901,18 +3945,26 @@ public final class McpSchema {
|
|||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record ElicitRequest( // @formatter:off
|
||||
public record ElicitFormRequest( // @formatter:off
|
||||
@JsonProperty("message") String message,
|
||||
@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(requestedSchema, "requestedSchema must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonProperty("mode")
|
||||
public String mode() {
|
||||
return MODE;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
static ElicitRequest fromJson(@JsonProperty("message") String message,
|
||||
static ElicitFormRequest fromJson(@JsonProperty("message") String message,
|
||||
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
|
||||
@JsonProperty("_meta") Map<String, Object> meta) {
|
||||
if (message == null || requestedSchema == null) {
|
||||
|
|
@ -3925,23 +3977,10 @@ public final class McpSchema {
|
|||
missing.add("requestedSchema -> {}");
|
||||
requestedSchema = Map.of();
|
||||
}
|
||||
logger.warn("ElicitRequest: missing required fields during deserialization: {}",
|
||||
logger.warn("ElicitFormRequest: missing required fields during deserialization: {}",
|
||||
String.join(", ", missing));
|
||||
}
|
||||
return new ElicitRequest(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();
|
||||
return new ElicitFormRequest(message, requestedSchema, meta);
|
||||
}
|
||||
|
||||
public static Builder builder(String message, Map<String, Object> requestedSchema) {
|
||||
|
|
@ -3957,11 +3996,11 @@ public final class McpSchema {
|
|||
private Map<String, Object> meta;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ElicitRequest#builder(String, Map)} factory method
|
||||
* instead.
|
||||
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
|
||||
* method instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder() {
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
private Builder(String message, Map<String, Object> requestedSchema) {
|
||||
|
|
@ -3971,12 +4010,22 @@ public final class McpSchema {
|
|||
this.requestedSchema = requestedSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
|
||||
* method instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder message(String message) {
|
||||
Assert.notNull(message, "message must not be null");
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
|
||||
* method instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder requestedSchema(Map<String, Object> requestedSchema) {
|
||||
Assert.notNull(requestedSchema, "requestedSchema must not be null");
|
||||
this.requestedSchema = requestedSchema;
|
||||
|
|
@ -3996,10 +4045,114 @@ public final class McpSchema {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ElicitRequest build() {
|
||||
public ElicitFormRequest build() {
|
||||
Assert.notNull(message, "message 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
|
||||
// ---------------------------
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,17 +4,17 @@
|
|||
|
||||
package io.modelcontextprotocol.server;
|
||||
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpServerSession;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
|
|
@ -531,6 +531,149 @@ class McpAsyncServerExchangeTests {
|
|||
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
|
||||
// ---------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
package io.modelcontextprotocol;
|
||||
|
||||
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
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.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
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.CreateMessageRequest;
|
||||
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.InitializeResult;
|
||||
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.TextContent;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||
import io.modelcontextprotocol.util.McpJsonMapperUtils;
|
||||
import io.modelcontextprotocol.util.Utils;
|
||||
import net.javacrumbs.jsonunit.core.Option;
|
||||
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 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.json;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.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.mockito.Mockito.mock;
|
||||
|
||||
|
|
@ -381,7 +384,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
|
||||
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||
.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))))
|
||||
.build();
|
||||
|
||||
|
|
@ -412,7 +415,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
|
||||
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.requestedSchema()).isNotNull();
|
||||
|
||||
|
|
@ -431,7 +434,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = McpSchema.ElicitRequest
|
||||
var elicitationRequest = McpSchema.ElicitFormRequest
|
||||
.builder("Test message",
|
||||
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||
.build();
|
||||
|
|
@ -447,7 +450,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
try (var mcpClient = clientBuilder
|
||||
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
|
||||
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||
.elicitation(elicitationHandler)
|
||||
.elicitation(formElicitationHandler)
|
||||
.build()) {
|
||||
|
||||
InitializeResult initResult = mcpClient.initialize();
|
||||
|
|
@ -476,7 +479,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
var clientBuilder = clientBuilders.get(clientType);
|
||||
|
||||
// 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.requestedSchema()).isNotNull();
|
||||
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
|
||||
|
|
@ -491,15 +494,13 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Provide your preferences")
|
||||
.requestedSchema(Map.of("type", "object", "properties",
|
||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
||||
Map.of("type", "integer", "default", 18), "subscribe",
|
||||
Map.of("type", "boolean", "default", true), "color",
|
||||
Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")),
|
||||
"required", List.of("nickname", "age", "subscribe", "color")))
|
||||
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences",
|
||||
Map.of("type", "object", "properties",
|
||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
||||
Map.of("type", "integer", "default", 18), "subscribe",
|
||||
Map.of("type", "boolean", "default", true), "color",
|
||||
Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")),
|
||||
"required", List.of("nickname", "age", "subscribe", "color")))
|
||||
.build();
|
||||
|
||||
return exchange.createElicitation(elicitationRequest)
|
||||
|
|
@ -544,7 +545,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
|
||||
// Client handler returns an unmodifiable map (Map.of()) — SDK must copy into a
|
||||
// mutable map before applying defaults.
|
||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||
McpSchema.ElicitResult.Action.ACCEPT, Map.of());
|
||||
|
||||
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())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Provide your preferences")
|
||||
.requestedSchema(Map.of("type", "object", "properties",
|
||||
var elicitationRequest = McpSchema.ElicitFormRequest
|
||||
.builder("Provide your preferences", Map.of("type", "object", "properties",
|
||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
||||
Map.of("type", "integer", "default", 18)),
|
||||
"required", List.of("nickname", "age")))
|
||||
|
|
@ -603,7 +603,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
|
||||
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<>());
|
||||
|
||||
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())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Provide your preferences")
|
||||
.requestedSchema(Map.of("type", "object", "properties",
|
||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
|
||||
"object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||
.build();
|
||||
|
||||
return exchange.createElicitation(elicitationRequest)
|
||||
|
|
@ -659,7 +657,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
|
||||
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<>());
|
||||
|
||||
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())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Provide your preferences")
|
||||
.requestedSchema(Map.of("type", "object", "properties",
|
||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
|
||||
"object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||
.build();
|
||||
|
||||
return exchange.createElicitation(elicitationRequest)
|
||||
|
|
@ -716,7 +712,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
var clientBuilder = clientBuilders.get(clientType);
|
||||
|
||||
Map<String, Object> meta = Map.of("trace-id", "abc-123");
|
||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>(), meta);
|
||||
|
||||
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())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Provide your preferences")
|
||||
.requestedSchema(Map.of("type", "object", "properties",
|
||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
|
||||
"object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||
.build();
|
||||
|
||||
return exchange.createElicitation(elicitationRequest)
|
||||
|
|
@ -773,9 +767,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
|
||||
var clientBuilder = clientBuilders.get(clientType);
|
||||
|
||||
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||
Function<ElicitFormRequest, ElicitResult> elicitationHandler = request -> {
|
||||
assertThat(request.message()).isNotEmpty();
|
||||
assertThat(request.requestedSchema()).isNotNull();
|
||||
assertThat(((McpSchema.ElicitFormRequest) request).requestedSchema()).isNotNull();
|
||||
return ElicitResult.builder(ElicitResult.Action.ACCEPT)
|
||||
.content(Map.of("message", request.message()))
|
||||
.build();
|
||||
|
|
@ -791,7 +785,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = McpSchema.ElicitRequest
|
||||
var elicitationRequest = McpSchema.ElicitFormRequest
|
||||
.builder("Test message",
|
||||
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||
.build();
|
||||
|
|
@ -840,7 +834,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
|
||||
var clientBuilder = clientBuilders.get(clientType);
|
||||
|
||||
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||
Function<ElicitFormRequest, ElicitResult> elicitationHandler = request -> {
|
||||
assertThat(request.message()).isNotEmpty();
|
||||
assertThat(request.requestedSchema()).isNotNull();
|
||||
|
||||
|
|
@ -867,7 +861,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
|||
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||
.callHandler((exchange, request) -> {
|
||||
|
||||
var elicitationRequest = ElicitRequest
|
||||
var elicitationRequest = ElicitFormRequest
|
||||
.builder("Test message",
|
||||
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||
.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
|
||||
// ---------------------------------------
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
|
@ -685,11 +686,13 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||
.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());
|
||||
|
||||
withClient(createMcpTransport(),
|
||||
builder -> builder.capabilities(capabilities).sampling(samplingHandler).elicitation(elicitationHandler),
|
||||
builder -> builder.capabilities(capabilities)
|
||||
.sampling(samplingHandler)
|
||||
.elicitation(formElicitationHandler),
|
||||
client ->
|
||||
|
||||
StepVerifier.create(client.initialize()).assertNext(result -> {
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ class McpAsyncClientResponseHandlerTests {
|
|||
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||
|
||||
// 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.requestedSchema()).isInstanceOf(Map.class);
|
||||
assertThat(request.requestedSchema().get("type")).isEqualTo("object");
|
||||
|
|
@ -458,7 +458,7 @@ class McpAsyncClientResponseHandlerTests {
|
|||
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||
|
||||
// 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());
|
||||
|
||||
// Create client with elicitation capability and handler
|
||||
|
|
@ -534,17 +534,6 @@ class McpAsyncClientResponseHandlerTests {
|
|||
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
|
||||
void testPingMessageRequestHandling() {
|
||||
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,15 +1,9 @@
|
|||
/*
|
||||
* Copyright 2025 - 2026 the original author or authors.
|
||||
*/
|
||||
* Copyright 2025 - 2026 the original author or authors.
|
||||
*/
|
||||
|
||||
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.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
@ -17,12 +11,17 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
||||
import net.javacrumbs.jsonunit.core.Option;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import net.javacrumbs.jsonunit.core.Option;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
|
|
@ -1557,7 +1556,7 @@ public class McpSchemaTests {
|
|||
|
||||
@Test
|
||||
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"),
|
||||
"properties", Map.of("foo", Map.of("type", "string"))))
|
||||
.build();
|
||||
|
|
@ -1567,9 +1566,43 @@ public class McpSchemaTests {
|
|||
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
||||
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||
.isObject()
|
||||
.isEqualTo(
|
||||
json("""
|
||||
{"message":"Please provide additional information","requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}"""));
|
||||
.isEqualTo(json("""
|
||||
{
|
||||
"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
|
||||
|
|
@ -1588,12 +1621,57 @@ public class McpSchemaTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception {
|
||||
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{}", McpSchema.ElicitRequest.class);
|
||||
void testElicitRequestDeserializationDefaultsToForm() throws Exception {
|
||||
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.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
|
||||
|
|
@ -1604,7 +1682,8 @@ public class McpSchemaTests {
|
|||
Map<String, Object> meta = new HashMap<>();
|
||||
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)
|
||||
.build();
|
||||
|
||||
|
|
@ -1612,7 +1691,8 @@ public class McpSchemaTests {
|
|||
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
||||
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||
.isObject()
|
||||
.containsEntry("_meta", Map.of("progressToken", "elicit-token-789"));
|
||||
.containsEntry("_meta", Map.of("progressToken", "elicit-token-789"))
|
||||
.containsEntry("mode", "form");
|
||||
|
||||
// Test Request interface methods
|
||||
assertThat(request.meta()).isEqualTo(meta);
|
||||
|
|
@ -1627,16 +1707,25 @@ public class McpSchemaTests {
|
|||
requestedSchema.put("properties", Map.of("name", Map.of("type", "string")));
|
||||
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();
|
||||
|
||||
String json = JSON_MAPPER.writeValueAsString(request);
|
||||
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);
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@Test
|
||||
|
|
@ -1889,15 +1978,13 @@ public class McpSchemaTests {
|
|||
@Test
|
||||
void testElicitRequestWithDefaultValues() throws Exception {
|
||||
// Test that schemas with default values serialize correctly in an ElicitRequest
|
||||
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder()
|
||||
.message("Please provide your info")
|
||||
.requestedSchema(Map.of("type", "object", "properties",
|
||||
Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
|
||||
Map.of("type", "integer", "default", 30), "score",
|
||||
Map.of("type", "number", "default", 95.5), "status",
|
||||
Map.of("type", "string", "enum", List.of("active", "inactive"), "default", "active"),
|
||||
"verified", Map.of("type", "boolean", "default", true)),
|
||||
"required", List.of("name")))
|
||||
McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest.builder("Please provide your info", Map.of("type",
|
||||
"object", "properties",
|
||||
Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
|
||||
Map.of("type", "integer", "default", 30), "score", Map.of("type", "number", "default", 95.5),
|
||||
"status", Map.of("type", "string", "enum", List.of("active", "inactive"), "default", "active"),
|
||||
"verified", Map.of("type", "boolean", "default", true)),
|
||||
"required", List.of("name")))
|
||||
.build();
|
||||
|
||||
String value = JSON_MAPPER.writeValueAsString(request);
|
||||
|
|
@ -1909,6 +1996,41 @@ public class McpSchemaTests {
|
|||
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
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in a new issue