Skip to content

Commit cf779a8

Browse files
committed
feat: add sampling support and documentation
- Add sampling support with new annotations, callbacks, and providers: - New `@McpSampling` annotation for handling sampling requests - Sync and async sampling method callbacks and providers - Comprehensive test coverage for sampling functionality - Refactor Spring integration: - Rename SpringAiMcpAnnotationProvider to SyncMcpAnnotationProvider - Add AsyncMcpAnnotationProvider for better separation of concerns Resolves spring-ai-community#1 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent b44788a commit cf779a8

16 files changed

+1853
-3
lines changed

README.md

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ To use the mcp-annotations snapshot version you need to add the following reposi
8484

8585
### Core Module (mcp-annotations)
8686

87-
The core module provides a set of annotations and callback implementations for three primary MCP operations:
87+
The core module provides a set of annotations and callback implementations for primary MCP operations:
8888

8989
1. **Complete** - For auto-completion functionality in prompts and URI templates
9090
2. **Prompt** - For generating prompt messages
9191
3. **Resource** - For accessing resources via URI templates
92+
4. **Logging Consumer** - For handling logging message notifications
93+
5. **Sampling** - For handling sampling requests
9294

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

@@ -104,6 +106,7 @@ The Spring integration module provides seamless integration with Spring AI and S
104106
- **`@McpPrompt`** - Annotates methods that generate prompt messages
105107
- **`@McpResource`** - Annotates methods that provide access to resources
106108
- **`@McpLoggingConsumer`** - Annotates methods that handle logging message notifications from MCP servers
109+
- **`@McpSampling`** - Annotates methods that handle sampling requests from MCP servers
107110
- **`@McpArg`** - Annotates method parameters as MCP arguments
108111

109112
### Method Callbacks
@@ -130,6 +133,11 @@ The modules provide callback implementations for each operation type:
130133
- `SyncMcpLoggingConsumerMethodCallback` - Synchronous implementation
131134
- `AsyncMcpLoggingConsumerMethodCallback` - Asynchronous implementation using Reactor's Mono
132135

136+
#### Sampling
137+
- `AbstractMcpSamplingMethodCallback` - Base class for sampling method callbacks
138+
- `SyncMcpSamplingMethodCallback` - Synchronous implementation
139+
- `AsyncMcpSamplingMethodCallback` - Asynchronous implementation using Reactor's Mono
140+
133141
### Providers
134142

135143
The project includes provider classes that scan for annotated methods and create appropriate callbacks:
@@ -139,6 +147,8 @@ The project includes provider classes that scan for annotated methods and create
139147
- `SyncMcpResourceProvider` - Processes `@McpResource` annotations for synchronous operations
140148
- `SyncMcpLoggingConsumerProvider` - Processes `@McpLoggingConsumer` annotations for synchronous operations
141149
- `AsyncMcpLoggingConsumerProvider` - Processes `@McpLoggingConsumer` annotations for asynchronous operations
150+
- `SyncMcpSamplingProvider` - Processes `@McpSampling` annotations for synchronous operations
151+
- `AsyncMcpSamplingProvider` - Processes `@McpSampling` annotations for asynchronous operations
142152

143153
### Spring Integration
144154

