From d1f62a508e149fe92427288102c58ad933e9fd1f Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 11 Jun 2026 09:05:02 +0200 Subject: [PATCH 1/3] Clarify tool validation error messages - DefaultJsonSchemaValidator now emits a neutralmessage; - Call sites in ToolInputValidator and the server output handlers prepend their own context prefix, - Integration test assertions strengthened accordingly. Closes #986 Signed-off-by: Christian Tzolov --- .../java/io/modelcontextprotocol/server/McpAsyncServer.java | 5 +++-- .../modelcontextprotocol/server/McpStatelessAsyncServer.java | 5 +++-- .../io/modelcontextprotocol/util/ToolInputValidator.java | 5 +++-- .../json/schema/jackson2/DefaultJsonSchemaValidator.java | 3 +-- .../json/jackson2/DefaultJsonSchemaValidatorTests.java | 2 +- .../json/schema/jackson3/DefaultJsonSchemaValidator.java | 3 +-- .../json/DefaultJsonSchemaValidatorTests.java | 2 +- .../server/ToolInputValidationIntegrationTests.java | 3 +++ 8 files changed, 16 insertions(+), 12 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index d0476e5f2..4d3772525 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -433,9 +433,10 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent()); if (!validation.valid()) { - logger.warn("Tool call result validation failed: {}", validation.errorMessage()); + String message = "Tool output validation failed: " + validation.errorMessage(); + logger.warn(message); return CallToolResult.builder() - .content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build())) + .content(List.of(McpSchema.TextContent.builder(message).build())) .isError(true) .build(); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index 48b17ed2a..d99a4a79b 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -293,9 +293,10 @@ public Mono apply(McpTransportContext transportContext, McpSchem var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent()); if (!validation.valid()) { - logger.warn("Tool call result validation failed: {}", validation.errorMessage()); + String message = "Tool output validation failed: " + validation.errorMessage(); + logger.warn(message); return CallToolResult.builder() - .content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build())) + .content(List.of(McpSchema.TextContent.builder(message).build())) .isError(true) .build(); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java index 17a313323..2233a9e5e 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java @@ -42,9 +42,10 @@ public static CallToolResult validate(McpSchema.Tool tool, Map a Map args = arguments != null ? arguments : Map.of(); var validation = validator.validate(tool.inputSchema(), args); if (!validation.valid()) { - logger.warn("Tool '{}' input validation failed: {}", tool.name(), validation.errorMessage()); + String message = "Tool input validation failed: " + validation.errorMessage(); + logger.warn("Tool '{}' {}", tool.name(), message); return CallToolResult.builder() - .content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build())) + .content(List.of(McpSchema.TextContent.builder(message).build())) .isError(true) .build(); } diff --git a/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson2/DefaultJsonSchemaValidator.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson2/DefaultJsonSchemaValidator.java index d632b2d16..09bf5b5b6 100644 --- a/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson2/DefaultJsonSchemaValidator.java +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson2/DefaultJsonSchemaValidator.java @@ -76,8 +76,7 @@ public ValidationResponse validate(Map schema, Object structured // Check if validation passed if (!validationResult.isEmpty()) { return ValidationResponse - .asInvalid("Validation failed: structuredContent does not match tool outputSchema. " - + "Validation errors: " + validationResult); + .asInvalid("Validation failed: JSON schema validation errors: " + validationResult); } return ValidationResponse.asValid(jsonStructuredOutput.toString()); diff --git a/mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/jackson2/DefaultJsonSchemaValidatorTests.java b/mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/jackson2/DefaultJsonSchemaValidatorTests.java index 3cf59aa3c..3707c0f7c 100644 --- a/mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/jackson2/DefaultJsonSchemaValidatorTests.java +++ b/mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/jackson2/DefaultJsonSchemaValidatorTests.java @@ -308,7 +308,7 @@ void testValidateWithInvalidTypeSchema() { assertFalse(response.valid()); assertNotNull(response.errorMessage()); assertTrue(response.errorMessage().contains("Validation failed")); - assertTrue(response.errorMessage().contains("structuredContent does not match tool outputSchema")); + assertTrue(response.errorMessage().contains("JSON schema validation errors")); } @Test diff --git a/mcp-json-jackson3/src/main/java/io/modelcontextprotocol/json/schema/jackson3/DefaultJsonSchemaValidator.java b/mcp-json-jackson3/src/main/java/io/modelcontextprotocol/json/schema/jackson3/DefaultJsonSchemaValidator.java index 284289895..9af17ebcd 100644 --- a/mcp-json-jackson3/src/main/java/io/modelcontextprotocol/json/schema/jackson3/DefaultJsonSchemaValidator.java +++ b/mcp-json-jackson3/src/main/java/io/modelcontextprotocol/json/schema/jackson3/DefaultJsonSchemaValidator.java @@ -75,8 +75,7 @@ public ValidationResponse validate(Map schema, Object structured // Check if validation passed if (!validationResult.isEmpty()) { return ValidationResponse - .asInvalid("Validation failed: structuredContent does not match tool outputSchema. " - + "Validation errors: " + validationResult); + .asInvalid("Validation failed: JSON schema validation errors: " + validationResult); } return ValidationResponse.asValid(jsonStructuredOutput.toString()); diff --git a/mcp-json-jackson3/src/test/java/io/modelcontextprotocol/json/DefaultJsonSchemaValidatorTests.java b/mcp-json-jackson3/src/test/java/io/modelcontextprotocol/json/DefaultJsonSchemaValidatorTests.java index be01eb23c..d56606a25 100644 --- a/mcp-json-jackson3/src/test/java/io/modelcontextprotocol/json/DefaultJsonSchemaValidatorTests.java +++ b/mcp-json-jackson3/src/test/java/io/modelcontextprotocol/json/DefaultJsonSchemaValidatorTests.java @@ -308,7 +308,7 @@ void testValidateWithInvalidTypeSchema() { assertFalse(response.valid()); assertNotNull(response.errorMessage()); assertTrue(response.errorMessage().contains("Validation failed")); - assertTrue(response.errorMessage().contains("structuredContent does not match tool outputSchema")); + assertTrue(response.errorMessage().contains("JSON schema validation errors")); } @Test diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java index 5bd2a5dad..c2fde7253 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java @@ -182,6 +182,9 @@ void invalidInput_withDefaultValidation_shouldReturnToolError(String serverType, assertThat(result.isError()).isTrue(); String errorMessage = ((TextContent) result.content().get(0)).text(); + assertThat(errorMessage).startsWith("Tool input validation failed:"); + assertThat(errorMessage).containsIgnoringCase("Validation failed"); + assertThat(errorMessage).containsIgnoringCase("JSON schema validation errors"); assertThat(errorMessage).containsIgnoringCase(expectedErrorSubstring); } finally { From f89bb9927480ebdd4e289c6bada2f2a8dc2bc484 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 11 Jun 2026 09:29:30 +0200 Subject: [PATCH 2/3] address review comments Signed-off-by: Christian Tzolov --- .../server/McpAsyncServer.java | 4 +-- .../server/McpStatelessAsyncServer.java | 27 ++++++++++--------- .../util/ToolInputValidator.java | 4 +-- .../ToolInputValidationIntegrationTests.java | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 4d3772525..83aa8f5ba 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -26,7 +26,6 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CompleteResult.CompleteCompletion; import io.modelcontextprotocol.spec.McpSchema.ErrorCodes; -import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import io.modelcontextprotocol.spec.McpSchema.PromptReference; import io.modelcontextprotocol.spec.McpSchema.ResourceReference; import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest; @@ -433,7 +432,8 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent()); if (!validation.valid()) { - String message = "Tool output validation failed: " + validation.errorMessage(); + String message = "Tool (" + request.name() + ") output validation failed: " + + validation.errorMessage(); logger.warn(message); return CallToolResult.builder() .content(List.of(McpSchema.TextContent.builder(message).build())) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index d99a4a79b..c6105267d 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -4,9 +4,19 @@ package io.modelcontextprotocol.server; -import io.modelcontextprotocol.json.TypeRef; -import io.modelcontextprotocol.json.McpJsonMapper; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiFunction; + import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.spec.McpError; @@ -28,16 +38,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.BiFunction; - import static io.modelcontextprotocol.spec.McpError.RESOURCE_NOT_FOUND; /** @@ -293,7 +293,8 @@ public Mono apply(McpTransportContext transportContext, McpSchem var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent()); if (!validation.valid()) { - String message = "Tool output validation failed: " + validation.errorMessage(); + String message = "Tool (" + request.name() + ") output validation failed: " + + validation.errorMessage(); logger.warn(message); return CallToolResult.builder() .content(List.of(McpSchema.TextContent.builder(message).build())) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java index 2233a9e5e..1fa4cd8b8 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java @@ -42,8 +42,8 @@ public static CallToolResult validate(McpSchema.Tool tool, Map a Map args = arguments != null ? arguments : Map.of(); var validation = validator.validate(tool.inputSchema(), args); if (!validation.valid()) { - String message = "Tool input validation failed: " + validation.errorMessage(); - logger.warn("Tool '{}' {}", tool.name(), message); + String message = "Tool (" + tool.name() + ") input validation failed: " + validation.errorMessage(); + logger.warn("'{}' {}", tool.name(), message); return CallToolResult.builder() .content(List.of(McpSchema.TextContent.builder(message).build())) .isError(true) diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java index c2fde7253..3e4f5fbd7 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/ToolInputValidationIntegrationTests.java @@ -182,7 +182,7 @@ void invalidInput_withDefaultValidation_shouldReturnToolError(String serverType, assertThat(result.isError()).isTrue(); String errorMessage = ((TextContent) result.content().get(0)).text(); - assertThat(errorMessage).startsWith("Tool input validation failed:"); + assertThat(errorMessage).startsWith("Tool (test-tool) input validation failed:"); assertThat(errorMessage).containsIgnoringCase("Validation failed"); assertThat(errorMessage).containsIgnoringCase("JSON schema validation errors"); assertThat(errorMessage).containsIgnoringCase(expectedErrorSubstring); From 2291c7bd6580b71c3e34d8ee22082f123b0b9191 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 11 Jun 2026 09:37:13 +0200 Subject: [PATCH 3/3] address review comments Signed-off-by: Christian Tzolov --- .../java/io/modelcontextprotocol/util/ToolInputValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java index 1fa4cd8b8..76f9390a8 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java @@ -43,7 +43,7 @@ public static CallToolResult validate(McpSchema.Tool tool, Map a var validation = validator.validate(tool.inputSchema(), args); if (!validation.valid()) { String message = "Tool (" + tool.name() + ") input validation failed: " + validation.errorMessage(); - logger.warn("'{}' {}", tool.name(), message); + logger.warn(message); return CallToolResult.builder() .content(List.of(McpSchema.TextContent.builder(message).build())) .isError(true)