Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,28 @@ This capability allows:
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:

```java
// Configure elicitation handler
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
// Configure form elicitation handler
Function<ElicitFormRequest, ElicitResult> formElicitationHandler = request -> {
// Present the request to the user and collect their response
// The request contains a message and a schema describing the expected input
Map<String, Object> userResponse = collectUserInput(request.message(), request.requestedSchema());
return new ElicitResult(ElicitResult.Action.ACCEPT, userResponse);
};

// Configure URL elicitation handler
Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler = request -> {
// Prompt the user to visit the URL
// e.g. openBrowser(request.url());
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of());
};

// Create client with elicitation support
var client = McpClient.sync(transport)
.capabilities(ClientCapabilities.builder()
.elicitation()
.elicitation(true, true) // enables both form and URL elicitation
.build())
.elicitation(elicitationHandler)
.elicitation(formElicitationHandler)
.urlElicitation(urlElicitationHandler)
.build();
```

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

#### URL Elicitation Required Handling

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.

```java
try {
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
} catch (McpError e) {
if (e.getJsonRpcError().code() == McpSchema.ErrorCodes.URL_ELICITATION_REQUIRED) {
// Extract elicitation requests from the error data
Map<String, Object> data = (Map<String, Object>) e.getJsonRpcError().data();
TypeRef<List<McpSchema.ElicitUrlRequest>> typeRef = new TypeRef<>() {};
var requests = McpJsonDefaults.getMapper()
.convertValue(data.get("elicitations"), typeRef);

for (var req : requests) {
// handle elicitation requests
}
}
}
```

### Logging Support

The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages:
Expand Down
32 changes: 29 additions & 3 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,7 @@ var tool = SyncToolSpecification.builder()
}

// Request user confirmation
ElicitRequest elicitRequest = ElicitRequest.builder()
.message("Do you want to proceed with this action?")
.requestedSchema(Map.of(
ElicitRequest elicitRequest = ElicitFormRequest.builder("Do you want to proceed with this action?", Map.of(
"type", "object",
"properties", Map.of("confirmed", Map.of("type", "boolean"))
))
Expand All @@ -739,6 +737,34 @@ var tool = SyncToolSpecification.builder()
.build();
```

To request out-of-band URL elicitation, such as a user authorizing an OAuth flow:

```java
var urlTool = SyncToolSpecification.builder()
.tool(Tool.builder()
.name("oauth-auth")
.description("Authenticates via OAuth")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Request URL elicitation from client
ElicitRequest urlRequest = McpSchema.ElicitUrlRequest.builder("Please authenticate", "https://example.com/oauth", "oauth-123").build();

ElicitResult result = exchange.elicit(urlRequest);

if (result.action() == ElicitResult.Action.ACCEPT) {
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Authentication successful")))
.build();
} else {
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Authentication failed or cancelled")))
.build();
}
})
.build();
```

### Logging Support

The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public static final TypeRef<McpSchema.ElicitationCompleteNotification> ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF = new TypeRef<>() {
};

public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";

/**
Expand Down Expand Up @@ -145,7 +150,14 @@ public class McpAsyncClient {
* necessary information dynamically. Servers can request structured data from users
* with optional JSON schemas to validate responses.
*/
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;

/**
* MCP provides a standardized way for servers to request additional information from
* users out-of-band during interactions. This flow allows users to share information
* with the server without sharing it with the client.
*/
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;

/**
* Client transport implementation.
Expand Down Expand Up @@ -226,11 +238,18 @@ public class McpAsyncClient {

// Elicitation Handler
if (this.clientCapabilities.elicitation() != null) {
if (features.elicitationHandler() == null) {
if ((this.clientCapabilities.elicitation().url() == null
|| this.clientCapabilities.elicitation().form() != null)
&& features.formElicitationHandler() == null) {
throw new IllegalArgumentException(
"Elicitation handler must not be null when client capabilities include elicitation");
"Form elicitation handler must not be null when client capabilities include form elicitation");
}
this.elicitationHandler = features.elicitationHandler();
if (this.clientCapabilities.elicitation().url() != null && features.urlElicitationHandler() == null) {
throw new IllegalArgumentException(
"URL elicitation handler must not be null when client capabilities include URL elicitation");
}
this.formElicitationHandler = features.formElicitationHandler();
this.urlElicitationHandler = features.urlElicitationHandler();
requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
}

Expand Down Expand Up @@ -301,6 +320,16 @@ public class McpAsyncClient {
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
asyncProgressNotificationHandler(progressConsumersFinal));

// Elicitation Complete Notification
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumersFinal = new ArrayList<>();
elicitationCompleteConsumersFinal
.add((notification) -> Mono.fromRunnable(() -> logger.debug("Elicitation complete: {}", notification)));
if (!Utils.isEmpty(features.elicitationCompleteConsumers())) {
elicitationCompleteConsumersFinal.addAll(features.elicitationCompleteConsumers());
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ELICITATION_COMPLETE,
asyncElicitationCompleteNotificationHandler(elicitationCompleteConsumersFinal));

Function<Initialization, Mono<Void>> postInitializationHook = init -> {

if (init.initializeResult().capabilities().tools() == null || !enableCallToolSchemaCaching) {
Expand Down Expand Up @@ -552,23 +581,48 @@ private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
};
}

// --------------------------
// Elicitation
// --------------------------
private RequestHandler<ElicitResult> elicitationCreateHandler() {
return params -> {
ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
McpSchema.ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() {
});

return this.elicitationHandler.apply(request).map(result -> {
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
&& result.content() != null) {
Map<String, Object> merged = new HashMap<>(result.content());
applyElicitationDefaults(request.requestedSchema(), merged);
return new ElicitResult(result.action(), merged, result.meta());
if (request instanceof ElicitUrlRequest urlRequest) {
if (this.urlElicitationHandler == null) {
return Mono.error(new IllegalStateException(
"Received URL elicitation request, but urlElicitation handler is null"));
}
return result;
});
return this.urlElicitationHandler.apply(urlRequest);
}
else if (request instanceof ElicitFormRequest formRequest) {
if (this.formElicitationHandler == null) {
return Mono.error(new IllegalStateException(
"Received FORM elicitation request, but formElicitationHandler handler is null"));

}
return this.formElicitationHandler.apply(formRequest).map(result -> {
if (this.applyElicitationDefaults && result.action() == ElicitResult.Action.ACCEPT
&& result.content() != null) {
Map<String, Object> merged = new HashMap<>(result.content());
applyElicitationDefaults(formRequest.requestedSchema(), merged);
return new ElicitResult(result.action(), merged, result.meta());
}
return result;
});
}

return Mono.error(new IllegalStateException("Unknown elictation type deserialized"));
};
}

private NotificationHandler asyncElicitationCompleteNotificationHandler(
List<Function<McpSchema.ElicitationCompleteNotification, Mono<Void>>> elicitationCompleteConsumers) {
return params -> {
McpSchema.ElicitationCompleteNotification notification = transport.unmarshalFrom(params,
ELICITATION_COMPLETE_NOTIFICATION_TYPE_REF);

return Flux.fromIterable(elicitationCompleteConsumers)
.flatMap(consumer -> consumer.apply(notification))
.then();
};
}

Expand Down
Loading
Loading