spring-projects/spring-ai · error · UndeclaredThrowableException
UndeclaredThrowableException
Error message
UndeclaredThrowableException
What it means
AbstractMcpToolMethodCallback.callMethod rethrows the checked exception raised inside a tool method as UndeclaredThrowableException when it is neither a RuntimeException nor an Error. Because the callback's invoke path does not declare checked exceptions, any checked exception thrown by the user's tool method is wrapped; the real cause is available via getCause().
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/tool/AbstractMcpToolMethodCallback.java:90
* @throws RuntimeException if there's an error invoking the method
*/
protected Object callMethod(Object[] methodArguments) {
this.toolMethod.setAccessible(true);
try {
return this.toolMethod.invoke(this.toolObject, methodArguments);
}
catch (IllegalAccessException ex) {
throw new RuntimeException("Failed to access tool method", ex);
}
catch (InvocationTargetException ex) {
Throwable cause = ex.getCause();
if (cause instanceof RuntimeException re) {
throw re;
}
if (cause instanceof Error err) {
throw err;
}
throw new UndeclaredThrowableException(cause);
}
}
/**
* Builds the method arguments from the context, tool input arguments, and optionally
* the full request.
* @param exchangeOrContext The exchange or context object (e.g.,
* McpSyncServerExchange, McpAsyncServerExchange, or McpTransportContext)
* @param toolInputArguments The input arguments from the tool request
* @param request The full CallToolRequest (optional, can be null)
* @return An array of method arguments
*/
protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object> toolInputArguments,
CallToolRequest request) {
return Stream.of(this.toolMethod.getParameters()).map(parameter -> {
if (McpSyncRequestContext.class.isAssignableFrom(parameter.getType())View on GitHub (pinned to 98a7beda4f)
Solutions
- Unwrap with exception.getCause() in the calling error handler to see the real checked exception
- Change the tool method to wrap checked exceptions in a RuntimeException (or a custom unchecked type)
- Catch the checked exception inside the tool method and return an error CallToolResult instead of throwing
- Catch UndeclaredThrowableException at the dispatch boundary and inspect its cause
Example fix
// before
String readFile(String p) throws IOException { return Files.readString(Path.of(p)); }
// after
String readFile(String p) {
try { return Files.readString(Path.of(p)); }
catch (IOException e) { throw new UncheckedIOException(e); }
} Defensive patterns
Strategy: try-catch
Try / catch
try {
result = callback.call(request);
} catch (UndeclaredThrowableException e) {
Throwable real = e.getCause();
log.error("Tool method threw checked exception", real);
return errorCallToolResult(real);
} Prevention
- Wrap checked exceptions in unchecked types inside tool methods
- Catch checked exceptions in the tool and convert to error CallToolResult
- Never let raw checked exceptions escape tool method signatures
When it happens
Trigger: A tool method declares and throws a checked exception (e.g. IOException, SQLException) and the underlying transport call hits that path.
Common situations: Tool methods calling file/DB/network APIs with checked exceptions; proxy-based invocation (proxies only propagate unchecked exceptions) making the wrap mandatory.
Related errors
- Failed to access tool method
- Neither createIndex() nor ensureIndex() method found on Inde
- Failed to invoke ensureIndex() method
- Failed to invoke createIndex() method
- Required no-arg constructor not found in
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/aa9570a3deaac49f.
Report an issue: GitHub.