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
Request context objects provide access to MCP request metadata, logging, progress notifications, and advanced features like elicitation and sampling.
Unified request context for synchronous MCP operations.
/**
* Request context for synchronous MCP operations
* Provides access to request metadata, logging, progress, and advanced features
*/
interface McpSyncRequestContext {
/**
* Access the MCP request
*/
CallToolRequest request();
/**
* Access MCP metadata provided by the client
*/
McpMeta meta();
/**
* Send info-level log message to client
*/
void info(String message);
/**
* Send debug-level log message to client
*/
void debug(String message);
/**
* Send warning-level log message to client
*/
void warn(String message);
/**
* Send error-level log message to client
*/
void error(String message);
/**
* Send progress notification with simple percentage (0-100)
*/
void progress(int percentage);
/**
* Send custom progress notification using builder
*/
void progress(Consumer<ProgressNotification.Builder> configurer);
/**
* Ping the client to keep connection alive
*/
void ping();
/**
* Check if elicitation is enabled (only available in stateful mode)
*/
boolean elicitEnabled();
/**
* Check if sampling is enabled (only available in stateful mode)
*/
boolean sampleEnabled();
/**
* Request structured user input (stateful mode only)
* @param type The class representing the expected structure
* @return StructuredElicitResult containing user input or rejection
*/
<T> StructuredElicitResult<T> elicit(Class<T> type);
/**
* Request structured user input with custom configuration (stateful mode only)
* @param configurer Builder consumer to configure the elicitation request
* @param type The class representing the expected structure
* @return StructuredElicitResult containing user input or rejection
*/
<T> StructuredElicitResult<T> elicit(Consumer<ElicitRequest.Builder> configurer, Class<T> type);
/**
* Request LLM sampling with a prompt (stateful mode only)
* @param prompt The prompt to send to the LLM
* @return CreateMessageResult containing the LLM response
*/
CreateMessageResult sample(String prompt);
/**
* Request LLM sampling with custom configuration (stateful mode only)
* @param configurer Builder consumer to configure the sampling request
* @return CreateMessageResult containing the LLM response
*/
CreateMessageResult sample(Consumer<CreateMessageRequest.Builder> configurer);
}Usage Examples:
Basic logging and progress:
@McpTool(name = "process", description = "Process data with progress tracking")
public String process(
McpSyncRequestContext context,
@McpToolParam(description = "Data", required = true) String data) {
context.info("Starting processing");
context.progress(0);
// Step 1
context.debug("Step 1: Validate data");
validateData(data);
context.progress(33);
// Step 2
context.info("Step 2: Transform data");
String transformed = transform(data);
context.progress(66);
// Step 3
context.info("Step 3: Save result");
save(transformed);
context.progress(100);
return "Processing complete";
}Custom progress with builder:
@McpTool(name = "long-task", description = "Long-running task")
public String longTask(
McpSyncRequestContext context,
@McpToolParam(description = "Task name", required = true) String taskName) {
// Access progress token from request
String progressToken = context.request().progressToken();
if (progressToken != null) {
context.progress(p -> p
.progress(0.0)
.total(1.0)
.message("Starting task: " + taskName));
// Perform work...
processTask(taskName);
context.progress(p -> p
.progress(0.5)
.total(1.0)
.message("Halfway through task"));
// More work...
finalizeTask(taskName);
context.progress(p -> p
.progress(1.0)
.total(1.0)
.message("Task completed"));
}
return "Task " + taskName + " completed";
}Elicitation (stateful only):
public record UserInfo(String name, String email, int age) {}
@McpTool(name = "collect-user-info", description = "Collect user information")
public String collectUserInfo(McpSyncRequestContext context) {
if (!context.elicitEnabled()) {
return "Elicitation not available in this mode";
}
context.info("Requesting user information");
StructuredElicitResult<UserInfo> result = context.elicit(UserInfo.class);
if (result.action() == ElicitResult.Action.ACCEPT) {
UserInfo info = result.data();
context.info("Received user info: " + info.name());
return "User info collected: " + info;
} else {
context.warn("User rejected elicitation");
return "User declined to provide information";
}
}Sampling (stateful only):
@McpTool(name = "enhanced-search", description = "Search with LLM enhancement")
public String enhancedSearch(
McpSyncRequestContext context,
@McpToolParam(description = "Query", required = true) String query) {
if (!context.sampleEnabled()) {
return "Sampling not available";
}
context.info("Enhancing query with LLM");
CreateMessageResult llmResult = context.sample(
"Enhance this search query to be more specific: " + query);
String enhancedQuery = extractText(llmResult);
context.info("Enhanced query: " + enhancedQuery);
return performSearch(enhancedQuery);
}Accessing request metadata:
@McpTool(name = "inspect-request", description = "Inspect request details")
public String inspectRequest(McpSyncRequestContext context) {
CallToolRequest request = context.request();
McpMeta meta = context.meta();
StringBuilder info = new StringBuilder();
info.append("Tool: ").append(request.name()).append("\n");
info.append("Progress Token: ").append(request.progressToken()).append("\n");
info.append("Arguments: ").append(request.arguments()).append("\n");
return info.toString();
}Unified request context for asynchronous MCP operations (reactive).
/**
* Request context for asynchronous MCP operations
* All methods return Mono for reactive composition
*/
interface McpAsyncRequestContext {
/**
* Access the MCP request
*/
CallToolRequest request();
/**
* Access MCP metadata
*/
McpMeta meta();
/**
* Send info-level log message
*/
Mono<Void> info(String message);
/**
* Send debug-level log message
*/
Mono<Void> debug(String message);
/**
* Send warning-level log message
*/
Mono<Void> warn(String message);
/**
* Send error-level log message
*/
Mono<Void> error(String message);
/**
* Send progress notification with percentage
*/
Mono<Void> progress(int percentage);
/**
* Send custom progress notification
*/
Mono<Void> progress(Consumer<ProgressNotification.Builder> configurer);
/**
* Ping the client
*/
Mono<Void> ping();
/**
* Check if elicitation is enabled
*/
boolean elicitEnabled();
/**
* Check if sampling is enabled
*/
boolean sampleEnabled();
/**
* Request structured user input (reactive)
*/
<T> Mono<StructuredElicitResult<T>> elicit(Class<T> type);
/**
* Request structured user input with custom configuration (reactive)
* @param configurer Builder consumer to configure the elicitation request
* @param type The class representing the expected structure
* @return Mono of StructuredElicitResult containing user input or rejection
*/
<T> Mono<StructuredElicitResult<T>> elicit(Consumer<ElicitRequest.Builder> configurer, Class<T> type);
/**
* Request LLM sampling (reactive)
*/
Mono<CreateMessageResult> sample(String prompt);
/**
* Request LLM sampling with custom configuration (reactive)
* @param configurer Builder consumer to configure the sampling request
* @return Mono of CreateMessageResult containing the LLM response
*/
Mono<CreateMessageResult> sample(Consumer<CreateMessageRequest.Builder> configurer);
}Usage Example:
@McpTool(name = "async-process", description = "Async processing")
public Mono<String> asyncProcess(
McpAsyncRequestContext context,
@McpToolParam(description = "Data", required = true) String data) {
return context.info("Starting async processing")
.then(context.progress(0))
.then(Mono.defer(() -> processDataAsync(data)))
.flatMap(result -> context.progress(50).thenReturn(result))
.flatMap(result -> context.info("Processing complete").thenReturn(result))
.flatMap(result -> context.progress(100).thenReturn(result))
.map(result -> "Result: " + result);
}Lightweight transport context for stateless operations.
/**
* Lightweight context for stateless operations
* Provides access to transport-level information only
* No bidirectional operations (logging, progress, elicitation, sampling)
*/
interface McpTransportContext {
/**
* Access transport metadata
*/
Map<String, Object> getTransportMetadata();
/**
* Get request identifier
*/
String getRequestId();
}Usage Example:
@McpTool(name = "stateless-tool", description = "Stateless operation")
public String statelessTool(
McpTransportContext context,
@McpToolParam(description = "Input", required = true) String input) {
String requestId = context.getRequestId();
// Process without bidirectional operations
return "Processed: " + input + " (request: " + requestId + ")";
}Access to MCP metadata provided by the client.
/**
* Record providing access to MCP metadata map
*/
record McpMeta(Map<String, Object> map) {
/**
* Get metadata as a map
*/
Map<String, Object> asMap() {
return map;
}
/**
* Get a specific metadata value
*/
Object get(String key) {
return map.get(key);
}
/**
* Check if metadata key exists
*/
boolean contains(String key) {
return map.containsKey(key);
}
}Usage Example:
@McpTool(name = "check-metadata", description = "Check client metadata")
public String checkMetadata(McpSyncRequestContext context) {
McpMeta meta = context.meta();
if (meta.contains("client-version")) {
String version = (String) meta.get("client-version");
context.info("Client version: " + version);
}
return "Metadata keys: " + String.join(", ", meta.asMap().keySet());
}/**
* Progress notification sent to client
*/
class ProgressNotification {
static Builder builder() { ... }
interface Builder {
Builder progressToken(String token);
Builder progress(double progress);
Builder total(double total);
Builder message(String message);
ProgressNotification build();
}
}/**
* Result from elicitation request
*/
class StructuredElicitResult<T> {
/**
* The action taken by the user
*/
ElicitResult.Action action();
/**
* The structured data if accepted
*/
T data();
}
enum ElicitResult.Action {
ACCEPT,
REJECT
}/**
* Result from LLM sampling request
*/
class CreateMessageResult {
/**
* The message content from the LLM
*/
String content();
/**
* The role of the message
*/
Role role();
/**
* The model used for sampling
*/
String model();
/**
* Stop reason
*/
String stopReason();
}For simple operations that don't need request context:
@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;
}For operations that need logging and progress tracking:
@McpTool(name = "with-progress", description = "Operation with progress")
public String withProgress(
McpSyncRequestContext context,
@McpToolParam(description = "Input", required = true) String input) {
context.info("Starting");
context.progress(0);
// Work...
context.progress(50);
// More work...
context.progress(100);
return "Done";
}For stateless operations with minimal context:
@McpTool(name = "stateless", description = "Stateless operation")
public String stateless(
McpTransportContext context,
@McpToolParam(description = "Input", required = true) String input) {
// Access transport metadata only
return "Processed";
}For operations that need user input or LLM assistance:
@McpTool(name = "interactive", description = "Interactive operation")
public String interactive(McpSyncRequestContext context) {
if (context.elicitEnabled()) {
var result = context.elicit(UserInput.class);
// Use elicited data
}
if (context.sampleEnabled()) {
var llmResult = context.sample("Generate suggestion");
// Use LLM output
}
return "Done";
}| Feature | Stateful (SSE/Streamable) | Stateless |
|---|---|---|
| Logging | ✓ Supported | ✗ Not supported |
| Progress | ✓ Supported | ✗ Not supported |
| Ping | ✓ Supported | ✗ Not supported |
| Elicitation | ✓ Supported | ✗ Not supported |
| Sampling | ✓ Supported | ✗ Not supported |
| Context Type | McpSyncRequestContext / McpAsyncRequestContext | McpTransportContext |
Note: Methods using full request context (McpSyncRequestContext/McpAsyncRequestContext) are automatically filtered out in stateless mode. Use McpTransportContext or no context for stateless compatibility.
// Request context interfaces
import org.springaicommunity.mcp.server.McpSyncRequestContext;
import org.springaicommunity.mcp.server.McpAsyncRequestContext;
import org.springaicommunity.mcp.server.McpTransportContext;
// Schema types used in context
import org.springaicommunity.mcp.schema.McpSchema.CallToolRequest;
import org.springaicommunity.mcp.schema.McpSchema.ProgressNotification;
import org.springaicommunity.mcp.schema.McpSchema.LoggingLevel;
import org.springaicommunity.mcp.schema.McpSchema.CreateMessageResult;
import org.springaicommunity.mcp.schema.McpSchema.ElicitResult;
import org.springaicommunity.mcp.schema.McpSchema.StructuredElicitResult;
// Supporting types
import org.springaicommunity.mcp.schema.McpSchema.McpMeta;
// Reactive types (for async context)
import reactor.core.publisher.Mono;
// Java standard library
import java.util.Map;
import java.util.function.Consumer;