forked from kimhyungsik/ax_hub_mcp_tool
feat: add tool service manifest and scaffold generation
Some checks failed
Deploy Tools / deploy (push) Has been cancelled
Some checks failed
Deploy Tools / deploy (push) Has been cancelled
This commit is contained in:
@@ -3,6 +3,8 @@ server:
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: dat-was-cus
|
name: dat-was-cus
|
||||||
|
config:
|
||||||
|
import: optional:classpath:tool-service-manifest.yml
|
||||||
profiles:
|
profiles:
|
||||||
active: local
|
active: local
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
16
dat-was-cus/src/main/resources/tool-service-manifest.yml
Normal file
16
dat-was-cus/src/main/resources/tool-service-manifest.yml
Normal file
@@ -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: 요청의 주요 엔티티와 업무 영역을 기준으로 고객 서버를 선택합니다.
|
||||||
@@ -5,6 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package io.shinhanlife.dat.lib.config
|
* @package io.shinhanlife.dat.lib.config
|
||||||
@@ -32,6 +33,24 @@ public class McpProperties {
|
|||||||
public static class Manifest {
|
public static class Manifest {
|
||||||
private String bundleId;
|
private String bundleId;
|
||||||
private String namePrefix;
|
private String namePrefix;
|
||||||
|
private List<RoutingFunction> routingFunctions = List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@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<String> primaryEntities = List.of();
|
||||||
|
private List<String> capabilities = List.of();
|
||||||
|
private String selectIf;
|
||||||
|
private String rejectIf;
|
||||||
|
private List<String> confidenceServerIds = List.of();
|
||||||
|
private String decisionPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,20 @@ public class ToolManifestService {
|
|||||||
return new ToolManifestResponse(bundleId, revision(bundleId, tools), tools);
|
return new ToolManifestResponse(bundleId, revision(bundleId, tools), tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ToolServiceManifestResponse currentToolServiceManifest() {
|
||||||
|
McpProperties.Manifest manifest = properties.getManifest();
|
||||||
|
String bundleId = manifest == null ? null : manifest.getBundleId();
|
||||||
|
List<McpProperties.RoutingFunction> 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) {
|
private boolean isMatchCategory(String requestedCategory, String toolCategory, String bundleId) {
|
||||||
if (requestedCategory == null) {
|
if (requestedCategory == null) {
|
||||||
return true;
|
return true;
|
||||||
@@ -168,4 +182,15 @@ public class ToolManifestService {
|
|||||||
private List<String> defaultList(List<String> value) {
|
private List<String> defaultList(List<String> value) {
|
||||||
return value == null ? List.of() : List.copyOf(value);
|
return value == null ? List.of() : List.copyOf(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<McpProperties.RoutingFunction> defaultRoutingList(List<McpProperties.RoutingFunction> 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<McpProperties.RoutingFunction> routingFunctions) {
|
||||||
|
}
|
||||||
@@ -44,11 +44,24 @@ public class PodScaffolder {
|
|||||||
envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||||
}
|
}
|
||||||
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
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,
|
static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName,
|
||||||
String author, String createDate) throws IOException {
|
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));
|
Path modulePath = rootDir.resolve(Paths.get(moduleName));
|
||||||
if (Files.exists(modulePath)) {
|
if (Files.exists(modulePath)) {
|
||||||
return "[오류] 이미 존재하는 모듈입니다: " + moduleName;
|
return "[오류] 이미 존재하는 모듈입니다: " + moduleName;
|
||||||
@@ -129,6 +142,8 @@ public class PodScaffolder {
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: %s
|
name: %s
|
||||||
|
config:
|
||||||
|
import: optional:classpath:tool-service-manifest.yml
|
||||||
profiles:
|
profiles:
|
||||||
active: local
|
active: local
|
||||||
logging:
|
logging:
|
||||||
@@ -146,6 +161,10 @@ public class PodScaffolder {
|
|||||||
""".formatted(portStr, moduleName, moduleName.replace("dap-", ""));
|
""".formatted(portStr, moduleName, moduleName.replace("dap-", ""));
|
||||||
writeUtf8(resPath.resolve("application.yml"), applicationYml);
|
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 = """
|
String applicationLocalYml = """
|
||||||
# Local 환경 전용 설정 (H2 메모리 DB 등)
|
# Local 환경 전용 설정 (H2 메모리 DB 등)
|
||||||
spring:
|
spring:
|
||||||
@@ -363,6 +382,28 @@ public class PodScaffolder {
|
|||||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
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) {
|
private static String capitalize(String str) {
|
||||||
if (str == null || str.isEmpty()) return str;
|
if (str == null || str.isEmpty()) return str;
|
||||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.shinhanlife.dat.mcc.presentation;
|
|||||||
|
|
||||||
import io.shinhanlife.dat.lib.manifest.ToolManifestResponse;
|
import io.shinhanlife.dat.lib.manifest.ToolManifestResponse;
|
||||||
import io.shinhanlife.dat.lib.manifest.ToolManifestService;
|
import io.shinhanlife.dat.lib.manifest.ToolManifestService;
|
||||||
|
import io.shinhanlife.dat.lib.manifest.ToolServiceManifestResponse;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -33,6 +34,11 @@ public class ToolManifestController {
|
|||||||
return handleManifest(toolManifestService.currentManifest(categoryKey), ifNoneMatch);
|
return handleManifest(toolManifestService.currentManifest(categoryKey), ifNoneMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/tool-service-manifest", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<ToolServiceManifestResponse> getToolServiceManifest() {
|
||||||
|
return ResponseEntity.ok(toolManifestService.currentToolServiceManifest());
|
||||||
|
}
|
||||||
|
|
||||||
private ResponseEntity<ToolManifestResponse> handleManifest(ToolManifestResponse manifest, String ifNoneMatch) {
|
private ResponseEntity<ToolManifestResponse> handleManifest(ToolManifestResponse manifest, String ifNoneMatch) {
|
||||||
String eTag = '"' + manifest.revision() + '"';
|
String eTag = '"' + manifest.revision() + '"';
|
||||||
if (eTag.equals(ifNoneMatch)) {
|
if (eTag.equals(ifNoneMatch)) {
|
||||||
@@ -40,4 +46,4 @@ public class ToolManifestController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.ok().eTag(eTag).body(manifest);
|
return ResponseEntity.ok().eTag(eTag).body(manifest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ server:
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: dat-was-pro
|
name: dat-was-pro
|
||||||
|
config:
|
||||||
|
import: optional:classpath:tool-service-manifest.yml
|
||||||
profiles:
|
profiles:
|
||||||
active: local
|
active: local
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
16
dat-was-pro/src/main/resources/tool-service-manifest.yml
Normal file
16
dat-was-pro/src/main/resources/tool-service-manifest.yml
Normal file
@@ -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: 요청의 주요 엔티티와 업무 영역을 기준으로 처리계 서버를 선택합니다.
|
||||||
@@ -3,6 +3,8 @@ server:
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: dat-was-sal
|
name: dat-was-sal
|
||||||
|
config:
|
||||||
|
import: optional:classpath:tool-service-manifest.yml
|
||||||
profiles:
|
profiles:
|
||||||
active: local
|
active: local
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
16
dat-was-sal/src/main/resources/tool-service-manifest.yml
Normal file
16
dat-was-sal/src/main/resources/tool-service-manifest.yml
Normal file
@@ -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: 요청의 주요 엔티티와 업무 영역을 기준으로 영업 서버를 선택합니다.
|
||||||
@@ -3,6 +3,8 @@ server:
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: dat-was-sys
|
name: dat-was-sys
|
||||||
|
config:
|
||||||
|
import: optional:classpath:tool-service-manifest.yml
|
||||||
profiles:
|
profiles:
|
||||||
active: local
|
active: local
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
16
dat-was-sys/src/main/resources/tool-service-manifest.yml
Normal file
16
dat-was-sys/src/main/resources/tool-service-manifest.yml
Normal file
@@ -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: 요청의 주요 엔티티와 업무 영역을 기준으로 시스템 서버를 선택합니다.
|
||||||
Reference in New Issue
Block a user