feat: Add elicitation support to MCP protocol

Implement elicitation capabilities allowing servers to request additional information
from users through clients during interactions. This feature provides a standardized
way for servers to gather necessary information dynamically while clients maintain
control over user interactions and data sharing.

- Add ElicitRequest and ElicitResult classes to McpSchema
- Implement elicitation handlers in client classes
- Add elicitation capabilities to server exchange classes
- Add tests for elicitation functionality with various scenarios
This commit is contained in:
Luca Chang 2025-05-27 15:26:44 -07:00 • committed by Christian Tzolov
commit 8a5a591d39
10 changed files with 1106 additions and 28 deletions

View file

@ -23,6 +23,8 @@ 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.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
@ -141,6 +143,15 @@ public class McpAsyncClient {
*/
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
/**
* MCP provides a standardized way for servers to request additional information from
* users through the client during interactions. This flow allows clients to maintain
* control over user interactions and data sharing while enabling servers to gather
* necessary information dynamically. Servers can request structured data from users
* with optional JSON schemas to validate responses.
*/
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
/**
* Client transport implementation.
*/
@ -189,6 +200,15 @@ public class McpAsyncClient {
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
}
// Elicitation Handler
if (this.clientCapabilities.elicitation() != null) {
if (features.elicitationHandler() == null) {
throw new McpError("Elicitation handler must not be null when client capabilities include elicitation");
}
this.elicitationHandler = features.elicitationHandler();
requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
}
// Notification Handlers
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
@ -500,6 +520,18 @@ public class McpAsyncClient {
};
}
// --------------------------
// Elicitation
// --------------------------
private RequestHandler<ElicitResult> elicitationCreateHandler() {
return params -> {
ElicitRequest request = transport.unmarshalFrom(params, new TypeReference<>() {
});
return this.elicitationHandler.apply(request);
};
}
// --------------------------
// Tools
// --------------------------

View file

