Skip to content

Commit 9cbe43b

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 c49a994 commit 9cbe43b

18 files changed

Lines changed: 1412 additions & 179 deletions

File tree

docs/client.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,28 @@ This capability allows:
270270
Elicitation enables servers to request additional information or user input through the client. This is useful when a server needs clarification or confirmation during an operation:
271271

272272
```java
273-
// Configure elicitation handler
274-
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
273+
// Configure form elicitation handler
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());
278278
return new ElicitResult(ElicitResult.Action.ACCEPT, userResponse);
279279
};
280280

281+
// Configure URL elicitation handler
282+
Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler = request -> {
283+
// Prompt the user to visit the URL
284+
// e.g. openBrowser(request.url());
285+
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of());
286+
};
287+
281288
// Create client with elicitation support
282289
var client = McpClient.sync(transport)
283290
.capabilities(ClientCapabilities.builder()
284-
.elicitation()
291+
.elicitation(true, true) // enables both form and URL elicitation
285292
.build())
286-
.elicitation(elicitationHandler)
293+
.elicitation(formElicitationHandler)
294+
.urlElicitation(urlElicitationHandler)
287295
.build();
288296
```
289297

@@ -293,6 +301,28 @@ The `ElicitResult` supports three actions:
293301
- `DECLINE` - The user declined to provide the information
294302
- `CANCEL` - The operation was cancelled
295303

304+
#### URL Elicitation Required Handling
305+
306+
When a server requires out-of-band URL elicitation but the client has not negotiated support for it (or the server strictly requires out-of-band handling), the server may return a `URL_ELICITATION_REQUIRED` error during tool execution or prompt retrieval.
307+
308+
```java
309+
try {
310+
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
311+
} catch (McpError e) {
312+
if (e.getJsonRpcError().code() == McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED) {
313+
// Extract elicitation requests from the error data
314+
Map<String, Object> data = (Map<String, Object>) e.getJsonRpcError().data();
315+
TypeRef<List<McpSchema.ElicitUrlRequest>> typeRef = new TypeRef<>() {};
316+
var requests = McpJsonDefaults.getMapper()
317+
.convertValue(data.get("elicitations"), typeRef);
318+
319+
for (var req : requests) {
320+
// handle elicitation requests
321+
}
322+
}
323+
}
324+
```
325+
296326
### Logging Support
297327

298328
The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages:

docs/server.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -715,9 +715,7 @@ var tool = SyncToolSpecification.builder()
715715
}
716716

717717
// Request user confirmation
718-
ElicitRequest elicitRequest = ElicitRequest.builder()
719-
.message("Do you want to proceed with this action?")
720-
.requestedSchema(Map.of(
718+
ElicitRequest elicitRequest = ElicitFormRequest.builder("Do you want to proceed with this action?", Map.of(
721719
"type", "object",
722720
"properties", Map.of("confirmed", Map.of("type", "boolean"))
723721
))
@@ -739,6 +737,34 @@ var tool = SyncToolSpecification.builder()
739737
.build();
740738
```
741739

740+
To request out-of-band URL elicitation, such as a user authorizing an OAuth flow:
741+
742+
```java
743+
var urlTool = SyncToolSpecification.builder()
744+
.tool(Tool.builder()
745+
.name("oauth-auth")
746+
.description("Authenticates via OAuth")
747+
.inputSchema(schema)
748+
.build())
749+
.callHandler((exchange, request) -> {
750+
// Request URL elicitation from client
751+
ElicitRequest urlRequest = McpSchema.ElicitUrlRequest.builder("Please authenticate", "https://example.com/oauth", "oauth-123").build();
752+
753+
ElicitResult result = exchange.elicit(urlRequest);
754+
755+
if (result.action() == ElicitResult.Action.ACCEPT) {
756+
return CallToolResult.builder()
757+
.content(List.of(new McpSchema.TextContent("Authentication successful")))
758+
.build();
759+
} else {
760+
return CallToolResult.builder()
761+
.content(List.of(new McpSchema.TextContent("Authentication failed or cancelled")))
762+
.build();
763+
}
764+
})
765+
.build();
766+
```
767+
742768
### Logging Support
743769

744770
The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.

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) {
@@ -552,23 +581,48 @@ private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
552581
};
553582
}
554583

555-
// --------------------------
556-
// Elicitation
557-
// --------------------------
558584
private RequestHandler<ElicitResult> elicitationCreateHandler() {
559585
return params -> {
560-
ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
586+
McpSchema.ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
561587
});
562588

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());
589+
if (request instanceof ElicitUrlRequest urlRequest) {
590+
if (this.urlElicitationHandler == null) {
591+
return Mono.error(new IllegalStateException(
592+
"Received URL elicitation request, but urlElicitation handler is null"));
569593
}
570-
return result;
571-
});
594+
return this.urlElicitationHandler.apply(urlRequest);
595+
}
596+
else if (request instanceof ElicitFormRequest formRequest) {
597+
if (this.formElicitationHandler == null) {
598+
return Mono.error(new IllegalStateException(
599+
"Received FORM elicitation request, but formElicitationHandler handler is null"));
600+
601+
}
602+
return this.formElicitationHandler.apply(formRequest).map(result -> {
603+
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
604+
&& result.content() != null) {
605+
Map<String, Object> merged = new HashMap<>(result.content());
606+
applyElicitationDefaults(formRequest.requestedSchema(), merged);
607+
return new ElicitResult(result.action(), merged, result.meta());
608+
}
609+
return result;
610+
});
611+
}
612+
613+
return Mono.error(new IllegalStateException("Unknown elictation type deserialized"));
614+
};
615+
}
616+
617+
private NotificationHandler asyncElicitationCompleteNotificationHandler(
618+
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
619+
return params -> {
620+
McpSchema.ElicitationCompleteNotification notification = transport.unmarshalFrom(params,
621+
ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF);
622+
623+
return Flux.fromIterable(elicitationCompleteConsumers)
624+
.flatMap(consumer -> consumer.apply(notification))
625+
.then();
572626
};
573627
}
574628

0 commit comments

Comments
 (0)