feat: opt in to output schema validation
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m41s

This commit is contained in:
jade
2026-08-10 09:24:05 +09:00
parent 6809435cc2
commit 7819b11ce3
4 changed files with 40 additions and 41 deletions

View File

@@ -0,0 +1,14 @@
package io.shinhanlife.dap.lib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a response DTO whose generated JSON Schema must be exposed and validated for a Tool response.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface McpOutputSchema {
}

View File

@@ -76,7 +76,8 @@ public class JsonSchemaGenerator {
if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) {
fieldSchema.put("description", schemaAnnotation.description());
}
if (schemaAnnotation.required() && !requiredList.contains(field.getName())) {
if ((schemaAnnotation.required() || schemaAnnotation.requiredMode() == Schema.RequiredMode.REQUIRED)
&& !requiredList.contains(field.getName())) {
requiredList.add(field.getName());
}
if (!schemaAnnotation.pattern().isEmpty()) {

View File

@@ -2,14 +2,13 @@ package io.shinhanlife.dap.lib.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
// removed McpOutputSchema
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import java.io.InputStream;
import java.util.Map;
import org.springframework.core.io.ClassPathResource;
import io.shinhanlife.dap.lib.annotation.ToolHint;
/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */
/** Resolves MCP Tool schemas from resources or DTO metadata. */
public class ToolSchemaResolver {
private final ObjectMapper objectMapper;
@@ -18,7 +17,8 @@ public class ToolSchemaResolver {
this.objectMapper = objectMapper;
}
public Map<String, Object> resolve(org.springaicommunity.mcp.annotation.McpTool function, ToolHint hint, Class<?> requestType) {
public Map<String, Object> resolve(org.springaicommunity.mcp.annotation.McpTool function,
ToolHint hint, Class<?> requestType) {
if (hint != null && !hint.inputSchemaResource().isBlank()) {
return loadResource(hint.inputSchemaResource());
}
@@ -26,12 +26,11 @@ public class ToolSchemaResolver {
}
/**
* Resolves an explicitly declared response schema.
* Response schemas are opt-in so existing tools keep their current response behavior.
* ToolHint.outputSchemaResource()가 있으면 classpath JSON 파일에서 로드하고,
* 없으면 responseType DTO를 분석하여 자동 생성합니다.
* Resolves a response schema only when it is explicitly declared.
* A JSON resource has precedence over a DTO marker annotation.
*/
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, Class<?> responseType, ToolHint hint) {
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function,
Class<?> responseType, ToolHint hint) {
if (hint != null && !hint.outputSchemaResource().isBlank()) {
return loadResource(hint.outputSchemaResource());
}
@@ -39,16 +38,16 @@ public class ToolSchemaResolver {
}
/**
* Resolves an explicitly declared response schema.
* Response schemas are opt-in so existing tools keep their current response behavior.
* Generates a response schema only for DTOs marked with {@link McpOutputSchema}.
*/
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, Class<?> responseType) {
// Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function,
Class<?> responseType) {
if (responseType == null
|| responseType == Object.class
|| Map.class.isAssignableFrom(responseType)
|| responseType == Void.class
|| responseType == void.class) {
|| responseType == void.class
|| !responseType.isAnnotationPresent(McpOutputSchema.class)) {
return Map.of();
}
return JsonSchemaGenerator.generateSchema(responseType);
@@ -58,7 +57,7 @@ public class ToolSchemaResolver {
* Retained for callers that use only explicit output schemas.
*/
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function) {
return resolveOutput(function, null);
return Map.of();
}
private Map<String, Object> loadResource(String location) {
@@ -76,12 +75,4 @@ public class ToolSchemaResolver {
throw new IllegalStateException("Failed to load MCP schema resource: " + location, e);
}
}
private Map<String, Object> parse(String schema, String source) {
try {
return objectMapper.readValue(schema, new TypeReference<>() { });
} catch (Exception e) {
throw new IllegalStateException("Failed to parse MCP input schema from " + source, e);
}
}
}
}