Skip to content

Add pluggable exception handler for MCP tool method invocations#5772

Open
eocantu wants to merge 1 commit intospring-projects:mainfrom
eocantu:GH-4488
Open

Add pluggable exception handler for MCP tool method invocations#5772
eocantu wants to merge 1 commit intospring-projects:mainfrom
eocantu:GH-4488

Conversation

@eocantu
Copy link
Copy Markdown

@eocantu eocantu commented Apr 7, 2026

Introduces McpToolCallExceptionHandler, an interface for customizing how exceptions thrown during MCP tool method invocations are converted into CallToolResult error responses. The MCP annotation framework silently swallows all exceptions from tool method calls and returns a generic error result. There's no hook to intercept those exceptions to customize or control the error message format.

This feature should help with:

Summary of the changes:

  • McpToolCallExceptionHandler - New functional interface with a defaultHandler() factory method
  • AbstractMcpToolProvider.setExceptionHandler() to configure the handler at the provider level
  • SyncMcpAnnotationProviders / AsyncMcpAnnotationProviders utility overloads accepting a handler
  • Auto-configuration registers a @ConditionalOnMissingBean McpToolCallExceptionHandler bean for Spring Boot override support

Signed-off-by: Edgar Cantu <eocantu@users.noreply.github.com>
Comment on lines +86 to +93
Throwable cause = ex.getCause();
if (cause instanceof RuntimeException re) {
throw re;
}
if (cause instanceof Error error) {
throw error;
}
throw new RuntimeException(cause);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callMethod always re-wrapped exceptions in a RuntimeException("Error invoking method...). This destroyed the original exception type before it reached the toolCallExceptionClass matching logic, silently breaking the feature for any specific exception subclass.

Additionally, error messages used Java internals (the Java method name), rather than the MCP tool name the client actually knows.

This change here necessitated some updates to tests to match the update to the error string.

// IllegalArgumentException) and propagates with its original message
assertThatThrownBy(() -> callback.apply(exchange, request)).isInstanceOf(RuntimeException.class)
.hasMessageContaining("Error invoking method");
.hasMessageContaining("Runtime error: test");
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously callMethod always re-threw as new RuntimeException("Error invoking method: "). The original exception now propagates unchanged, so the assertion must match its actual message.

// propagates with the original cause accessible
assertThatThrownBy(() -> callback.apply(exchange, request)).isInstanceOf(RuntimeException.class)
.hasMessageContaining("Error invoking method")
.hasMessageContaining("Business error: test")
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root change: checked exceptions are now re-wrapped as new RuntimeException(cause) (no custom message), so the wrapper's message becomes cause.toString() which includes "Business error: test".

@eocantu
Copy link
Copy Markdown
Author

eocantu commented Apr 16, 2026

Hi @ericbottard ! This error handler would be really helpful to us. Do you think we could get some eyes on this PR? Any thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant