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:
parent
ef8399c1cb
commit
09a853f555
8 changed files with 413 additions and 13 deletions
|
|
@ -30,6 +30,7 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||||
|
import io.modelcontextprotocol.util.ToolNameValidator;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||||
|
|
@ -656,6 +657,10 @@ public class McpAsyncClient {
|
||||||
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
|
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||||
LIST_TOOLS_RESULT_TYPE_REF)
|
LIST_TOOLS_RESULT_TYPE_REF)
|
||||||
.doOnNext(result -> {
|
.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) {
|
if (this.enableCallToolSchemaCaching && result.tools() != null) {
|
||||||
// Cache tools output schema
|
// Cache tools output schema
|
||||||
result.tools()
|
result.tools()
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,24 @@
|
||||||
package io.modelcontextprotocol.server;
|
package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
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.McpJsonMapper;
|
||||||
|
|
||||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
|
||||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||||
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
|
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
|
||||||
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
|
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
|
import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory;
|
||||||
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
||||||
|
import io.modelcontextprotocol.util.ToolNameValidator;
|
||||||
import reactor.core.publisher.Mono;
|
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
|
* Factory class for creating Model Context Protocol (MCP) servers. MCP servers expose
|
||||||
* tools, resources, and prompts to AI models through a standardized interface.
|
* tools, resources, and prompts to AI models through a standardized interface.
|
||||||
|
|
@ -291,6 +286,8 @@ public interface McpServer {
|
||||||
|
|
||||||
String instructions;
|
String instructions;
|
||||||
|
|
||||||
|
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
||||||
* invoked by language models. Tools enable models to interact with external
|
* invoked by language models. Tools enable models to interact with external
|
||||||
|
|
@ -407,6 +404,18 @@ public interface McpServer {
|
||||||
return this;
|
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
|
* Sets the server capabilities that will be advertised to clients during
|
||||||
* connection initialization. Capabilities define what features the server
|
* connection initialization. Capabilities define what features the server
|
||||||
|
|
@ -459,6 +468,7 @@ public interface McpServer {
|
||||||
BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<CallToolResult>> handler) {
|
BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<CallToolResult>> handler) {
|
||||||
Assert.notNull(tool, "Tool must not be null");
|
Assert.notNull(tool, "Tool must not be null");
|
||||||
Assert.notNull(handler, "Handler must not be null");
|
Assert.notNull(handler, "Handler must not be null");
|
||||||
|
validateToolName(tool.name());
|
||||||
assertNoDuplicateTool(tool.name());
|
assertNoDuplicateTool(tool.name());
|
||||||
|
|
||||||
this.tools.add(new McpServerFeatures.AsyncToolSpecification(tool, handler));
|
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(tool, "Tool must not be null");
|
||||||
Assert.notNull(callHandler, "Handler must not be null");
|
Assert.notNull(callHandler, "Handler must not be null");
|
||||||
|
validateToolName(tool.name());
|
||||||
assertNoDuplicateTool(tool.name());
|
assertNoDuplicateTool(tool.name());
|
||||||
|
|
||||||
this.tools
|
this.tools
|
||||||
|
|
@ -506,6 +517,7 @@ public interface McpServer {
|
||||||
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
||||||
|
|
||||||
for (var tool : toolSpecifications) {
|
for (var tool : toolSpecifications) {
|
||||||
|
validateToolName(tool.tool().name());
|
||||||
assertNoDuplicateTool(tool.tool().name());
|
assertNoDuplicateTool(tool.tool().name());
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
|
|
@ -533,12 +545,17 @@ public interface McpServer {
|
||||||
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
||||||
|
|
||||||
for (McpServerFeatures.AsyncToolSpecification tool : toolSpecifications) {
|
for (McpServerFeatures.AsyncToolSpecification tool : toolSpecifications) {
|
||||||
|
validateToolName(tool.tool().name());
|
||||||
assertNoDuplicateTool(tool.tool().name());
|
assertNoDuplicateTool(tool.tool().name());
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateToolName(String toolName) {
|
||||||
|
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
|
||||||
|
}
|
||||||
|
|
||||||
private void assertNoDuplicateTool(String toolName) {
|
private void assertNoDuplicateTool(String toolName) {
|
||||||
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
||||||
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
||||||
|
|
@ -888,6 +905,8 @@ public interface McpServer {
|
||||||
|
|
||||||
String instructions;
|
String instructions;
|
||||||
|
|
||||||
|
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
||||||
* invoked by language models. Tools enable models to interact with external
|
* invoked by language models. Tools enable models to interact with external
|
||||||
|
|
@ -1008,6 +1027,18 @@ public interface McpServer {
|
||||||
return this;
|
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
|
* Sets the server capabilities that will be advertised to clients during
|
||||||
* connection initialization. Capabilities define what features the server
|
* connection initialization. Capabilities define what features the server
|
||||||
|
|
@ -1059,6 +1090,7 @@ public interface McpServer {
|
||||||
BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> handler) {
|
BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> handler) {
|
||||||
Assert.notNull(tool, "Tool must not be null");
|
Assert.notNull(tool, "Tool must not be null");
|
||||||
Assert.notNull(handler, "Handler must not be null");
|
Assert.notNull(handler, "Handler must not be null");
|
||||||
|
validateToolName(tool.name());
|
||||||
assertNoDuplicateTool(tool.name());
|
assertNoDuplicateTool(tool.name());
|
||||||
|
|
||||||
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, handler));
|
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, handler));
|
||||||
|
|
@ -1083,6 +1115,7 @@ public interface McpServer {
|
||||||
BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler) {
|
BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler) {
|
||||||
Assert.notNull(tool, "Tool must not be null");
|
Assert.notNull(tool, "Tool must not be null");
|
||||||
Assert.notNull(handler, "Handler must not be null");
|
Assert.notNull(handler, "Handler must not be null");
|
||||||
|
validateToolName(tool.name());
|
||||||
assertNoDuplicateTool(tool.name());
|
assertNoDuplicateTool(tool.name());
|
||||||
|
|
||||||
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, null, handler));
|
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, null, handler));
|
||||||
|
|
@ -1105,7 +1138,8 @@ public interface McpServer {
|
||||||
|
|
||||||
for (var tool : toolSpecifications) {
|
for (var tool : toolSpecifications) {
|
||||||
String toolName = tool.tool().name();
|
String toolName = tool.tool().name();
|
||||||
assertNoDuplicateTool(toolName); // Check against existing tools
|
validateToolName(toolName);
|
||||||
|
assertNoDuplicateTool(toolName);
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1133,12 +1167,17 @@ public interface McpServer {
|
||||||
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
||||||
|
|
||||||
for (McpServerFeatures.SyncToolSpecification tool : toolSpecifications) {
|
for (McpServerFeatures.SyncToolSpecification tool : toolSpecifications) {
|
||||||
|
validateToolName(tool.tool().name());
|
||||||
assertNoDuplicateTool(tool.tool().name());
|
assertNoDuplicateTool(tool.tool().name());
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateToolName(String toolName) {
|
||||||
|
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
|
||||||
|
}
|
||||||
|
|
||||||
private void assertNoDuplicateTool(String toolName) {
|
private void assertNoDuplicateTool(String toolName) {
|
||||||
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
||||||
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
||||||
|
|
@ -1434,6 +1473,8 @@ public interface McpServer {
|
||||||
|
|
||||||
String instructions;
|
String instructions;
|
||||||
|
|
||||||
|
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
||||||
* invoked by language models. Tools enable models to interact with external
|
* invoked by language models. Tools enable models to interact with external
|
||||||
|
|
@ -1551,6 +1592,18 @@ public interface McpServer {
|
||||||
return this;
|
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
|
* Sets the server capabilities that will be advertised to clients during
|
||||||
* connection initialization. Capabilities define what features the server
|
* 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(tool, "Tool must not be null");
|
||||||
Assert.notNull(callHandler, "Handler must not be null");
|
Assert.notNull(callHandler, "Handler must not be null");
|
||||||
|
validateToolName(tool.name());
|
||||||
assertNoDuplicateTool(tool.name());
|
assertNoDuplicateTool(tool.name());
|
||||||
|
|
||||||
this.tools.add(new McpStatelessServerFeatures.AsyncToolSpecification(tool, callHandler));
|
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");
|
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
||||||
|
|
||||||
for (var tool : toolSpecifications) {
|
for (var tool : toolSpecifications) {
|
||||||
|
validateToolName(tool.tool().name());
|
||||||
assertNoDuplicateTool(tool.tool().name());
|
assertNoDuplicateTool(tool.tool().name());
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
|
|
@ -1639,12 +1694,17 @@ public interface McpServer {
|
||||||
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
||||||
|
|
||||||
for (var tool : toolSpecifications) {
|
for (var tool : toolSpecifications) {
|
||||||
|
validateToolName(tool.tool().name());
|
||||||
assertNoDuplicateTool(tool.tool().name());
|
assertNoDuplicateTool(tool.tool().name());
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateToolName(String toolName) {
|
||||||
|
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
|
||||||
|
}
|
||||||
|
|
||||||
private void assertNoDuplicateTool(String toolName) {
|
private void assertNoDuplicateTool(String toolName) {
|
||||||
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
||||||
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
||||||
|
|
@ -1896,6 +1956,8 @@ public interface McpServer {
|
||||||
|
|
||||||
String instructions;
|
String instructions;
|
||||||
|
|
||||||
|
boolean strictToolNameValidation = ToolNameValidator.isStrictByDefault();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
||||||
* invoked by language models. Tools enable models to interact with external
|
* invoked by language models. Tools enable models to interact with external
|
||||||
|
|
@ -2013,6 +2075,18 @@ public interface McpServer {
|
||||||
return this;
|
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
|
* Sets the server capabilities that will be advertised to clients during
|
||||||
* connection initialization. Capabilities define what features the server
|
* 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(tool, "Tool must not be null");
|
||||||
Assert.notNull(callHandler, "Handler must not be null");
|
Assert.notNull(callHandler, "Handler must not be null");
|
||||||
|
validateToolName(tool.name());
|
||||||
assertNoDuplicateTool(tool.name());
|
assertNoDuplicateTool(tool.name());
|
||||||
|
|
||||||
this.tools.add(new McpStatelessServerFeatures.SyncToolSpecification(tool, callHandler));
|
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");
|
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
||||||
|
|
||||||
for (var tool : toolSpecifications) {
|
for (var tool : toolSpecifications) {
|
||||||
|
validateToolName(tool.tool().name());
|
||||||
assertNoDuplicateTool(tool.tool().name());
|
assertNoDuplicateTool(tool.tool().name());
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
|
|
@ -2101,12 +2177,17 @@ public interface McpServer {
|
||||||
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
|
||||||
|
|
||||||
for (var tool : toolSpecifications) {
|
for (var tool : toolSpecifications) {
|
||||||
|
validateToolName(tool.tool().name());
|
||||||
assertNoDuplicateTool(tool.tool().name());
|
assertNoDuplicateTool(tool.tool().name());
|
||||||
this.tools.add(tool);
|
this.tools.add(tool);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateToolName(String toolName) {
|
||||||
|
ToolNameValidator.validate(toolName, this.strictToolNameValidation);
|
||||||
|
}
|
||||||
|
|
||||||
private void assertNoDuplicateTool(String toolName) {
|
private void assertNoDuplicateTool(String toolName) {
|
||||||
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) {
|
||||||
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered.");
|
||||||
|
|
|
||||||
|
|
@ -1466,7 +1466,6 @@ public final class McpSchema {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tool build() {
|
public Tool build() {
|
||||||
Assert.hasText(name, "name must not be empty");
|
|
||||||
return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta);
|
return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,19 @@ package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
|
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||||
|
|
@ -264,4 +271,60 @@ class AsyncToolSpecificationBuilderTest {
|
||||||
assertThat(McpServerFeatures.AsyncToolSpecification.fromSync(null)).isNull();
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,18 @@ package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
|
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||||
|
|
@ -102,4 +109,60 @@ class SyncToolSpecificationBuilderTest {
|
||||||
assertThat(result.isError()).isFalse();
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1765,4 +1765,14 @@ public class McpSchemaTests {
|
||||||
{"progressToken":"progress-token-789","progress":0.25}"""));
|
{"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");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue