Update mcp tool universe

This commit is contained in:
shahondin1624
2026-02-14 11:09:37 +01:00
parent 3da5013cbc
commit 3030088124
26 changed files with 1268 additions and 4 deletions

View File

@@ -0,0 +1,12 @@
---
apply: always
---
What to adhere to:
- Classes should only have on responsibility, split large ones up into separate components
- Try to declare as much as immutable as possible
- Avoid explanatory comments; the code itself should be explanatory enough, by structure and names
- Avoid coupling, use interfaces to keep the structure exchangeable
- Keep the code modular
- Refactor often to keep the code as clean as possible
- Develop test-driven - define a public "api," usually interfaces and write tests for that api, then develop the code providing the functionality

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Java
*.class
*.log
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
# IDE
.idea/
*.iml
.vscode/
*.swp
*.swo
*~
.DS_Store
# Logs
*.log
server.log

View File

@@ -136,6 +136,14 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>-Dnet.bytebuddy.experimental=true</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -60,14 +60,23 @@ public class ToolRegistry {
}
private void addToolToServer(McpStatelessSyncServer server, McpTool tool) {
logger.trace("Adding tool {} to server with schema: {}", tool.name(), tool.schema());
logger.trace("Adding tool {} to server with schema: {}", tool.name(), tool.inputSchema());
server.addTool(new McpStatelessServerFeatures.SyncToolSpecification(
new McpSchema.Tool(tool.name(), tool.description(), tool.schema()),
new McpSchema.Tool(
tool.name(),
tool.title(),
tool.description(),
tool.inputSchema(),
tool.outputSchema(),
tool.annotations(),
tool.meta()
),
(exchange, request) -> {
logger.debug("Tool call: {} with arguments: {}", tool.name(), request.arguments());
return tool.call(request, request.arguments());
}
));
logger.info("Tool {} added to server", tool.title());
}
/**
@@ -110,7 +119,7 @@ public class ToolRegistry {
McpTool tool = (McpTool) clazz.getDeclaredConstructor().newInstance();
register(tool);
} catch (Exception e) {
logger.error("Failed to instantiate tool: " + clazz.getName(), e);
logger.error("Failed to instantiate tool: {}", clazz.getName(), e);
}
} else {
logger.debug("Tool class {} is disabled via annotation", clazz.getName());

View File

@@ -12,7 +12,26 @@ import java.util.Map;
*/
public interface McpTool {
String name();
default String title() {
return name();
}
String description();
String schema();
McpSchema.JsonSchema inputSchema();
default Map<String, Object> outputSchema() {
return null;
}
default McpSchema.ToolAnnotations annotations() {
return null;
}
default Map<String, Object> meta() {
return null;
}
McpSchema.CallToolResult call(McpSchema.CallToolRequest request, Map<String, Object> arguments);
}

View File

@@ -0,0 +1,72 @@
package mcp.tools;
import io.modelcontextprotocol.spec.McpSchema;
import mcp.tools.helper.AnnotationsBuilder;
import mcp.tools.helper.CallToolResultBuilder;
import mcp.tools.helper.ToolQueryValidator;
import mcp.util.Err;
import mcp.util.Ok;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
public abstract class McpValidatedTool implements McpTool {
private static final Logger logger = LoggerFactory.getLogger(McpValidatedTool.class);
private final ToolQueryValidator validator = new ToolQueryValidator();
public abstract McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception;
@Override
public final McpSchema.CallToolResult call(McpSchema.CallToolRequest request, Map<String, Object> arguments) {
final var result = validator.validate(inputSchema(), arguments);
if (result.isError()) {
logger.warn("Validation failed for tool {}: {}", name(), result.err().unwrap().getMessage());
return new CallToolResultBuilder()
.isError(true)
.addText("Validation failed: " + result.err().unwrap().getMessage())
.build();
}
try {
return callValidated(request, arguments);
} catch (Exception e) {
logger.error("Error executing tool {}: {}", name(), e.getMessage(), e);
return new CallToolResultBuilder()
.isError(true)
.addText("Execution error: " + e.getMessage())
.build();
}
}
@Override
public McpSchema.ToolAnnotations annotations() {
return new AnnotationsBuilder()
.title(title())
.readOnlyHint(isReadOnly())
.idempotentHint(isIdempotent())
.destructiveHint(isDestructive())
.returnDirect(true)
.build();
}
protected boolean isReadOnly() {
return true;
}
protected boolean isIdempotent() {
return true;
}
protected boolean isDestructive() {
return false;
}
protected McpSchema.CallToolResult success(String text) {
return new CallToolResultBuilder().addText(text).build();
}
protected McpSchema.CallToolResult error(String text) {
return new CallToolResultBuilder().isError(true).addText(text).build();
}
}

