Error Handling
This guide covers error handling strategies for MCP server features, including how to handle business logic errors, protocol errors, and automatic error wrapping.
Overview
The Quarkus MCP Server extension provides several mechanisms for handling errors in your server features:
-
Business Logic Errors: Using
ToolCallExceptionfor tool-specific errors -
Protocol Errors: Using
McpExceptionfor JSON-RPC protocol errors -
Automatic Error Wrapping: Using
@WrapBusinessErrorto automatically convert exceptions -
Validation Errors: Integration with Hibernate Validator (see Hibernate Validator Integration)
Business Logic Errors
ToolCallException
The io.quarkiverse.mcp.server.ToolCallException is specifically designed for business logic errors in @Tool methods.
When a tool throws this exception, it’s automatically converted to a failed ToolResponse with the exception message included in the response content.
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolCallException;
public class MyTools {
@Tool(description = "Calculate the price with discount")
String calculatePrice(int price, int discount) {
if (discount < 0 || discount > 100) {
throw new ToolCallException("Discount must be between 0 and 100"); (1)
}
int finalPrice = price - (price * discount / 100);
return "Final price: $" + finalPrice;
}
}
| 1 | The exception message becomes the text content of the failed tool response. |
When to use ToolCallException:
-
Invalid input parameters (validation failures)
-
Business rule violations
-
Expected error conditions that the client should handle gracefully
-
Any situation where the tool cannot complete successfully, but it’s not a protocol error
Response Format
When a ToolCallException is thrown, the response looks like:
{
"isError": true,
"content": [
{
"type": "text",
"text": "Discount must be between 0 and 100"
}
]
}
The client receives a failed tool response (not a JSON-RPC error), allowing them to handle it as a business logic failure.
Protocol Errors
McpException
The io.quarkiverse.mcp.server.McpException is used for protocol-level errors that should result in JSON-RPC error responses.
Unlike ToolCallException, which results in a failed tool response, McpException results in a JSON-RPC error message.
import io.quarkiverse.mcp.server.McpException;
import io.quarkiverse.mcp.server.JsonRpcErrorCodes;
import io.quarkiverse.mcp.server.Tool;
public class MyTools {
@Tool(description = "Access a resource")
String accessResource(String resourceId) {
if (!resourceExists(resourceId)) {
throw new McpException(
"Resource not found: " + resourceId,
JsonRpcErrorCodes.RESOURCE_NOT_FOUND); (1)
}
return loadResource(resourceId);
}
private boolean resourceExists(String id) {
// Implementation
return false;
}
private String loadResource(String id) {
return "Resource content";
}
}
| 1 | Use standard JSON-RPC error codes for protocol-level errors. |
JSON-RPC Error Codes
The extension provides standard JSON-RPC error codes via the io.quarkiverse.mcp.server.JsonRpcErrorCodes class:
| Code | Constant | Description |
|---|---|---|
|
|
Invalid JSON was received by the server |
|
|
The JSON sent is not a valid request object |
|
|
The method does not exist or is not available |
|
|
Invalid method parameter(s) |
|
|
Internal JSON-RPC error |
|
|
MCP-specific: requested resource not found |
|
|
MCP-specific: security/authorization error |
When to use McpException:
-
Resource not found scenarios
-
Protocol-level violations
-
Security/authorization failures (for HTTP transports)
-
Internal server errors that aren’t business logic failures
Automatic Error Wrapping
@WrapBusinessError Annotation
The @io.quarkiverse.mcp.server.WrapBusinessError annotation automatically wraps exceptions in ToolCallException, eliminating the need to manually throw ToolCallException for every error condition.
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.WrapBusinessError;
public class MyTools {
@WrapBusinessError (1)
@Tool(description = "Process an order")
String processOrder(String orderId) {
if (orderId == null || orderId.isEmpty()) {
throw new IllegalArgumentException("Order ID cannot be empty"); (2)
}
// Process order...
return "Order processed";
}
}
| 1 | All exceptions from this method will be wrapped in ToolCallException. |
| 2 | This IllegalArgumentException will be automatically wrapped and converted to a failed tool response. |
The client receives a failed tool response with the message: "java.lang.IllegalArgumentException: Order ID cannot be empty"
Selective Wrapping
You can specify which exception types to wrap:
@WrapBusinessError(NullPointerException.class) (1)
@Tool
String processData(String data) {
if (data == null) {
throw new NullPointerException("Data cannot be null"); (2)
}
throw new IllegalStateException("Invalid state"); (3)
}
| 1 | Only wrap NullPointerException and its subclasses. |
| 2 | This exception is wrapped and becomes a failed tool response. |
| 3 | This exception is not wrapped and propagates as-is (becomes a protocol error). |
Excluding Specific Exceptions
Use the unless parameter to exclude certain exception types from wrapping:
import io.quarkiverse.mcp.server.McpException;
@WrapBusinessError(unless = McpException.class) (1)
@Tool
String complexOperation() {
// Business logic error - will be wrapped
if (someCondition) {
throw new IllegalArgumentException("Business error");
}
// Protocol error - will NOT be wrapped
if (securityViolation) {
throw new McpException("Unauthorized", JsonRpcErrorCodes.SECURITY_ERROR); (2)
}
return "Success";
}
| 1 | Wrap all exceptions except McpException and its subclasses. |
| 2 | This protocol error bypasses the wrapper and becomes a JSON-RPC error. |
Reactive Error Handling
Error handling works seamlessly with reactive return types:
import io.smallrye.mutiny.Uni;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolCallException;
import io.quarkiverse.mcp.server.WrapBusinessError;
public class MyTools {
@Tool
Uni<String> fetchData(String url) {
return httpClient.get(url)
.onFailure().transform(e ->
new ToolCallException("Failed to fetch data: " + e.getMessage())); (1)
}
@WrapBusinessError(unless = ToolCallException.class)
@Tool
Uni<String> processAsync(String input) {
return Uni.createFrom().item(() -> {
if (input.isEmpty()) {
throw new IllegalArgumentException("Input cannot be empty"); (2)
}
return process(input);
});
}
private String process(String input) {
return input.toUpperCase();
}
}
| 1 | Manually transform failures to ToolCallException. |
| 2 | Exceptions from reactive pipelines are also automatically wrapped. |
Class-Level Error Wrapping
Apply @WrapBusinessError at the class level to cover all tool methods:
@WrapBusinessError (1)
public class MyTools {
@Tool
String tool1() {
throw new RuntimeException("Error in tool1"); (2)
}
@Tool
String tool2() {
throw new IllegalStateException("Error in tool2"); (2)
}
@WrapBusinessError(unless = Exception.class) (3)
@Tool
String tool3() {
throw new RuntimeException("Not wrapped");
}
}
| 1 | All tool methods in this class will have errors wrapped by default. |
| 2 | Both exceptions are automatically wrapped. |
| 3 | Method-level annotation overrides the class-level annotation. |