This version is still in development and is not considered stable yet. For the latest snapshot version, please use Spring AI 1.0.3! |
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 Request Context
Tools can access the request context for advanced operations:
@McpTool(name = "process-data", description = "Process data with request context")
public String processData(
McpSyncRequestContext context,
@McpToolParam(description = "Data to process", required = true) String data) {
// Send logging notification
context.info("Processing data: " + data);
// Send progress notification (using convenient method)
context.progress(p -> p.progress(0.5).total(1.0).message("Processing..."));
// Ping the client
context.ping();
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(
McpSyncRequestContext context,
@McpToolParam(description = "Task name", required = true) String taskName) {
// Access progress token from context
String progressToken = context.request().progressToken();
if (progressToken != null) {
context.progress(p -> p.progress(0.0).total(1.0).message("Starting task"));
// Perform work...
context.progress(p -> p.progress(1.0).total(1.0).message("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 Request Context
@McpResource(
uri = "data://{id}",
name = "Data Resource",
description = "Resource with request context")
public ReadResourceResult getData(
McpSyncRequestContext context,
String id) {
// Send logging notification using convenient method
context.info("Accessing resource: " + id);
// Ping the client
context.ping();
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
Unified Request Context (Recommended)
Use McpSyncRequestContext
or McpAsyncRequestContext
for a unified interface that works with both stateful and stateless operations:
public record UserInfo(String name, String email, int age) {}
@McpTool(name = "unified-tool", description = "Tool with unified request context")
public String unifiedTool(
McpSyncRequestContext context,
@McpToolParam(description = "Input", required = true) String input) {
// Access request and metadata
String progressToken = context.request().progressToken();
// Logging with convenient methods
context.info("Processing: " + input);
// Progress notifications (Note client should set a progress token
// with its request to be able to receive progress updates)
context.progress(50); // Simple percentage
// Ping client
context.ping();
// Check capabilities before using
if (context.elicitEnabled()) {
// Request user input (only in stateful mode)
StructuredElicitResult<UserInfo> elicitResult = context.elicit(UserInfo.class);
if (elicitResult.action() == ElicitResult.Action.ACCEPT) {
// Use elicited data
}
}
if (context.sampleEnabled()) {
// Request LLM sampling (only in stateful mode)
CreateMessageResult samplingResult = context.sample("Generate response");
// Use sampling result
}
return "Processed with unified context";
}
Simple Operations (No Context)
For simple operations, you can omit context parameters entirely:
@McpTool(name = "simple-add", description = "Simple addition")
public int simpleAdd(
@McpToolParam(description = "First number", required = true) int a,
@McpToolParam(description = "Second number", required = true) int b) {
return a + b;
}
Lightweight Stateless (with McpTransportContext)
For stateless operations where you need minimal transport context:
@McpTool(name = "stateless-tool", description = "Stateless with transport context")
public String statelessTool(
McpTransportContext context,
@McpToolParam(description = "Input", required = true) String input) {
// Access transport-level context only
// No bidirectional operations (roots, elicitation, sampling)
return "Processed: " + input;
}
Stateless servers do not support bidirectional operations: |
Therefore methods using McpSyncRequestContext
or McpAsyncRequestContext
in stateless mode are ignored.
Method Filtering by Server Type
The MCP annotations framework automatically filters annotated methods based on the server type and method characteristics. This ensures that only appropriate methods are registered for each server configuration. A warning is logged for each filtered method to help with debugging.
Synchronous vs Asynchronous Filtering
Synchronous Servers
Synchronous servers (configured with spring.ai.mcp.server.type=SYNC
) use synchronous providers that:
-
Accept methods with non-reactive return types:
-
Primitive types (
int
,double
,boolean
) -
Object types (
String
,Integer
, custom POJOs) -
MCP types (
CallToolResult
,ReadResourceResult
,GetPromptResult
,CompleteResult
) -
Collections (
List<String>
,Map<String, Object>
)
-
-
Filter out methods with reactive return types:
-
Mono<T>
-
Flux<T>
-
Publisher<T>
-
@Component
public class SyncTools {
@McpTool(name = "sync-tool", description = "Synchronous tool")
public String syncTool(String input) {
// This method WILL be registered on sync servers
return "Processed: " + input;
}
@McpTool(name = "async-tool", description = "Async tool")
public Mono<String> asyncTool(String input) {
// This method will be FILTERED OUT on sync servers
// A warning will be logged
return Mono.just("Processed: " + input);
}
}
Asynchronous Servers
Asynchronous servers (configured with spring.ai.mcp.server.type=ASYNC
) use asynchronous providers that:
-
Accept methods with reactive return types:
-
Mono<T>
(for single results) -
Flux<T>
(for streaming results) -
Publisher<T>
(generic reactive type)
-
-
Filter out methods with non-reactive return types:
-
Primitive types
-
Object types
-
Collections
-
MCP result types
-
@Component
public class AsyncTools {
@McpTool(name = "async-tool", description = "Async tool")
public Mono<String> asyncTool(String input) {
// This method WILL be registered on async servers
return Mono.just("Processed: " + input);
}
@McpTool(name = "sync-tool", description = "Sync tool")
public String syncTool(String input) {
// This method will be FILTERED OUT on async servers
// A warning will be logged
return "Processed: " + input;
}
}
Stateful vs Stateless Filtering
Stateful Servers
Stateful servers support bidirectional communication and accept methods with:
-
Bidirectional context parameters:
-
McpSyncRequestContext
(for sync operations) -
McpAsyncRequestContext
(for async operations) -
McpSyncServerExchange
(legacy, for sync operations) -
McpAsyncServerExchange
(legacy, for async operations)
-
-
Support for bidirectional operations:
-
roots()
- Access root directories -
elicit()
- Request user input -
sample()
- Request LLM sampling
-
@Component
public class StatefulTools {
@McpTool(name = "interactive-tool", description = "Tool with bidirectional operations")
public String interactiveTool(
McpSyncRequestContext context,
@McpToolParam(description = "Input", required = true) String input) {
// This method WILL be registered on stateful servers
// Can use elicitation, sampling, roots
if (context.sampleEnabled()) {
var samplingResult = context.sample("Generate response");
// Process sampling result...
}
return "Processed with context";
}
}
Stateless Servers
Stateless servers are optimized for simple request-response patterns and:
-
Filter out methods with bidirectional context parameters:
-
Methods with
McpSyncRequestContext
are skipped -
Methods with
McpAsyncRequestContext
are skipped -
Methods with
McpSyncServerExchange
are skipped -
Methods with
McpAsyncServerExchange
are skipped -
A warning is logged for each filtered method
-
-
Accept methods with:
-
McpTransportContext
(lightweight stateless context) -
No context parameter at all
-
Only regular
@McpToolParam
parameters
-
-
Do not support bidirectional operations:
-
roots()
- Not available -
elicit()
- Not available -
sample()
- Not available
-
@Component
public class StatelessTools {
@McpTool(name = "simple-tool", description = "Simple stateless tool")
public String simpleTool(@McpToolParam(description = "Input") String input) {
// This method WILL be registered on stateless servers
return "Processed: " + input;
}
@McpTool(name = "context-tool", description = "Tool with transport context")
public String contextTool(
McpTransportContext context,
@McpToolParam(description = "Input") String input) {
// This method WILL be registered on stateless servers
return "Processed: " + input;
}
@McpTool(name = "bidirectional-tool", description = "Tool with bidirectional context")
public String bidirectionalTool(
McpSyncRequestContext context,
@McpToolParam(description = "Input") String input) {
// This method will be FILTERED OUT on stateless servers
// A warning will be logged
return "Processed with sampling";
}
}
Filtering Summary
Server Type | Accepted Methods | Filtered Methods |
---|---|---|
Sync Stateful |
Non-reactive returns + bidirectional context |
Reactive returns (Mono/Flux) |
Async Stateful |
Reactive returns (Mono/Flux) + bidirectional context |
Non-reactive returns |
Sync Stateless |
Non-reactive returns + no bidirectional context |
Reactive returns OR bidirectional context parameters |
Async Stateless |
Reactive returns (Mono/Flux) + no bidirectional context |
Non-reactive returns OR bidirectional context parameters |
Best Practices for Method Filtering: |
-
Keep methods aligned with your server type - use sync methods for sync servers, async for async servers
-
Separate stateful and stateless implementations into different classes for clarity
-
Check logs during startup for filtered method warnings
-
Use the right context -
McpSyncRequestContext
/McpAsyncRequestContext
for stateful,McpTransportContext
for stateless -
Test both modes if you support both stateful and stateless deployments
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