@@ -399,6 +409,78 @@ public class MyMcpClient {
399409
}
400410
```
401411

412+
### Mcp Client Sampling Example
413+
414+
```java
415+
public class SamplingHandler {
416+
417+
/**
418+
* Handle sampling requests with a synchronous implementation.
419+
* @param request The create message request
420+
* @return The create message result
421+
*/
422+
@McpSampling
423+
public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) {
424+
// Process the request and generate a response
425+
return CreateMessageResult.builder()
426+
.role(Role.ASSISTANT)
427+
.content(new TextContent("This is a response to the sampling request"))
428+
.model("test-model")
429+
.build();
430+
}
431+
}
432+
433+
public class AsyncSamplingHandler {
434+
435+
/**
436+
* Handle sampling requests with an asynchronous implementation.
437+
* @param request The create message request
438+
* @return A Mono containing the create message result
439+
*/
440+
@McpSampling
441+
public Mono<CreateMessageResult> handleAsyncSamplingRequest(CreateMessageRequest request) {
442+
return Mono.just(CreateMessageResult.builder()
443+
.role(Role.ASSISTANT)
444+
.content(new TextContent("This is an async response to the sampling request"))
445+
.model("test-model")
446+
.build());
447+
}
448+
}
449+
450+
public class MyMcpClient {
451+
452+
public static McpSyncClient createSyncClient(SamplingHandler samplingHandler) {
453+
Function<CreateMessageRequest, CreateMessageResult> samplingHandler =
454+
new SyncMcpSamplingProvider(List.of(samplingHandler)).getSamplingHandler();
455+
456+
McpSyncClient client = McpClient.sync(transport)
457+
.capabilities(ClientCapabilities.builder()
458+
.sampling(true) // Enable sampling support
459+
// Other capabilities...
460+
.build())
461+
.samplingHandler(samplingHandler)
462+
.build();
463+
464+
return client;
465+
}
466+
467+
public static McpAsyncClient createAsyncClient(AsyncSamplingHandler asyncSamplingHandler) {
468+
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler =
469+
new AsyncMcpSamplingProvider(List.of(asyncSamplingHandler)).getSamplingHandler();
470+
471+
McpAsyncClient client = McpClient.async(transport)
472+
.capabilities(ClientCapabilities.builder()
473+
.sampling(true) // Enable sampling support
474+
// Other capabilities...
475+
.build())
476+
.samplingHandler(samplingHandler)
477+
.build();
478+
479+
return client;
480+
}
481+
}
482+
```
483+
402484

403485
### Spring Integration Example
404486

@@ -428,7 +510,19 @@ public class McpConfig {
428510
public List<Consumer<LoggingMessageNotification>> syncLoggingConsumers(
429511
List<LoggingHandler> loggingHandlers) {
430512
return SpringAiMcpAnnotationProvider.createSyncLoggingConsumers(loggingHandlers);
431-
}
513+
}
514+
515+
@Bean
516+
public Function<CreateMessageRequest, CreateMessageResult> syncSamplingHandler(
517+
List<SamplingHandler> samplingHandlers) {
518+
return SpringAiMcpAnnotationProvider.createSyncSamplingHandler(samplingHandlers);
519+
}
520+
521+
@Bean
522+
public Function<CreateMessageRequest, Mono<CreateMessageResult>> asyncSamplingHandler(
523+
List<AsyncSamplingHandler> asyncSamplingHandlers) {
524+
return SpringAiMcpAnnotationProvider.createAsyncSamplingHandler(asyncSamplingHandlers);
525+
}
432526
}
433527
```
434528

