refactor: apply new Naming standard (uid, categoryKey, subToolName) across modules
This commit is contained in:
@@ -39,12 +39,12 @@ public class McpBridge {
|
||||
HttpResponse<String> response = client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
// Parse tools from response string manually (basic string manipulation)
|
||||
// axhub-gateway returns: {"result":{"tools":[{"toolName":"other_get_sample_string","description":"...","parametersSchema":{...}}]}}
|
||||
// axhub-gateway returns: {"result":{"tools":[{"subToolName":"other_get_sample_string","description":"...","parametersSchema":{...}}]}}
|
||||
// We need to format it to standard MCP format
|
||||
String rawJson = response.body();
|
||||
// For simplicity, we can just proxy the response if it matches closely, but it doesn't.
|
||||
// Let's just do a naive conversion by replacing "toolName" with "name" and "parametersSchema" with "inputSchema"
|
||||
String transformed = rawJson.replace("\"toolName\"", "\"name\"").replace("\"parametersSchema\"", "\"inputSchema\"");
|
||||
// Let's just do a naive conversion by replacing "subToolName" with "name" and "parametersSchema" with "inputSchema"
|
||||
String transformed = rawJson.replace("\"subToolName\"", "\"name\"").replace("\"parametersSchema\"", "\"inputSchema\"");
|
||||
// Add type: "object" to inputSchema if missing - too complex with string replace, let's hope axhub-gateway already provides correct schema
|
||||
|
||||
// The result format expects: {"tools": [ ... ]}
|
||||
|
||||
@@ -36,7 +36,10 @@ import java.util.Map;
|
||||
public class ToolMetadata {
|
||||
|
||||
// 1. Tool 기본 정보
|
||||
private String toolName; // 툴 고유 명칭 (예: CustomerSearchTool)
|
||||
private String uid; // UUID 형식의 고유 식별자
|
||||
private String semver; // 버전 (예: 1.0.0)
|
||||
private String name; // 사람이 읽는 라벨 (1-128자)
|
||||
private String subToolName; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||
|
||||
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||
@@ -45,8 +48,8 @@ public class ToolMetadata {
|
||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||
private Map<String, String> actionPrompts;
|
||||
|
||||
// 2-1. 도메인 부서 그룹명 (권한 관리에 사용)
|
||||
private String domainGroup;
|
||||
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
|
||||
private String categoryKey;
|
||||
|
||||
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
|
||||
private String endpoint;
|
||||
|
||||
@@ -181,12 +181,13 @@ public class ToolScaffolder {
|
||||
@Service
|
||||
@McpTool(
|
||||
routingType = "%s",
|
||||
group = "%s"
|
||||
categoryKey = "%s"
|
||||
)
|
||||
public class %sService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(
|
||||
name = "%s",
|
||||
name = "%s 툴",
|
||||
subToolName = "%s",
|
||||
description = "%s",
|
||||
prompt = "%s",
|
||||
mappingId = "%s",
|
||||
@@ -200,8 +201,8 @@ public class ToolScaffolder {
|
||||
""".formatted(
|
||||
BASE_PACKAGE, BASE_PACKAGE, BASE_PACKAGE, BASE_PACKAGE, BASE_PACKAGE,
|
||||
BASE_PACKAGE, baseName, author, createDate, createDate, author,
|
||||
routingType, group, baseName,
|
||||
toolName, description, description + " 해줘.", interfaceId, register,
|
||||
routingType, group.toLowerCase(), baseName,
|
||||
baseName, toolName, description, description + " 해줘.", interfaceId, register,
|
||||
baseName, routingType, interfaceId
|
||||
);
|
||||
|
||||
|
||||
@@ -36,7 +36,10 @@ import java.util.Map;
|
||||
public class ToolMetadata {
|
||||
|
||||
// 1. Tool 기본 정보
|
||||
private String toolName; // 툴 고유 명칭 (예: CustomerSearchTool)
|
||||
private String uid; // UUID 형식의 고유 식별자
|
||||
private String semver; // 버전 (예: 1.0.0)
|
||||
private String name; // 사람이 읽는 라벨 (1-128자)
|
||||
private String subToolName; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||
|
||||
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||
@@ -45,8 +48,8 @@ public class ToolMetadata {
|
||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||
private Map<String, String> actionPrompts;
|
||||
|
||||
// 2-1. 도메인 부서 그룹명 (권한 관리에 사용)
|
||||
private String domainGroup;
|
||||
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
|
||||
private String categoryKey;
|
||||
|
||||
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
|
||||
private String endpoint;
|
||||
|
||||
@@ -109,11 +109,11 @@ public class McpRouterController {
|
||||
public ResponseEntity<JsonRpcResponse> listTools() {
|
||||
List<ToolMetadata> activeTools = redisRegistryService.getAllTools()
|
||||
.stream()
|
||||
.filter(ToolMetadata::isVisible)
|
||||
.filter(ToolMetadata::getVisible)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
java.util.Set<String> knownTools = activeTools.stream()
|
||||
.map(ToolMetadata::getToolName)
|
||||
.map(ToolMetadata::getUid)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
java.util.Set<String> fallbackUrls = new java.util.HashSet<>(gatewayFallbackProperties.getRoutes().values());
|
||||
@@ -130,9 +130,9 @@ public class McpRouterController {
|
||||
|
||||
if (localTools != null) {
|
||||
for (ToolMetadata t : localTools) {
|
||||
if (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getToolName())) {
|
||||
if (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getUid())) {
|
||||
activeTools.add(t);
|
||||
knownTools.add(t.getToolName());
|
||||
knownTools.add(t.getUid());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ public class McpRouterController {
|
||||
public ResponseEntity<String> generateToolsMarkdown() {
|
||||
List<ToolMetadata> tools = redisRegistryService.getAllTools()
|
||||
.stream()
|
||||
.filter(ToolMetadata::isVisible)
|
||||
.filter(ToolMetadata::getVisible)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
StringBuilder md = new StringBuilder();
|
||||
@@ -165,7 +165,7 @@ public class McpRouterController {
|
||||
// Group by Domain Group
|
||||
Map<String, List<ToolMetadata>> groupedTools = new java.util.HashMap<>();
|
||||
for (ToolMetadata tool : tools) {
|
||||
String group = tool.getDomainGroup() != null ? tool.getDomainGroup() : "기타 (Others)";
|
||||
String group = tool.getCategoryKey() != null ? tool.getCategoryKey() : "기타 (Others)";
|
||||
groupedTools.computeIfAbsent(group, k -> new java.util.ArrayList<>()).add(tool);
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ public class McpRouterController {
|
||||
|
||||
int index = 1;
|
||||
for (ToolMetadata tool : entry.getValue()) {
|
||||
md.append("### ").append(index++).append(". ").append(tool.getToolName()).append("\n");
|
||||
md.append("### ").append(index++).append(". ").append(tool.getUid()).append("\n");
|
||||
if (tool.getDescription() != null) {
|
||||
md.append("- **설명**: ").append(tool.getDescription()).append("\n");
|
||||
}
|
||||
@@ -223,14 +223,14 @@ public class McpRouterController {
|
||||
}
|
||||
|
||||
@PostMapping("/registry/deregister")
|
||||
public ResponseEntity<String> deregisterTool(@RequestBody String toolName) {
|
||||
redisRegistryService.removeTool(toolName);
|
||||
public ResponseEntity<String> deregisterTool(@RequestBody String uid) {
|
||||
redisRegistryService.removeTool(uid);
|
||||
return ResponseEntity.ok("Deregistered");
|
||||
}
|
||||
|
||||
@PostMapping("/registry/heartbeat")
|
||||
public ResponseEntity<String> heartbeat(@RequestBody String toolName) {
|
||||
boolean success = redisRegistryService.refreshHeartbeat(toolName);
|
||||
public ResponseEntity<String> heartbeat(@RequestBody String uid) {
|
||||
boolean success = redisRegistryService.refreshHeartbeat(uid);
|
||||
if (success) {
|
||||
return ResponseEntity.ok("Heartbeat updated");
|
||||
} else {
|
||||
|
||||
@@ -39,28 +39,28 @@ public class RedisRegistryService {
|
||||
* 툴 등록 및 갱신 (TTL 기반으로 60초 뒤 자동 만료)
|
||||
*/
|
||||
public void saveTool(ToolMetadata meta) {
|
||||
String key = KEY_PREFIX + meta.getToolName();
|
||||
String key = KEY_PREFIX + meta.getUid();
|
||||
meta.setLastHeartbeat(System.currentTimeMillis());
|
||||
redisTemplate.opsForValue().set(key, meta, DEFAULT_TTL);
|
||||
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getToolName());
|
||||
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 하트비트 갱신 (TTL 초기화)
|
||||
*/
|
||||
public boolean refreshHeartbeat(String toolName) {
|
||||
String key = KEY_PREFIX + toolName;
|
||||
public boolean refreshHeartbeat(String uid) {
|
||||
String key = KEY_PREFIX + uid;
|
||||
Boolean exists = redisTemplate.expire(key, DEFAULT_TTL);
|
||||
if (Boolean.TRUE.equals(exists)) {
|
||||
log.debug(" [RedisRegistry] 하트비트 갱신: {}", toolName);
|
||||
log.debug(" [RedisRegistry] 하트비트 갱신: {}", uid);
|
||||
return true;
|
||||
} else {
|
||||
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", toolName);
|
||||
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public List<String> getAvailablePods(String toolName) {
|
||||
ToolMetadata tool = getTool(toolName);
|
||||
public List<String> getAvailablePods(String uid) {
|
||||
ToolMetadata tool = getTool(uid);
|
||||
|
||||
if (tool != null && tool.getPodUrl() != null) {
|
||||
return List.of(tool.getPodUrl());
|
||||
@@ -71,8 +71,8 @@ public class RedisRegistryService {
|
||||
/**
|
||||
* 실행 시 툴 정보 조회 (Tool Execution 시 참조)
|
||||
*/
|
||||
public ToolMetadata getTool(String toolName) {
|
||||
return redisTemplate.opsForValue().get(KEY_PREFIX + toolName);
|
||||
public ToolMetadata getTool(String uid) {
|
||||
return redisTemplate.opsForValue().get(KEY_PREFIX + uid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,8 +90,8 @@ public class RedisRegistryService {
|
||||
/**
|
||||
* 툴 명시적 제거 (Deregister)
|
||||
*/
|
||||
public void removeTool(String toolName) {
|
||||
redisTemplate.delete(KEY_PREFIX + toolName);
|
||||
log.info(" [RedisRegistry] 툴 삭제 완료: {}", toolName);
|
||||
public void removeTool(String uid) {
|
||||
redisTemplate.delete(KEY_PREFIX + uid);
|
||||
log.info(" [RedisRegistry] 툴 삭제 완료: {}", uid);
|
||||
}
|
||||
}
|
||||
@@ -73,19 +73,19 @@ public class ToolPlanner {
|
||||
log.info(" [Planner] Fallback 라우팅 매칭됨: {} -> {}", toolName, fallbackPodUrl);
|
||||
|
||||
toolMetadata = ToolMetadata.builder()
|
||||
.toolName(toolName)
|
||||
.uid(toolName)
|
||||
.integrationType("DIRECT")
|
||||
.podUrl(fallbackPodUrl)
|
||||
.build();
|
||||
}
|
||||
|
||||
// 2-1. [신규] 도메인 그룹핑 기반 권한 검증
|
||||
if (tenantId != null && toolMetadata.getDomainGroup() != null) {
|
||||
if (tenantId != null && toolMetadata.getCategoryKey() != null) {
|
||||
List<String> allowedDomains = securityProperties.getTenantDomains().get(tenantId);
|
||||
if (allowedDomains == null ||
|
||||
(!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getDomainGroup()))) {
|
||||
log.warn(" [Planner] 권한 거부 - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getDomainGroup());
|
||||
throw new SecurityException("해당 도메인(" + toolMetadata.getDomainGroup() + ")의 툴을 실행할 권한이 없습니다.");
|
||||
(!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getCategoryKey()))) {
|
||||
log.warn(" [Planner] 권한 거부 - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getCategoryKey());
|
||||
throw new SecurityException("해당 도메인(" + toolMetadata.getCategoryKey() + ")의 툴을 실행할 권한이 없습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">소속 도메인 그룹</label>
|
||||
<input type="text" class="form-control" name="group" list="groupList" placeholder="선택 또는 직접 입력 (대문자)" oninput="this.value = this.value.toUpperCase()" required>
|
||||
<input type="text" class="form-control" name="categoryKey" pattern="^[a-z0-9][a-z0-9_-]*$" list="groupList" placeholder="선택 또는 직접 입력 (대문자)" oninput="this.value = this.value.toUpperCase()" required>
|
||||
<datalist id="groupList">
|
||||
<option value="COMMON">COMMON (공통)</option>
|
||||
<option value="CUSTOMER">CUSTOMER (고객)</option>
|
||||
@@ -353,7 +353,7 @@
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 10%; min-width: 100px;">그룹</th>
|
||||
<th style="width: 20%; min-width: 150px;">Tool 이름</th>
|
||||
<th style="width: 20%; min-width: 150px;">서브툴 이름</th>
|
||||
<th style="width: 35%; min-width: 200px;">설명</th>
|
||||
<th style="width: 15%; min-width: 180px;">Pod 모듈</th>
|
||||
<th style="width: 20%; min-width: 150px;">인터페이스 (ID)</th>
|
||||
@@ -386,12 +386,12 @@
|
||||
<div class="modal-body">
|
||||
<form id="editToolForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tool 이름 (수정불가)</label>
|
||||
<input type="text" class="form-control" id="editToolName" name="toolName" readonly>
|
||||
<label class="form-label">서브툴 이름 (수정불가)</label>
|
||||
<input type="text" class="form-control" id="editSubToolName" name="subToolName" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">소속 도메인 그룹</label>
|
||||
<input type="text" class="form-control" id="editDomainGroup" name="domainGroup" oninput="this.value = this.value.toUpperCase()">
|
||||
<input type="text" class="form-control" id="editCategoryKey" name="categoryKey" pattern="^[a-z0-9][a-z0-9_-]*$" oninput="this.value = this.value.toUpperCase()">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설명</label>
|
||||
@@ -485,14 +485,14 @@
|
||||
|
||||
// 그룹명 기준으로 오름차순 정렬
|
||||
tools.sort((a, b) => {
|
||||
const gA = a.domainGroup || '';
|
||||
const gB = b.domainGroup || '';
|
||||
const gA = a.categoryKey || '';
|
||||
const gB = b.categoryKey || '';
|
||||
return gA.localeCompare(gB);
|
||||
});
|
||||
|
||||
// 그룹 필터 옵션 채우기
|
||||
const groupSet = new Set();
|
||||
tools.forEach(t => groupSet.add(t.domainGroup || 'UNKNOWN'));
|
||||
tools.forEach(t => groupSet.add(t.categoryKey || 'UNKNOWN'));
|
||||
const groupSelect = document.getElementById('groupFilterSelect');
|
||||
const currentSelectedGroup = groupSelect.value;
|
||||
groupSelect.innerHTML = '<option value="ALL">ALL (전체 그룹)</option>';
|
||||
@@ -513,10 +513,10 @@
|
||||
|
||||
return `
|
||||
<tr class="tool-row">
|
||||
<td class="text-nowrap"><span class="badge bg-secondary tool-group">${t.domainGroup || 'UNKNOWN'}</span></td>
|
||||
<td class="text-nowrap"><span class="badge bg-secondary tool-group">${t.categoryKey || 'UNKNOWN'}</span></td>
|
||||
<td class="fw-bold text-primary text-break tool-name">
|
||||
<a href="javascript:void(0)" onclick="openEditModal('${t.toolName}', '${t.domainGroup}', '${escDesc}', ${isReg})" class="text-decoration-none">
|
||||
${t.toolName}
|
||||
<a href="javascript:void(0)" onclick="openEditModal('${t.subToolName}', '${t.categoryKey}', '${escDesc}', ${isReg})" class="text-decoration-none">
|
||||
${t.subToolName}
|
||||
</a>
|
||||
${badgeHtml}
|
||||
</td>
|
||||
@@ -555,9 +555,9 @@
|
||||
}
|
||||
|
||||
let editModalInstance = null;
|
||||
function openEditModal(toolName, domainGroup, description, isReg) {
|
||||
document.getElementById('editToolName').value = toolName;
|
||||
document.getElementById('editDomainGroup').value = domainGroup !== 'null' ? domainGroup : '';
|
||||
function openEditModal(subToolName, categoryKey, description, isReg) {
|
||||
document.getElementById('editSubToolName').value = subToolName;
|
||||
document.getElementById('editCategoryKey').value = categoryKey !== 'null' ? categoryKey : '';
|
||||
document.getElementById('editDescription').value = description;
|
||||
document.getElementById('editRegister').value = isReg ? 'true' : 'false';
|
||||
|
||||
@@ -568,8 +568,8 @@
|
||||
}
|
||||
|
||||
function submitToolUpdate() {
|
||||
const toolName = document.getElementById('editToolName').value;
|
||||
const domainGroup = document.getElementById('editDomainGroup').value;
|
||||
const subToolName = document.getElementById('editSubToolName').value;
|
||||
const categoryKey = document.getElementById('editCategoryKey').value;
|
||||
const description = document.getElementById('editDescription').value;
|
||||
const register = document.getElementById('editRegister').value;
|
||||
|
||||
@@ -582,8 +582,8 @@
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
toolName: toolName,
|
||||
domainGroup: domainGroup,
|
||||
subToolName: subToolName,
|
||||
categoryKey: categoryKey,
|
||||
description: description,
|
||||
register: register
|
||||
})
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
// Group by domain
|
||||
const grouped = {};
|
||||
tools.forEach(tool => {
|
||||
const group = tool.domainGroup || '기타 (Others)';
|
||||
const group = tool.categoryKey || '기타 (Others)';
|
||||
if (!grouped[group]) grouped[group] = [];
|
||||
grouped[group].push(tool);
|
||||
});
|
||||
@@ -317,7 +317,7 @@
|
||||
<div class="absolute top-4 right-4 px-2 py-0.5 rounded text-[9px] font-bold font-mono border tracking-wider ${typeClass}">
|
||||
${tool.integrationType || 'DIRECT'}
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-white mb-1 pr-16">${tool.toolName}</h3>
|
||||
<h3 class="text-lg font-bold text-white mb-1 pr-16">${tool.subToolName}</h3>
|
||||
<p class="text-sm text-slate-400 leading-relaxed min-h-[40px]">${tool.description || '설명이 없습니다.'}</p>
|
||||
|
||||
${paramHtml}
|
||||
|
||||
@@ -6,7 +6,8 @@ import java.lang.annotation.*;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface McpFunction {
|
||||
String name();
|
||||
String name(); // 사람이 읽는 라벨 (예: "고객 조회 툴")
|
||||
String subToolName(); // MCP 서브툴 명칭 (예: "customer_search")
|
||||
String description();
|
||||
String prompt() default "";
|
||||
String mappingId() default "";
|
||||
|
||||
@@ -13,6 +13,6 @@ public @interface McpTool {
|
||||
@AliasFor(annotation = Component.class)
|
||||
String value() default "";
|
||||
|
||||
String group() default "COMMON";
|
||||
String categoryKey() default "common";
|
||||
String routingType() default "HTTP";
|
||||
}
|
||||
|
||||
@@ -28,11 +28,21 @@ import java.util.Map;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ToolMetadata {
|
||||
private String toolName;
|
||||
private String description;
|
||||
// 1. Tool 기본 정보
|
||||
private String uid; // UUID 형식의 고유 식별자
|
||||
private String semver; // 버전 (예: 1.0.0)
|
||||
private String name; // 사람이 읽는 라벨 (1-128자)
|
||||
private String subToolName; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||
|
||||
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||
private Map<String, Object> parametersSchema;
|
||||
|
||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||
private Map<String, String> actionPrompts;
|
||||
private String domainGroup;
|
||||
|
||||
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
|
||||
private String categoryKey;
|
||||
private String endpoint;
|
||||
private String podUrl;
|
||||
private String integrationType;
|
||||
|
||||
@@ -81,19 +81,23 @@ public class ToolRegistryHeartbeatSender {
|
||||
McpFunction functionAnnotation = AnnotationUtils.findAnnotation(method, McpFunction.class);
|
||||
if (functionAnnotation != null) {
|
||||
String baseName = functionAnnotation.name();
|
||||
String finalName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + baseName
|
||||
: baseName;
|
||||
String rawSubToolName = functionAnnotation.subToolName();
|
||||
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + rawSubToolName
|
||||
: rawSubToolName;
|
||||
|
||||
boolean isRegister = functionAnnotation.register();
|
||||
if (!isRegister) {
|
||||
log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, finalName);
|
||||
log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName);
|
||||
}
|
||||
|
||||
ToolMetadata meta = new ToolMetadata();
|
||||
meta.setToolName(finalName);
|
||||
meta.setUid(java.util.UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
|
||||
meta.setName(baseName);
|
||||
meta.setSubToolName(subToolName);
|
||||
meta.setSemver("1.0.0");
|
||||
meta.setDescription(functionAnnotation.description());
|
||||
meta.setDomainGroup(toolAnnotation.group());
|
||||
meta.setCategoryKey(toolAnnotation.categoryKey());
|
||||
meta.setIntegrationType(toolAnnotation.routingType());
|
||||
meta.setMciServiceId(functionAnnotation.mappingId());
|
||||
meta.setPodUrl(podUrl);
|
||||
@@ -105,7 +109,7 @@ public class ToolRegistryHeartbeatSender {
|
||||
|
||||
Map<String, String> prompts = new HashMap<>();
|
||||
String promptText = functionAnnotation.prompt();
|
||||
prompts.put(finalName, promptText);
|
||||
prompts.put(subToolName, promptText);
|
||||
meta.setActionPrompts(prompts);
|
||||
|
||||
if (method.getParameterCount() > 0) {
|
||||
@@ -117,7 +121,7 @@ public class ToolRegistryHeartbeatSender {
|
||||
finalSchema.put("properties", schema);
|
||||
meta.setParametersSchema(finalSchema);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to generate schema for {}", finalName, e);
|
||||
log.error("Failed to generate schema for {}", subToolName, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +129,7 @@ public class ToolRegistryHeartbeatSender {
|
||||
registeredTools.add(meta);
|
||||
}
|
||||
allScannedTools.add(meta);
|
||||
log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getToolName(), isRegister);
|
||||
log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,15 +145,15 @@ public class ToolRegistryHeartbeatSender {
|
||||
.uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-API-KEY", "SHINHAN_MCP_TEST_KEY_9999")
|
||||
.body(tool.getToolName())
|
||||
.body(tool.getUid())
|
||||
.retrieve()
|
||||
.toEntity(String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getToolName());
|
||||
log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getToolName(), e.getMessage());
|
||||
log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage());
|
||||
registerTool(tool);
|
||||
}
|
||||
}
|
||||
@@ -164,7 +168,7 @@ public class ToolRegistryHeartbeatSender {
|
||||
.body(tool)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getToolName());
|
||||
log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getUid());
|
||||
} catch (Exception ex) {
|
||||
log.error(" [HeartbeatSender] 툴 등록 실패: {}", ex.getMessage());
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ import java.util.Map;
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@McpTool(routingType = "EAI", group = "NOTIFICATION")
|
||||
@McpTool(routingType = "EAI", categoryKey = "notification")
|
||||
public class EmailToolService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(name = "send_email", description = "이메일 발송", prompt = "고객에게 이메일을 발송해줘.", mappingId = "EMAIL_SEND_001")
|
||||
@McpFunction(name = "send_email 툴", subToolName = "send_email", description = "이메일 발송", prompt = "고객에게 이메일을 발송해줘.", mappingId = "EMAIL_SEND_001")
|
||||
public Object sendEmail(EmailSendReq req) {
|
||||
log.info("[Email] 이메일 발송 요청 수신. 수신자: {}", req.getEmailAddress());
|
||||
|
||||
|
||||
@@ -11,11 +11,10 @@ import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@McpTool(routingType = "MCI", group = "COMMON")
|
||||
@McpTool(routingType = "MCI", categoryKey = "common")
|
||||
public class BalanceService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(
|
||||
name = "balance",
|
||||
@McpFunction(name = "balance 툴", subToolName = "balance",
|
||||
description = "고객의 계좌 잔액을 조회합니다.",
|
||||
prompt = "고객 계좌 잔액을 조회해줘.",
|
||||
mappingId = "ACC_001"
|
||||
|
||||
@@ -11,7 +11,7 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.BillingProcessReq;
|
||||
|
||||
@McpTool(
|
||||
routingType = "MCI",
|
||||
group = "CLAIM"
|
||||
categoryKey = "claim"
|
||||
)
|
||||
/**
|
||||
* @package io.shinhanlife.axhub.biz.mcp.tool.service
|
||||
@@ -30,12 +30,12 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.BillingProcessReq;
|
||||
@lombok.extern.slf4j.Slf4j
|
||||
public class BillingProcessService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(name = "status", description = "청구심사 상태 조회", prompt = "현재 접수된 청구건 상태를 알려줘.", mappingId = "BILL_001")
|
||||
@McpFunction(name = "status 툴", subToolName = "status", description = "청구심사 상태 조회", prompt = "현재 접수된 청구건 상태를 알려줘.", mappingId = "BILL_001")
|
||||
public Object getStatus(BillingStatusReq data) {
|
||||
return executeBillingLogic("BILL_001", data);
|
||||
}
|
||||
|
||||
@McpFunction(name = "process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
|
||||
@McpFunction(name = "process 툴", subToolName = "process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
|
||||
public Object processBilling(BillingProcessReq data) {
|
||||
return executeBillingLogic("BILL_002", data);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.BondIssueReq;
|
||||
|
||||
@McpTool(
|
||||
routingType = "EAI",
|
||||
group = "POLICY"
|
||||
categoryKey = "policy"
|
||||
)
|
||||
/**
|
||||
* @package io.shinhanlife.axhub.biz.mcp.tool.service
|
||||
@@ -25,12 +25,12 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.BondIssueReq;
|
||||
*/
|
||||
public class BondIssueService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(name = "check", register = true, description = "발행 가능 여부 조회 테스트", prompt = "디지털 증권 발행 한도가 충분한지 확인해줘.", mappingId = "BOND_001")
|
||||
@McpFunction(name = "check 툴", subToolName = "check", register = true, description = "발행 가능 여부 조회 테스트", prompt = "디지털 증권 발행 한도가 충분한지 확인해줘.", mappingId = "BOND_001")
|
||||
public Object check(BondCheckReq data) {
|
||||
return executeLegacy("EAI", "BOND_001", data);
|
||||
}
|
||||
|
||||
1 @McpFunction(name = "issue", register = true, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
|
||||
@McpFunction(name = "issue 툴", subToolName = "issue", register = true, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
|
||||
public Object issue(BondIssueReq data) {
|
||||
return executeLegacy("EAI", "BOND_002", data);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.LeaveCountReq;
|
||||
|
||||
@McpTool(
|
||||
routingType = "HTTP",
|
||||
group = "HR"
|
||||
categoryKey = "hr"
|
||||
)
|
||||
/**
|
||||
* @package io.shinhanlife.axhub.biz.mcp.tool.service
|
||||
@@ -26,17 +26,17 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.LeaveCountReq;
|
||||
*/
|
||||
public class CommonUtilityService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(name = "register_vacation", description = "휴가 등록", prompt = "내일 하루 연차 휴가를 등록해줘.", mappingId = "HR_VAC_01")
|
||||
@McpFunction(name = "register_vacation 툴", subToolName = "register_vacation", description = "휴가 등록", prompt = "내일 하루 연차 휴가를 등록해줘.", mappingId = "HR_VAC_01")
|
||||
public Object registerVacation(VacationRegisterReq data) {
|
||||
return executeLegacy("HTTP", "HR_VAC_01", data);
|
||||
}
|
||||
|
||||
@McpFunction(name = "get_leave_count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02")
|
||||
@McpFunction(name = "get_leave_count 툴", subToolName = "get_leave_count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02")
|
||||
public Object getLeaveCount(LeaveCountReq data) {
|
||||
return executeLegacy("HTTP", "HR_VAC_02", data);
|
||||
}
|
||||
|
||||
@McpFunction(name = "secret_tool", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false)
|
||||
@McpFunction(name = "secret_tool 툴", subToolName = "secret_tool", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false)
|
||||
public Object secretTool(LeaveCountReq data) {
|
||||
return executeLegacy("HTTP", "SECRET_001", data);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.ContractDetailReq;
|
||||
|
||||
@McpTool(
|
||||
routingType = "HTTP",
|
||||
group = "CONTRACT"
|
||||
categoryKey = "contract"
|
||||
)
|
||||
/**
|
||||
* @package io.shinhanlife.axhub.biz.mcp.tool.service
|
||||
@@ -25,12 +25,12 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.ContractDetailReq;
|
||||
*/
|
||||
public class ContractInquiryService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(name = "contract_status", description = "계약상태 조회", prompt = "김신한 고객의 현재 계약 상태를 조회해줘.", mappingId = "CNTR_001")
|
||||
@McpFunction(name = "contract_status 툴", subToolName = "contract_status", description = "계약상태 조회", prompt = "김신한 고객의 현재 계약 상태를 조회해줘.", mappingId = "CNTR_001")
|
||||
public Object getStatus(ContractStatusReq data) {
|
||||
return executeLegacy("HTTP", "CNTR_001", data);
|
||||
}
|
||||
|
||||
@McpFunction(name = "contract_detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002")
|
||||
@McpFunction(name = "contract_detail 툴", subToolName = "contract_detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002")
|
||||
public Object getDetail(ContractDetailReq data) {
|
||||
return executeLegacy("HTTP", "CNTR_002", data);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.CustomerDetailReq;
|
||||
|
||||
@McpTool(
|
||||
routingType = "TCP",
|
||||
group = "CUSTOMER"
|
||||
categoryKey = "customer"
|
||||
)
|
||||
/**
|
||||
* @package io.shinhanlife.axhub.biz.mcp.tool.service
|
||||
@@ -25,12 +25,12 @@ import io.shinhanlife.axhub.biz.mcp.tool.dto.CustomerDetailReq;
|
||||
*/
|
||||
public class CustomerInfoService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(name = "grade", description = "고객등급 조회", prompt = "이 고객의 VIP 등급을 조회해줘.", mappingId = "CRM_001")
|
||||
@McpFunction(name = "grade 툴", subToolName = "grade", description = "고객등급 조회", prompt = "이 고객의 VIP 등급을 조회해줘.", mappingId = "CRM_001")
|
||||
public Object getGrade(CustomerGradeReq req) {
|
||||
return executeLegacy("TCP", "CRM_001", req);
|
||||
}
|
||||
|
||||
@McpFunction(name = "detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
|
||||
@McpFunction(name = "detail 툴", subToolName = "detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
|
||||
public Object getDetail(CustomerDetailReq data) {
|
||||
return executeLegacy("TCP", "CRM_002", data);
|
||||
}
|
||||
|
||||
@@ -25,12 +25,11 @@ import java.util.Map;
|
||||
@Service
|
||||
@McpTool(
|
||||
routingType = "MCI_STRING",
|
||||
group = "COMMON"
|
||||
categoryKey = "common"
|
||||
)
|
||||
public class SampleStringToolService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(
|
||||
name = "get_sample_string",
|
||||
@McpFunction(name = "get_sample_string 툴", subToolName = "get_sample_string",
|
||||
description = "MCI String 버전과 GlowTrgmField 파싱을 테스트하는 샘플 툴입니다.",
|
||||
prompt = "MCI 전문(String) 연계 및 고정 길이 파싱 테스트 해줘.",
|
||||
mappingId = "TRGM_001"
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.util.Map;
|
||||
@Service
|
||||
@McpTool(
|
||||
routingType = "HTTP",
|
||||
group = "COMMON"
|
||||
categoryKey = "common"
|
||||
)
|
||||
/**
|
||||
* @package io.shinhanlife.axhub.biz.mcp.tool.service
|
||||
@@ -29,8 +29,7 @@ import java.util.Map;
|
||||
*/
|
||||
public class TemplateUtilityService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(
|
||||
name = "get_template_file_url",
|
||||
@McpFunction(name = "get_template_file_url 툴", subToolName = "get_template_file_url",
|
||||
description = "특정 템플릿의 양식 파일(엑셀, 워드 등)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.",
|
||||
prompt = "요청하신 템플릿 양식 파일 다운로드 URL은 다음과 같습니다. 클릭하여 다운로드하세요:",
|
||||
register = false
|
||||
|
||||
@@ -27,12 +27,11 @@ import java.util.UUID;
|
||||
@Service
|
||||
@McpTool(
|
||||
routingType = "MCI",
|
||||
group = "PAYMENT"
|
||||
categoryKey = "payment"
|
||||
)
|
||||
public class PaymentApprovalService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(
|
||||
name = "paymentapproval",
|
||||
@McpFunction(name = "paymentapproval 툴", subToolName = "paymentapproval",
|
||||
description = "결제 승인 처리",
|
||||
prompt = "결제 승인 처리 해줘.",
|
||||
mappingId = "PAY_001"
|
||||
|
||||
@@ -25,10 +25,10 @@ import java.util.Map;
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@McpTool(routingType = "EAI", group = "NOTIFICATION")
|
||||
@McpTool(routingType = "EAI", categoryKey = "notification")
|
||||
public class SmsToolService extends AbstractMcpToolService {
|
||||
|
||||
@McpFunction(name = "send_sms", description = "SMS 발송", prompt = "고객에게 SMS 메시지를 발송해줘.", mappingId = "SMS_SEND_001")
|
||||
@McpFunction(name = "send_sms 툴", subToolName = "send_sms", description = "SMS 발송", prompt = "고객에게 SMS 메시지를 발송해줘.", mappingId = "SMS_SEND_001")
|
||||
public Object sendSms(SmsSendReq req) {
|
||||
log.info("[SMS] SMS 발송 요청 수신. 수신자: {}", req.getPhoneNumber());
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
build:
|
||||
context: ./axhub-gateway
|
||||
ports:
|
||||
- "8081:8081"
|
||||
- "8281:8081"
|
||||
volumes:
|
||||
- ./:/src
|
||||
depends_on:
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
build:
|
||||
context: ./axhub-tool-sms
|
||||
ports:
|
||||
- "8082:8082"
|
||||
- "8282:8082"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
build:
|
||||
context: ./axhub-tool-email
|
||||
ports:
|
||||
- "8083:8083"
|
||||
- "8283:8083"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
build:
|
||||
context: ./axhub-tool-other
|
||||
ports:
|
||||
- "8084:8084"
|
||||
- "8284:8084"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
@@ -93,7 +93,7 @@ services:
|
||||
build:
|
||||
context: ./axhub-tool-payment
|
||||
ports:
|
||||
- "8085:8085"
|
||||
- "8285:8085"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user