Skip to content

Commit c48b84e

Browse files
author
Sainath Reddy Bobbala
committed
feat(core): add URL elicitation support (SEP-1036)
Add URL-type elicitation schema support allowing servers to request URL input from users during tool execution. Includes: - UrlElicitationSchema and UrlElicitationResult types in McpSchema - Client-side URL elicitation handler registration and dispatch - Server exchange methods for URL elicitation requests - Comprehensive tests for schema, client handler, and server exchange
1 parent 5895b2e commit c48b84e

10 files changed

Lines changed: 616 additions & 53 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,16 @@ public class McpAsyncClient {
297297
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
298298
asyncProgressNotificationHandler(progressConsumersFinal));
299299

300+
// Elicitation Complete Notification
301+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumersFinal = new ArrayList<>();
302+
elicitationCompleteConsumersFinal
303+
.add((notification) -> Mono.fromRunnable(() -> logger.debug("Elicitation complete: {}", notification)));
304+
if (!Utils.isEmpty(features.elicitationCompleteConsumers())) {
305+
elicitationCompleteConsumersFinal.addAll(features.elicitationCompleteConsumers());
306+
}
307+
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
308+
asyncElicitationCompleteNotificationHandler(elicitationCompleteConsumersFinal));
309+
300310
Function<Initialization, Mono<Void>> postInitializationHook = init -> {
301311

302312
if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
@@ -1039,6 +1049,20 @@ private NotificationHandler asyncProgressNotificationHandler(
10391049
};
10401050
}
10411051

1052+
private NotificationHandler asyncElicitationCompleteNotificationHandler(
1053+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
1054+
1055+
return params -> {
1056+
McpSchema.ElicitationCompleteNotification notification = transport.unmarshalFrom(params,
1057+
new TypeRef<McpSchema.ElicitationCompleteNotification>() {
1058+
});
1059+
1060+
return Flux.fromIterable(elicitationCompleteConsumers)
1061+
.flatMap(consumer -> consumer.apply(notification))
1062+
.then();
1063+
};
1064+
}
1065+
10421066
/**
10431067
* This method is package-private and used for test only. Should not be called by user
10441068
* code.

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

Lines changed: 49 additions & 11 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;
@@ -20,15 +29,6 @@
2029
import io.modelcontextprotocol.util.Assert;
2130
import reactor.core.publisher.Mono;
2231

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-
3232
/**
3333
* Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that
3434
* enables AI models to interact with external tools and resources through a standardized
@@ -185,6 +185,8 @@ class SyncSpec {
185185

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

188+
private final List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers = new ArrayList<>();
189+
188190
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
189191

190192
private Function<ElicitRequest, ElicitResult> elicitationHandler;
@@ -437,6 +439,22 @@ public SyncSpec progressConsumers(List<Consumer<McpSchema.ProgressNotification>>
437439
return this;
438440
}
439441

442+
/**
443+
* Adds a consumer to be notified when an elicitation complete notification is
444+
* received from the server. This allows the client to react when a URL-mode
445+
* elicitation flow has been completed.
446+
* @param elicitationCompleteConsumer A consumer that receives elicitation
447+
* complete notifications. Must not be null.
448+
* @return This builder instance for method chaining
449+
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
450+
*/
451+
public SyncSpec elicitationCompleteConsumer(
452+
Consumer<McpSchema.ElicitationCompleteNotification> elicitationCompleteConsumer) {
453+
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
454+
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
455+
return this;
456+
}
457+
440458
/**
441459
* Add a provider of {@link McpTransportContext}, providing a context before
442460
* calling any client operation. This allows to extract thread-locals and hand
@@ -488,7 +506,7 @@ public McpSyncClient build() {
488506
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
489507
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
490508
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
491-
this.elicitationHandler, this.enableCallToolSchemaCaching);
509+
this.elicitationHandler, this.elicitationCompleteConsumers, this.enableCallToolSchemaCaching);
492510

493511
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
494512

@@ -541,6 +559,8 @@ class AsyncSpec {
541559

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

562+
private final List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
563+
544564
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
545565

546566
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
@@ -795,6 +815,23 @@ public AsyncSpec progressConsumers(
795815
return this;
796816
}
797817

818+
/**
819+
* Adds a consumer to be notified when an elicitation complete notification is
820+
* received from the server. This allows the client to react when a URL-mode
821+
* elicitation flow has been completed.
822+
* @param elicitationCompleteConsumer A function that receives elicitation
823+
* complete notifications and returns a Mono indicating completion. Must not be
824+
* null.
825+
* @return This builder instance for method chaining
826+
* @throws IllegalArgumentException if elicitationCompleteConsumer is null
827+
*/
828+
public AsyncSpec elicitationCompleteConsumer(
829+
Function<McpSchema.ElicitationCompleteNotification, Mono<Void>> elicitationCompleteConsumer) {
830+
Assert.notNull(elicitationCompleteConsumer, "Elicitation complete consumer must not be null");
831+
this.elicitationCompleteConsumers.add(elicitationCompleteConsumer);
832+
return this;
833+
}
834+
798835
/**
799836
* Sets the JSON schema validator to use for validating tool responses against
800837
* output schemas.
@@ -833,7 +870,8 @@ public McpAsyncClient build() {
833870
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
834871
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
835872
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
836-
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
873+
this.samplingHandler, this.elicitationHandler, this.elicitationCompleteConsumers,
874+
this.enableCallToolSchemaCaching));
837875
}
838876

839877
}

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class McpClientFeatures {
6262
* @param progressConsumers the progress consumers.
6363
* @param samplingHandler the sampling handler.
6464
* @param elicitationHandler the elicitation handler.
65+
* @param elicitationCompleteConsumers the elicitation complete consumers.
6566
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
6667
*/
6768
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
@@ -73,6 +74,7 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
7374
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
7475
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
7576
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
77+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
7678
boolean enableCallToolSchemaCaching) {
7779

7880
/**
@@ -86,6 +88,7 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
8688
* @param progressConsumers the progress consumers.
8789
* @param samplingHandler the sampling handler.
8890
* @param elicitationHandler the elicitation handler.
91+
* @param elicitationCompleteConsumers the elicitation complete consumers.
8992
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
9093
*/
9194
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
@@ -98,6 +101,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
98101
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
99102
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
100103
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
104+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers,
101105
boolean enableCallToolSchemaCaching) {
102106

103107
Assert.notNull(clientInfo, "Client info must not be null");
@@ -117,6 +121,8 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
117121
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
118122
this.samplingHandler = samplingHandler;
119123
this.elicitationHandler = elicitationHandler;
124+
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
125+
: List.of();
120126
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
121127
}
122128

