feat: opt in to output schema validation
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m41s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m41s
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user