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

real-world-scenarios.mddocs/examples/

Real-World Scenarios

Production-ready examples of Spring AI MCP Server implementations.

Scenario 1: Database Query Server

A server that provides database query capabilities with proper error handling and progress tracking.

@Component
public class DatabaseTools {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @McpTool(
        name = "query_users",
        description = "Query users from database with filters")
    public CallToolResult queryUsers(
            McpSyncRequestContext context,
            @McpToolParam(description = "Status filter (active/inactive)", required = false) String status,
            @McpToolParam(description = "Maximum results", required = false) Integer limit) {
        
        try {
            context.info("Querying users with status: " + (status != null ? status : "all"));
            context.progress(0);
            
            String sql = "SELECT * FROM users";
            List<Object> params = new ArrayList<>();
            
            if (status != null) {
                sql += " WHERE status = ?";
                params.add(status);
            }
            
            if (limit != null && limit > 0) {
                sql += " LIMIT ?";
                params.add(limit);
            }
            
            context.progress(50);
            
            List<Map<String, Object>> users = jdbcTemplate.queryForList(sql, params.toArray());
            
            context.progress(100);
            context.info("Found " + users.size() + " users");
            
            String result = new ObjectMapper().writeValueAsString(users);
            return CallToolResult.builder()
                .addTextContent(result)
                .build();
                
        } catch (Exception e) {
            context.error("Query failed: " + e.getMessage());
            return CallToolResult.builder()
                .addTextContent("Error: " + e.getMessage())
                .isError(true)
                .build();
        }
    }
    
    @McpResource(
        uri = "db://user/{id}",
        name = "User Details",
        description = "Get detailed user information",
        mimeType = "application/json")
    public ReadResourceResult getUserDetails(
            McpSyncRequestContext context,
            String id) {
        
        try {
            Map<String, Object> user = jdbcTemplate.queryForMap(
                "SELECT * FROM users WHERE id = ?", id);
            
            String json = new ObjectMapper().writeValueAsString(user);
            
            return new ReadResourceResult(
                List.of(new TextResourceContents(
                    "db://user/" + id,
                    "application/json",
                    json
                ))
            );
            
        } catch (EmptyResultDataAccessException e) {
            context.warn("User not found: " + id);
            return new ReadResourceResult(
                List.of(new TextResourceContents(
                    "db://user/" + id,
                    "application/json",
                    "{\"error\": \"User not found\"}"
                ))
            );
        }
    }
}

Scenario 2: File System Server

Server for file operations with proper validation and security.

@Component
public class FileSystemTools {
    
    @Value("${app.data-directory}")
    private String dataDirectory;
    
    @McpTool(
        name = "list_files",
        description = "List files in a directory")
    public CallToolResult listFiles(
            McpSyncRequestContext context,
            @McpToolParam(description = "Directory path", required = true) String path) {
        
        try {
            // Security: Validate path is within allowed directory
            Path fullPath = Paths.get(dataDirectory, path).normalize();
            if (!fullPath.startsWith(dataDirectory)) {
                return CallToolResult.builder()
                    .addTextContent("Error: Access denied - path outside allowed directory")
                    .isError(true)
                    .build();
            }
            
            context.info("Listing files in: " + path);
            
            List<String> files = Files.list(fullPath)
                .map(p -> p.getFileName().toString())
                .collect(Collectors.toList());
            
            context.info("Found " + files.size() + " files");
            
            return CallToolResult.builder()
                .addTextContent(String.join("\n", files))
                .build();
                
        } catch (IOException e) {
            context.error("Failed to list files: " + e.getMessage());
            return CallToolResult.builder()
                .addTextContent("Error: " + e.getMessage())
                .isError(true)
                .build();
        }
    }
    
