Add tool name validation SEP-986 (#764)

* feat: added tools name format validation accordingly #SEP-986

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
ashakirin 2026-02-10 10:52:58 +01:00 • committed by GitHub
commit 09a853f555
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 413 additions and 13 deletions

View file

@ -30,6 +30,7 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.util.ToolNameValidator;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
@ -656,6 +657,10 @@ public class McpAsyncClient {
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
LIST_TOOLS_RESULT_TYPE_REF)
.doOnNext(result -> {
// Validate tool names (warn only)
if (result.tools() != null) {
result.tools().forEach(tool -> ToolNameValidator.validate(tool.name(), false));
}
if (this.enableCallToolSchemaCaching && result.tools() != null) {
// Cache tools output schema
result.tools()

View file

@ -5,29 +5,24 @@
package io.modelcontextprotocol.server;
import io.modelcontextprotocol.common.McpTransportContext;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.ToolNameValidator;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
/**
* Factory class for creating Model Context Protocol (MCP) servers. MCP servers expose
* tools, resources, and prompts to AI models through a standardized interface.
@ -291,6 +286,8 @@ public interface McpServer {
String instructions;
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@ -407,6 +404,18 @@ public interface McpServer {
return this;
}
/**
* Sets whether to use strict tool name validation for this server. When set, this
* takes priority over the system property
* {@code io.modelcontextprotocol.strictToolNameValidation}.
* @param strict true to throw exception on invalid names and false to warn only
* @return This builder instance for method chaining
*/
public AsyncSpecification<S> strictToolNameValidation(boolean strict) {
this.strictToolNameValidation = strict;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -459,6 +468,7 @@ public interface McpServer {
BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<CallToolResult>> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");
validateToolName(tool.name());
assertNoDuplicateTool(tool.name());
this.tools.add(new McpServerFeatures.AsyncToolSpecification(tool, handler));
@ -484,6 +494,7 @@ public interface McpServer {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(callHandler, "Handler must not be null");
validateToolName(tool.name());
assertNoDuplicateTool(tool.name());
this.tools
@ -506,6 +517,7 @@ public interface McpServer {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (var tool : toolSpecifications) {
validateToolName(tool.tool().name());
assertNoDuplicateTool(tool.tool().name());
this.tools.add(tool);
}
@ -533,12 +545,17 @@ public interface McpServer {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (McpServerFeatures.AsyncToolSpecification tool : toolSpecifications) {
validateToolName(tool.tool().name());
assertNoDuplicateTool(tool.tool().name());
this.tools.add(tool);
}
return this;
}
private void validateToolName(String toolName) {
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
}
private void assertNoDuplicateTool(String toolName) {
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
@ -888,6 +905,8 @@ public interface McpServer {
String instructions;
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@ -1008,6 +1027,18 @@ public interface McpServer {
return this;
}
/**
* Sets whether to use strict tool name validation for this server. When set, this
* takes priority over the system property
* {@code io.modelcontextprotocol.strictToolNameValidation}.
* @param strict true to throw exception on invalid names, false to warn only
* @return This builder instance for method chaining
*/
public SyncSpecification<S> strictToolNameValidation(boolean strict) {
this.strictToolNameValidation = strict;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -1059,6 +1090,7 @@ public interface McpServer {
BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");
validateToolName(tool.name());
assertNoDuplicateTool(tool.name());
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, handler));
@ -1083,6 +1115,7 @@ public interface McpServer {
BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");
validateToolName(tool.name());
assertNoDuplicateTool(tool.name());
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, null, handler));
@ -1105,7 +1138,8 @@ public interface McpServer {
for (var tool : toolSpecifications) {
String toolName = tool.tool().name();
assertNoDuplicateTool(toolName); // Check against existing tools
validateToolName(toolName);
assertNoDuplicateTool(toolName);
this.tools.add(tool);
}
@ -1133,12 +1167,17 @@ public interface McpServer {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (McpServerFeatures.SyncToolSpecification tool : toolSpecifications) {
validateToolName(tool.tool().name());
assertNoDuplicateTool(tool.tool().name());
this.tools.add(tool);
}
return this;
}
private void validateToolName(String toolName) {
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
}
private void assertNoDuplicateTool(String toolName) {
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
@ -1434,6 +1473,8 @@ public interface McpServer {
String instructions;
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
@ -1551,6 +1592,18 @@ public interface McpServer {
return this;
}
/**
* Sets whether to use strict tool name validation for this server. When set, this
* takes priority over the system property
* {@code io.modelcontextprotocol.strictToolNameValidation}.
* @param strict true to throw exception on invalid names, false to warn only
* @return This builder instance for method chaining
*/
public StatelessAsyncSpecification strictToolNameValidation(boolean strict) {
this.strictToolNameValidation = strict;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -1589,6 +1642,7 @@ public interface McpServer {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(callHandler, "Handler must not be null");
validateToolName(tool.name());
assertNoDuplicateTool(tool.name());
this.tools.add(new McpStatelessServerFeatures.AsyncToolSpecification(tool, callHandler));
@ -1611,6 +1665,7 @@ public interface McpServer {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (var tool : toolSpecifications) {
validateToolName(tool.tool().name());
assertNoDuplicateTool(tool.tool().name());
this.tools.add(tool);
}
@ -1639,12 +1694,17 @@ public interface McpServer {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (var tool : toolSpecifications) {
validateToolName(tool.tool().name());
assertNoDuplicateTool(tool.tool().name());
this.tools.add(tool);
}
return this;
}
private void validateToolName(String toolName) {
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
}
private void assertNoDuplicateTool(String toolName) {
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
@ -1896,6 +1956,8 @@ public interface McpServer {
String instructions;
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
/**
* 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 +2075,18 @@ public interface McpServer {
return this;
}
/**
* Sets whether to use strict tool name validation for this server. When set, this
* takes priority over the system property
* {@code io.modelcontextprotocol.strictToolNameValidation}.
* @param strict true to throw exception on invalid names, false to warn only
* @return This builder instance for method chaining
*/
public StatelessSyncSpecification strictToolNameValidation(boolean strict) {
this.strictToolNameValidation = strict;
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
@ -2051,6 +2125,7 @@ public interface McpServer {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(callHandler, "Handler must not be null");
validateToolName(tool.name());
assertNoDuplicateTool(tool.name());
this.tools.add(new McpStatelessServerFeatures.SyncToolSpecification(tool, callHandler));
@ -2073,6 +2148,7 @@ public interface McpServer {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (var tool : toolSpecifications) {
validateToolName(tool.tool().name());
assertNoDuplicateTool(tool.tool().name());
this.tools.add(tool);
}
@ -2101,12 +2177,17 @@ public interface McpServer {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (var tool : toolSpecifications) {
validateToolName(tool.tool().name());
assertNoDuplicateTool(tool.tool().name());
this.tools.add(tool);
}
return this;
}
private void validateToolName(String toolName) {
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
}
private void assertNoDuplicateTool(String toolName) {
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");

View file

@ -1466,7 +1466,6 @@ public final class McpSchema {
}
public Tool build() {
Assert.hasText(name, "name must not be empty");
return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta);
}

View file

@ -0,0 +1,83 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.util;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Validates tool names according to the MCP specification.
*
* <p>
* Tool names must conform to the following rules:
* <ul>
* <li>Must be between 1 and 128 characters in length</li>
* <li>May only contain: A-Z, a-z, 0-9, underscore (_), hyphen (-), and dot (.)</li>
* <li>Must not contain spaces, commas, or other special characters</li>
* </ul>
*
* @see <a href=
* "https://modelcontextprotocol.io/specification/draft/server/tools#tool-names">MCP
* Specification - Tool Names</a>
* @author Andrei Shakirin
*/
public final class ToolNameValidator {
private static final Logger logger = LoggerFactory.getLogger(ToolNameValidator.class);
private static final int MAX_LENGTH = 128;
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_\\-.]+$");
/**
* System property for strict tool name validation. Set to "false" to warn only
* instead of throwing exceptions. Default is true (strict).
*/
public static final String STRICT_VALIDATION_PROPERTY = "io.modelcontextprotocol.strictToolNameValidation";
private ToolNameValidator() {
}
/**
* Returns the default strict validation setting from system property.
* @return true if strict validation is enabled (default), false if disabled via
* system property
*/
public static boolean isStrictByDefault() {
return !"false".equalsIgnoreCase(System.getProperty(STRICT_VALIDATION_PROPERTY));
}
/**
* Validates a tool name according to MCP specification.
* @param name the tool name to validate
* @param strict if true, throws exception on invalid name; if false, logs warning
* only
* @throws IllegalArgumentException if validation fails and strict is true
*/
public static void validate(String name, boolean strict) {
if (name == null || name.isEmpty()) {
handleError("Tool name must not be null or empty", name, strict);
}
else if (name.length() > MAX_LENGTH) {
handleError("Tool name must not exceed 128 characters", name, strict);
}
else if (!VALID_NAME_PATTERN.matcher(name).matches()) {
handleError("Tool name contains invalid characters (allowed: A-Z, a-z, 0-9, _, -, .)", name, strict);
}
}
private static void handleError(String message, String name, boolean strict) {
String fullMessage = message + ": '" + name + "'";
if (strict) {
throw new IllegalArgumentException(fullMessage);
}
else {
logger.warn("{}. Processing continues, but tool name should be fixed.", fullMessage);
}
}
}

View file

@ -6,12 +6,19 @@ package io.modelcontextprotocol.server;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.ToolNameValidator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
@ -264,4 +271,60 @@ class AsyncToolSpecificationBuilderTest {
assertThat(McpServerFeatures.AsyncToolSpecification.fromSync(null)).isNull();
}
@Nested
class ToolNameValidation {
private McpServerTransportProvider transportProvider;
@BeforeEach
void setUp() {
transportProvider = mock(McpServerTransportProvider.class);
System.clearProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY);
}
@AfterEach
void tearDown() {
System.clearProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY);
}
@Test
void defaultShouldThrowOnInvalidName() {
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatThrownBy(() -> McpServer.async(transportProvider).tool(invalidTool, (exchange, args) -> null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("invalid characters");
}
@Test
void systemPropertyFalseShouldWarnOnly() {
System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false");
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatCode(() -> McpServer.async(transportProvider).tool(invalidTool, (exchange, args) -> null))
.doesNotThrowAnyException();
}
@Test
void perServerFalseShouldWarnOnly() {
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatCode(() -> McpServer.async(transportProvider)
.strictToolNameValidation(false)
.tool(invalidTool, (exchange, args) -> null)).doesNotThrowAnyException();
}
@Test
void perServerTrueShouldOverrideSystemProperty() {
System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false");
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatThrownBy(() -> McpServer.async(transportProvider)
.strictToolNameValidation(true)
.tool(invalidTool, (exchange, args) -> null)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("invalid characters");
}
}
}

View file

@ -6,11 +6,18 @@ package io.modelcontextprotocol.server;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.ToolNameValidator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
@ -102,4 +109,60 @@ class SyncToolSpecificationBuilderTest {
assertThat(result.isError()).isFalse();
}
@Nested
class ToolNameValidation {
private McpServerTransportProvider transportProvider;
@BeforeEach
void setUp() {
transportProvider = mock(McpServerTransportProvider.class);
System.clearProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY);
}
@AfterEach
void tearDown() {
System.clearProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY);
}
@Test
void defaultShouldThrowOnInvalidName() {
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatThrownBy(() -> McpServer.sync(transportProvider).tool(invalidTool, (exchange, args) -> null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("invalid characters");
}
@Test
void systemPropertyFalseShouldWarnOnly() {
System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false");
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatCode(() -> McpServer.sync(transportProvider).tool(invalidTool, (exchange, args) -> null))
.doesNotThrowAnyException();
}
@Test
void perServerFalseShouldWarnOnly() {
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatCode(() -> McpServer.sync(transportProvider)
.strictToolNameValidation(false)
.tool(invalidTool, (exchange, args) -> null)).doesNotThrowAnyException();
}
@Test
void perServerTrueShouldOverrideSystemProperty() {
System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false");
Tool invalidTool = Tool.builder().name("invalid tool name").build();
assertThatThrownBy(() -> McpServer.sync(transportProvider)
.strictToolNameValidation(true)
.tool(invalidTool, (exchange, args) -> null)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("invalid characters");
}
}
}

View file

@ -1765,4 +1765,14 @@ public class McpSchemaTests {
{"progressToken":"progress-token-789","progress":0.25}"""));
}
// Tool Name Validation Tests
@Test
void testToolBuilderWithValidName() {
McpSchema.Tool tool = McpSchema.Tool.builder().name("valid_tool-name.v1").description("A test tool").build();
assertThat(tool.name()).isEqualTo("valid_tool-name.v1");
assertThat(tool.description()).isEqualTo("A test tool");
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link ToolNameValidator}.
*/
class ToolNameValidatorTests {
@ParameterizedTest
@ValueSource(strings = { "getUser", "DATA_EXPORT_v2", "admin.tools.list", "my-tool", "Tool123", "a", "A",
"_private", "tool_name", "tool-name", "tool.name", "UPPERCASE", "lowercase", "MixedCase123" })
void validToolNames_strictMode(String name) {
assertThatCode(() -> ToolNameValidator.validate(name, true)).doesNotThrowAnyException();
}
@Test
void validToolName_maxLength() {
String name = "a".repeat(128);
assertThatCode(() -> ToolNameValidator.validate(name, true)).doesNotThrowAnyException();
}
@Test
void invalidToolName_null_strictMode() {
assertThatThrownBy(() -> ToolNameValidator.validate(null, true)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("null or empty");
}
@Test
void invalidToolName_empty_strictMode() {
assertThatThrownBy(() -> ToolNameValidator.validate("", true)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("null or empty");
}
@Test
void invalidToolName_tooLong_strictMode() {
String name = "a".repeat(129);
assertThatThrownBy(() -> ToolNameValidator.validate(name, true)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("128 characters");
}
@ParameterizedTest
@ValueSource(strings = { "tool name", // space
"tool,name", // comma
"tool@name", // at sign
"tool#name", // hash
"tool$name", // dollar
"tool%name", // percent
"tool&name", // ampersand
"tool*name", // asterisk
"tool+name", // plus
"tool=name", // equals
"tool/name", // slash
"tool\\name", // backslash
"tool:name", // colon
"tool;name", // semicolon
"tool'name", // single quote
"tool\"name", // double quote
"tool<name", // less than
"tool>name", // greater than
"tool?name", // question mark
"tool!name", // exclamation
"tool(name)", // parentheses
"tool[name]", // brackets
"tool{name}", // braces
"tool|name", // pipe
"tool~name", // tilde
"tool`name", // backtick
"tool^name", // caret
"tööl", // non-ASCII
"工具" // unicode
})
void invalidToolNames_specialCharacters_strictMode(String name) {
assertThatThrownBy(() -> ToolNameValidator.validate(name, true)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("invalid characters");
}
@Test
void invalidToolName_nonStrictMode_doesNotThrow() {
// strict=false means warn only, should not throw
assertThatCode(() -> ToolNameValidator.validate("invalid name", false)).doesNotThrowAnyException();
assertThatCode(() -> ToolNameValidator.validate(null, false)).doesNotThrowAnyException();
assertThatCode(() -> ToolNameValidator.validate("", false)).doesNotThrowAnyException();
assertThatCode(() -> ToolNameValidator.validate("a".repeat(129), false)).doesNotThrowAnyException();
}
}