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 77db8400..27b4fe06 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 @@ -212,6 +212,7 @@ public class BusinessToolController { long elapsed = System.currentTimeMillis() - startTime; // 5. 결과 반환 (순수 REST 응답) + log.info("[Tool -> MCP Gateway] 동적 툴 Output Schema: {}", outputSchema); try { log.info("[Tool -> MCP Gateway] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(methodResult)); } catch (Exception e) { 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 index f87b6910..85a292ac 100644 --- 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 @@ -3,6 +3,7 @@ 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; @@ -10,7 +11,9 @@ import lombok.NoArgsConstructor; import lombok.Setter; /** - * Simple response DTO sample that generates an MCP output schema from annotations. + * 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 @@ -20,15 +23,66 @@ import lombok.Setter; @McpOutputSchema public class ClaimSearchResponse { - @McpParameter(description = "Tool execution result code.") + @McpParameter(description = "Execution result code.") @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"}) private String resultCode; - @McpParameter(description = "User-readable execution message.") - @McpValidation(required = true, maxLength = 200) - private String message; + @McpParameter(description = "User-readable label for resultCode.") + @McpValidation(required = true, maxLength = 100) + private String resultLabel; - @McpParameter(description = "Number of matched claims.") + @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) + private Long approvedAmount; + + @McpParameter(description = "Present only when status is REJECTED; otherwise null.") + @McpValidation(maxLength = 200) + private String rejectionReason; + + @McpParameter(description = "Claim summaries, ordered by received date descending.") + @McpValidation(required = true) + private List 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) + 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 779150c5..3be3a498 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 = "청구번호 또는 계약번호로 보험금 청구를 조회해줘.", + 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/claim-search-resource-input-schema.json", - outputSchemaResource = "classpath:tool-schemas/claim-search-resource-output-schema.json", readOnlyHint = true, + + 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 e6cd98b6..1a817f59 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-output-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-output-schema.json index 9d86a5b3..f94c6613 100644 --- a/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-output-schema.json +++ b/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-output-schema.json @@ -1,10 +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": { - "message": { "type": "string", "description": "Tool execution summary." }, - "request": { "type": "object", "description": "Normalized Tool request." } + "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": ["message", "request"], + "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 24800021..16d996de 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 @@ -7,7 +7,8 @@ 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.ClaimSearchSchemaSampleUseCase; +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; @@ -50,9 +51,24 @@ class ClaimSearchRequestSchemaTest { McpFunction function = method.getAnnotation(McpFunction.class); Map schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function); assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema")); - assertEquals(List.of("message", "request"), schema.get("required")); + 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 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()); + } @SuppressWarnings("unchecked") private Map> properties(Map schema) { return (Map>) schema.get("properties");