716 lines
34 KiB
Diff
716 lines
34 KiB
Diff
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
|
||
index c7251dd..6ee5f33 100644
|
||
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
|
||
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
|
||
@@ -39,6 +39,17 @@ public @interface McpFunction {
|
||
*/
|
||
// 異붽?: Redis ?먮룞 ?깅줉 諛?Heartbeat ????щ? ?쒖뼱
|
||
String inputSchemaResource() default "";
|
||
+
|
||
+ /**
|
||
+ * Tool response JSON Schema. When unset, output validation is skipped.
|
||
+ */
|
||
+ String outputSchema() default "{}";
|
||
+
|
||
+ /**
|
||
+ * Classpath resource for a complex Tool response JSON Schema.
|
||
+ * This value has priority over outputSchema.
|
||
+ */
|
||
+ String outputSchemaResource() default "";
|
||
boolean register() default false;
|
||
|
||
// 異붽?: ??紐⑸줉 ?몄텧 ?щ? ?쒖뼱 (false ???쇱슦?낆? ?섎굹 紐⑸줉?먯꽌 ?④?)
|
||
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java
|
||
new file mode 100644
|
||
index 0000000..cfb37dc
|
||
--- /dev/null
|
||
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java
|
||
@@ -0,0 +1,17 @@
|
||
+package io.shinhanlife.dap.lib.annotation;
|
||
+
|
||
+import java.lang.annotation.Documented;
|
||
+import java.lang.annotation.ElementType;
|
||
+import java.lang.annotation.Retention;
|
||
+import java.lang.annotation.RetentionPolicy;
|
||
+import java.lang.annotation.Target;
|
||
+
|
||
+/**
|
||
+ * Marks a Tool response DTO for automatic output JSON Schema generation.
|
||
+ * Field constraints are declared with {@link McpValidation}.
|
||
+ */
|
||
+@Target(ElementType.TYPE)
|
||
+@Retention(RetentionPolicy.RUNTIME)
|
||
+@Documented
|
||
+public @interface McpOutputSchema {
|
||
+}
|
||
\ No newline at end of file
|
||
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
|
||
index ecf23c8..4353edf 100644
|
||
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
|
||
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
|
||
@@ -31,6 +31,6 @@ public @interface McpValidation {
|
||
int maxLength() default -1;
|
||
String[] allowedValues() default {};
|
||
String format() default "";
|
||
- String defaultValue() default "";
|
||
+ boolean nullable() default false; String defaultValue() default "";
|
||
String[] examples() default {};
|
||
}
|
||
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
|
||
index a90efa0..d95dae6 100644
|
||
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
|
||
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
|
||
@@ -103,6 +103,14 @@ public class JsonSchemaGenerator {
|
||
if (validation != null && validation.examples().length > 0) {
|
||
fieldSchema.put("examples", List.of(validation.examples()));
|
||
}
|
||
+ if (validation != null && validation.nullable()) {
|
||
+ Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
|
||
+ fieldSchema = new HashMap<>();
|
||
+ fieldSchema.put("anyOf", List.of(
|
||
+ nonNullSchema,
|
||
+ Map.of("type", "null")
|
||
+ ));
|
||
+ }
|
||
|
||
properties.put(field.getName(), fieldSchema);
|
||
}
|
||
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
|
||
index 787cbc1..5ea54df 100644
|
||
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
|
||
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
|
||
@@ -3,7 +3,7 @@ package io.shinhanlife.dap.lib.util;
|
||
import com.fasterxml.jackson.core.type.TypeReference;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||
-import java.io.InputStream;
|
||
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream;
|
||
import java.util.Map;
|
||
import org.springframework.core.io.ClassPathResource;
|
||
|
||
@@ -27,19 +27,44 @@ public class ToolSchemaResolver {
|
||
return JsonSchemaGenerator.generateSchema(requestType);
|
||
}
|
||
|
||
+ /**
|
||
+ * Resolves an explicitly declared response schema.
|
||
+ * Response schemas are opt-in so existing tools keep their current response behavior.
|
||
+ */
|
||
+ public Map<String, Object> resolveOutput(McpFunction function, Class<?> responseType) {
|
||
+ if (function != null && !function.outputSchemaResource().isBlank()) {
|
||
+ return loadResource(function.outputSchemaResource());
|
||
+ }
|
||
+ if (function != null && !function.outputSchema().isBlank()
|
||
+ && !"{}".equals(function.outputSchema().trim())) {
|
||
+ return parse(function.outputSchema(), "McpFunction.outputSchema");
|
||
+ }
|
||
+ if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) {
|
||
+ return JsonSchemaGenerator.generateSchema(responseType);
|
||
+ }
|
||
+ return Map.of();
|
||
+ }
|
||
+
|
||
+ /**
|
||
+ * Retained for callers that use only explicit output schemas.
|
||
+ */
|
||
+ public Map<String, Object> resolveOutput(McpFunction function) {
|
||
+ return resolveOutput(function, null);
|
||
+ }
|
||
+
|
||
private Map<String, Object> loadResource(String location) {
|
||
String path = location.startsWith("classpath:")
|
||
? location.substring("classpath:".length())
|
||
: location;
|
||
ClassPathResource resource = new ClassPathResource(path);
|
||
if (!resource.exists()) {
|
||
- throw new IllegalStateException("MCP input schema resource not found: " + location);
|
||
+ throw new IllegalStateException("MCP schema resource not found: " + location);
|
||
}
|
||
|
||
try (InputStream inputStream = resource.getInputStream()) {
|
||
return objectMapper.readValue(inputStream, new TypeReference<>() { });
|
||
} catch (Exception e) {
|
||
- throw new IllegalStateException("Failed to load MCP input schema resource: " + location, e);
|
||
+ throw new IllegalStateException("Failed to load MCP schema resource: " + location, e);
|
||
}
|
||
}
|
||
|
||
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
|
||
index daf0018..ec840ed 100644
|
||
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
|
||
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
|
||
@@ -192,14 +192,30 @@ public class BusinessToolController {
|
||
} else {
|
||
methodResult = targetMethod.invoke(targetBean, invokeArgument);
|
||
}
|
||
+
|
||
+ Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType());
|
||
+ if (!outputSchema.isEmpty()) {
|
||
+ List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
|
||
+ if (!outputErrors.isEmpty()) {
|
||
+ log.error("[Tool] Output schema validation failed. tool={}, errors={}",
|
||
+ functionName, outputErrors);
|
||
+ Map<String, Object> errorBody = new HashMap<>();
|
||
+ errorBody.put("code", "INVALID_TOOL_RESPONSE");
|
||
+ errorBody.put("message", "Tool response does not match its output schema");
|
||
+ if (finalRequestId != null) {
|
||
+ errorBody.put("request_id", finalRequestId);
|
||
+ }
|
||
+ return ResponseEntity.internalServerError().body(errorBody);
|
||
+ }
|
||
+ }
|
||
|
||
long elapsed = System.currentTimeMillis() - startTime;
|
||
|
||
// 5. 寃곌낵 諛섑솚 (?쒖닔 REST ?묐떟)
|
||
try {
|
||
- log.info("[Tool -> MCP Gateway] ?숈쟻 ???ㅽ뻾 寃곌낵 諛섑솚: {}", objectMapper.writeValueAsString(methodResult));
|
||
+ log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
|
||
} catch (Exception e) {
|
||
- log.info("[Tool -> MCP Gateway] ?숈쟻 ???ㅽ뻾 寃곌낵 諛섑솚: {}", methodResult);
|
||
+ log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult);
|
||
}
|
||
|
||
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
|
||
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
|
||
index 876144c..59c68ff 100644
|
||
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
|
||
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
|
||
@@ -21,8 +21,12 @@ public class ToolArgumentSchemaValidator {
|
||
}
|
||
|
||
public List<Error> validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) throws Exception {
|
||
+ return validateValue(schemaDefinition, arguments);
|
||
+ }
|
||
+
|
||
+ public List<Error> validateValue(Map<String, Object> schemaDefinition, Object value) throws Exception {
|
||
SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
|
||
Schema schema = schemaRegistry.getSchema(objectMapper.writeValueAsString(schemaDefinition));
|
||
- return schema.validate(objectMapper.writeValueAsString(arguments), InputFormat.JSON);
|
||
+ return schema.validate(objectMapper.writeValueAsString(value), InputFormat.JSON);
|
||
}
|
||
}
|
||
diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
|
||
index 6737a00..bc05f05 100644
|
||
--- a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
|
||
+++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
|
||
@@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
|
||
+import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||
import java.lang.reflect.Method;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
@@ -15,66 +17,103 @@ class ToolSchemaResolverTest {
|
||
private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper());
|
||
|
||
@Test
|
||
- void usesExplicitSchemaResourceBeforeAutomaticDtoSchema() throws Exception {
|
||
- Method method = SchemaBackedTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
|
||
- McpFunction function = method.getAnnotation(McpFunction.class);
|
||
+ void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
|
||
+ Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||
|
||
- Map<String, Object> schema = resolver.resolve(function, AutoGeneratedRequest.class);
|
||
+ Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
|
||
|
||
assertEquals(false, schema.get("additionalProperties"));
|
||
- assertTrue(schema.containsKey("anyOf"));
|
||
- assertEquals(50, ((Map<?, ?>) ((Map<?, ?>) schema.get("properties")).get("size")).get("maximum"));
|
||
- assertEquals(List.of(Map.of("required", List.of("claimNo"))), schema.get("anyOf"));
|
||
+ assertTrue(!properties(schema).containsKey("differentField"));
|
||
}
|
||
|
||
+ @Test
|
||
+ void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
|
||
+ Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||
|
||
- /* Inline schema resolution is covered by the same resolver branch at integration level.
|
||
- void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
|
||
- Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
|
||
+ Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
|
||
|
||
- Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class);
|
||
+ assertTrue(properties(schema).containsKey("differentField"));
|
||
+ }
|
||
|
||
- assertEquals("object", schema.get("type"));
|
||
+ @Test
|
||
+ void resolvesExplicitOutputSchema() throws Exception {
|
||
+ Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||
+ Map<String, Object> schema = resolver.resolveOutput(method.getAnnotation(McpFunction.class));
|
||
assertEquals(false, schema.get("additionalProperties"));
|
||
- assertTrue(!((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
|
||
+ assertTrue(properties(schema).containsKey("resultCode"));
|
||
}
|
||
|
||
- */
|
||
@Test
|
||
- void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
|
||
- Method method = SchemaBackedTool.AutomaticSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
|
||
+ void generatesOutputSchemaFromMarkedResponseDto() throws Exception {
|
||
+ Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||
|
||
- Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class);
|
||
+ Map<String, Object> schema = resolver.resolveOutput(
|
||
+ method.getAnnotation(McpFunction.class), SimpleResponse.class);
|
||
|
||
- assertTrue(((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
|
||
+ assertEquals(List.of("resultCode"), schema.get("required"));
|
||
+ assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum"));
|
||
}
|
||
|
||
- static class InlineSchemaTool {
|
||
+ @Test
|
||
+ void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception {
|
||
+ Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||
+ Map<String, Object> schema = resolver.resolveOutput(
|
||
+ method.getAnnotation(McpFunction.class), AutomaticRequest.class);
|
||
+ assertTrue(schema.isEmpty());
|
||
+ }
|
||
+ @SuppressWarnings("unchecked")
|
||
+ private Map<String, Object> properties(Map<String, Object> schema) {
|
||
+ return (Map<String, Object>) schema.get("properties");
|
||
+ }
|
||
+
|
||
+ @SuppressWarnings("unchecked")
|
||
+ private Map<String, Object> property(Map<String, Object> schema, String name) {
|
||
+ return (Map<String, Object>) properties(schema).get(name);
|
||
+ }
|
||
|
||
- @McpFunction(displayName = "inline", name = "sample.inline", description = "inline", inputSchema = "{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{},\\\"additionalProperties\\\":false}")
|
||
- void search(AutoGeneratedRequest request) {
|
||
+ static class InlineSchemaTool {
|
||
+ @McpFunction(
|
||
+ displayName = "inline",
|
||
+ name = "sample.inline",
|
||
+ description = "inline schema",
|
||
+ inputSchema = "{\"type\":\"object\",\"properties\":{\"keyword\":{\"type\":\"string\"}},\"additionalProperties\":false}")
|
||
+ void search(AutomaticRequest request) {
|
||
}
|
||
}
|
||
- static class SchemaBackedTool {
|
||
|
||
static class AutomaticSchemaTool {
|
||
+ @McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic schema")
|
||
+ void search(AutomaticRequest request) {
|
||
+ }
|
||
+ }
|
||
|
||
- @McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic")
|
||
- void search(AutoGeneratedRequest request) {
|
||
+ static class AutomaticOutputSchemaTool {
|
||
+ @McpFunction(displayName = "automatic-output", name = "sample.automatic-output", description = "automatic output")
|
||
+ SimpleResponse search(AutomaticRequest request) {
|
||
+ return null;
|
||
}
|
||
}
|
||
|
||
+ @McpOutputSchema
|
||
+ static class SimpleResponse {
|
||
+ @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
|
||
+ private String resultCode;
|
||
+
|
||
+ @McpValidation(maxLength = 200)
|
||
+ private String message;
|
||
+ }
|
||
|
||
+ static class OutputSchemaTool {
|
||
@McpFunction(
|
||
- displayName = "泥?뎄 議고쉶",
|
||
- name = "processing.claim.search",
|
||
- description = "蹂댄뿕湲?泥?뎄瑜?議고쉶?쒕떎.",
|
||
- inputSchemaResource = "classpath:tool-schemas/claim-search-input-schema.json")
|
||
- void search(AutoGeneratedRequest request) {
|
||
+ displayName = "output",
|
||
+ name = "sample.output",
|
||
+ description = "output schema",
|
||
+ outputSchema = "{\"type\":\"object\",\"properties\":{\"resultCode\":{\"type\":\"string\"}},\"required\":[\"resultCode\"],\"additionalProperties\":false}")
|
||
+ void search(AutomaticRequest request) {
|
||
}
|
||
}
|
||
|
||
- static class AutoGeneratedRequest {
|
||
+ static class AutomaticRequest {
|
||
private String differentField;
|
||
}
|
||
}
|
||
diff --git a/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json b/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json
|
||
deleted file mode 100644
|
||
index e595b42..0000000
|
||
--- a/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json
|
||
+++ /dev/null
|
||
@@ -1,20 +0,0 @@
|
||
-{
|
||
- "$schema": "https://json-schema.org/draft/2020-12/schema",
|
||
- "type": "object",
|
||
- "properties": {
|
||
- "claimNo": {
|
||
- "type": "string",
|
||
- "pattern": "^CLM[0-9]{13}$"
|
||
- },
|
||
- "size": {
|
||
- "type": "integer",R
|
||
- "minimum": 1,
|
||
- "maximum": 50,
|
||
- "default": 20
|
||
- }
|
||
- },
|
||
- "additionalProperties": false,
|
||
- "anyOf": [
|
||
- { "required": ["claimNo"] }
|
||
- ]
|
||
-}
|
||
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java
|
||
new file mode 100644
|
||
index 0000000..abd2d2d
|
||
--- /dev/null
|
||
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java
|
||
@@ -0,0 +1,88 @@
|
||
+package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||
+
|
||
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
|
||
+import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||
+import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||
+import java.util.List;
|
||
+import lombok.AllArgsConstructor;
|
||
+import lombok.Builder;
|
||
+import lombok.Getter;
|
||
+import lombok.NoArgsConstructor;
|
||
+import lombok.Setter;
|
||
+
|
||
+/**
|
||
+ * Claim search response sample.
|
||
+ * Complex response rules are defined by outputSchemaResource; annotations document
|
||
+ * the same simple field constraints for automatic schema generation examples.
|
||
+ */
|
||
+@Getter
|
||
+@Setter
|
||
+@Builder
|
||
+@NoArgsConstructor
|
||
+@AllArgsConstructor
|
||
+@McpOutputSchema
|
||
+public class ClaimSearchResponse {
|
||
+
|
||
+ @McpParameter(description = "Execution result code.")
|
||
+ @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
|
||
+ private String resultCode;
|
||
+
|
||
+ @McpParameter(description = "User-readable label for resultCode.")
|
||
+ @McpValidation(required = true, maxLength = 100)
|
||
+ private String resultLabel;
|
||
+
|
||
+ @McpParameter(description = "Current claim processing status code.")
|
||
+ @McpValidation(required = true, allowedValues = {
|
||
+ "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
|
||
+ "APPROVED", "PAID", "REJECTED", "WITHDRAWN"
|
||
+ })
|
||
+ private String status;
|
||
+
|
||
+ @McpParameter(description = "User-readable label for status.")
|
||
+ @McpValidation(required = true, maxLength = 100)
|
||
+ private String statusLabel;
|
||
+
|
||
+ @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
|
||
+ @McpValidation(minimum = 0, nullable = true)
|
||
+ private Long approvedAmount;
|
||
+
|
||
+ @McpParameter(description = "Present only when status is REJECTED; otherwise null.")
|
||
+ @McpValidation(maxLength = 200, nullable = true)
|
||
+ private String rejectionReason;
|
||
+
|
||
+ @McpParameter(description = "Claim summaries, ordered by received date descending.")
|
||
+ @McpValidation(required = true)
|
||
+ private List<ClaimSummary> items;
|
||
+
|
||
+ @McpParameter(description = "True when additional results exist beyond this response.")
|
||
+ @McpValidation(required = true)
|
||
+ private Boolean hasMore;
|
||
+
|
||
+ @McpParameter(description = "Total number of matched claims.")
|
||
+ @McpValidation(required = true, minimum = 0)
|
||
+ private Integer totalCount;
|
||
+
|
||
+ @Getter
|
||
+ @Setter
|
||
+ @Builder
|
||
+ @NoArgsConstructor
|
||
+ @AllArgsConstructor
|
||
+ public static class ClaimSummary {
|
||
+
|
||
+ @McpParameter(description = "Claim processing status code.")
|
||
+ @McpValidation(required = true)
|
||
+ private String status;
|
||
+
|
||
+ @McpParameter(description = "User-readable label for status.")
|
||
+ @McpValidation(required = true)
|
||
+ private String statusLabel;
|
||
+
|
||
+ @McpParameter(description = "Received date in YYYY-MM-DD format.")
|
||
+ @McpValidation(required = true, format = "date")
|
||
+ private String receivedDate;
|
||
+
|
||
+ @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
|
||
+ @McpValidation(minimum = 0, nullable = true)
|
||
+ private Long approvedAmount;
|
||
+ }
|
||
+}
|
||
\ No newline at end of file
|
||
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
|
||
index 16d1aa0..9a55c1a 100644
|
||
--- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
|
||
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
|
||
@@ -3,6 +3,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
|
||
+import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
|
||
|
||
@McpTool(
|
||
routingType = "DIRECT",
|
||
@@ -12,13 +13,14 @@ public interface ClaimSearchSchemaSampleUseCase {
|
||
|
||
@McpFunction(
|
||
register = false,
|
||
- displayName = "泥?뎄 議고쉶 JSON Schema ?섑뵆",
|
||
+ displayName = "Claim search JSON Schema sample",
|
||
name = "sample.claim.search.resource",
|
||
- description = "inputSchemaResource瑜??ъ슜?섎뒗 泥?뎄 議고쉶 Tool ?섑뵆?낅땲??",
|
||
- prompt = "泥?뎄踰덊샇 ?먮뒗 怨꾩빟踰덊샇濡?蹂댄뿕湲?泥?뎄瑜?議고쉶?댁쨾.",
|
||
- inputSchemaResource = "classpath:tool-schemas/claim-search-resource-input-schema.json",
|
||
+ description = "Claim search Tool sample using input and output JSON Schema resources.",
|
||
+ prompt = "Search an insurance claim by claim number or contract number.",
|
||
+ inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
|
||
+ outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json",
|
||
readOnlyHint = true,
|
||
idempotentHint = true
|
||
)
|
||
- Object search(ClaimSearchRequest request);
|
||
-}
|
||
+ ClaimSearchResponse search(ClaimSearchRequest request);
|
||
+}
|
||
\ No newline at end of file
|
||
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
|
||
index e6cd98b..1a817f5 100644
|
||
--- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
|
||
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
|
||
@@ -1,22 +1,36 @@
|
||
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
|
||
|
||
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
|
||
+import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
|
||
import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
|
||
-import java.util.Map;
|
||
+import java.util.List;
|
||
import org.springframework.stereotype.Service;
|
||
|
||
/**
|
||
- * inputSchemaResource ?곸슜 諛⑹떇??蹂댁뿬二쇰뒗 鍮꾨끂異??섑뵆 Tool?대떎.
|
||
- * ?ㅼ젣 MCI/EIMS ?곕룞? 異붽??섏? ?딅뒗??
|
||
+ * Non-exposed sample Tool. It does not call MCI or EIMS.
|
||
*/
|
||
@Service
|
||
public class ClaimSearchSchemaSampleUseCaseImpl implements ClaimSearchSchemaSampleUseCase {
|
||
|
||
@Override
|
||
- public Object search(ClaimSearchRequest request) {
|
||
- return Map.of(
|
||
- "message", "inputSchemaResource JSON Schema sample",
|
||
- "request", request == null ? Map.of() : request
|
||
- );
|
||
+ public ClaimSearchResponse search(ClaimSearchRequest request) {
|
||
+ ClaimSearchResponse.ClaimSummary item = ClaimSearchResponse.ClaimSummary.builder()
|
||
+ .status("REVIEWING")
|
||
+ .statusLabel("Under review")
|
||
+ .receivedDate("2026-08-04")
|
||
+ .approvedAmount(null)
|
||
+ .build();
|
||
+
|
||
+ return ClaimSearchResponse.builder()
|
||
+ .resultCode("SUCCESS")
|
||
+ .resultLabel("Success")
|
||
+ .status("REVIEWING")
|
||
+ .statusLabel("Under review")
|
||
+ .approvedAmount(null)
|
||
+ .rejectionReason(null)
|
||
+ .items(List.of(item))
|
||
+ .hasMore(false)
|
||
+ .totalCount(1)
|
||
+ .build();
|
||
}
|
||
-}
|
||
+}
|
||
\ No newline at end of file
|
||
diff --git a/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json
|
||
similarity index 100%
|
||
rename from dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json
|
||
rename to dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json
|
||
diff --git a/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json
|
||
new file mode 100644
|
||
index 0000000..f94c661
|
||
--- /dev/null
|
||
+++ b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json
|
||
@@ -0,0 +1,81 @@
|
||
+{
|
||
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
|
||
+ "type": "object",
|
||
+ "description": "Claim search response. This schema intentionally excludes employee identifiers, customer names, contact details, account information, and other PII.",
|
||
+ "properties": {
|
||
+ "resultCode": {
|
||
+ "type": "string",
|
||
+ "enum": ["SUCCESS", "FAILURE"],
|
||
+ "description": "Machine-readable execution result code."
|
||
+ },
|
||
+ "resultLabel": {
|
||
+ "type": "string",
|
||
+ "description": "User-readable label for resultCode."
|
||
+ },
|
||
+ "status": {
|
||
+ "type": "string",
|
||
+ "enum": ["RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", "APPROVED", "PAID", "REJECTED", "WITHDRAWN"],
|
||
+ "description": "Current claim processing status code."
|
||
+ },
|
||
+ "statusLabel": {
|
||
+ "type": "string",
|
||
+ "description": "User-readable label for status."
|
||
+ },
|
||
+ "approvedAmount": {
|
||
+ "anyOf": [
|
||
+ { "type": "number", "minimum": 0 },
|
||
+ { "type": "null" }
|
||
+ ],
|
||
+ "description": "Approved amount. It is null before review and must not be interpreted as zero."
|
||
+ },
|
||
+ "rejectionReason": {
|
||
+ "type": ["string", "null"],
|
||
+ "maxLength": 200,
|
||
+ "description": "Has a value only when status is REJECTED. It is null for every other status."
|
||
+ },
|
||
+ "items": {
|
||
+ "type": "array",
|
||
+ "description": "Claim summaries ordered by receivedDate descending. No personally identifiable information is included.",
|
||
+ "items": {
|
||
+ "type": "object",
|
||
+ "properties": {
|
||
+ "status": { "type": "string", "description": "Claim status code." },
|
||
+ "statusLabel": { "type": "string", "description": "User-readable label for status." },
|
||
+ "receivedDate": { "type": "string", "format": "date", "description": "Claim received date." },
|
||
+ "approvedAmount": {
|
||
+ "anyOf": [
|
||
+ { "type": "number", "minimum": 0 },
|
||
+ { "type": "null" }
|
||
+ ],
|
||
+ "description": "Null before review; do not interpret as zero."
|
||
+ }
|
||
+ },
|
||
+ "required": ["status", "statusLabel", "receivedDate"],
|
||
+ "additionalProperties": false
|
||
+ }
|
||
+ },
|
||
+ "hasMore": {
|
||
+ "type": "boolean",
|
||
+ "description": "True when additional results exist beyond this response."
|
||
+ },
|
||
+ "totalCount": {
|
||
+ "type": "integer",
|
||
+ "minimum": 0,
|
||
+ "description": "Total number of matched claims."
|
||
+ }
|
||
+ },
|
||
+ "required": ["resultCode", "resultLabel", "status", "statusLabel", "items", "hasMore", "totalCount"],
|
||
+ "allOf": [
|
||
+ {
|
||
+ "if": {
|
||
+ "properties": { "status": { "const": "REJECTED" } },
|
||
+ "required": ["status"]
|
||
+ },
|
||
+ "then": {
|
||
+ "properties": { "rejectionReason": { "type": "string", "minLength": 1 } },
|
||
+ "required": ["rejectionReason"]
|
||
+ }
|
||
+ }
|
||
+ ],
|
||
+ "additionalProperties": false
|
||
+}
|
||
\ No newline at end of file
|
||
diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
|
||
index b52e10c..cead062 100644
|
||
--- a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
|
||
+++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
|
||
@@ -3,7 +3,13 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||
|
||
+import com.fasterxml.jackson.databind.ObjectMapper;
|
||
+import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||
import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
|
||
+import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||
+import io.shinhanlife.dap.mcc.biz.cmm.usecase.impl.ClaimSearchSchemaSampleUseCaseImpl;
|
||
+import io.shinhanlife.dap.mcc.presentation.ToolArgumentSchemaValidator;import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
|
||
+import java.lang.reflect.Method;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import org.junit.jupiter.api.Test;
|
||
@@ -24,8 +30,59 @@ class ClaimSearchRequestSchemaTest {
|
||
Map.of("required", List.of("contractNo"))), schema.get("anyOf"));
|
||
}
|
||
|
||
+ @Test
|
||
+ void resolvesSchemaFromToolModuleResource() throws Exception {
|
||
+ Method method = ClaimSearchSchemaSampleUseCase.class
|
||
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
|
||
+ McpFunction function = method.getAnnotation(McpFunction.class);
|
||
+
|
||
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper())
|
||
+ .resolve(function, ClaimSearchRequest.class);
|
||
+
|
||
+ assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
|
||
+ assertTrue(schema.containsKey("anyOf"));
|
||
+ assertEquals(50, property(schema, "size").get("maximum"));
|
||
+ }
|
||
+
|
||
+ @Test
|
||
+ void resolvesOutputSchemaFromToolModuleResource() throws Exception {
|
||
+ Method method = ClaimSearchSchemaSampleUseCase.class
|
||
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
|
||
+ McpFunction function = method.getAnnotation(McpFunction.class);
|
||
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function);
|
||
+ assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
|
||
+ assertTrue(properties(schema).containsKey("resultCode"));
|
||
+ assertTrue(properties(schema).containsKey("statusLabel"));
|
||
+ assertTrue(properties(schema).containsKey("hasMore"));
|
||
+ assertTrue(schema.containsKey("allOf"));
|
||
+ }
|
||
+
|
||
+ @Test
|
||
+ void sampleResponseConformsToOutputSchema() throws Exception {
|
||
+ Method method = ClaimSearchSchemaSampleUseCase.class
|
||
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
|
||
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper())
|
||
+ .resolveOutput(method.getAnnotation(McpFunction.class));
|
||
+
|
||
+ ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest());
|
||
+ ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
|
||
+
|
||
+ assertTrue(validator.validateValue(schema, response).isEmpty());
|
||
+ }
|
||
+ @Test
|
||
+ void sampleResponseConformsToAutomaticallyGeneratedOutputSchema() throws Exception {
|
||
+ Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ClaimSearchResponse.class);
|
||
+ ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest());
|
||
+ ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
|
||
+
|
||
+ assertTrue(validator.validateValue(schema, response).isEmpty());
|
||
+ }
|
||
@SuppressWarnings("unchecked")
|
||
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
|
||
return (Map<String, Map<String, Object>>) schema.get("properties");
|
||
}
|
||
+
|
||
+ private Map<String, Object> property(Map<String, Object> schema, String name) {
|
||
+ return properties(schema).get(name);
|
||
+ }
|
||
}
|