|
15 | 15 | import java.util.concurrent.ConcurrentHashMap; |
16 | 16 | import java.util.function.Function; |
17 | 17 |
|
18 | | -import org.slf4j.Logger; |
19 | | -import org.slf4j.LoggerFactory; |
20 | | - |
21 | 18 | import io.modelcontextprotocol.client.LifecycleInitializer.Initialization; |
22 | 19 | import io.modelcontextprotocol.json.TypeRef; |
23 | 20 | import io.modelcontextprotocol.json.schema.JsonSchemaValidator; |
|
29 | 26 | import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; |
30 | 27 | import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; |
31 | 28 | import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; |
| 29 | +import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest; |
32 | 30 | import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; |
33 | 31 | import io.modelcontextprotocol.spec.McpSchema.ElicitResult; |
| 32 | +import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest; |
34 | 33 | import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; |
35 | 34 | import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; |
36 | 35 | import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult; |
|
41 | 40 | import io.modelcontextprotocol.util.Assert; |
42 | 41 | import io.modelcontextprotocol.util.ToolNameValidator; |
43 | 42 | import io.modelcontextprotocol.util.Utils; |
| 43 | +import org.slf4j.Logger; |
| 44 | +import org.slf4j.LoggerFactory; |
44 | 45 | import reactor.core.publisher.Flux; |
45 | 46 | import reactor.core.publisher.Mono; |
46 | 47 |
|
@@ -108,6 +109,9 @@ public class McpAsyncClient { |
108 | 109 | public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() { |
109 | 110 | }; |
110 | 111 |
|
| 112 | + public static final TypeRef<McpSchema.ElicitationCompleteNotification> ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF = new TypeRef<>() { |
| 113 | + }; |
| 114 | + |
111 | 115 | public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version"; |
112 | 116 |
|
113 | 117 | /** |
@@ -145,7 +149,14 @@ public class McpAsyncClient { |
145 | 149 | * necessary information dynamically. Servers can request structured data from users |
146 | 150 | * with optional JSON schemas to validate responses. |
147 | 151 | */ |
148 | | - private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler; |
| 152 | + private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler; |
| 153 | + |
| 154 | + /** |
| 155 | + * MCP provides a standardized way for servers to request additional information from |
| 156 | + * users out-of-band during interactions. This flow allows users to share information |
| 157 | + * with the server without sharing it with the client. |
| 158 | + */ |
| 159 | + private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler; |
149 | 160 |
|
150 | 161 | /** |
151 | 162 | * Client transport implementation. |
@@ -226,11 +237,21 @@ public class McpAsyncClient { |
226 | 237 |
|
227 | 238 | // Elicitation Handler |
228 | 239 | if (this.clientCapabilities.elicitation() != null) { |
229 | | - if (features.elicitationHandler() == null) { |
| 240 | + // elicitation: {} is equivalent to elicitation: { form: {} } for |
| 241 | + // backwards-compatiblity |
| 242 | + var supportsForm = this.clientCapabilities.elicitation().form() != null |
| 243 | + || this.clientCapabilities.elicitation().url() == null; |
| 244 | + var supportsUrl = this.clientCapabilities.elicitation().url() != null; |
| 245 | + if (supportsForm && features.formElicitationHandler() == null) { |
230 | 246 | throw new IllegalArgumentException( |
231 | | - "Elicitation handler must not be null when client capabilities include elicitation"); |
| 247 | + "Form elicitation handler must not be null when client capabilities include form elicitation"); |
232 | 248 | } |
233 | | - this.elicitationHandler = features.elicitationHandler(); |
| 249 | + if (supportsUrl && features.urlElicitationHandler() == null) { |
| 250 | + throw new IllegalArgumentException( |
| 251 | + "URL elicitation handler must not be null when client capabilities include URL elicitation"); |
| 252 | + } |
| 253 | + this.formElicitationHandler = features.formElicitationHandler(); |
| 254 | + this.urlElicitationHandler = features.urlElicitationHandler(); |
234 | 255 | requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler()); |
235 | 256 | } |
236 | 257 |
|
@@ -301,6 +322,16 @@ public class McpAsyncClient { |
301 | 322 | notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS, |
302 | 323 | asyncProgressNotificationHandler(progressConsumersFinal)); |
303 | 324 |
|
| 325 | + // Elicitation Complete Notification |
| 326 | + List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumersFinal = new ArrayList<>(); |
| 327 | + elicitationCompleteConsumersFinal |
| 328 | + .add((notification) -> Mono.fromRunnable(() -> logger.debug("Elicitation complete: {}", notification))); |
| 329 | + if (!Utils.isEmpty(features.elicitationCompleteConsumers())) { |
| 330 | + elicitationCompleteConsumersFinal.addAll(features.elicitationCompleteConsumers()); |
| 331 | + } |
| 332 | + notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE, |
| 333 | + asyncElicitationCompleteNotificationHandler(elicitationCompleteConsumersFinal)); |
| 334 | + |
304 | 335 | Function<Initialization, Mono<Void>> postInitializationHook = init -> { |
305 | 336 |
|
306 | 337 | if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) { |
@@ -552,23 +583,47 @@ private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() { |
552 | 583 | }; |
553 | 584 | } |
554 | 585 |
|
555 | | - // -------------------------- |
556 | | - // Elicitation |
557 | | - // -------------------------- |
558 | 586 | private RequestHandler<ElicitResult> elicitationCreateHandler() { |
559 | 587 | return params -> { |
560 | | - ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() { |
| 588 | + McpSchema.ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() { |
561 | 589 | }); |
562 | 590 |
|
563 | | - return this.elicitationHandler.apply(request).map(result -> { |
564 | | - if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT |
565 | | - && result.content() != null) { |
566 | | - Map<String, Object> merged = new HashMap<>(result.content()); |
567 | | - applyElicitationDefaults(request.requestedSchema(), merged); |
568 | | - return new ElicitResult(result.action(), merged, result.meta()); |
| 591 | + if (request instanceof ElicitUrlRequest urlRequest) { |
| 592 | + if (this.urlElicitationHandler == null) { |
| 593 | + return Mono.error(new IllegalStateException( |
| 594 | + "Received URL elicitation request, but urlElicitation handler is null")); |
569 | 595 | } |
570 | | - return result; |
571 | | - }); |
| 596 | + return this.urlElicitationHandler.apply(urlRequest); |
| 597 | + } |
| 598 | + else if (request instanceof ElicitFormRequest formRequest) { |
| 599 | + if (this.formElicitationHandler == null) { |
| 600 | + return Mono.error(new IllegalStateException( |
| 601 | + "Received FORM elicitation request, but formElicitationHandler handler is null")); |
| 602 | + } |
| 603 | + return this.formElicitationHandler.apply(formRequest).map(result -> { |
| 604 | + if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT |
| 605 | + && result.content() != null) { |
| 606 | + Map<String, Object> merged = new HashMap<>(result.content()); |
| 607 | + applyElicitationDefaults(formRequest.requestedSchema(), merged); |
| 608 | + return new ElicitResult(result.action(), merged, result.meta()); |
| 609 | + } |
| 610 | + return result; |
| 611 | + }); |
| 612 | + } |
| 613 | + |
| 614 | + return Mono.error(new IllegalStateException("Unknown elictation type deserialized")); |
| 615 | + }; |
| 616 | + } |
| 617 | + |
| 618 | + private NotificationHandler asyncElicitationCompleteNotificationHandler( |
| 619 | + List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) { |
| 620 | + return params -> { |
| 621 | + McpSchema.ElicitationCompleteNotification notification = transport.unmarshalFrom(params, |
| 622 | + ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF); |
| 623 | + |
| 624 | + return Flux.fromIterable(elicitationCompleteConsumers) |
| 625 | + .flatMap(consumer -> consumer.apply(notification)) |
| 626 | + .then(); |
572 | 627 | }; |
573 | 628 | } |
574 | 629 |
|
|
0 commit comments