@@ -134,7 +140,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
134140
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
135141
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
136142
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
137-
elicitationHandler, false);
143+
elicitationHandler, List.of(), false);
138144
}
139145

140146
/**
@@ -182,6 +188,13 @@ public static Async fromSync(Sync syncSpec) {
182188
.subscribeOn(Schedulers.boundedElastic()));
183189
}
184190

191+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
192+
for (Consumer<McpSchema.ElicitationCompleteNotification> consumer : syncSpec
193+
.elicitationCompleteConsumers()) {
194+
elicitationCompleteConsumers.add(n -> Mono.<Void>fromRunnable(() -> consumer.accept(n))
195+
.subscribeOn(Schedulers.boundedElastic()));
196+
}
197+
185198
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
186199
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
187200
.subscribeOn(Schedulers.boundedElastic());
@@ -193,7 +206,7 @@ public static Async fromSync(Sync syncSpec) {
193206
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
194207
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
195208
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
196-
syncSpec.enableCallToolSchemaCaching);
209+
elicitationCompleteConsumers, syncSpec.enableCallToolSchemaCaching);
197210
}
198211
}
199212

@@ -211,6 +224,7 @@ public static Async fromSync(Sync syncSpec) {
211224
* @param progressConsumers the progress consumers.
212225
* @param samplingHandler the sampling handler.
213226
* @param elicitationHandler the elicitation handler.
227+
* @param elicitationCompleteConsumers the elicitation complete consumers.
214228
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
215229
*/
216230
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
@@ -222,6 +236,7 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
222236
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
223237
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
224238
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
239+
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
225240
boolean enableCallToolSchemaCaching) {
226241

227242
/**
@@ -237,6 +252,7 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
237252
* @param progressConsumers the progress consumers.
238253
* @param samplingHandler the sampling handler.
239254
* @param elicitationHandler the elicitation handler.
255+
* @param elicitationCompleteConsumers the elicitation complete consumers.
240256
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
241257
*/
242258
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
@@ -248,6 +264,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
248264
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
249265
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
250266
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
267+
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers,
251268
boolean enableCallToolSchemaCaching) {
252269

253270
Assert.notNull(clientInfo, "Client info must not be null");
@@ -267,6 +284,8 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
267284
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
268285
this.samplingHandler = samplingHandler;
269286
this.elicitationHandler = elicitationHandler;
287+
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
288+
: List.of();
270289
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
271290
}
272291

