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