diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index e6a09cd08..93fcc332a 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -30,6 +30,7 @@ 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 @@ private Mono listToolsInternal(Initialization init, S .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() diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java index fe3125271..f4196c0bf 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -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 @@ abstract class AsyncSpecification> { 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 AsyncSpecification instructions(String instructions) { 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 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 AsyncSpecification tool(McpSchema.Tool tool, BiFunction, Mono> 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 AsyncSpecification toolCall(McpSchema.Tool tool, 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 AsyncSpecification tools(List tools(McpServerFeatures.AsyncToolSpecification... t 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 @@ abstract class SyncSpecification> { 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 SyncSpecification instructions(String instructions) { 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 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 SyncSpecification tool(McpSchema.Tool tool, BiFunction, 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 SyncSpecification toolCall(McpSchema.Tool tool, BiFunction 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 SyncSpecification tools(List 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 SyncSpecification tools(McpServerFeatures.SyncToolSpecification... too 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 @@ class StatelessAsyncSpecification { 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 StatelessAsyncSpecification instructions(String instructions) { 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 StatelessAsyncSpecification toolCall(McpSchema.Tool tool, 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 StatelessAsyncSpecification tools( 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 StatelessAsyncSpecification tools( 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 @@ class StatelessSyncSpecification { 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 StatelessSyncSpecification instructions(String instructions) { 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 StatelessSyncSpecification toolCall(McpSchema.Tool tool, 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 StatelessSyncSpecification tools( 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 StatelessSyncSpecification tools( 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."); diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 97bde0b10..50e43107d 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -1466,7 +1466,6 @@ public Builder meta(Map meta) { } public Tool build() { - Assert.hasText(name, "name must not be empty"); return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolNameValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolNameValidator.java new file mode 100644 index 000000000..d7ac18705 --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolNameValidator.java @@ -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. + * + *

+ * Tool names must conform to the following rules: + *

    + *
  • Must be between 1 and 128 characters in length
  • + *
  • May only contain: A-Z, a-z, 0-9, underscore (_), hyphen (-), and dot (.)
  • + *
  • Must not contain spaces, commas, or other special characters
  • + *
+ * + * @see MCP + * Specification - Tool Names + * @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); + } + } + +} diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java index 62332fcdb..b0a5b2b9b 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java @@ -6,12 +6,19 @@ 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 @@ void fromSyncShouldReturnNullWhenSyncSpecIsNull() { 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"); + } + + } + } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java index 9bcd2bc84..e8075eb46 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java @@ -6,11 +6,18 @@ 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 @@ void builtSpecificationShouldExecuteCallToolCorrectly() { 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"); + } + + } + } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 82ffe9ede..45c1c9afc 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -1765,4 +1765,14 @@ void testProgressNotificationWithoutMessage() throws Exception { {"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"); + } + } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/util/ToolNameValidatorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/util/ToolNameValidatorTests.java new file mode 100644 index 000000000..f6eaf5496 --- /dev/null +++ b/mcp-core/src/test/java/io/modelcontextprotocol/util/ToolNameValidatorTests.java @@ -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 + "toolname", // 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(); + } + +}