@@ -283,7 +302,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
283302
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
284303
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
285304
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
286-
elicitationHandler, false);
305+
elicitationHandler, List.of(), false);
287306
}
288307
}
289308

mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@
44

55
package io.modelcontextprotocol.server;
66

7-
import io.modelcontextprotocol.common.McpTransportContext;
87
import java.util.ArrayList;
98
import java.util.Collections;
109

10+
import io.modelcontextprotocol.common.McpTransportContext;
1111
import io.modelcontextprotocol.json.TypeRef;
12-
import io.modelcontextprotocol.spec.McpError;
1312
import io.modelcontextprotocol.spec.McpLoggableSession;
1413
import io.modelcontextprotocol.spec.McpSchema;
1514
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
1615
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
17-
import io.modelcontextprotocol.spec.McpSession;
1816
import io.modelcontextprotocol.util.Assert;
1917
import reactor.core.publisher.Mono;
2018

@@ -152,10 +150,31 @@ public Mono<McpSchema.ElicitResult> createElicitation(McpSchema.ElicitRequest el
152150
if (this.clientCapabilities.elicitation() == null) {
153151
return Mono.error(new IllegalStateException("Client must be configured with elicitation capabilities"));
154152
}
153+
if ("url".equals(elicitRequest.mode()) && this.clientCapabilities.elicitation().url() == null) {
154+
return Mono.error(new IllegalStateException("Client must be configured with URL elicitation capabilities"));
155+
}
155156
return this.session.sendRequest(McpSchema.METHOD_ELICITATION_CREATE, elicitRequest,
156157
ELICITATION_RESULT_TYPE_REF);
157158
}
158159

160+
/**
161+
* Sends a notification to the client that an out-of-band URL elicitation interaction
162+
* has completed.
163+
* @param elicitationId The ID of the elicitation that completed
164+
* @return A Mono that completes when the notification has been sent
165+
*/
166+
public Mono<Void> sendElicitationComplete(String elicitationId) {
167+
if (this.clientCapabilities == null) {
168+
return Mono
169+
.error(new IllegalStateException("Client must be initialized. Call the initialize method first!"));
170+
}
171+
if (this.clientCapabilities.elicitation() == null || this.clientCapabilities.elicitation().url() == null) {
172+
return Mono.error(new IllegalStateException("Client must be configured with URL elicitation capabilities"));
173+
}
174+
return this.session.sendNotification(McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
175+
new McpSchema.ElicitationCompleteNotification(elicitationId, null));
176+
}
177+
159178
/**
160179
* Retrieves the list of all roots provided by the client.
161180
* @return A Mono that emits the list of roots result.

mcp-core/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ public McpSchema.ElicitResult createElicitation(McpSchema.ElicitRequest elicitRe
100100
return this.exchange.createElicitation(elicitRequest).block();
101101
}
102102

103+
/**
104+
* Sends a notification to the client that an out-of-band URL elicitation interaction
105+
* has completed.
106+
* @param elicitationId The ID of the elicitation that completed
107+
*/
108+
public void sendElicitationComplete(String elicitationId) {
109+
this.exchange.sendElicitationComplete(elicitationId).block();
110+
}
111+
103112
/**
104113
* Retrieves the list of all roots provided by the client.
105114
* @return The list of roots result.

0 commit comments

Comments
 (0)