CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-starter-mcp-server

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

Overview
Eval results
Files

integration-patterns.mddocs/examples/

Integration Patterns

Common patterns for integrating MCP servers with other systems.

Pattern 1: Spring Security Integration

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();
    }
}

Pattern 2: Transaction Management

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();
        }
    }
}

Pattern 3: Event-Driven Architecture

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());
        }
    }
}

Pattern 4: Caching Strategy

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();
    }
}

Pattern 5: Metrics and Monitoring

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;
    }
}

Pattern 6: Rate Limiting

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();
    }
}

Pattern 7: Async Processing with Queues

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);
        }
    }
}

Pattern 8: Multi-Tenancy

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;
    }
}

Next Steps

  • Real-World Scenarios - Complete implementations
  • Edge Cases - Handle complex scenarios
  • Configuration Guide - Configure integrations
tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-server@1.1.0

docs

examples

edge-cases.md

integration-patterns.md

real-world-scenarios.md

index.md

tile.json