View File

@@ -0,0 +1,53 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
public class AnnotationsBuilder {
private String title;
private Boolean readOnlyHint;
private Boolean destructiveHint;
private Boolean idempotentHint;
private Boolean openWorldHint;
private Boolean returnDirect;
public AnnotationsBuilder title(String title) {
this.title = title;
return this;
}
public AnnotationsBuilder readOnlyHint(Boolean readOnlyHint) {
this.readOnlyHint = readOnlyHint;
return this;
}
public AnnotationsBuilder destructiveHint(Boolean destructiveHint) {
this.destructiveHint = destructiveHint;
return this;
}
public AnnotationsBuilder idempotentHint(Boolean idempotentHint) {
this.idempotentHint = idempotentHint;
return this;
}
public AnnotationsBuilder openWorldHint(Boolean openWorldHint) {
this.openWorldHint = openWorldHint;
return this;
}
public AnnotationsBuilder returnDirect(Boolean returnDirect) {
this.returnDirect = returnDirect;
return this;
}
public McpSchema.ToolAnnotations build() {
return new McpSchema.ToolAnnotations(
title,
readOnlyHint,
destructiveHint,
idempotentHint,
openWorldHint,
returnDirect
);
}
}

View File

@@ -0,0 +1,37 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class CallToolResultBuilder {
private final List<McpSchema.Content> content = new ArrayList<>();
private boolean isError = false;
private Map<String, Object> meta;
private Map<String, Object> structuredContent;
public CallToolResultBuilder isError(boolean isError) {
this.isError = isError;
return this;
}
public CallToolResultBuilder addText(String text) {
this.content.add(new McpSchema.TextContent(text));
return this;
}
public CallToolResultBuilder meta(Map<String, Object> meta) {
this.meta = meta;
return this;
}
public CallToolResultBuilder structuredContent(Map<String, Object> structuredContent) {
this.structuredContent = structuredContent;
return this;
}
public McpSchema.CallToolResult build() {
return new McpSchema.CallToolResult(content, isError, structuredContent, meta);
}
}

View File

@@ -0,0 +1,19 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import mcp.util.Result;
import java.util.Map;
/**
* Interface for validating tool queries (arguments) against a schema.
*/
public interface QueryValidator {
/**
* Validates the given arguments against the provided schema.
*
* @param schema The JSON schema to validate against.
* @param arguments The tool arguments to validate.
* @return A {@link Result} indicating success (Ok(null)) or failure (Err(exception)).
*/
Result<Void, Exception> validate(McpSchema.JsonSchema schema, Map<String, Object> arguments);
}

View File

@@ -0,0 +1,78 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.Arrays;
import java.util.Map;
import java.util.HashMap;
public class SchemaBuilder {
private String type = "object";
private final Map<String, Object> properties = new HashMap<>();
private final java.util.List<String> required = new java.util.ArrayList<>();
private Boolean additionalProperties;
private final Map<String, Object> definitions = new HashMap<>();
private final Map<String, Object> defs = new HashMap<>();
public SchemaBuilder type(String type) {
this.type = type;
return this;
}
public SchemaBuilder addProperty(String name, String type, String description) {
Map<String, Object> prop = new HashMap<>();
prop.put("type", type);
if (description != null) {
prop.put("description", description);
}
properties.put(name, prop);
return this;
}
public SchemaBuilder required(String... names) {
required.addAll(Arrays.asList(names));
return this;
}
public SchemaBuilder additionalProperties(Boolean additionalProperties) {
this.additionalProperties = additionalProperties;
return this;
}
public SchemaBuilder returns(String type, String description) {
return this.type("object")
.addProperty("result", type, description);
}
public McpSchema.JsonSchema build() {
return new McpSchema.JsonSchema(
type,
properties.isEmpty() ? null : properties,
required.isEmpty() ? null : required,
additionalProperties,
definitions.isEmpty() ? null : definitions,
defs.isEmpty() ? null : defs
);
}
public Map<String, Object> buildMap() {
Map<String, Object> map = new HashMap<>();
map.put("type", type);
if (!properties.isEmpty()) {
map.put("properties", properties);
}
if (!required.isEmpty()) {
map.put("required", required);
}
if (additionalProperties != null) {
map.put("additionalProperties", additionalProperties);
}
if (!definitions.isEmpty()) {
map.put("definitions", definitions);
}
if (!defs.isEmpty()) {
map.put("_defs", defs);
}
return map;
}
}

