Skip to content
Merged
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
143 changes: 143 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ The core module provides a set of annotations and callback implementations for p
4. **Tool** - For implementing MCP tools with automatic JSON schema generation
5. **Logging Consumer** - For handling logging message notifications
6. **Sampling** - For handling sampling requests
7. **Elicitation** - For handling elicitation requests to gather additional information from users

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

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

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

#### Elicitation
- `AbstractMcpElicitationMethodCallback` - Base class for elicitation method callbacks
- `SyncMcpElicitationMethodCallback` - Synchronous implementation
- `AsyncMcpElicitationMethodCallback` - Asynchronous implementation using Reactor's Mono

### Providers

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

### Spring Integration

Expand Down Expand Up @@ -650,6 +659,128 @@ public class MyMcpClient {
}
```

### Mcp Client Elicitation Example

```java
public class ElicitationHandler {

/**
* Handle elicitation requests with a synchronous implementation.
* @param request The elicitation request
* @return The elicitation result
*/
@McpElicitation
public ElicitResult handleElicitationRequest(ElicitRequest request) {
// Example implementation that accepts the request and returns user data
// In a real implementation, this would present a form to the user
// and collect their input based on the requested schema

Map<String, Object> userData = new HashMap<>();

// Check what information is being requested based on the schema
Map<String, Object> schema = request.requestedSchema();
if (schema != null && schema.containsKey("properties")) {
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) schema.get("properties");

// Simulate user providing the requested information
if (properties.containsKey("name")) {
userData.put("name", "John Doe");
}
if (properties.containsKey("email")) {
userData.put("email", "[email protected]");
}
if (properties.containsKey("age")) {
userData.put("age", 30);
}
if (properties.containsKey("preferences")) {
userData.put("preferences", Map.of("theme", "dark", "notifications", true));
}
}

return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}

/**
* Handle elicitation requests that should be declined.
* @param request The elicitation request
* @return The elicitation result with decline action
*/
@McpElicitation
public ElicitResult handleDeclineElicitationRequest(ElicitRequest request) {
// Example of declining an elicitation request
return new ElicitResult(ElicitResult.Action.DECLINE, null);
}
}

public class AsyncElicitationHandler {

/**
* Handle elicitation requests with an asynchronous implementation.
* @param request The elicitation request
* @return A Mono containing the elicitation result
*/
@McpElicitation
public Mono<ElicitResult> handleAsyncElicitationRequest(ElicitRequest request) {
return Mono.fromCallable(() -> {
// Simulate async processing of the elicitation request
// In a real implementation, this might involve showing a UI form
// and waiting for user input

Map<String, Object> userData = new HashMap<>();
userData.put("response", "Async elicitation response");
userData.put("timestamp", System.currentTimeMillis());
userData.put("message", request.message());

return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}).delayElement(Duration.ofMillis(100)); // Simulate processing delay
}

/**
* Handle elicitation requests that might be cancelled.
* @param request The elicitation request
* @return A Mono containing the elicitation result with cancel action
*/
@McpElicitation
public Mono<ElicitResult> handleCancelElicitationRequest(ElicitRequest request) {
return Mono.just(new ElicitResult(ElicitResult.Action.CANCEL, null));
}
}

