Skip to content

Commit 5261f3f

Browse files
Sainath Reddy BobbalaKehrlann
andcommitted
Add URL elicitation support (SEP-1036)
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>
1 parent 4f3f7d9 commit 5261f3f

17 files changed

Lines changed: 1351 additions & 174 deletions

File tree

docs/client.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ Elicitation enables servers to request additional information or user input thro
271271

272272
```java
273273
// Configure elicitation handler
274-
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
274+
Function<ElicitFormRequest, ElicitResult> formElicitationHandler = request -> {
275275
// Present the request to the user and collect their response
276276
// The request contains a message and a schema describing the expected input
277277
Map<String, Object> userResponse = collectUserInput(request.message(), request.requestedSchema());
@@ -283,7 +283,7 @@ var client = McpClient.sync(transport)
283283
.capabilities(ClientCapabilities.builder()
284284
.elicitation()
285285
.build())
286-
.elicitation(elicitationHandler)
286+
.elicitation(formElicitationHandler)
287287
.build();
288288
```
289289

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import java.util.concurrent.ConcurrentHashMap;
1616
import java.util.function.Function;
1717

18+
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
19+
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
1820
import org.slf4j.Logger;
1921
import org.slf4j.LoggerFactory;
2022

@@ -108,6 +110,9 @@ public class McpAsyncClient {
108110
public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() {
109111
};
110112

113+
public static final TypeRef<McpSchema.ElicitationCompleteNotification> ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF = new TypeRef<>() {
114+
};
115+
111116
public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";
112117

113118
/**
@@ -145,7 +150,14 @@ public class McpAsyncClient {
145150
* necessary information dynamically. Servers can request structured data from users
146151
* with optional JSON schemas to validate responses.
147152
*/
148-
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
153+
private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;
154+
155+
/**
156+
* MCP provides a standardized way for servers to request additional information from
157+
* users out-of-band during interactions. This flow allows users to share information
158+
* with the server without sharing it with the client.
159+
*/
160+
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;
149161

150162
/**
151163
* Client transport implementation.
@@ -226,11 +238,18 @@ public class McpAsyncClient {
226238

227239
// Elicitation Handler
228240
if (this.clientCapabilities.elicitation() != null) {
229-
if (features.elicitationHandler() == null) {
241+
if ((this.clientCapabilities.elicitation().url() == null
242+
|| this.clientCapabilities.elicitation().form() != null)
243+
&& features.formElicitationHandler() == null) {
230244
throw new IllegalArgumentException(
231-
"Elicitation handler must not be null when client capabilities include elicitation");
245+
"Form elicitation handler must not be null when client capabilities include form elicitation");
232246
}
233-
this.elicitationHandler = features.elicitationHandler();
247+
if (this.clientCapabilities.elicitation().url() != null && features.urlElicitationHandler() == null) {
248+
throw new IllegalArgumentException(
249+
"URL elicitation handler must not be null when client capabilities include URL elicitation");
250+
}
251+
this.formElicitationHandler = features.formElicitationHandler();
252+
this.urlElicitationHandler = features.urlElicitationHandler();
234253
requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
235254
}
236255

@@ -301,6 +320,16 @@ public class McpAsyncClient {
301320
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
302321
asyncProgressNotificationHandler(progressConsumersFinal));
303322

323+
// Elicitation Complete Notification
324+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumersFinal = new ArrayList<>();
325+
elicitationCompleteConsumersFinal
326+
.add((notification) -> Mono.fromRunnable(() -> logger.debug("Elicitation complete: {}", notification)));
327+
if (!Utils.isEmpty(features.elicitationCompleteConsumers())) {
328+
elicitationCompleteConsumersFinal.addAll(features.elicitationCompleteConsumers());
329+
}
330+
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
331+
asyncElicitationCompleteNotificationHandler(elicitationCompleteConsumersFinal));
332+
304333
Function<Initialization, Mono<Void>> postInitializationHook = init -> {
305334

306335
if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
@@ -557,23 +586,48 @@ private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
557586
};
558587
}
559588

560-
// --------------------------
561-
// Elicitation
562-
// --------------------------
563589
private RequestHandler<ElicitResult> elicitationCreateHandler() {
564590
return params -> {
565-
ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
591+
McpSchema.ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
566592
});
567593

568-
return this.elicitationHandler.apply(request).map(result -> {
569-
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
570-
&& result.content() != null) {
571-
Map<String, Object> merged = new HashMap<>(result.content());
572-
applyElicitationDefaults(request.requestedSchema(), merged);
573-
return new ElicitResult(result.action(), merged, result.meta());
594+
if (request instanceof ElicitUrlRequest urlRequest) {
595+
if (this.urlElicitationHandler == null) {
596+
return Mono.error(new IllegalStateException(
597+
"Received URL elicitation request, but urlElicitation handler is null"));
574598
}
575-
return result;
576-
});
599+
return this.urlElicitationHandler.apply(urlRequest);
600+
}
601+
else if (request instanceof ElicitFormRequest formRequest) {
602+
if (this.formElicitationHandler == null) {
603+
return Mono.error(new IllegalStateException(
604+
"Received FORM elicitation request, but formElicitationHandler handler is null"));
605+
606+
}
607+
return this.formElicitationHandler.apply(formRequest).map(result -> {
608+
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
609+
&& result.content() != null) {
610+
Map<String, Object> merged = new HashMap<>(result.content());
611+
applyElicitationDefaults(formRequest.requestedSchema(), merged);
612+
return new ElicitResult(result.action(), merged, result.meta());
613+
}
614+
return result;
615+
});
616+
}
617+
618+
return Mono.error(new IllegalStateException("Unknown elictation type deserialized"));
619+
};
620+
}
621+
622+
private NotificationHandler asyncElicitationCompleteNotificationHandler(
623+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
624+
return params -> {
625+
McpSchema.ElicitationCompleteNotification notification = transport.unmarshalFrom(params,
626+
ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF);
627+
628+
return Flux.fromIterable(elicitationCompleteConsumers)
629+
.flatMap(consumer -> consumer.apply(notification))
630+
.then();
577631
};
578632
}
579633

mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44

55
package io.modelcontextprotocol.client;
66

7+
import java.time.Duration;
8+
import java.util.ArrayList;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.function.Consumer;
13+
import java.util.function.Function;
14+
import java.util.function.Supplier;
15+
716
import io.modelcontextprotocol.common.McpTransportContext;
817
import io.modelcontextprotocol.json.McpJsonDefaults;
918
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
@@ -12,23 +21,15 @@
1221
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
1322
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
1423
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
15-
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
24+
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
1625
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
26+
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
1727
import io.modelcontextprotocol.spec.McpSchema.Implementation;
1828
import io.modelcontextprotocol.spec.McpSchema.Root;
1929
import io.modelcontextprotocol.spec.McpTransport;
2030
import io.modelcontextprotocol.util.Assert;
2131
import reactor.core.publisher.Mono;
2232

23-
import java.time.Duration;
24-
import java.util.ArrayList;
25-
import java.util.HashMap;
26-
import java.util.List;
27-
import java.util.Map;
28-
import java.util.function.Consumer;
29-
import java.util.function.Function;
30-
import java.util.function.Supplier;
31-
3233
/**
3334
* Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that
3435
* enables AI models to interact with external tools and resources through a standardized
@@ -185,9 +186,13 @@ class SyncSpec {
185186

186187
private final List<Consumer<McpSchema.ProgressNotification>> progressConsumers = new ArrayList<>();
187188

189+
private final List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers = new ArrayList<>();
190+
188191
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
189192

190-
private Function<ElicitRequest, ElicitResult> elicitationHandler;
193+
private Function<ElicitFormRequest, ElicitResult> formElicitationHandler;
194+
195+
private Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler;
191196

192197
private Supplier<McpTransportContext> contextProvider = () -> McpTransportContext.EMPTY;
193198

@@ -314,9 +319,24 @@ public SyncSpec sampling(Function<CreateMessageRequest, CreateMessageResult> sam
314319
* @return This builder instance for method chaining
315320
* @throws IllegalArgumentException if elicitationHandler is null
316321
*/
317-
public SyncSpec elicitation(Function<ElicitRequest, ElicitResult> elicitationHandler) {
322+
public SyncSpec elicitation(Function<ElicitFormRequest, ElicitResult> elicitationHandler) {
318323
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
319-
this.elicitationHandler = elicitationHandler;
324+
this.formElicitationHandler = elicitationHandler;
325+
return this;
326+
}
327+
328+
/**
329+
* Sets a custom elicitation handler for processing URL-mode elicitation message
330+
* requests. The elicitation handler can modify or validate messages before they
331+
* are sent to the server, enabling custom processing logic.
332+
* @param elicitationHandler A function that processes elicitation requests and
333+
* returns results. Must not be null.
334+
* @return This builder instance for method chaining
335+
* @throws IllegalArgumentException if elicitationHandler is null
336+
*/
337+
public SyncSpec urlElicitation(Function<ElicitUrlRequest, ElicitResult> elicitationHandler) {
338+
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
339+
this.urlElicitationHandler = elicitationHandler;
320340
return this;
321341
}
322342

@@ -439,6 +459,36 @@ public SyncSpec progressConsumers(List<Consumer<McpSchema.ProgressNotification>>
439459
return this;
440460
}
441461

462+
/**
463+
* Adds a consumer to be notified by the server when an URL elicitation is
464+
* complete.
465+
* @param elicitationCompleteConsumer A consumer that receives elicitation
466+
* complete notifications. Must not be null.
467+
* @return This builder instance for method chaining
468+
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
469+
*/
470+
public SyncSpec elicitationCompleteConsumer(
471+
Consumer<McpSchema.ElicitationCompleteNotification> elicitationCompleteConsumer) {
472+
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
473+
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
474+
return this;
475+
}
476+
477+
/**
478+
* Adds multiple consumers to be notified by the server when an URL elicitation is
479+
* complete.
480+
* @param elicitationCompleteConsumers A list of consumers that receives
481+
* elicitation complete notifications. Must not be null.
482+
* @return This builder instance for method chaining
483+
* @throws IllegalArgumentException if elicitationCompleteConsumers is null
484+
*/
485+
public SyncSpec elicitationCompleteConsumers(
486+
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers) {
487+
Assert.notNull(elicitationCompleteConsumers, "Elicitation complete consumers must not be null");
488+
this.elicitationCompleteConsumers.addAll(elicitationCompleteConsumers);
489+
return this;
490+
}
491+
442492
/**
443493
* Add a provider of {@link McpTransportContext}, providing a context before
444494
* calling any client operation. This allows to extract thread-locals and hand
@@ -502,8 +552,9 @@ public SyncSpec applyElicitationDefaults(boolean applyElicitationDefaults) {
502552
public McpSyncClient build() {
503553
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
504554
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
505-
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
506-
this.elicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
555+
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
556+
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
557+
this.urlElicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
507558

508559
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
509560

@@ -556,9 +607,13 @@ class AsyncSpec {
556607

557608
private final List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers = new ArrayList<>();
558609

610+
private final List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
611+
559612
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
560613

561-
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
614+
private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;
615+
616+
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;
562617

563618
private JsonSchemaValidator jsonSchemaValidator;
564619

@@ -683,9 +738,24 @@ public AsyncSpec sampling(Function<CreateMessageRequest, Mono<CreateMessageResul
683738
* @return This builder instance for method chaining
684739
* @throws IllegalArgumentException if elicitationHandler is null
685740
*/
686-
public AsyncSpec elicitation(Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler) {
741+
public AsyncSpec elicitation(Function<ElicitFormRequest, Mono<ElicitResult>> elicitationHandler) {
742+
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
743+
this.formElicitationHandler = elicitationHandler;
744+
return this;
745+
}
746+
747+
/**
748+
* Sets a custom elicitation handler for processing elicitation message requests.
749+
* The elicitation handler can modify or validate messages before they are sent to
750+
* the server, enabling custom processing logic.
751+
* @param elicitationHandler A function that processes elicitation requests and
752+
* returns results. Must not be null.
753+
* @return This builder instance for method chaining
754+
* @throws IllegalArgumentException if elicitationHandler is null
755+
*/
756+
public AsyncSpec urlElicitation(Function<ElicitUrlRequest, Mono<ElicitResult>> elicitationHandler) {
687757
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
688-
this.elicitationHandler = elicitationHandler;
758+
this.urlElicitationHandler = elicitationHandler;
689759
return this;
690760
}
691761

@@ -812,6 +882,36 @@ public AsyncSpec progressConsumers(
812882
return this;
813883
}
814884

885+
/**
886+
* Adds a consumer to be notified by the server when an URL elicitation is
887+
* complete.
888+
* @param elicitationCompleteConsumer A consumer that receives elicitation
889+
* complete notifications. Must not be null.
890+
* @return This builder instance for method chaining
891+
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
892+
*/
893+
public AsyncSpec elicitationCompleteConsumer(
894+
Function<McpSchema.ElicitationCompleteNotification, Mono<Void>> elicitationCompleteConsumer) {
895+
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
896+
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
897+
return this;
898+
}
899+
900+
/**
901+
* Adds multiple consumers to be notified by the server when an URL elicitation is
902+
* complete.
903+
* @param elicitationCompleteConsumers A list of consumers that receives
904+
* elicitation complete notifications. Must not be null.
905+
* @return This builder instance for method chaining
906+
* @throws IllegalArgumentException if elicitationCompleteConsumers is null
907+
*/
908+
public AsyncSpec elicitationCompleteConsumers(
909+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
910+
Assert.notNull(elicitationCompleteConsumers, "Elicitation complete consumers must not be null");
911+
this.elicitationCompleteConsumers.addAll(elicitationCompleteConsumers);
912+
return this;
913+
}
914+
815915
/**
816916
* Sets the JSON schema validator to use for validating tool responses against
817917
* output schemas.
@@ -863,7 +963,8 @@ public McpAsyncClient build() {
863963
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
864964
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
865965
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
866-
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching,
966+
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
967+
this.urlElicitationHandler, this.enableCallToolSchemaCaching,
867968
this.applyElicitationDefaults));
868969
}
869970

0 commit comments

Comments
 (0)