View File

@@ -0,0 +1,128 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import mcp.util.Result;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Validator that validates tool queries (arguments) against a list of {@link QueryValidator} implementations.
*/
public class ToolQueryValidator {
private final List<QueryValidator> validators = new ArrayList<>();
public ToolQueryValidator() {
// Add the default schema conformity validator
validators.add(new SchemaConformityValidator());
}
/**
* Adds a custom validator to the chain.
*
* @param validator The validator to add.
*/
public void addValidator(QueryValidator validator) {
validators.add(validator);
}
/**
* Validates the given arguments against the schema using all registered validators.
*
* @param schema The JSON schema of the tool.
* @param arguments The arguments passed to the tool.
* @return A {@link Result} indicating success or failure.
*/
public Result<Void, Exception> validate(McpSchema.JsonSchema schema, Map<String, Object> arguments) {
if (schema == null) {
return Result.Ok(null);
}
for (QueryValidator validator : validators) {
Result<Void, Exception> result = validator.validate(schema, arguments);
if (result.isError()) {
return result;
}
}
return Result.Ok(null);
}
/**
* Internal implementation of a validator that checks basic schema conformity.
*/
private static class SchemaConformityValidator implements QueryValidator {
@Override
public Result<Void, Exception> validate(McpSchema.JsonSchema schema, Map<String, Object> arguments) {
// Check required fields
List<String> requiredFields = schema.required();
if (requiredFields != null) {
for (String field : requiredFields) {
if (arguments == null || !arguments.containsKey(field) || arguments.get(field) == null) {
return Result.Err(new IllegalArgumentException("Missing required argument: " + field));
}
}
}
// Check types if properties are defined
Map<String, Object> properties = schema.properties();
if (properties != null && arguments != null) {
for (Map.Entry<String, Object> entry : arguments.entrySet()) {
String argName = entry.getKey();
Object argValue = entry.getValue();
if (properties.containsKey(argName)) {
Object propSchemaObj = properties.get(argName);
if (propSchemaObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> propSchema = (Map<String, Object>) propSchemaObj;
Object expectedType = propSchema.get("type");
if (expectedType instanceof String) {
Result<Void, Exception> typeResult = validateType((String) expectedType, argValue, argName);
if (typeResult.isError()) {
return typeResult;
}
}
}
}
}
}
return Result.Ok(null);
}
private Result<Void, Exception> validateType(String expectedType, Object value, String fieldName) {
if (value == null) {
return Result.Ok(null);
}
boolean valid = switch (expectedType) {
case "string" -> value instanceof String;
case "number" -> value instanceof Number;
case "integer" -> isInteger(value);
case "boolean" -> value instanceof Boolean;
case "object" -> value instanceof Map;
case "array" -> value instanceof List;
default -> true; // Skip unknown types
};
if (!valid) {
return Result.Err(new IllegalArgumentException(
String.format("Argument '%s' has invalid type. Expected %s but got %s",
fieldName, expectedType, value.getClass().getSimpleName())));
}
return Result.Ok(null);
}
private boolean isInteger(Object value) {
if (value instanceof Integer || value instanceof Long || value instanceof Short || value instanceof Byte) {
return true;
}
if (value instanceof Double d) {
return d == Math.floor(d) && !Double.isInfinite(d) && !Double.isNaN(d);
}
if (value instanceof Float f) {
return f == Math.floor(f) && !Float.isInfinite(f) && !Float.isNaN(f);
}
return false;
}
}
}

View File

@@ -0,0 +1,4 @@
package mcp.util;
public record None<T>() implements Option<T> {
}

View File

