Skip to content

Commit 869d1b3

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. This enables out-of-band interactions like OAuth flows, payment processing, and API key entry. Schema changes (following CONTRIBUTING.md rules): - ElicitRequest: append mode, url, elicitationId at end of record components (new optional fields default to null) - ElicitationCompleteNotification: new record implementing Notification - ErrorCodes.URL_ELICITATION_REQUIRED (-32042): new error code - METHOD_NOTIFICATION_ELICITATION_COMPLETE: new method constant Server exchange: - createElicitation validates URL mode against client capability - sendElicitationComplete sends completion notification to client Client: - elicitationCompleteConsumer() builder method on SyncSpec and AsyncSpec - Notification handler dispatches to registered consumers - Existing elicitation handler naturally receives URL mode requests Validation: - Canonical constructor enforces url/elicitationId non-null for URL mode - Builder rejects requestedSchema when mode is 'url' Backward compatible: existing form-mode callers are unaffected. Old constructors delegate to canonical constructor with null for new fields. Ref #939
1 parent 5d4a1cc commit 869d1b3

8 files changed

Lines changed: 489 additions & 70 deletions

File tree

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

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

18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
1821
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
1922
import io.modelcontextprotocol.json.TypeRef;
2023
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
@@ -30,16 +33,14 @@
3033
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
3134
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
3235
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
33-
import io.modelcontextprotocol.util.ToolNameValidator;
3436
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
3537
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
3638
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
3739
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
3840
import io.modelcontextprotocol.spec.McpSchema.Root;
3941
import io.modelcontextprotocol.util.Assert;
42+
import io.modelcontextprotocol.util.ToolNameValidator;
4043
import io.modelcontextprotocol.util.Utils;
41-
import org.slf4j.Logger;
42-
import org.slf4j.LoggerFactory;
4344
import reactor.core.publisher.Flux;
4445
import reactor.core.publisher.Mono;
4546

