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:
parent
95203233c8
commit
d1823389a9
4 changed files with 496 additions and 9 deletions
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue