CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux

Spring Boot starter providing auto-configuration for Model Context Protocol (MCP) client with Spring WebFlux, enabling reactive AI applications to connect to MCP servers via SSE and Streamable HTTP transports

Overview
Eval results
Files

edge-cases.mddocs/examples/

Edge Cases and Advanced Scenarios

Advanced usage patterns, corner cases, and solutions for complex requirements.

Edge Case 1: Handling Duplicate Tool Names

When multiple servers provide tools with the same name.

Problem

spring.ai.mcp.client:
  sse:
    connections:
      server-a:
        url: http://localhost:8080  # Provides tool "search"
      server-b:
        url: http://localhost:9000  # Also provides tool "search"

Solution: Custom Prefix Generator

@Bean
public McpToolNamePrefixGenerator uniqueNamingStrategy() {
    Map<String, Integer> nameCounters = new ConcurrentHashMap<>();
    
    return (connectionInfo, tool) -> {
        String originalName = tool.name();
        String serverName = connectionInfo.initializeResult()
            .serverInfo().name();
        
        // Track how many times we've seen this tool name
        int count = nameCounters.merge(originalName, 1, Integer::sum);
        
        if (count == 1) {
            // First occurrence - use original name
            return originalName;
        } else {
            // Duplicate - prefix with server name
            return sanitize(serverName) + "_" + originalName;
        }
    };
}

Edge Case 2: Server Not Available at Startup

Handle scenarios where MCP servers aren't available when Spring starts.

Solution: Lazy Initialization

spring.ai.mcp.client:
  initialized: false  # Don't auto-initialize
  sse:
    connections:
      optional-server:
        url: http://localhost:8080
@Service
public class LazyMcpService {
    
    private final List<McpSyncClient> clients;
    
    public LazyMcpService(List<McpSyncClient> clients) {
        this.clients = clients;
    }
    
    public void ensureInitialized(McpSyncClient client) {
        if (!client.isInitialized()) {
            try {
                client.initialize();
                log.info("Initialized client: {}", client.getClientInfo().name());
            } catch (Exception e) {
                log.error("Failed to initialize client", e);
                // Handle failure - maybe schedule retry
                scheduleRetry(client);
            }
        }
    }
    
    private void scheduleRetry(McpSyncClient client) {
        // Implement retry logic with exponential backoff
    }
}

Edge Case 3: Large Tool Result Payloads

Handle tools that return large amounts of data.

Solution: Streaming and Chunking

@Bean
public WebClient.Builder largePayloadWebClientBuilder() {
    return WebClient.builder()
        .codecs(configurer -> configurer
            .defaultCodecs()
            .maxInMemorySize(100 * 1024 * 1024)); // 100MB buffer
}
@Service
public class LargeResultHandler {
    
    public void handleLargeResult(String toolName, Map<String, Object> args) {
        // For async clients with large results
        asyncClient.callTool(request)
            .doOnNext(result -> {
                // Process in chunks
                processInChunks(result.content());
            })
            .subscribe();
    }
    
    private void processInChunks(Object content) {
        // Chunk processing logic
        if (content instanceof String large) {
            int chunkSize = 1024 * 1024; // 1MB chunks
            for (int i = 0; i < large.length(); i += chunkSize) {
                String chunk = large.substring(i, 
                    Math.min(i + chunkSize, large.length()));
                processChunk(chunk);
            }
        }
    }
    
    private void processChunk(String chunk) {
        // Process each chunk
    }
}

Edge Case 4: Tool Schema Validation

Validate tool arguments before execution.

Solution: Schema Validator

import com.fasterxml.jackson.databind.JsonNode;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;

@Service
public class ToolValidator {
    
    private final JsonSchemaFactory schemaFactory = 
        JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
    
    public void validateAndExecute(McpSyncClient client, String toolName,
                                  Map<String, Object> args) {
        // Get tool schema
        Tool tool = client.listTools().stream()
            .filter(t -> t.name().equals(toolName))
            .findFirst()
            .orElseThrow();
        
        // Validate arguments against schema
        Set<ValidationMessage> errors = validateArguments(tool.inputSchema(), args);
        
        if (!errors.isEmpty()) {
            throw new IllegalArgumentException(
                "Invalid arguments: " + errors.stream()
                    .map(ValidationMessage::getMessage)
                    .collect(Collectors.joining(", "))
            );
        }
        
        // Execute tool
        var request = CallToolRequest.builder()
            .name(toolName)
            .arguments(args)
            .build();
        client.callTool(request);
    }
    
    private Set<ValidationMessage> validateArguments(Object schema, 
                                                    Map<String, Object> args) {
        try {
            JsonSchema jsonSchema = schemaFactory.getSchema(
                objectMapper.writeValueAsString(schema)
            );
            JsonNode argsNode = objectMapper.valueToTree(args);
            return jsonSchema.validate(argsNode);
        } catch (Exception e) {
            log.error("Schema validation error", e);
            return Set.of();
        }
    }
}

Edge Case 5: Stdio Process Crash Recovery

Handle stdio process crashes and automatic recovery.

Solution: Process Monitor

@Component
public class StdioProcessMonitor {
    
    @Scheduled(fixedRate = 30000) // Check every 30 seconds
    public void monitorProcesses() {
        for (McpSyncClient client : clients) {
            if (!isHealthy(client)) {
                log.warn("Client unhealthy: {}", client.getClientInfo().name());
                attemptRecovery(client);
            }
        }
    }
    
    private boolean isHealthy(McpSyncClient client) {
        try {
            // Simple health check - list tools
            client.listTools();
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    private void attemptRecovery(McpSyncClient client) {
        try {
            // Try to reinitialize
            if (!client.isInitialized()) {
                client.initialize();
                log.info("Recovered client: {}", client.getClientInfo().name());
            }
        } catch (Exception e) {
            log.error("Recovery failed", e);
            sendAlert("MCP client recovery failed: " + 
                client.getClientInfo().name());
        }
    }
    
    private void sendAlert(String message) {
        // Alert implementation
    }
}

Edge Case 6: Tool Timeout Handling

Different timeouts for different tools.

Solution: Per-Tool Timeout

@Service
public class SmartTimeoutService {
    
    private final Map<String, Duration> toolTimeouts = Map.of(
        "heavy_computation", Duration.ofMinutes(5),
        "database_query", Duration.ofSeconds(30),
        "quick_lookup", Duration.ofSeconds(5)
    );
    
    public Object executeWithCustomTimeout(McpAsyncClient client,
                                           String toolName,
                                           Map<String, Object> args) {
        Duration timeout = toolTimeouts.getOrDefault(toolName,
            Duration.ofSeconds(20));
        
        var request = CallToolRequest.builder()
            .name(toolName)
            .arguments(args)
            .build();
        
        return client.callTool(request)
            .timeout(timeout)
            .onErrorResume(TimeoutException.class, e -> {
                log.error("Tool {} timed out after {}", toolName, timeout);
                return Mono.just(createErrorResult(toolName, "Timeout"));
            })
            .map(CallToolResult::content)
            .block();
    }
    
    private CallToolResult createErrorResult(String toolName, String error) {
        return CallToolResult.builder()
            .content(Map.of("error", error, "tool", toolName))
            .isError(true)
            .build();
    }
}

Edge Case 7: Concurrent Tool Execution Limits

Prevent overwhelming servers with too many concurrent requests.

Solution: Rate Limiting

import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Refill;

@Service
public class RateLimitedToolService {
    
    private final Map<String, Bucket> rateLimiters = new ConcurrentHashMap<>();
    
    public Object executeWithRateLimit(McpSyncClient client,
                                       String toolName,
                                       Map<String, Object> args) {
        String serverName = client.getCurrentInitializationResult()
            .serverInfo().name();
        
        // Get or create rate limiter for this server
        Bucket bucket = rateLimiters.computeIfAbsent(serverName, k -> {
            // Allow 10 requests per second
            Bandwidth limit = Bandwidth.classic(10, 
                Refill.intervally(10, Duration.ofSeconds(1)));
            return Bucket.builder().addLimit(limit).build();
        });
        
        // Wait for rate limit
        bucket.asBlocking().consume(1);
        
        // Execute tool
        var request = CallToolRequest.builder()
            .name(toolName)
            .arguments(args)
            .build();
        
        return client.callTool(request).content();
    }
}

Edge Case 8: Tool Execution Retries

Retry failed tool executions with exponential backoff.

Solution: Resilience4j Integration

import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;

@Configuration
public class RetryConfiguration {
    
    @Bean
    public RetryRegistry retryRegistry() {
        RetryConfig config = RetryConfig.custom()
            .maxAttempts(3)
            .waitDuration(Duration.ofSeconds(2))
            .exponentialBackoffMultiplier(2.0)
            .retryExceptions(IOException.class, TimeoutException.class)
            .ignoreExceptions(IllegalArgumentException.class)
            .build();
        
        return RetryRegistry.of(config);
    }
    
    @Bean
    public Retry toolRetry(RetryRegistry registry) {
        return registry.retry("mcp-tools");
    }
}

@Service
public class RetryableToolService {
    
    private final Retry retry;
    private final List<McpSyncClient> clients;
    
    public RetryableToolService(Retry retry, List<McpSyncClient> clients) {
        this.retry = retry;
        this.clients = clients;
    }
    
    public Object executeWithRetry(String toolName, Map<String, Object> args) {
        return Retry.decorateSupplier(retry, () -> 
            executeToolInternal(toolName, args)
        ).get();
    }
    
    private Object executeToolInternal(String toolName, Map<String, Object> args) {
        // Tool execution logic
        for (McpSyncClient client : clients) {
            // ... find and execute tool
        }
        throw new IllegalStateException("Tool not found");
    }
}

Edge Case 9: Mixed Sync/Async Usage

Use both sync and async patterns in the same application (not recommended but sometimes necessary).

Problem

Can't configure both type: SYNC and type: ASYNC simultaneously.

Solution: Manual Client Creation

@Configuration
public class MixedClientConfig {
    
    // Use default auto-configured sync clients
    // Then manually create async client for specific use case
    
    @Bean
    public McpAsyncClient manualAsyncClient(
            List<NamedClientMcpTransport> transports) {
        
        // Find specific transport
        NamedClientMcpTransport transport = transports.stream()
            .filter(t -> "special-server".equals(t.name()))
            .findFirst()
            .orElseThrow();
        
        // Manually create async client
        return McpClient.async(transport.transport())
            .clientInfo(new Implementation("manual-async", "1.0.0"))
            .requestTimeout(Duration.ofSeconds(30))
            .build();
    }
}

Edge Case 10: Tool Result Streaming

Stream large tool results progressively.

Solution: Reactive Streaming

@Service
public class StreamingToolService {
    
    private final List<McpAsyncClient> clients;
    
    public Flux<String> streamToolResult(String toolName, 
                                        Map<String, Object> args) {
        return Flux.fromIterable(clients)
            .flatMap(client -> 
                client.callTool(createRequest(toolName, args))
                    .flatMapMany(result -> {
                        // Split result into chunks
                        String content = result.content().toString();
                        return Flux.fromArray(content.split("\n"));
                    })
            )
            .take(1); // First successful result
    }
    
    private CallToolRequest createRequest(String toolName, 
                                         Map<String, Object> args) {
        return CallToolRequest.builder()
            .name(toolName)
            .arguments(args)
            .build();
    }
}

Edge Case 11: Connection-Specific ObjectMapper

Different JSON configuration for different servers.

Solution: Custom Transport Creation

@Configuration
public class CustomTransportConfig {
    
    @Bean
    public List<NamedClientMcpTransport> customSseTransports(
            McpSseClientConnectionDetails connectionDetails) {
        
        List<NamedClientMcpTransport> transports = new ArrayList<>();
        
        for (var entry : connectionDetails.getConnections().entrySet()) {
            String name = entry.getKey();
            SseParameters params = entry.getValue();
            
            // Create connection-specific ObjectMapper
            ObjectMapper mapper = createMapperForConnection(name);
            
            // Create transport with custom mapper
            var webClient = WebClient.builder()
                .baseUrl(params.url())
                .build();
            
            var transport = WebFluxSseClientTransport.builder(webClient)
                .sseEndpoint(params.sseEndpoint() != null ? 
                    params.sseEndpoint() : "/sse")
                .jsonMapper(new JacksonMcpJsonMapper(mapper))
                .build();
            
            transports.add(new NamedClientMcpTransport(name, transport));
        }
        
        return transports;
    }
    
    private ObjectMapper createMapperForConnection(String connectionName) {
        ObjectMapper mapper = new ObjectMapper();
        
        // Connection-specific configuration
        if (connectionName.equals("strict-server")) {
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
        } else {
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        }
        
        return mapper;
    }
}

Edge Case 12: Conditional Tool Availability

Tools should only be available in certain contexts.

Solution: Context-Aware Filter

@Component
public class ContextAwareToolFilter implements McpToolFilter {
    
    private final SecurityContext securityContext;
    
    public ContextAwareToolFilter(SecurityContext securityContext) {
        this.securityContext = securityContext;
    }
    
    @Override
    public boolean test(McpConnectionInfo connectionInfo, Tool tool) {
        String toolName = tool.name();
        
        // Admin-only tools
        if (toolName.startsWith("admin_")) {
            return securityContext.hasRole("ADMIN");
        }
        
        // Premium features
        if (toolName.startsWith("premium_")) {
            return securityContext.hasPremiumSubscription();
        }
        
        // Time-based restrictions
        if (toolName.equals("batch_process")) {
            return isOffPeakHours();
        }
        
        return true;
    }
    
    private boolean isOffPeakHours() {
        int hour = LocalTime.now().getHour();
        return hour < 6 || hour > 22;
    }
}

Edge Case 13: Stdio Process Environment Variables

Pass dynamic environment variables to stdio processes.

Solution: Programmatic Configuration

@Bean
public McpStdioClientProperties dynamicStdioProperties() {
    McpStdioClientProperties properties = new McpStdioClientProperties();
    
    // Build dynamic environment
    Map<String, String> env = new HashMap<>();
    env.put("DATABASE_URL", getDatabaseUrl());
    env.put("API_KEY", getApiKey());
    env.put("LOG_LEVEL", getLogLevel());
    env.put("TEMP_DIR", System.getProperty("java.io.tmpdir"));
    
    Parameters params = new Parameters(
        "node",
        List.of("./mcp-server.js"),
        env
    );
    
    properties.getConnections().put("dynamic-server", params);
    
    return properties;
}

private String getDatabaseUrl() {
    // Load from vault or config server
    return vaultService.getSecret("database.url");
}

private String getApiKey() {
    // Load from secure source
    return secretManager.getApiKey();
}

private String getLogLevel() {
    // Based on profile
    return environment.getActiveProfiles()[0].equals("prod") ? "INFO" : "DEBUG";
}

Edge Case 14: Tool Result Caching

Cache expensive tool results with TTL.

Solution: Caffeine Cache

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

@Service
public class CachedToolExecutionService {
    
    private final Cache<CacheKey, Object> resultCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(Duration.ofMinutes(10))
        .recordStats()
        .build();
    
    private final List<McpSyncClient> clients;
    
    public Object executeCached(String toolName, Map<String, Object> args) {
        CacheKey key = new CacheKey(toolName, args);
        
        return resultCache.get(key, k -> {
            // Cache miss - execute tool
            return executeToolInternal(toolName, args);
        });
    }
    
    private Object executeToolInternal(String toolName, Map<String, Object> args) {
        // Find and execute tool
        for (McpSyncClient client : clients) {
            // ... execution logic
        }
        throw new IllegalStateException("Tool not found");
    }
    
    @EventListener
    public void invalidateOnToolChange(McpToolsChangedEvent event) {
        // Invalidate cache when tools change
        resultCache.invalidateAll();
    }
    
    private record CacheKey(String toolName, Map<String, Object> args) {}
}

Edge Case 15: SSE Connection Failure Handling

Handle SSE connection drops and reconnection logic.

Solution: Connection Health Monitor

@Service
public class SseHealthMonitor {
    
    private final Map<String, ConnectionHealth> healthStatus = 
        new ConcurrentHashMap<>();
    
    @Scheduled(fixedRate = 10000) // Check every 10 seconds
    public void monitorHealth() {
        for (McpSyncClient client : clients) {
            String clientName = client.getClientInfo().name();
            
            try {
                // Lightweight health check
                client.listTools();
                recordHealthy(clientName);
            } catch (Exception e) {
                recordUnhealthy(clientName, e);
                handleUnhealthyConnection(clientName, e);
            }
        }
    }
    
    private void recordHealthy(String clientName) {
        healthStatus.compute(clientName, (k, v) -> {
            if (v == null) {
                return new ConnectionHealth(0, System.currentTimeMillis());
            }
            // Reset failure count on success
            return new ConnectionHealth(0, System.currentTimeMillis());
        });
    }
    
    private void recordUnhealthy(String clientName, Exception error) {
        healthStatus.compute(clientName, (k, v) -> {
            int failures = (v != null ? v.failureCount : 0) + 1;
            return new ConnectionHealth(failures, System.currentTimeMillis());
        });
    }
    
    private void handleUnhealthyConnection(String clientName, Exception error) {
        ConnectionHealth health = healthStatus.get(clientName);
        
        if (health != null && health.failureCount >= 3) {
            log.error("Connection {} has failed {} times", 
                clientName, health.failureCount);
            sendAlert("MCP connection unhealthy: " + clientName);
        }
    }
    
    private record ConnectionHealth(int failureCount, long lastCheck) {}
    
    private void sendAlert(String message) {
        // Alert implementation
    }
}

Edge Case 16: Tool Parameter Type Conversion

Handle type mismatches between Spring AI and MCP.

Solution: Type Converter

@Component
public class ToolParameterConverter {
    
    public Map<String, Object> convertParameters(
            Tool tool,
            Map<String, Object> springAiArgs) {
        
        Map<String, Object> mcpArgs = new HashMap<>();
        
        // Parse input schema
        JsonNode schema = objectMapper.valueToTree(tool.inputSchema());
        JsonNode properties = schema.get("properties");
        
        if (properties != null) {
            properties.fields().forEachRemaining(entry -> {
                String paramName = entry.getKey();
                JsonNode paramSchema = entry.getValue();
                Object value = springAiArgs.get(paramName);
                
                if (value != null) {
                    // Convert based on schema type
                    Object converted = convertValue(value, paramSchema);
                    mcpArgs.put(paramName, converted);
                }
            });
        }
        
        return mcpArgs;
    }
    
    private Object convertValue(Object value, JsonNode schema) {
        String type = schema.get("type").asText();
        
        return switch (type) {
            case "integer" -> convertToInt(value);
            case "number" -> convertToDouble(value);
            case "boolean" -> convertToBoolean(value);
            case "array" -> convertToList(value);
            case "object" -> convertToMap(value);
            default -> value.toString();
        };
    }
    
    private Integer convertToInt(Object value) {
        if (value instanceof Number num) return num.intValue();
        return Integer.parseInt(value.toString());
    }
    
    private Double convertToDouble(Object value) {
        if (value instanceof Number num) return num.doubleValue();
        return Double.parseDouble(value.toString());
    }
    
    private Boolean convertToBoolean(Object value) {
        if (value instanceof Boolean bool) return bool;
        return Boolean.parseBoolean(value.toString());
    }
    
    private List<?> convertToList(Object value) {
        if (value instanceof List<?> list) return list;
        return List.of(value);
    }
    
    private Map<?, ?> convertToMap(Object value) {
        if (value instanceof Map<?, ?> map) return map;
        throw new IllegalArgumentException("Cannot convert to map: " + value);
    }
}

Edge Case 17: Server Certificate Validation

Custom certificate validation for self-signed certificates.

Solution: Custom SSL Context

import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

@Configuration
public class SslConfiguration {
    
    @Bean
    @Profile("dev") // Only for development!
    public WebClient.Builder devWebClientBuilder() throws Exception {
        // ONLY FOR DEVELOPMENT - accepts self-signed certificates
        TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) {}
                public void checkServerTrusted(X509Certificate[] chain, String authType) {}
                public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
            }
        };
        