@@ -107,6 +108,9 @@ public class McpAsyncClient {
107108
public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() {
108109
};
109110

111+
public static final TypeRef<McpSchema.ElicitationCompleteNotification> ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF = new TypeRef<>() {
112+
};
113+
110114
public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";
111115

112116
/**
@@ -297,6 +301,16 @@ public class McpAsyncClient {
297301
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
298302
asyncProgressNotificationHandler(progressConsumersFinal));
299303

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

302316
if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
@@ -1037,6 +1051,18 @@ private NotificationHandler asyncProgressNotificationHandler(
10371051
};
10381052
}
10391053

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

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

Lines changed: 47 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
@@ -189,6 +189,8 @@ class SyncSpec {
189189

190190
private Function<ElicitRequest, ElicitResult> elicitationHandler;
191191

192+
private final List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers = new ArrayList<>();
193+
192194
private Supplier<McpTransportContext> contextProvider = () -> McpTransportContext.EMPTY;
193195

194196
private JsonSchemaValidator jsonSchemaValidator;
@@ -318,6 +320,21 @@ public SyncSpec elicitation(Function<ElicitRequest, ElicitResult> elicitationHan
318320
return this;
319321
}
320322

323+
/**
324+
* Adds a consumer to be notified when an elicitation complete notification is
325+
* received from the server. This allows the client to react when an out-of-band
326+
* URL elicitation interaction has completed.
327+
* @param consumer A consumer that receives the elicitation complete notification.
328+
* Must not be null.
329+
* @return This builder instance for method chaining
330+
* @throws IllegalArgumentException if consumer is null
331+
*/
332+
public SyncSpec elicitationCompleteConsumer(Consumer<McpSchema.ElicitationCompleteNotification> consumer) {
333+
Assert.notNull(consumer, "Elicitation complete consumer must not be null");
334+
this.elicitationCompleteConsumers.add(consumer);
335+
return this;
336+
}
337+
321338
/**
322339
* Adds a consumer to be notified when the available tools change. This allows the
323340
* client to react to changes in the server's tool capabilities, such as tools
@@ -488,7 +505,7 @@ public McpSyncClient build() {
488505
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
489506
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
490507
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
491-
this.elicitationHandler, this.enableCallToolSchemaCaching);
508+
this.elicitationHandler, this.enableCallToolSchemaCaching, this.elicitationCompleteConsumers);
492509

493510
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
494511

@@ -545,6 +562,8 @@ class AsyncSpec {
545562

546563
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
547564

565+
private final List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
566+
548567
private JsonSchemaValidator jsonSchemaValidator;
549568

550569
private boolean enableCallToolSchemaCaching = false; // Default to false
@@ -672,6 +691,22 @@ public AsyncSpec elicitation(Function<ElicitRequest, Mono<ElicitResult>> elicita
672691
return this;
673692
}
674693

694+
/**
695+
* Adds a consumer to be notified when an elicitation complete notification is
696+
* received from the server. This allows the client to react when an out-of-band
697+
* URL elicitation interaction has completed.
698+
* @param consumer A function that receives the elicitation complete notification
699+
* and returns a Mono signaling completion. Must not be null.
700+
* @return This builder instance for method chaining
701+
* @throws IllegalArgumentException if consumer is null
702+
*/
703+
public AsyncSpec elicitationCompleteConsumer(
704+
Function<McpSchema.ElicitationCompleteNotification, Mono<Void>> consumer) {
705+
Assert.notNull(consumer, "Elicitation complete consumer must not be null");
706+
this.elicitationCompleteConsumers.add(consumer);
707+
return this;
708+
}
709+
675710
/**
676711
* Adds a consumer to be notified when the available tools change. This allows the
677712
* client to react to changes in the server's tool capabilities, such as tools
@@ -833,7 +868,8 @@ public McpAsyncClient build() {
833868
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
834869
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
835870
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
836-
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
871+
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching,
872+
this.elicitationCompleteConsumers));
837873
}
838874

839875
}

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

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class McpClientFeatures {
6363
* @param samplingHandler the sampling handler.
6464
* @param elicitationHandler the elicitation handler.
6565
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
66+
* @param elicitationCompleteConsumers the elicitation complete notification
67+
* consumers.
6668
*/
6769
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
6870
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
@@ -73,7 +75,8 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
7375
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
7476
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
7577
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
76-
boolean enableCallToolSchemaCaching) {
78+
boolean enableCallToolSchemaCaching,
79+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
7780

7881
/**
7982
* Create an instance and validate the arguments.
@@ -87,6 +90,8 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
8790
* @param samplingHandler the sampling handler.
8891
* @param elicitationHandler the elicitation handler.
8992
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
93+
* @param elicitationCompleteConsumers the elicitation complete notification
94+
* consumers.
9095
*/
9196
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
9297
Map<String, McpSchema.Root> roots,
@@ -98,7 +103,8 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
98103
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
99104
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
100105
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler,
101-
boolean enableCallToolSchemaCaching) {
106+
boolean enableCallToolSchemaCaching,
107+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
102108

103109
Assert.notNull(clientInfo, "Client info must not be null");
104110
this.clientInfo = clientInfo;
@@ -119,6 +125,8 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
119125
this.samplingHandler = samplingHandler;
120126
this.elicitationHandler = elicitationHandler;
121127
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
128+
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
129+
: List.of();
122130
}
123131