public class MyMcpClient {

public static McpSyncClient createSyncClientWithElicitation(ElicitationHandler elicitationHandler) {
Function<ElicitRequest, ElicitResult> elicitationHandler =
new SyncMcpElicitationProvider(List.of(elicitationHandler)).getElicitationHandler();

McpSyncClient client = McpClient.sync(transport)
.capabilities(ClientCapabilities.builder()
.elicitation() // Enable elicitation support
// Other capabilities...
.build())
.elicitationHandler(elicitationHandler)
.build();

return client;
}

public static McpAsyncClient createAsyncClientWithElicitation(AsyncElicitationHandler asyncElicitationHandler) {
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler =
new AsyncMcpElicitationProvider(List.of(asyncElicitationHandler)).getElicitationHandler();

McpAsyncClient client = McpClient.async(transport)
.capabilities(ClientCapabilities.builder()
.elicitation() // Enable elicitation support
// Other capabilities...
.build())
.elicitationHandler(elicitationHandler)
.build();

return client;
}
}
```


### Spring Integration Example

Expand Down Expand Up @@ -704,6 +835,18 @@ public class McpConfig {
List<AsyncSamplingHandler> asyncSamplingHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncSamplingHandler(asyncSamplingHandlers);
}

@Bean
public Function<ElicitRequest, ElicitResult> syncElicitationHandler(
List<ElicitationHandler> elicitationHandlers) {
return SpringAiMcpAnnotationProvider.createSyncElicitationHandler(elicitationHandlers);
}

@Bean
public Function<ElicitRequest, Mono<ElicitResult>> asyncElicitationHandler(
List<AsyncElicitationHandler> asyncElicitationHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncElicitationHandler(asyncElicitationHandlers);
}
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
import java.util.List;
import java.util.function.Function;

import org.springaicommunity.mcp.provider.AsyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.AsyncMcpLoggingConsumerProvider;
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
import org.springaicommunity.mcp.provider.AsyncMcpToolProvider;

import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import reactor.core.publisher.Mono;

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

}

private static class SpringAiAsyncMcpElicitationProvider extends AsyncMcpElicitationProvider {

public SpringAiAsyncMcpElicitationProvider(List<Object> elicitationObjects) {
super(elicitationObjects);
}

@Override
protected Method[] doGetClassMethods(Object bean) {
return AnnotationProviderUtil.beanMethods(bean);
}

}

private static class SpringAiAsyncMcpToolProvider extends AsyncMcpToolProvider {

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

public static Function<ElicitRequest, Mono<ElicitResult>> createAsyncElicitationHandler(
List<Object> elicitationObjects) {
return new SpringAiAsyncMcpElicitationProvider(elicitationObjects).getElicitationHandler();
}

public static List<AsyncToolSpecification> createAsyncToolSpecifications(List<Object> toolObjects) {
return new SpringAiAsyncMcpToolProvider(toolObjects).getToolSpecifications();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.function.Function;

import org.springaicommunity.mcp.provider.SyncMcpCompletionProvider;
import org.springaicommunity.mcp.provider.SyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.SyncMcpLoggingConsumerProvider;
import org.springaicommunity.mcp.provider.SyncMcpPromptProvider;
import org.springaicommunity.mcp.provider.SyncMcpResourceProvider;
Expand All @@ -33,6 +34,8 @@
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;

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

}

private static class SpringAiSyncMcpElicitationProvider extends SyncMcpElicitationProvider {

public SpringAiSyncMcpElicitationProvider(List<Object> elicitationObjects) {
super(elicitationObjects);
}

@Override
protected Method[] doGetClassMethods(Object bean) {
return AnnotationProviderUtil.beanMethods(bean);
}

}

public static List<SyncToolSpecification> createSyncToolSpecifications(List<Object> toolObjects) {
return new SpringAiSyncToolProvider(toolObjects).getToolSpecifications();
}
Expand All @@ -143,4 +159,8 @@ public static Function<CreateMessageRequest, CreateMessageResult> createSyncSamp
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
}

public static Function<ElicitRequest, ElicitResult> createSyncElicitationHandler(List<Object> elicitationObjects) {
return new SpringAiSyncMcpElicitationProvider(elicitationObjects).getElicitationHandler();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2025-2025 the original author or authors.
*/

package org.springaicommunity.mcp.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation for methods that handle elicitation requests from MCP servers.
*
* <p>
* Methods annotated with this annotation can be used to process elicitation requests from
* MCP servers.
*
* <p>
* For synchronous handlers, the method must return {@code ElicitResult}. For asynchronous
* handlers, the method must return {@code Mono<ElicitResult>}.
*
* <p>
* Example usage: <pre>{@code
* &#64;McpElicitation
* public ElicitResult handleElicitationRequest(ElicitRequest request) {
* return ElicitResult.builder()
* .message("Generated response")
* .requestedSchema(
* Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
* .build();
* }
*
* &#64;McpElicitation
* public Mono<ElicitResult> handleAsyncElicitationRequest(ElicitRequest request) {
* return Mono.just(ElicitResult.builder()
* .message("Generated response")
* .requestedSchema(
* Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
* .build());
* }
* }</pre>
*
* @author Christian Tzolov
* @see io.modelcontextprotocol.spec.McpSchema.ElicitRequest
* @see io.modelcontextprotocol.spec.McpSchema.ElicitResult
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpElicitation {

}
Loading