@ -18,6 +18,8 @@ import io.modelcontextprotocol.spec.McpTransport;
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.util.Assert;
@ -175,6 +177,8 @@ public interface McpClient {
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
private Function<ElicitRequest, ElicitResult> elicitationHandler;
private SyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
@ -283,6 +287,21 @@ public interface McpClient {
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 SyncSpec elicitation(Function<ElicitRequest, ElicitResult> elicitationHandler) {
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
this.elicitationHandler = elicitationHandler;
return this;
}
/**
* Adds a consumer to be notified when the available tools change. This allows the
* client to react to changes in the server's tool capabilities, such as tools
@ -364,7 +383,7 @@ public interface McpClient {
public McpSyncClient build() {
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
this.loggingConsumers, this.samplingHandler);
this.loggingConsumers, this.samplingHandler, this.elicitationHandler);
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
@ -414,6 +433,8 @@ public interface McpClient {
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
@ -522,6 +543,21 @@ public interface McpClient {
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 elicitation(Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler) {
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
this.elicitationHandler = elicitationHandler;
return this;
}
/**
* Adds a consumer to be notified when the available tools change. This allows the
* client to react to changes in the server's tool capabilities, such as tools
@ -606,7 +642,7 @@ public interface McpClient {
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
this.loggingConsumers, this.samplingHandler));
this.loggingConsumers, this.samplingHandler, this.elicitationHandler));
}
}

View file

@ -60,13 +60,15 @@ class McpClientFeatures {
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
*/
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
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) {
/**
* Create an instance and validate the arguments.
@ -77,6 +79,7 @@ class McpClientFeatures {
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
*/
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots,
@ -84,14 +87,16 @@ class McpClientFeatures {
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
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) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
: new McpSchema.ClientCapabilities(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 ? new McpSchema.ClientCapabilities.Elicitation() : null);
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
@ -99,6 +104,7 @@ class McpClientFeatures {
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler;
}
/**
@ -138,9 +144,14 @@ class McpClientFeatures {
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
.subscribeOn(Schedulers.boundedElastic());
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = r -> Mono
.fromCallable(() -> syncSpec.elicitationHandler().apply(r))
.subscribeOn(Schedulers.boundedElastic());
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers,
samplingHandler);
samplingHandler, elicitationHandler);
}
}
@ -156,13 +167,15 @@ class McpClientFeatures {
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
*/
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
/**
* Create an instance and validate the arguments.
@ -174,20 +187,23 @@ class McpClientFeatures {
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
* @param elicitationHandler the elicitation handler.
*/
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
: new McpSchema.ClientCapabilities(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 ? new McpSchema.ClientCapabilities.Elicitation() : null);
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
@ -195,6 +211,7 @@ class McpClientFeatures {
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.samplingHandler = samplingHandler;
this.elicitationHandler = elicitationHandler;
}
}

View file

@ -36,6 +36,9 @@ public class McpAsyncServerExchange {
private static final TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
};
private static final TypeReference<McpSchema.ElicitResult> ELICITATION_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
* Create a new asynchronous exchange with the client.
* @param session The server session representing a 1-1 interaction.
@ -93,6 +96,31 @@ public class McpAsyncServerExchange {
CREATE_MESSAGE_RESULT_TYPE_REF);
}
/**
* Creates a new elicitation. MCP provides a standardized way for servers to request
* additional information from users through the client during interactions. This flow
* allows clients to maintain control over user interactions and data sharing while
* enabling servers to gather necessary information dynamically. Servers can request
* structured data from users with optional JSON schemas to validate responses.
* @param elicitRequest The request to create a new elicitation
* @return A Mono that completes when the elicitation has been resolved.
* @see McpSchema.ElicitRequest
* @see McpSchema.ElicitResult
* @see <a href=
* "https://spec.modelcontextprotocol.io/specification/client/elicitation/">Elicitation
* Specification</a>
*/
public Mono<McpSchema.ElicitResult> createElicitation(McpSchema.ElicitRequest elicitRequest) {
if (this.clientCapabilities == null) {
return Mono.error(new McpError("Client must be initialized. Call the initialize method first!"));
}
if (this.clientCapabilities.elicitation() == null) {
return Mono.error(new McpError("Client must be configured with elicitation capabilities"));
}
return this.session.sendRequest(McpSchema.METHOD_ELICITATION_CREATE, elicitRequest,
ELICITATION_RESULT_TYPE_REF);
}
/**
* Retrieves the list of all roots provided by the client.
* @return A Mono that emits the list of roots result.

View file

@ -64,6 +64,24 @@ public class McpSyncServerExchange {
return this.exchange.createMessage(createMessageRequest).block();
}
/**
* Creates a new elicitation. MCP provides a standardized way for servers to request
* additional information from users through the client during interactions. This flow
* allows clients to maintain control over user interactions and data sharing while
* enabling servers to gather necessary information dynamically. Servers can request
* structured data from users with optional JSON schemas to validate responses.
* @param elicitRequest The request to create a new elicitation
* @return A result containing the elicitation response.
* @see McpSchema.ElicitRequest
* @see McpSchema.ElicitResult
* @see <a href=
* "https://spec.modelcontextprotocol.io/specification/client/elicitation/">Elicitation
* Specification</a>
*/
public McpSchema.ElicitResult createElicitation(McpSchema.ElicitRequest elicitRequest) {
return this.exchange.createElicitation(elicitRequest).block();
}
/**
* Retrieves the list of all roots provided by the client.
* @return The list of roots result.

View file

@ -94,6 +94,9 @@ public final class McpSchema {
// Sampling Methods
public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage";
// Elicitation Methods
public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// ---------------------------
@ -131,8 +134,8 @@ public final class McpSchema {
}
public sealed interface Request
permits InitializeRequest, CallToolRequest, CreateMessageRequest, CompleteRequest, GetPromptRequest {
public sealed interface Request permits InitializeRequest, CallToolRequest, CreateMessageRequest, ElicitRequest,
CompleteRequest, GetPromptRequest {
}
@ -221,7 +224,7 @@ public final class McpSchema {
public record InitializeRequest( // @formatter:off
@JsonProperty("protocolVersion") String protocolVersion,
@JsonProperty("capabilities") ClientCapabilities capabilities,
@JsonProperty("clientInfo") Implementation clientInfo) implements Request {
@JsonProperty("clientInfo") Implementation clientInfo) implements Request {
} // @formatter:on
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@ -245,6 +248,8 @@ public final class McpSchema {
* access to.
* @param sampling Provides a standardized way for servers to request LLM sampling
* (“completions” or “generations”) from language models via clients.
* @param elicitation Provides a standardized way for servers to request additional
* information from users through the client during interactions.
*
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@ -252,7 +257,8 @@ public final class McpSchema {
public record ClientCapabilities( // @formatter:off
@JsonProperty("experimental") Map<String, Object> experimental,
@JsonProperty("roots") RootCapabilities roots,
@JsonProperty("sampling") Sampling sampling) {
@JsonProperty("sampling") Sampling sampling,
@JsonProperty("elicitation") Elicitation elicitation) {
/**
* Roots define the boundaries of where servers can operate within the filesystem,
@ -264,7 +270,7 @@ public final class McpSchema {
* has changed since the last time the server checked.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public record RootCapabilities(
@JsonProperty("listChanged") Boolean listChanged) {
}
@ -279,10 +285,22 @@ public final class McpSchema {
* image-based interactions and optionally include context
* from MCP servers in their prompts.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record Sampling() {
}
/**
* Provides a standardized way for servers to request additional
* information from users through the client during interactions.
* This flow allows clients to maintain control over user
* interactions and data sharing while enabling servers to gather
* necessary information dynamically. Servers can request structured
* data from users with optional JSON schemas to validate responses.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record Elicitation() {
}
public static Builder builder() {
return new Builder();
}
@ -291,6 +309,7 @@ public final class McpSchema {
private Map<String, Object> experimental;
private RootCapabilities roots;
private Sampling sampling;
private Elicitation elicitation;
public Builder experimental(Map<String, Object> experimental) {
this.experimental = experimental;
@ -307,8 +326,13 @@ public final class McpSchema {
return this;
}
public Builder elicitation() {
this.elicitation = new Elicitation();
return this;
}
public ClientCapabilities build() {
return new ClientCapabilities(experimental, roots, sampling);
return new ClientCapabilities(experimental, roots, sampling, elicitation);
}
}
}// @formatter:on
@ -326,11 +350,11 @@ public final class McpSchema {
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record CompletionCapabilities() {
}
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record LoggingCapabilities() {
}
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record PromptCapabilities(
@JsonProperty("listChanged") Boolean listChanged) {
@ -727,11 +751,11 @@ public final class McpSchema {
@JsonProperty("name") String name,
@JsonProperty("description") String description,
@JsonProperty("inputSchema") JsonSchema inputSchema) {
public Tool(String name, String description, String schema) {
this(name, description, parseSchema(schema));
}
} // @formatter:on
private static JsonSchema parseSchema(String schema) {
@ -758,7 +782,7 @@ public final class McpSchema {
@JsonProperty("arguments") Map<String, Object> arguments) implements Request {
public CallToolRequest(String name, String jsonArguments) {
this(name, parseJsonArguments(jsonArguments));
this(name, parseJsonArguments(jsonArguments));
}
private static Map<String, Object> parseJsonArguments(String jsonArguments) {
@ -893,7 +917,7 @@ public final class McpSchema {
@JsonProperty("costPriority") Double costPriority,
@JsonProperty("speedPriority") Double speedPriority,
@JsonProperty("intelligencePriority") Double intelligencePriority) {
public static Builder builder() {
return new Builder();
}
@ -963,7 +987,7 @@ public final class McpSchema {
@JsonProperty("includeContext") ContextInclusionStrategy includeContext,
@JsonProperty("temperature") Double temperature,
@JsonProperty("maxTokens") int maxTokens,
@JsonProperty("stopSequences") List<String> stopSequences,
@JsonProperty("stopSequences") List<String> stopSequences,
@JsonProperty("metadata") Map<String, Object> metadata) implements Request {
public enum ContextInclusionStrategy {
@ -971,7 +995,7 @@ public final class McpSchema {
@JsonProperty("thisServer") THIS_SERVER,
@JsonProperty("allServers") ALL_SERVERS
}
public static Builder builder() {
return new Builder();
}
@ -1040,7 +1064,7 @@ public final class McpSchema {
@JsonProperty("content") Content content,
@JsonProperty("model") String model,
@JsonProperty("stopReason") StopReason stopReason) {
public enum StopReason {
@JsonProperty("endTurn") END_TURN,
@JsonProperty("stopSequence") STOP_SEQUENCE,
@ -1088,6 +1112,79 @@ public final class McpSchema {
}
}// @formatter:on
// Elicitation
/**
* Used by the server to send an elicitation to the client.
*
* @param message The body of the elicitation message.
* @param requestedSchema The elicitation response schema that must be satisfied.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ElicitRequest(// @formatter:off
@JsonProperty("message") String message,
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema) implements Request {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String message;
private Map<String, Object> requestedSchema;
public Builder message(String message) {
this.message = message;
return this;
}
public Builder requestedSchema(Map<String, Object> requestedSchema) {
this.requestedSchema = requestedSchema;
return this;
}
public ElicitRequest build() {
return new ElicitRequest(message, requestedSchema);
}
}
}// @formatter:on
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ElicitResult(// @formatter:off
@JsonProperty("action") Action action,
@JsonProperty("content") Map<String, Object> content) {
public enum Action {
@JsonProperty("accept") ACCEPT,
@JsonProperty("decline") DECLINE,
@JsonProperty("cancel") CANCEL
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Action action;
private Map<String, Object> content;
public Builder message(Action action) {
this.action = action;
return this;
}
public Builder content(Map<String, Object> content) {
this.content = content;
return this;
}
public ElicitResult build() {
return new ElicitResult(action, content);
}
}
}// @formatter:on
// ---------------------------
// Pagination Interfaces
// ---------------------------

View file

@ -19,6 +19,8 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
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.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.Resource;
@ -422,6 +424,20 @@ public abstract class AbstractMcpAsyncClientTests {
});
}
@Test
void testInitializeWithElicitationCapability() {
ClientCapabilities capabilities = ClientCapabilities.builder().elicitation().build();
ElicitResult elicitResult = ElicitResult.builder()
.message(ElicitResult.Action.ACCEPT)
.content(Map.of("foo", "bar"))
.build();
withClient(createMcpTransport(),
builder -> builder.capabilities(capabilities).elicitation(request -> Mono.just(elicitResult)),
client -> {
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
});
}
@Test
void testInitializeWithAllCapabilities() {
var capabilities = ClientCapabilities.builder()
@ -433,7 +449,11 @@ public abstract class AbstractMcpAsyncClientTests {
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
.just(CreateMessageResult.builder().message("test").model("test-model").build());
withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(samplingHandler),
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler = request -> Mono
.just(ElicitResult.builder().message(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build());
withClient(createMcpTransport(),
builder -> builder.capabilities(capabilities).sampling(samplingHandler).elicitation(elicitationHandler),
client ->
StepVerifier.create(client.initialize()).assertNext(result -> {

View file

@ -19,6 +19,8 @@ import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.Root;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import reactor.core.publisher.Mono;
import static io.modelcontextprotocol.spec.McpSchema.METHOD_INITIALIZE;
@ -349,4 +351,152 @@ class McpAsyncClientResponseHandlerTests {
.hasMessage("Sampling handler must not be null when client capabilities include sampling");
}
@Test
@SuppressWarnings("unchecked")
void testElicitationCreateRequestHandling() {
MockMcpClientTransport transport = initializationEnabledTransport();
// Create a test elicitation handler that echoes back the input
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isInstanceOf(Map.class);
assertThat(request.requestedSchema().get("type")).isEqualTo("object");
var properties = request.requestedSchema().get("properties");
assertThat(properties).isNotNull();
assertThat(((Map<String, Object>) properties).get("message")).isInstanceOf(Map.class);
return Mono.just(McpSchema.ElicitResult.builder()
.message(McpSchema.ElicitResult.Action.ACCEPT)
.content(Map.of("message", request.message()))
.build());
};
// Create client with elicitation capability and handler
McpAsyncClient asyncMcpClient = McpClient.async(transport)
.capabilities(ClientCapabilities.builder().elicitation().build())
.elicitation(elicitationHandler)
.build();
assertThat(asyncMcpClient.initialize().block()).isNotNull();
// Create a mock elicitation
var elicitRequest = McpSchema.ElicitRequest.builder()
.message("Test message")
.requestedSchema(Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build();
// Simulate incoming request
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
McpSchema.METHOD_ELICITATION_CREATE, "test-id", elicitRequest);
transport.simulateIncomingMessage(request);
// Verify response
McpSchema.JSONRPCMessage sentMessage = transport.getLastSentMessage();
assertThat(sentMessage).isInstanceOf(McpSchema.JSONRPCResponse.class);
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sentMessage;
assertThat(response.id()).isEqualTo("test-id");
assertThat(response.error()).isNull();
McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeReference<>() {
});
assertThat(result).isNotNull();
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
assertThat(result.content()).isEqualTo(Map.of("message", "Test message"));
asyncMcpClient.closeGracefully();
}
@ParameterizedTest
@EnumSource(value = McpSchema.ElicitResult.Action.class, names = { "DECLINE", "CANCEL" })
void testElicitationFailRequestHandling(McpSchema.ElicitResult.Action action) {
MockMcpClientTransport transport = initializationEnabledTransport();
// Create a test elicitation handler to decline the request
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> Mono
.just(McpSchema.ElicitResult.builder().message(action).build());
// Create client with elicitation capability and handler
McpAsyncClient asyncMcpClient = McpClient.async(transport)
.capabilities(ClientCapabilities.builder().elicitation().build())
.elicitation(elicitationHandler)
.build();
assertThat(asyncMcpClient.initialize().block()).isNotNull();
// Create a mock elicitation
var elicitRequest = McpSchema.ElicitRequest.builder()
.message("Test message")
.requestedSchema(Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build();
// Simulate incoming request
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
McpSchema.METHOD_ELICITATION_CREATE, "test-id", elicitRequest);
transport.simulateIncomingMessage(request);
// Verify response
McpSchema.JSONRPCMessage sentMessage = transport.getLastSentMessage();
assertThat(sentMessage).isInstanceOf(McpSchema.JSONRPCResponse.class);
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sentMessage;
assertThat(response.id()).isEqualTo("test-id");
assertThat(response.error()).isNull();
McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeReference<>() {
});
assertThat(result).isNotNull();
assertThat(result.action()).isEqualTo(action);
assertThat(result.content()).isNull();
asyncMcpClient.closeGracefully();
}
@Test
void testElicitationCreateRequestHandlingWithoutCapability() {
MockMcpClientTransport transport = initializationEnabledTransport();
// Create client without elicitation capability
McpAsyncClient asyncMcpClient = McpClient.async(transport)
.capabilities(ClientCapabilities.builder().build()) // No elicitation
// capability
.build();
assertThat(asyncMcpClient.initialize().block()).isNotNull();
// Create a mock elicitation
var elicitRequest = new McpSchema.ElicitRequest("test",
Map.of("type", "object", "properties", Map.of("test", Map.of("type", "boolean", "defaultValue", true,
"description", "test-description", "title", "test-title"))));
// Simulate incoming request
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
McpSchema.METHOD_ELICITATION_CREATE, "test-id", elicitRequest);
transport.simulateIncomingMessage(request);
// Verify error response
McpSchema.JSONRPCMessage sentMessage = transport.getLastSentMessage();
assertThat(sentMessage).isInstanceOf(McpSchema.JSONRPCResponse.class);
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sentMessage;
assertThat(response.id()).isEqualTo("test-id");
assertThat(response.result()).isNull();
assertThat(response.error()).isNotNull();
assertThat(response.error().message()).contains("Method not found: elicitation/create");
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(McpError.class)
.hasMessage("Elicitation handler must not be null when client capabilities include elicitation");
}
}

View file

@ -24,6 +24,8 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
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.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
import io.modelcontextprotocol.spec.McpSchema.Role;
@ -339,6 +341,217 @@ class HttpServletSseServerTransportProviderIntegrationTests {
mcpServer.close();
}
// ---------------------------------------
// Elicitation Tests
// ---------------------------------------
@Test
@Disabled
void testCreateElicitationWithoutElicitationCapabilities() {
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
exchange.createElicitation(mock(ElicitRequest.class)).block();
return Mono.just(mock(CallToolResult.class));
});
var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
try (
// Create client without elicitation capabilities
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build()) {
assertThat(client.initialize()).isNotNull();
try {
client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
}
catch (McpError e) {
assertThat(e).isInstanceOf(McpError.class)
.hasMessage("Client must be configured with elicitation capabilities");
}
}
server.closeGracefully().block();
}
@Test
void testCreateElicitationSuccess() {
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull();
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
};
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
null);
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
var elicitationRequest = ElicitRequest.builder()
.message("Test message")
.requestedSchema(
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build();
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
assertThat(result).isNotNull();
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
assertThat(result.content().get("message")).isEqualTo("Test message");
}).verifyComplete();
return Mono.just(callResponse);
});
var mcpServer = McpServer.async(mcpServerTransportProvider)
.serverInfo("test-server", "1.0.0")
.tools(tool)
.build();
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
.capabilities(ClientCapabilities.builder().elicitation().build())
.elicitation(elicitationHandler)
.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
assertThat(response).isNotNull();
assertThat(response).isEqualTo(callResponse);
}
mcpServer.closeGracefully().block();
}
@Test
void testCreateElicitationWithRequestTimeoutSuccess() {
// Client
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull();
try {
TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
};
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
.capabilities(ClientCapabilities.builder().elicitation().build())
.elicitation(elicitationHandler)
.build();
// Server
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
null);
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
var elicitationRequest = ElicitRequest.builder()
.message("Test message")
.requestedSchema(
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build();
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
assertThat(result).isNotNull();
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
assertThat(result.content().get("message")).isEqualTo("Test message");
}).verifyComplete();
return Mono.just(callResponse);
});
var mcpServer = McpServer.async(mcpServerTransportProvider)
.serverInfo("test-server", "1.0.0")
.requestTimeout(Duration.ofSeconds(3))
.tools(tool)
.build();
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
assertThat(response).isNotNull();
assertThat(response).isEqualTo(callResponse);
mcpClient.closeGracefully();
mcpServer.closeGracefully().block();
}
@Test
void testCreateElicitationWithRequestTimeoutFail() {
// Client
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
assertThat(request.message()).isNotEmpty();
assertThat(request.requestedSchema()).isNotNull();
try {
TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
};
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
.capabilities(ClientCapabilities.builder().elicitation().build())
.elicitation(elicitationHandler)
.build();
// Server
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
null);
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
var elicitationRequest = ElicitRequest.builder()
.message("Test message")
.requestedSchema(
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
.build();
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
assertThat(result).isNotNull();
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
assertThat(result.content().get("message")).isEqualTo("Test message");
}).verifyComplete();
return Mono.just(callResponse);
});
var mcpServer = McpServer.async(mcpServerTransportProvider)
.serverInfo("test-server", "1.0.0")
.requestTimeout(Duration.ofSeconds(1))
.tools(tool)
.build();
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
}).withMessageContaining("Timeout");
mcpClient.closeGracefully();
mcpServer.closeGracefully().block();
}
// ---------------------------------------
// Roots Tests
// ---------------------------------------

View file

@ -807,6 +807,40 @@ public class McpSchemaTests {
{"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"endTurn"}"""));
}
// Elicitation Tests
@Test
void testCreateElicitationRequest() throws Exception {
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder()
.requestedSchema(Map.of("type", "object", "required", List.of("a"), "properties",
Map.of("foo", Map.of("type", "string"))))
.build();
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}"""));
}
@Test
void testCreateElicitationResult() throws Exception {
McpSchema.ElicitResult result = McpSchema.ElicitResult.builder()
.content(Map.of("foo", "bar"))
.message(McpSchema.ElicitResult.Action.ACCEPT)
.build();
String value = mapper.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"action":"accept","content":{"foo":"bar"}}"""));
}
// Roots Tests
@Test