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
Common patterns for integrating MCP servers with other systems.
Secure MCP tools with Spring Security.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/mcp/**", "/sse/**").authenticated()
.anyRequest().permitAll()
)
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()); // Disable for MCP endpoints
return http.build();
}
}
@Component
public class SecureTools {
@McpTool(name = "admin_operation", description = "Admin-only operation")
@PreAuthorize("hasRole('ADMIN')")
public CallToolResult adminOperation(
@McpToolParam(description = "Action", required = true) String action) {
// Get current user
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// Perform admin operation
return CallToolResult.builder()
.addTextContent("Admin operation '" + action + "' executed by " + username)
.build();
}
}Ensure data consistency with transactions.
@Component
public class TransactionalTools {
@Autowired
private UserRepository userRepository;
@Autowired
private AuditRepository auditRepository;
@McpTool(name = "update_user", description = "Update user with audit trail")
@Transactional
public CallToolResult updateUser(
McpSyncRequestContext context,
@McpToolParam(description = "User ID", required = true) Long userId,
@McpToolParam(description = "New email", required = true) String email) {
try {
context.info("Starting user update transaction");
// Update user
User user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
String oldEmail = user.getEmail();
user.setEmail(email);
userRepository.save(user);
// Create audit record
AuditLog audit = new AuditLog();
audit.setAction("UPDATE_USER_EMAIL");
audit.setUserId(userId);
audit.setOldValue(oldEmail);
audit.setNewValue(email);
audit.setTimestamp(LocalDateTime.now());
auditRepository.save(audit);
context.info("Transaction completed successfully");
return CallToolResult.builder()
.addTextContent("User updated successfully")
.build();
} catch (Exception e) {
context.error("Transaction failed: " + e.getMessage());
// Transaction will be rolled back automatically
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
}
}
}Integrate with Spring's event system.
// Custom event
public class ToolExecutedEvent extends ApplicationEvent {
private final String toolName;
private final Map<String, Object> arguments;
private final boolean success;
public ToolExecutedEvent(Object source, String toolName,
Map<String, Object> arguments, boolean success) {
super(source);
this.toolName = toolName;
this.arguments = arguments;
this.success = success;
}
// Getters...
}
@Component
public class EventDrivenTools {
@Autowired
private ApplicationEventPublisher eventPublisher;
@McpTool(name = "process_order", description = "Process customer order")
public CallToolResult processOrder(
McpSyncRequestContext context,
@McpToolParam(description = "Order ID", required = true) String orderId) {
try {
context.info("Processing order: " + orderId);
// Process order
Order order = orderService.process(orderId);
// Publish event
eventPublisher.publishEvent(new ToolExecutedEvent(
this,
"process_order",
Map.of("orderId", orderId),
true
));
return CallToolResult.builder()
.addTextContent("Order processed: " + order.getId())
.build();
} catch (Exception e) {
// Publish failure event
eventPublisher.publishEvent(new ToolExecutedEvent(
this,
"process_order",
Map.of("orderId", orderId),
false
));
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
}
}
}
@Component
public class ToolEventListener {
@EventListener
public void handleToolExecuted(ToolExecutedEvent event) {
// Log, send notifications, update metrics, etc.
if (event.isSuccess()) {
logger.info("Tool {} executed successfully", event.getToolName());
} else {
logger.error("Tool {} failed", event.getToolName());
alertService.sendAlert("Tool execution failed: " + event.getToolName());
}
}
}Improve performance with caching.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new CaffeineCacheManager("tools", "resources", "api-responses");
}
}
@Component
public class CachedTools {
@McpTool(name = "get_expensive_data", description = "Get data with caching")
@Cacheable(value = "api-responses", key = "#id")
public String getExpensiveData(
McpSyncRequestContext context,
@McpToolParam(description = "Data ID", required = true) String id) {
context.info("Cache miss - fetching data for: " + id);
// Expensive operation
String data = externalApiService.fetchData(id);
context.info("Data fetched and cached");
return data;
}
@McpTool(name = "invalidate_cache", description = "Clear cached data")
@CacheEvict(value = "api-responses", key = "#id")
public CallToolResult invalidateCache(
@McpToolParam(description = "Data ID", required = true) String id) {
return CallToolResult.builder()
.addTextContent("Cache invalidated for: " + id)
.build();
}
@McpTool(name = "clear_all_cache", description = "Clear all caches")
@CacheEvict(value = {"tools", "resources", "api-responses"}, allEntries = true)
public CallToolResult clearAllCache() {
return CallToolResult.builder()
.addTextContent("All caches cleared")
.build();
}
}Monitor tool usage with Spring Boot Actuator and Micrometer.
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
}
@Component
public class MonitoredTools {
@Autowired
private MeterRegistry meterRegistry;
@McpTool(name = "monitored_operation", description = "Operation with metrics")
public CallToolResult monitoredOperation(
McpSyncRequestContext context,
@McpToolParam(description = "Input", required = true) String input) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
// Increment counter
meterRegistry.counter("mcp.tool.invocations",
"tool", "monitored_operation").increment();
// Perform operation
String result = performOperation(input);
// Record success
meterRegistry.counter("mcp.tool.success",
"tool", "monitored_operation").increment();
return CallToolResult.builder()
.addTextContent(result)
.build();
} catch (Exception e) {
// Record failure
meterRegistry.counter("mcp.tool.errors",
"tool", "monitored_operation",
"error", e.getClass().getSimpleName()).increment();
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
} finally {
// Record duration
sample.stop(meterRegistry.timer("mcp.tool.duration",
"tool", "monitored_operation"));
}
}
}
// Expose metrics endpoint
@Configuration
public class ActuatorConfig {
@Bean
public ManagementServerProperties managementServerProperties() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.getMetrics().getExport().getPrometheus().setEnabled(true);
return properties;
}
}Protect tools with rate limiting.
@Component
public class RateLimitedTools {
private final RateLimiter rateLimiter = RateLimiter.create(10.0); // 10 requests/second
private final Map<String, RateLimiter> userLimiters = new ConcurrentHashMap<>();
@McpTool(name = "rate_limited_api", description = "API call with rate limiting")
public CallToolResult rateLimitedApi(
McpSyncRequestContext context,
@McpToolParam(description = "User ID", required = true) String userId,
@McpToolParam(description = "Query", required = true) String query) {
// Get or create user-specific rate limiter
RateLimiter userLimiter = userLimiters.computeIfAbsent(
userId,
k -> RateLimiter.create(5.0) // 5 requests/second per user
);
// Check global rate limit
if (!rateLimiter.tryAcquire()) {
return CallToolResult.builder()
.addTextContent("Error: Global rate limit exceeded. Please try again later.")
.isError(true)
.build();
}
// Check user rate limit
if (!userLimiter.tryAcquire()) {
return CallToolResult.builder()
.addTextContent("Error: User rate limit exceeded. Please slow down.")
.isError(true)
.build();
}
// Process request
String result = apiService.query(query);
return CallToolResult.builder()
.addTextContent(result)
.build();
}
}Offload long-running tasks to message queues.
@Configuration
public class QueueConfig {
@Bean
public Queue taskQueue() {
return new Queue("mcp-tasks", true);
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
return new RabbitTemplate(connectionFactory);
}
}
@Component
public class AsyncQueueTools {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private TaskRepository taskRepository;
@McpTool(name = "submit_async_task", description = "Submit task for async processing")
public CallToolResult submitAsyncTask(
McpSyncRequestContext context,
@McpToolParam(description = "Task data", required = true) String taskData) {
// Create task record
Task task = new Task();
task.setId(UUID.randomUUID().toString());
task.setData(taskData);
task.setStatus("QUEUED");
task.setCreatedAt(LocalDateTime.now());
taskRepository.save(task);
// Send to queue
rabbitTemplate.convertAndSend("mcp-tasks", task);
context.info("Task queued: " + task.getId());
return CallToolResult.builder()
.addTextContent("Task submitted. ID: " + task.getId())
.build();
}
@McpTool(name = "check_task_status", description = "Check async task status")
public CallToolResult checkTaskStatus(
@McpToolParam(description = "Task ID", required = true) String taskId) {
Task task = taskRepository.findById(taskId)
.orElseThrow(() -> new EntityNotFoundException("Task not found"));
return CallToolResult.builder()
.addTextContent("Task status: " + task.getStatus())
.build();
}
}
@Component
public class TaskProcessor {
@RabbitListener(queues = "mcp-tasks")
public void processTask(Task task) {
try {
// Update status
task.setStatus("PROCESSING");
taskRepository.save(task);
// Process task
String result = performLongRunningTask(task.getData());
// Update with result
task.setStatus("COMPLETED");
task.setResult(result);
task.setCompletedAt(LocalDateTime.now());
taskRepository.save(task);
} catch (Exception e) {
task.setStatus("FAILED");
task.setError(e.getMessage());
taskRepository.save(task);
}
}
}Support multiple tenants in a single MCP server.
@Component
public class MultiTenantTools {
@Autowired
private TenantContext tenantContext;
@McpTool(name = "get_tenant_data", description = "Get data for current tenant")
public CallToolResult getTenantData(
McpSyncRequestContext context,
@McpToolParam(description = "Data key", required = true) String key) {
// Extract tenant from metadata
McpMeta meta = context.meta();
String tenantId = (String) meta.get("tenant-id");
if (tenantId == null) {
return CallToolResult.builder()
.addTextContent("Error: Tenant ID not provided")
.isError(true)
.build();
}
// Set tenant context
tenantContext.setTenantId(tenantId);
try {
// Query tenant-specific data
String data = dataService.getData(key); // Automatically filtered by tenant
return CallToolResult.builder()
.addTextContent(data)
.build();
} finally {
tenantContext.clear();
}
}
}
// Tenant context holder
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TenantContext {
private String tenantId;
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
public String getTenantId() {
return tenantId;
}
public void clear() {
this.tenantId = null;
}
}