From d0471dfc6a10a4bcf1fa629cd8836492fb7e9030 Mon Sep 17 00:00:00 2001 From: jade Date: Wed, 2 Sep 2026 13:29:29 +0900 Subject: [PATCH] feat: add tool service manifest and scaffold generation --- .../src/main/resources/application.yml | 2 + .../main/resources/tool-service-manifest.yml | 16 +++++++ .../dat/lib/config/McpProperties.java | 21 ++++++++- .../dat/lib/manifest/ToolManifestService.java | 25 +++++++++++ .../manifest/ToolServiceManifestResponse.java | 9 ++++ .../dat/lib/util/PodScaffolder.java | 43 ++++++++++++++++++- .../presentation/ToolManifestController.java | 8 +++- .../src/main/resources/application.yml | 2 + .../main/resources/tool-service-manifest.yml | 16 +++++++ .../src/main/resources/application.yml | 2 + .../main/resources/tool-service-manifest.yml | 16 +++++++ .../src/main/resources/application.yml | 2 + .../main/resources/tool-service-manifest.yml | 16 +++++++ 13 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 dat-was-cus/src/main/resources/tool-service-manifest.yml create mode 100644 dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolServiceManifestResponse.java create mode 100644 dat-was-pro/src/main/resources/tool-service-manifest.yml create mode 100644 dat-was-sal/src/main/resources/tool-service-manifest.yml create mode 100644 dat-was-sys/src/main/resources/tool-service-manifest.yml diff --git a/dat-was-cus/src/main/resources/application.yml b/dat-was-cus/src/main/resources/application.yml index 3e7fa37df..52b49f87d 100644 --- a/dat-was-cus/src/main/resources/application.yml +++ b/dat-was-cus/src/main/resources/application.yml @@ -3,6 +3,8 @@ server: spring: application: name: dat-was-cus + config: + import: optional:classpath:tool-service-manifest.yml profiles: active: local logging: diff --git a/dat-was-cus/src/main/resources/tool-service-manifest.yml b/dat-was-cus/src/main/resources/tool-service-manifest.yml new file mode 100644 index 000000000..dea88a7f5 --- /dev/null +++ b/dat-was-cus/src/main/resources/tool-service-manifest.yml @@ -0,0 +1,16 @@ +mcp: + manifest: + routing-functions: + - name: route_to_dat_was_cus + description: 고객 업무 서버로 요청을 라우팅합니다. + server-id: dat-was-cus + category-key: cus + product-boundary: insurance + business-domain: 고객 관리 + business-outcome: 고객 정보와 고객 상담 업무를 처리합니다. + primary-entities: [고객, 상담, 접촉이력] + capabilities: [고객 조회, 고객 정보 수정, 상담 이력 조회] + select-if: 고객 또는 상담 관련 요청인 경우 + reject-if: 계약, 상품, 시스템 운영 업무가 주된 요청인 경우 + confidence-server-ids: [dat-was-cus] + decision-policy: 요청의 주요 엔티티와 업무 영역을 기준으로 고객 서버를 선택합니다. diff --git a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/config/McpProperties.java b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/config/McpProperties.java index bcc27be4e..bf8a64c28 100644 --- a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/config/McpProperties.java +++ b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/config/McpProperties.java @@ -5,6 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import java.util.Map; +import java.util.List; /** * @package io.shinhanlife.dat.lib.config @@ -32,6 +33,24 @@ public class McpProperties { public static class Manifest { private String bundleId; private String namePrefix; + private List routingFunctions = List.of(); } -} \ No newline at end of file + @Data + public static class RoutingFunction { + private String name; + private String description; + private String serverId; + private String categoryKey; + private String productBoundary; + private String businessDomain; + private String businessOutcome; + private List primaryEntities = List.of(); + private List capabilities = List.of(); + private String selectIf; + private String rejectIf; + private List confidenceServerIds = List.of(); + private String decisionPolicy; + } + +} diff --git a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolManifestService.java b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolManifestService.java index 2697191f9..4c479a5f0 100644 --- a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolManifestService.java +++ b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolManifestService.java @@ -62,6 +62,20 @@ public class ToolManifestService { return new ToolManifestResponse(bundleId, revision(bundleId, tools), tools); } + public ToolServiceManifestResponse currentToolServiceManifest() { + McpProperties.Manifest manifest = properties.getManifest(); + String bundleId = manifest == null ? null : manifest.getBundleId(); + List routingFunctions = manifest == null + ? List.of() : defaultRoutingList(manifest.getRoutingFunctions()); + try { + String source = objectMapper.writeValueAsString(Map.of("bundleId", bundleId, + "routingFunctions", routingFunctions)); + return new ToolServiceManifestResponse(bundleId, sha256(source), routingFunctions); + } catch (Exception exception) { + throw new IllegalStateException("Failed to build Tool Service manifest", exception); + } + } + private boolean isMatchCategory(String requestedCategory, String toolCategory, String bundleId) { if (requestedCategory == null) { return true; @@ -168,4 +182,15 @@ public class ToolManifestService { private List defaultList(List value) { return value == null ? List.of() : List.copyOf(value); } + + private List defaultRoutingList(List value) { + return value == null ? List.of() : List.copyOf(value); + } + + private String sha256(String value) throws Exception { + byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder result = new StringBuilder(); + for (byte item : digest) result.append(String.format("%02x", item)); + return result.toString(); + } } diff --git a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolServiceManifestResponse.java b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolServiceManifestResponse.java new file mode 100644 index 000000000..589e287b0 --- /dev/null +++ b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolServiceManifestResponse.java @@ -0,0 +1,9 @@ +package io.shinhanlife.dat.lib.manifest; + +import io.shinhanlife.dat.lib.config.McpProperties; +import java.util.List; + +/** Server-level routing metadata loaded from tool-service-manifest.yml. */ +public record ToolServiceManifestResponse(String bundleId, String revision, + List routingFunctions) { +} diff --git a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java index d4754f016..680d184de 100644 --- a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java +++ b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java @@ -44,11 +44,24 @@ public class PodScaffolder { envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); } Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); - return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate); + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, null); + } + + public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, + String createDate, String toolServiceManifest) throws IOException { + String envSourceDir = System.getProperty("AXHUB_SOURCE_DIR"); + if (envSourceDir == null || envSourceDir.isBlank()) envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, toolServiceManifest); } static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName, String author, String createDate) throws IOException { + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, null); + } + + static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName, + String author, String createDate, String toolServiceManifest) throws IOException { Path modulePath = rootDir.resolve(Paths.get(moduleName)); if (Files.exists(modulePath)) { return "[오류] 이미 존재하는 모듈입니다: " + moduleName; @@ -129,6 +142,8 @@ public class PodScaffolder { spring: application: name: %s + config: + import: optional:classpath:tool-service-manifest.yml profiles: active: local logging: @@ -146,6 +161,10 @@ public class PodScaffolder { """.formatted(portStr, moduleName, moduleName.replace("dap-", "")); writeUtf8(resPath.resolve("application.yml"), applicationYml); + String manifest = toolServiceManifest == null || toolServiceManifest.isBlank() + ? defaultToolServiceManifest(moduleName) : toolServiceManifest.trim() + System.lineSeparator(); + writeUtf8(resPath.resolve("tool-service-manifest.yml"), manifest); + String applicationLocalYml = """ # Local 환경 전용 설정 (H2 메모리 DB 등) spring: @@ -363,6 +382,28 @@ public class PodScaffolder { Files.writeString(path, content, StandardCharsets.UTF_8); } + private static String defaultToolServiceManifest(String moduleName) { + String key = moduleName.replace("dat-was-", ""); + return """ + mcp: + manifest: + routing-functions: + - name: route_to_%s + description: %s 업무 서버로 요청을 라우팅합니다. + server-id: %s + category-key: %s + product-boundary: insurance + business-domain: %s 업무 + business-outcome: %s 관련 업무를 처리합니다. + primary-entities: [] + capabilities: [] + select-if: %s 관련 요청인 경우 + reject-if: 다른 업무 영역이 주된 요청인 경우 + confidence-server-ids: [%s] + decision-policy: 요청의 주요 업무 영역을 기준으로 서버를 선택합니다. + """.formatted(moduleName.replace("-", "_"), key, moduleName, key, key, key, key, moduleName); + } + private static String capitalize(String str) { if (str == null || str.isEmpty()) return str; return str.substring(0, 1).toUpperCase() + str.substring(1); diff --git a/dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation/ToolManifestController.java b/dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation/ToolManifestController.java index 1aae2d42e..19ec12dfe 100644 --- a/dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation/ToolManifestController.java +++ b/dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation/ToolManifestController.java @@ -2,6 +2,7 @@ package io.shinhanlife.dat.mcc.presentation; import io.shinhanlife.dat.lib.manifest.ToolManifestResponse; import io.shinhanlife.dat.lib.manifest.ToolManifestService; +import io.shinhanlife.dat.lib.manifest.ToolServiceManifestResponse; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -33,6 +34,11 @@ public class ToolManifestController { return handleManifest(toolManifestService.currentManifest(categoryKey), ifNoneMatch); } + @GetMapping(value = "/tool-service-manifest", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getToolServiceManifest() { + return ResponseEntity.ok(toolManifestService.currentToolServiceManifest()); + } + private ResponseEntity handleManifest(ToolManifestResponse manifest, String ifNoneMatch) { String eTag = '"' + manifest.revision() + '"'; if (eTag.equals(ifNoneMatch)) { @@ -40,4 +46,4 @@ public class ToolManifestController { } return ResponseEntity.ok().eTag(eTag).body(manifest); } -} \ No newline at end of file +} diff --git a/dat-was-pro/src/main/resources/application.yml b/dat-was-pro/src/main/resources/application.yml index e413f00bd..ba11656cb 100644 --- a/dat-was-pro/src/main/resources/application.yml +++ b/dat-was-pro/src/main/resources/application.yml @@ -3,6 +3,8 @@ server: spring: application: name: dat-was-pro + config: + import: optional:classpath:tool-service-manifest.yml profiles: active: local logging: diff --git a/dat-was-pro/src/main/resources/tool-service-manifest.yml b/dat-was-pro/src/main/resources/tool-service-manifest.yml new file mode 100644 index 000000000..080f19397 --- /dev/null +++ b/dat-was-pro/src/main/resources/tool-service-manifest.yml @@ -0,0 +1,16 @@ +mcp: + manifest: + routing-functions: + - name: route_to_dat_was_pro + description: 처리계 업무 서버로 요청을 라우팅합니다. + server-id: dat-was-pro + category-key: pro + product-boundary: insurance + business-domain: 보험 계약·청약·보험금 처리 + business-outcome: 보험 계약, 청약, 변경 및 보험금 지급 업무를 처리합니다. + primary-entities: [계약, 청약, 보험금, 지급, 고객] + capabilities: [계약 조회, 청약 처리, 계약 변경, 보험금 지급 조회] + select-if: 보험 계약, 청약, 계약 변경, 보험금 또는 지급 관련 요청인 경우 + reject-if: 상품 소개, 고객 일반정보, 영업조직, 시스템 운영 업무가 주된 요청인 경우 + confidence-server-ids: [dat-was-pro] + decision-policy: 요청의 주요 엔티티와 업무 영역을 기준으로 처리계 서버를 선택합니다. diff --git a/dat-was-sal/src/main/resources/application.yml b/dat-was-sal/src/main/resources/application.yml index 3a1a08e73..5b53d9746 100644 --- a/dat-was-sal/src/main/resources/application.yml +++ b/dat-was-sal/src/main/resources/application.yml @@ -3,6 +3,8 @@ server: spring: application: name: dat-was-sal + config: + import: optional:classpath:tool-service-manifest.yml profiles: active: local logging: diff --git a/dat-was-sal/src/main/resources/tool-service-manifest.yml b/dat-was-sal/src/main/resources/tool-service-manifest.yml new file mode 100644 index 000000000..1060c9da9 --- /dev/null +++ b/dat-was-sal/src/main/resources/tool-service-manifest.yml @@ -0,0 +1,16 @@ +mcp: + manifest: + routing-functions: + - name: route_to_dat_was_sal + description: 영업 업무 서버로 요청을 라우팅합니다. + server-id: dat-was-sal + category-key: sal + product-boundary: insurance + business-domain: 영업 관리 + business-outcome: 영업 조직과 설계사 업무를 처리합니다. + primary-entities: [설계사, 영업조직, 실적] + capabilities: [설계사 조회, 영업조직 조회, 실적 조회] + select-if: 설계사, 영업조직 또는 영업실적 관련 요청인 경우 + reject-if: 고객, 계약, 상품, 시스템 운영 업무가 주된 요청인 경우 + confidence-server-ids: [dat-was-sal] + decision-policy: 요청의 주요 엔티티와 업무 영역을 기준으로 영업 서버를 선택합니다. diff --git a/dat-was-sys/src/main/resources/application.yml b/dat-was-sys/src/main/resources/application.yml index a5016ea68..a76e432c4 100644 --- a/dat-was-sys/src/main/resources/application.yml +++ b/dat-was-sys/src/main/resources/application.yml @@ -3,6 +3,8 @@ server: spring: application: name: dat-was-sys + config: + import: optional:classpath:tool-service-manifest.yml profiles: active: local logging: diff --git a/dat-was-sys/src/main/resources/tool-service-manifest.yml b/dat-was-sys/src/main/resources/tool-service-manifest.yml new file mode 100644 index 000000000..d9446ac81 --- /dev/null +++ b/dat-was-sys/src/main/resources/tool-service-manifest.yml @@ -0,0 +1,16 @@ +mcp: + manifest: + routing-functions: + - name: route_to_dat_was_sys + description: 시스템 업무 서버로 요청을 라우팅합니다. + server-id: dat-was-sys + category-key: sys + product-boundary: insurance + business-domain: 시스템 운영 관리 + business-outcome: 시스템 상태와 공통 운영 업무를 처리합니다. + primary-entities: [시스템, 사용자, 권한] + capabilities: [시스템 조회, 사용자 조회, 권한 조회] + select-if: 시스템 운영, 사용자, 권한 관련 요청인 경우 + reject-if: 고객, 계약, 상품, 영업 업무가 주된 요청인 경우 + confidence-server-ids: [dat-was-sys] + decision-policy: 요청의 주요 엔티티와 업무 영역을 기준으로 시스템 서버를 선택합니다.