forked from kimhyungsik/ax_hub_mcp_tool
Refactor: Switch schema validator to MCP SDK DefaultJsonSchemaValidator (JSON Schema 2020-12) to match dapms
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package io.shinhanlife.dap.lib.config;
|
package io.shinhanlife.dap.lib.config;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
|
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -18,7 +20,13 @@ public class ToolSchemaConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ToolArgumentSchemaValidator toolArgumentSchemaValidator(ObjectMapper objectMapper) {
|
public JsonSchemaValidator mcpJsonSchemaValidator() {
|
||||||
return new ToolArgumentSchemaValidator(objectMapper);
|
return new DefaultJsonSchemaValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ToolArgumentSchemaValidator toolArgumentSchemaValidator(ObjectMapper objectMapper,
|
||||||
|
JsonSchemaValidator jsonSchemaValidator) {
|
||||||
|
return new ToolArgumentSchemaValidator(objectMapper, jsonSchemaValidator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.lib.mcp;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.Error;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -61,8 +60,8 @@ public class McpToolExecutionService {
|
|||||||
if (tool.method().getParameterCount() == 0 || Map.class.isAssignableFrom(tool.method().getParameterTypes()[0])) return null;
|
if (tool.method().getParameterCount() == 0 || Map.class.isAssignableFrom(tool.method().getParameterTypes()[0])) return null;
|
||||||
try {
|
try {
|
||||||
Map<String, Object> schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]);
|
Map<String, Object> schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]);
|
||||||
List<Error> errors = toolArgumentSchemaValidator.validate(schema, arguments);
|
ValidationResponse result = toolArgumentSchemaValidator.validate(schema, arguments);
|
||||||
return errors.isEmpty() ? null : error(422, "INVALID_PARAM", "Tool arguments do not match the input schema", requestId);
|
return result.valid() ? null : error(422, "INVALID_PARAM", "Tool arguments do not match the input schema", requestId);
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
log.error("[Tool] Input schema validation failed unexpectedly. tool={}", tool.annotation().name(), error);
|
log.error("[Tool] Input schema validation failed unexpectedly. tool={}", tool.annotation().name(), error);
|
||||||
return null;
|
return null;
|
||||||
@@ -82,7 +81,7 @@ public class McpToolExecutionService {
|
|||||||
Object methodResult, String requestId) {
|
Object methodResult, String requestId) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(tool.annotation(), tool.method().getReturnType(), tool.hint());
|
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(tool.annotation(), tool.method().getReturnType(), tool.hint());
|
||||||
if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).isEmpty()) {
|
if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).valid()) {
|
||||||
return error(500, "INVALID_TOOL_RESPONSE", "Tool response does not match its output schema", requestId);
|
return error(500, "INVALID_TOOL_RESPONSE", "Tool response does not match its output schema", requestId);
|
||||||
}
|
}
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
package io.shinhanlife.dap.lib.validation;
|
package io.shinhanlife.dap.lib.validation;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.Error;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
import com.networknt.schema.InputFormat;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
|
||||||
import com.networknt.schema.Schema;
|
|
||||||
import com.networknt.schema.SchemaRegistry;
|
|
||||||
import com.networknt.schema.SpecificationVersion;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/** Validates tool arguments with the NetworkNT version selected by the MCP SDK. */
|
/** Validates tool arguments with the MCP SDK JsonSchemaValidator (JSON Schema 2020-12). */
|
||||||
public class ToolArgumentSchemaValidator {
|
public class ToolArgumentSchemaValidator {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final JsonSchemaValidator jsonSchemaValidator;
|
||||||
|
|
||||||
public ToolArgumentSchemaValidator(ObjectMapper objectMapper) {
|
public ToolArgumentSchemaValidator(ObjectMapper objectMapper, JsonSchemaValidator jsonSchemaValidator) {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
|
this.jsonSchemaValidator = jsonSchemaValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Error> validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) throws Exception {
|
public ValidationResponse validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) {
|
||||||
return validateValue(schemaDefinition, arguments);
|
return validateValue(schemaDefinition, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Error> validateValue(Map<String, Object> schemaDefinition, Object value) throws Exception {
|
public ValidationResponse validateValue(Map<String, Object> schemaDefinition, Object value) {
|
||||||
SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
|
Object converted = objectMapper.convertValue(value, Object.class);
|
||||||
Schema schema = schemaRegistry.getSchema(objectMapper.writeValueAsString(schemaDefinition));
|
return jsonSchemaValidator.validate(schemaDefinition, converted);
|
||||||
return schema.validate(objectMapper.writeValueAsString(value), InputFormat.JSON);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.shinhanlife.dap.lib.config;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
@@ -17,6 +18,7 @@ class ToolSchemaConfigurationTest {
|
|||||||
context.refresh();
|
context.refresh();
|
||||||
|
|
||||||
assertNotNull(context.getBean(ToolArgumentSchemaValidator.class));
|
assertNotNull(context.getBean(ToolArgumentSchemaValidator.class));
|
||||||
|
assertNotNull(context.getBean(JsonSchemaValidator.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package io.shinhanlife.dap.lib.mcp;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||||
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
|
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springaicommunity.mcp.annotation.McpTool;
|
import org.springaicommunity.mcp.annotation.McpTool;
|
||||||
import org.springaicommunity.mcp.annotation.McpToolParam;
|
import org.springaicommunity.mcp.annotation.McpToolParam;
|
||||||
@@ -67,7 +68,8 @@ class McpToolExecutionServiceTest {
|
|||||||
McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties());
|
McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties());
|
||||||
registry.initialize();
|
registry.initialize();
|
||||||
return new McpToolExecutionService(registry, objectMapper,
|
return new McpToolExecutionService(registry, objectMapper,
|
||||||
new ToolArgumentSchemaValidator(objectMapper), new ToolSchemaResolver(objectMapper));
|
new ToolArgumentSchemaValidator(objectMapper, new DefaultJsonSchemaValidator()),
|
||||||
|
new ToolSchemaResolver(objectMapper));
|
||||||
}
|
}
|
||||||
|
|
||||||
static class EchoTool {
|
static class EchoTool {
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.Error;
|
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||||
import com.networknt.schema.InputFormat;
|
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||||
import com.networknt.schema.Schema;
|
|
||||||
import com.networknt.schema.SchemaRegistry;
|
|
||||||
import com.networknt.schema.SpecificationVersion;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -47,7 +44,7 @@ class JsonSchemaGeneratorTest {
|
|||||||
|
|
||||||
assertEquals("object", childSchema.get("type"));
|
assertEquals("object", childSchema.get("type"));
|
||||||
assertTrue(required(childSchema).contains("businessDate"));
|
assertTrue(required(childSchema).contains("businessDate"));
|
||||||
assertEquals("^\\d{8}$", property(childSchema, "businessDate").get("pattern"));
|
assertEquals("^\\\\d{8}$", property(childSchema, "businessDate").get("pattern"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -62,12 +59,13 @@ class JsonSchemaGeneratorTest {
|
|||||||
@Test
|
@Test
|
||||||
void validatorRejectsInvalidNestedValue() throws Exception {
|
void validatorRejectsInvalidNestedValue() throws Exception {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7)
|
JsonSchemaValidator validator = new DefaultJsonSchemaValidator();
|
||||||
.getSchema(objectMapper.writeValueAsString(JsonSchemaGenerator.generateSchema(NestedRequest.class)));
|
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(NestedRequest.class);
|
||||||
List<Error> errors = schema.validate(objectMapper.writeValueAsString(Map.of(
|
Object arguments = objectMapper.convertValue(
|
||||||
"child", Map.of("businessDate", "2026-07-28"))), InputFormat.JSON);
|
Map.of("child", Map.of("businessDate", "2026-07-28")), Object.class);
|
||||||
|
JsonSchemaValidator.ValidationResponse result = validator.validate(schema, arguments);
|
||||||
|
|
||||||
assertFalse(errors.isEmpty());
|
assertFalse(result.valid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -124,7 +122,7 @@ class JsonSchemaGeneratorTest {
|
|||||||
private static class NestedChild {
|
private static class NestedChild {
|
||||||
@io.swagger.v3.oas.annotations.media.Schema(
|
@io.swagger.v3.oas.annotations.media.Schema(
|
||||||
requiredMode = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED,
|
requiredMode = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED,
|
||||||
pattern = "^\\d{8}$")
|
pattern = "^\\\\d{8}$")
|
||||||
private String businessDate;
|
private String businessDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -11,16 +12,17 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
class ToolArgumentSchemaValidatorTest {
|
class ToolArgumentSchemaValidatorTest {
|
||||||
|
|
||||||
private final ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
|
private final ToolArgumentSchemaValidator validator =
|
||||||
|
new ToolArgumentSchemaValidator(new ObjectMapper(), new DefaultJsonSchemaValidator());
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validatesDraft7SchemaWithTheRuntimeNetworkntVersion() throws Exception {
|
void validatesJsonSchema202012WithTheMcpSdkValidator() {
|
||||||
Map<String, Object> schema = Map.of(
|
Map<String, Object> schema = Map.of(
|
||||||
"type", "object",
|
"type", "object",
|
||||||
"properties", Map.of("name", Map.of("type", "string")),
|
"properties", Map.of("name", Map.of("type", "string")),
|
||||||
"required", List.of("name"));
|
"required", List.of("name"));
|
||||||
|
|
||||||
assertTrue(validator.validate(schema, Map.of("name", "Hong")).isEmpty());
|
assertTrue(validator.validate(schema, Map.of("name", "Hong")).valid());
|
||||||
assertFalse(validator.validate(schema, Map.of()).isEmpty());
|
assertFalse(validator.validate(schema, Map.of()).valid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user