feat: enforce underscore MCP tool names
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s

This commit is contained in:
jade
2026-08-11 10:18:36 +09:00
parent 40303ee9b8
commit 4275f76104
42 changed files with 380 additions and 52 deletions

View File

@@ -144,7 +144,7 @@ UI에서 사용하는 Tailwind CSS와 Chart.js는 `dap-gateway/src/main/resource
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "oth.cmm.claim.search",
"name": "oth_cmm_claim_search",
"arguments": {
"claimNo": "CLM2026070100120"
}
@@ -170,8 +170,8 @@ Agent나 외부 클라이언트의 표준 MCP 진입은 Gateway를 사용합니
Tool 함수명은 아래 4단계 규칙을 사용합니다.
```text
pod.domain.service.action
예: oth.cmm.claim.search
pod_domain_service_action
예: oth_cmm_claim_search
```
- `pod`: Tool Pod 식별자 (`oth`, `sms` 등)
@@ -234,7 +234,7 @@ https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv
배포 전에는 다음을 확인합니다.
- Tool 이름의 전역 중복 여부와 `pod.domain.service.action` 규칙(아래 확인 사항 반영 후)
- Tool 이름의 전역 중복 여부와 `pod_domain_service_action` 규칙(아래 확인 사항 반영 후)
- Request/Response Schema 및 실제 예제 JSON
- Tool Pod 단위 테스트와 Gateway 경유 호출
- MCI/EAI 오류 코드의 사용자용 응답 매핑
@@ -266,6 +266,6 @@ https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv
Tool 관련 공통 기능은 `dap-was-*` 모듈명만 기준으로 동작합니다.
- `validateMcpToolNames``dap-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다.
- Tool Scaffold는 `dap-was-sms`처럼 선택한 Pod 이름을 Tool 함수명 첫 번째 구간에 반영합니다. 예: `sms.cmm.notification.send`
- Tool Scaffold는 `dap-was-sms`처럼 선택한 Pod 이름을 Tool 함수명 첫 번째 구간에 반영합니다. 예: `sms_cmm_notification_send`
- Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다.
- Tool Source Update 기능은 `dap-was-*` 아래의 `*UseCase.java`를 검색합니다.

View File

@@ -0,0 +1,21 @@
package io.shinhanlife.dap.lib.config;
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
/**
* Registers the temporary Glow HTTP compatibility component from the DAP library scan scope.
* The bean is only created when an official GlowHttpComponent has not already been supplied.
*/
@Configuration(proxyBeanMethods = false)
public class AxhubHttpConfiguration {
@Bean
@ConditionalOnMissingBean(GlowHttpComponent.class)
public GlowHttpComponent glowHttpComponent(RestClient.Builder restClientBuilder) {
return new GlowHttpComponent(restClientBuilder);
}
}

View File

