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

edge-cases.mddocs/examples/

Edge Cases and Advanced Scenarios

Handle complex edge cases and advanced scenarios in MCP servers.

Dynamic Schema Handling

When you need to accept arbitrary JSON structures:

@McpTool(name = "process_dynamic", description = "Process dynamic JSON data")
public CallToolResult processDynamic(CallToolRequest request) {
    Map<String, Object> args = request.arguments();
    
    // Handle unknown structure
    StringBuilder result = new StringBuilder("Processed fields:\n");
    
    for (Map.Entry<String, Object> entry : args.entrySet()) {
        String key = entry.getKey();
        Object value = entry.getValue();
        
        // Type-safe handling
        if (value instanceof String) {
            result.append(key).append(": ").append(value).append(" (string)\n");
        } else if (value instanceof Number) {
            result.append(key).append(": ").append(value).append(" (number)\n");
        } else if (value instanceof Boolean) {
            result.append(key).append(": ").append(value).append(" (boolean)\n");
        } else if (value instanceof Map) {
            result.append(key).append(": [nested object]\n");
        } else if (value instanceof List) {
            result.append(key).append(": [array with ")
                  .append(((List<?>) value).size()).append(" items]\n");
        }
    }
    
    return CallToolResult.builder()
        .addTextContent(result.toString())
        .build();
}

Handling Large Payloads

Streaming Large Results

@McpTool(name = "export_large_dataset", description = "Export large dataset")
public CallToolResult exportLargeDataset(
        McpSyncRequestContext context,
        @McpToolParam(description = "Table name", required = true) String table) {
    
    try {
        context.info("Starting export of table: " + table);
        
        // Stream to temporary file
        Path tempFile = Files.createTempFile("export-", ".json");
        
        try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) {
            writer.write("[");
            
            AtomicInteger count = new AtomicInteger(0);
            AtomicInteger lastProgress = new AtomicInteger(0);
            
            jdbcTemplate.query(
                "SELECT * FROM " + table,
                rs -> {
                    if (count.get() > 0) {
                        writer.write(",");
                    }
                    
                    // Convert row to JSON
                    Map<String, Object> row = new HashMap<>();
                    ResultSetMetaData metaData = rs.getMetaData();
                    for (int i = 1; i <= metaData.getColumnCount(); i++) {
                        row.put(metaData.getColumnName(i), rs.getObject(i));
                    }
                    
                    writer.write(new ObjectMapper().writeValueAsString(row));
                    
                    // Update progress every 100 rows
                    int current = count.incrementAndGet();
                    if (current % 100 == 0) {
                        int progress = Math.min(90, current / 100);
                        if (progress > lastProgress.get()) {
                            context.progress(progress);
                            lastProgress.set(progress);
                        }
                    }
                }
            );
            
            writer.write("]");
        }
        
        context.progress(100);
        context.info("Export complete: " + count.get() + " rows");
        
        // Return file path or upload to storage
        return CallToolResult.builder()
            .addTextContent("Export complete. File: " + tempFile.toString())
            .build();
            
    } catch (Exception e) {
        context.error("Export failed: " + e.getMessage());
        return CallToolResult.builder()
            .addTextContent("Error: " + e.getMessage())
            .isError(true)
            .build();
    }
}

Chunked Processing

@McpTool(name = "process_in_chunks", description = "Process large data in chunks")
public CallToolResult processInChunks(
        McpSyncRequestContext context,
        @McpToolParam(description = "Data array", required = true) List<String> data) {
    
    int chunkSize = 100;
    int totalChunks = (data.size() + chunkSize - 1) / chunkSize;
    
    context.info("Processing " + data.size() + " items in " + totalChunks + " chunks");
    
    List<String> results = new ArrayList<>();
    
    for (int i = 0; i < data.size(); i += chunkSize) {
        int end = Math.min(i + chunkSize, data.size());
        List<String> chunk = data.subList(i, end);
        
        // Process chunk
        String chunkResult = processChunk(chunk);
        results.add(chunkResult);
        
        // Update progress
        int progress = ((i + chunkSize) * 100) / data.size();
        context.progress(Math.min(progress, 100));
        context.info("Processed chunk " + (i / chunkSize + 1) + "/" + totalChunks);
    }
    
    return CallToolResult.builder()
        .addTextContent("Processed " + results.size() + " chunks successfully")
        .build();
}

