feat: enrich claim search output schema
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s

This commit is contained in:
jade
2026-08-04 13:34:02 +09:00
parent 31e6fd605d
commit 1bac8d9d87
6 changed files with 184 additions and 26 deletions

View File

@@ -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) {

View File

@@ -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<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)
private Long approvedAmount;
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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
}

View File

@@ -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<String, Object> 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<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());
}
@SuppressWarnings("unchecked")
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
return (Map<String, Map<String, Object>>) schema.get("properties");