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;
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
// 5. 결과 반환 (순수 REST 응답)
|
// 5. 결과 반환 (순수 REST 응답)
|
||||||
|
log.info("[Tool -> MCP Gateway] 동적 툴 Output Schema: {}", outputSchema);
|
||||||
try {
|
try {
|
||||||
log.info("[Tool -> MCP Gateway] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(methodResult));
|
log.info("[Tool -> MCP Gateway] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(methodResult));
|
||||||
} catch (Exception e) {
|
} 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.McpOutputSchema;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||||||
|
import java.util.List;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -10,7 +11,9 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
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
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -20,15 +23,66 @@ import lombok.Setter;
|
|||||||
@McpOutputSchema
|
@McpOutputSchema
|
||||||
public class ClaimSearchResponse {
|
public class ClaimSearchResponse {
|
||||||
|
|
||||||
@McpParameter(description = "Tool execution result code.")
|
@McpParameter(description = "Execution result code.")
|
||||||
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
|
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
|
||||||
private String resultCode;
|
private String resultCode;
|
||||||
|
|
||||||
@McpParameter(description = "User-readable execution message.")
|
@McpParameter(description = "User-readable label for resultCode.")
|
||||||
@McpValidation(required = true, maxLength = 200)
|
@McpValidation(required = true, maxLength = 100)
|
||||||
private String message;
|
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)
|
@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;
|
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.McpFunction;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
|
||||||
|
|
||||||
@McpTool(
|
@McpTool(
|
||||||
routingType = "DIRECT",
|
routingType = "DIRECT",
|
||||||
@@ -12,13 +13,14 @@ public interface ClaimSearchSchemaSampleUseCase {
|
|||||||
|
|
||||||
@McpFunction(
|
@McpFunction(
|
||||||
register = false,
|
register = false,
|
||||||
displayName = "청구 조회 JSON Schema 샘플",
|
displayName = "Claim search JSON Schema sample",
|
||||||
name = "sample.claim.search.resource",
|
name = "sample.claim.search.resource",
|
||||||
description = "inputSchemaResource를 사용하는 청구 조회 Tool 샘플입니다.",
|
description = "Claim search Tool sample using input and output JSON Schema resources.",
|
||||||
prompt = "청구번호 또는 계약번호로 보험금 청구를 조회해줘.",
|
prompt = "Search an insurance claim by claim number or contract number.",
|
||||||
inputSchemaResource = "classpath:tool-schemas/claim-search-resource-input-schema.json",
|
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
|
idempotentHint = true
|
||||||
)
|
)
|
||||||
Object search(ClaimSearchRequest request);
|
ClaimSearchResponse search(ClaimSearchRequest request);
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,36 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
|
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.ClaimSearchRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
|
import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
|
||||||
import java.util.Map;
|
import java.util.List;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* inputSchemaResource 적용 방식을 보여주는 비노출 샘플 Tool이다.
|
* Non-exposed sample Tool. It does not call MCI or EIMS.
|
||||||
* 실제 MCI/EIMS 연동은 추가하지 않는다.
|
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ClaimSearchSchemaSampleUseCaseImpl implements ClaimSearchSchemaSampleUseCase {
|
public class ClaimSearchSchemaSampleUseCaseImpl implements ClaimSearchSchemaSampleUseCase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object search(ClaimSearchRequest request) {
|
public ClaimSearchResponse search(ClaimSearchRequest request) {
|
||||||
return Map.of(
|
ClaimSearchResponse.ClaimSummary item = ClaimSearchResponse.ClaimSummary.builder()
|
||||||
"message", "inputSchemaResource JSON Schema sample",
|
.status("REVIEWING")
|
||||||
"request", request == null ? Map.of() : request
|
.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",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"description": "Claim search response. This schema intentionally excludes employee identifiers, customer names, contact details, account information, and other PII.",
|
||||||
"properties": {
|
"properties": {
|
||||||
"message": { "type": "string", "description": "Tool execution summary." },
|
"resultCode": {
|
||||||
"request": { "type": "object", "description": "Normalized Tool request." }
|
"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
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||||
import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
|
import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
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.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -50,9 +51,24 @@ class ClaimSearchRequestSchemaTest {
|
|||||||
McpFunction function = method.getAnnotation(McpFunction.class);
|
McpFunction function = method.getAnnotation(McpFunction.class);
|
||||||
Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function);
|
Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function);
|
||||||
assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
|
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
|
||||||
return (Map<String, Map<String, Object>>) schema.get("properties");
|
return (Map<String, Map<String, Object>>) schema.get("properties");
|
||||||
|
|||||||
Reference in New Issue
Block a user