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 Annotations Special Parameters
The MCP Annotations support several special parameter types that provide additional context and functionality to annotated methods. These parameters are automatically injected by the framework and are excluded from JSON schema generation.
Special Parameter Types
McpMeta
The McpMeta
class provides access to metadata from MCP requests, notifications, and results.
Overview
-
Automatically injected when used as a method parameter
-
Excluded from parameter count limits and JSON schema generation
-
Provides convenient access to metadata through the
get(String key)
method -
If no metadata is present in the request, an empty
McpMeta
object is injected
Usage in Tools
@McpTool(name = "contextual-tool", description = "Tool with metadata access")
public String processWithContext(
@McpToolParam(description = "Input data", required = true) String data,
McpMeta meta) {
// Access metadata from the request
String userId = (String) meta.get("userId");
String sessionId = (String) meta.get("sessionId");
String userRole = (String) meta.get("userRole");
// Use metadata to customize behavior
if ("admin".equals(userRole)) {
return processAsAdmin(data, userId);
} else {
return processAsUser(data, userId);
}
}
Usage in Resources
@McpResource(uri = "secure-data://{id}", name = "Secure Data")
public ReadResourceResult getSecureData(String id, McpMeta meta) {
String requestingUser = (String) meta.get("requestingUser");
String accessLevel = (String) meta.get("accessLevel");
// Check access permissions using metadata
if (!"admin".equals(accessLevel)) {
return new ReadResourceResult(List.of(
new TextResourceContents("secure-data://" + id,
"text/plain", "Access denied")
));
}
String data = loadSecureData(id);
return new ReadResourceResult(List.of(
new TextResourceContents("secure-data://" + id,
"text/plain", data)
));
}
Usage in Prompts
@McpPrompt(name = "localized-prompt", description = "Localized prompt generation")
public GetPromptResult localizedPrompt(
@McpArg(name = "topic", required = true) String topic,
McpMeta meta) {
String language = (String) meta.get("language");
String region = (String) meta.get("region");
// Generate localized content based on metadata
String message = generateLocalizedMessage(topic, language, region);
return new GetPromptResult("Localized Prompt",
List.of(new PromptMessage(Role.ASSISTANT, new TextContent(message)))
);
}
@McpProgressToken
The @McpProgressToken
annotation marks a parameter to receive progress tokens from MCP requests.
Overview
-
Parameter type should be
String
-
Automatically receives the progress token value from the request
-
Excluded from the generated JSON schema
-
If no progress token is present,
null
is injected -
Used for tracking long-running operations
Usage in Tools
@McpTool(name = "long-operation", description = "Long-running operation with progress")
public String performLongOperation(
@McpProgressToken String progressToken,
@McpToolParam(description = "Operation name", required = true) String operation,
@McpToolParam(description = "Duration in seconds", required = true) int duration,
McpSyncServerExchange exchange) {
if (progressToken != null) {
// Send initial progress
exchange.progressNotification(new ProgressNotification(
progressToken, 0.0, 1.0, "Starting " + operation));
// Simulate work with progress updates
for (int i = 1; i <= duration; i++) {
Thread.sleep(1000);
double progress = (double) i / duration;
exchange.progressNotification(new ProgressNotification(
progressToken, progress, 1.0,
String.format("Processing... %d%%", (int)(progress * 100))));
}
}
return "Operation " + operation + " completed";
}
Usage in Resources
@McpResource(uri = "large-file://{path}", name = "Large File Resource")
public ReadResourceResult getLargeFile(
@McpProgressToken String progressToken,
String path,
McpSyncServerExchange exchange) {
File file = new File(path);
long fileSize = file.length();
if (progressToken != null) {
// Track file reading progress
exchange.progressNotification(new ProgressNotification(
progressToken, 0.0, fileSize, "Reading file"));
}
String content = readFileWithProgress(file, progressToken, exchange);
if (progressToken != null) {
exchange.progressNotification(new ProgressNotification(
progressToken, fileSize, fileSize, "File read complete"));
}
return new ReadResourceResult(List.of(
new TextResourceContents("large-file://" + path, "text/plain", content)
));
}
McpSyncServerExchange / McpAsyncServerExchange
Server exchange objects provide full access to server-side MCP operations.
Overview
-
Provides stateful context for server operations
-
Automatically injected when used as a parameter
-
Excluded from JSON schema generation
-
Enables advanced features like logging, progress notifications, and client calls
McpSyncServerExchange Features
@McpTool(name = "advanced-tool", description = "Tool with full server capabilities")
public String advancedTool(
McpSyncServerExchange exchange,
@McpToolParam(description = "Input", required = true) String input) {
// Send logging notification
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.logger("advanced-tool")
.data("Processing: " + input)
.build());
// Ping the client
exchange.ping();
// Request additional information from user
ElicitRequest elicitRequest = ElicitRequest.builder()
.message("Need additional information")
.requestedSchema(Map.of(
"type", "object",
"properties", Map.of(
"confirmation", Map.of("type", "boolean")
)
))
.build();
ElicitResult elicitResult = exchange.createElicitation(elicitRequest);
// Request LLM sampling
CreateMessageRequest messageRequest = CreateMessageRequest.builder()
.messages(List.of(new SamplingMessage(Role.USER,
new TextContent("Process: " + input))))
.modelPreferences(ModelPreferences.builder()
.hints(List.of(ModelHint.of("gpt-4")))
.build())
.build();
CreateMessageResult samplingResult = exchange.createMessage(messageRequest);
return "Processed with advanced features";
}
McpAsyncServerExchange Features
@McpTool(name = "async-advanced-tool", description = "Async tool with server capabilities")
public Mono<String> asyncAdvancedTool(
McpAsyncServerExchange exchange,
@McpToolParam(description = "Input", required = true) String input) {
return Mono.fromCallable(() -> {
// Send async logging
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Async processing: " + input)
.build());
return "Started processing";
})
.flatMap(msg -> {
// Chain async operations
return exchange.createMessage(/* request */)
.map(result -> "Completed: " + result);
});
}
McpTransportContext
Lightweight context for stateless operations.
Overview
-
Provides minimal context without full server exchange
-
Used in stateless implementations
-
Automatically injected when used as a parameter
-
Excluded from JSON schema generation
Usage Example
@McpTool(name = "stateless-tool", description = "Stateless tool with context")
public String statelessTool(
McpTransportContext context,
@McpToolParam(description = "Input", required = true) String input) {
// Limited context access
// Useful for transport-level operations
return "Processed in stateless mode: " + input;
}
@McpResource(uri = "stateless://{id}", name = "Stateless Resource")
public ReadResourceResult statelessResource(
McpTransportContext context,
String id) {
// Access transport context if needed
String data = loadData(id);
return new ReadResourceResult(List.of(
new TextResourceContents("stateless://" + id, "text/plain", data)
));
}
CallToolRequest
Special parameter for tools that need access to the full request with dynamic schema.
Overview
-
Provides access to the complete tool request
-
Enables dynamic schema handling at runtime
-
Automatically injected and excluded from schema generation
-
Useful for flexible tools that adapt to different input schemas
Usage Examples
@McpTool(name = "dynamic-tool", description = "Tool with dynamic schema support")
public CallToolResult processDynamicSchema(CallToolRequest request) {
Map<String, Object> args = request.arguments();
// Process based on whatever schema was provided at runtime
StringBuilder result = new StringBuilder("Processed:\n");
for (Map.Entry<String, Object> entry : args.entrySet()) {
result.append(" ").append(entry.getKey())
.append(": ").append(entry.getValue()).append("\n");
}
return CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
Mixed Parameters
@McpTool(name = "hybrid-tool", description = "Tool with typed and dynamic parameters")
public String processHybrid(
@McpToolParam(description = "Operation", required = true) String operation,
@McpToolParam(description = "Priority", required = false) Integer priority,
CallToolRequest request) {
// Use typed parameters for known fields
String result = "Operation: " + operation;
if (priority != null) {
result += " (Priority: " + priority + ")";
}
// Access additional dynamic arguments
Map<String, Object> allArgs = request.arguments();
// Remove known parameters to get only additional ones
Map<String, Object> additionalArgs = new HashMap<>(allArgs);
additionalArgs.remove("operation");
additionalArgs.remove("priority");
if (!additionalArgs.isEmpty()) {
result += " with " + additionalArgs.size() + " additional parameters";
}
return result;
}
With Progress Token
@McpTool(name = "flexible-with-progress", description = "Flexible tool with progress")
public CallToolResult flexibleWithProgress(
@McpProgressToken String progressToken,
CallToolRequest request,
McpSyncServerExchange exchange) {
Map<String, Object> args = request.arguments();
if (progressToken != null) {
exchange.progressNotification(new ProgressNotification(
progressToken, 0.0, 1.0, "Processing dynamic request"));
}
// Process dynamic arguments
String result = processDynamicArgs(args);
if (progressToken != null) {
exchange.progressNotification(new ProgressNotification(
progressToken, 1.0, 1.0, "Complete"));
}
return CallToolResult.builder()
.addTextContent(result)
.build();
}
Parameter Injection Rules
Automatic Injection
The following parameters are automatically injected by the framework:
-
McpMeta
- Metadata from the request -
@McpProgressToken String
- Progress token if available -
McpSyncServerExchange
/McpAsyncServerExchange
- Server exchange context -
McpTransportContext
- Transport context for stateless operations -
CallToolRequest
- Full tool request for dynamic schema
Best Practices
Use McpMeta for Context
@McpTool(name = "context-aware", description = "Context-aware tool")
public String contextAware(
@McpToolParam(description = "Data", required = true) String data,
McpMeta meta) {
// Always check for null values in metadata
String userId = (String) meta.get("userId");
if (userId == null) {
userId = "anonymous";
}
return processForUser(data, userId);
}
Progress Token Null Checks
@McpTool(name = "safe-progress", description = "Safe progress handling")
public String safeProgress(
@McpProgressToken String progressToken,
@McpToolParam(description = "Task", required = true) String task,
McpSyncServerExchange exchange) {
// Always check if progress token is available
if (progressToken != null) {
exchange.progressNotification(new ProgressNotification(
progressToken, 0.0, 1.0, "Starting"));
}
// Perform work...
if (progressToken != null) {
exchange.progressNotification(new ProgressNotification(
progressToken, 1.0, 1.0, "Complete"));
}
return "Task completed";
}