        SslContext sslContext = SslContextBuilder
            .forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .build();
        
        HttpClient httpClient = HttpClient.create()
            .secure(spec -> spec.sslContext(sslContext));
        
        return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient));
    }
}

Edge Case 18: Tool Execution Auditing

Complete audit trail for all tool executions.

Solution: Audit Aspect

@Aspect
@Component
public class ToolAuditAspect {
    
    private final AuditRepository auditRepository;
    
    @Around("execution(* io.modelcontextprotocol.client.McpSyncClient.callTool(..))")
    public Object auditToolExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        CallToolRequest request = (CallToolRequest) joinPoint.getArgs()[0];
        McpSyncClient client = (McpSyncClient) joinPoint.getTarget();
        
        String clientName = client.getClientInfo().name();
        String toolName = request.name();
        Map<String, Object> args = request.arguments();
        
        AuditRecord audit = new AuditRecord();
        audit.setClientName(clientName);
        audit.setToolName(toolName);
        audit.setArguments(objectMapper.writeValueAsString(args));
        audit.setTimestamp(Instant.now());
        audit.setUserId(getCurrentUserId());
        
        try {
            long startTime = System.currentTimeMillis();
            CallToolResult result = (CallToolResult) joinPoint.proceed();
            long duration = System.currentTimeMillis() - startTime;
            
            audit.setDuration(duration);
            audit.setSuccess(true);
            audit.setResult(objectMapper.writeValueAsString(result.content()));
            
            return result;
        } catch (Exception e) {
            audit.setSuccess(false);
            audit.setError(e.getMessage());
            throw e;
        } finally {
            auditRepository.save(audit);
        }
    }
    
    private String getCurrentUserId() {
        // Get from security context
        return SecurityContextHolder.getContext()
            .getAuthentication()
            .getName();
    }
}

