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

error-handling.mddocs/guides/

Error Handling Guide

Best practices for handling errors in MCP servers.

Tool Error Handling

Using CallToolResult

@McpTool(name = "divide", description = "Divide two numbers")
public CallToolResult divide(
        @McpToolParam(description = "Numerator", required = true) double a,
        @McpToolParam(description = "Denominator", required = true) double b) {
    
    if (b == 0) {
        return CallToolResult.builder()
            .addTextContent("Error: Division by zero is not allowed")
            .isError(true)
            .build();
    }
    
    double result = a / b;
    return CallToolResult.builder()
        .addTextContent("Result: " + result)
        .build();
}

Type Safety

@McpTool(name = "process", description = "Process user input")
public CallToolResult process(CallToolRequest request) {
    try {
        Map<String, Object> args = request.arguments();
        
        // Safe type extraction
        Object rawAge = args.get("age");
        int age = (rawAge instanceof Number) 
            ? ((Number) rawAge).intValue() 
            : Integer.parseInt(String.valueOf(rawAge));
        
        return CallToolResult.builder()
            .addTextContent("Processed age: " + age)
            .build();
            
    } catch (NumberFormatException | ClassCastException e) {
        return CallToolResult.builder()
            .addTextContent("Error: Invalid age format - " + e.getMessage())
            .isError(true)
            .build();
    }
}

Validation Errors

@McpTool(name = "createUser", description = "Create a new user")
public CallToolResult createUser(
        @McpToolParam(description = "Username", required = true) String username,
        @McpToolParam(description = "Email", required = true) String email) {
    
    // Validate username
    if (username == null || username.trim().isEmpty()) {
        return CallToolResult.builder()
            .addTextContent("Error: Username cannot be empty")
            .isError(true)
            .build();
    }
    
    // Validate email format
    if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
        return CallToolResult.builder()
            .addTextContent("Error: Invalid email format")
            .isError(true)
            .build();
    }
    
    // Create user
    User user = userService.create(username, email);
    return CallToolResult.builder()
        .addTextContent("User created: " + user.getId())
        .build();
}

Exception Handling

Try-Catch Pattern

@McpTool(name = "fetchData", description = "Fetch data from external API")
public CallToolResult fetchData(
        @McpToolParam(description = "URL", required = true) String url) {
    
    try {
        String data = httpClient.get(url);
        return CallToolResult.builder()
            .addTextContent(data)
            .build();
            
    } catch (IOException e) {
        return CallToolResult.builder()
            .addTextContent("Error: Network error - " + e.getMessage())
            .isError(true)
            .build();
            
    } catch (Exception e) {
        return CallToolResult.builder()
            .addTextContent("Error: Unexpected error - " + e.getMessage())
            .isError(true)
            .build();
    }
}

Logging Errors

@McpTool(name = "processFile", description = "Process a file")
public CallToolResult processFile(
        McpSyncRequestContext context,
        @McpToolParam(description = "File path", required = true) String path) {
    
    try {
        context.info("Processing file: " + path);
        String result = fileProcessor.process(path);
        context.info("File processed successfully");
        
        return CallToolResult.builder()
            .addTextContent(result)
            .build();
            
    } catch (FileNotFoundException e) {
        context.error("File not found: " + path);
        return CallToolResult.builder()
            .addTextContent("Error: File not found")
            .isError(true)
            .build();
            
    } catch (IOException e) {
        context.error("IO error processing file: " + e.getMessage());
        return CallToolResult.builder()
            .addTextContent("Error: Cannot read file")
            .isError(true)
            .build();
    }
}

Resource Error Handling

@McpResource(
    uri = "file://{path}",
    name = "File Content",
    description = "Read file content")
public ReadResourceResult readFile(
        McpSyncRequestContext context,
        String path) {
    
    try {
        String content = Files.readString(Path.of(path));
        
        return new ReadResourceResult(
            List.of(new TextResourceContents(
                "file://" + path,
                "text/plain",
                content
            ))
        );
        
    } catch (IOException e) {
        context.error("Cannot read file: " + e.getMessage());
        
        // Return error as resource content
        return new ReadResourceResult(
            List.of(new TextResourceContents(
                "file://" + path,
                "text/plain",
                "Error: Cannot read file - " + e.getMessage()
            ))
        );
    }
}