@@ -440,6 +534,7 @@ public class McpConfig {
440534
- **Comprehensive validation** - Ensures method signatures are compatible with MCP operations
441535
- **URI template support** - Powerful URI template handling for resource and completion operations
442536
- **Logging consumer support** - Handle logging message notifications from MCP servers
537+
- **Sampling support** - Handle sampling requests from MCP servers
443538
- **Spring integration** - Seamless integration with Spring Framework and Spring AI
444539
- **AOP proxy support** - Proper handling of Spring AOP proxies when processing annotations
445540

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.logaritex.mcp.spring;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.List;
20+
import java.util.function.Function;
21+
22+
import com.logaritex.mcp.provider.AsyncMcpLoggingConsumerProvider;
23+
import com.logaritex.mcp.provider.AsyncMcpSamplingProvider;
24+
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
25+
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
26+
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
27+
import reactor.core.publisher.Mono;
28+
29+
import org.springframework.aop.support.AopUtils;
30+
import org.springframework.util.ReflectionUtils;
31+
32+
/**
33+
* @author Christian Tzolov
34+
*/
35+
public class AsyncMcpAnnotationProvider {
36+
37+
private static class SpringAiAsyncMcpLoggingConsumerProvider extends AsyncMcpLoggingConsumerProvider {
38+
39+
public SpringAiAsyncMcpLoggingConsumerProvider(List<Object> loggingObjects) {
40+
super(loggingObjects);
41+
}
42+
43+
@Override
44+
protected Method[] doGetClassMethods(Object bean) {
45+
return ReflectionUtils
46+
.getDeclaredMethods(AopUtils.isAopProxy(bean) ? AopUtils.getTargetClass(bean) : bean.getClass());
47+
}
48+
49+
}
50+
51+
private static class SpringAiAsyncMcpSamplingProvider extends AsyncMcpSamplingProvider {
52+
53+
public SpringAiAsyncMcpSamplingProvider(List<Object> samplingObjects) {
54+
super(samplingObjects);
55+
}
56+
57+
@Override
58+
protected Method[] doGetClassMethods(Object bean) {
59+
return ReflectionUtils
60+
.getDeclaredMethods(AopUtils.isAopProxy(bean) ? AopUtils.getTargetClass(bean) : bean.getClass());
61+
}
62+
63+
}
64+
65+
public static List<Function<LoggingMessageNotification, Mono<Void>>> createAsyncLoggingConsumers(
66+
List<Object> loggingObjects) {
67+
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
68+
}
69+
70+
public static Function<CreateMessageRequest, Mono<CreateMessageResult>> createAsyncSamplingHandler(
71+
List<Object> samplingObjects) {
72+
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
73+
}
74+
75+
}

mcp-annotations-spring/src/main/java/com/logaritex/mcp/spring/SpringAiMcpAnnotationProvider.java renamed to mcp-annotations-spring/src/main/java/com/logaritex/mcp/spring/SyncMcpAnnotationProvider.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,29 @@
1818
import java.lang.reflect.Method;
1919
import java.util.List;
2020
import java.util.function.Consumer;
21+
import java.util.function.Function;
2122

23+
import com.logaritex.mcp.provider.AsyncMcpSamplingProvider;
2224
import com.logaritex.mcp.provider.SyncMcpCompletionProvider;
2325
import com.logaritex.mcp.provider.SyncMcpLoggingConsumerProvider;
2426
import com.logaritex.mcp.provider.SyncMcpPromptProvider;
2527
import com.logaritex.mcp.provider.SyncMcpResourceProvider;
28+
import com.logaritex.mcp.provider.SyncMcpSamplingProvider;
2629
import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification;
2730
import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification;
2831
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
32+
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
33+
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
2934
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
35+
import reactor.core.publisher.Mono;
3036

3137
import org.springframework.aop.support.AopUtils;
3238
import org.springframework.util.ReflectionUtils;
3339

3440
/**
3541
* @author Christian Tzolov
3642
*/
37-
public class SpringAiMcpAnnotationProvider {
43+
public class SyncMcpAnnotationProvider {
3844

3945
private static class SpringAiSyncMcpCompletionProvider extends SyncMcpCompletionProvider {
4046

@@ -92,6 +98,34 @@ protected Method[] doGetClassMethods(Object bean) {
9298

9399
}
94100

101+
private static class SpringAiSyncMcpSamplingProvider extends SyncMcpSamplingProvider {
102+
103+
public SpringAiSyncMcpSamplingProvider(List<Object> samplingObjects) {
104+
super(samplingObjects);
105+
}
106+
107+
@Override
108+
protected Method[] doGetClassMethods(Object bean) {
109+
return ReflectionUtils
110+
.getDeclaredMethods(AopUtils.isAopProxy(bean) ? AopUtils.getTargetClass(bean) : bean.getClass());
111+
}
112+
113+
}
114+
115+
private static class SpringAiAsyncMcpSamplingProvider extends AsyncMcpSamplingProvider {
116+
117+
public SpringAiAsyncMcpSamplingProvider(List<Object> samplingObjects) {
118+
super(samplingObjects);
119+
}
120+
121+
@Override
122+
protected Method[] doGetClassMethods(Object bean) {
123+
return ReflectionUtils
124+
.getDeclaredMethods(AopUtils.isAopProxy(bean) ? AopUtils.getTargetClass(bean) : bean.getClass());
125+
}
126+
127+
}
128+
95129
public static List<SyncCompletionSpecification> createSyncCompleteSpecifications(List<Object> completeObjects) {
96130
return new SpringAiSyncMcpCompletionProvider(completeObjects).getCompleteSpecifications();
97131
}
@@ -108,4 +142,14 @@ public static List<Consumer<LoggingMessageNotification>> createSyncLoggingConsum
108142
return new SpringAiSyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
109143
}
110144

145+
public static Function<CreateMessageRequest, CreateMessageResult> createSyncSamplingHandler(
146+
List<Object> samplingObjects) {
147+
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
148+
}
149+
150+
public static Function<CreateMessageRequest, Mono<CreateMessageResult>> createAsyncSamplingHandler(
151+
List<Object> samplingObjects) {
152+
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
153+
}
154+
111155
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package com.logaritex.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 sampling requests from MCP servers.
15+
*
16+
* <p>
17+
* Methods annotated with this annotation can be used to process sampling requests from
18+
* MCP servers. The methods can have one of two signatures:
19+
* <ul>
20+
* <li>A single parameter of type {@code CreateMessageRequest}
21+
* <li>Multiple parameters corresponding to the fields of {@code CreateMessageRequest}
22+
* </ul>
23+
*
24+
* <p>
25+
* For synchronous handlers, the method must return {@code CreateMessageResult}. For
26+
* asynchronous handlers, the method must return {@code Mono<CreateMessageResult>}.
27+
*
28+
* <p>
29+
* Example usage: <pre>{@code
30+
* &#64;McpSampling
31+
* public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) {
32+
* // Process the request and return a result
33+
* return CreateMessageResult.builder()
34+
* .message("Generated response")
35+
* .build();
36+
* }
37+
*
38+
* &#64;McpSampling
39+
* public Mono<CreateMessageResult> handleAsyncSamplingRequest(CreateMessageRequest request) {
40+
* // Process the request asynchronously and return a result
41+
* return Mono.just(CreateMessageResult.builder()
42+
* .message("Generated response")
43+
* .build());
44+
* }
45+
* }</pre>
46+
*
47+
* @author Christian Tzolov
48+
* @see io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest
49+
* @see io.modelcontextprotocol.spec.McpSchema.CreateMessageResult
50+
*/
51+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
52+
@Retention(RetentionPolicy.RUNTIME)
53+
@Documented
54+
public @interface McpSampling {
55+
56+
}

0 commit comments

Comments
 (0)