Edge Case 19: Stdio Process Output Redirection

Capture and log stdio process stderr.

Solution: Process Output Handler

// Note: This requires custom transport implementation
// Showing conceptual approach

@Component
public class StdioOutputHandler {
    
    @EventListener
    public void onContextRefresh(ContextRefreshedEvent event) {
        // Find stdio transports and monitor their processes
        List<NamedClientMcpTransport> transports = 
            applicationContext.getBean(
                new ParameterizedTypeReference<List<NamedClientMcpTransport>>() {}
            );
        
        transports.stream()
            .filter(t -> t.transport() instanceof StdioClientTransport)
            .forEach(this::monitorProcess);
    }
    
    private void monitorProcess(NamedClientMcpTransport transport) {
        // Access underlying process (if exposed)
        // Read stderr asynchronously
        new Thread(() -> {
            // Pseudo-code - actual implementation depends on MCP SDK
            BufferedReader reader = getProcessStderr(transport);
            String line;
            try {
                while ((line = reader.readLine()) != null) {
                    log.info("[{}] {}", transport.name(), line);
                }
            } catch (IOException e) {
                log.error("Error reading process output", e);
            }
        }).start();
    }
}

Edge Case 20: Multi-Tenant Tool Access

Different tool access based on tenant/user.

Solution: Tenant-Aware Filter

@Component
public class TenantAwareToolFilter implements McpToolFilter {
    
    @Override
    public boolean test(McpConnectionInfo connectionInfo, Tool tool) {
        String tenantId = getCurrentTenantId();
        String toolName = tool.name();
        
        // Load tenant permissions
        Set<String> allowedTools = getTenantAllowedTools(tenantId);
        
        return allowedTools.contains(toolName);
    }
    
    private String getCurrentTenantId() {
        // Get from security context or request scope
        return TenantContext.getCurrentTenant();
    }
    
    private Set<String> getTenantAllowedTools(String tenantId) {
        // Load from database or cache
        return tenantPermissionRepository.getAllowedTools(tenantId);
    }
}

// Request-scoped filter that changes per request
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.INTERFACES)
public McpToolFilter requestScopedFilter() {
    return new TenantAwareToolFilter();
}

Related Documentation

  • Real-World Scenarios - Common patterns
  • Configuration Reference - All properties
  • Customization Guide - Advanced customization
  • Tool Callbacks - Filtering and naming strategies
tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0

docs

examples

edge-cases.md

real-world-scenarios.md

index.md

tile.json