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
113 changes: 99 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ To use the Spring integration module, add the following dependency:
</dependency>
```

The Spring integration module also requires the Spring AI dependency.

### Snapshot repositories

To use the mcp-annotations snapshot version you need to add the following repositories to your Maven POM:
Expand Down Expand Up @@ -587,6 +585,24 @@ public class LoggingHandler {
public void handleLoggingMessageWithParams(LoggingLevel level, String logger, String data) {
System.out.println("Received logging message with params: " + level + " - " + logger + " - " + data);
}

/**
* Handle logging message notifications for a specific client.
* @param notification The logging message notification
*/
@McpLoggingConsumer(clientId = "client-1")
public void handleClient1LoggingMessage(LoggingMessageNotification notification) {
System.out.println("Client-1 logging message: " + notification.level() + " - " + notification.data());
}

/**
* Handle logging message notifications for another specific client.
* @param notification The logging message notification
*/
@McpLoggingConsumer(clientId = "client-2")
public void handleClient2LoggingMessage(LoggingMessageNotification notification) {
System.out.println("Client-2 logging message: " + notification.level() + " - " + notification.data());
}
}

public class MyMcpClient {
Expand Down Expand Up @@ -627,6 +643,20 @@ public class SamplingHandler {
.model("test-model")
.build();
}

/**
* Handle sampling requests for a specific client.
* @param request The create message request
* @return The create message result
*/
@McpSampling(clientId = "client-1")
public CreateMessageResult handleClient1SamplingRequest(CreateMessageRequest request) {
return CreateMessageResult.builder()
.role(Role.ASSISTANT)
.content(new TextContent("Client-1 specific sampling response"))
.model("client-1-model")
.build();
}
}

public class AsyncSamplingHandler {
Expand All @@ -644,13 +674,30 @@ public class AsyncSamplingHandler {
.model("test-model")
.build());
}

/**
* Handle sampling requests for a specific client asynchronously.
* @param request The create message request
* @return A Mono containing the create message result
*/
@McpSampling(clientId = "client-2")
public Mono<CreateMessageResult> handleClient2AsyncSamplingRequest(CreateMessageRequest request) {
return Mono.just(CreateMessageResult.builder()
.role(Role.ASSISTANT)
.content(new TextContent("Client-2 async sampling response"))
.model("client-2-model")
.build());
}
}

public class MyMcpClient {

public static McpSyncClient createSyncClient(SamplingHandler samplingHandler) {
List<SyncSamplingSpecification> samplingSpecifications =
new SyncMcpSamplingProvider(List.of(samplingHandler)).getSamplingSpecifications();

Function<CreateMessageRequest, CreateMessageResult> samplingHandler =
new SyncMcpSamplingProvider(List.of(samplingHandler)).getSamplingHandler();
samplingSpecifications.get(0).samplingHandler();

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

public static McpAsyncClient createAsyncClient(AsyncSamplingHandler asyncSamplingHandler) {
List<AsyncSamplingSpecification> samplingSpecifications =
new AsyncMcpSamplingProvider(List.of(asyncSamplingHandler)).getSamplingSpecifications();

Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler =
new AsyncMcpSamplingProvider(List.of(asyncSamplingHandler)).getSamplingHandler();
samplingSpecifications.get(0).samplingHandler();

McpAsyncClient client = McpClient.async(transport)
.capabilities(ClientCapabilities.builder()
Expand Down Expand Up @@ -732,6 +782,19 @@ public class ElicitationHandler {
// Example of declining an elicitation request
return new ElicitResult(ElicitResult.Action.DECLINE, null);
}

/**
* Handle elicitation requests for a specific client.
* @param request The elicitation request
* @return The elicitation result
*/
@McpElicitation(clientId = "client-1")
public ElicitResult handleClient1ElicitationRequest(ElicitRequest request) {
Map<String, Object> userData = new HashMap<>();
userData.put("client", "client-1");
userData.put("response", "Client-1 specific elicitation response");
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}
}

public class AsyncElicitationHandler {
Expand Down Expand Up @@ -766,6 +829,22 @@ public class AsyncElicitationHandler {
public Mono<ElicitResult> handleCancelElicitationRequest(ElicitRequest request) {
return Mono.just(new ElicitResult(ElicitResult.Action.CANCEL, null));
}

/**
* Handle elicitation requests for a specific client asynchronously.
* @param request The elicitation request
* @return A Mono containing the elicitation result
*/
@McpElicitation(clientId = "client-2")
public Mono<ElicitResult> handleClient2AsyncElicitationRequest(ElicitRequest request) {
return Mono.fromCallable(() -> {
Map<String, Object> userData = new HashMap<>();
userData.put("client", "client-2");
userData.put("response", "Client-2 async elicitation response");
userData.put("timestamp", System.currentTimeMillis());
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}).delayElement(Duration.ofMillis(50));
}
}

public class MyMcpClient {
Expand Down Expand Up @@ -1022,33 +1101,39 @@ public class McpConfig {
}

@Bean
public List<Consumer<LoggingMessageNotification>> syncLoggingConsumers(
public List<SyncLoggingSpecification> syncLoggingSpecifications(
List<LoggingHandler> loggingHandlers) {
return SpringAiMcpAnnotationProvider.createSyncLoggingConsumers(loggingHandlers);
return SpringAiMcpAnnotationProvider.createSyncLoggingSpecifications(loggingHandlers);
}

@Bean
public List<AsyncLoggingSpecification> asyncLoggingSpecifications(
List<AsyncLoggingHandler> asyncLoggingHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncLoggingSpecifications(asyncLoggingHandlers);
}

@Bean
public Function<CreateMessageRequest, CreateMessageResult> syncSamplingHandler(
public List<SyncSamplingSpecification> syncSamplingSpecifications(
List<SamplingHandler> samplingHandlers) {
return SpringAiMcpAnnotationProvider.createSyncSamplingHandler(samplingHandlers);
return SpringAiMcpAnnotationProvider.createSyncSamplingSpecifications(samplingHandlers);
}

@Bean
public Function<CreateMessageRequest, Mono<CreateMessageResult>> asyncSamplingHandler(
public List<AsyncSamplingSpecification> asyncSamplingSpecifications(
List<AsyncSamplingHandler> asyncSamplingHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncSamplingHandler(asyncSamplingHandlers);
return SpringAiMcpAnnotationProvider.createAsyncSamplingSpecifications(asyncSamplingHandlers);
}

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

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

// Stateless Spring Integration Examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Function;

import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification;
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
import org.springaicommunity.mcp.provider.AsyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.AsyncMcpLoggingConsumerProvider;
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
Expand All @@ -29,12 +31,6 @@

import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
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;

/**
* @author Christian Tzolov
Expand Down Expand Up @@ -132,19 +128,17 @@ protected Method[] doGetClassMethods(Object bean) {

}

public static List<Function<LoggingMessageNotification, Mono<Void>>> createAsyncLoggingConsumers(
List<Object> loggingObjects) {
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
public static List<AsyncLoggingSpecification> createAsyncLoggingSpecifications(List<Object> loggingObjects) {
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingSpecifications();
}

public static Function<CreateMessageRequest, Mono<CreateMessageResult>> createAsyncSamplingHandler(
List<Object> samplingObjects) {
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
public static List<AsyncSamplingSpecification> createAsyncSamplingSpecifications(List<Object> samplingObjects) {
return new SpringAiAsyncMcpSamplingProvider(samplingObjects).getSamplingSpecifictions();
}

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

public static List<AsyncToolSpecification> createAsyncToolSpecifications(List<Object> toolObjects) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;

import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification;
import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification;
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
import org.springaicommunity.mcp.provider.SyncMcpCompletionProvider;
import org.springaicommunity.mcp.provider.SyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.SyncMcpLoggingConsumerProvider;
Expand All @@ -36,11 +37,6 @@
import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
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;

/**
* @author Christian Tzolov
Expand Down Expand Up @@ -208,17 +204,17 @@ public static List<McpStatelessServerFeatures.SyncResourceSpecification> createS
return new SpringAiSyncStatelessResourceProvider(resourceObjects).getResourceSpecifications();
}

public static List<Consumer<LoggingMessageNotification>> createSyncLoggingConsumers(List<Object> loggingObjects) {
return new SpringAiSyncMcpLoggingConsumerProvider(loggingObjects).getLoggingConsumers();
public static List<SyncLoggingSpecification> createSyncLoggingSpecifications(List<Object> loggingObjects) {
return new SpringAiSyncMcpLoggingConsumerProvider(loggingObjects).getLoggingSpecifications();
}

public static Function<CreateMessageRequest, CreateMessageResult> createSyncSamplingHandler(
List<Object> samplingObjects) {
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingHandler();
public static List<SyncSamplingSpecification> createSyncSamplingSpecifications(List<Object> samplingObjects) {
return new SpringAiSyncMcpSamplingProvider(samplingObjects).getSamplingSpecifications();
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@
@Documented
public @interface McpElicitation {

/**
* Used as connection or client identifier to select the MCP client, the elicitation
* method is associated with. If not specified, is applied to all clients.
*/
String clientId() default "";

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,10 @@
@Documented
public @interface McpLoggingConsumer {

/**
* Used as connection or client identifier to select the MCP client, the logging
* consumer is associated with. If not specified, is applied to all clients.
*/
String clientId() default "";

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@
@Documented
public @interface McpSampling {

/**
* Used as connection or client identifier to select the MCP client, the sampling
* method is associated with. If not specified, is applied to all clients.
*/
String clientId() default "";

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

package org.springaicommunity.mcp.method.elicitation;

import java.util.function.Function;

import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import reactor.core.publisher.Mono;

public record AsyncElicitationSpecification(String clientId,
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler) {

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

package org.springaicommunity.mcp.method.elicitation;

import java.util.function.Function;

import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;

public record SyncElicitationSpecification(String clientId, Function<ElicitRequest, ElicitResult> elicitationHandler) {

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

package org.springaicommunity.mcp.method.logging;

import java.util.function.Function;

import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import reactor.core.publisher.Mono;

public record AsyncLoggingSpecification(String clientId,
Function<LoggingMessageNotification, Mono<Void>> loggingHandler) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2025-2025 the original author or authors.
*/

package org.springaicommunity.mcp.method.logging;

import java.util.function.Consumer;

import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;

public record SyncLoggingSpecification(String clientId, Consumer<LoggingMessageNotification> loggingHandler) {
}
Loading