feat!: add tool input arguments validation (#873)

added tool input arguments validation causes tool execution error. Breaking change, because validation is activated by default

closes #697

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
ashakirin 2026-04-13 16:58:04 +02:00 • committed by GitHub
commit d1823389a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 496 additions and 9 deletions

View file

@ -38,6 +38,7 @@ import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.ToolInputValidator;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -98,6 +99,8 @@ public class McpAsyncServer {
private final JsonSchemaValidator jsonSchemaValidator;
private final boolean validateToolInputs;
private final McpSchema.ServerCapabilities serverCapabilities;
private final McpSchema.Implementation serverInfo;
@ -129,7 +132,8 @@ public class McpAsyncServer {
*/
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
boolean validateToolInputs) {
this.mcpTransportProvider = mcpTransportProvider;
this.jsonMapper = jsonMapper;
this.serverInfo = features.serverInfo();
@ -142,6 +146,7 @@ public class McpAsyncServer {
this.completions.putAll(features.completions());
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
this.validateToolInputs = validateToolInputs;
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@ -157,7 +162,8 @@ public class McpAsyncServer {
McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
boolean validateToolInputs) {
this.mcpTransportProvider = mcpTransportProvider;
this.jsonMapper = jsonMapper;
this.serverInfo = features.serverInfo();
@ -170,6 +176,7 @@ public class McpAsyncServer {
this.completions.putAll(features.completions());
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
this.validateToolInputs = validateToolInputs;
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@ -543,6 +550,13 @@ public class McpAsyncServer {
.build());
}
McpSchema.Tool tool = toolSpecification.get().tool();
CallToolResult validationError = ToolInputValidator.validate(tool, callToolRequest.arguments(),
this.validateToolInputs, this.jsonSchemaValidator);
if (validationError != null) {
return Mono.just(validationError);
}
return toolSpecification.get().callHandler().apply(exchange, callToolRequest);
};
}

View file

@ -243,7 +243,7 @@ public interface McpServer {
: McpJsonDefaults.getSchemaValidator();
return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
}
}
@ -269,7 +269,7 @@ public interface McpServer {
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();
return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
}
}
@ -293,6 +293,8 @@ public interface McpServer {
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
boolean validateToolInputs = true;
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@ -421,6 +423,17 @@ public interface McpServer {
return this;
}
/**
* Sets whether to validate tool inputs against the tool's input schema.
* @param validate true to validate inputs and return error on validation failure,
* false to skip validation. Defaults to true.
* @return This builder instance for method chaining
*/
public AsyncSpecification<S> validateToolInputs(boolean validate) {
this.validateToolInputs = validate;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -818,7 +831,8 @@ public interface McpServer {
var asyncServer = new McpAsyncServer(transportProvider,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
uriTemplateManagerFactory,
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator());
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
validateToolInputs);
return new McpSyncServer(asyncServer, this.immediateExecution);
}
@ -849,7 +863,7 @@ public interface McpServer {
: McpJsonDefaults.getSchemaValidator();
var asyncServer = new McpAsyncServer(transportProvider,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, this.requestTimeout,
this.uriTemplateManagerFactory, jsonSchemaValidator);
this.uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
return new McpSyncServer(asyncServer, this.immediateExecution);
}
@ -872,6 +886,8 @@ public interface McpServer {
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
boolean validateToolInputs = true;
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@ -1004,6 +1020,17 @@ public interface McpServer {
return this;
}
/**
* Sets whether to validate tool inputs against the tool's input schema.
* @param validate true to validate inputs and return error on validation failure,
* false to skip validation. Defaults to true.
* @return This builder instance for method chaining
*/
public SyncSpecification<S> validateToolInputs(boolean validate) {
this.validateToolInputs = validate;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -1401,6 +1428,8 @@ public interface McpServer {
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
boolean validateToolInputs = true;
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@ -1530,6 +1559,17 @@ public interface McpServer {
return this;
}
/**
* Sets whether to validate tool inputs against the tool's input schema.
* @param validate true to validate inputs and return error on validation failure,
* false to skip validation. Defaults to true.
* @return This builder instance for method chaining
*/
public StatelessAsyncSpecification validateToolInputs(boolean validate) {
this.validateToolInputs = validate;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -1859,7 +1899,8 @@ public interface McpServer {
this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
return new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
features, requestTimeout, uriTemplateManagerFactory,
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator());
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
validateToolInputs);
}
}
@ -1884,6 +1925,8 @@ public interface McpServer {
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
boolean validateToolInputs = true;
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@ -2013,6 +2056,17 @@ public interface McpServer {
return this;
}
/**
* Sets whether to validate tool inputs against the tool's input schema.
* @param validate true to validate inputs and return error on validation failure,
* false to skip validation. Defaults to true.
* @return This builder instance for method chaining
*/
public StatelessSyncSpecification validateToolInputs(boolean validate) {
this.validateToolInputs = validate;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -2360,7 +2414,8 @@ public interface McpServer {
var asyncServer = new McpStatelessAsyncServer(transport,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
uriTemplateManagerFactory,
this.jsonSchemaValidator != null ? this.jsonSchemaValidator : McpJsonDefaults.getSchemaValidator());
this.jsonSchemaValidator != null ? this.jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
validateToolInputs);
return new McpStatelessSyncServer(asyncServer, this.immediateExecution);
}

View file

@ -21,6 +21,7 @@ import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.ToolInputValidator;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -77,9 +78,12 @@ public class McpStatelessAsyncServer {
private final JsonSchemaValidator jsonSchemaValidator;
private final boolean validateToolInputs;
McpStatelessAsyncServer(McpStatelessServerTransport mcpTransport, McpJsonMapper jsonMapper,
McpStatelessServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
boolean validateToolInputs) {
this.mcpTransportProvider = mcpTransport;
this.jsonMapper = jsonMapper;
this.serverInfo = features.serverInfo();
@ -92,6 +96,7 @@ public class McpStatelessAsyncServer {
this.completions.putAll(features.completions());
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
this.validateToolInputs = validateToolInputs;
Map<String, McpStatelessRequestHandler<?>> requestHandlers = new HashMap<>();
@ -409,6 +414,13 @@ public class McpStatelessAsyncServer {
.build());
}
McpSchema.Tool tool = toolSpecification.get().tool();
CallToolResult validationError = ToolInputValidator.validate(tool, callToolRequest.arguments(),
this.validateToolInputs, this.jsonSchemaValidator);
if (validationError != null) {
return Mono.just(validationError);
}
return toolSpecification.get().callHandler().apply(ctx, callToolRequest);
};
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.util;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Validates tool input arguments against JSON schema.
*
* @author Andrei Shakirin
*/
public final class ToolInputValidator {
private static final Logger logger = LoggerFactory.getLogger(ToolInputValidator.class);
private ToolInputValidator() {
}
/**
* Validates tool arguments against the tool's input schema.
* @param tool the tool definition containing the input schema
* @param arguments the arguments to validate
* @param validateToolInputs whether validation is enabled
* @param validator the JSON schema validator (may be null)
* @return CallToolResult with isError=true if validation fails, null if valid or
* validation skipped
*/
public static CallToolResult validate(McpSchema.Tool tool, Map<String, Object> arguments,
boolean validateToolInputs, JsonSchemaValidator validator) {
if (!validateToolInputs || tool.inputSchema() == null || validator == null) {
return null;
}
Map<String, Object> 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());
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
.isError(true)
.build();
}
return null;
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.util;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link ToolInputValidator}.
*
* @author Andrei Shakirin
*/
class ToolInputValidatorTests {
private final JsonSchemaValidator validator = mock(JsonSchemaValidator.class);
private final Map<String, Object> inputSchema = Map.of("type", "object", "properties",
Map.of("name", Map.of("type", "string")), "required", List.of("name"));
private final Tool toolWithSchema = Tool.builder()
.name("test-tool")
.description("Test tool")
.inputSchema(inputSchema)
.build();
private final Tool toolWithoutSchema = Tool.builder().name("test-tool").description("Test tool").build();
@Test
void validate_whenDisabled_returnsNull() {
CallToolResult result = ToolInputValidator.validate(toolWithSchema, Map.of("name", "test"), false, validator);
assertThat(result).isNull();
verify(validator, never()).validate(any(), any());
}
@Test
void validate_whenNoSchema_returnsNull() {
CallToolResult result = ToolInputValidator.validate(toolWithoutSchema, Map.of("name", "test"), true, validator);
assertThat(result).isNull();
verify(validator, never()).validate(any(), any());
}
@Test
void validate_whenNoValidator_returnsNull() {
CallToolResult result = ToolInputValidator.validate(toolWithSchema, Map.of("name", "test"), true, null);
assertThat(result).isNull();
}
@Test
void validate_withValidInput_returnsNull() {
when(validator.validate(any(), any())).thenReturn(ValidationResponse.asValid(null));
CallToolResult result = ToolInputValidator.validate(toolWithSchema, Map.of("name", "test"), true, validator);
assertThat(result).isNull();
}
@Test
void validate_withInvalidInput_returnsErrorResult() {
when(validator.validate(any(), any())).thenReturn(ValidationResponse.asInvalid("missing required: 'name'"));
CallToolResult result = ToolInputValidator.validate(toolWithSchema, Map.of(), true, validator);
assertThat(result).isNotNull();
assertThat(result.isError()).isTrue();
assertThat(((TextContent) result.content().get(0)).text()).contains("missing required: 'name'");
verify(validator).validate(any(), any());
}
@Test
void validate_withNullArguments_usesEmptyMap() {
when(validator.validate(any(), any())).thenReturn(ValidationResponse.asValid(null));
CallToolResult result = ToolInputValidator.validate(toolWithSchema, null, true, validator);
assertThat(result).isNull();
verify(validator).validate(any(), any());
}
}

View file

@ -0,0 +1,254 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.server;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.startup.Tomcat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for tool input validation against JSON schema. Validates that input validation
* errors are returned as Tool Execution Errors (isError=true) rather than Protocol
* Errors, per MCP specification.
*
* @author Andrei Shakirin
*/
@Timeout(15)
class ToolInputValidationIntegrationTests {
private static final int PORT = TomcatTestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
private static final String TOOL_NAME = "test-tool";
private static final McpSchema.JsonSchema INPUT_SCHEMA = new McpSchema.JsonSchema("object",
Map.of("name", Map.of("type", "string"), "age", Map.of("type", "integer", "minimum", 0)),
List.of("name", "age"), null, null, null);
private static final McpTransportContextExtractor<HttpServletRequest> TEST_CONTEXT_EXTRACTOR = (
r) -> McpTransportContext.create(Map.of("important", "value"));
private HttpServletStreamableServerTransportProvider mcpServerTransportProvider;
private Tomcat tomcat;
static Stream<Arguments> validInputTestCases() {
return Stream.of(
// serverType, validationEnabled, inputArgs, expectedOutput
Arguments.of("sync", true, Map.of("name", "Alice", "age", 30), "Hello Alice, age 30"),
Arguments.of("async", true, Map.of("name", "Bob", "age", 25), "Hello Bob, age 25"),
Arguments.of("sync", false, Map.of("name", "Alice", "age", 30), "Hello Alice, age 30"),
Arguments.of("async", false, Map.of("name", "Bob", "age", 25), "Hello Bob, age 25"));
}
static Stream<Arguments> invalidInputTestCases() {
return Stream.of(
// serverType, inputArgs, expectedErrorSubstring
Arguments.of("sync", Map.of("name", "Alice"), "age"), // missing required
Arguments.of("async", Map.of("name", "Bob", "age", -10), "minimum")); // invalid
// value
}
private final McpClient.SyncSpec clientBuilder = McpClient
.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT).endpoint(MESSAGE_ENDPOINT).build())
.requestTimeout(Duration.ofSeconds(10));
@BeforeEach
public void before() {
mcpServerTransportProvider = HttpServletStreamableServerTransportProvider.builder()
.mcpEndpoint(MESSAGE_ENDPOINT)
.contextExtractor(TEST_CONTEXT_EXTRACTOR)
.build();
tomcat = TomcatTestUtil.createTomcatServer("", PORT, mcpServerTransportProvider);
try {
tomcat.start();
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
}
catch (Exception e) {
throw new RuntimeException("Failed to start Tomcat", e);
}
}
protected McpServer.AsyncSpecification<?> prepareAsyncServerBuilder() {
return McpServer.async(this.mcpServerTransportProvider);
}
protected McpServer.SyncSpecification<?> prepareSyncServerBuilder() {
return McpServer.sync(this.mcpServerTransportProvider);
}
@AfterEach
public void after() {
if (mcpServerTransportProvider != null) {
mcpServerTransportProvider.closeGracefully().block();
}
if (tomcat != null) {
try {
tomcat.stop();
tomcat.destroy();
}
catch (LifecycleException e) {
throw new RuntimeException("Failed to stop Tomcat", e);
}
}
}
private McpServerFeatures.SyncToolSpecification createSyncTool() {
Tool tool = Tool.builder()
.name(TOOL_NAME)
.description("Test tool with schema")
.inputSchema(INPUT_SCHEMA)
.build();
return McpServerFeatures.SyncToolSpecification.builder().tool(tool).callHandler((exchange, request) -> {
String name = (String) request.arguments().get("name");
Integer age = ((Number) request.arguments().get("age")).intValue();
return CallToolResult.builder()
.content(List.of(new TextContent("Hello " + name + ", age " + age)))
.isError(false)
.build();
}).build();
}
private McpServerFeatures.AsyncToolSpecification createAsyncTool() {
Tool tool = Tool.builder()
.name(TOOL_NAME)
.description("Test tool with schema")
.inputSchema(INPUT_SCHEMA)
.build();
return McpServerFeatures.AsyncToolSpecification.builder().tool(tool).callHandler((exchange, request) -> {
String name = (String) request.arguments().get("name");
Integer age = ((Number) request.arguments().get("age")).intValue();
return Mono.just(CallToolResult.builder()
.content(List.of(new TextContent("Hello " + name + ", age " + age)))
.isError(false)
.build());
}).build();
}
@ParameterizedTest(name = "{0} server, validation={1}")
@MethodSource("validInputTestCases")
void validInput_shouldSucceed(String serverType, boolean validationEnabled, Map<String, Object> input,
String expectedOutput) {
Object server = createServer(serverType, validationEnabled);
try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("test-client", "1.0.0")).build()) {
client.initialize();
CallToolResult result = client.callTool(new CallToolRequest(TOOL_NAME, input));
assertThat(result.isError()).isFalse();
assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedOutput);
}
finally {
closeServer(server, serverType);
}
}
@ParameterizedTest(name = "{0} server, input={1}")
@MethodSource("invalidInputTestCases")
void invalidInput_withDefaultValidation_shouldReturnToolError(String serverType, Map<String, Object> input,
String expectedErrorSubstring) {
Object server = createServerWithDefaultValidation(serverType);
try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("test-client", "1.0.0")).build()) {
client.initialize();
CallToolResult result = client.callTool(new CallToolRequest(TOOL_NAME, input));
assertThat(result.isError()).isTrue();
String errorMessage = ((TextContent) result.content().get(0)).text();
assertThat(errorMessage).containsIgnoringCase(expectedErrorSubstring);
}
finally {
closeServer(server, serverType);
}
}
@ParameterizedTest(name = "{0} server, input={1}")
@MethodSource("invalidInputTestCases")
void invalidInput_withValidationDisabled_shouldSucceed(String serverType, Map<String, Object> input,
String ignored) {
Object server = createServer(serverType, false);
try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("test-client", "1.0.0")).build()) {
client.initialize();
// Invalid input should pass through when validation is disabled
// The tool handler will fail, but that's expected - we're testing validation
// is skipped
try {
client.callTool(new CallToolRequest(TOOL_NAME, input));
}
catch (Exception e) {
// Expected - tool handler fails on invalid input, but validation didn't
// block it
assertThat(e.getMessage()).doesNotContainIgnoringCase("validation");
}
}
finally {
closeServer(server, serverType);
}
}
private Object createServerWithDefaultValidation(String serverType) {
if ("sync".equals(serverType)) {
return prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").tools(createSyncTool()).build();
}
else {
return prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(createAsyncTool()).build();
}
}
private Object createServer(String serverType, boolean validationEnabled) {
if ("sync".equals(serverType)) {
return prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.validateToolInputs(validationEnabled)
.tools(createSyncTool())
.build();
}
else {
return prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.validateToolInputs(validationEnabled)
.tools(createAsyncTool())
.build();
}
}
private void closeServer(Object server, String serverType) {
if ("async".equals(serverType)) {
((McpAsyncServer) server).closeGracefully().block();
}
else {
((McpSyncServer) server).close();
}
}
}