Concurrent Tool Execution

Thread-Safe Tool Implementation

@Component
public class ConcurrentTools {
    
    private final ConcurrentHashMap<String, AtomicInteger> counters = new ConcurrentHashMap<>();
    private final ReentrantLock lock = new ReentrantLock();
    
    @McpTool(name = "increment_counter", description = "Thread-safe counter increment")
    public CallToolResult incrementCounter(
            @McpToolParam(description = "Counter name", required = true) String name) {
        
        AtomicInteger counter = counters.computeIfAbsent(name, k -> new AtomicInteger(0));
        int newValue = counter.incrementAndGet();
        
        return CallToolResult.builder()
            .addTextContent("Counter '" + name + "' is now: " + newValue)
            .build();
    }
    
    @McpTool(name = "critical_section", description = "Execute with exclusive lock")
    public CallToolResult criticalSection(
            McpSyncRequestContext context,
            @McpToolParam(description = "Operation", required = true) String operation) {
        
        try {
            if (!lock.tryLock(5, TimeUnit.SECONDS)) {
                return CallToolResult.builder()
                    .addTextContent("Error: Could not acquire lock within timeout")
                    .isError(true)
                    .build();
            }
            
            try {
                context.info("Lock acquired, executing operation");
                
                // Critical section
                Thread.sleep(1000); // Simulate work
                
                return CallToolResult.builder()
                    .addTextContent("Operation completed: " + operation)
                    .build();
                    
            } finally {
                lock.unlock();
                context.info("Lock released");
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return CallToolResult.builder()
                .addTextContent("Error: Operation interrupted")
                .isError(true)
                .build();
        }
    }
}

Timeout Handling

@McpTool(name = "long_running_task", description = "Task with timeout")
public CallToolResult longRunningTask(
        McpSyncRequestContext context,
        @McpToolParam(description = "Duration in seconds", required = true) int duration) {
    
    ExecutorService executor = Executors.newSingleThreadExecutor();
    
    try {
        Future<String> future = executor.submit(() -> {
            for (int i = 0; i < duration; i++) {
                if (Thread.currentThread().isInterrupted()) {
                    throw new InterruptedException("Task cancelled");
                }
                Thread.sleep(1000);
                context.progress((i + 1) * 100 / duration);
            }
            return "Task completed after " + duration + " seconds";
        });
        
        // Wait with timeout
        String result = future.get(30, TimeUnit.SECONDS);
        
        return CallToolResult.builder()
            .addTextContent(result)
            .build();
            
    } catch (TimeoutException e) {
        context.error("Task timed out after 30 seconds");
        executor.shutdownNow();
        return CallToolResult.builder()
            .addTextContent("Error: Task timed out")
            .isError(true)
            .build();
            
    } catch (Exception e) {
        context.error("Task failed: " + e.getMessage());
        return CallToolResult.builder()
            .addTextContent("Error: " + e.getMessage())
            .isError(true)
            .build();
            
    } finally {
        executor.shutdown();
    }
}

Stateless Mode Compatibility

Designing for Both Stateful and Stateless

@Component
public class CompatibleTools {
    
    // Works in both modes - uses transport context
    @McpTool(name = "simple_calc", description = "Simple calculation")
    public int simpleCalc(
            McpTransportContext context,
            @McpToolParam(description = "Value", required = true) int value) {
        
        // Can access transport metadata
        String requestId = context.getRequestId();
        
        return value * 2;
    }
    
    // Only works in stateful mode - uses full context
    @McpTool(name = "calc_with_progress", description = "Calculation with progress")
    public int calcWithProgress(
            McpSyncRequestContext context,
            @McpToolParam(description = "Value", required = true) int value) {
        
        context.info("Starting calculation");
        context.progress(50);
        int result = value * 2;
        context.progress(100);
        
        return result;
    }
    
