Skip to content

Commit d182338

Browse files
authored
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>
1 parent 9520323 commit d182338

6 files changed

Lines changed: 496 additions & 9 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.modelcontextprotocol.util.Assert;
3939
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
4040
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
41+
import io.modelcontextprotocol.util.ToolInputValidator;
4142
import io.modelcontextprotocol.util.Utils;
4243
import org.slf4j.Logger;
4344
import org.slf4j.LoggerFactory;
@@ -98,6 +99,8 @@ public class McpAsyncServer {
9899

99100
private final JsonSchemaValidator jsonSchemaValidator;
100101

102+
private final boolean validateToolInputs;
103+
101104
private final McpSchema.ServerCapabilities serverCapabilities;
102105

103106
private final McpSchema.Implementation serverInfo;
@@ -129,7 +132,8 @@ public class McpAsyncServer {
129132
*/
130133
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
131134
McpServerFeatures.Async features, Duration requestTimeout,
132-
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
135+
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
136+
boolean validateToolInputs) {
133137
this.mcpTransportProvider = mcpTransportProvider;
134138
this.jsonMapper = jsonMapper;
135139
this.serverInfo = features.serverInfo();
@@ -142,6 +146,7 @@ public class McpAsyncServer {
142146
this.completions.putAll(features.completions());
143147
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
144148
this.jsonSchemaValidator = jsonSchemaValidator;
149+
this.validateToolInputs = validateToolInputs;
145150

146151
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
147152
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@@ -157,7 +162,8 @@ public class McpAsyncServer {
157162

158163
McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
159164
McpServerFeatures.Async features, Duration requestTimeout,
160-
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
165+
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
166+
boolean validateToolInputs) {
161167
this.mcpTransportProvider = mcpTransportProvider;
162168
this.jsonMapper = jsonMapper;
163169
this.serverInfo = features.serverInfo();
@@ -170,6 +176,7 @@ public class McpAsyncServer {
170176
this.completions.putAll(features.completions());
171177
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
172178
this.jsonSchemaValidator = jsonSchemaValidator;
179+
this.validateToolInputs = validateToolInputs;
173180

174181
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
175182
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@@ -543,6 +550,13 @@ private McpRequestHandler<CallToolResult> toolsCallRequestHandler() {
543550
.build());
544551
}
545552

553+
McpSchema.Tool tool = toolSpecification.get().tool();
554+
CallToolResult validationError = ToolInputValidator.validate(tool, callToolRequest.arguments(),
555+
this.validateToolInputs, this.jsonSchemaValidator);
556+
if (validationError != null) {
557+
return Mono.just(validationError);
558+
}
559+
546560
return toolSpecification.get().callHandler().apply(exchange, callToolRequest);
547561
};
548562
}

mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public McpAsyncServer build() {
243243
: McpJsonDefaults.getSchemaValidator();
244244

245245
return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
246-
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
246+
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
247247
}
248248

249249
}
@@ -269,7 +269,7 @@ public McpAsyncServer build() {
269269
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
270270
: McpJsonDefaults.getSchemaValidator();
271271
return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
272-
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
272+
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
273273
}
274274

275275
}
@@ -293,6 +293,8 @@ abstract class AsyncSpecification<S extends AsyncSpecification<S>> {
293293

294294
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
295295

296+
boolean validateToolInputs = true;
297+
296298
/**
297299
* The Model Context Protocol (MCP) allows servers to expose tools that can be
298300
* invoked by language models. Tools enable models to interact with external
@@ -421,6 +423,17 @@ public AsyncSpecification<S> strictToolNameValidation(boolean strict) {
421423
return this;
422424
}
423425

426+
/**
427+
* Sets whether to validate tool inputs against the tool's input schema.
428+
* @param validate true to validate inputs and return error on validation failure,
429+
* false to skip validation. Defaults to true.
430+
* @return This builder instance for method chaining
431+
*/
432+
public AsyncSpecification<S> validateToolInputs(boolean validate) {
433+
this.validateToolInputs = validate;
434+
return this;
435+
}
436+
424437
/**
425438
* Sets the server capabilities that will be advertised to clients during
426439
* connection initialization. Capabilities define what features the server
@@ -818,7 +831,8 @@ public McpSyncServer build() {
818831
var asyncServer = new McpAsyncServer(transportProvider,
819832
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
820833
uriTemplateManagerFactory,
821-
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator());
834+
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
835+
validateToolInputs);
822836
return new McpSyncServer(asyncServer, this.immediateExecution);
823837
}
824838

@@ -849,7 +863,7 @@ public McpSyncServer build() {
849863
: McpJsonDefaults.getSchemaValidator();
850864
var asyncServer = new McpAsyncServer(transportProvider,
851865
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, this.requestTimeout,
852-
this.uriTemplateManagerFactory, jsonSchemaValidator);
866+
this.uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
853867
return new McpSyncServer(asyncServer, this.immediateExecution);
854868
}
855869

@@ -872,6 +886,8 @@ abstract class SyncSpecification<S extends SyncSpecification<S>> {
872886

873887
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
874888

889+
boolean validateToolInputs = true;
890+
875891
/**
876892
* The Model Context Protocol (MCP) allows servers to expose tools that can be
877893
* invoked by language models. Tools enable models to interact with external
@@ -1004,6 +1020,17 @@ public SyncSpecification<S> strictToolNameValidation(boolean strict) {
10041020
return this;
10051021
}
10061022

1023+
/**
1024+
* Sets whether to validate tool inputs against the tool's input schema.
1025+
* @param validate true to validate inputs and return error on validation failure,
1026+
* false to skip validation. Defaults to true.
1027+
* @return This builder instance for method chaining
1028+
*/
1029+
public SyncSpecification<S> validateToolInputs(boolean validate) {
1030+
this.validateToolInputs = validate;
1031+
return this;
1032+
}
1033+
10071034
/**
10081035
* Sets the server capabilities that will be advertised to clients during
10091036
* connection initialization. Capabilities define what features the server
@@ -1401,6 +1428,8 @@ class StatelessAsyncSpecification {
14011428

14021429
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
14031430

1431+
boolean validateToolInputs = true;
1432+
14041433
/**
14051434
* The Model Context Protocol (MCP) allows servers to expose tools that can be
14061435
* invoked by language models. Tools enable models to interact with external
@@ -1530,6 +1559,17 @@ public StatelessAsyncSpecification strictToolNameValidation(boolean strict) {
15301559
return this;
15311560
}
15321561

1562+
/**
1563+
* Sets whether to validate tool inputs against the tool's input schema.
1564+
* @param validate true to validate inputs and return error on validation failure,
1565+
* false to skip validation. Defaults to true.
1566+
* @return This builder instance for method chaining
1567+
*/
1568+
public StatelessAsyncSpecification validateToolInputs(boolean validate) {
1569+
this.validateToolInputs = validate;
1570+
return this;
1571+
}
1572+
15331573
/**
15341574
* Sets the server capabilities that will be advertised to clients during
15351575
* connection initialization. Capabilities define what features the server
@@ -1859,7 +1899,8 @@ public McpStatelessAsyncServer build() {
18591899
this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
18601900
return new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
18611901
features, requestTimeout, uriTemplateManagerFactory,
1862-
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator());
1902+
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
1903+
validateToolInputs);
18631904
}
18641905

18651906
}
@@ -1884,6 +1925,8 @@ class StatelessSyncSpecification {
18841925

18851926
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
18861927

1928+
boolean validateToolInputs = true;
1929+
18871930
/**
18881931
* The Model Context Protocol (MCP) allows servers to expose tools that can be
18891932
* invoked by language models. Tools enable models to interact with external
@@ -2013,6 +2056,17 @@ public StatelessSyncSpecification strictToolNameValidation(boolean strict) {
20132056
return this;
20142057
}
20152058

2059+
/**
2060+
* Sets whether to validate tool inputs against the tool's input schema.
2061+
* @param validate true to validate inputs and return error on validation failure,
2062+
* false to skip validation. Defaults to true.
2063+
* @return This builder instance for method chaining
2064+
*/
2065+
public StatelessSyncSpecification validateToolInputs(boolean validate) {
2066+
this.validateToolInputs = validate;
2067+
return this;
2068+
}
2069+
20162070
/**
20172071
* Sets the server capabilities that will be advertised to clients during
20182072
* connection initialization. Capabilities define what features the server
@@ -2360,7 +2414,8 @@ public McpStatelessSyncServer build() {
23602414
var asyncServer = new McpStatelessAsyncServer(transport,
23612415
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
23622416
uriTemplateManagerFactory,
2363-
this.jsonSchemaValidator != null ? this.jsonSchemaValidator : McpJsonDefaults.getSchemaValidator());
2417+
this.jsonSchemaValidator != null ? this.jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
2418+
validateToolInputs);
23642419
return new McpStatelessSyncServer(asyncServer, this.immediateExecution);
23652420
}
23662421

mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.modelcontextprotocol.util.Assert;
2222
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
2323
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
24+
import io.modelcontextprotocol.util.ToolInputValidator;
2425
import io.modelcontextprotocol.util.Utils;
2526
import org.slf4j.Logger;
2627
import org.slf4j.LoggerFactory;
@@ -77,9 +78,12 @@ public class McpStatelessAsyncServer {
7778

7879
private final JsonSchemaValidator jsonSchemaValidator;
7980

81+
private final boolean validateToolInputs;
82+
8083
McpStatelessAsyncServer(McpStatelessServerTransport mcpTransport, McpJsonMapper jsonMapper,
8184
McpStatelessServerFeatures.Async features, Duration requestTimeout,
82-
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
85+
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
86+
boolean validateToolInputs) {
8387
this.mcpTransportProvider = mcpTransport;
8488
this.jsonMapper = jsonMapper;
8589
this.serverInfo = features.serverInfo();
@@ -92,6 +96,7 @@ public class McpStatelessAsyncServer {
9296
this.completions.putAll(features.completions());
9397
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
9498
this.jsonSchemaValidator = jsonSchemaValidator;
99+
this.validateToolInputs = validateToolInputs;
95100

96101
Map<String, McpStatelessRequestHandler<?>> requestHandlers = new HashMap<>();
97102

@@ -409,6 +414,13 @@ private McpStatelessRequestHandler<CallToolResult> toolsCallRequestHandler() {
409414
.build());
410415
}
411416

417+
McpSchema.Tool tool = toolSpecification.get().tool();
418+
CallToolResult validationError = ToolInputValidator.validate(tool, callToolRequest.arguments(),
419+
this.validateToolInputs, this.jsonSchemaValidator);
420+
if (validationError != null) {
421+
return Mono.just(validationError);
422+
}
423+
412424
return toolSpecification.get().callHandler().apply(ctx, callToolRequest);
413425
};
414426
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.util;
6+
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
11+
import io.modelcontextprotocol.spec.McpSchema;
12+
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
/**
17+
* Validates tool input arguments against JSON schema.
18+
*
19+
* @author Andrei Shakirin
20+
*/
21+
public final class ToolInputValidator {
22+
23+
private static final Logger logger = LoggerFactory.getLogger(ToolInputValidator.class);
24+
25+
private ToolInputValidator() {
26+
}
27+
28+
/**
29+
* Validates tool arguments against the tool's input schema.
30+
* @param tool the tool definition containing the input schema
31+
* @param arguments the arguments to validate
32+
* @param validateToolInputs whether validation is enabled
33+
* @param validator the JSON schema validator (may be null)
34+
* @return CallToolResult with isError=true if validation fails, null if valid or
35+
* validation skipped
36+
*/
37+
public static CallToolResult validate(McpSchema.Tool tool, Map<String, Object> arguments,
38+
boolean validateToolInputs, JsonSchemaValidator validator) {
39+
if (!validateToolInputs || tool.inputSchema() == null || validator == null) {
40+
return null;
41+
}
42+
Map<String, Object> args = arguments != null ? arguments : Map.of();
43+
var validation = validator.validate(tool.inputSchema(), args);
44+
if (!validation.valid()) {
45+
logger.warn("Tool '{}' input validation failed: {}", tool.name(), validation.errorMessage());
46+
return CallToolResult.builder()
47+
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
48+
.isError(true)
49+
.build();
50+
}
51+
return null;
52+
}
53+
54+
}

0 commit comments

Comments
 (0)