The MCP Annotations project provides annotation-based method handling for Model Context Protocol (MCP) servers in Java. It simplifies the creation and registration of MCP server methods through a clean, declarative approach using Java annotations.
- Overview
- Installation
- Key Components
- Usage Examples
- Features
- Requirements
- Building from Source
- Contributing
This project consists of two main modules:
- mcp-annotations - Core annotations and method handling for MCP operations. Depends only on MCP Java SDK.
- spring-ai-mcp-annotations - Spring AI integration for MCP annotations
The MCP Annotations project enables developers to easily create and register methods for handling MCP operations using simple annotations. It provides a clean, declarative approach to implementing MCP server functionality, reducing boilerplate code and improving maintainability.
This library builds on top of the MCP Java SDK to provide a higher-level, annotation-based programming model for implementing MCP servers and clients.
To use the MCP Annotations core module in your project, add the following dependency to your Maven POM file:
<dependency>
<groupId>com.logaritex.mcp</groupId>
<artifactId>mcp-annotations</artifactId>
<version>0.1.0</version>
</dependency>
To use the Spring integration module, add the following dependency:
<dependency>
<groupId>com.logaritex.mcp</groupId>
<artifactId>spring-ai-mcp-annotations</artifactId>
<version>0.1.0</version>
<
10000
;/dependency>
The Spring integration module also requires the Spring AI dependency.
To use the mcp-annotations snapshot version you need to add the following repositories to your Maven POM:
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
The core module provides a set of annotations and callback implementations for primary MCP operations:
- Complete - For auto-completion functionality in prompts and URI templates
- Prompt - For generating prompt messages
- Resource - For accessing resources via URI templates
- Logging Consumer - For handling logging message notifications
- Sampling - For handling sampling requests
Each operation type has both synchronous and asynchronous implementations, allowing for flexible integration with different application architectures.
The Spring integration module provides seamless integration with Spring AI and Spring Framework applications. It handles Spring-specific concerns such as AOP proxies and integrates with Spring AI's model abstractions.
@McpComplete
- Annotates methods that provide completion functionality for prompts or URI templates@McpPrompt
- Annotates methods that generate prompt messages@McpResource
- Annotates methods that provide access to resources@McpLoggingConsumer
- Annotates methods that handle logging message notifications from MCP servers@McpSampling
- Annotates methods that handle sampling requests from MCP servers@McpArg
- Annotates method parameters as MCP arguments
The modules provide callback implementations for each operation type:
AbstractMcpCompleteMethodCallback
- Base class for complete method callbacksSyncMcpCompleteMethodCallback
- Synchronous implementationAsyncMcpCompleteMethodCallback
- Asynchronous implementation using Reactor's Mono
AbstractMcpPromptMethodCallback
- Base class for prompt method callbacksSyncMcpPromptMethodCallback
- Synchronous implementationAsyncMcpPromptMethodCallback
- Asynchronous implementation using Reactor's Mono
AbstractMcpResourceMethodCallback
- Base class for resource method callbacksSyncMcpResourceMethodCallback
- Synchronous implementationAsyncMcpResourceMethodCallback
- Asynchronous implementation using Reactor's Mono
AbstractMcpLoggingConsumerMethodCallback
- Base class for logging consumer method callbacksSyncMcpLoggingConsumerMethodCallback
- Synchronous implementationAsyncMcpLoggingConsumerMethodCallback
- Asynchronous implementation using Reactor's Mono
AbstractMcpSamplingMethodCallback
- Base class for sampling method callbacksSyncMcpSamplingMethodCallback
- Synchronous implementationAsyncMcpSamplingMethodCallback
- Asynchronous implementation using Reactor's Mono
The project includes provider classes that scan for annotated methods and create appropriate callbacks:
SyncMcpCompletionProvider
- Processes@McpComplete
annotations for synchronous operationsSyncMcpPromptProvider
- Processes@McpPrompt
annotations for synchronous operationsSyncMcpResourceProvider
- Processes@McpResource
annotations for synchronous operationsSyncMcpLoggingConsumerProvider
- Processes@McpLoggingConsumer
annotations for synchronous operationsAsyncMcpLoggingConsumerProvider
- Processes@McpLoggingConsumer
annotations for asynchronous operationsSyncMcpSamplingProvider
- Processes@McpSampling
annotations for synchronous operationsAsyncMcpSamplingProvider
- Processes@McpSampling
annotations for asynchronous operations
The Spring integration module provides:
SpringAiMcpAnnotationProvider
- Handles Spring-specific concerns when processing MCP annotations- Integration with Spring AOP proxies
- Support for Spring AI model abstractions
public class PromptProvider {
@McpPrompt(name = "personalized-message",
description = "Generates a personalized message based on user information")
public GetPromptResult personalizedMessage(McpSyncServerExchange exchange,
@McpArg(name = "name", description = "The user's name", required = true) String name,
@McpArg(name = "age", description = "The user's age", required = false) Integer age,
@McpArg(name = "interests", description = "The user's interests", required = false) String interests) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("personalized-message event").build());
StringBuilder message = new StringBuilder();
message.append("Hello, ").append(name).append("!\n\n");
if (age != null) {
message.append("At ").append(age).append(" years old, you have ");
if (age < 30) {
message.append("so much ahead of you.\n\n");
}
else if (age < 60) {
message.append("gained valuable life experience.\n\n");
}
else {
message.append("accumulated wisdom to share with others.\n\n");
}
}
if (interests != null && !interests.isEmpty()) {
message.append("Your interest in ")
.append(interests)
.append(" shows your curiosity and passion for learning.\n\n");
}
message
.append("I'm here to assist you with any questions you might have about the Model Context Protocol.");
return new GetPromptResult("Personalized Message",
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message.toString()))));
}
}
public class AutocompleteProvider {
private final Map<String, List<String>> usernameDatabase = new HashMap<>();
private final Map<String, List<String>> cityDatabase = new HashMap<>();
public AutocompleteProvider() {
// Initialize with sample data
cityDatabase.put("l", List.of("Lagos", "Lima", "Lisbon", "London", "Los Angeles"));
// ....
usernameDatabase.put("a", List.of("alex123", "admin", "alice_wonder", "andrew99"));
// Add more data...
}
@McpComplete(prompt = "personalized-message")
public List<String> completeName(String name) {
String prefix = name.toLowerCase();
String firstLetter = prefix.substring(0, 1);
List<String> usernames = usernameDatabase.getOrDefault(firstLetter, List.of());
return usernames.stream().filter(username -> username.toLowerCase().startsWith(prefix)).toList();
}
@McpComplete(prompt = "travel-planner")
public List<String> completeCityName(CompleteRequest.CompleteArgument argument) {
String prefix = argument.value().toLowerCase();
String firstLetter = prefix.substring(0, 1);
List<String> cities = cityDatabase.getOrDefault(firstLetter, List.of());
return cities.stream()
.filter(city -> city.toLowerCase().startsWith(prefix))
.toList();
}
}
// Create the autocomplete provider
AutocompleteProvider provider = new AutocompleteProvider();
// Register a method with SyncMcpCompleteMethodCallback
Method method = AutocompleteProvider.class.getMethod("completeCityName", CompleteRequest.CompleteArgument.class);
McpComplete annotation = method.getAnnotation(McpComplete.class);
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> callback =
SyncMcpCompleteMethodCallback.builder()
.method(method)
.bean(provider)
.complete(annotation)
.build();
// Use the callback with your MCP server
public class AsyncAutocompleteProvider {
// ...
@McpComplete(prompt = "travel-planner")
public Mono<List<String>> completeCityNameAsync(CompleteRequest.CompleteArgument argument) {
return Mono.fromCallable(() -> {
// Implementation similar to sync version
// ...
});
}
}
public class MyResourceProvider {
private String getUserStatus(String username) {
// Simple logic to generate a status
if (username.equals("john")) {
return "🟢 Online";
} else if (username.equals("jane")) {
return "🟠Away";
} else if (username.equals("bob")) {
return "⚪ Offline";
} else if (username.equals("alice")) {
return "🔴 Busy";
} else {
return "⚪ Offline";
}
}
@McpResource(uri = "user-status://{username}",
name = "User Status",
description = "Provides the current status for a specific user")
public String getUserStatus(String username) {
return this.getUserStatus(username);
}
@McpResource(uri = "user-profile-exchange://{username}",
name = "User Profile with Exchange",
description = "Provides user profile information with server exchange context")
public ReadResourceResult getProfileWithExchange(McpSyncServerExchange exchange, String username) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("user-profile-exchange")
.build());
String profileInfo = formatProfileInfo(userProfiles.getOrDefault(username.toLowerCase(), new HashMap<>()));
return new ReadResourceResult(List.of(new TextResourceContents("user-profile-exchange://" + username,
"text/plain", "Profile with exchange for " + username + ": " + profileInfo)));
}
}
public class McpServerFactory {
public McpSyncServer createMcpServer(
MyResourceProvider myResourceProvider,
AutocompleteProvider autocompleteProvider,
PromptProvider promptProvider) {
List<SyncResourceSpecification> resourceSpecifications =
new SyncMcpResourceProvider(List.of(myResourceProvider)).getResourceSpecifications();
List<SyncCompletionSpecification> completionSpecifications =
new SyncMcpCompletionProvider(List.of(autocompleteProvider)).getCompleteSpecifications();
List<SyncPromptSpecification> promptSpecifications =
new SyncMcpPromptProvider(List.of(promptProvider)).getPromptSpecifications();
// Create a server with custom configuration
McpSyncServer syncServer = McpServer.sync(transportProvider)
.serverInfo("my-server", "1.0.0")
.capabilities(ServerCapabilities.builder()
.resources(true) // Enable resource support
.prompts(true) // Enable prompt support
.logging() // Enable logging support
.completions() // Enable completions support
.build())
.resources(resourceSpecifications)
.completions(completionSpecifications)
.prompts(promptSpecifications)
.build();
return syncServer;
}
}
public class LoggingHandler {
/**
* Handle logging message notifications with a single parameter.
* @param notification The logging message notification
*/
@McpLoggingConsumer
public void handleLoggingMessage(LoggingMessageNotification notification) {
System.out.println("Received logging message: " + notification.level() + " - " + notification.logger() + " - "
+ notification.data());
}
/**
* Handle logging message notifications with individual parameters.
* @param level The logging level
* @param logger The logger name
* @param data The log message data
*/
@McpLoggingConsumer
public void handleLoggingMessageWithParams(LoggingLevel level, String logger, String data) {
System.out.println("Received logging message with params: " + level + " - " + logger + " - " + data);
}
}
public class MyMcpClient {
public static McpSyncClient createClient(LoggingHandler loggingHandler) {
List<Consumer<LoggingMessageNotification>> loggingCOnsummers =
new SyncMcpLoggingConsumerProvider(List.of(loggingHandler)).getLoggingConsumers();
McpSyncClient client = McpClient.sync(transport)
.capabilities(ClientCapabilities.builder()
// Enable capabilities ..
.build())
.loggingConsumers(loggingCOnsummers)
.build();
return client;
}
}
public class SamplingHandler {
/**
* Handle sampling requests with a synchronous implementation.
* @param request The create message request
* @return The create message result
*/
@McpSampling
public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) {
// Process the request and generate a response
return CreateMessageResult.builder()
.role(Role.ASSISTANT)
.content(new TextContent("This is a response to the sampling request"))
.model("test-model")
.build();
}
}
public class AsyncSamplingHandler {
/**
* Handle sampling requests with an asynchronous implementation.
* @param request The create message request
* @return A Mono containing the create message result
*/
@McpSampling
public Mono<CreateMessageResult> handleAsyncSamplingRequest(CreateMessageRequest request) {
return Mono.just(CreateMessageResult.builder()
.role(Role.ASSISTANT)
.content(new TextContent("This is an async response to the sampling request"))
.model("test-model")
.build());
}
}
public class MyMcpClient {
public static McpSyncClient createSyncClient(SamplingHandler samplingHandler) {
Function<CreateMessageRequest, CreateMessageResult> samplingHandler =
new SyncMcpSamplingProvider(List.of(samplingHandler)).getSamplingHandler();
McpSyncClient client = McpClient.sync(transport)
.capabilities(ClientCapabilities.builder()
.sampling(true) // Enable sampling support
// Other capabilities...
.build())
.samplingHandler(samplingHandler)
.build();
return client;
}
public static McpAsyncClient createAsyncClient(AsyncSamplingHandler asyncSamplingHandler) {
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler =
new AsyncMcpSamplingProvider(List.of(asyncSamplingHandler)).getSamplingHandler();
McpAsyncClient client = McpClient.async(transport)
.capabilities(ClientCapabilities.builder()
.sampling(true) // Enable sampling support
// Other capabilities...
.build())
.samplingHandler(samplingHandler)
.build();
return client;
}
}
@Configuration
public class McpConfig {
@Bean
public List<SyncCompletionSpecification> syncCompletionSpecifications(
List<AutocompleteProvider> completeProviders) {
return SpringAiMcpAnnotationProvider.createSyncCompleteSpecifications(completeProviders);
}
@Bean
public List<SyncPromptSpecification> syncPromptSpecifications(
List<PromptProvider> promptProviders) {
return SpringAiMcpAnnotationProvider.createSyncPromptSpecifications(promptProviders);
}
@Bean
public List<SyncResourceSpecification> syncResourceSpecifications(
List<ResourceProvider> resourceProviders) {
return SpringAiMcpAnnotationProvider.createSyncResourceSpecifications(resourceProviders);
}
@Bean
public List<Consumer<LoggingMessageNotification>> syncLoggingConsumers(
List<LoggingHandler> loggingHandlers) {
return SpringAiMcpAnnotationProvider.createSyncLoggingConsumers(loggingHandlers);
}
@Bean
public Function<CreateMessageRequest, CreateMessageResult> syncSamplingHandler(
List<SamplingHandler> samplingHandlers) {
return SpringAiMcpAnnotationProvider.createSyncSamplingHandler(samplingHandlers);
}
@Bean
public Function<CreateMessageRequest, Mono<CreateMessageResult>> asyncSamplingHandler(
List<AsyncSamplingHandler> asyncSamplingHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncSamplingHandler(asyncSamplingHandlers);
}
}
- Annotation-based method handling - Simplifies the creation and registration of MCP methods
- Support for both synchronous and asynchronous operations - Flexible integration with different application architectures
- Builder pattern for callback creation - Clean and fluent API for creating method callbacks
- Comprehensive validation - Ensures method signatures are compatible with MCP operations
- URI template support - Powerful URI template handling for resource and completion operations
- Logging consumer support - Handle logging message notifications from MCP servers
- Sampling support - Handle sampling requests from MCP servers
- Spring integration - Seamless integration with Spring Framework and Spring AI
- AOP proxy support - Proper handling of Spring AOP proxies when processing annotations
- Java 17 or higher
- Reactor Core (for async operations)
- MCP Java SDK 0.10.0 or higher
- Spring Framework and Spring AI (for spring-ai-mcp-annotations module)
To build the project from source, you'll need:
- JDK 17 or later
- Maven 3.6 or later
Clone the repository and build using Maven:
git clone https://github.com/spring-ai-community/mcp-annotations.git
cd mcp-annotations
./mvnw clean install
Contributions to the MCP Annotations project are welcome! Here's how you can contribute:
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature-name
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin feature/your-feature-name
) - Create a new Pull Request
Please make sure to follow the existing code style and include appropriate tests for your changes.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.