124132
/**
@@ -135,7 +143,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
135143
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
136144
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
137145
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
138-
elicitationHandler, false);
146+
elicitationHandler, false, List.of());
139147
}
140148

141149
/**
@@ -191,10 +199,17 @@ public static Async fromSync(Sync syncSpec) {
191199
.fromCallable(() -> syncSpec.elicitationHandler().apply(r))
192200
.subscribeOn(Schedulers.boundedElastic());
193201

202+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers = new ArrayList<>();
203+
for (Consumer<McpSchema.ElicitationCompleteNotification> consumer : syncSpec
204+
.elicitationCompleteConsumers()) {
205+
elicitationCompleteConsumers.add(n -> Mono.<Void>fromRunnable(() -> consumer.accept(n))
206+
.subscribeOn(Schedulers.boundedElastic()));
207+
}
208+
194209
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
195210
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
196211
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler,
197-
syncSpec.enableCallToolSchemaCaching);
212+
syncSpec.enableCallToolSchemaCaching(), elicitationCompleteConsumers);
198213
}
199214
}
200215

@@ -213,6 +228,8 @@ public static Async fromSync(Sync syncSpec) {
213228
* @param samplingHandler the sampling handler.
214229
* @param elicitationHandler the elicitation handler.
215230
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
231+
* @param elicitationCompleteConsumers the elicitation complete notification
232+
* consumers.
216233
*/
217234
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
218235
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
@@ -223,22 +240,11 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
223240
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
224241
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
225242
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
226-
boolean enableCallToolSchemaCaching) {
243+
boolean enableCallToolSchemaCaching,
244+
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers) {
227245

228246
/**
229247
* Create an instance and validate the arguments.
230-
* @param clientInfo the client implementation information.
231-
* @param clientCapabilities the client capabilities.
232-
* @param roots the roots.
233-
* @param toolsChangeConsumers the tools change consumers.
234-
* @param resourcesChangeConsumers the resources change consumers.
235-
* @param resourcesUpdateConsumers the resource update consumers.
236-
* @param promptsChangeConsumers the prompts change consumers.
237-
* @param loggingConsumers the logging consumers.
238-
* @param progressConsumers the progress consumers.
239-
* @param samplingHandler the sampling handler.
240-
* @param elicitationHandler the elicitation handler.
241-
* @param enableCallToolSchemaCaching whether to enable call tool schema caching.
242248
*/
243249
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
244250
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
@@ -249,7 +255,8 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
249255
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
250256
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
251257
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler,
252-
boolean enableCallToolSchemaCaching) {
258+
boolean enableCallToolSchemaCaching,
259+
List<Consumer<McpSchema.ElicitationCompleteNotification>> elicitationCompleteConsumers) {
253260

254261
Assert.notNull(clientInfo, "Client info must not be null");
255262
this.clientInfo = clientInfo;
@@ -270,6 +277,8 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
270277
this.samplingHandler = samplingHandler;
271278
this.elicitationHandler = elicitationHandler;
272279
this.enableCallToolSchemaCaching = enableCallToolSchemaCaching;
280+
this.elicitationCompleteConsumers = elicitationCompleteConsumers != null ? elicitationCompleteConsumers
281+
: List.of();
273282
}
274283

275284
/**
@@ -285,7 +294,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
285294
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
286295
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
287296
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
288-
elicitationHandler, false);
297+
elicitationHandler, false, List.of());
289298
}
290299
}
291300

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

Lines changed: 24 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,33 @@ 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(
155+
"Client must be configured with URL elicitation capabilities to handle URL mode requests"));
156+
}
155157
return this.session.sendRequest(McpSchema.METHOD_ELICITATION_CREATE, elicitRequest,
156158
ELICITATION_RESULT_TYPE_REF);
157159
}
158160

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

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

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

103+
/**
104+
* Sends an elicitation complete notification to the client, indicating that an
105+
* out-of-band URL elicitation interaction has completed.
106+
* @param notification The notification containing the elicitation ID
107+
* @see McpSchema.ElicitationCompleteNotification
108+
*/
109+
public void sendElicitationComplete(McpSchema.ElicitationCompleteNotification notification) {
110+
this.exchange.sendElicitationComplete(notification).block();
111+
}
112+
103113
/**
104114
* Retrieves the list of all roots provided by the client.
105115
* @return The list of roots result.

0 commit comments

Comments
 (0)