    // Conditional feature usage
    @McpTool(name = "smart_calc", description = "Calculation with optional progress")
    public CallToolResult smartCalc(
            McpSyncRequestContext context,
            @McpToolParam(description = "Value", required = true) int value) {
        
        // Check if progress is available
        try {
            context.progress(50);
        } catch (Exception e) {
            // Progress not available in stateless mode, continue anyway
        }
        
        int result = value * 2;
        
        try {
            context.progress(100);
        } catch (Exception e) {
            // Ignore
        }
        
        return CallToolResult.builder()
            .addTextContent("Result: " + result)
            .build();
    }
}

Handling Binary Data

@McpTool(name = "process_image", description = "Process image data")
public CallToolResult processImage(
        @McpToolParam(description = "Base64 encoded image", required = true) String base64Image) {
    
    try {
        // Decode base64
        byte[] imageBytes = Base64.getDecoder().decode(base64Image);
        
        // Process image
        BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
        
        if (image == null) {
            return CallToolResult.builder()
                .addTextContent("Error: Invalid image format")
                .isError(true)
                .build();
        }
        
        // Get image info
        String info = String.format(
            "Image: %dx%d pixels, type: %s",
            image.getWidth(),
            image.getHeight(),
            image.getType()
        );
        
        // Return processed image
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(image, "PNG", baos);
        String processedBase64 = Base64.getEncoder().encodeToString(baos.toByteArray());
        
        return CallToolResult.builder()
            .addTextContent(info)
            .addImageContent(processedBase64, "image/png")
            .build();
            
    } catch (IllegalArgumentException e) {
        return CallToolResult.builder()
            .addTextContent("Error: Invalid base64 encoding")
            .isError(true)
            .build();
            
    } catch (IOException e) {
        return CallToolResult.builder()
            .addTextContent("Error: Cannot process image - " + e.getMessage())
            .isError(true)
            .build();
    }
}

Resource URI Pattern Matching

@Component
public class AdvancedResources {
    
    // Multiple path segments
    @McpResource(
        uri = "api://{service}/{version}/{endpoint}",
        name = "API Endpoint",
        description = "Access API endpoints")
    public ReadResourceResult getApiEndpoint(
            String service,
            String version,
            String endpoint) {
        
        String url = String.format(
            "https://%s.api.example.com/%s/%s",
            service, version, endpoint);
        
        String response = restTemplate.getForObject(url, String.class);
        
        return new ReadResourceResult(
            List.of(new TextResourceContents(
                String.format("api://%s/%s/%s", service, version, endpoint),
                "application/json",
                response
            ))
        );
    }
    
    // Query parameters in URI
    @McpResource(
        uri = "search://{query}",
        name = "Search Results",
        description = "Search with query")
    public ReadResourceResult search(String query) {
        // Parse query parameters from URI
        Map<String, String> params = parseQueryParams(query);
        
        String searchTerm = params.getOrDefault("q", "");
        int limit = Integer.parseInt(params.getOrDefault("limit", "10"));
        
        List<String> results = searchService.search(searchTerm, limit);
        
        return new ReadResourceResult(
            List.of(new TextResourceContents(
                "search://" + query,
                "application/json",
                new ObjectMapper().writeValueAsString(results)
            ))
        );
    }
}

Sampling with Retry Logic

@McpTool(name = "enhanced_query", description = "Query with LLM enhancement and retry")
public CallToolResult enhancedQuery(
        McpSyncRequestContext context,
        @McpToolParam(description = "Query", required = true) String query) {
    
    if (!context.sampleEnabled()) {
        return CallToolResult.builder()
            .addTextContent("Error: Sampling not available")
            .isError(true)
            .build();
    }
    
    int maxRetries = 3;
    CreateMessageResult llmResult = null;
    
    for (int attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            context.info("Attempt " + attempt + " to enhance query");
            
            llmResult = context.sample(builder -> builder
                .messages(List.of(new PromptMessage(
                    Role.USER,
                    new TextContent("Enhance this query: " + query)
                )))
                .maxTokens(100)
                .temperature(0.7)
            );
            
            break; // Success
            
        } catch (Exception e) {
            context.warn("Attempt " + attempt + " failed: " + e.getMessage());
            
            if (attempt == maxRetries) {
                return CallToolResult.builder()
                    .addTextContent("Error: Failed to enhance query after " + maxRetries + " attempts")
                    .isError(true)
                    .build();
            }
            
            // Wait before retry
            try {
                Thread.sleep(1000 * attempt);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
    
    String enhancedQuery = llmResult.content();
    return CallToolResult.builder()
        .addTextContent("Enhanced query: " + enhancedQuery)
        .build();
}

Next Steps

  • Real-World Scenarios - See complete implementations
  • Integration Patterns - Common patterns
  • Architecture Reference - Framework internals
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