Skip to content

Commit 45825cb

Browse files
committed
feat: tools name validation accordingly #SEP-986 (fixed PR comments, refactoring)
1 parent 32fe526 commit 45825cb

File tree

8 files changed

+256
-74
lines changed

8 files changed

+256
-74
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
3131
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
3232
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
33+
import io.modelcontextprotocol.util.ToolNameValidator;
3334
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
3435
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
3536
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
@@ -656,6 +657,10 @@ private Mono<McpSchema.ListToolsResult> listToolsInternal(Initialization init, S
656657
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
657658
LIST_TOOLS_RESULT_TYPE_REF)
658659
.doOnNext(result -> {
660+
// Validate tool names (warn only)
661+
if (result.tools() != null) {
662+
result.tools().forEach(tool -> ToolNameValidator.validate(tool.name(), true));
663+
}
659664
if (this.enableCallToolSchemaCaching && result.tools() != null) {
660665
// Cache tools output schema
661666
result.tools()

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

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,24 @@
55
package io.modelcontextprotocol.server;
66

77
import io.modelcontextprotocol.common.McpTransportContext;
8-
import java.time.Duration;
9-
import java.util.ArrayList;
10-
import java.util.Arrays;
11-
import java.util.HashMap;
12-
import java.util.List;
13-
import java.util.Map;
14-
import java.util.function.BiConsumer;
15-
import java.util.function.BiFunction;
16-
178
import io.modelcontextprotocol.json.McpJsonMapper;
18-
199
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
2010
import io.modelcontextprotocol.spec.McpSchema;
2111
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
22-
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
2312
import io.modelcontextprotocol.spec.McpServerTransportProvider;
2413
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
2514
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
2615
import io.modelcontextprotocol.util.Assert;
2716
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
2817
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
18+
import io.modelcontextprotocol.util.ToolNameValidator;
2919
import reactor.core.publisher.Mono;
3020

21+
import java.time.Duration;
22+
import java.util.*;
23+
import java.util.function.BiConsumer;
24+
import java.util.function.BiFunction;
25+
3126
/**
3227
* Factory class for creating Model Context Protocol (MCP) servers. MCP servers expose
3328
* tools, resources, and prompts to AI models through a standardized interface.
@@ -291,6 +286,8 @@ abstract class AsyncSpecification<S extends AsyncSpecification<S>> {
291286

292287
String instructions;
293288

289+
Boolean skipStrictToolNameValidation;
290+
294291
/**
295292
* The Model Context Protocol (MCP) allows servers to expose tools that can be
296293
* invoked by language models. Tools enable models to interact with external
@@ -407,6 +404,18 @@ public AsyncSpecification<S> instructions(String instructions) {
407404
return this;
408405
}
409406

407+
/**
408+
* Sets whether to skip strict tool name validation for this server. When set,
409+
* this takes priority over the system property
410+
* {@code io.modelcontextprotocol.skipStrictToolNameValidation}.
411+
* @param skip true to warn only, false to throw exception on invalid names
412+
* @return This builder instance for method chaining
413+
*/
414+
public AsyncSpecification<S> skipStrictToolNameValidation(boolean strict) {
415+
this.skipStrictToolNameValidation = strict;
416+
return this;
417+
}
418+
410419
/**
411420
* Sets the server capabilities that will be advertised to clients during
412421
* connection initialization. Capabilities define what features the server
@@ -459,6 +468,7 @@ public AsyncSpecification<S> tool(McpSchema.Tool tool,
459468
BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<CallToolResult>> handler) {
460469
Assert.notNull(tool, "Tool must not be null");
461470
Assert.notNull(handler, "Handler must not be null");
471+
validateToolName(tool.name());
462472
assertNoDuplicateTool(tool.name());
463473

464474
this.tools.add(new McpServerFeatures.AsyncToolSpecification(tool, handler));
@@ -484,6 +494,7 @@ public AsyncSpecification<S> toolCall(McpSchema.Tool tool,
484494

485495
Assert.notNull(tool, "Tool must not be null");
486496
Assert.notNull(callHandler, "Handler must not be null");
497+
validateToolName(tool.name());
487498
assertNoDuplicateTool(tool.name());
488499

489500
this.tools
@@ -506,6 +517,7 @@ public AsyncSpecification<S> tools(List<McpServerFeatures.AsyncToolSpecification
506517
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
507518

508519
for (var tool : toolSpecifications) {
520+
validateToolName(tool.tool().name());
509521
assertNoDuplicateTool(tool.tool().name());
510522
this.tools.add(tool);
511523
}
@@ -533,12 +545,17 @@ public AsyncSpecification<S> tools(McpServerFeatures.AsyncToolSpecification... t
533545
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
534546

535547
for (McpServerFeatures.AsyncToolSpecification tool : toolSpecifications) {
548+
validateToolName(tool.tool().name());
536549
assertNoDuplicateTool(tool.tool().name());
537550
this.tools.add(tool);
538551
}
539552
return this;
540553
}
541554

555+
private void validateToolName(String toolName) {
556+
ToolNameValidator.validate(toolName, this.skipStrictToolNameValidation);
557+
}
558+
542559
private void assertNoDuplicateTool(String toolName) {
543560
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
544561
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
@@ -888,6 +905,8 @@ abstract class SyncSpecification<S extends SyncSpecification<S>> {
888905

889906
String instructions;
890907

908+
Boolean skipStrictToolNameValidation;
909+
891910
/**
892911
* The Model Context Protocol (MCP) allows servers to expose tools that can be
893912
* invoked by language models. Tools enable models to interact with external
@@ -1008,6 +1027,18 @@ public SyncSpecification<S> instructions(String instructions) {
10081027
return this;
10091028
}
10101029

1030+
/**
1031+
* Sets whether to skip strict tool name validation for this server. When set,
1032+
* this takes priority over the system property
1033+
* {@code io.modelcontextprotocol.skipStrictToolNameValidation}.
1034+
* @param skip true to warn only, false to throw exception on invalid names
1035+
* @return This builder instance for method chaining
1036+
*/
1037+
public SyncSpecification<S> skipStrictToolNameValidation(boolean strict) {
1038+
this.skipStrictToolNameValidation = strict;
1039+
return this;
1040+
}
1041+
10111042
/**
10121043
* Sets the server capabilities that will be advertised to clients during
10131044
* connection initialization. Capabilities define what features the server
@@ -1059,6 +1090,7 @@ public SyncSpecification<S> tool(McpSchema.Tool tool,
10591090
BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> handler) {
10601091
Assert.notNull(tool, "Tool must not be null");
10611092
Assert.notNull(handler, "Handler must not be null");
1093+
validateToolName(tool.name());
10621094
assertNoDuplicateTool(tool.name());
10631095

10641096
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, handler));
@@ -1083,6 +1115,7 @@ public SyncSpecification<S> toolCall(McpSchema.Tool tool,
10831115
BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler) {
10841116
Assert.notNull(tool, "Tool must not be null");
10851117
Assert.notNull(handler, "Handler must not be null");
1118+
validateToolName(tool.name());
10861119
assertNoDuplicateTool(tool.name());
10871120

10881121
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, null, handler));
@@ -1105,7 +1138,8 @@ public SyncSpecification<S> tools(List<McpServerFeatures.SyncToolSpecification>
11051138

11061139
for (var tool : toolSpecifications) {
11071140
String toolName = tool.tool().name();
1108-
assertNoDuplicateTool(toolName); // Check against existing tools
1141+
validateToolName(toolName);
1142+
assertNoDuplicateTool(toolName);
11091143
this.tools.add(tool);
11101144
}
11111145

@@ -1133,12 +1167,17 @@ public SyncSpecification<S> tools(McpServerFeatures.SyncToolSpecification... too
11331167
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
11341168

11351169
for (McpServerFeatures.SyncToolSpecification tool : toolSpecifications) {
1170+
validateToolName(tool.tool().name());
11361171
assertNoDuplicateTool(tool.tool().name());
11371172
this.tools.add(tool);
11381173
}
11391174
return this;
11401175
}
11411176

1177+
private void validateToolName(String toolName) {
1178+
ToolNameValidator.validate(toolName, this.skipStrictToolNameValidation);
1179+
}
1180+
11421181
private void assertNoDuplicateTool(String toolName) {
11431182
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
11441183
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
@@ -1434,6 +1473,8 @@ class StatelessAsyncSpecification {
14341473

14351474
String instructions;
14361475

1476+
Boolean skipStrictToolNameValidation;
1477+
14371478
/**
14381479
* The Model Context Protocol (MCP) allows servers to expose tools that can be
14391480
* invoked by language models. Tools enable models to interact with external
@@ -1551,6 +1592,18 @@ public StatelessAsyncSpecification instructions(String instructions) {
15511592
return this;
15521593
}
15531594

1595+
/**
1596+
* Sets whether to skip strict tool name validation for this server. When set,
1597+
* this takes priority over the system property
1598+
* {@code io.modelcontextprotocol.skipStrictToolNameValidation}.
1599+
* @param skip true to warn only, false to throw exception on invalid names
1600+
* @return This builder instance for method chaining
1601+
*/
1602+
public StatelessAsyncSpecification skipStrictToolNameValidation(boolean strict) {
1603+
this.skipStrictToolNameValidation = strict;
1604+
return this;
1605+
}
1606+
15541607
/**
15551608
* Sets the server capabilities that will be advertised to clients during
15561609
* connection initialization. Capabilities define what features the server
@@ -1589,6 +1642,7 @@ public StatelessAsyncSpecification toolCall(McpSchema.Tool tool,
15891642

15901643
Assert.notNull(tool, "Tool must not be null");
15911644
Assert.notNull(callHandler, "Handler must not be null");
1645+
validateToolName(tool.name());
15921646
assertNoDuplicateTool(tool.name());
15931647

15941648
this.tools.add(new McpStatelessServerFeatures.AsyncToolSpecification(tool, callHandler));
@@ -1611,6 +1665,7 @@ public StatelessAsyncSpecification tools(
16111665
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
16121666

16131667
for (var tool : toolSpecifications) {
1668+
validateToolName(tool.tool().name());
16141669
assertNoDuplicateTool(tool.tool().name());
16151670
this.tools.add(tool);
16161671
}
@@ -1639,12 +1694,17 @@ public StatelessAsyncSpecification tools(
16391694
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
16401695

16411696
for (var tool : toolSpecifications) {
1697+
validateToolName(tool.tool().name());
16421698
assertNoDuplicateTool(tool.tool().name());
16431699
this.tools.add(tool);
16441700
}
16451701
return this;
16461702
}
16471703

1704+
private void validateToolName(String toolName) {
1705+
ToolNameValidator.validate(toolName, this.skipStrictToolNameValidation);
1706+
}
1707+
16481708
private void assertNoDuplicateTool(String toolName) {
16491709
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
16501710
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
@@ -1896,6 +1956,8 @@ class StatelessSyncSpecification {
18961956

18971957
String instructions;
18981958

1959+
Boolean skipStrictToolNameValidation;
1960+
18991961
/**
19001962
* The Model Context Protocol (MCP) allows servers to expose tools that can be
19011963
* invoked by language models. Tools enable models to interact with external
@@ -2013,6 +2075,18 @@ public StatelessSyncSpecification instructions(String instructions) {
20132075
return this;
20142076
}
20152077

2078+
/**
2079+
* Sets whether to skip strict tool name validation for this server. When set,
2080+
* this takes priority over the system property
2081+
* {@code io.modelcontextprotocol.skipStrictToolNameValidation}.
2082+
* @param skip true to warn only, false to throw exception on invalid names
2083+
* @return This builder instance for method chaining
2084+
*/
2085+
public StatelessSyncSpecification skipStrictToolNameValidation(boolean strict) {
2086+
this.skipStrictToolNameValidation = strict;
2087+
return this;
2088+
}
2089+
20162090
/**
20172091
* Sets the server capabilities that will be advertised to clients during
20182092
* connection initialization. Capabilities define what features the server
@@ -2051,6 +2125,7 @@ public StatelessSyncSpecification toolCall(McpSchema.Tool tool,
20512125

20522126
Assert.notNull(tool, "Tool must not be null");
20532127
Assert.notNull(callHandler, "Handler must not be null");
2128+
validateToolName(tool.name());
20542129
assertNoDuplicateTool(tool.name());
20552130

20562131
this.tools.add(new McpStatelessServerFeatures.SyncToolSpecification(tool, callHandler));
@@ -2073,6 +2148,7 @@ public StatelessSyncSpecification tools(
20732148
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
20742149

20752150
for (var tool : toolSpecifications) {
2151+
validateToolName(tool.tool().name());
20762152
assertNoDuplicateTool(tool.tool().name());
20772153
this.tools.add(tool);
20782154
}
@@ -2101,12 +2177,17 @@ public StatelessSyncSpecification tools(
21012177
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
21022178

21032179
for (var tool : toolSpecifications) {
2180+
validateToolName(tool.tool().name());
21042181
assertNoDuplicateTool(tool.tool().name());
21052182
this.tools.add(tool);
21062183
}
21072184
return this;
21082185
}
21092186

2187+
private void validateToolName(String toolName) {
2188+
ToolNameValidator.validate(toolName, this.skipStrictToolNameValidation);
2189+
}
2190+
21102191
private void assertNoDuplicateTool(String toolName) {
21112192
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
21122193
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,15 +1325,6 @@ public record ListToolsResult( // @formatter:off
13251325
@JsonProperty("nextCursor") String nextCursor,
13261326
@JsonProperty("_meta") Map<String, Object> meta) implements Result { // @formatter:on
13271327

1328-
/**
1329-
* Compact constructor that validates tool names on deserialization (warns only).
1330-
*/
1331-
public ListToolsResult {
1332-
if (tools != null) {
1333-
tools.forEach(tool -> ToolNameValidator.validate(tool.name(), false));
1334-
}
1335-
}
1336-
13371328
public ListToolsResult(List<Tool> tools, String nextCursor) {
13381329
this(tools, nextCursor, null);
13391330
}
@@ -1475,7 +1466,6 @@ public Builder meta(Map<String, Object> meta) {
14751466
}
14761467

14771468
public Tool build() {
1478-
ToolNameValidator.validate(name, true);
14791469
return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta);
14801470
}
14811471

@@ -1517,13 +1507,6 @@ public record CallToolRequest( // @formatter:off
15171507
@JsonProperty("arguments") Map<String, Object> arguments,
15181508
@JsonProperty("_meta") Map<String, Object> meta) implements Request { // @formatter:on
15191509

1520-
/**
1521-
* Compact constructor that validates tool name on deserialization (warns only).
1522-
*/
1523-
public CallToolRequest {
1524-
ToolNameValidator.validate(name, false);
1525-
}
1526-
15271510
public CallToolRequest(McpJsonMapper jsonMapper, String name, String jsonArguments) {
15281511
this(name, parseJsonArguments(jsonMapper, jsonArguments), null);
15291512
}

0 commit comments

Comments
 (0)