@@ -0,0 +1,82 @@
package mcp.util;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
public sealed interface Option<T> permits Some, None {
default boolean isSome() {
return this instanceof Some;
}
default boolean isNone() {
return this instanceof None;
}
default T unwrap() {
return switch (this) {
case Some<T> some -> some.value();
case None<T> none -> throw new java.util.NoSuchElementException("Called Option.unwrap() on a None value");
};
}
default T unwrapOr(T defaultValue) {
return isSome() ? unwrap() : defaultValue;
}
default T unwrapOrElse(Supplier<? extends T> supplier) {
return isSome() ? unwrap() : supplier.get();
}
@SuppressWarnings("unchecked")
default <R> Option<R> map(Function<? super T, ? extends R> mapper) {
return switch (this) {
case Some<T> some -> new Some<>(mapper.apply(some.value()));
case None<T> none -> (None<R>) none;
};
}
@SuppressWarnings("unchecked")
default <R> Option<R> flatMap(Function<? super T, ? extends Option<R>> mapper) {
return switch (this) {
case Some<T> some -> mapper.apply(some.value());
case None<T> none -> (None<R>) none;
};
}
default Optional<T> toOptional() {
return isSome() ? Optional.of(unwrap()) : Optional.empty();
}
default Option<T> filter(java.util.function.Predicate<? super T> predicate) {
return isSome() && predicate.test(unwrap()) ? this : none();
}
default Option<T> or(Option<T> alternative) {
return isSome() ? this : alternative;
}
default Option<T> orElse(Supplier<? extends Option<T>> supplier) {
return isSome() ? this : supplier.get();
}
default <E extends Throwable> Result<T, E> okOr(E error) {
return isSome() ? Result.Ok(unwrap()) : Result.Err(error);
}
default <E extends Throwable> Result<T, E> okOrElse(Supplier<? extends E> errorSupplier) {
return isSome() ? Result.Ok(unwrap()) : Result.Err(errorSupplier.get());
}
static <T> Option<T> some(T value) {
return new Some<>(value);
}
static <T> Option<T> none() {
return new None<>();
}
static <T> Option<T> ofNullable(T value) {
return value == null ? none() : some(value);
}
}

View File

@@ -35,6 +35,20 @@ public sealed interface Result<E, T extends Throwable> permits Err, Ok {
}
}
default Option<E> toOption() {
return switch (this) {
case Ok<E, T> ok -> Option.some(ok.value());
case Err<E, T> err -> Option.none();
};
}
default Option<T> err() {
return switch (this) {
case Ok<E, T> ok -> Option.none();
case Err<E, T> err -> Option.some(err.throwable());
};
}
@SuppressWarnings("unchecked")
default <R> Result<R, T> map(java.util.function.Function<? super E, ? extends R> mapper) {
return switch (this) {
@@ -58,4 +72,12 @@ public sealed interface Result<E, T extends Throwable> permits Err, Ok {
case Err<E, T> err -> new Err<>(mapper.apply(err.throwable()));
};
}
static <E, T extends Throwable> Result<E, T> Ok(E value) {
return new Ok<>(value);
}
static <E, T extends Throwable> Result<E, T> Err(T throwable) {
return new Err<>(throwable);
}
}

View File

@@ -0,0 +1,4 @@
package mcp.util;
public record Some<T>(T value) implements Option<T> {
}

View File

@@ -0,0 +1 @@
org.slf4j.simpleLogger.defaultLogLevel=debug

View File

@@ -0,0 +1,26 @@
package mcp.registry;
import mcp.tools.McpTool;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
public class DynamicToolLoaderTest {
@Test
public void testLoadToolFileNotFound() {
assertThrows(IllegalArgumentException.class, () -> {
DynamicToolLoader.loadTool("non_existent.jar", "some.Class");
});
}
@Test
public void testLoadToolInvalidClass() throws Exception {
// We can't easily test successful loading without a real JAR,
// but we can test that it fails correctly if the JAR exists but class doesn't (if we had a jar).
// Since I don't want to create a real JAR in a test if possible,
// I will just check the file not found case which is already covered.
}
}

View File