    @McpResource(
        uri = "file://{path}",
        name = "File Content",
        description = "Read file content")
    public ReadResourceResult readFile(
            McpSyncRequestContext context,
            String path) {
        
        try {
            Path fullPath = Paths.get(dataDirectory, path).normalize();
            
            // Security check
            if (!fullPath.startsWith(dataDirectory)) {
                context.error("Access denied: " + path);
                return new ReadResourceResult(
                    List.of(new TextResourceContents(
                        "file://" + path,
                        "text/plain",
                        "Error: Access denied"
                    ))
                );
            }
            
            String content = Files.readString(fullPath);
            String mimeType = Files.probeContentType(fullPath);
            
            return new ReadResourceResult(
                List.of(new TextResourceContents(
                    "file://" + path,
                    mimeType != null ? mimeType : "text/plain",
                    content
                ))
            );
            
        } catch (IOException e) {
            context.error("Cannot read file: " + e.getMessage());
            return new ReadResourceResult(
                List.of(new TextResourceContents(
                    "file://" + path,
                    "text/plain",
                    "Error: " + e.getMessage()
                ))
            );
        }
    }
}

Scenario 3: API Integration Server

Server that integrates with external APIs with retry logic and caching.

@Component
public class WeatherTools {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Autowired
    private CacheManager cacheManager;
    
    @Value("${weather.api.key}")
    private String apiKey;
    
    @McpTool(
        name = "get_weather",
        description = "Get current weather for a location")
    public CallToolResult getWeather(
            McpSyncRequestContext context,
            @McpToolParam(description = "City name", required = true) String city,
            @McpToolParam(description = "Country code", required = false) String country) {
        
        try {
            String location = country != null ? city + "," + country : city;
            context.info("Fetching weather for: " + location);
            
            // Check cache first
            Cache cache = cacheManager.getCache("weather");
            String cachedData = cache.get(location, String.class);
            
            if (cachedData != null) {
                context.info("Returning cached data");
                return CallToolResult.builder()
                    .addTextContent(cachedData)
                    .build();
            }
            
            context.progress(50);
            
            // Fetch from API with retry
            String url = String.format(
                "https://api.weatherapi.com/v1/current.json?key=%s&q=%s",
                apiKey, location);
            
            String response = restTemplate.getForObject(url, String.class);
            
            // Cache the result
            cache.put(location, response);
            
            context.progress(100);
            context.info("Weather data fetched successfully");
            
            return CallToolResult.builder()
                .addTextContent(response)
                .build();
                
        } catch (RestClientException e) {
            context.error("API request failed: " + e.getMessage());
            return CallToolResult.builder()
                .addTextContent("Error: Cannot fetch weather data - " + e.getMessage())
                .isError(true)
                .build();
        }
    }
}

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofSeconds(5))
            .setReadTimeout(Duration.ofSeconds(10))
            .build();
    }
}

Scenario 4: Multi-Step Workflow Server

Server that orchestrates complex multi-step workflows with elicitation.

@Component
public class WorkflowTools {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private EmailService emailService;
    
    public record UserRegistration(
        String username,
        String email,
        String fullName,
        boolean acceptTerms
    ) {}
    
