feat: Show unregistered tools and add UI filtering
This commit is contained in:
@@ -54,6 +54,13 @@ public class ToolMetadata {
|
|||||||
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
|
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
|
||||||
private String podUrl;
|
private String podUrl;
|
||||||
|
|
||||||
|
// 2-4. 가시성 여부
|
||||||
|
private boolean visible = true;
|
||||||
|
|
||||||
|
// 2-5. Redis 등록 여부 (UI 표출용)
|
||||||
|
@Builder.Default
|
||||||
|
private boolean isRegistered = true;
|
||||||
|
|
||||||
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
||||||
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ public class ToolMetadata {
|
|||||||
private String podUrl;
|
private String podUrl;
|
||||||
|
|
||||||
// 2-4. 가시성 여부
|
// 2-4. 가시성 여부
|
||||||
private boolean visible;
|
private boolean visible = true;
|
||||||
|
|
||||||
|
// 2-5. Redis 등록 여부 (UI 표출용)
|
||||||
|
@Builder.Default
|
||||||
|
private boolean isRegistered = true;
|
||||||
|
|
||||||
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
||||||
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ import io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcResponse;
|
|||||||
import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params;
|
import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params;
|
||||||
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||||
import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService;
|
import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.config.GatewayFallbackProperties;
|
||||||
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService;
|
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService;
|
||||||
import io.shinhanlife.axhub.common.mcp.security.SecurityProperties;
|
import io.shinhanlife.axhub.common.mcp.security.SecurityProperties;
|
||||||
|
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -43,15 +47,20 @@ public class McpRouterController {
|
|||||||
private final ExecuteService executeService;
|
private final ExecuteService executeService;
|
||||||
private final SecurityProperties securityProperties;
|
private final SecurityProperties securityProperties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final GatewayFallbackProperties gatewayFallbackProperties;
|
||||||
|
private final RestClient restClient;
|
||||||
|
|
||||||
public McpRouterController(RedisRegistryService redisRegistryService,
|
public McpRouterController(RedisRegistryService redisRegistryService,
|
||||||
ExecuteService executeService,
|
ExecuteService executeService,
|
||||||
SecurityProperties securityProperties,
|
SecurityProperties securityProperties,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper,
|
||||||
|
GatewayFallbackProperties gatewayFallbackProperties) {
|
||||||
this.redisRegistryService = redisRegistryService;
|
this.redisRegistryService = redisRegistryService;
|
||||||
this.executeService = executeService;
|
this.executeService = executeService;
|
||||||
this.securityProperties = securityProperties;
|
this.securityProperties = securityProperties;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
|
this.gatewayFallbackProperties = gatewayFallbackProperties;
|
||||||
|
this.restClient = RestClient.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "MCP 파이프라인 호출", description = "MCP 표준 파이프라인(ExecuteService)을 통해 레거시 툴을 호출합니다.")
|
@Operation(summary = "MCP 파이프라인 호출", description = "MCP 표준 파이프라인(ExecuteService)을 통해 레거시 툴을 호출합니다.")
|
||||||
@@ -102,6 +111,35 @@ public class McpRouterController {
|
|||||||
.stream()
|
.stream()
|
||||||
.filter(ToolMetadata::isVisible)
|
.filter(ToolMetadata::isVisible)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
|
java.util.Set<String> knownTools = activeTools.stream()
|
||||||
|
.map(ToolMetadata::getToolName)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
|
||||||
|
java.util.Set<String> fallbackUrls = new java.util.HashSet<>(gatewayFallbackProperties.getRoutes().values());
|
||||||
|
if (gatewayFallbackProperties.getDefaultUrl() != null) {
|
||||||
|
fallbackUrls.add(gatewayFallbackProperties.getDefaultUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String url : fallbackUrls) {
|
||||||
|
try {
|
||||||
|
List<ToolMetadata> localTools = restClient.get()
|
||||||
|
.uri(url + "/mcp/api/v1/tools/local")
|
||||||
|
.retrieve()
|
||||||
|
.body(new ParameterizedTypeReference<List<ToolMetadata>>() {});
|
||||||
|
|
||||||
|
if (localTools != null) {
|
||||||
|
for (ToolMetadata t : localTools) {
|
||||||
|
if (!t.isRegistered() && t.isVisible() && !knownTools.contains(t.getToolName())) {
|
||||||
|
activeTools.add(t);
|
||||||
|
knownTools.add(t.getToolName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to fetch local tools from fallback URL: {}", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JsonRpcResponse response = new JsonRpcResponse();
|
JsonRpcResponse response = new JsonRpcResponse();
|
||||||
response.setId(UUID.randomUUID().toString());
|
response.setId(UUID.randomUUID().toString());
|
||||||
|
|||||||
@@ -326,11 +326,17 @@
|
|||||||
<!-- Tool List Form -->
|
<!-- Tool List Form -->
|
||||||
<div class="tab-pane fade" id="list" role="tabpanel">
|
<div class="tab-pane fade" id="list" role="tabpanel">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h5 class="mb-0 text-secondary">현재 시스템에 등록되어 라우팅 가능한 Tool 목록입니다.</h5>
|
<div>
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="loadToolList()">🔄 새로고침</button>
|
<h5 class="mb-1 text-secondary">현재 시스템에 라우팅 가능한 Tool 목록입니다.</h5>
|
||||||
|
<small class="text-muted">Redis에 등록되지 않은(비공개) Tool도 포함되어 조회됩니다.</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<input type="text" id="toolFilter" class="form-control form-control-sm" placeholder="🔍 이름 또는 그룹 검색..." onkeyup="filterTools()">
|
||||||
|
<button class="btn btn-sm btn-outline-primary text-nowrap" onclick="loadToolList()">🔄 새로고침</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle" id="toolTable">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 10%; min-width: 100px;">그룹</th>
|
<th style="width: 10%; min-width: 100px;">그룹</th>
|
||||||
@@ -422,20 +428,43 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = tools.map(t => `
|
tbody.innerHTML = tools.map(t => {
|
||||||
<tr>
|
const isReg = t.registered !== false; // default true
|
||||||
<td class="text-nowrap"><span class="badge bg-secondary">${t.domainGroup || 'UNKNOWN'}</span></td>
|
const badgeHtml = isReg ? '' : ' <span class="badge bg-warning text-dark border border-warning" title="Redis 미등록 (직접 호출만 가능)">비공개 (미등록)</span>';
|
||||||
<td class="fw-bold text-primary text-break">${t.toolName}</td>
|
|
||||||
|
return `
|
||||||
|
<tr class="tool-row">
|
||||||
|
<td class="text-nowrap"><span class="badge bg-secondary tool-group">${t.domainGroup || 'UNKNOWN'}</span></td>
|
||||||
|
<td class="fw-bold text-primary text-break tool-name">${t.toolName}${badgeHtml}</td>
|
||||||
<td class="text-break">${t.description}</td>
|
<td class="text-break">${t.description}</td>
|
||||||
<td class="text-nowrap"><span class="badge bg-light text-dark border">${t.podUrl}</span></td>
|
<td class="text-nowrap"><span class="badge bg-light text-dark border">${t.podUrl}</span></td>
|
||||||
<td class="text-nowrap"><span class="badge bg-info text-dark">${t.integrationType}</span> <small class="text-muted ms-1">${t.mciServiceId || '-'}</small></td>
|
<td class="text-nowrap"><span class="badge bg-info text-dark">${t.integrationType}</span> <small class="text-muted ms-1">${t.mciServiceId || '-'}</small></td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`}).join('');
|
||||||
|
|
||||||
|
// Re-apply filter if text is already present
|
||||||
|
filterTools();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
tbody.innerHTML = `<tr><td colspan="5" class="text-center py-4 text-danger">목록을 불러오는 중 오류가 발생했습니다: ${err.message}</td></tr>`;
|
tbody.innerHTML = `<tr><td colspan="5" class="text-center py-4 text-danger">목록을 불러오는 중 오류가 발생했습니다: ${err.message}</td></tr>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterTools() {
|
||||||
|
const input = document.getElementById('toolFilter').value.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll('.tool-row');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const name = row.querySelector('.tool-name').textContent.toLowerCase();
|
||||||
|
const group = row.querySelector('.tool-group').textContent.toLowerCase();
|
||||||
|
|
||||||
|
if (name.includes(input) || group.includes(input)) {
|
||||||
|
row.style.display = '';
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ public class ToolMetadata {
|
|||||||
private String mciServiceId;
|
private String mciServiceId;
|
||||||
|
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
private boolean visible = true;
|
private boolean visible = true;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private boolean isRegistered = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.shinhanlife.axhub.biz.mcp.tool.presentation;
|
package io.shinhanlife.axhub.biz.mcp.tool.presentation;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -23,6 +24,8 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
|
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
|
||||||
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction;
|
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction;
|
||||||
import io.shinhanlife.axhub.biz.mcp.tool.config.McpProperties;
|
import io.shinhanlife.axhub.biz.mcp.tool.config.McpProperties;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.tool.dto.ToolMetadata;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.tool.service.ToolRegistryHeartbeatSender;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator;
|
import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
@@ -54,6 +57,13 @@ public class BusinessToolController {
|
|||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final McpProperties mcpProperties;
|
private final McpProperties mcpProperties;
|
||||||
|
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
|
||||||
|
|
||||||
|
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
||||||
|
@GetMapping("/mcp/api/v1/tools/local")
|
||||||
|
public List<ToolMetadata> getLocalTools() {
|
||||||
|
return toolRegistryHeartbeatSender.getAllScannedTools();
|
||||||
|
}
|
||||||
|
|
||||||
// JSON RPC 기반 단일 라우팅 엔드포인트
|
// JSON RPC 기반 단일 라우팅 엔드포인트
|
||||||
@PostMapping("/mcp/api/v1/tools/call")
|
@PostMapping("/mcp/api/v1/tools/call")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
|
|||||||
import io.shinhanlife.axhub.biz.mcp.tool.dto.ToolMetadata;
|
import io.shinhanlife.axhub.biz.mcp.tool.dto.ToolMetadata;
|
||||||
import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator;
|
import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -60,6 +61,9 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
private String podUrl;
|
private String podUrl;
|
||||||
|
|
||||||
private List<ToolMetadata> registeredTools = new ArrayList<>();
|
private List<ToolMetadata> registeredTools = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<ToolMetadata> allScannedTools = new ArrayList<>();
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
@@ -89,7 +93,6 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
boolean isRegister = prop != null && prop.getRegister() != null ? prop.getRegister() : functionAnnotation.register();
|
boolean isRegister = prop != null && prop.getRegister() != null ? prop.getRegister() : functionAnnotation.register();
|
||||||
if (!isRegister) {
|
if (!isRegister) {
|
||||||
log.info(" [HeartbeatSender] '{}' 툴은 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, finalName);
|
log.info(" [HeartbeatSender] '{}' 툴은 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, finalName);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolMetadata meta = new ToolMetadata();
|
ToolMetadata meta = new ToolMetadata();
|
||||||
@@ -102,6 +105,7 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
|
|
||||||
boolean isVisible = prop != null && prop.getVisible() != null ? prop.getVisible() : functionAnnotation.visible();
|
boolean isVisible = prop != null && prop.getVisible() != null ? prop.getVisible() : functionAnnotation.visible();
|
||||||
meta.setVisible(isVisible);
|
meta.setVisible(isVisible);
|
||||||
|
meta.setRegistered(isRegister);
|
||||||
|
|
||||||
Map<String, String> prompts = new HashMap<>();
|
Map<String, String> prompts = new HashMap<>();
|
||||||
String promptText = prop != null && prop.getPrompt() != null ? prop.getPrompt() : functionAnnotation.prompt();
|
String promptText = prop != null && prop.getPrompt() != null ? prop.getPrompt() : functionAnnotation.prompt();
|
||||||
@@ -121,8 +125,11 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredTools.add(meta);
|
if (isRegister) {
|
||||||
log.info(" [HeartbeatSender] 도구 메타데이터 생성: {}", meta.getToolName());
|
registeredTools.add(meta);
|
||||||
|
}
|
||||||
|
allScannedTools.add(meta);
|
||||||
|
log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getToolName(), isRegister);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user