refactor: restructure packages, delete ZtUsac, fix gateway context
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m15s

This commit is contained in:
jade
2026-08-05 10:09:36 +09:00
parent 65222b5b1a
commit 4b34df774d
52 changed files with 1370 additions and 519 deletions

715
recent_changes.diff Normal file
View File

@@ -0,0 +1,715 @@
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);
+ }
}