Spring Boot Starter for building Model Context Protocol (MCP) servers with auto-configuration, annotation-based tool/resource/prompt definitions, and support for STDIO, SSE, and Streamable-HTTP transports
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 by automatically generating JSON schemas and registering capabilities with the MCP server.
The Spring AI MCP Server starter supports two categories of tool annotations:
@Tool, @ToolParam) - General-purpose Spring AI tool annotations@McpTool, @McpResource, @McpPrompt, etc.) - MCP-specific server annotationsSpring AI Annotations (org.springframework.ai.tool.annotation):
MCP Server Annotations (org.springaicommunity.mcp.annotation):
When to use each:
@Tool when building general Spring AI tools that may be used outside MCP context@McpTool when you need MCP-specific features like request context, metadata, or tool hintsMarks a method as a general Spring AI tool that will be automatically exposed as an MCP tool by the server.
/**
* Marks a method as a tool in Spring AI
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface Tool {
/**
* The name of the tool (defaults to method name if not provided)
* For maximum compatibility, use only alphanumeric characters,
* underscores, hyphens, and dots
*/
String name() default "";
/**
* Description of what the tool does
*/
String description() default "";
/**
* Whether the tool result should be returned directly or
* passed back to the model
*/
boolean returnDirect() default false;
/**
* The class to use to convert the tool call result to a String
*/
Class<? extends ToolCallResultConverter> resultConverter()
default DefaultToolCallResultConverter.class;
}Usage Examples:
Basic Spring AI tool:
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class WeatherTools {
@Tool(
name = "get_weather",
description = "Get the current weather for a location"
)
public String getWeather(
@ToolParam(description = "City name", required = true) String city,
@ToolParam(description = "Country code", required = false) String country) {
// Implementation
return "Weather in " + city + ": Sunny, 22°C";
}
}Tool with return direct:
@Tool(
name = "execute_command",
description = "Execute a system command and return result",
returnDirect = true
)
public String executeCommand(
@ToolParam(description = "Command to execute") String command) {
// When returnDirect is true, the result goes directly
// to the user without further model processing
return executeSystemCommand(command);
}Tool with custom result converter:
@Tool(
name = "get_user_data",
description = "Get user data in JSON format",
resultConverter = JsonResultConverter.class
)
public UserData getUserData(
@ToolParam(description = "User ID") String userId) {
return userRepository.findById(userId);
}Marks a tool method parameter with metadata for JSON schema generation.
/**
* Marks a tool argument/parameter
*/
@Target({ ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface ToolParam {
/**
* Whether the tool argument is required (default: true)
*/
boolean required() default true;
/**
* Description of the parameter for the AI model
*/
String description() default "";
}Usage Examples:
Required and optional parameters:
@Tool(name = "search_products", description = "Search for products")
public List<Product> searchProducts(
@ToolParam(description = "Search query", required = true) String query,
@ToolParam(description = "Maximum results", required = false) Integer maxResults,
@ToolParam(description = "Category filter", required = false) String category) {
int limit = maxResults != null ? maxResults : 10;
return productService.search(query, category, limit);
}Complex parameter types:
@Tool(name = "create_user", description = "Create a new user account")
public User createUser(
@ToolParam(description = "User details including name, email, and preferences")
UserCreateRequest request) {
// Spring AI automatically generates JSON schema for complex types
return userService.create(request);
}Marks a method as an MCP tool implementation with automatic JSON schema generation.
/**
* Marks a method as an MCP tool that can be invoked by AI models
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface McpTool {
/**
* The name of the tool (must be unique)
*/
String name();
/**
* Description of what the tool does
*/
String description();
/**
* Optional metadata annotations for the tool
*/
McpAnnotations annotations() default @McpAnnotations;
@interface McpAnnotations {
String title() default "";
boolean readOnlyHint() default false;
boolean destructiveHint() default false;
boolean idempotentHint() default false;
}
}Usage Examples:
Basic tool:
@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;
}
}Tool with metadata:
@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 double calculateRectangleArea(
@McpToolParam(description = "Width in meters", required = true) double width,
@McpToolParam(description = "Height in meters", required = true) double height) {
return width * height;
}Tool with request context:
@McpTool(name = "process-data", description = "Process data with logging and progress")
public String processData(
McpSyncRequestContext context,
@McpToolParam(description = "Data to process", required = true) String data) {
context.info("Processing data: " + data);
context.progress(50);
String result = data.toUpperCase();
context.progress(100);
return "Processed: " + result;
}Tool with dynamic schema (CallToolRequest):
@McpTool(name = "flexible-tool", description = "Process dynamic arguments")
public CallToolResult processDynamic(CallToolRequest request) {
Map<String, Object> args = request.arguments();
String result = "Processed " + args.size() + " arguments";
return CallToolResult.builder()
.addTextContent(result)
.build();
}Return Types:
int, double, boolean, etc.String, custom POJOs (automatically serialized to JSON)CallToolResult: Full control over result contentMono<T>, Flux<T> (for async servers)Special Parameters:
McpSyncRequestContext / McpAsyncRequestContext: Access to request contextMcpTransportContext: Lightweight transport context (stateless)McpMeta: Access to MCP metadataCallToolRequest: Dynamic schema handlingDescribes a tool parameter for JSON schema generation.
/**
* Describes a parameter for an @McpTool method
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpToolParam {
/**
* Description of the parameter
*/
String description();
/**
* Whether the parameter is required
*/
boolean required() default true;
}Usage Example:
@McpTool(name = "search", description = "Search for items")
public List<String> search(
@McpToolParam(description = "Search query", required = true) String query,
@McpToolParam(description = "Maximum results", required = false) Integer limit) {
int maxResults = (limit != null) ? limit : 10;
// Perform search...
return results;
}Provides access to resources via URI templates.
/**
* Marks a method as an MCP resource provider
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface McpResource {
/**
* URI template with placeholders (e.g., "config://{key}")
*/
String uri();
/**
* Resource name for identification
*/
String name();
/**
* Description of the resource
*/
String description();
/**
* Optional MIME type for the resource content
*/
String mimeType() default "";
}Usage Examples:
Basic resource:
@Component
public class ResourceProvider {
@McpResource(
uri = "config://{key}",
name = "Configuration",
description = "Provides configuration data")
public String getConfig(String key) {
return configData.get(key);
}
}Resource with ReadResourceResult:
@McpResource(
uri = "user-profile://{username}",
name = "User Profile",
description = "Provides user profile information",
mimeType = "application/json")
public ReadResourceResult getUserProfile(String username) {
String profileJson = loadUserProfile(username);
return new ReadResourceResult(List.of(
new TextResourceContents(
"user-profile://" + username,
"application/json",
profileJson)
));
}Resource with request context:
@McpResource(
uri = "data://{id}",
name = "Data Resource",
description = "Fetch data by ID")
public ReadResourceResult getData(
McpSyncRequestContext context,
String id) {
context.info("Accessing resource: " + id);
context.ping();
String data = fetchData(id);
return new ReadResourceResult(List.of(
new TextResourceContents("data://" + id, "text/plain", data)
));
}URI Templates:
{placeholder} syntax for dynamic segmentsconfig://, file://, db://)Return Types:
String: Automatically wrapped in TextResourceContentsReadResourceResult: Full control over resource contentsMono<ReadResourceResult>, Flux<ResourceContents>Generates prompt messages for AI interactions.
/**
* Marks a method as an MCP prompt provider
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface McpPrompt {
/**
* Unique prompt name
*/
String name();
/**
* Description of the prompt's purpose
*/
String description();
}Usage Examples:
Basic prompt:
@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)))
);
}
}Prompt with optional arguments:
@McpPrompt(
name = "personalized-message",
description = "Generate a personalized message")
public GetPromptResult personalizedMessage(
@McpArg(name = "name", description = "User's name", required = true) String name,
@McpArg(name = "age", description = "User's age", required = false) Integer age,
@McpArg(name = "interests", description = "User 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, ");
}
if (interests != null && !interests.isEmpty()) {
message.append("With interests in ").append(interests).append(", ");
}
message.append("I can help you with many tasks!");
return new GetPromptResult(
"Personalized Message",
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message.toString())))
);
}Return Types:
GetPromptResult: Contains description and list of prompt messagesDescribes prompt arguments.
/**
* Describes an argument for an @McpPrompt method
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpArg {
/**
* Argument name
*/
String name();
/**
* Optional description of the argument
*/
String description() default "";
/**
* Whether the argument is required
*/
boolean required() default true;
}Provides auto-completion functionality for prompts.
/**
* Marks a method as a completion provider for a prompt
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface McpComplete {
/**
* The prompt name to provide completions for
*/
String prompt();
}Usage Examples:
Basic completion:
@Component
public class CompletionProvider {
private final List<String> cities = List.of("New York", "Los Angeles", "Chicago", "Houston");
@McpComplete(prompt = "city-search")
public List<String> completeCityName(String prefix) {
return cities.stream()
.filter(city -> city.toLowerCase().startsWith(prefix.toLowerCase()))
.limit(10)
.toList();
}
}Completion with CompleteRequest.CompleteArgument:
@McpComplete(prompt = "travel-planner")
public List<String> completeTravelDestination(CompleteRequest.CompleteArgument argument) {
String prefix = argument.value().toLowerCase();
String argumentName = argument.name();
if ("city".equals(argumentName)) {
return completeCities(prefix);
} else if ("country".equals(argumentName)) {
return completeCountries(prefix);
}
return List.of();
}Completion 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(),
hasMoreCompletions
)
);
}Return Types:
List<String>: Simple list of completion suggestionsCompleteResult: Full control over completion metadataAnnotation scanning is enabled by default and controlled via configuration:
# Enable/disable annotation scanning
spring.ai.mcp.server.annotation-scanner.enabled=trueThe starter automatically scans all Spring beans for MCP annotations and registers them with the MCP server. Annotated methods are filtered based on:
Filtered methods log warnings to help with debugging.
Support bidirectional operations including:
Use McpSyncRequestContext or McpAsyncRequestContext for full access.
Do not support bidirectional operations. Methods using full request context are ignored in stateless mode.
Use McpTransportContext for lightweight operations or no context parameter for simple operations.
Accept methods with:
int, double, boolean)String, Integer, custom POJOs)CallToolResult, ReadResourceResult, GetPromptResult, CompleteResult)List<String>, Map<String, Object>)Accept methods with:
Mono<T>, Flux<T>)The following annotations are provided by the external MCP Java SDK (org.springaicommunity:mcp-annotations) for advanced functionality:
Injects MCP metadata into method parameters.
/**
* Injects MCP metadata provided by the client
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpMeta {}Usage Example:
@McpTool(name = "inspect-metadata", description = "Inspect client metadata")
public String inspectMetadata(@McpMeta Map<String, Object> metadata) {
return "Received metadata: " + metadata.toString();
}Marks a method parameter to receive progress notification callback.
/**
* Marks a parameter to receive progress notification functionality
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpProgress {}Usage Example:
@McpTool(name = "long-task", description = "Long-running task with progress")
public String longTask(
@McpProgress Consumer<ProgressNotification> progressCallback,
@McpToolParam(description = "Data", required = true) String data) {
progressCallback.accept(ProgressNotification.builder()
.progress(0.0).total(1.0).message("Starting").build());
// Perform work
processData(data);
progressCallback.accept(ProgressNotification.builder()
.progress(1.0).total(1.0).message("Complete").build());
return "Done";
}Injects the progress token from the request.
/**
* Injects the progress token from the tool call request
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpProgressToken {}Usage Example:
@McpTool(name = "trackable-task", description = "Task with progress tracking")
public String trackableTask(
@McpProgressToken String progressToken,
@McpToolParam(description = "Input", required = true) String input) {
if (progressToken != null) {
// Use progress token for tracking
trackProgress(progressToken, 0);
performWork(input);
trackProgress(progressToken, 100);
}
return "Complete";
}Marks a method parameter to receive logging callback.
/**
* Marks a parameter to receive logging functionality
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpLogging {}Usage Example:
@McpTool(name = "logged-operation", description = "Operation with logging")
public String loggedOperation(
@McpLogging BiConsumer<LoggingLevel, String> logger,
@McpToolParam(description = "Input", required = true) String input) {
logger.accept(LoggingLevel.INFO, "Starting operation");
String result = processInput(input);
logger.accept(LoggingLevel.INFO, "Operation complete");
return result;
}Marks a method parameter to receive elicitation functionality (stateful mode only).
/**
* Marks a parameter to receive elicitation functionality
* Only available in stateful server modes (SSE, Streamable)
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpElicitation {}Usage Example:
public record UserInput(String name, String email) {}
@McpTool(name = "collect-input", description = "Collect user input via elicitation")
public String collectInput(
@McpElicitation Function<Class<?>, StructuredElicitResult<?>> elicit) {
StructuredElicitResult<UserInput> result =
(StructuredElicitResult<UserInput>) elicit.apply(UserInput.class);
if (result.action() == ElicitResult.Action.ACCEPT) {
UserInput input = result.data();
return "Received: " + input.name() + ", " + input.email();
}
return "User declined";
}Marks a method parameter to receive LLM sampling functionality (stateful mode only).
/**
* Marks a parameter to receive sampling functionality
* Only available in stateful server modes (SSE, Streamable)
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface McpSampling {}Usage Example:
@McpTool(name = "enhanced-query", description = "Enhance query with LLM")
public String enhancedQuery(
@McpSampling Function<String, CreateMessageResult> sample,
@McpToolParam(description = "Query", required = true) String query) {
CreateMessageResult llmResult = sample.apply(
"Enhance this query: " + query);
String enhancedQuery = llmResult.content();
return performSearch(enhancedQuery);
}These advanced annotations provide fine-grained control over MCP features without requiring full request context injection.