From 63187429b4644632bee8f88ca0e536e87ba9ab61 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 27 Jul 2026 13:01:40 +0900 Subject: [PATCH] feat: validate MCP tool input constraints --- .../dap/lib/annotation/McpValidation.java | 30 ++++++++++ .../dap/lib/util/JsonSchemaGenerator.java | 15 +++++ .../dap/lib/util/JsonSchemaGeneratorTest.java | 57 +++++++++++++++++++ .../dap/mcc/biz/cmm/dto/BalanceRequest.java | 2 + .../biz/cmm/dto/BillingProcessRequest.java | 5 +- .../dap/mcc/biz/cmm/dto/BondIssueRequest.java | 5 +- .../biz/cmm/dto/VacationRegisterRequest.java | 5 +- .../dap/mcc/biz/sms/dto/SmsSendRequest.java | 5 +- 8 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java create mode 100644 dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java new file mode 100644 index 00000000..2b7d975d --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java @@ -0,0 +1,30 @@ +package io.shinhanlife.dap.lib.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @package io.shinhanlife.dap.lib.annotation + * @className McpValidation + * @description Declares JSON Schema validation constraints for MCP tool input fields + * @author 0986406 + * @create 2026.07.27 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface McpValidation { + boolean required() default false; + String pattern() default ""; + long minimum() default Long.MIN_VALUE; + String[] allowedValues() default {}; +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java index 587e0a1e..46d303b2 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java @@ -3,6 +3,7 @@ package io.shinhanlife.dap.lib.util; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; @@ -58,6 +59,20 @@ public class JsonSchemaGenerator { if ((jsonProp != null && jsonProp.required()) || (paramAnnotation != null && paramAnnotation.required())) { requiredList.add(field.getName()); } + + McpValidation validation = field.getAnnotation(McpValidation.class); + if (validation != null && validation.required() && !requiredList.contains(field.getName())) { + requiredList.add(field.getName()); + } + if (validation != null && !validation.pattern().isEmpty()) { + fieldSchema.put("pattern", validation.pattern()); + } + if (validation != null && validation.minimum() != Long.MIN_VALUE) { + fieldSchema.put("minimum", validation.minimum()); + } + if (validation != null && validation.allowedValues().length > 0) { + fieldSchema.put("enum", List.of(validation.allowedValues())); + } properties.put(field.getName(), fieldSchema); } diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java new file mode 100644 index 00000000..f5340464 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java @@ -0,0 +1,57 @@ +package io.shinhanlife.dap.lib.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import java.util.List; +import java.util.Map; +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 + *
+ * ---------- revision history ----------
+ * date       author    description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406    initial creation
+ * 
+ */ +class JsonSchemaGeneratorTest { + + @Test + void includesMcpParameterConstraintsInGeneratedSchema() { + Map schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class); + Map> properties = properties(schema); + + assertTrue(((List) schema.get("required")).contains("phoneNumber")); + assertTrue(((List) schema.get("required")).contains("approvalStatus")); + assertEquals("^01[0-9]{8,9}$", properties.get("phoneNumber").get("pattern")); + assertEquals(1L, properties.get("amount").get("minimum")); + assertEquals(List.of("APPROVE", "REJECT"), properties.get("approvalStatus").get("enum")); + } + + @SuppressWarnings("unchecked") + private Map> properties(Map schema) { + return (Map>) schema.get("properties"); + } + + private static class ValidatedRequest { + @McpParameter(description = "recipient phone number", required = true) + @McpValidation(pattern = "^01[0-9]{8,9}$") + private String phoneNumber; + + @McpParameter(description = "issue amount", required = true) + @McpValidation(minimum = 1) + private Long amount; + + @McpParameter(description = "approval result") + @McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"}) + private String approvalStatus; + } +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java index e8fb92db..a16c0c7f 100644 --- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java @@ -16,10 +16,12 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; * */ import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; import lombok.Data; @Data public class BalanceRequest { @McpParameter(description = "고객의 계좌번호 (- 제외)", required = true) + @McpValidation(pattern = "\\S") private String accountNumber; } diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java index a788451e..795537ae 100644 --- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java @@ -5,6 +5,7 @@ import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; import lombok.Getter; import lombok.Setter; @@ -30,8 +31,10 @@ import lombok.Setter; public class BillingProcessRequest { @McpParameter(description = "처리할 청구 접수 번호", required = true) + @McpValidation(pattern = "\\S") private String billingId; @McpParameter(description = "심사 승인 여부 (예: APPROVE, REJECT)") + @McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"}) private String approvalStatus; -} \ No newline at end of file +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java index e310fdb2..aa6d8248 100644 --- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java @@ -5,6 +5,7 @@ import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; import lombok.Getter; import lombok.Setter; @@ -30,8 +31,10 @@ import lombok.Setter; public class BondIssueRequest { @McpParameter(description = "발행할 디지털 증권 금액", required = true) + @McpValidation(minimum = 1) private Long amount; @McpParameter(description = "발행 대상 계좌 번호", required = true) + @McpValidation(pattern = "\\S") private String targetAccount; -} \ No newline at end of file +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java index 5fabdb0f..bde48595 100644 --- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java @@ -5,6 +5,7 @@ import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; import lombok.Getter; import lombok.Setter; @@ -30,8 +31,10 @@ import lombok.Setter; public class VacationRegisterRequest { @McpParameter(description = "연차를 등록할 사원 번호", required = true) + @McpValidation(pattern = "\\S") private String employeeId; @McpParameter(description = "휴가 일자 (YYYY-MM-DD 형식)", required = true) + @McpValidation(pattern = "^\\d{4}-\\d{2}-\\d{2}$") private String date; -} \ No newline at end of file +} diff --git a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java index 8912d901..b7cea0df 100644 --- a/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java +++ b/dap-tool-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java @@ -5,6 +5,7 @@ import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; import lombok.Getter; import lombok.Setter; @@ -30,10 +31,12 @@ import lombok.Setter; public class SmsSendRequest { @McpParameter(description = "수신자 전화번호", required = true) + @McpValidation(pattern = "^01[0-9]-?\\d{3,4}-?\\d{4}$") private String phoneNumber; @McpParameter(description = "전송할 메시지 내용", required = true) + @McpValidation(pattern = "\\S") private String message; -} \ No newline at end of file +}