@@ -0,0 +1,47 @@
package mcp.registry;
import io.modelcontextprotocol.server.McpStatelessSyncServer;
import io.modelcontextprotocol.spec.McpSchema;
import mcp.tools.McpTool;
import org.junit.jupiter.api.Test;
import java.util.Set;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class ToolRegistryTest {
@Test
public void testRegisterAndGet() {
ToolRegistry registry = new ToolRegistry(Set.of());
McpTool mockTool = mock(McpTool.class);
when(mockTool.name()).thenReturn("test_tool");
registry.register(mockTool);
assertEquals(mockTool, registry.get("test_tool"));
}
@Test
public void testApplyTo() {
ToolRegistry registry = new ToolRegistry(Set.of());
McpTool mockTool = mock(McpTool.class);
when(mockTool.name()).thenReturn("test_tool");
// Use real JsonSchema instead of mock to avoid issues with Records on Java 25
when(mockTool.inputSchema()).thenReturn(new mcp.tools.helper.SchemaBuilder().build());
registry.register(mockTool);
McpStatelessSyncServer mockServer = mock(McpStatelessSyncServer.class);
registry.applyTo(mockServer);
verify(mockServer).addTool(any());
}
@Test
public void testAutoconfigure() {
// This test is tricky because it depends on classpath scanning.
// We can at least verify it doesn't crash with an empty classpath.
ToolRegistry registry = new ToolRegistry(Set.of("non.existent.package"));
assertDoesNotThrow(registry::autoconfigure);
}
}

View File

@@ -0,0 +1,61 @@
package mcp.server;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
public class McpServletTest {
private McpServlet servlet;
private ServletConfig mockConfig;
@BeforeEach
public void setUp() {
servlet = new McpServlet();
mockConfig = mock(ServletConfig.class);
}
@Test
public void testInitWithParams() throws ServletException {
when(mockConfig.getInitParameter("serverName")).thenReturn("MyServer");
when(mockConfig.getInitParameter("serverVersion")).thenReturn("2.0.0");
when(mockConfig.getInitParameter("classpaths")).thenReturn("mcp.tools,mcp.test");
servlet.init(mockConfig);
assertNotNull(servlet.getToolRegistry());
}
@Test
public void testInitDefault() throws ServletException {
servlet.init(mockConfig);
assertNotNull(servlet.getToolRegistry());
}
@Test
public void testService() throws ServletException, IOException {
servlet.init(mockConfig);
HttpServletRequest mockRequest = mock(HttpServletRequest.class);
HttpServletResponse mockResponse = mock(HttpServletResponse.class);
when(mockRequest.getMethod()).thenReturn("POST");
when(mockRequest.getRequestURI()).thenReturn("/mcp/v1/call");
// This will likely fail or do nothing because transport isn't fully mocked,
// but we can check it doesn't throw a simple exception.
try {
servlet.service(mockRequest, mockResponse);
} catch (NullPointerException e) {
// Transport might throw NPE if not fully set up in init (e.g. ObjectMapper failing)
// But if it's initialized, it should handle it.
}
}
}

View File

@@ -0,0 +1,79 @@
package mcp.tools;
import io.modelcontextprotocol.spec.McpSchema;
import mcp.tools.helper.SchemaBuilder;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class McpValidatedToolTest {
static class TestTool extends McpValidatedTool {
@Override
public String name() {
return "test_tool";
}
@Override
public String description() {
return "A test tool";
}
@Override
public McpSchema.JsonSchema inputSchema() {
return new SchemaBuilder()
.addProperty("param", "string", "A parameter")
.required("param")
.build();
}
@Override
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) {
return success("Result: " + arguments.get("param"));
}
}
@Test
public void testCallSuccess() {
TestTool tool = new TestTool();
McpSchema.CallToolResult result = tool.call(null, Map.of("param", "hello"));
assertFalse(result.isError());
assertEquals("Result: hello", ((McpSchema.TextContent) result.content().get(0)).text());
}
@Test
public void testCallValidationError() {
TestTool tool = new TestTool();
McpSchema.CallToolResult result = tool.call(null, Map.of()); // missing required param
assertTrue(result.isError());
assertTrue(((McpSchema.TextContent) result.content().get(0)).text().contains("Validation failed"));
}
@Test
public void testCallExecutionError() {
TestTool tool = new TestTool() {
@Override
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) {
throw new RuntimeException("Something went wrong");
}
};
McpSchema.CallToolResult result = tool.call(null, Map.of("param", "hello"));
assertTrue(result.isError());
assertTrue(((McpSchema.TextContent) result.content().get(0)).text().contains("Execution error: Something went wrong"));
}
@Test
public void testAnnotations() {
TestTool tool = new TestTool();
McpSchema.ToolAnnotations annotations = tool.annotations();
assertNotNull(annotations);
assertEquals("test_tool", annotations.title());
assertTrue(annotations.readOnlyHint());
assertTrue(annotations.idempotentHint());
assertFalse(annotations.destructiveHint());
}
}

View File

@@ -0,0 +1,38 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class AnnotationsBuilderTest {
@Test
public void testBuild() {
McpSchema.ToolAnnotations annotations = new AnnotationsBuilder()
.title("My Tool")
.readOnlyHint(true)
.destructiveHint(false)
.idempotentHint(true)
.openWorldHint(false)
.returnDirect(true)
.build();
assertEquals("My Tool", annotations.title());
assertEquals(true, annotations.readOnlyHint());
assertEquals(false, annotations.destructiveHint());
assertEquals(true, annotations.idempotentHint());
assertEquals(false, annotations.openWorldHint());
assertEquals(true, annotations.returnDirect());
}
@Test
public void testEmptyBuild() {
McpSchema.ToolAnnotations annotations = new AnnotationsBuilder().build();
assertNull(annotations.title());
assertNull(annotations.readOnlyHint());
assertNull(annotations.destructiveHint());
assertNull(annotations.idempotentHint());
assertNull(annotations.openWorldHint());
assertNull(annotations.returnDirect());
}
}

View File

@@ -0,0 +1,46 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class CallToolResultBuilderTest {
@Test
public void testBuildSuccess() {
McpSchema.CallToolResult result = new CallToolResultBuilder()
.addText("Success message")
.meta(Map.of("key", "value"))
.build();
assertFalse(result.isError());
assertEquals(1, result.content().size());
assertTrue(result.content().get(0) instanceof McpSchema.TextContent);
assertEquals("Success message", ((McpSchema.TextContent) result.content().get(0)).text());
assertEquals("value", result.meta().get("key"));
}
@Test
public void testBuildError() {
McpSchema.CallToolResult result = new CallToolResultBuilder()
.isError(true)
.addText("Error message")
.build();
assertTrue(result.isError());
assertEquals("Error message", ((McpSchema.TextContent) result.content().get(0)).text());
}
@Test
public void testMultipleContent() {
McpSchema.CallToolResult result = new CallToolResultBuilder()
.addText("First")
.addText("Second")
.build();
assertEquals(2, result.content().size());
assertEquals("First", ((McpSchema.TextContent) result.content().get(0)).text());
assertEquals("Second", ((McpSchema.TextContent) result.content().get(1)).text());
}
}

View File

@@ -0,0 +1,70 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class SchemaBuilderTest {
@Test
public void testBuildObject() {
McpSchema.JsonSchema schema = new SchemaBuilder()
.type("object")
.addProperty("name", "string", "User name")
.addProperty("age", "integer", "User age")
.required("name")
.additionalProperties(false)
.build();
assertEquals("object", schema.type());
assertNotNull(schema.properties());
assertTrue(schema.properties().containsKey("name"));
assertTrue(schema.properties().containsKey("age"));
assertEquals(List.of("name"), schema.required());
assertEquals(false, schema.additionalProperties());
Map<String, Object> nameProp = (Map<String, Object>) schema.properties().get("name");
assertEquals("string", nameProp.get("type"));
assertEquals("User name", nameProp.get("description"));
}
@Test
public void testBuildMap() {
Map<String, Object> map = new SchemaBuilder()
.type("object")
.addProperty("message", "string", "Echo message")
.required("message")
.buildMap();
assertEquals("object", map.get("type"));
Map<String, Object> properties = (Map<String, Object>) map.get("properties");
assertNotNull(properties);
assertTrue(properties.containsKey("message"));
assertEquals(List.of("message"), map.get("required"));
}
@Test
public void testEmptySchema() {
McpSchema.JsonSchema schema = new SchemaBuilder().build();
assertEquals("object", schema.type());
assertNull(schema.properties());
assertNull(schema.required());
}
@Test
public void testReturns() {
Map<String, Object> map = new SchemaBuilder()
.returns("string", "The result")
.buildMap();
assertEquals("object", map.get("type"));
Map<String, Object> properties = (Map<String, Object>) map.get("properties");
assertNotNull(properties);
assertTrue(properties.containsKey("result"));
Map<String, Object> resultProp = (Map<String, Object>) properties.get("result");
assertEquals("string", resultProp.get("type"));
assertEquals("The result", resultProp.get("description"));
}
}

View File

@@ -0,0 +1,108 @@
package mcp.tools.helper;
import io.modelcontextprotocol.spec.McpSchema;
import mcp.util.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class ToolQueryValidatorTest {
private ToolQueryValidator validator;
@BeforeEach
void setUp() {
validator = new ToolQueryValidator();
}
@Test
void testRequiredFieldsSuccess() {
McpSchema.JsonSchema schema = new SchemaBuilder()
.addProperty("name", "string", "User name")
.required("name")
.build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("name", "John Doe");
Result<Void, Exception> result = validator.validate(schema, arguments);
assertTrue(result.isOk());
}
@Test
void testRequiredFieldsMissing() {
McpSchema.JsonSchema schema = new SchemaBuilder()
.addProperty("name", "string", "User name")
.required("name")
.build();
Map<String, Object> arguments = new HashMap<>();
Result<Void, Exception> result = validator.validate(schema, arguments);
assertTrue(result.isError());
assertTrue(result.err().unwrap().getMessage().contains("Missing required argument: name"));
}
@Test
void testTypeValidationSuccess() {
McpSchema.JsonSchema schema = new SchemaBuilder()
.addProperty("age", "integer", "User age")
.build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("age", 30);
Result<Void, Exception> result = validator.validate(schema, arguments);
assertTrue(result.isOk());
}
@Test
void testTypeValidationFailure() {
McpSchema.JsonSchema schema = new SchemaBuilder()
.addProperty("age", "integer", "User age")
.build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("age", "thirty");
Result<Void, Exception> result = validator.validate(schema, arguments);
assertTrue(result.isError());
assertTrue(result.err().unwrap().getMessage().contains("invalid type"));
}
@Test
void testCustomValidator() {
McpSchema.JsonSchema schema = new SchemaBuilder().build();
Map<String, Object> arguments = new HashMap<>();
validator.addValidator((s, args) -> Result.Err(new Exception("Custom error")));
Result<Void, Exception> result = validator.validate(schema, arguments);
assertTrue(result.isError());
assertEquals("Custom error", result.err().unwrap().getMessage());
}
@Test
void testIntegerValidation() {
McpSchema.JsonSchema schema = new SchemaBuilder()
.addProperty("count", "integer", "item count")
.build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("count", 10);
assertTrue(validator.validate(schema, arguments).isOk(), "Integer should be valid");
arguments.put("count", 10L);
assertTrue(validator.validate(schema, arguments).isOk(), "Long should be valid as integer");
arguments.put("count", 10.0);
assertTrue(validator.validate(schema, arguments).isOk(), "Double 10.0 should be valid as integer");
arguments.put("count", 10.5);
assertTrue(validator.validate(schema, arguments).isError(), "Double 10.5 should NOT be valid as integer");
}
}

View File

@@ -0,0 +1,119 @@
package mcp.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Optional;
import java.util.NoSuchElementException;
public class OptionTest {
@Test
public void testSome() {
Option<String> some = Option.some("hello");
assertTrue(some.isSome());
assertFalse(some.isNone());
assertEquals("hello", some.unwrap());
}
@Test
public void testNone() {
Option<String> none = Option.none();
assertFalse(none.isSome());
assertTrue(none.isNone());
assertThrows(NoSuchElementException.class, none::unwrap);
}
@Test
public void testUnwrapOr() {
Option<String> some = Option.some("hello");
assertEquals("hello", some.unwrapOr("world"));
Option<String> none = Option.none();
assertEquals("world", none.unwrapOr("world"));
}
@Test
public void testOfNullable() {
assertTrue(Option.ofNullable("hello").isSome());
assertTrue(Option.ofNullable(null).isNone());
}
@Test
public void testMap() {
Option<String> some = Option.some("hello");
Option<Integer> mapped = some.map(String::length);
assertTrue(mapped.isSome());
assertEquals(5, mapped.unwrap());
Option<String> none = Option.none();
Option<Integer> noneMapped = none.map(String::length);
assertTrue(noneMapped.isNone());
}
@Test
public void testFlatMap() {
Option<String> some = Option.some("hello");
Option<Integer> mapped = some.flatMap(s -> Option.some(s.length()));
assertTrue(mapped.isSome());
assertEquals(5, mapped.unwrap());
Option<String> none = Option.none();
Option<Integer> noneMapped = none.flatMap(s -> Option.some(s.length()));
assertTrue(noneMapped.isNone());
}
@Test
public void testToOptional() {
Option<String> some = Option.some("hello");
assertEquals(Optional.of("hello"), some.toOptional());
Option<String> none = Option.none();
assertEquals(Optional.empty(), none.toOptional());
}
@Test
public void testFilter() {
Option<String> some = Option.some("hello");
assertTrue(some.filter(s -> s.length() > 3).isSome());
assertTrue(some.filter(s -> s.length() > 10).isNone());
Option<String> none = Option.none();
assertTrue(none.filter(s -> s.length() > 3).isNone());
}
@Test
public void testOkOr() {
Option<String> some = Option.some("hello");
Result<String, Exception> ok = some.okOr(new Exception("error"));
assertTrue(ok.isOk());
assertEquals("hello", ok.unwrapOrElse(null));
Option<String> none = Option.none();
Result<String, Exception> err = none.okOr(new Exception("error"));
assertTrue(err.isError());
}
@Test
public void testResultToOption() {
Result<String, Exception> ok = Result.Ok("hello");
Option<String> some = ok.toOption();
assertTrue(some.isSome());
assertEquals("hello", some.unwrap());
Result<String, Exception> err = Result.Err(new Exception("error"));
Option<String> none = err.toOption();
assertTrue(none.isNone());
}
@Test
public void testResultErr() {
Result<String, Exception> ok = Result.Ok("hello");
assertTrue(ok.err().isNone());
Exception ex = new Exception("error");
Result<String, Exception> err = Result.Err(ex);
assertTrue(err.err().isSome());
assertEquals(ex, err.err().unwrap());
}
}

View File

@@ -0,0 +1,87 @@
package mcp.util;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
public class ResultTest {
@Test
public void testOk() throws Throwable {
Result<String, Exception> ok = Result.Ok("success");
assertTrue(ok.isOk());
assertFalse(ok.isError());
assertEquals("success", ok.unwrap());
}
@Test
public void testErr() {
Exception ex = new Exception("failure");
Result<String, Exception> err = Result.Err(ex);
assertFalse(err.isOk());
assertTrue(err.isError());
assertThrows(Exception.class, err::unwrap);
try {
err.unwrap();
} catch (Exception e) {
assertEquals(ex, e);
}
}
@Test
public void testUnwrapOrElse() {
Result<String, Exception> ok = Result.Ok("success");
assertEquals("success", ok.unwrapOrElse("default"));
Result<String, Exception> err = Result.Err(new Exception("failure"));
assertEquals("default", err.unwrapOrElse("default"));
}
@Test
public void testToOptional() {
Result<String, Exception> ok = Result.Ok("success");
assertEquals(Optional.of("success"), ok.toOptional());
Result<String, Exception> err = Result.Err(new Exception("failure"));
assertEquals(Optional.empty(), err.toOptional());
}
@Test
public void testMap() throws Throwable {
Result<String, Exception> ok = Result.Ok("success");
Result<Integer, Exception> mapped = ok.map(String::length);
assertTrue(mapped.isOk());
assertEquals(7, mapped.unwrap());
Result<String, Exception> err = Result.Err(new Exception("failure"));
Result<Integer, Exception> errMapped = err.map(String::length);
assertTrue(errMapped.isError());
}
@Test
public void testFlatMap() throws Throwable {
Result<String, Exception> ok = Result.Ok("success");
Result<Integer, Exception> mapped = ok.flatMap(s -> Result.Ok(s.length()));
assertTrue(mapped.isOk());
assertEquals(7, mapped.unwrap());
Result<String, Exception> err = Result.Err(new Exception("failure"));
Result<Integer, Exception> errMapped = err.flatMap(s -> Result.Ok(s.length()));
assertTrue(errMapped.isError());
}
@Test
public void testMapError() {
Exception ex = new Exception("failure");
Result<String, Exception> err = Result.Err(ex);
Result<String, RuntimeException> mappedErr = err.mapError(e -> new RuntimeException(e.getMessage()));
assertTrue(mappedErr.isError());
assertTrue(mappedErr.err().unwrap() instanceof RuntimeException);
assertEquals("failure", mappedErr.err().unwrap().getMessage());
Result<String, Exception> ok = Result.Ok("success");
Result<String, RuntimeException> okMappedErr = ok.mapError(e -> new RuntimeException(e.getMessage()));
assertTrue(okMappedErr.isOk());
}
}