@@ -940,7 +940,7 @@ public class ToolScaffolder {
String[] words = normalizedName.split("\\s+");
String service = words[0];
String action = words.length == 1 ? "execute" : words[words.length - 1];
return "%s.%s.%s.%s".formatted(
return "%s_%s_%s_%s".formatted(
pod.toLowerCase(Locale.ROOT),
group.toLowerCase(Locale.ROOT),
service,

View File

@@ -28,7 +28,7 @@ import java.util.regex.Pattern;
public final class McpToolNameValidator {
private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\"");
private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$");
private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-zA-Z0-9_-]{1,128}$");
private McpToolNameValidator() {
}
@@ -139,7 +139,7 @@ public final class McpToolNameValidator {
}
private static String buildInvalidNameMessage(List<Map.Entry<String, List<ToolDeclaration>>> invalidNames) {
StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected pod.domain.service.action using lowercase letters, digits, or hyphens.");
StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected 1-128 characters using letters, digits, underscores, or hyphens.");
for (Map.Entry<String, List<ToolDeclaration>> invalidName : invalidNames) {
message.append("\n\n").append(invalidName.getKey());
invalidName.getValue().stream()

View File

@@ -0,0 +1,31 @@
package io.shinhanlife.dap.lib.config;
import static org.assertj.core.api.Assertions.assertThat;
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.client.RestClient;
class AxhubHttpConfigurationTest {
@Test
void registersGlowHttpComponentFromDapLibConfiguration() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class)) {
assertThat(context.getBean(GlowHttpComponent.class)).isNotNull();
}
}
@Configuration
@Import(AxhubHttpConfiguration.class)
static class TestConfiguration {
@Bean
RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
}

View File

@@ -18,10 +18,10 @@ class McpToolMethodRegistryTest {
registry.initialize();
McpToolMethodRegistry.RegisteredTool tool = registry.find("oth.cmm.echo.search");
McpToolMethodRegistry.RegisteredTool tool = registry.find("oth_cmm_echo_search");
assertNotNull(tool);
assertEquals("execute", tool.method().getName());
assertEquals("oth.cmm.echo.search", tool.annotation().name());
assertEquals("oth_cmm_echo_search", tool.annotation().name());
}
@Test
@@ -42,14 +42,14 @@ class McpToolMethodRegistryTest {
}
static class EchoTool {
@McpTool(name = "oth.cmm.echo.search")
@McpTool(name = "oth_cmm_echo_search")
public String execute(String request) {
return request;
}
}
static class DuplicateEchoTool {
@McpTool(name = "oth.cmm.echo.search")
@McpTool(name = "oth_cmm_echo_search")
public String execute(String request) {
return request;
}

View File

@@ -25,7 +25,7 @@ class ToolScaffolderTest {
String useCase = Files.readString(root.resolve("usecase/ClaimSearchUseCase.java"));
String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java"));
assertTrue(useCase.contains("name = \"oth.cmm.claim.search\""));
assertTrue(useCase.contains("name = \"oth_cmm_claim_search\""));
assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")"));
assertTrue(response.contains("private String resultCode;"));
assertTrue(response.contains("private String resultMessage;"));
@@ -41,7 +41,7 @@ class ToolScaffolderTest {
"src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/NotificationSendUseCase.java");
String useCase = Files.readString(useCasePath);
assertTrue(useCase.contains("name = \"sms.cmm.notification.send\""));
assertTrue(useCase.contains("name = \"sms_cmm_notification_send\""));
}
@Test
@@ -121,7 +121,7 @@ class ToolScaffolderTest {
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
"tester", "2026.08.10", true, null, null, null, List.of(), outputFields);
Path mockResponse = root.resolve("dap-was-oth/src/main/resources/mock-responses/oth.cmm.claim.search.json");
Path mockResponse = root.resolve("dap-was-oth/src/main/resources/mock-responses/oth_cmm_claim_search.json");
Path useCaseTest = root.resolve("dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java");
assertTrue(Files.exists(mockResponse));

View File

@@ -57,13 +57,13 @@ class ToolSchemaResolverTest {
}
static class AutomaticSchemaTool {
@McpTool(name = "oth.test.automatic.search")
@McpTool(name = "oth_test_automatic_search")
void search(AutomaticRequest request) {
}
}
static class AutomaticOutputSchemaTool {
@McpTool(name = "oth.test.output.search")
@McpTool(name = "oth_test_output_search")
SimpleResponse search(AutomaticRequest request) {
return null;
}

View File

@@ -21,16 +21,16 @@ class ToolSourceUpdaterTest {
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
interface SampleUseCase {
@McpTool(name = "oth.cmm.sample.search", description = "old")
@McpTool(name = "oth_cmm_sample_search", description = "old")
@ToolHint(register = false, requiresApproval = false)
void search();
}
""");
ToolSourceUpdater.updateToolSource(temporaryRoot, "oth.cmm.sample.search", "customer", "new", true, true);
ToolSourceUpdater.updateToolSource(temporaryRoot, "oth_cmm_sample_search", "customer", "new", true, true);
String updated = Files.readString(source);
assertTrue(updated.contains("@McpTool(name = \"oth.cmm.sample.search\", description = \"new\")"));
assertTrue(updated.contains("@McpTool(name = \"oth_cmm_sample_search\", description = \"new\")"));
assertTrue(updated.contains("@ToolHint(register = true, requiresApproval = true"));
assertTrue(updated.contains("categoryKey = \"customer\""), updated);
}

View File

@@ -31,35 +31,53 @@ class McpToolNameValidatorTest {
@Test
void rejectsDuplicateMcpToolNamesAcrossToolModules() throws IOException {
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth_sms_notification_send");
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth_sms_notification_send");
IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> McpToolNameValidator.assertUnique(temporaryRoot));
assertTrue(exception.getMessage().contains("oth.sms.notification.send"));
assertTrue(exception.getMessage().contains("oth_sms_notification_send"));
assertTrue(exception.getMessage().contains("dap-was-first"));
assertTrue(exception.getMessage().contains("dap-was-second"));
}
@Test
void validationRunnerRejectsDuplicateMcpToolNamesBeforePackaging() throws IOException {
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth_sms_notification_send");
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth_sms_notification_send");
assertThrows(IllegalStateException.class,
() -> McpToolNameValidationRunner.validate(temporaryRoot));
}
@Test
void rejectsToolNameOutsidePodDomainServiceActionConvention() throws IOException {
writeToolSource("dap-was-first", "FirstTool.java", "first", "bond_issue");
void rejectsToolNameOutsideConfiguredPattern() throws IOException {
writeToolSource("dap-was-first", "FirstTool.java", "first", "bond.issue");
IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> McpToolNameValidator.assertUnique(temporaryRoot));
assertTrue(exception.getMessage().contains("Invalid MCP tool name(s)"));
assertTrue(exception.getMessage().contains("bond_issue"));
assertTrue(exception.getMessage().contains("bond.issue"));
}
@Test
void acceptsLettersDigitsUnderscoresAndDashesWithin128Characters() throws IOException {
String validName = "Tool_Name-" + "a".repeat(118);
writeToolSource("dap-was-first", "FirstTool.java", "first", validName);
assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(temporaryRoot));
}
@Test
void rejectsToolNameLongerThan128Characters() throws IOException {
String invalidName = "a".repeat(129);
writeToolSource("dap-was-first", "FirstTool.java", "first", invalidName);
IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> McpToolNameValidator.assertUnique(temporaryRoot));
assertTrue(exception.getMessage().contains(invalidName));
}
@Test
void acceptsCurrentProjectToolNames() {
@@ -71,11 +89,11 @@ class McpToolNameValidatorTest {
Path root = findProjectRoot();
assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java",
"oth.cmm.customer.detail", "detail");
"oth_cmm_customer_detail", "detail");
assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java",
"oth.cmm.billing.process", "process");
"oth_cmm_billing_process", "process");
assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java",
"oth.cmm.bond.issue", "issue");
"oth_cmm_bond_issue", "issue");
}
private void assertToolName(Path root, String relativePath, String expectedName, String legacyName) throws IOException {

View File

@@ -52,7 +52,7 @@ class ToolManifestServiceTest {
void rejectsEntireManifestWhenToolNameDoesNotMatchConfiguredPrefix() {
McpProperties properties = manifestProperties("insurance-processing", "processing.");
ToolManifestService service = new ToolManifestService(
() -> List.of(tool("notification.sms.send", "1.0.0", 3000)), objectMapper, properties);
() -> List.of(tool("notification_sms_send", "1.0.0", 3000)), objectMapper, properties);
IllegalStateException error = assertThrows(IllegalStateException.class, service::currentManifest);

View File

@@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface BalanceUseCase {
@McpTool(name = "oth.cmm.balance.inquiry", title = "잔고 조회 툴", description = "고객의 계좌 잔액을 조회합니다.")
@McpTool(name = "oth_cmm_balance_inquiry", title = "잔고 조회 툴", description = "고객의 계좌 잔액을 조회합니다.")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "ACC_001")
Object execute(BalanceRequest req);
}

View File

@@ -7,7 +7,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface BillingProcessUseCase {
Object getStatus(BillingStatusRequest req);
@McpTool(name = "oth.cmm.billing.process", title = "청구 프로세스 툴", description = "청구 처리")
@McpTool(name = "oth_cmm_billing_process", title = "청구 프로세스 툴", description = "청구 처리")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "BILL_002")
Object processBilling(BillingProcessRequest data);
}

View File

@@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface BondIssueUseCase {
Object check(BondCheckRequest req);
@McpTool(name = "oth.cmm.bond.issue", title = "채권 발행 툴", description = "증권 발행 테스트1")
@McpTool(name = "oth_cmm_bond_issue", title = "채권 발행 툴", description = "증권 발행 테스트1")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "BOND_002")
Object issue(BondIssueRequest data);
}

View File

@@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
public interface ClaimSearchSchemaSampleUseCase {
@McpTool(name = "oth.cmm.claim.search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true))
@McpTool(name = "oth_cmm_claim_search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true))
@ToolHint(register = false, categoryKey = "cmm",
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json")

View File

@@ -8,11 +8,11 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface CommonUtilityUseCase {
Object registerVacation(VacationRegisterRequest req);
@McpTool(name = "oth.cmm.leave.count", title = "공통 유틸리티 툴", description = "연차 갯수 조회")
@McpTool(name = "oth_cmm_leave_count", title = "공통 유틸리티 툴", description = "연차 갯수 조회")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "HR_VAC_02")
Object getLeaveCount(LeaveCountRequest data);
@McpTool(name = "oth.cmm.secret.execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트")
@McpTool(name = "oth_cmm_secret_execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "SECRET_001")
Object secretTool(LeaveCountRequest data);
}

View File

@@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface ContractInquiryUseCase {
Object getStatus(ContractStatusRequest req);
@McpTool(name = "oth.cmm.contract.detail", title = "계약 상세조회 툴", description = "계약상세 조회")
@McpTool(name = "oth_cmm_contract_detail", title = "계약 상세조회 툴", description = "계약상세 조회")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "CNTR_002")
Object getDetail(ContractDetailRequest data);
}

View File

@@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface CustomerInfoUseCase {
Object getGrade(CustomerGradeRequest req);
@McpTool(name = "oth.cmm.customer.detail", title = "고객 상세조회 툴", description = "고객상세 정보 조회")
@McpTool(name = "oth_cmm_customer_detail", title = "고객 상세조회 툴", description = "고객상세 정보 조회")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "CRM_002")
Object getDetail(CustomerDetailRequest data);
}

View File

@@ -20,7 +20,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
* </pre>
*/
public interface MetaCommonCodeUseCase {
@McpTool(name = "oth.cmm.common-code.lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@McpTool(name = "oth_cmm_commonCode_lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
Object execute(MetaCommonCodeRequest req);
}

View File

@@ -20,7 +20,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
* </pre>
*/
public interface MetaTableUseCase {
@McpTool(name = "oth.cmm.meta.table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@McpTool(name = "oth_cmm_meta_table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
Object execute(MetaTableRequest req);
}

View File

@@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
import java.util.Map;
public interface TemplateUtilityUseCase {
@McpTool(name = "oth.cmm.template.url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.")
@McpTool(name = "oth_cmm_template_url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.")
@ToolHint(categoryKey = "cmm")
Map<String, Object> getTemplateFileUrl(TemplateDownloadRequest req);
}

View File

@@ -5,7 +5,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.oth.dto.*;
public interface Onnba3011UseCase {
@McpTool(name = "oth.oth.onnba3011.call", description = "Onnba3011 호출 툴")
@McpTool(name = "oth_oth_onnba3011_call", description = "Onnba3011 호출 툴")
@ToolHint(categoryKey = "oth", register = true)
Object execute(Onnba3011Request req);
}

View File

@@ -7,7 +7,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse;
public interface DailyQuoteToolUseCase {
@McpTool(name = "oth.smp.quote.daily", title = "오늘의 명언 툴", description = "무작위로 영감을 주는 명언을 하나 가져옵니다.")
@McpTool(name = "oth_smp_quote_daily", title = "오늘의 명언 툴", description = "무작위로 영감을 주는 명언을 하나 가져옵니다.")
@ToolHint(register = false, categoryKey = "smp", mappingId = "QUOTE_001")
DailyQuoteResponse execute(DailyQuoteRequest req);
}

View File

@@ -9,7 +9,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse;
public interface ExchangeRateToolUseCase {
@McpTool(name = "oth.smp.exchange-rate.inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)")
@McpTool(name = "oth_smp_exchangeRate_inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)")
@ToolHint(register = false, categoryKey = "smp", mappingId = "EXCHANGE_001")
ExchangeRateResponse execute(ExchangeRateRequest req);
}

View File

@@ -7,7 +7,7 @@ import org.springaicommunity.mcp.annotation.McpTool;
/** Sample Tool that demonstrates a configured HTTP API integration. */
public interface SampleHttpStatusUseCase {
@McpTool(name = "oth.smp.sample.http.status",
@McpTool(name = "oth_smp_sample_status",
title = "Sample external HTTP API status",
description = "Calls the configured sample HTTP API and returns its status.")
@ToolHint(register = false, categoryKey = "smp", mappingId = "HTTP_SAMPLE_001")

View File

@@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest;
public interface TeamMemberUseCase {
@McpTool(name = "oth.smp.team.list", title = "신한라이프 MCP, TOOL 파트 구성원 조회", description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@McpTool(name = "oth_smp_team_list", title = "신한라이프 MCP, TOOL 파트 구성원 조회", description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "smp", mappingId = "DIRECT0001")
Object execute(TeamMemberRequest req);
}

View File

@@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.smp.dto.*;
public interface WeatherToolUseCase {
@McpTool(name = "oth.smp.weather.inquiry", title = "날씨 조회 툴", description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.")
@McpTool(name = "oth_smp_weather_inquiry", title = "날씨 조회 툴", description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.")
@ToolHint(register = false, categoryKey = "smp", mappingId = "WEATHER_001")
WeatherResponse execute(WeatherRequest req);
}

View File

@@ -21,7 +21,7 @@ import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest;
*/
public interface SolReqDetailUseCase {
@McpTool(name = "oth.sol.request.detail", title = "SolReqDetail 툴", description = "SOL 의뢰서 상세 조회", annotations = @McpTool.McpAnnotations(openWorldHint = true, readOnlyHint = true))
@McpTool(name = "oth_sol_request_detail", title = "SolReqDetail 툴", description = "SOL 의뢰서 상세 조회", annotations = @McpTool.McpAnnotations(openWorldHint = true, readOnlyHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000002")
Object execute(SolReqDetailRequest req);
}

View File

@@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest;
public interface SolReqListUseCase {
@McpTool(name = "oth.sol.request.list", title = "SolReqList 툴", description = "SOL 의뢰서 목록 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@McpTool(name = "oth_sol_request_list", title = "SolReqList 툴", description = "SOL 의뢰서 목록 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000001")
Object execute(SolReqListRequest req);
}

View File

@@ -0,0 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.converter;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_O;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface ClaimSearchConverter {
CLCNNB00001_I toLegacyRequest(ClaimSearchRequest request);
ClaimSearchRequest toRequest(CLCNNB00001_I mciRequest);
ClaimSearchResponse toResponse(CLCNNB00001_O mciRes);
}

View File

@@ -0,0 +1,16 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ClaimSearchRequest {
@Schema(description = "보험금 청구번호", example = "CLM202608100001", requiredMode = Schema.RequiredMode.REQUIRED)
private String claimNo;
@Schema(description = "보험 계약번호", example = "10023456789")
private String contractNo;
}

View File

@@ -0,0 +1,22 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ClaimSearchResponse {
private String resultCode;
private String resultMessage;
@Schema(description = "청구 처리 상태 코드", example = "RECEIVED", requiredMode = Schema.RequiredMode.REQUIRED)
private String status;
@Schema(description = "청구 처리 상태명", example = "접수", requiredMode = Schema.RequiredMode.REQUIRED)
private String statusLabel;
@Schema(description = "승인 금액", example = "150000")
private Long approvedAmount;
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className ClaimSearchUseCase
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.08.10
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.08.10 jade 최초생성
*
* </pre>
*/
public interface ClaimSearchUseCase {
@McpTool(name = "sms_cmm_claim_search", title = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.", description = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "CLCNNB00001",
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json")
ClaimSearchResponse execute(ClaimSearchRequest req);
}

View File

@@ -0,0 +1,68 @@
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.ClaimSearchUseCase;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import io.shinhanlife.dap.mcc.biz.cmm.converter.ClaimSearchConverter;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_O;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.MciNclaClient;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl
* @className ClaimSearchUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.08.10
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.08.10 jade 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ClaimSearchUseCaseImpl implements ClaimSearchUseCase {
private final MciNclaClient mci;
private final ClaimSearchConverter converter;
@Override
public ClaimSearchResponse execute(ClaimSearchRequest req) {
log.info("[MCI Tool] {} 요청 수신.", "sms_cmm_claim_search");
try {
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
CLCNNB00001_I mciReq = converter.toLegacyRequest(req);
Transfer<CLCNNB00001_O> resTransfer = mci.callTo(
"CLCNNB00001",
null,
mciReq,
CLCNNB00001_O.class
);
ClaimSearchResponse response = new ClaimSearchResponse();
if (resTransfer.getBody() != null) {
response = converter.toResponse(resTransfer.getBody());
}
response.setResultCode("SUCCESS");
response.setResultMessage(resTransfer.getBody() != null
? "MCI call completed."
: "MCI call completed without a response body.");
return response;
} catch (Exception e) {
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
ClaimSearchResponse response = new ClaimSearchResponse();
response.setResultCode("ERROR");
response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
return response;
}
}
}

View File

@@ -5,7 +5,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.sms.dto.*;
public interface SmsToolUseCase {
@McpTool(name = "sms.sms.msg.send", title = "SMS 발송 툴", description = "SMS 발송 기능을 제공합니다.")
@McpTool(name = "sms_sms_msg_send", title = "SMS 발송 툴", description = "SMS 발송 기능을 제공합니다.")
@ToolHint(register = false, categoryKey = "sms", mappingId = "SMS_SEND")
Object sendSms(SmsSendRequest req);
}

View File

@@ -0,0 +1,30 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncla;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncla
* @className MciNclaClient
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.08.10
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.08.10 jade 최초생성
*
* </pre>
*/
@Component
@RequiredArgsConstructor
public class MciNclaClient {
private final AxhubMciComponent mci;
public <O> Transfer<O> callTo(String interfaceId, String dummy, Object mciReq, Class<O> resType) throws Exception {
return mci.callTo(interfaceId, dummy, mciReq, resType);
}
}

View File

@@ -0,0 +1,14 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class CLCNNB00001_I {
@Schema(description = "보험금 청구번호", example = "CLM202608100001", requiredMode = Schema.RequiredMode.REQUIRED)
private String claimNo;
@Schema(description = "보험 계약번호", example = "10023456789")
private String contractNo;
}

View File

@@ -0,0 +1,17 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class CLCNNB00001_O {
@Schema(description = "청구 처리 상태 코드", example = "RECEIVED", requiredMode = Schema.RequiredMode.REQUIRED)
private String status;
@Schema(description = "청구 처리 상태명", example = "접수", requiredMode = Schema.RequiredMode.REQUIRED)
private String statusLabel;
@Schema(description = "승인 금액", example = "150000")
private Long approvedAmount;
}

View File

@@ -0,0 +1,5 @@
{
"status" : "RECEIVED",
"statusLabel" : "접수",
"approvedAmount" : 150000
}

View File

@@ -0,0 +1,11 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"TODO_FIELD": {
"type": "string",
"description": "TODO: 파라미터 설명을 입력하세요."
}
},
"required": []
}

View File

@@ -0,0 +1,16 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"status": {
"type": "string",
"description": "처리 결과 상태 (SUCCESS / FAILURE)",
"enum": ["SUCCESS", "FAILURE"]
},
"message": {
"type": "string",
"description": "처리 결과 메시지"
}
},
"required": ["status"]
}

View File

@@ -0,0 +1,16 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
import org.junit.jupiter.api.Test;
class ClaimSearchUseCaseTest {
@Test
void createsToolRequestAndResponseDtos() {
assertNotNull(new ClaimSearchRequest());
assertNotNull(new ClaimSearchResponse());
}
}