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:
|
Elicitation enables servers to request additional information or user input through the client. This is useful when a server needs clarification or confirmation during an operation:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// Configure elicitation handler
|
// Configure form elicitation handler
|
||||||
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
Function<ElicitFormRequest, ElicitResult> formElicitationHandler = request -> {
|
||||||
// Present the request to the user and collect their response
|
// Present the request to the user and collect their response
|
||||||
// The request contains a message and a schema describing the expected input
|
// The request contains a message and a schema describing the expected input
|
||||||
Map<String, Object> userResponse = collectUserInput(request.message(), request.requestedSchema());
|
Map<String, Object> userResponse = collectUserInput(request.message(), request.requestedSchema());
|
||||||
return new ElicitResult(ElicitResult.Action.ACCEPT, userResponse);
|
return new ElicitResult(ElicitResult.Action.ACCEPT, userResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Configure URL elicitation handler
|
||||||
|
Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler = request -> {
|
||||||
|
// Prompt the user to visit the URL
|
||||||
|
// e.g. openBrowser(request.url());
|
||||||
|
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of());
|
||||||
|
};
|
||||||
|
|
||||||
// Create client with elicitation support
|
// Create client with elicitation support
|
||||||
var client = McpClient.sync(transport)
|
var client = McpClient.sync(transport)
|
||||||
.capabilities(ClientCapabilities.builder()
|
.capabilities(ClientCapabilities.builder()
|
||||||
.elicitation()
|
.elicitation(true, true) // enables both form and URL elicitation
|
||||||
.build())
|
.build())
|
||||||
.elicitation(elicitationHandler)
|
.elicitation(formElicitationHandler)
|
||||||
|
.urlElicitation(urlElicitationHandler)
|
||||||
.build();
|
.build();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -293,6 +301,28 @@ The `ElicitResult` supports three actions:
|
||||||
- `DECLINE` - The user declined to provide the information
|
- `DECLINE` - The user declined to provide the information
|
||||||
- `CANCEL` - The operation was cancelled
|
- `CANCEL` - The operation was cancelled
|
||||||
|
|
||||||
|
#### URL Elicitation Required Handling
|
||||||
|
|
||||||
|
When a server requires out-of-band URL elicitation but the client has not negotiated support for it (or the server strictly requires out-of-band handling), the server may return a `URL_ELICITATION_REQUIRED` error during tool execution or prompt retrieval.
|
||||||
|
|
||||||
|
```java
|
||||||
|
try {
|
||||||
|
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
} catch (McpError e) {
|
||||||
|
if (e.getJsonRpcError().code() == McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED) {
|
||||||
|
// Extract elicitation requests from the error data
|
||||||
|
Map<String, Object> data = (Map<String, Object>) e.getJsonRpcError().data();
|
||||||
|
TypeRef<List<McpSchema.ElicitUrlRequest>> typeRef = new TypeRef<>() {};
|
||||||
|
var requests = McpJsonDefaults.getMapper()
|
||||||
|
.convertValue(data.get("elicitations"), typeRef);
|
||||||
|
|
||||||
|
for (var req : requests) {
|
||||||
|
// handle elicitation requests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Logging Support
|
### Logging Support
|
||||||
|
|
||||||
The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages:
|
The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages:
|
||||||
|
|
|
||||||
|
|
@ -715,9 +715,7 @@ var tool = SyncToolSpecification.builder()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request user confirmation
|
// Request user confirmation
|
||||||
ElicitRequest elicitRequest = ElicitRequest.builder()
|
ElicitRequest elicitRequest = ElicitFormRequest.builder("Do you want to proceed with this action?", Map.of(
|
||||||
.message("Do you want to proceed with this action?")
|
|
||||||
.requestedSchema(Map.of(
|
|
||||||
"type", "object",
|
"type", "object",
|
||||||
"properties", Map.of("confirmed", Map.of("type", "boolean"))
|
"properties", Map.of("confirmed", Map.of("type", "boolean"))
|
||||||
))
|
))
|
||||||
|
|
@ -739,6 +737,40 @@ var tool = SyncToolSpecification.builder()
|
||||||
.build();
|
.build();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To request out-of-band URL elicitation, such as a user authorizing an OAuth flow:
|
||||||
|
|
||||||
|
```java
|
||||||
|
var urlTool = SyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder()
|
||||||
|
.name("oauth-auth")
|
||||||
|
.description("Authenticates via OAuth")
|
||||||
|
.inputSchema(schema)
|
||||||
|
.build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
// Request URL elicitation from client
|
||||||
|
if (
|
||||||
|
exchange.getClientCapabilities().elicitation() != null
|
||||||
|
&& exchange.getClientCapabilities().elicitation().url() != null
|
||||||
|
) {
|
||||||
|
ElicitRequest urlRequest = McpSchema.ElicitUrlRequest
|
||||||
|
.builder("Please authenticate", "https://example.com/oauth", "oauth-123").build();
|
||||||
|
ElicitResult result = exchange.elicit(urlRequest);
|
||||||
|
// handle result.action == CANCELLED or DENIED
|
||||||
|
if (result.action() != ElicitResult.Action.ACCEPT) {
|
||||||
|
return CallToolResult.builder()
|
||||||
|
.content(List.of(new McpSchema.TextContent("Authentication failed or cancelled")))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for user to visit the URL
|
||||||
|
return CallToolResult.builder()
|
||||||
|
.content(List.of(new McpSchema.TextContent("Authentication successful")))
|
||||||
|
.build();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
### Logging Support
|
### Logging Support
|
||||||
|
|
||||||
The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.
|
The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@ import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
|
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
|
|
@ -29,8 +26,10 @@ import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||||
|
|
@ -41,6 +40,8 @@ import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import io.modelcontextprotocol.util.ToolNameValidator;
|
import io.modelcontextprotocol.util.ToolNameValidator;
|
||||||
import io.modelcontextprotocol.util.Utils;
|
import io.modelcontextprotocol.util.Utils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
|
@ -108,6 +109,9 @@ public class McpAsyncClient {
|
||||||
public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() {
|
public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final TypeRef<McpSchema.ElicitationCompleteNotification> ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF = new TypeRef<>() {
|
||||||
|
};
|
||||||
|
|
||||||
public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";
|
public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -145,7 +149,14 @@ public class McpAsyncClient {
|
||||||
* necessary information dynamically. Servers can request structured data from users
|
* necessary information dynamically. Servers can request structured data from users
|
||||||
* with optional JSON schemas to validate responses.
|
* with optional JSON schemas to validate responses.
|
||||||
*/
|
*/
|
||||||
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
|
private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP provides a standardized way for servers to request additional information from
|
||||||
|
* users out-of-band during interactions. This flow allows users to share information
|
||||||
|
* with the server without sharing it with the client.
|
||||||
|
*/
|
||||||
|
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client transport implementation.
|
* Client transport implementation.
|
||||||
|
|
@ -226,11 +237,21 @@ public class McpAsyncClient {
|
||||||
|
|
||||||
// Elicitation Handler
|
// Elicitation Handler
|
||||||
if (this.clientCapabilities.elicitation() != null) {
|
if (this.clientCapabilities.elicitation() != null) {
|
||||||
if (features.elicitationHandler() == null) {
|
// elicitation: {} is equivalent to elicitation: { form: {} } for
|
||||||
|
// backwards-compatiblity
|
||||||
|
var supportsForm = this.clientCapabilities.elicitation().form() != null
|
||||||
|
|| this.clientCapabilities.elicitation().url() == null;
|
||||||
|
var supportsUrl = this.clientCapabilities.elicitation().url() != null;
|
||||||
|
if (supportsForm && features.formElicitationHandler() == null) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Elicitation handler must not be null when client capabilities include elicitation");
|
"Form elicitation handler must not be null when client capabilities include form elicitation");
|
||||||
}
|
}
|
||||||
this.elicitationHandler = features.elicitationHandler();
|
if (supportsUrl && features.urlElicitationHandler() == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"URL elicitation handler must not be null when client capabilities include URL elicitation");
|
||||||
|
}
|
||||||
|
this.formElicitationHandler = features.formElicitationHandler();
|
||||||
|
this.urlElicitationHandler = features.urlElicitationHandler();
|
||||||
requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
|
requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,6 +322,16 @@ public class McpAsyncClient {
|
||||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
|
||||||
asyncProgressNotificationHandler(progressConsumersFinal));
|
asyncProgressNotificationHandler(progressConsumersFinal));
|
||||||
|
|
||||||
|
// Elicitation Complete Notification
|
||||||
|
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumersFinal = new ArrayList<>();
|
||||||
|
elicitationCompleteConsumersFinal
|
||||||
|
.add((notification) -> Mono.fromRunnable(() -> logger.debug("Elicitation complete: {}", notification)));
|
||||||
|
if (!Utils.isEmpty(features.elicitationCompleteConsumers())) {
|
||||||
|
elicitationCompleteConsumersFinal.addAll(features.elicitationCompleteConsumers());
|
||||||
|
}
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
|
||||||
|
asyncElicitationCompleteNotificationHandler(elicitationCompleteConsumersFinal));
|
||||||
|
|
||||||
Function<Initialization, Mono<Void>> postInitializationHook = init -> {
|
Function<Initialization, Mono<Void>> postInitializationHook = init -> {
|
||||||
|
|
||||||
if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
|
if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
|
||||||
|
|
@ -552,23 +583,47 @@ public class McpAsyncClient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------
|
|
||||||
// Elicitation
|
|
||||||
// --------------------------
|
|
||||||
private RequestHandler<ElicitResult> elicitationCreateHandler() {
|
private RequestHandler<ElicitResult> elicitationCreateHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
|
McpSchema.ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.elicitationHandler.apply(request).map(result -> {
|
if (request instanceof ElicitUrlRequest urlRequest) {
|
||||||
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
|
if (this.urlElicitationHandler == null) {
|
||||||
&& result.content() != null) {
|
return Mono.error(new IllegalStateException(
|
||||||
Map<String, Object> merged = new HashMap<>(result.content());
|
"Received URL elicitation request, but urlElicitation handler is null"));
|
||||||
applyElicitationDefaults(request.requestedSchema(), merged);
|
|
||||||
return new ElicitResult(result.action(), merged, result.meta());
|
|
||||||
}
|
}
|
||||||
return result;
|
return this.urlElicitationHandler.apply(urlRequest);
|
||||||
});
|
}
|
||||||
|
else if (request instanceof ElicitFormRequest formRequest) {
|
||||||
|
if (this.formElicitationHandler == null) {
|
||||||
|
return Mono.error(new IllegalStateException(
|
||||||
|
"Received FORM elicitation request, but formElicitationHandler handler is null"));
|
||||||
|
}
|
||||||
|
return this.formElicitationHandler.apply(formRequest).map(result -> {
|
||||||
|
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
|
||||||
|
&& result.content() != null) {
|
||||||
|
Map<String, Object> merged = new HashMap<>(result.content());
|
||||||
|
applyElicitationDefaults(formRequest.requestedSchema(), merged);
|
||||||
|
return new ElicitResult(result.action(), merged, result.meta());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.error(new IllegalStateException("Unknown elictation type deserialized"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private NotificationHandler asyncElicitationCompleteNotificationHandler(
|
||||||
|
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
|
||||||
|
return params -> {
|
||||||
|
McpSchema.ElicitationCompleteNotification notification = transport.unmarshalFrom(params,
|
||||||
|
ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF);
|
||||||
|
|
||||||
|
return Flux.fromIterable(elicitationCompleteConsumers)
|
||||||
|
.flatMap(consumer -> consumer.apply(notification))
|
||||||
|
.then();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,6 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
|
||||||
import io.modelcontextprotocol.json.McpJsonDefaults;
|
|
||||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Implementation;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
|
||||||
import io.modelcontextprotocol.spec.McpTransport;
|
|
||||||
import io.modelcontextprotocol.util.Assert;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -29,6 +13,23 @@ import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
|
import io.modelcontextprotocol.json.McpJsonDefaults;
|
||||||
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.Implementation;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
|
import io.modelcontextprotocol.spec.McpTransport;
|
||||||
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that
|
* Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that
|
||||||
* enables AI models to interact with external tools and resources through a standardized
|
* enables AI models to interact with external tools and resources through a standardized
|
||||||
|
|
@ -185,9 +186,13 @@ public interface McpClient {
|
||||||
|
|
||||||
private final List<Consumer<McpSchema.ProgressNotification>> progressConsumers = new ArrayList<>();
|
private final List<Consumer<McpSchema.ProgressNotification>> progressConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private final List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers = new ArrayList<>();
|
||||||
|
|
||||||
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||||
|
|
||||||
private Function<ElicitRequest, ElicitResult> elicitationHandler;
|
private Function<ElicitFormRequest, ElicitResult> formElicitationHandler;
|
||||||
|
|
||||||
|
private Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler;
|
||||||
|
|
||||||
private Supplier<McpTransportContext> contextProvider = () -> McpTransportContext.EMPTY;
|
private Supplier<McpTransportContext> contextProvider = () -> McpTransportContext.EMPTY;
|
||||||
|
|
||||||
|
|
@ -314,9 +319,24 @@ public interface McpClient {
|
||||||
* @return This builder instance for method chaining
|
* @return This builder instance for method chaining
|
||||||
* @throws IllegalArgumentException if elicitationHandler is null
|
* @throws IllegalArgumentException if elicitationHandler is null
|
||||||
*/
|
*/
|
||||||
public SyncSpec elicitation(Function<ElicitRequest, ElicitResult> elicitationHandler) {
|
public SyncSpec elicitation(Function<ElicitFormRequest, ElicitResult> elicitationHandler) {
|
||||||
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
||||||
this.elicitationHandler = elicitationHandler;
|
this.formElicitationHandler = elicitationHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom elicitation handler for processing URL-mode elicitation message
|
||||||
|
* requests. The elicitation handler can modify or validate messages before they
|
||||||
|
* are sent to the server, enabling custom processing logic.
|
||||||
|
* @param elicitationHandler A function that processes elicitation requests and
|
||||||
|
* returns results. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationHandler is null
|
||||||
|
*/
|
||||||
|
public SyncSpec urlElicitation(Function<ElicitUrlRequest, ElicitResult> elicitationHandler) {
|
||||||
|
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
||||||
|
this.urlElicitationHandler = elicitationHandler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,6 +459,36 @@ public interface McpClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified by the server when an URL elicitation is
|
||||||
|
* complete.
|
||||||
|
* @param elicitationCompleteConsumer A consumer that receives elicitation
|
||||||
|
* complete notifications. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
|
||||||
|
*/
|
||||||
|
public SyncSpec elicitationCompleteConsumer(
|
||||||
|
Consumer<McpSchema.ElicitationCompleteNotification> elicitationCompleteConsumer) {
|
||||||
|
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
|
||||||
|
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple consumers to be notified by the server when an URL elicitation is
|
||||||
|
* complete.
|
||||||
|
* @param elicitationCompleteConsumers A list of consumers that receives
|
||||||
|
* elicitation complete notifications. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationCompleteConsumers is null
|
||||||
|
*/
|
||||||
|
public SyncSpec elicitationCompleteConsumers(
|
||||||
|
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers) {
|
||||||
|
Assert.notNull(elicitationCompleteConsumers, "Elicitation complete consumers must not be null");
|
||||||
|
this.elicitationCompleteConsumers.addAll(elicitationCompleteConsumers);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a provider of {@link McpTransportContext}, providing a context before
|
* Add a provider of {@link McpTransportContext}, providing a context before
|
||||||
* calling any client operation. This allows to extract thread-locals and hand
|
* calling any client operation. This allows to extract thread-locals and hand
|
||||||
|
|
@ -502,8 +552,9 @@ public interface McpClient {
|
||||||
public McpSyncClient build() {
|
public McpSyncClient build() {
|
||||||
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
||||||
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
|
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
||||||
this.elicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
|
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
|
||||||
|
this.urlElicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
|
||||||
|
|
||||||
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
|
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
|
||||||
|
|
||||||
|
|
@ -556,9 +607,13 @@ public interface McpClient {
|
||||||
|
|
||||||
private final List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers = new ArrayList<>();
|
private final List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers = new ArrayList<>();
|
||||||
|
|
||||||
|
private final List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
|
||||||
|
|
||||||
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
||||||
|
|
||||||
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
|
private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;
|
||||||
|
|
||||||
|
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;
|
||||||
|
|
||||||
private JsonSchemaValidator jsonSchemaValidator;
|
private JsonSchemaValidator jsonSchemaValidator;
|
||||||
|
|
||||||
|
|
@ -683,9 +738,24 @@ public interface McpClient {
|
||||||
* @return This builder instance for method chaining
|
* @return This builder instance for method chaining
|
||||||
* @throws IllegalArgumentException if elicitationHandler is null
|
* @throws IllegalArgumentException if elicitationHandler is null
|
||||||
*/
|
*/
|
||||||
public AsyncSpec elicitation(Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler) {
|
public AsyncSpec elicitation(Function<ElicitFormRequest, Mono<ElicitResult>> elicitationHandler) {
|
||||||
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
||||||
this.elicitationHandler = elicitationHandler;
|
this.formElicitationHandler = elicitationHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom elicitation handler for processing elicitation message requests.
|
||||||
|
* The elicitation handler can modify or validate messages before they are sent to
|
||||||
|
* the server, enabling custom processing logic.
|
||||||
|
* @param elicitationHandler A function that processes elicitation requests and
|
||||||
|
* returns results. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationHandler is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec urlElicitation(Function<ElicitUrlRequest, Mono<ElicitResult>> elicitationHandler) {
|
||||||
|
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
||||||
|
this.urlElicitationHandler = elicitationHandler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -812,6 +882,36 @@ public interface McpClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a consumer to be notified by the server when an URL elicitation is
|
||||||
|
* complete.
|
||||||
|
* @param elicitationCompleteConsumer A consumer that receives elicitation
|
||||||
|
* complete notifications. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec elicitationCompleteConsumer(
|
||||||
|
Function<McpSchema.ElicitationCompleteNotification, Mono<Void>> elicitationCompleteConsumer) {
|
||||||
|
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
|
||||||
|
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple consumers to be notified by the server when an URL elicitation is
|
||||||
|
* complete.
|
||||||
|
* @param elicitationCompleteConsumers A list of consumers that receives
|
||||||
|
* elicitation complete notifications. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationCompleteConsumers is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec elicitationCompleteConsumers(
|
||||||
|
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
|
||||||
|
Assert.notNull(elicitationCompleteConsumers, "Elicitation complete consumers must not be null");
|
||||||
|
this.elicitationCompleteConsumers.addAll(elicitationCompleteConsumers);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the JSON schema validator to use for validating tool responses against
|
* Sets the JSON schema validator to use for validating tool responses against
|
||||||
* output schemas.
|
* output schemas.
|
||||||
|
|
@ -863,7 +963,8 @@ public interface McpClient {
|
||||||
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
||||||
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
||||||
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching,
|
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
|
||||||
|
this.urlElicitationHandler, this.enableCallToolSchemaCaching,
|
||||||
this.applyElicitationDefaults));
|
this.applyElicitationDefaults));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class McpClientFeatures {
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param progressConsumers the progress consumers.
|
* @param progressConsumers the progress consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param formElicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
||||||
* @param applyElicitationDefaults whether the client should fill in missing fields of
|
* @param applyElicitationDefaults whether the client should fill in missing fields of
|
||||||
* an accepted {@code ElicitResult.content} with the {@code default} values declared
|
* an accepted {@code ElicitResult.content} with the {@code default} values declared
|
||||||
|
|
@ -74,8 +74,10 @@ class McpClientFeatures {
|
||||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||||
|
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
|
Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> formElicitationHandler,
|
||||||
|
Function<McpSchema.ElicitUrlRequest, Mono<McpSchema.ElicitResult>> urlElicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,7 +90,7 @@ class McpClientFeatures {
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param progressConsumers the progress consumers.
|
* @param progressConsumers the progress consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param formElicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
||||||
* @param applyElicitationDefaults whether the client should fill in missing
|
* @param applyElicitationDefaults whether the client should fill in missing
|
||||||
* fields of an accepted {@code ElicitResult.content} with the {@code default}
|
* fields of an accepted {@code ElicitResult.content} with the {@code default}
|
||||||
|
|
@ -102,8 +104,10 @@ class McpClientFeatures {
|
||||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||||
|
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
|
Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> formElicitationHandler,
|
||||||
|
Function<McpSchema.ElicitUrlRequest, Mono<McpSchema.ElicitResult>> urlElicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
Assert.notNull(clientInfo, "Client info must not be null");
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
|
|
@ -112,8 +116,7 @@ class McpClientFeatures {
|
||||||
: new McpSchema.ClientCapabilities(null,
|
: new McpSchema.ClientCapabilities(null,
|
||||||
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||||
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
||||||
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build()
|
elicitationCapabilities(formElicitationHandler, urlElicitationHandler));
|
||||||
: null);
|
|
||||||
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
||||||
|
|
||||||
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||||
|
|
@ -122,8 +125,11 @@ class McpClientFeatures {
|
||||||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||||
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
||||||
|
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
|
||||||
|
: List.of();
|
||||||
this.samplingHandler = samplingHandler;
|
this.samplingHandler = samplingHandler;
|
||||||
this.elicitationHandler = elicitationHandler;
|
this.formElicitationHandler = formElicitationHandler;
|
||||||
|
this.urlElicitationHandler = urlElicitationHandler;
|
||||||
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
||||||
this.applyElicitationDefaults = applyElicitationDefaults;
|
this.applyElicitationDefaults = applyElicitationDefaults;
|
||||||
}
|
}
|
||||||
|
|
@ -139,10 +145,10 @@ class McpClientFeatures {
|
||||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
||||||
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
||||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), List.of(),
|
||||||
elicitationHandler, false, false);
|
samplingHandler, elicitationHandler, null, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -190,19 +196,36 @@ class McpClientFeatures {
|
||||||
.subscribeOn(Schedulers.boundedElastic()));
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
|
||||||
|
for (Consumer<McpSchema.ElicitationCompleteNotification> consumer : syncSpec
|
||||||
|
.elicitationCompleteConsumers()) {
|
||||||
|
elicitationCompleteConsumers.add(l -> Mono.<Void>fromRunnable(() -> consumer.accept(l))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
|
||||||
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
|
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = r -> Mono
|
Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> formElicitationHandler = syncSpec
|
||||||
.fromCallable(() -> syncSpec.elicitationHandler().apply(r))
|
.formElicitationHandler() != null
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
? r -> Mono.fromCallable(() -> syncSpec.formElicitationHandler().apply(r))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitUrlRequest, Mono<McpSchema.ElicitResult>> urlElicitationHandler = syncSpec
|
||||||
|
.urlElicitationHandler() != null
|
||||||
|
? r -> Mono.fromCallable(() -> syncSpec.urlElicitationHandler().apply(r))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
: null;
|
||||||
|
|
||||||
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
||||||
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
|
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
|
||||||
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
|
loggingConsumers, progressConsumers, elicitationCompleteConsumers, samplingHandler,
|
||||||
syncSpec.enableCallToolSchemaCaching, syncSpec.applyElicitationDefaults);
|
formElicitationHandler, urlElicitationHandler, syncSpec.enableCallToolSchemaCaching,
|
||||||
|
syncSpec.applyElicitationDefaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -218,7 +241,7 @@ class McpClientFeatures {
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param progressConsumers the progress consumers.
|
* @param progressConsumers the progress consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param formElicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
||||||
* @param applyElicitationDefaults whether the client should fill in missing fields of
|
* @param applyElicitationDefaults whether the client should fill in missing fields of
|
||||||
* an accepted {@code ElicitResult.content} with the {@code default} values declared
|
* an accepted {@code ElicitResult.content} with the {@code default} values declared
|
||||||
|
|
@ -231,8 +254,10 @@ class McpClientFeatures {
|
||||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||||
|
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler,
|
||||||
|
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -247,7 +272,7 @@ class McpClientFeatures {
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param progressConsumers the progress consumers.
|
* @param progressConsumers the progress consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
* @param elicitationHandler the elicitation handler.
|
* @param formElicitationHandler the elicitation handler.
|
||||||
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
|
||||||
* @param applyElicitationDefaults whether the client should fill in missing
|
* @param applyElicitationDefaults whether the client should fill in missing
|
||||||
* fields of an accepted {@code ElicitResult.content} with the {@code default}
|
* fields of an accepted {@code ElicitResult.content} with the {@code default}
|
||||||
|
|
@ -260,8 +285,10 @@ class McpClientFeatures {
|
||||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||||
|
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler,
|
||||||
|
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler,
|
||||||
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
boolean enableCallToolSchemaCaching, boolean applyElicitationDefaults) {
|
||||||
|
|
||||||
Assert.notNull(clientInfo, "Client info must not be null");
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
|
|
@ -270,8 +297,7 @@ class McpClientFeatures {
|
||||||
: new McpSchema.ClientCapabilities(null,
|
: new McpSchema.ClientCapabilities(null,
|
||||||
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||||
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
||||||
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build()
|
elicitationCapabilities(formElicitationHandler, urlElicitationHandler));
|
||||||
: null);
|
|
||||||
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
|
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
|
||||||
|
|
||||||
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||||
|
|
@ -280,8 +306,11 @@ class McpClientFeatures {
|
||||||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||||
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
||||||
|
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
|
||||||
|
: List.of();
|
||||||
this.samplingHandler = samplingHandler;
|
this.samplingHandler = samplingHandler;
|
||||||
this.elicitationHandler = elicitationHandler;
|
this.formElicitationHandler = formElicitationHandler;
|
||||||
|
this.urlElicitationHandler = urlElicitationHandler;
|
||||||
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
|
||||||
this.applyElicitationDefaults = applyElicitationDefaults;
|
this.applyElicitationDefaults = applyElicitationDefaults;
|
||||||
}
|
}
|
||||||
|
|
@ -296,11 +325,29 @@ class McpClientFeatures {
|
||||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler,
|
||||||
|
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler) {
|
||||||
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
||||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), List.of(),
|
||||||
elicitationHandler, false, false);
|
samplingHandler, formElicitationHandler, urlElicitationHandler, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static McpSchema.ClientCapabilities.Elicitation elicitationCapabilities(
|
||||||
|
Function<McpSchema.ElicitFormRequest, ?> formElicitationHandler,
|
||||||
|
Function<McpSchema.ElicitUrlRequest, ?> urlElicitationHandler) {
|
||||||
|
McpSchema.ClientCapabilities.Elicitation elicitationCapabilities = null;
|
||||||
|
if (formElicitationHandler != null || urlElicitationHandler != null) {
|
||||||
|
var elicitationCapabilitiesBuilder = McpSchema.ClientCapabilities.Elicitation.builder();
|
||||||
|
if (formElicitationHandler != null) {
|
||||||
|
elicitationCapabilitiesBuilder.form(new McpSchema.ClientCapabilities.Elicitation.Form());
|
||||||
|
}
|
||||||
|
if (urlElicitationHandler != null) {
|
||||||
|
elicitationCapabilitiesBuilder.url(new McpSchema.ClientCapabilities.Elicitation.Url());
|
||||||
|
}
|
||||||
|
elicitationCapabilities = elicitationCapabilitiesBuilder.build();
|
||||||
|
}
|
||||||
|
return elicitationCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -920,6 +920,25 @@ public class McpAsyncServer {
|
||||||
return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
|
return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an elicitation complete notification to a specific client session, indicating
|
||||||
|
* that an out-of-band URL elicitation interaction has completed.
|
||||||
|
* @param sessionId The ID of the session to notify
|
||||||
|
* @param notification The notification containing the elicitation ID
|
||||||
|
* @return A Mono that completes when the notification has been sent
|
||||||
|
*/
|
||||||
|
public Mono<Void> sendElicitationComplete(String sessionId,
|
||||||
|
McpSchema.ElicitationCompleteNotification notification) {
|
||||||
|
if (sessionId == null) {
|
||||||
|
return Mono.error(new IllegalArgumentException("Session ID must not be null"));
|
||||||
|
}
|
||||||
|
if (notification == null) {
|
||||||
|
return Mono.error(new IllegalArgumentException("Notification must not be null"));
|
||||||
|
}
|
||||||
|
return this.mcpTransportProvider.notifyClient(sessionId, McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
|
||||||
|
notification);
|
||||||
|
}
|
||||||
|
|
||||||
private McpRequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
|
private McpRequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
|
||||||
return (exchange, params) -> {
|
return (exchange, params) -> {
|
||||||
// TODO: Implement pagination
|
// TODO: Implement pagination
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,16 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.server;
|
package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
|
||||||
import io.modelcontextprotocol.spec.McpLoggableSession;
|
import io.modelcontextprotocol.spec.McpLoggableSession;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||||
import io.modelcontextprotocol.spec.McpSession;
|
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
|
@ -171,13 +169,27 @@ public class McpAsyncServerExchange {
|
||||||
return Mono
|
return Mono
|
||||||
.error(new IllegalStateException("Client must be initialized. Call the initialize method first!"));
|
.error(new IllegalStateException("Client must be initialized. Call the initialize method first!"));
|
||||||
}
|
}
|
||||||
if (this.clientCapabilities.elicitation() == null) {
|
McpSchema.ClientCapabilities.Elicitation elicitation = this.clientCapabilities.elicitation();
|
||||||
|
if (elicitation == null) {
|
||||||
return Mono.error(new IllegalStateException("Client must be configured with elicitation capabilities"));
|
return Mono.error(new IllegalStateException("Client must be configured with elicitation capabilities"));
|
||||||
}
|
}
|
||||||
if (this.jsonSchemaValidator != null) {
|
|
||||||
|
// elicitation: {} is equivalent to elicitation: { form: {} }
|
||||||
|
boolean supportsForm = elicitation.form() != null || elicitation.url() == null;
|
||||||
|
boolean supportsUrl = elicitation.url() != null;
|
||||||
|
|
||||||
|
if (elicitRequest instanceof McpSchema.ElicitFormRequest && !supportsForm) {
|
||||||
|
return Mono
|
||||||
|
.error(new IllegalStateException("Client must be configured with form elicitation capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elicitRequest instanceof McpSchema.ElicitUrlRequest && !supportsUrl) {
|
||||||
|
return Mono.error(new IllegalStateException("Client must be configured with URL elicitation capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.jsonSchemaValidator != null && elicitRequest instanceof McpSchema.ElicitFormRequest formRequest) {
|
||||||
try {
|
try {
|
||||||
this.jsonSchemaValidator.assertConforms("ElicitRequest requestedSchema",
|
this.jsonSchemaValidator.assertConforms("ElicitRequest requestedSchema", formRequest.requestedSchema());
|
||||||
elicitRequest.requestedSchema());
|
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
return Mono.error(e);
|
return Mono.error(e);
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,16 @@ public class McpSyncServer {
|
||||||
this.asyncServer.notifyPromptsListChanged().block();
|
this.asyncServer.notifyPromptsListChanged().block();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an elicitation complete notification to a specific client session, indicating
|
||||||
|
* that an out-of-band URL elicitation interaction has completed.
|
||||||
|
* @param sessionId The ID of the session to notify
|
||||||
|
* @param notification The notification containing the elicitation ID
|
||||||
|
*/
|
||||||
|
public void sendElicitationComplete(String sessionId, McpSchema.ElicitationCompleteNotification notification) {
|
||||||
|
this.asyncServer.sendElicitationComplete(sessionId, notification).block();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the server gracefully.
|
* Close the server gracefully.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 - 2024 the original author or authors.
|
* Copyright 2024 - 2026 the original author or authors.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol.spec;
|
package io.modelcontextprotocol.spec;
|
||||||
|
|
@ -20,6 +20,15 @@ public class McpError extends RuntimeException {
|
||||||
public static final Function<String, McpError> RESOURCE_NOT_FOUND = resourceUri -> new McpError(new JSONRPCError(
|
public static final Function<String, McpError> RESOURCE_NOT_FOUND = resourceUri -> new McpError(new JSONRPCError(
|
||||||
McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found", Map.of("uri", resourceUri)));
|
McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found", Map.of("uri", resourceUri)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-elicitation-required-error">URL
|
||||||
|
* Elicitation Required</a>
|
||||||
|
*/
|
||||||
|
public static final Function<java.util.List<McpSchema.ElicitUrlRequest>, McpError> URL_ELICITATION_REQUIRED = elicitations -> new McpError(
|
||||||
|
new JSONRPCError(McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED, "URL elicitation required",
|
||||||
|
Map.of("elicitations", elicitations)));
|
||||||
|
|
||||||
private JSONRPCError jsonRpcError;
|
private JSONRPCError jsonRpcError;
|
||||||
|
|
||||||
public McpError(JSONRPCError jsonRpcError) {
|
public McpError(JSONRPCError jsonRpcError) {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,6 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
@ -20,10 +17,11 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
|
||||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on the <a href="http://www.jsonrpc.org/specification">JSON-RPC 2.0
|
* Based on the <a href="http://www.jsonrpc.org/specification">JSON-RPC 2.0
|
||||||
|
|
@ -114,6 +112,8 @@ public final class McpSchema {
|
||||||
// Elicitation Methods
|
// Elicitation Methods
|
||||||
public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
|
public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
|
||||||
|
|
||||||
|
public static final String METHOD_NOTIFICATION_ELICITATION_COMPLETE = "notifications/elicitation/complete";
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// JSON-RPC Error Codes
|
// JSON-RPC Error Codes
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
@ -152,6 +152,11 @@ public final class McpSchema {
|
||||||
*/
|
*/
|
||||||
public static final int RESOURCE_NOT_FOUND = -32002;
|
public static final int RESOURCE_NOT_FOUND = -32002;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL elicitation is required before the request can proceed.
|
||||||
|
*/
|
||||||
|
public static final int URL_ELICITATION_REQUIRED = -32042;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3883,9 +3888,48 @@ public final class McpSchema {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Elicitation
|
// Elicitation
|
||||||
|
/**
|
||||||
|
* A request from the server to elicit additional information from the user, either
|
||||||
|
* through the client or out-of-band.
|
||||||
|
*
|
||||||
|
* @see ElicitFormRequest
|
||||||
|
* @see ElicitUrlRequest
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "mode",
|
||||||
|
defaultImpl = ElicitFormRequest.class)
|
||||||
|
@JsonSubTypes({ @JsonSubTypes.Type(value = ElicitFormRequest.class, name = ElicitFormRequest.MODE),
|
||||||
|
@JsonSubTypes.Type(value = ElicitUrlRequest.class, name = ElicitUrlRequest.MODE) })
|
||||||
|
public interface ElicitRequest extends Request {
|
||||||
|
|
||||||
|
String message();
|
||||||
|
|
||||||
|
Map<String, Object> meta();
|
||||||
|
|
||||||
|
String mode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
static ElicitFormRequest.Builder builder() {
|
||||||
|
return new ElicitFormRequest.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
static ElicitFormRequest.Builder builder(String message, Map<String, Object> requestedSchema) {
|
||||||
|
return new ElicitFormRequest.Builder(message, requestedSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request from the server to elicit additional information from the user via the
|
* A request from the server to elicit additional information from the user via the
|
||||||
* client.
|
* client, using {@code form} mode.
|
||||||
*
|
*
|
||||||
* @param message The message to present to the user
|
* @param message The message to present to the user
|
||||||
* @param requestedSchema A restricted subset of JSON Schema. Only top-level
|
* @param requestedSchema A restricted subset of JSON Schema. Only top-level
|
||||||
|
|
@ -3901,18 +3945,26 @@ public final class McpSchema {
|
||||||
*/
|
*/
|
||||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record ElicitRequest( // @formatter:off
|
public record ElicitFormRequest( // @formatter:off
|
||||||
@JsonProperty("message") String message,
|
@JsonProperty("message") String message,
|
||||||
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
|
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
|
||||||
@JsonProperty("_meta") Map<String, Object> meta) implements Request { // @formatter:on
|
@JsonProperty("_meta") Map<String, Object> meta) implements ElicitRequest { // @formatter:on
|
||||||
|
|
||||||
public ElicitRequest {
|
public static final String MODE = "form";
|
||||||
|
|
||||||
|
public ElicitFormRequest {
|
||||||
Assert.notNull(message, "message must not be null");
|
Assert.notNull(message, "message must not be null");
|
||||||
Assert.notNull(requestedSchema, "requestedSchema must not be null");
|
Assert.notNull(requestedSchema, "requestedSchema must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@JsonProperty("mode")
|
||||||
|
public String mode() {
|
||||||
|
return MODE;
|
||||||
|
}
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
static ElicitRequest fromJson(@JsonProperty("message") String message,
|
static ElicitFormRequest fromJson(@JsonProperty("message") String message,
|
||||||
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
|
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
|
||||||
@JsonProperty("_meta") Map<String, Object> meta) {
|
@JsonProperty("_meta") Map<String, Object> meta) {
|
||||||
if (message == null || requestedSchema == null) {
|
if (message == null || requestedSchema == null) {
|
||||||
|
|
@ -3925,23 +3977,10 @@ public final class McpSchema {
|
||||||
missing.add("requestedSchema -> {}");
|
missing.add("requestedSchema -> {}");
|
||||||
requestedSchema = Map.of();
|
requestedSchema = Map.of();
|
||||||
}
|
}
|
||||||
logger.warn("ElicitRequest: missing required fields during deserialization: {}",
|
logger.warn("ElicitFormRequest: missing required fields during deserialization: {}",
|
||||||
String.join(", ", missing));
|
String.join(", ", missing));
|
||||||
}
|
}
|
||||||
return new ElicitRequest(message, requestedSchema, meta);
|
return new ElicitFormRequest(message, requestedSchema, meta);
|
||||||
}
|
|
||||||
|
|
||||||
// backwards compatibility constructor
|
|
||||||
public ElicitRequest(String message, Map<String, Object> requestedSchema) {
|
|
||||||
this(message, requestedSchema, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link #builder(String, Map)} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder builder(String message, Map<String, Object> requestedSchema) {
|
public static Builder builder(String message, Map<String, Object> requestedSchema) {
|
||||||
|
|
@ -3957,11 +3996,11 @@ public final class McpSchema {
|
||||||
private Map<String, Object> meta;
|
private Map<String, Object> meta;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link ElicitRequest#builder(String, Map)} factory method
|
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
|
||||||
* instead.
|
* method instead.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public Builder() {
|
private Builder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Builder(String message, Map<String, Object> requestedSchema) {
|
private Builder(String message, Map<String, Object> requestedSchema) {
|
||||||
|
|
@ -3971,12 +4010,22 @@ public final class McpSchema {
|
||||||
this.requestedSchema = requestedSchema;
|
this.requestedSchema = requestedSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
|
||||||
|
* method instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public Builder message(String message) {
|
public Builder message(String message) {
|
||||||
Assert.notNull(message, "message must not be null");
|
Assert.notNull(message, "message must not be null");
|
||||||
this.message = message;
|
this.message = message;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ElicitFormRequest#builder(String, Map)} factory
|
||||||
|
* method instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public Builder requestedSchema(Map<String, Object> requestedSchema) {
|
public Builder requestedSchema(Map<String, Object> requestedSchema) {
|
||||||
Assert.notNull(requestedSchema, "requestedSchema must not be null");
|
Assert.notNull(requestedSchema, "requestedSchema must not be null");
|
||||||
this.requestedSchema = requestedSchema;
|
this.requestedSchema = requestedSchema;
|
||||||
|
|
@ -3996,10 +4045,114 @@ public final class McpSchema {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ElicitRequest build() {
|
public ElicitFormRequest build() {
|
||||||
Assert.notNull(message, "message must not be null");
|
Assert.notNull(message, "message must not be null");
|
||||||
Assert.notNull(requestedSchema, "requestedSchema must not be null");
|
Assert.notNull(requestedSchema, "requestedSchema must not be null");
|
||||||
return new ElicitRequest(message, requestedSchema, meta);
|
return new ElicitFormRequest(message, requestedSchema, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request from the server to elicit additional information from the user out of
|
||||||
|
* band, using {@code url} mode.
|
||||||
|
*
|
||||||
|
* @param message The message to present to the user
|
||||||
|
* @param url The URL the user must navigate to.
|
||||||
|
* @param elicitationId The elicitation ID of the elicitations reques.t
|
||||||
|
* @param meta See specification for notes on _meta usage
|
||||||
|
* <p>
|
||||||
|
* Note: {@code message}, {@code url} and {@code elicitationId} are required by the
|
||||||
|
* MCP specification. Deserialization accepts missing values and substitutes defaults
|
||||||
|
* to avoid breaking existing integrations that may omit these fields.
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ElicitUrlRequest( // @formatter:off
|
||||||
|
@JsonProperty("message") String message,
|
||||||
|
@JsonProperty("url") String url,
|
||||||
|
@JsonProperty("elicitationId") String elicitationId,
|
||||||
|
@JsonProperty("_meta") Map<String, Object> meta) implements ElicitRequest { // @formatter:on
|
||||||
|
|
||||||
|
public static final String MODE = "url";
|
||||||
|
|
||||||
|
public ElicitUrlRequest {
|
||||||
|
Assert.notNull(message, "message must not be null");
|
||||||
|
Assert.notNull(url, "url must not be null");
|
||||||
|
Assert.notNull(elicitationId, "elicitationId must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@JsonProperty("mode")
|
||||||
|
public String mode() {
|
||||||
|
return MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
static ElicitUrlRequest fromJson(@JsonProperty("message") String message, @JsonProperty("url") String url,
|
||||||
|
@JsonProperty("elicitationId") String elicitationId, @JsonProperty("_meta") Map<String, Object> meta) {
|
||||||
|
if (message == null || url == null || elicitationId == null) {
|
||||||
|
List<String> missing = new ArrayList<>();
|
||||||
|
if (message == null) {
|
||||||
|
missing.add("message -> ''");
|
||||||
|
message = "";
|
||||||
|
}
|
||||||
|
if (url == null) {
|
||||||
|
missing.add("url -> ''");
|
||||||
|
url = "";
|
||||||
|
}
|
||||||
|
if (elicitationId == null) {
|
||||||
|
missing.add("elicitationId -> ''");
|
||||||
|
elicitationId = "";
|
||||||
|
}
|
||||||
|
logger.warn("ElicitUrlRequest: missing required fields during deserialization: {}",
|
||||||
|
String.join(", ", missing));
|
||||||
|
}
|
||||||
|
return new ElicitUrlRequest(message, url, elicitationId, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder(String message, String url, String elicitationId) {
|
||||||
|
return new Builder(message, url, elicitationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
private final String elicitationId;
|
||||||
|
|
||||||
|
private Map<String, Object> meta;
|
||||||
|
|
||||||
|
private Builder(String message, String url, String elicitationId) {
|
||||||
|
Assert.notNull(message, "message must not be null");
|
||||||
|
Assert.notNull(url, "url must not be null");
|
||||||
|
Assert.notNull(elicitationId, "elicitationId must not be null");
|
||||||
|
this.message = message;
|
||||||
|
this.url = url;
|
||||||
|
this.elicitationId = elicitationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder meta(Map<String, Object> meta) {
|
||||||
|
this.meta = meta;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder progressToken(Object progressToken) {
|
||||||
|
if (this.meta == null) {
|
||||||
|
this.meta = new HashMap<>();
|
||||||
|
}
|
||||||
|
this.meta.put("progressToken", progressToken);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ElicitUrlRequest build() {
|
||||||
|
Assert.notNull(message, "message must not be null");
|
||||||
|
Assert.notNull(url, "url must not be null");
|
||||||
|
Assert.notNull(elicitationId, "elicitationId must not be null");
|
||||||
|
return new ElicitUrlRequest(message, url, elicitationId, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -4103,6 +4256,39 @@ public final class McpSchema {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A notification from the server to the client indicating that an out-of-band URL
|
||||||
|
* elicitation interaction has completed.
|
||||||
|
*
|
||||||
|
* @param elicitationId The unique identifier of the completed elicitation
|
||||||
|
* @param meta See specification for notes on _meta usage
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ElicitationCompleteNotification( // @formatter:off
|
||||||
|
@JsonProperty("elicitationId") String elicitationId,
|
||||||
|
@JsonProperty("_meta") Map<String, Object> meta) implements Notification { // @formatter:on
|
||||||
|
|
||||||
|
public ElicitationCompleteNotification {
|
||||||
|
Assert.notNull(elicitationId, "elicitationId must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
static ElicitationCompleteNotification fromJson(@JsonProperty("elicitationId") String elicitationId,
|
||||||
|
@JsonProperty("_meta") Map<String, Object> meta) {
|
||||||
|
if (elicitationId == null || elicitationId.isBlank()) {
|
||||||
|
logger.warn(
|
||||||
|
"ElicitationCompleteNotification: missing required field 'elicitationId' during deserialization, using default ''");
|
||||||
|
elicitationId = "";
|
||||||
|
}
|
||||||
|
return new ElicitationCompleteNotification(elicitationId, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ElicitationCompleteNotification(String elicitationId) {
|
||||||
|
this(elicitationId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Pagination Interfaces
|
// Pagination Interfaces
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
|
||||||
|
|
@ -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;
|
package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpServerSession;
|
import io.modelcontextprotocol.spec.McpServerSession;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
|
@ -531,6 +531,149 @@ class McpAsyncServerExchangeTests {
|
||||||
any(TypeRef.class));
|
any(TypeRef.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationWithUrlRequest() {
|
||||||
|
McpSchema.ClientCapabilities capabilitiesWithUrlElicitation = McpSchema.ClientCapabilities.builder()
|
||||||
|
.elicitation(false, true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpAsyncServerExchange exchangeWithElicitation = new McpAsyncServerExchange("testSessionId", mockSession,
|
||||||
|
capabilitiesWithUrlElicitation, clientInfo, McpTransportContext.EMPTY);
|
||||||
|
|
||||||
|
McpSchema.ElicitUrlRequest elicitUrlRequest = McpSchema.ElicitUrlRequest
|
||||||
|
.builder("Please authenticate via URL", "https://example.com/auth", "elicit-url-123")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitUrlRequest), any(TypeRef.class)))
|
||||||
|
.thenReturn(Mono.just(expectedResult));
|
||||||
|
|
||||||
|
StepVerifier.create(exchangeWithElicitation.createElicitation(elicitUrlRequest)).assertNext(result -> {
|
||||||
|
assertThat(result).isEqualTo(expectedResult);
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
}).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationWithUrlRequestBypassesValidator() {
|
||||||
|
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
|
||||||
|
.elicitation(false, true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
JsonSchemaValidator rejectingValidator = new JsonSchemaValidator() {
|
||||||
|
@Override
|
||||||
|
public ValidationResponse validate(Map<String, Object> schema, Object content) {
|
||||||
|
return ValidationResponse.asInvalid("should not be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResponse validateSchema(Map<String, Object> schema) {
|
||||||
|
return ValidationResponse.asInvalid("should not be called");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
McpAsyncServerExchange exchangeWithValidator = new McpAsyncServerExchange("testSessionId", mockSession,
|
||||||
|
capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY, rejectingValidator);
|
||||||
|
|
||||||
|
McpSchema.ElicitUrlRequest elicitUrlRequest = McpSchema.ElicitUrlRequest
|
||||||
|
.builder("Please visit the URL", "https://example.com/oauth", "elicit-oauth-123")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitUrlRequest), any(TypeRef.class)))
|
||||||
|
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
|
||||||
|
|
||||||
|
StepVerifier.create(exchangeWithValidator.createElicitation(elicitUrlRequest)).assertNext(result -> {
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitUrlRequest),
|
||||||
|
any(TypeRef.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCapabilitiesEmptyObject() {
|
||||||
|
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
|
||||||
|
McpAsyncServerExchange exchangeEmpty = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
|
||||||
|
clientInfo, McpTransportContext.EMPTY);
|
||||||
|
|
||||||
|
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
|
||||||
|
.build();
|
||||||
|
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
|
||||||
|
|
||||||
|
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(formRequest), any(TypeRef.class)))
|
||||||
|
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
|
||||||
|
|
||||||
|
StepVerifier.create(exchangeEmpty.createElicitation(formRequest)).expectNextCount(1).verifyComplete();
|
||||||
|
StepVerifier.create(exchangeEmpty.createElicitation(urlRequest))
|
||||||
|
.verifyErrorSatisfies(e -> assertThat(e).isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessage("Client must be configured with URL elicitation capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCapabilitiesFormOnly() {
|
||||||
|
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
|
||||||
|
.elicitation(true, false)
|
||||||
|
.build();
|
||||||
|
McpAsyncServerExchange exchangeForm = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
|
||||||
|
clientInfo, McpTransportContext.EMPTY);
|
||||||
|
|
||||||
|
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
|
||||||
|
.build();
|
||||||
|
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
|
||||||
|
|
||||||
|
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(formRequest), any(TypeRef.class)))
|
||||||
|
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
|
||||||
|
|
||||||
|
StepVerifier.create(exchangeForm.createElicitation(formRequest)).expectNextCount(1).verifyComplete();
|
||||||
|
StepVerifier.create(exchangeForm.createElicitation(urlRequest))
|
||||||
|
.verifyErrorSatisfies(e -> assertThat(e).isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessage("Client must be configured with URL elicitation capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCapabilitiesUrlOnly() {
|
||||||
|
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
|
||||||
|
.elicitation(false, true)
|
||||||
|
.build();
|
||||||
|
McpAsyncServerExchange exchangeUrl = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
|
||||||
|
clientInfo, McpTransportContext.EMPTY);
|
||||||
|
|
||||||
|
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
|
||||||
|
.build();
|
||||||
|
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
|
||||||
|
|
||||||
|
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(urlRequest), any(TypeRef.class)))
|
||||||
|
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
|
||||||
|
|
||||||
|
StepVerifier.create(exchangeUrl.createElicitation(urlRequest)).expectNextCount(1).verifyComplete();
|
||||||
|
StepVerifier.create(exchangeUrl.createElicitation(formRequest))
|
||||||
|
.verifyErrorSatisfies(e -> assertThat(e).isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessage("Client must be configured with form elicitation capabilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCapabilitiesBoth() {
|
||||||
|
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
|
||||||
|
.elicitation(true, true)
|
||||||
|
.build();
|
||||||
|
McpAsyncServerExchange exchangeBoth = new McpAsyncServerExchange("testSessionId", mockSession, capabilities,
|
||||||
|
clientInfo, McpTransportContext.EMPTY);
|
||||||
|
|
||||||
|
McpSchema.ElicitFormRequest formRequest = McpSchema.ElicitRequest.builder("form", Map.of("type", "object"))
|
||||||
|
.build();
|
||||||
|
McpSchema.ElicitUrlRequest urlRequest = McpSchema.ElicitUrlRequest.builder("url", "http", "123").build();
|
||||||
|
|
||||||
|
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(formRequest), any(TypeRef.class)))
|
||||||
|
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
|
||||||
|
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(urlRequest), any(TypeRef.class)))
|
||||||
|
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
|
||||||
|
|
||||||
|
StepVerifier.create(exchangeBoth.createElicitation(formRequest)).expectNextCount(1).verifyComplete();
|
||||||
|
StepVerifier.create(exchangeBoth.createElicitation(urlRequest)).expectNextCount(1).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Create Message Tests
|
// Create Message Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol;
|
package io.modelcontextprotocol;
|
||||||
|
|
||||||
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
|
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
|
|
@ -21,6 +19,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
@ -39,7 +38,7 @@ import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
|
import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
|
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
|
import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
|
||||||
|
|
@ -51,7 +50,6 @@ import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||||
import io.modelcontextprotocol.util.McpJsonMapperUtils;
|
|
||||||
import io.modelcontextprotocol.util.Utils;
|
import io.modelcontextprotocol.util.Utils;
|
||||||
import net.javacrumbs.jsonunit.core.Option;
|
import net.javacrumbs.jsonunit.core.Option;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
|
@ -59,11 +57,16 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
|
||||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
||||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
|
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.assertj.core.api.Assertions.assertWith;
|
import static org.assertj.core.api.Assertions.assertWith;
|
||||||
|
import static org.assertj.core.api.InstanceOfAssertFactories.LIST;
|
||||||
|
import static org.assertj.core.api.InstanceOfAssertFactories.MAP;
|
||||||
|
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||||
import static org.awaitility.Awaitility.await;
|
import static org.awaitility.Awaitility.await;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
|
@ -381,7 +384,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||||
.callHandler((exchange, request) -> exchange.createElicitation(mock(ElicitRequest.class))
|
.callHandler((exchange, request) -> exchange.createElicitation(mock(McpSchema.ElicitFormRequest.class))
|
||||||
.then(Mono.just(mock(CallToolResult.class))))
|
.then(Mono.just(mock(CallToolResult.class))))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -412,7 +415,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> formElicitationHandler = request -> {
|
||||||
assertThat(request.message()).isNotEmpty();
|
assertThat(request.message()).isNotEmpty();
|
||||||
assertThat(request.requestedSchema()).isNotNull();
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -431,7 +434,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
var elicitationRequest = McpSchema.ElicitRequest
|
var elicitationRequest = McpSchema.ElicitFormRequest
|
||||||
.builder("Test message",
|
.builder("Test message",
|
||||||
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -447,7 +450,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
try (var mcpClient = clientBuilder
|
try (var mcpClient = clientBuilder
|
||||||
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
|
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
|
||||||
.capabilities(ClientCapabilities.builder().elicitation().build())
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
.elicitation(elicitationHandler)
|
.elicitation(formElicitationHandler)
|
||||||
.build()) {
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
|
@ -476,7 +479,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
// Client handler returns empty content — SDK should apply defaults
|
// Client handler returns empty content — SDK should apply defaults
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
|
||||||
assertThat(request.message()).isNotEmpty();
|
assertThat(request.message()).isNotEmpty();
|
||||||
assertThat(request.requestedSchema()).isNotNull();
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
|
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
|
||||||
|
|
@ -491,15 +494,13 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences",
|
||||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
Map.of("type", "object", "properties",
|
||||||
.message("Provide your preferences")
|
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
||||||
.requestedSchema(Map.of("type", "object", "properties",
|
Map.of("type", "integer", "default", 18), "subscribe",
|
||||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
Map.of("type", "boolean", "default", true), "color",
|
||||||
Map.of("type", "integer", "default", 18), "subscribe",
|
Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")),
|
||||||
Map.of("type", "boolean", "default", true), "color",
|
"required", List.of("nickname", "age", "subscribe", "color")))
|
||||||
Map.of("type", "string", "enum", List.of("red", "green"), "default", "green")),
|
|
||||||
"required", List.of("nickname", "age", "subscribe", "color")))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return exchange.createElicitation(elicitationRequest)
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
|
@ -544,7 +545,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
// Client handler returns an unmodifiable map (Map.of()) — SDK must copy into a
|
// Client handler returns an unmodifiable map (Map.of()) — SDK must copy into a
|
||||||
// mutable map before applying defaults.
|
// mutable map before applying defaults.
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
McpSchema.ElicitResult.Action.ACCEPT, Map.of());
|
McpSchema.ElicitResult.Action.ACCEPT, Map.of());
|
||||||
|
|
||||||
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
|
@ -557,9 +558,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
var elicitationRequest = McpSchema.ElicitFormRequest
|
||||||
.message("Provide your preferences")
|
.builder("Provide your preferences", Map.of("type", "object", "properties",
|
||||||
.requestedSchema(Map.of("type", "object", "properties",
|
|
||||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
Map.of("nickname", Map.of("type", "string", "default", "Guest"), "age",
|
||||||
Map.of("type", "integer", "default", 18)),
|
Map.of("type", "integer", "default", 18)),
|
||||||
"required", List.of("nickname", "age")))
|
"required", List.of("nickname", "age")))
|
||||||
|
|
@ -603,7 +603,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
|
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>());
|
||||||
|
|
||||||
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
|
@ -616,10 +616,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
|
||||||
.message("Provide your preferences")
|
"object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||||
.requestedSchema(Map.of("type", "object", "properties",
|
|
||||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return exchange.createElicitation(elicitationRequest)
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
|
@ -659,7 +657,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
McpSchema.ElicitResult.Action.DECLINE, new HashMap<>());
|
McpSchema.ElicitResult.Action.DECLINE, new HashMap<>());
|
||||||
|
|
||||||
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
|
@ -672,10 +670,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
|
||||||
.message("Provide your preferences")
|
"object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||||
.requestedSchema(Map.of("type", "object", "properties",
|
|
||||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return exchange.createElicitation(elicitationRequest)
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
|
@ -716,7 +712,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
Map<String, Object> meta = Map.of("trace-id", "abc-123");
|
Map<String, Object> meta = Map.of("trace-id", "abc-123");
|
||||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
Function<McpSchema.ElicitFormRequest, McpSchema.ElicitResult> elicitationHandler = request -> new McpSchema.ElicitResult(
|
||||||
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>(), meta);
|
McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>(), meta);
|
||||||
|
|
||||||
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
|
@ -729,10 +725,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
var elicitationRequest = McpSchema.ElicitFormRequest.builder("Provide your preferences", Map.of("type",
|
||||||
.message("Provide your preferences")
|
"object", "properties", Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
||||||
.requestedSchema(Map.of("type", "object", "properties",
|
|
||||||
Map.of("nickname", Map.of("type", "string", "default", "Guest"))))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return exchange.createElicitation(elicitationRequest)
|
return exchange.createElicitation(elicitationRequest)
|
||||||
|
|
@ -773,9 +767,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
Function<ElicitFormRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
assertThat(request.message()).isNotEmpty();
|
assertThat(request.message()).isNotEmpty();
|
||||||
assertThat(request.requestedSchema()).isNotNull();
|
assertThat(((McpSchema.ElicitFormRequest) request).requestedSchema()).isNotNull();
|
||||||
return ElicitResult.builder(ElicitResult.Action.ACCEPT)
|
return ElicitResult.builder(ElicitResult.Action.ACCEPT)
|
||||||
.content(Map.of("message", request.message()))
|
.content(Map.of("message", request.message()))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -791,7 +785,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
var elicitationRequest = McpSchema.ElicitRequest
|
var elicitationRequest = McpSchema.ElicitFormRequest
|
||||||
.builder("Test message",
|
.builder("Test message",
|
||||||
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -840,7 +834,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
Function<ElicitFormRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
assertThat(request.message()).isNotEmpty();
|
assertThat(request.message()).isNotEmpty();
|
||||||
assertThat(request.requestedSchema()).isNotNull();
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -867,7 +861,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
|
|
||||||
var elicitationRequest = ElicitRequest
|
var elicitationRequest = ElicitFormRequest
|
||||||
.builder("Test message",
|
.builder("Test message",
|
||||||
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -904,6 +898,165 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testCreateUrlElicitationSuccess(String clientType) {
|
||||||
|
var elicitationRequest = McpSchema.ElicitUrlRequest
|
||||||
|
.builder("Test message", "https://example.com/auth", "elicitation-123")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitUrlRequest, McpSchema.ElicitResult> urlElicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isEqualTo("Test message");
|
||||||
|
assertThat(request.url()).isEqualTo("https://example.com/auth");
|
||||||
|
assertThat(request.elicitationId()).isEqualTo("elicitation-123");
|
||||||
|
|
||||||
|
return McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build();
|
||||||
|
};
|
||||||
|
|
||||||
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
.addContent(McpSchema.TextContent.builder("CALL RESPONSE").build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AtomicReference<McpSchema.ElicitResult> elicitResultRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||||
|
.callHandler((exchange, request) -> exchange.createElicitation(elicitationRequest)
|
||||||
|
.doOnNext(elicitResultRef::set)
|
||||||
|
.thenReturn(callResponse))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder
|
||||||
|
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation(false, true).build())
|
||||||
|
.urlElicitation(urlElicitationHandler)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient
|
||||||
|
.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build());
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
var elicitResult = elicitResultRef.get();
|
||||||
|
assertThat(elicitResult).isNotNull();
|
||||||
|
assertThat(elicitResult.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testElicitationCompleteNotification(String clientType) throws InterruptedException {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
CountDownLatch notificationLatch = new CountDownLatch(1);
|
||||||
|
AtomicReference<McpSchema.ElicitationCompleteNotification> notificationRef = new AtomicReference<>();
|
||||||
|
AtomicReference<String> sessionId = new AtomicReference<>();
|
||||||
|
|
||||||
|
Consumer<McpSchema.ElicitationCompleteNotification> elicitationCompleteConsumer = notification -> {
|
||||||
|
notificationRef.set(notification);
|
||||||
|
notificationLatch.countDown();
|
||||||
|
};
|
||||||
|
|
||||||
|
CallToolResult callResponse = McpSchema.CallToolResult.builder()
|
||||||
|
.addContent(McpSchema.TextContent.builder("CALL RESPONSE").build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Capture the session ID so we can trigger an "elicitation complete" notification
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
sessionId.set(exchange.sessionId());
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder
|
||||||
|
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
|
||||||
|
.elicitationCompleteConsumer(elicitationCompleteConsumer)
|
||||||
|
// enable elicitation so that we can register an elicitation complete consumer
|
||||||
|
.urlElicitation(request -> McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build())
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
var response = mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build());
|
||||||
|
var capturedSessionId = sessionId.get();
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(capturedSessionId).isNotNull();
|
||||||
|
mcpServer
|
||||||
|
.sendElicitationComplete(capturedSessionId,
|
||||||
|
new McpSchema.ElicitationCompleteNotification("elicitation-123"))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
assertThat(notificationLatch.await(5, TimeUnit.SECONDS)).isTrue();
|
||||||
|
var notification = notificationRef.get();
|
||||||
|
assertThat(notification).isNotNull();
|
||||||
|
assertThat(notification.elicitationId()).isEqualTo("elicitation-123");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@MethodSource("clientsForTesting")
|
||||||
|
void testElicitationRequiredError(String clientType) throws InterruptedException {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
// Capture the session ID so we can trigger an "elicitation complete" notification
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
|
||||||
|
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
return Mono.error(McpError.URL_ELICITATION_REQUIRED.apply(List
|
||||||
|
.of(McpSchema.ElicitUrlRequest.builder("do the thing", "https://example.com", "elicitation-1234")
|
||||||
|
.build())));
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitUrlRequest, ElicitResult> elicitationHandler = request -> ElicitResult
|
||||||
|
.builder(ElicitResult.Action.ACCEPT)
|
||||||
|
.build();
|
||||||
|
try (var mcpClient = clientBuilder
|
||||||
|
.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build())
|
||||||
|
.urlElicitation(elicitationHandler)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
assertThatThrownBy(
|
||||||
|
() -> mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build()))
|
||||||
|
.isInstanceOf(McpError.class)
|
||||||
|
.extracting("jsonRpcError")
|
||||||
|
.asInstanceOf(type(McpSchema.JSONRPCResponse.JSONRPCError.class))
|
||||||
|
.satisfies(error -> {
|
||||||
|
assertThat(error.code()).isEqualTo(McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED);
|
||||||
|
assertThat(error.data()).asInstanceOf(MAP)
|
||||||
|
.hasSize(1)
|
||||||
|
.extracting("elicitations")
|
||||||
|
.asInstanceOf(LIST)
|
||||||
|
.hasSize(1)
|
||||||
|
.first()
|
||||||
|
.asInstanceOf(MAP)
|
||||||
|
.containsEntry("mode", "url")
|
||||||
|
.containsEntry("message", "do the thing")
|
||||||
|
.containsEntry("url", "https://example.com")
|
||||||
|
.containsEntry("elicitationId", "elicitation-1234");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Roots Tests
|
// Roots Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
@ -685,11 +686,13 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||||
.just(CreateMessageResult.builder(McpSchema.Role.ASSISTANT, "test", "test-model").build());
|
.just(CreateMessageResult.builder(McpSchema.Role.ASSISTANT, "test", "test-model").build());
|
||||||
|
|
||||||
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler = request -> Mono
|
Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler = request -> Mono
|
||||||
.just(ElicitResult.builder(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build());
|
.just(ElicitResult.builder(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build());
|
||||||
|
|
||||||
withClient(createMcpTransport(),
|
withClient(createMcpTransport(),
|
||||||
builder -> builder.capabilities(capabilities).sampling(samplingHandler).elicitation(elicitationHandler),
|
builder -> builder.capabilities(capabilities)
|
||||||
|
.sampling(samplingHandler)
|
||||||
|
.elicitation(formElicitationHandler),
|
||||||
client ->
|
client ->
|
||||||
|
|
||||||
StepVerifier.create(client.initialize()).assertNext(result -> {
|
StepVerifier.create(client.initialize()).assertNext(result -> {
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,7 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
MockMcpClientTransport transport = initializationEnabledTransport();
|
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||||
|
|
||||||
// Create a test elicitation handler that echoes back the input
|
// Create a test elicitation handler that echoes back the input
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> {
|
Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> {
|
||||||
assertThat(request.message()).isNotEmpty();
|
assertThat(request.message()).isNotEmpty();
|
||||||
assertThat(request.requestedSchema()).isInstanceOf(Map.class);
|
assertThat(request.requestedSchema()).isInstanceOf(Map.class);
|
||||||
assertThat(request.requestedSchema().get("type")).isEqualTo("object");
|
assertThat(request.requestedSchema().get("type")).isEqualTo("object");
|
||||||
|
|
@ -458,7 +458,7 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
MockMcpClientTransport transport = initializationEnabledTransport();
|
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||||
|
|
||||||
// Create a test elicitation handler to decline the request
|
// Create a test elicitation handler to decline the request
|
||||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> Mono
|
Function<McpSchema.ElicitFormRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> Mono
|
||||||
.just(McpSchema.ElicitResult.builder(action).build());
|
.just(McpSchema.ElicitResult.builder(action).build());
|
||||||
|
|
||||||
// Create client with elicitation capability and handler
|
// Create client with elicitation capability and handler
|
||||||
|
|
@ -534,17 +534,6 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
asyncMcpClient.closeGracefully();
|
asyncMcpClient.closeGracefully();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testElicitationCreateRequestHandlingWithNullHandler() {
|
|
||||||
MockMcpClientTransport transport = new MockMcpClientTransport();
|
|
||||||
|
|
||||||
// Create client with elicitation capability but null handler
|
|
||||||
assertThatThrownBy(() -> McpClient.async(transport)
|
|
||||||
.capabilities(ClientCapabilities.builder().elicitation().build())
|
|
||||||
.build()).isInstanceOf(IllegalArgumentException.class)
|
|
||||||
.hasMessage("Elicitation handler must not be null when client capabilities include elicitation");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPingMessageRequestHandling() {
|
void testPingMessageRequestHandling() {
|
||||||
MockMcpClientTransport transport = initializationEnabledTransport();
|
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
package io.modelcontextprotocol.spec;
|
||||||
|
|
||||||
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
|
|
||||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
|
||||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
@ -17,12 +11,17 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
||||||
|
import net.javacrumbs.jsonunit.core.Option;
|
||||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
|
||||||
import net.javacrumbs.jsonunit.core.Option;
|
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
||||||
|
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
|
@ -1557,7 +1556,7 @@ public class McpSchemaTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateElicitationRequest() throws Exception {
|
void testCreateElicitationRequest() throws Exception {
|
||||||
McpSchema.ElicitRequest request = McpSchema.ElicitRequest
|
McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest
|
||||||
.builder("Please provide additional information", Map.of("type", "object", "required", List.of("a"),
|
.builder("Please provide additional information", Map.of("type", "object", "required", List.of("a"),
|
||||||
"properties", Map.of("foo", Map.of("type", "string"))))
|
"properties", Map.of("foo", Map.of("type", "string"))))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -1567,9 +1566,43 @@ public class McpSchemaTests {
|
||||||
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
.isObject()
|
.isObject()
|
||||||
.isEqualTo(
|
.isEqualTo(json("""
|
||||||
json("""
|
{
|
||||||
{"message":"Please provide additional information","requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}"""));
|
"mode": "form",
|
||||||
|
"message": "Please provide additional information",
|
||||||
|
"requestedSchema": {
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"a"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}"""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationUrlRequest() throws Exception {
|
||||||
|
McpSchema.ElicitRequest request = McpSchema.ElicitUrlRequest
|
||||||
|
.builder("Please visit the URL", "https://example.com/oauth", "elicit-oauth-123")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String value = JSON_MAPPER.writeValueAsString(request);
|
||||||
|
|
||||||
|
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isObject()
|
||||||
|
.isEqualTo(json("""
|
||||||
|
{
|
||||||
|
"mode": "url",
|
||||||
|
"message": "Please visit the URL",
|
||||||
|
"url": "https://example.com/oauth",
|
||||||
|
"elicitationId": "elicit-oauth-123"
|
||||||
|
}
|
||||||
|
"""));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -1588,12 +1621,57 @@ public class McpSchemaTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception {
|
void testElicitRequestDeserializationDefaultsToForm() throws Exception {
|
||||||
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{}", McpSchema.ElicitRequest.class);
|
var request = JSON_MAPPER.readValue("{\"message\":\"do the thing\"}", McpSchema.ElicitRequest.class);
|
||||||
|
|
||||||
assertThat(request).isNotNull();
|
assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitFormRequest.class);
|
||||||
|
assertThat(request.message()).isEqualTo("do the thing");
|
||||||
|
assertThat(request.mode()).isEqualTo("form");
|
||||||
|
var formRequest = (McpSchema.ElicitFormRequest) request;
|
||||||
|
assertThat(formRequest.requestedSchema()).isEmpty();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception {
|
||||||
|
var request = JSON_MAPPER.readValue("{\"mode\":\"form\"}", McpSchema.ElicitRequest.class);
|
||||||
|
|
||||||
|
assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitFormRequest.class);
|
||||||
assertThat(request.message()).isEmpty();
|
assertThat(request.message()).isEmpty();
|
||||||
assertThat(request.requestedSchema()).isEmpty();
|
assertThat(request.mode()).isEqualTo("form");
|
||||||
|
var formRequest = (McpSchema.ElicitFormRequest) request;
|
||||||
|
assertThat(formRequest.requestedSchema()).isEmpty();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitUrlRequestDeserializationWithMissingRequiredFields() throws Exception {
|
||||||
|
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{\"mode\":\"url\"}", McpSchema.ElicitRequest.class);
|
||||||
|
assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitUrlRequest.class);
|
||||||
|
assertThat(request.message()).isEmpty();
|
||||||
|
assertThat(request.mode()).isEqualTo("url");
|
||||||
|
var urlRequest = (McpSchema.ElicitUrlRequest) request;
|
||||||
|
assertThat(urlRequest.url()).isEmpty();
|
||||||
|
assertThat(urlRequest.elicitationId()).isEmpty();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitUrlDeserialization() throws Exception {
|
||||||
|
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("""
|
||||||
|
{
|
||||||
|
"mode": "url",
|
||||||
|
"message": "Please visit the URL",
|
||||||
|
"url": "https://example.com/oauth",
|
||||||
|
"elicitationId": "elicit-oauth-123"
|
||||||
|
}
|
||||||
|
""", McpSchema.ElicitRequest.class);
|
||||||
|
assertThat(request).isNotNull().isInstanceOf(McpSchema.ElicitUrlRequest.class);
|
||||||
|
assertThat(request.message()).isEqualTo("Please visit the URL");
|
||||||
|
assertThat(request.mode()).isEqualTo("url");
|
||||||
|
var urlRequest = (McpSchema.ElicitUrlRequest) request;
|
||||||
|
assertThat(urlRequest.url()).isEqualTo("https://example.com/oauth");
|
||||||
|
assertThat(urlRequest.elicitationId()).isEqualTo("elicit-oauth-123");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -1604,7 +1682,8 @@ public class McpSchemaTests {
|
||||||
Map<String, Object> meta = new HashMap<>();
|
Map<String, Object> meta = new HashMap<>();
|
||||||
meta.put("progressToken", "elicit-token-789");
|
meta.put("progressToken", "elicit-token-789");
|
||||||
|
|
||||||
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder("Please provide your name", requestedSchema)
|
McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest
|
||||||
|
.builder("Please provide your name", requestedSchema)
|
||||||
.meta(meta)
|
.meta(meta)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -1612,7 +1691,8 @@ public class McpSchemaTests {
|
||||||
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
.isObject()
|
.isObject()
|
||||||
.containsEntry("_meta", Map.of("progressToken", "elicit-token-789"));
|
.containsEntry("_meta", Map.of("progressToken", "elicit-token-789"))
|
||||||
|
.containsEntry("mode", "form");
|
||||||
|
|
||||||
// Test Request interface methods
|
// Test Request interface methods
|
||||||
assertThat(request.meta()).isEqualTo(meta);
|
assertThat(request.meta()).isEqualTo(meta);
|
||||||
|
|
@ -1627,16 +1707,25 @@ public class McpSchemaTests {
|
||||||
requestedSchema.put("properties", Map.of("name", Map.of("type", "string")));
|
requestedSchema.put("properties", Map.of("name", Map.of("type", "string")));
|
||||||
requestedSchema.put("required", List.of("name"));
|
requestedSchema.put("required", List.of("name"));
|
||||||
|
|
||||||
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder("Please provide name", requestedSchema)
|
McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest.builder("Please provide name", requestedSchema)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
String json = JSON_MAPPER.writeValueAsString(request);
|
String json = JSON_MAPPER.writeValueAsString(request);
|
||||||
assertThatJson(json).inPath("$.requestedSchema.$schema").isEqualTo(McpSchema.JSON_SCHEMA_DIALECT_2020_12);
|
assertThatJson(json).inPath("$.requestedSchema.$schema").isEqualTo(McpSchema.JSON_SCHEMA_DIALECT_2020_12);
|
||||||
|
|
||||||
McpSchema.ElicitRequest parsed = JSON_MAPPER.readValue(json, McpSchema.ElicitRequest.class);
|
McpSchema.ElicitFormRequest parsed = (McpSchema.ElicitFormRequest) JSON_MAPPER.readValue(json,
|
||||||
|
McpSchema.ElicitRequest.class);
|
||||||
assertThat(parsed.requestedSchema()).containsEntry("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
|
assertThat(parsed.requestedSchema()).containsEntry("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitRequestToleratesUnknownFields() throws Exception {
|
||||||
|
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("""
|
||||||
|
{"message":"hello","requestedSchema":{"type":"object"},"futureField":42}""",
|
||||||
|
McpSchema.ElicitRequest.class);
|
||||||
|
assertThat(request.message()).isEqualTo("hello");
|
||||||
|
}
|
||||||
|
|
||||||
// Pagination Tests
|
// Pagination Tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -1889,15 +1978,13 @@ public class McpSchemaTests {
|
||||||
@Test
|
@Test
|
||||||
void testElicitRequestWithDefaultValues() throws Exception {
|
void testElicitRequestWithDefaultValues() throws Exception {
|
||||||
// Test that schemas with default values serialize correctly in an ElicitRequest
|
// Test that schemas with default values serialize correctly in an ElicitRequest
|
||||||
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder()
|
McpSchema.ElicitRequest request = McpSchema.ElicitFormRequest.builder("Please provide your info", Map.of("type",
|
||||||
.message("Please provide your info")
|
"object", "properties",
|
||||||
.requestedSchema(Map.of("type", "object", "properties",
|
Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
|
||||||
Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
|
Map.of("type", "integer", "default", 30), "score", Map.of("type", "number", "default", 95.5),
|
||||||
Map.of("type", "integer", "default", 30), "score",
|
"status", Map.of("type", "string", "enum", List.of("active", "inactive"), "default", "active"),
|
||||||
Map.of("type", "number", "default", 95.5), "status",
|
"verified", Map.of("type", "boolean", "default", true)),
|
||||||
Map.of("type", "string", "enum", List.of("active", "inactive"), "default", "active"),
|
"required", List.of("name")))
|
||||||
"verified", Map.of("type", "boolean", "default", true)),
|
|
||||||
"required", List.of("name")))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
String value = JSON_MAPPER.writeValueAsString(request);
|
String value = JSON_MAPPER.writeValueAsString(request);
|
||||||
|
|
@ -1909,6 +1996,41 @@ public class McpSchemaTests {
|
||||||
assertThatJson(value).node("requestedSchema.properties.verified.default").isEqualTo(true);
|
assertThatJson(value).node("requestedSchema.properties.verified.default").isEqualTo(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Elicitation Complete Notification Tests (SEP-1036)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCompleteNotification() throws Exception {
|
||||||
|
McpSchema.ElicitationCompleteNotification notification = new McpSchema.ElicitationCompleteNotification(
|
||||||
|
"elicit-789");
|
||||||
|
|
||||||
|
String json = JSON_MAPPER.writeValueAsString(notification);
|
||||||
|
assertThatJson(json).isObject().containsEntry("elicitationId", "elicit-789");
|
||||||
|
|
||||||
|
McpSchema.ElicitationCompleteNotification deserialized = JSON_MAPPER.readValue(json,
|
||||||
|
McpSchema.ElicitationCompleteNotification.class);
|
||||||
|
assertThat(deserialized.elicitationId()).isEqualTo("elicit-789");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCompleteNotificationNullElicitationIdThrows() {
|
||||||
|
assertThatThrownBy(() -> new McpSchema.ElicitationCompleteNotification(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCompleteNotificationDeserializesWithoutElicitationId() throws Exception {
|
||||||
|
McpSchema.ElicitationCompleteNotification notification = JSON_MAPPER.readValue("""
|
||||||
|
{}""", McpSchema.ElicitationCompleteNotification.class);
|
||||||
|
assertThat(notification.elicitationId()).isEqualTo("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCompleteNotificationToleratesUnknownFields() throws Exception {
|
||||||
|
McpSchema.ElicitationCompleteNotification notification = JSON_MAPPER.readValue("""
|
||||||
|
{"elicitationId":"abc","futureField":"ignored"}""", McpSchema.ElicitationCompleteNotification.class);
|
||||||
|
assertThat(notification.elicitationId()).isEqualTo("abc");
|
||||||
|
}
|
||||||
|
|
||||||
// Progress Notification Tests
|
// Progress Notification Tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue