Update mcp tool universe
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
72
src/main/java/mcp/tools/McpValidatedTool.java
Normal file
72
src/main/java/mcp/tools/McpValidatedTool.java
Normal 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();
|
||||
}
|
||||
}
|
||||
53
src/main/java/mcp/tools/helper/AnnotationsBuilder.java
Normal file
53
src/main/java/mcp/tools/helper/AnnotationsBuilder.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/main/java/mcp/tools/helper/CallToolResultBuilder.java
Normal file
37
src/main/java/mcp/tools/helper/CallToolResultBuilder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/main/java/mcp/tools/helper/QueryValidator.java
Normal file
19
src/main/java/mcp/tools/helper/QueryValidator.java
Normal 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);
|
||||
}
|
||||
78
src/main/java/mcp/tools/helper/SchemaBuilder.java
Normal file
78
src/main/java/mcp/tools/helper/SchemaBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
128
src/main/java/mcp/tools/helper/ToolQueryValidator.java
Normal file
128
src/main/java/mcp/tools/helper/ToolQueryValidator.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/main/java/mcp/util/None.java
Normal file
4
src/main/java/mcp/util/None.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package mcp.util;
|
||||
|
||||
public record None<T>() implements Option<T> {
|
||||
}
|
||||
82
src/main/java/mcp/util/Option.java
Normal file
82
src/main/java/mcp/util/Option.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
4
src/main/java/mcp/util/Some.java
Normal file
4
src/main/java/mcp/util/Some.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package mcp.util;
|
||||
|
||||
public record Some<T>(T value) implements Option<T> {
|
||||
}
|
||||
Reference in New Issue
Block a user