feat: support explicit MCP input schemas
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
This commit is contained in:
@@ -27,7 +27,18 @@ public @interface McpFunction {
|
|||||||
String prompt() default "";
|
String prompt() default "";
|
||||||
String mappingId() default "";
|
String mappingId() default "";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다.
|
||||||
|
*/
|
||||||
|
String inputSchema() default "{}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다.
|
||||||
|
* inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다.
|
||||||
|
*/
|
||||||
// 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어
|
// 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어
|
||||||
|
String inputSchemaResource() default "";
|
||||||
boolean register() default false;
|
boolean register() default false;
|
||||||
|
|
||||||
// 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김)
|
// 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김)
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public @interface McpValidation {
|
|||||||
boolean required() default false;
|
boolean required() default false;
|
||||||
String pattern() default "";
|
String pattern() default "";
|
||||||
long minimum() default Long.MIN_VALUE;
|
long minimum() default Long.MIN_VALUE;
|
||||||
|
long maximum() default Long.MAX_VALUE;
|
||||||
|
int minLength() default -1;
|
||||||
|
int maxLength() default -1;
|
||||||
String[] allowedValues() default {};
|
String[] allowedValues() default {};
|
||||||
String format() default "";
|
String format() default "";
|
||||||
String defaultValue() default "";
|
String defaultValue() default "";
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package io.shinhanlife.dap.lib.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common MCP Tool Schema Bean configuration.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class ToolSchemaConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) {
|
||||||
|
return new ToolSchemaResolver(objectMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,15 @@ public class JsonSchemaGenerator {
|
|||||||
if (validation != null && validation.minimum() != Long.MIN_VALUE) {
|
if (validation != null && validation.minimum() != Long.MIN_VALUE) {
|
||||||
fieldSchema.put("minimum", validation.minimum());
|
fieldSchema.put("minimum", validation.minimum());
|
||||||
}
|
}
|
||||||
|
if (validation != null && validation.maximum() != Long.MAX_VALUE) {
|
||||||
|
fieldSchema.put("maximum", validation.maximum());
|
||||||
|
}
|
||||||
|
if (validation != null && validation.minLength() >= 0) {
|
||||||
|
fieldSchema.put("minLength", validation.minLength());
|
||||||
|
}
|
||||||
|
if (validation != null && validation.maxLength() >= 0) {
|
||||||
|
fieldSchema.put("maxLength", validation.maxLength());
|
||||||
|
}
|
||||||
if (validation != null && validation.allowedValues().length > 0) {
|
if (validation != null && validation.allowedValues().length > 0) {
|
||||||
fieldSchema.put("enum", List.of(validation.allowedValues()));
|
fieldSchema.put("enum", List.of(validation.allowedValues()));
|
||||||
}
|
}
|
||||||
@@ -89,7 +98,7 @@ public class JsonSchemaGenerator {
|
|||||||
fieldSchema.put("format", validation.format());
|
fieldSchema.put("format", validation.format());
|
||||||
}
|
}
|
||||||
if (validation != null && !validation.defaultValue().isEmpty()) {
|
if (validation != null && !validation.defaultValue().isEmpty()) {
|
||||||
fieldSchema.put("default", validation.defaultValue());
|
fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType()));
|
||||||
}
|
}
|
||||||
if (validation != null && validation.examples().length > 0) {
|
if (validation != null && validation.examples().length > 0) {
|
||||||
fieldSchema.put("examples", List.of(validation.examples()));
|
fieldSchema.put("examples", List.of(validation.examples()));
|
||||||
@@ -108,6 +117,7 @@ public class JsonSchemaGenerator {
|
|||||||
List<Map<String, Object>> anyOfList = new ArrayList<>();
|
List<Map<String, Object>> anyOfList = new ArrayList<>();
|
||||||
for (String fieldName : anyOfAnnotation.value()) {
|
for (String fieldName : anyOfAnnotation.value()) {
|
||||||
anyOfList.add(Map.of("required", List.of(fieldName)));
|
anyOfList.add(Map.of("required", List.of(fieldName)));
|
||||||
|
|
||||||
}
|
}
|
||||||
schema.put("anyOf", anyOfList);
|
schema.put("anyOf", anyOfList);
|
||||||
}
|
}
|
||||||
@@ -116,6 +126,27 @@ public class JsonSchemaGenerator {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Object coerceDefaultValue(String value, Class<?> fieldType) {
|
||||||
|
try {
|
||||||
|
if (fieldType == Integer.class || fieldType == int.class
|
||||||
|
|| fieldType == Long.class || fieldType == long.class
|
||||||
|
|| fieldType == Short.class || fieldType == short.class
|
||||||
|
|| fieldType == Byte.class || fieldType == byte.class) {
|
||||||
|
return Long.valueOf(value);
|
||||||
|
}
|
||||||
|
if (fieldType == Double.class || fieldType == double.class
|
||||||
|
|| fieldType == Float.class || fieldType == float.class) {
|
||||||
|
return Double.valueOf(value);
|
||||||
|
}
|
||||||
|
if (fieldType == Boolean.class || fieldType == boolean.class) {
|
||||||
|
return Boolean.valueOf(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("Invalid MCP default value: " + value, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
private static Map<String, Object> createFieldSchema(Field field, Set<Class<?>> visiting) {
|
private static Map<String, Object> createFieldSchema(Field field, Set<Class<?>> visiting) {
|
||||||
Class<?> fieldType = field.getType();
|
Class<?> fieldType = field.getType();
|
||||||
if (isSimpleType(fieldType)) {
|
if (isSimpleType(fieldType)) {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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 java.util.Map;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */
|
||||||
|
public class ToolSchemaResolver {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public ToolSchemaResolver(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> resolve(McpFunction function, Class<?> requestType) {
|
||||||
|
if (function != null && !function.inputSchemaResource().isBlank()) {
|
||||||
|
return loadResource(function.inputSchemaResource());
|
||||||
|
}
|
||||||
|
if (function != null && !function.inputSchema().isBlank()
|
||||||
|
&& !"{}".equals(function.inputSchema().trim())) {
|
||||||
|
return parse(function.inputSchema(), "McpFunction.inputSchema");
|
||||||
|
}
|
||||||
|
return JsonSchemaGenerator.generateSchema(requestType);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> parse(String schema, String source) {
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(schema, new TypeReference<>() { });
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to parse MCP input schema from " + source, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import io.shinhanlife.dap.lib.annotation.McpTool;
|
|||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender;
|
import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender;
|
||||||
import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -57,6 +57,7 @@ public class BusinessToolController {
|
|||||||
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
|
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
|
||||||
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
||||||
|
|
||||||
|
private final ToolSchemaResolver toolSchemaResolver;
|
||||||
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
||||||
@GetMapping("/mcp/api/v1/tools/local")
|
@GetMapping("/mcp/api/v1/tools/local")
|
||||||
public List<ToolMetadata> getLocalTools() {
|
public List<ToolMetadata> getLocalTools() {
|
||||||
@@ -147,8 +148,8 @@ public class BusinessToolController {
|
|||||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
||||||
if (!Map.class.isAssignableFrom(paramType)) {
|
if (!Map.class.isAssignableFrom(paramType)) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> autoSchema = JsonSchemaGenerator.generateSchema(paramType);
|
Map<String, Object> inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType);
|
||||||
List<Error> errors = toolArgumentSchemaValidator.validate(autoSchema, arguments);
|
List<Error> errors = toolArgumentSchemaValidator.validate(inputSchema, arguments);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
|
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
|
||||||
List<String> errorMessages = new ArrayList<>();
|
List<String> errorMessages = new ArrayList<>();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -54,6 +54,7 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final McpProperties mcpProperties;
|
private final McpProperties mcpProperties;
|
||||||
private final RestClient restClient = RestClient.create();
|
private final RestClient restClient = RestClient.create();
|
||||||
|
private final ToolSchemaResolver toolSchemaResolver;
|
||||||
|
|
||||||
@Value("${axhub.gateway.url:http://localhost:8081}")
|
@Value("${axhub.gateway.url:http://localhost:8081}")
|
||||||
private String gatewayUrl;
|
private String gatewayUrl;
|
||||||
@@ -127,7 +128,7 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
if (method.getParameterCount() > 0) {
|
if (method.getParameterCount() > 0) {
|
||||||
try {
|
try {
|
||||||
Class<?> paramType = method.getParameterTypes()[0];
|
Class<?> paramType = method.getParameterTypes()[0];
|
||||||
Map<String, Object> finalSchema = JsonSchemaGenerator.generateSchema(paramType);
|
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType);
|
||||||
meta.setParametersSchema(finalSchema);
|
meta.setParametersSchema(finalSchema);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to generate schema for {}", subToolName, e);
|
log.error("Failed to generate schema for {}", subToolName, e);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package io.shinhanlife.dap.lib.config;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ToolSchemaConfigurationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registersToolSchemaResolver() {
|
||||||
|
ToolSchemaConfiguration configuration = new ToolSchemaConfiguration();
|
||||||
|
|
||||||
|
assertNotNull(configuration.toolSchemaResolver(new ObjectMapper()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,19 +16,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.util
|
|
||||||
* @className JsonSchemaGeneratorTest
|
|
||||||
* @description JSON Schema constraint generation test
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.07.27
|
|
||||||
* <pre>
|
|
||||||
* ---------- revision history ----------
|
|
||||||
* date author description
|
|
||||||
* ---------- --------- ---------------------------
|
|
||||||
* 2026.07.27 0986406 initial creation
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
class JsonSchemaGeneratorTest {
|
class JsonSchemaGeneratorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -43,6 +30,16 @@ class JsonSchemaGeneratorTest {
|
|||||||
assertEquals(List.of("APPROVE", "REJECT"), properties.get("approvalStatus").get("enum"));
|
assertEquals(List.of("APPROVE", "REJECT"), properties.get("approvalStatus").get("enum"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void includesExtendedMcpValidationConstraintsInGeneratedSchema() {
|
||||||
|
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class);
|
||||||
|
|
||||||
|
assertEquals(50L, property(schema, "pageSize").get("maximum"));
|
||||||
|
assertEquals(20L, property(schema, "pageSize").get("default"));
|
||||||
|
assertEquals(1, property(schema, "reference").get("minLength"));
|
||||||
|
assertEquals(30, property(schema, "reference").get("maxLength"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void includesNestedDtoConstraintsInGeneratedSchema() {
|
void includesNestedDtoConstraintsInGeneratedSchema() {
|
||||||
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(NestedRequest.class);
|
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(NestedRequest.class);
|
||||||
@@ -104,6 +101,14 @@ class JsonSchemaGeneratorTest {
|
|||||||
@McpParameter(description = "approval result")
|
@McpParameter(description = "approval result")
|
||||||
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
|
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
|
||||||
private String approvalStatus;
|
private String approvalStatus;
|
||||||
|
|
||||||
|
@McpParameter(description = "page size")
|
||||||
|
@McpValidation(minimum = 1, maximum = 50, defaultValue = "20")
|
||||||
|
private Integer pageSize;
|
||||||
|
|
||||||
|
@McpParameter(description = "reference")
|
||||||
|
@McpValidation(minLength = 1, maxLength = 30)
|
||||||
|
private String reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class NestedRequest {
|
private static class NestedRequest {
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
|
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 java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
Map<String, Object> schema = resolver.resolve(function, AutoGeneratedRequest.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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 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), AutoGeneratedRequest.class);
|
||||||
|
|
||||||
|
assertEquals("object", schema.get("type"));
|
||||||
|
assertEquals(false, schema.get("additionalProperties"));
|
||||||
|
assertTrue(!((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
|
||||||
|
Method method = SchemaBackedTool.AutomaticSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
|
||||||
|
|
||||||
|
Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class);
|
||||||
|
|
||||||
|
assertTrue(((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static class InlineSchemaTool {
|
||||||
|
|
||||||
|
@McpFunction(displayName = "inline", name = "sample.inline", description = "inline", inputSchema = "{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{},\\\"additionalProperties\\\":false}")
|
||||||
|
void search(AutoGeneratedRequest request) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static class SchemaBackedTool {
|
||||||
|
|
||||||
|
static class AutomaticSchemaTool {
|
||||||
|
|
||||||
|
@McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic")
|
||||||
|
void search(AutoGeneratedRequest request) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@McpFunction(
|
||||||
|
displayName = "청구 조회",
|
||||||
|
name = "processing.claim.search",
|
||||||
|
description = "보험금 청구를 조회한다.",
|
||||||
|
inputSchemaResource = "classpath:tool-schemas/claim-search-input-schema.json")
|
||||||
|
void search(AutoGeneratedRequest request) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class AutoGeneratedRequest {
|
||||||
|
private String differentField;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$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"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpAnyOf;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 보험금 청구 조회 Tool의 입력 DTO 샘플이다.
|
||||||
|
* 청구번호 또는 계약번호 중 하나를 반드시 입력받는다.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@McpAnyOf({"claimNo", "contractNo"})
|
||||||
|
public class ClaimSearchRequest {
|
||||||
|
|
||||||
|
@McpParameter(description = "청구번호. CLM 다음 숫자 13자리 형식이다.")
|
||||||
|
@McpValidation(pattern = "^CLM[0-9]{13}$", examples = {"CLM2026070100123"})
|
||||||
|
private String claimNo;
|
||||||
|
|
||||||
|
@McpParameter(description = "계약번호. 숫자 11자리 형식이다.")
|
||||||
|
@McpValidation(pattern = "^[0-9]{11}$", examples = {"10023456789"})
|
||||||
|
private String contractNo;
|
||||||
|
|
||||||
|
@McpParameter(description = "청구 상태 필터")
|
||||||
|
@McpValidation(allowedValues = {
|
||||||
|
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
|
||||||
|
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
|
||||||
|
})
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@McpParameter(description = "청구 유형 필터")
|
||||||
|
@McpValidation(allowedValues = {
|
||||||
|
"MEDICAL", "SURGERY", "HOSPITALIZATION",
|
||||||
|
"DIAGNOSIS", "DEATH", "DISABILITY"
|
||||||
|
})
|
||||||
|
private String claimType;
|
||||||
|
|
||||||
|
@McpParameter(description = "접수일 조회 시작일(YYYY-MM-DD)")
|
||||||
|
@McpValidation(format = "date", examples = {"2026-01-01"})
|
||||||
|
private String fromDate;
|
||||||
|
|
||||||
|
@McpParameter(description = "접수일 조회 종료일(YYYY-MM-DD)")
|
||||||
|
@McpValidation(format = "date", examples = {"2026-07-31"})
|
||||||
|
private String toDate;
|
||||||
|
|
||||||
|
@McpParameter(description = "반환할 최대 건수")
|
||||||
|
@McpValidation(minimum = 1, maximum = 50, defaultValue = "20")
|
||||||
|
private Integer size;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
@McpTool(
|
||||||
|
routingType = "DIRECT",
|
||||||
|
categoryKey = "cmm"
|
||||||
|
)
|
||||||
|
public interface ClaimSearchSchemaSampleUseCase {
|
||||||
|
|
||||||
|
@McpFunction(
|
||||||
|
register = false,
|
||||||
|
displayName = "청구 조회 JSON Schema 샘플",
|
||||||
|
name = "sample.claim.search.resource",
|
||||||
|
description = "inputSchemaResource를 사용하는 청구 조회 Tool 샘플입니다.",
|
||||||
|
prompt = "청구번호 또는 계약번호로 보험금 청구를 조회해줘.",
|
||||||
|
inputSchemaResource = "classpath:tool-schemas/claim-search-resource-input-schema.json",
|
||||||
|
readOnlyHint = true,
|
||||||
|
idempotentHint = true
|
||||||
|
)
|
||||||
|
Object search(ClaimSearchRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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.usecase.ClaimSearchSchemaSampleUseCase;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* inputSchemaResource 적용 방식을 보여주는 비노출 샘플 Tool이다.
|
||||||
|
* 실제 MCI/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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"claimNo": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "청구번호. CLM 다음 숫자 13자리 형식이다.",
|
||||||
|
"pattern": "^CLM[0-9]{13}$",
|
||||||
|
"examples": ["CLM2026070100123"]
|
||||||
|
},
|
||||||
|
"contractNo": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "계약번호. 숫자 11자리 형식이다.",
|
||||||
|
"pattern": "^[0-9]{11}$",
|
||||||
|
"examples": ["10023456789"]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
|
||||||
|
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 50,
|
||||||
|
"default": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"anyOf": [
|
||||||
|
{ "required": ["claimNo"] },
|
||||||
|
{ "required": ["contractNo"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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 io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ClaimSearchRequestSchemaTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generatesClaimOrContractSearchSchema() {
|
||||||
|
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ClaimSearchRequest.class);
|
||||||
|
Map<String, Map<String, Object>> properties = properties(schema);
|
||||||
|
|
||||||
|
assertEquals(false, schema.get("additionalProperties"));
|
||||||
|
assertEquals("^CLM[0-9]{13}$", properties.get("claimNo").get("pattern"));
|
||||||
|
assertEquals(50L, properties.get("size").get("maximum"));
|
||||||
|
assertEquals(20L, properties.get("size").get("default"));
|
||||||
|
assertEquals(List.of(
|
||||||
|
Map.of("required", List.of("claimNo")),
|
||||||
|
Map.of("required", List.of("contractNo"))), schema.get("anyOf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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