Skip to content

Commit cd7c67b

Browse files
authored
feat: add elicitation support to Spring annotation providers (#15)
- Add createAsyncElicitationHandler() method to AsyncMcpAnnotationProvider - Add createSyncElicitationHandler() method to SyncMcpAnnotationProvider - Implement SpringAiAsyncMcpElicitationProvider inner class - Implement SpringAiSyncMcpElicitationProvider inner class - Add ElicitRequest and ElicitResult imports to both providers - Update README with comprehensive elicitation documentation: - Add elicitation to core module operations list - Document @McpElicitation annotation - Add elicitation method callbacks documentation - Add elicitation providers documentation - Include complete usage examples for sync and async handlers - Add Spring integration examples for elicitation - Bump MCP Java SDK version from 0.11.0-SNAPSHOT to 0.12.0-SNAPSHOT This enables Spring applications to easily integrate MCP elicitation functionality using the familiar annotation-based approach, providing both synchronous and asynchronous implementations for gathering additional information from users. Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent 3b22fab commit cd7c67b

File tree

15 files changed

+1269
-3
lines changed

15 files changed

+1269
-3
lines changed

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ The core module provides a set of annotations and callback implementations for p
9292
4. **Tool** - For implementing MCP tools with automatic JSON schema generation
9393
5. **Logging Consumer** - For handling logging message notifications
9494
6. **Sampling** - For handling sampling requests
95+
7. **Elicitation** - For handling elicitation requests to gather additional information from users
9596

9697
Each operation type has both synchronous and asynchronous implementations, allowing for flexible integration with different application architectures.
9798

@@ -110,6 +111,7 @@ The Spring integration module provides seamless integration with Spring AI and S
110111
- **`@McpToolParam`** - Annotates tool method parameters with descriptions and requirement specifications
111112
- **`@McpLoggingConsumer`** - Annotates methods that handle logging message notifications from MCP servers
112113
- **`@McpSampling`** - Annotates methods that handle sampling requests from MCP servers
114+
- **`@McpElicitation`** - Annotates methods that handle elicitation requests to gather additional information from users
113115
- **`@McpArg`** - Annotates method parameters as MCP arguments
114116

115117
### Method Callbacks
@@ -145,6 +147,11 @@ The modules provide callback implementations for each operation type:
145147
- `SyncMcpSamplingMethodCallback` - Synchronous implementation
146148
- `AsyncMcpSamplingMethodCallback` - Asynchronous implementation using Reactor's Mono
147149

150+
#### Elicitation
151+
- `AbstractMcpElicitationMethodCallback` - Base class for elicitation method callbacks
152+
- `SyncMcpElicitationMethodCallback` - Synchronous implementation
153+
- `AsyncMcpElicitationMethodCallback` - Asynchronous implementation using Reactor's Mono
154+
148155
### Providers
149156

150157
The project includes provider classes that scan for annotated methods and create appropriate callbacks:
@@ -158,6 +165,8 @@ The project includes provider classes that scan for annotated methods and create
158165
- `AsyncMcpLoggingConsumerProvider` - Processes `@McpLoggingConsumer` annotations for asynchronous operations
159166
- `SyncMcpSamplingProvider` - Processes `@McpSampling` annotations for synchronous operations
160167
- `AsyncMcpSamplingProvider` - Processes `@McpSampling` annotations for asynchronous operations
168+
- `SyncMcpElicitationProvider` - Processes `@McpElicitation` annotations for synchronous operations
169+
- `AsyncMcpElicitationProvider` - Processes `@McpElicitation` annotations for asynchronous operations
161170

162171
### Spring Integration
163172

@@ -650,6 +659,128 @@ public class MyMcpClient {
650659
}
651660
```
652661

662+
### Mcp Client Elicitation Example
663+
664+
```java
665+
public class ElicitationHandler {
666+
667+
/**
668+
* Handle elicitation requests with a synchronous implementation.
669+
* @param request The elicitation request
670+
* @return The elicitation result
671+
*/
672+
@McpElicitation
673+
public ElicitResult handleElicitationRequest(ElicitRequest request) {
674+
// Example implementation that accepts the request and returns user data
675+
// In a real implementation, this would present a form to the user
676+
// and collect their input based on the requested schema
677+
678+
Map<String, Object> userData = new HashMap<>();
679+
680+
// Check what information is being requested based on the schema
681+
Map<String, Object> schema = request.requestedSchema();
682+
if (schema != null && schema.containsKey("properties")) {
683+
@SuppressWarnings("unchecked")
684+
Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
685+
686+
// Simulate user providing the requested information
687+
if (properties.containsKey("name")) {
688+
userData.put("name", "John Doe");
689+
}
690+
if (properties.containsKey("email")) {
691+
userData.put("email", "john.doe@example.com");
692+
}
693+
if (properties.containsKey("age")) {
694+
userData.put("age", 30);
695+
}
696+
if (properties.containsKey("preferences")) {
697+
userData.put("preferences", Map.of("theme", "dark", "notifications", true));
698+
}
699+
}
700+
701+
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
702+
}
703+
704+
/**
705+
* Handle elicitation requests that should be declined.
706+
* @param request The elicitation request
707+
* @return The elicitation result with decline action
708+
*/
709+
@McpElicitation
710+
public ElicitResult handleDeclineElicitationRequest(ElicitRequest request) {
711+
// Example of declining an elicitation request
712+
return new ElicitResult(ElicitResult.Action.DECLINE, null);
713+
}
714+
}
715+
716+
public class AsyncElicitationHandler {
717+
718+
/**
719+
* Handle elicitation requests with an asynchronous implementation.
720+
* @param request The elicitation request
721+
* @return A Mono containing the elicitation result
722+
*/
723+
@McpElicitation
724+
public Mono<ElicitResult> handleAsyncElicitationRequest(ElicitRequest request) {
725+
return Mono.fromCallable(() -> {
726+
// Simulate async processing of the elicitation request
727+
// In a real implementation, this might involve showing a UI form
728+
// and waiting for user input
729+
730+
Map<String, Object> userData = new HashMap<>();
731+
userData.put("response", "Async elicitation response");
732+
userData.put("timestamp", System.currentTimeMillis());
733+
userData.put("message", request.message());
734+
735+
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
736+
}).delayElement(Duration.ofMillis(100)); // Simulate processing delay
737+
}
738+
739+
/**
740+
* Handle elicitation requests that might be cancelled.
741+
* @param request The elicitation request
742+
* @return A Mono containing the elicitation result with cancel action
743+
*/
744+
@McpElicitation
745+
public Mono<ElicitResult> handleCancelElicitationRequest(ElicitRequest request) {
746+
return Mono.just(new ElicitResult(ElicitResult.Action.CANCEL, null));
747+
}
748+
}
749+
750+
public class MyMcpClient {
751+
752+
public static McpSyncClient createSyncClientWithElicitation(ElicitationHandler elicitationHandler) {
753+
Function<ElicitRequest, ElicitResult> elicitationHandler =
754+
new SyncMcpElicitationProvider(List.of(elicitationHandler)).getElicitationHandler();
755+
756+
McpSyncClient client = McpClient.sync(transport)
757+
.capabilities(ClientCapabilities.builder()
758+
.elicitation() // Enable elicitation support
759+
// Other capabilities...
760+
.build())
761+
.elicitationHandler(elicitationHandler)
762+
.build();
763+
764+
return client;
765+
}
766+
767+
public static McpAsyncClient createAsyncClientWithElicitation(AsyncElicitationHandler asyncElicitationHandler) {
768+
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler =
769+
new AsyncMcpElicitationProvider(List.of(asyncElicitationHandler)).getElicitationHandler();
770+
771+
McpAsyncClient client = McpClient.async(transport)
772+
.capabilities(ClientCapabilities.builder()
773+
.elicitation() // Enable elicitation support
774+
// Other capabilities...
775+
.build())
776+
.elicitationHandler(elicitationHandler)
777+
.build();
778+
779+
return client;
780+
}
781+
}
782+
```
783+
653784

654785
### Spring Integration Example
655786

@@ -704,6 +835,18 @@ public class McpConfig {
704835
List<AsyncSamplingHandler> asyncSamplingHandlers) {
705836
return SpringAiMcpAnnotationProvider.createAsyncSamplingHandler(asyncSamplingHandlers);
706837
}
838+
839+
@Bean
840+
public Function<ElicitRequest, ElicitResult> syncElicitationHandler(
841+
List<ElicitationHandler> elicitationHandlers) {
842+
return SpringAiMcpAnnotationProvider.createSyncElicitationHandler(elicitationHandlers);
843+
}
844+
845+
@Bean
846+
public Function<ElicitRequest, Mono<ElicitResult>> asyncElicitationHandler(
847+
List<AsyncElicitationHandler> asyncElicitationHandlers) {
848+
return SpringAiMcpAnnotationProvider.createAsyncElicitationHandler(asyncElicitationHandlers);
849+
}
707850
}
708851
```
709852

mcp-annotations-spring/src/main/java/org/springaicommunity/mcp/spring/AsyncMcpAnnotationProvider.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
1919
import java.util.List;
2020
import java.util.function.Function;
2121

22+
import org.springaicommunity.mcp.provider.AsyncMcpElicitationProvider;
2223
import org.springaicommunity.mcp.provider.AsyncMcpLoggingConsumerProvider;
2324
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
2425
import org.springaicommunity.mcp.provider.AsyncMcpToolProvider;
2526

2627
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
2728
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
2829
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
30+
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
31+
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
2932
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
3033
import reactor.core.publisher.Mono;
3134

@@ -60,6 +63,19 @@ protected Method[] doGetClassMethods(Object bean) {
6063

6164
}
6265

66+
private static class SpringAiAsyncMcpElicitationProvider extends AsyncMcpElicitationProvider {
67+
68+
public SpringAiAsyncMcpElicitationProvider(List<Object> elicitationObjects) {
69+
super(elicitationObjects);
70+
}
71+
72+
@Override
73+
protected Method[] doGetClassMethods(Object bean) {
74+
return AnnotationProviderUtil.beanMethods(bean);
75+
}
76+
77+
}
78+
6379
private static class SpringAiAsyncMcpToolProvider extends AsyncMcpToolProvider {
6480

6581
public SpringAiAsyncMcpToolProvider(List<Object> toolObjects) {
@@ -83,6 +99,11 @@ public static Function<CreateMessageRequest, Mono<CreateMessageResult>> createAs
8399
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
84100
}
85101

102+
public static Function<ElicitRequest, Mono<ElicitResult>> createAsyncElicitationHandler(
103+
List<Object> elicitationObjects) {
104+
return new SpringAiAsyncMcpElicitationProvider(elicitationObjects).getElicitationHandler();
105+
}
106+
86107
public static List<AsyncToolSpecification> createAsyncToolSpecifications(List<Object> toolObjects) {
87108
return new SpringAiAsyncMcpToolProvider(toolObjects).getToolSpecifications();
88109
}

mcp-annotations-spring/src/main/java/org/springaicommunity/mcp/spring/SyncMcpAnnotationProvider.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.function.Function;
2222

2323
import org.springaicommunity.mcp.provider.SyncMcpCompletionProvider;
24+
import org.springaicommunity.mcp.provider.SyncMcpElicitationProvider;
2425
import org.springaicommunity.mcp.provider.SyncMcpLoggingConsumerProvider;
2526
import org.springaicommunity.mcp.provider.SyncMcpPromptProvider;
2627
import org.springaicommunity.mcp.provider.SyncMcpResourceProvider;
@@ -33,6 +34,8 @@
3334
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
3435
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
3536
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
37+
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
38+
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
3639
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
3740

3841
/**
@@ -118,6 +121,19 @@ protected Method[] doGetClassMethods(Object bean) {
118121

119122
}
120123

124+
private static class SpringAiSyncMcpElicitationProvider extends SyncMcpElicitationProvider {
125+
126+
public SpringAiSyncMcpElicitationProvider(List<Object> elicitationObjects) {
127+
super(elicitationObjects);
128+
}
129+
130+
@Override
131+
protected Method[] doGetClassMethods(Object bean) {
132+
return AnnotationProviderUtil.beanMethods(bean);
133+
}
134+
135+
}
136+
121137
public static List<SyncToolSpecification> createSyncToolSpecifications(List<Object> toolObjects) {
122138
return new SpringAiSyncToolProvider(toolObjects).getToolSpecifications();
123139
}
@@ -143,4 +159,8 @@ public static Function<CreateMessageRequest, CreateMessageResult> createSyncSamp
143159
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
144160
}
145161

162+
public static Function<ElicitRequest, ElicitResult> createSyncElicitationHandler(List<Object> elicitationObjects) {
163+
return new SpringAiSyncMcpElicitationProvider(elicitationObjects).getElicitationHandler();
164+
}
165+
146166
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.annotation;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Annotation for methods that handle elicitation requests from MCP servers.
15+
*
16+
* <p>
17+
* Methods annotated with this annotation can be used to process elicitation requests from
18+
* MCP servers.
19+
*
20+
* <p>
21+
* For synchronous handlers, the method must return {@code ElicitResult}. For asynchronous
22+
* handlers, the method must return {@code Mono<ElicitResult>}.
23+
*
24+
* <p>
25+
* Example usage: <pre>{@code
26+
* &#64;McpElicitation
27+
* public ElicitResult handleElicitationRequest(ElicitRequest request) {
28+
* return ElicitResult.builder()
29+
* .message("Generated response")
30+
* .requestedSchema(
31+
* Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
32+
* .build();
33+
* }
34+
*
35+
* &#64;McpElicitation
36+
* public Mono<ElicitResult> handleAsyncElicitationRequest(ElicitRequest request) {
37+
* return Mono.just(ElicitResult.builder()
38+
* .message("Generated response")
39+
* .requestedSchema(
40+
* Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
41+
* .build());
42+
* }
43+
* }</pre>
44+
*
45+
* @author Christian Tzolov
46+
* @see io.modelcontextprotocol.spec.McpSchema.ElicitRequest
47+
* @see io.modelcontextprotocol.spec.McpSchema.ElicitResult
48+
*/
49+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
50+
@Retention(RetentionPolicy.RUNTIME)
51+
@Documented
52+
public @interface McpElicitation {
53+
54+
}

0 commit comments

Comments
 (0)