    @McpTool(
        name = "register_user",
        description = "Register a new user with interactive confirmation")
    public CallToolResult registerUser(McpSyncRequestContext context) {
        
        try {
            context.info("Starting user registration workflow");
            context.progress(0);
            
            // Step 1: Elicit user information
            if (!context.elicitEnabled()) {
                return CallToolResult.builder()
                    .addTextContent("Error: Interactive registration not available")
                    .isError(true)
                    .build();
            }
            
            context.info("Requesting user information");
            StructuredElicitResult<UserRegistration> result = 
                context.elicit(UserRegistration.class);
            
            if (result.action() != ElicitResult.Action.ACCEPT) {
                context.warn("User cancelled registration");
                return CallToolResult.builder()
                    .addTextContent("Registration cancelled by user")
                    .build();
            }
            
            UserRegistration registration = result.data();
            context.progress(33);
            
            // Step 2: Validate data
            context.info("Validating user data");
            if (!registration.acceptTerms()) {
                return CallToolResult.builder()
                    .addTextContent("Error: Terms must be accepted")
                    .isError(true)
                    .build();
            }
            
            if (userService.existsByUsername(registration.username())) {
                return CallToolResult.builder()
                    .addTextContent("Error: Username already exists")
                    .isError(true)
                    .build();
            }
            
            context.progress(66);
            
            // Step 3: Create user
            context.info("Creating user account");
            User user = userService.createUser(
                registration.username(),
                registration.email(),
                registration.fullName()
            );
            
            // Step 4: Send confirmation email
            context.info("Sending confirmation email");
            emailService.sendWelcomeEmail(user.getEmail(), user.getUsername());
            
            context.progress(100);
            context.info("User registered successfully");
            
            return CallToolResult.builder()
                .addTextContent("User registered successfully: " + user.getId())
                .build();
                
        } catch (Exception e) {
            context.error("Registration failed: " + e.getMessage());
            return CallToolResult.builder()
                .addTextContent("Error: " + e.getMessage())
                .isError(true)
                .build();
        }
    }
}

Scenario 5: Async Reactive Server

High-performance server using reactive programming.

@Component
public class ReactiveTools {
    
    @Autowired
    private WebClient webClient;
    
    @Autowired
    private R2dbcEntityTemplate template;
    
    @McpTool(
        name = "fetch_multiple_apis",
        description = "Fetch data from multiple APIs in parallel")
    public Mono<CallToolResult> fetchMultipleApis(
            McpAsyncRequestContext context,
            @McpToolParam(description = "API URLs (comma-separated)", required = true) String urls) {
        
        return context.info("Starting parallel API fetches")
            .then(Mono.defer(() -> {
                List<String> urlList = Arrays.asList(urls.split(","));
                
                // Fetch all APIs in parallel
                List<Mono<String>> requests = urlList.stream()
                    .map(url -> webClient.get()
                        .uri(url.trim())
                        .retrieve()
                        .bodyToMono(String.class)
                        .onErrorResume(e -> Mono.just("Error: " + e.getMessage())))
                    .collect(Collectors.toList());
                
                return Flux.merge(requests)
                    .collectList()
                    .flatMap(results -> {
                        String combined = String.join("\n---\n", results);
                        return context.info("All APIs fetched")
                            .then(Mono.just(CallToolResult.builder()
                                .addTextContent(combined)
                                .build()));
                    });
            }))
            .onErrorResume(e -> 
                context.error("Fetch failed: " + e.getMessage())
                    .then(Mono.just(CallToolResult.builder()
                        .addTextContent("Error: " + e.getMessage())
                        .isError(true)
                        .build()))
            );
    }
    
    @McpTool(
        name = "stream_database_results",
        description = "Stream large result sets efficiently")
    public Flux<String> streamDatabaseResults(
            McpAsyncRequestContext context,
            @McpToolParam(description = "Query", required = true) String query) {
        
        return context.info("Starting database stream")
            .thenMany(template.getDatabaseClient()
                .sql(query)
                .fetch()
                .all()
                .map(row -> new ObjectMapper().writeValueAsString(row))
                .onErrorResume(e -> {
                    context.error("Stream error: " + e.getMessage()).subscribe();
                    return Flux.just("Error: " + e.getMessage());
                }));
    }
}

Configuration for Production

# Server configuration
spring.ai.mcp.server.name=production-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.protocol=SSE
spring.ai.mcp.server.request-timeout=60s

# Connection pooling
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5

# Caching
spring.cache.type=caffeine
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=600s

# Logging
logging.level.org.springframework.ai.mcp=INFO
logging.level.com.myapp=DEBUG

# Security
app.data-directory=/var/data/mcp-server
weather.api.key=${WEATHER_API_KEY}

Next Steps

  • Edge Cases - Handle complex scenarios
  • Integration Patterns - Common integration approaches
  • Configuration Guide - Optimize configuration
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