fix: Skip structured output validation for error tool results
This ensures that when a tool handler returns an error result, the structured output schema validation is skipped, preventing validation failures on error responses that don't conform to the expected output schema. - Add validation bypass when CallToolResult.isError() is true in async/stateless servers - Fix async tool handler chaining to properly use then() instead of block() - Add comprehensive tests for structured output with in-handler errors - Improve error handling to use proper JSON-RPC error codes for unknown tools - Add findRootCause utility method for better error diagnostics - Increase test timeouts for stability in StdioMcp client tests. These tests use npx to download and run the MCP "everything" server locally. The first test execution will download the everything server scripts and cache them locally, which can take more than 15 seconds. Subsequent test runs will use the cached version and execute faster. Resolves #538 Related to #422 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
d8cc189341
commit
c7736b6ec6
3 changed files with 353 additions and 80 deletions
|
|
@ -29,10 +29,12 @@ import io.modelcontextprotocol.spec.McpClientSession;
|
|||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
||||
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||
import io.modelcontextprotocol.spec.McpServerSession;
|
||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||
|
|
@ -376,6 +378,11 @@ public class McpAsyncServer {
|
|||
|
||||
return this.delegateCallToolResult.apply(exchange, request).map(result -> {
|
||||
|
||||
if (Boolean.TRUE.equals(result.isError())) {
|
||||
// If the tool call resulted in an error, skip further validation
|
||||
return result;
|
||||
}
|
||||
|
||||
if (outputSchema == null) {
|
||||
if (result.structuredContent() != null) {
|
||||
logger.warn(
|
||||
|
|
@ -507,11 +514,11 @@ public class McpAsyncServer {
|
|||
.findAny();
|
||||
|
||||
if (toolSpecification.isEmpty()) {
|
||||
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
|
||||
return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS,
|
||||
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name())));
|
||||
}
|
||||
|
||||
return toolSpecification.map(tool -> Mono.defer(() -> tool.callHandler().apply(exchange, callToolRequest)))
|
||||
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
|
||||
return toolSpecification.get().callHandler().apply(exchange, callToolRequest);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import io.modelcontextprotocol.spec.JsonSchemaValidator;
|
|||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
||||
import io.modelcontextprotocol.spec.McpSchema.TextContent;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
|
|
@ -249,6 +251,11 @@ public class McpStatelessAsyncServer {
|
|||
|
||||
return this.delegateHandler.apply(transportContext, request).map(result -> {
|
||||
|
||||
if (Boolean.TRUE.equals(result.isError())) {
|
||||
// If the tool call resulted in an error, skip further validation
|
||||
return result;
|
||||
}
|
||||
|
||||
if (outputSchema == null) {
|
||||
if (result.structuredContent() != null) {
|
||||
logger.warn(
|
||||
|
|
@ -375,11 +382,11 @@ public class McpStatelessAsyncServer {
|
|||
.findAny();
|
||||
|
||||
if (toolSpecification.isEmpty()) {
|
||||
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
|
||||
return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS,
|
||||
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name())));
|
||||
}
|
||||
|
||||
return toolSpecification.map(tool -> tool.callHandler().apply(ctx, callToolRequest))
|
||||
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
|
||||
return toolSpecification.get().callHandler().apply(ctx, callToolRequest);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
package io.modelcontextprotocol.util;
|
||||
|
||||
import reactor.util.annotation.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.util.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Miscellaneous utility methods.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue