Skip to content

Commit a25d4f0

Browse files
authored
feat: add client-specific support and refactor to specification-based architecture (#17)
- Add clientId parameter to @McpElicitation, @McpLoggingConsumer, and @McpSampling annotations - Refactor providers to return specification lists instead of single handlers/consumers - Introduce AsyncElicitationSpecification, SyncElicitationSpecification, AsyncLoggingSpecification, SyncLoggingSpecification, AsyncSamplingSpecification, and SyncSamplingSpecification classes - Update Spring integration to use specification-based approach - Update all provider method names from Handler/Consumer to Specifications BREAKING CHANGE: Provider APIs now return specification lists instead of single handlers/consumers Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent 489b91b commit a25d4f0

24 files changed

+304
-94
lines changed

README.md

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ To use the Spring integration module, add the following dependency:
5252
</dependency>
5353
```
5454

55-
The Spring integration module also requires the Spring AI dependency.
56-
5755
### Snapshot repositories
5856

5957
To use the mcp-annotations snapshot version you need to add the following repositories to your Maven POM:
@@ -587,6 +585,24 @@ public class LoggingHandler {
587585
public void handleLoggingMessageWithParams(LoggingLevel level, String logger, String data) {
588586
System.out.println("Received logging message with params: " + level + " - " + logger + " - " + data);
589587
}
588+
589+
/**
590+
* Handle logging message notifications for a specific client.
591+
* @param notification The logging message notification
592+
*/
593+
@McpLoggingConsumer(clientId = "client-1")
594+
public void handleClient1LoggingMessage(LoggingMessageNotification notification) {
595+
System.out.println("Client-1 logging message: " + notification.level() + " - " + notification.data());
596+
}
597+
598+
/**
599+
* Handle logging message notifications for another specific client.
600+
* @param notification The logging message notification
601+
*/
602+
@McpLoggingConsumer(clientId = "client-2")
603+
public void handleClient2LoggingMessage(LoggingMessageNotification notification) {
604+
System.out.println("Client-2 logging message: " + notification.level() + " - " + notification.data());
605+
}
590606
}
591607

592608
public class MyMcpClient {
@@ -627,6 +643,20 @@ public class SamplingHandler {
627643
.model("test-model")
628644
.build();
629645
}
646+
647+
/**
648+
* Handle sampling requests for a specific client.
649+
* @param request The create message request
650+
* @return The create message result
651+
*/
652+
@McpSampling(clientId = "client-1")
653+
public CreateMessageResult handleClient1SamplingRequest(CreateMessageRequest request) {
654+
return CreateMessageResult.builder()
655+
.role(Role.ASSISTANT)
656+
.content(new TextContent("Client-1 specific sampling response"))
657+
.model("client-1-model")
658+
.build();
659+
}
630660
}
631661

632662
public class AsyncSamplingHandler {
@@ -644,13 +674,30 @@ public class AsyncSamplingHandler {
644674
.model("test-model")
645675
.build());
646676
}
677+
678+
/**
679+
* Handle sampling requests for a specific client asynchronously.
680+
* @param request The create message request
681+
* @return A Mono containing the create message result
682+
*/
683+
@McpSampling(clientId = "client-2")
684+
public Mono<CreateMessageResult> handleClient2AsyncSamplingRequest(CreateMessageRequest request) {
685+
return Mono.just(CreateMessageResult.builder()
686+
.role(Role.ASSISTANT)
687+
.content(new TextContent("Client-2 async sampling response"))
688+
.model("client-2-model")
689+
.build());
690+
}
647691
}
648692

649693
public class MyMcpClient {
650694

651695
public static McpSyncClient createSyncClient(SamplingHandler samplingHandler) {
696+
List<SyncSamplingSpecification> samplingSpecifications =
697+
new SyncMcpSamplingProvider(List.of(samplingHandler)).getSamplingSpecifications();
698+
652699
Function<CreateMessageRequest, CreateMessageResult> samplingHandler =
653-
new SyncMcpSamplingProvider(List.of(samplingHandler)).getSamplingHandler();
700+
samplingSpecifications.get(0).samplingHandler();
654701

655702
McpSyncClient client = McpClient.sync(transport)
656703
.capabilities(ClientCapabilities.builder()
@@ -664,8 +711,11 @@ public class MyMcpClient {
664711
}
665712

666713
public static McpAsyncClient createAsyncClient(AsyncSamplingHandler asyncSamplingHandler) {
714+
List<AsyncSamplingSpecification> samplingSpecifications =
715+
new AsyncMcpSamplingProvider(List.of(asyncSamplingHandler)).getSamplingSpecifications();
716+
667717
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler =
668-
new AsyncMcpSamplingProvider(List.of(asyncSamplingHandler)).getSamplingHandler();
718+
samplingSpecifications.get(0).samplingHandler();
669719

670720
McpAsyncClient client = McpClient.async(transport)
671721
.capabilities(ClientCapabilities.builder()
@@ -732,6 +782,19 @@ public class ElicitationHandler {
732782
// Example of declining an elicitation request
733783
return new ElicitResult(ElicitResult.Action.DECLINE, null);
734784
}
785+
786+
/**
787+
* Handle elicitation requests for a specific client.
788+
* @param request The elicitation request
789+
* @return The elicitation result
790+
*/
791+
@McpElicitation(clientId = "client-1")
792+
public ElicitResult handleClient1ElicitationRequest(ElicitRequest request) {
793+
Map<String, Object> userData = new HashMap<>();
794+
userData.put("client", "client-1");
795+
userData.put("response", "Client-1 specific elicitation response");
796+
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
797+
}
735798
}
736799

737800
public class AsyncElicitationHandler {
@@ -766,6 +829,22 @@ public class AsyncElicitationHandler {
766829
public Mono<ElicitResult> handleCancelElicitationRequest(ElicitRequest request) {
767830
return Mono.just(new ElicitResult(ElicitResult.Action.CANCEL, null));
768831
}
832+
833+
/**
834+
* Handle elicitation requests for a specific client asynchronously.
835+
* @param request The elicitation request
836+
* @return A Mono containing the elicitation result
837+
*/
838+
@McpElicitation(clientId = "client-2")
839+
public Mono<ElicitResult> handleClient2AsyncElicitationRequest(ElicitRequest request) {
840+
return Mono.fromCallable(() -> {
841+
Map<String, Object> userData = new HashMap<>();
842+
userData.put("client", "client-2");
843+
userData.put("response", "Client-2 async elicitation response");
844+
userData.put("timestamp", System.currentTimeMillis());
845+
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
846+
}).delayElement(Duration.ofMillis(50));
847+
}
769848
}
770849

771850
public class MyMcpClient {
@@ -1022,33 +1101,39 @@ public class McpConfig {
10221101
}
10231102

10241103
@Bean
1025-
public List<Consumer<LoggingMessageNotification>> syncLoggingConsumers(
1104+
public List<SyncLoggingSpecification> syncLoggingSpecifications(
10261105
List<LoggingHandler> loggingHandlers) {
1027-
return SpringAiMcpAnnotationProvider.createSyncLoggingConsumers(loggingHandlers);
1106+
return SpringAiMcpAnnotationProvider.createSyncLoggingSpecifications(loggingHandlers);
1107+
}
1108+
1109+
@Bean
1110+
public List<AsyncLoggingSpecification> asyncLoggingSpecifications(
1111+
List<AsyncLoggingHandler> asyncLoggingHandlers) {
1112+
return SpringAiMcpAnnotationProvider.createAsyncLoggingSpecifications(asyncLoggingHandlers);
10281113
}
10291114

10301115
@Bean
1031-
public Function<CreateMessageRequest, CreateMessageResult> syncSamplingHandler(
1116+
public List<SyncSamplingSpecification> syncSamplingSpecifications(
10321117
List<SamplingHandler> samplingHandlers) {
1033-
return SpringAiMcpAnnotationProvider.createSyncSamplingHandler(samplingHandlers);
1118+
return SpringAiMcpAnnotationProvider.createSyncSamplingSpecifications(samplingHandlers);
10341119
}
10351120

10361121
@Bean
1037-
public Function<CreateMessageRequest, Mono<CreateMessageResult>> asyncSamplingHandler(
1122+
public List<AsyncSamplingSpecification> asyncSamplingSpecifications(
10381123
List<AsyncSamplingHandler> asyncSamplingHandlers) {
1039-
return SpringAiMcpAnnotationProvider.createAsyncSamplingHandler(asyncSamplingHandlers);
1124+
return SpringAiMcpAnnotationProvider.createAsyncSamplingSpecifications(asyncSamplingHandlers);
10401125
}
10411126

10421127
@Bean
1043-
public Function<ElicitRequest, ElicitResult> syncElicitationHandler(
1128+
public List<SyncElicitationSpecification> syncElicitationSpecifications(
10441129
List<ElicitationHandler> elicitationHandlers) {
1045-
return SpringAiMcpAnnotationProvider.createSyncElicitationHandler(elicitationHandlers);
1130+
return SpringAiMcpAnnotationProvider.createSyncElicitationSpecifications(elicitationHandlers);
10461131
}
10471132

10481133
@Bean
1049-
public Function<ElicitRequest, Mono<ElicitResult>> asyncElicitationHandler(
1134+
public List<AsyncElicitationSpecification> asyncElicitationSpecifications(
10501135
List<AsyncElicitationHandler> asyncElicitationHandlers) {
1051-
return SpringAiMcpAnnotationProvider.createAsyncElicitationHandler(asyncElicitationHandlers);
1136+
return SpringAiMcpAnnotationProvider.createAsyncElicitationSpecifications(asyncElicitationHandlers);
10521137
}
10531138

10541139
// Stateless Spring Integration Examples

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

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import java.lang.reflect.Method;
1919
import java.util.List;
20-
import java.util.function.Function;
2120

21+
import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification;
22+
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
23+
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
2224
import org.springaicommunity.mcp.provider.AsyncMcpElicitationProvider;
2325
import org.springaicommunity.mcp.provider.AsyncMcpLoggingConsumerProvider;
2426
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
@@ -29,12 +31,6 @@
2931

3032
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
3133
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
32-
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
33-
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
34-
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
35-
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
36-
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
37-
import reactor.core.publisher.Mono;
3834

3935
/**
4036
* @author Christian Tzolov
@@ -132,19 +128,17 @@ protected Method[] doGetClassMethods(Object bean) {
132128

133129
}
134130

135-
public static List<Function<LoggingMessageNotification, Mono<Void>>> createAsyncLoggingConsumers(
136-
List<Object> loggingObjects) {
137-
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
131+
public static List<AsyncLoggingSpecification> createAsyncLoggingSpecifications(List<Object> loggingObjects) {
132+
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingSpecifications();
138133
}
139134

140-
public static Function<CreateMessageRequest, Mono<CreateMessageResult>> createAsyncSamplingHandler(
141-
List<Object> samplingObjects) {
142-
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
135+
public static List<AsyncSamplingSpecification> createAsyncSamplingSpecifications(List<Object> samplingObjects) {
136+
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingSpecifictions();
143137
}
144138

145-
public static Function<ElicitRequest, Mono<ElicitResult>> createAsyncElicitationHandler(
139+
public static List<AsyncElicitationSpecification> createAsyncElicitationSpecifications(
146140
List<Object> elicitationObjects) {
147-
return new SpringAiAsyncMcpElicitationProvider(elicitationObjects).getElicitationHandler();
141+
return new SpringAiAsyncMcpElicitationProvider(elicitationObjects).getElicitationSpecifications();
148142
}
149143

150144
public static List<AsyncToolSpecification> createAsyncToolSpecifications(List<Object> toolObjects) {

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717

1818
import java.lang.reflect.Method;
1919
import java.util.List;
20-
import java.util.function.Consumer;
21-
import java.util.function.Function;
2220

21+
import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification;
22+
import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification;
23+
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
2324
import org.springaicommunity.mcp.provider.SyncMcpCompletionProvider;
2425
import org.springaicommunity.mcp.provider.SyncMcpElicitationProvider;
2526
import org.springaicommunity.mcp.provider.SyncMcpLoggingConsumerProvider;
@@ -36,11 +37,6 @@
3637
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
3738
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
3839
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
39-
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
40-
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
41-
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
42-
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
43-
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
4440

4541
/**
4642
* @author Christian Tzolov
@@ -208,17 +204,17 @@ public static List<McpStatelessServerFeatures.SyncResourceSpecification> createS
208204
return new SpringAiSyncStatelessResourceProvider(resourceObjects).getResourceSpecifications();
209205
}
210206

211-
public static List<Consumer<LoggingMessageNotification>> createSyncLoggingConsumers(List<Object> loggingObjects) {
212-
return new SpringAiSyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
207+
public static List<SyncLoggingSpecification> createSyncLoggingSpecifications(List<Object> loggingObjects) {
208+
return new SpringAiSyncMcpLoggingConsumerProvider(loggingObjects).getLoggingSpecifications();
213209
}
214210

215-
public static Function<CreateMessageRequest, CreateMessageResult> createSyncSamplingHandler(
216-
List<Object> samplingObjects) {
217-
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
211+
public static List<SyncSamplingSpecification> createSyncSamplingSpecifications(List<Object> samplingObjects) {
212+
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingSpecifications();
218213
}
219214

220-
public static Function<ElicitRequest, ElicitResult> createSyncElicitationHandler(List<Object> elicitationObjects) {
221-
return new SpringAiSyncMcpElicitationProvider(elicitationObjects).getElicitationHandler();
215+
public static List<SyncElicitationSpecification> createSyncElicitationSpecifications(
216+
List<Object> elicitationObjects) {
217+
return new SpringAiSyncMcpElicitationProvider(elicitationObjects).getElicitationSpecifications();
222218
}
223219

224220
}

mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpElicitation.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,10 @@
5151
@Documented
5252
public @interface McpElicitation {
5353

54+
/**
55+
* Used as connection or client identifier to select the MCP client, the elicitation
56+
* method is associated with. If not specified, is applied to all clients.
57+
*/
58+
String clientId() default "";
59+
5460
}

mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpLoggingConsumer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,10 @@
5050
@Documented
5151
public @interface McpLoggingConsumer {
5252

53+
/**
54+
* Used as connection or client identifier to select the MCP client, the logging
55+
* consumer is associated with. If not specified, is applied to all clients.
56+
*/
57+
String clientId() default "";
58+
5359
}

mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpSampling.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,10 @@
5353
@Documented
5454
public @interface McpSampling {
5555

56+
/**
57+
* Used as connection or client identifier to select the MCP client, the sampling
58+
* method is associated with. If not specified, is applied to all clients.
59+
*/
60+
String clientId() default "";
61+
5662
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.method.elicitation;
6+
7+
import java.util.function.Function;
8+
9+
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
10+
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
11+
import reactor.core.publisher.Mono;
12+
13+
public record AsyncElicitationSpecification(String clientId,
14+
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler) {
15+
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.method.elicitation;
6+
7+
import java.util.function.Function;
8+
9+
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
10+
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
11+
12+
public record SyncElicitationSpecification(String clientId, Function<ElicitRequest, ElicitResult> elicitationHandler) {
13+
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.method.logging;
6+
7+
import java.util.function.Function;
8+
9+
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
10+
import reactor.core.publisher.Mono;
11+
12+
public record AsyncLoggingSpecification(String clientId,
13+
Function<LoggingMessageNotification, Mono<Void>> loggingHandler) {
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package org.springaicommunity.mcp.method.logging;
6+
7+
import java.util.function.Consumer;
8+
9+
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
10+
11+
public record SyncLoggingSpecification(String clientId, Consumer<LoggingMessageNotification> loggingHandler) {
12+
}

0 commit comments

Comments
 (0)