This version is still in development and is not considered stable yet. For the latest snapshot version, please use Spring AI 1.0.2! |
MCP Server Annotations
The MCP Server Annotations provide a declarative way to implement MCP server functionality using Java annotations. These annotations simplify the creation of tools, resources, prompts, and completion handlers.
Server Annotations
@McpTool
The @McpTool
annotation marks a method as an MCP tool implementation with automatic JSON schema generation.
Basic Usage
@Component
public class CalculatorTools {
@McpTool(name = "add", description = "Add two numbers together")
public int add(
@McpToolParam(description = "First number", required = true) int a,
@McpToolParam(description = "Second number", required = true) int b) {
return a + b;
}
}
Advanced Features
@McpTool(name = "calculate-area",
description = "Calculate the area of a rectangle",
annotations = McpTool.McpAnnotations(
title = "Rectangle Area Calculator",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true
))
public AreaResult calculateRectangleArea(
@McpToolParam(description = "Width", required = true) double width,
@McpToolParam(description = "Height", required = true) double height) {
return new AreaResult(width * height, "square units");
}
With Server Exchange
Tools can access the server exchange for advanced operations:
@McpTool(name = "process-data", description = "Process data with server context")
public String processData(
McpSyncServerExchange exchange,
@McpToolParam(description = "Data to process", required = true) String data) {
// Send logging notification
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Processing data: " + data)
.build());
// Send progress notification if progress token is available
exchange.progressNotification(new ProgressNotification(
progressToken, 0.5, 1.0, "Processing..."));
return "Processed: " + data.toUpperCase();
}
Dynamic Schema Support
Tools can accept CallToolRequest
for runtime schema handling:
@McpTool(name = "flexible-tool", description = "Process dynamic schema")
public CallToolResult processDynamic(CallToolRequest request) {
Map<String, Object> args = request.arguments();
// Process based on runtime schema
String result = "Processed " + args.size() + " arguments dynamically";
return CallToolResult.builder()
.addTextContent(result)
.build();
}
Progress Tracking
Tools can receive progress tokens for tracking long-running operations:
@McpTool(name = "long-task", description = "Long-running task with progress")
public String performLongTask(
@McpProgressToken String progressToken,
@McpToolParam(description = "Task name", required = true) String taskName,
McpSyncServerExchange exchange) {
if (progressToken != null) {
exchange.progressNotification(new ProgressNotification(
progressToken, 0.0, 1.0, "Starting task"));
// Perform work...
exchange.progressNotification(new ProgressNotification(
progressToken, 1.0, 1.0, "Task completed"));
}
return "Task " + taskName + " completed";
}
@McpResource
The @McpResource
annotation provides access to resources via URI templates.
Basic Usage
@Component
public class ResourceProvider {
@McpResource(
uri = "config://{key}",
name = "Configuration",
description = "Provides configuration data")
public String getConfig(String key) {
return configData.get(key);
}
}
With ReadResourceResult
@McpResource(
uri = "user-profile://{username}",
name = "User Profile",
description = "Provides user profile information")
public ReadResourceResult getUserProfile(String username) {
String profileData = loadUserProfile(username);
return new ReadResourceResult(List.of(
new TextResourceContents(
"user-profile://" + username,
"application/json",
profileData)
));
}
With Server Exchange
@McpResource(
uri = "data://{id}",
name = "Data Resource",
description = "Resource with server context")
public ReadResourceResult getData(
McpSyncServerExchange exchange,
String id) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Accessing resource: " + id)
.build());
String data = fetchData(id);
return new ReadResourceResult(List.of(
new TextResourceContents("data://" + id, "text/plain", data)
));
}
@McpPrompt
The @McpPrompt
annotation generates prompt messages for AI interactions.
Basic Usage
@Component
public class PromptProvider {
@McpPrompt(
name = "greeting",
description = "Generate a greeting message")
public GetPromptResult greeting(
@McpArg(name = "name", description = "User's name", required = true)
String name) {
String message = "Hello, " + name + "! How can I help you today?";
return new GetPromptResult(
"Greeting",
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message)))
);
}
}
With Optional Arguments
@McpPrompt(
name = "personalized-message",
description = "Generate a personalized message")
public GetPromptResult personalizedMessage(
@McpArg(name = "name", required = true) String name,
@McpArg(name = "age", required = false) Integer age,
@McpArg(name = "interests", required = false) String interests) {
StringBuilder message = new StringBuilder();
message.append("Hello, ").append(name).append("!\n\n");
if (age != null) {
message.append("At ").append(age).append(" years old, ");
// Add age-specific content
}
if (interests != null && !interests.isEmpty()) {
message.append("Your interest in ").append(interests);
// Add interest-specific content
}
return new GetPromptResult(
"Personalized Message",
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message.toString())))
);
}
@McpComplete
The @McpComplete
annotation provides auto-completion functionality for prompts.
Basic Usage
@Component
public class CompletionProvider {
@McpComplete(prompt = "city-search")
public List<String> completeCityName(String prefix) {
return cities.stream()
.filter(city -> city.toLowerCase().startsWith(prefix.toLowerCase()))
.limit(10)
.toList();
}
}
With CompleteRequest.CompleteArgument
@McpComplete(prompt = "travel-planner")
public List<String> completeTravelDestination(CompleteRequest.CompleteArgument argument) {
String prefix = argument.value().toLowerCase();
String argumentName = argument.name();
// Different completions based on argument name
if ("city".equals(argumentName)) {
return completeCities(prefix);
} else if ("country".equals(argumentName)) {
return completeCountries(prefix);
}
return List.of();
}
With CompleteResult
@McpComplete(prompt = "code-completion")
public CompleteResult completeCode(String prefix) {
List<String> completions = generateCodeCompletions(prefix);
return new CompleteResult(
new CompleteResult.CompleteCompletion(
completions,
completions.size(), // total
hasMoreCompletions // hasMore flag
)
);
}
Stateless vs Stateful Implementations
Stateful (with McpSyncServerExchange/McpAsyncServerExchange)
Stateful implementations have access to the full server exchange context:
@McpTool(name = "stateful-tool", description = "Tool with server exchange")
public String statefulTool(
McpSyncServerExchange exchange,
@McpToolParam(description = "Input", required = true) String input) {
// Access server exchange features
exchange.loggingNotification(...);
exchange.progressNotification(...);
exchange.ping();
// Can call client methods
CreateMessageResult result = exchange.createMessage(...);
ElicitResult elicitResult = exchange.createElicitation(...);
return "Processed with full context";
}
Stateless (with McpTransportContext or without)
Stateless implementations are simpler and don’t require server exchange:
@McpTool(name = "stateless-tool", description = "Simple stateless tool")
public int simpleAdd(
@McpToolParam(description = "First number", required = true) int a,
@McpToolParam(description = "Second number", required = true) int b) {
return a + b;
}
// With transport context if needed
@McpTool(name = "stateless-with-context", description = "Stateless with context")
public String withContext(
McpTransportContext context,
@McpToolParam(description = "Input", required = true) String input) {
// Limited context access
return "Processed: " + input;
}
Async Support
All server annotations support asynchronous implementations using Reactor:
@Component
public class AsyncTools {
@McpTool(name = "async-fetch", description = "Fetch data asynchronously")
public Mono<String> asyncFetch(
@McpToolParam(description = "URL", required = true) String url) {
return Mono.fromCallable(() -> {
// Simulate async operation
return fetchFromUrl(url);
}).subscribeOn(Schedulers.boundedElastic());
}
@McpResource(uri = "async-data://{id}", name = "Async Data")
public Mono<ReadResourceResult> asyncResource(String id) {
return Mono.fromCallable(() -> {
String data = loadData(id);
return new ReadResourceResult(List.of(
new TextResourceContents("async-data://" + id, "text/plain", data)
));
}).delayElements(Duration.ofMillis(100));
}
}
Spring Boot Integration
With Spring Boot auto-configuration, annotated beans are automatically detected and registered:
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
@Component
public class MyMcpTools {
// Your @McpTool annotated methods
}
@Component
public class MyMcpResources {
// Your @McpResource annotated methods
}
The auto-configuration will:
-
Scan for beans with MCP annotations
-
Create appropriate specifications
-
Register them with the MCP server
-
Handle both sync and async implementations based on configuration
Configuration Properties
Configure the server annotation scanner:
spring:
ai:
mcp:
server:
type: SYNC # or ASYNC
annotation-scanner:
enabled: true