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
Best practices for handling errors in MCP servers.
@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();
}@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();
}
}@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();
}@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();
}
}@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();
}
}@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()
))
);
}
}@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()));
});
}@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();
}@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();
}@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();
}@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();
}@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"));
}
}