Async Error Handling

@McpTool(name = "asyncProcess", description = "Process data asynchronously")
public Mono<CallToolResult> asyncProcess(
        McpAsyncRequestContext context,
        @McpToolParam(description = "Data", required = true) String data) {
    
    return context.info("Starting async processing")
        .then(processDataAsync(data))
        .map(result -> CallToolResult.builder()
            .addTextContent("Result: " + result)
            .build())
        .onErrorResume(e -> {
            return context.error("Processing failed: " + e.getMessage())
                .then(Mono.just(CallToolResult.builder()
                    .addTextContent("Error: " + e.getMessage())
                    .isError(true)
                    .build()));
        });
}

Common Error Patterns

Null Checks

@McpTool(name = "getData", description = "Get data by ID")
public CallToolResult getData(
        @McpToolParam(description = "ID", required = true) String id) {
    
    Data data = dataService.findById(id);
    
    if (data == null) {
        return CallToolResult.builder()
            .addTextContent("Error: Data not found for ID: " + id)
            .isError(true)
            .build();
    }
    
    return CallToolResult.builder()
        .addTextContent(data.toString())
        .build();
}

Range Validation

@McpTool(name = "setVolume", description = "Set volume level")
public CallToolResult setVolume(
        @McpToolParam(description = "Volume (0-100)", required = true) int volume) {
    
    if (volume < 0 || volume > 100) {
        return CallToolResult.builder()
            .addTextContent("Error: Volume must be between 0 and 100")
            .isError(true)
            .build();
    }
    
    audioService.setVolume(volume);
    return CallToolResult.builder()
        .addTextContent("Volume set to " + volume)
        .build();
}

State Validation

@McpTool(name = "stopService", description = "Stop the service")
public CallToolResult stopService() {
    
    if (!service.isRunning()) {
        return CallToolResult.builder()
            .addTextContent("Error: Service is not running")
            .isError(true)
            .build();
    }
    
    service.stop();
    return CallToolResult.builder()
        .addTextContent("Service stopped successfully")
        .build();
}

Error Messages Best Practices

✅ Good Error Messages

  • Specific: "Error: Division by zero is not allowed"
  • Actionable: "Error: Invalid email format. Expected: user@domain.com"
  • Contextual: "Error: File not found at path: /data/config.json"

❌ Poor Error Messages

  • Vague: "Error occurred"
  • Technical: "NullPointerException at line 42"
  • No context: "Invalid input"

Graceful Degradation

@McpTool(name = "enrichData", description = "Enrich data with external info")
public CallToolResult enrichData(
        McpSyncRequestContext context,
        @McpToolParam(description = "Data", required = true) String data) {
    
    String enriched = data;
    
    // Try to enrich, but don't fail if enrichment fails
    try {
        String additionalInfo = externalService.getInfo(data);
        enriched = data + " | " + additionalInfo;
        context.info("Data enriched successfully");
    } catch (Exception e) {
        context.warn("Enrichment failed, returning original data: " + e.getMessage());
        // Continue with original data
    }
    
    return CallToolResult.builder()
        .addTextContent(enriched)
        .build();
}

Testing Error Scenarios

@SpringBootTest
class ErrorHandlingTests {
    
    @Autowired
    private MyTools myTools;
    
    @Test
    void testDivisionByZero() {
        CallToolResult result = myTools.divide(10, 0);
        
        assertTrue(result.isError());
        assertTrue(result.getContent().get(0).toString().contains("Division by zero"));
    }
    
    @Test
    void testInvalidInput() {
        CallToolRequest request = new CallToolRequest(
            "process",
            Map.of("age", "invalid"),
            null
        );
        
        CallToolResult result = myTools.process(request);
        
        assertTrue(result.isError());
        assertTrue(result.getContent().get(0).toString().contains("Invalid age format"));
    }
}

Next Steps

  • Real-World Scenarios - See error handling in production
  • Edge Cases - Handle complex error scenarios
  • Request Context Reference - Learn about logging methods
tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-server@1.1.0

docs

index.md

tile.json