feat: Show unregistered tools and add UI filtering

This commit is contained in:
jade
2026-07-09 23:28:22 +09:00
parent 1f38392600
commit 49585082b3
7 changed files with 111 additions and 13 deletions

View File

@@ -55,7 +55,11 @@ public class ToolMetadata {
private String podUrl;
// 2-4. 가시성 여부
private boolean visible;
private boolean visible = true;
// 2-5. Redis 등록 여부 (UI 표출용)
@Builder.Default
private boolean isRegistered = true;
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"

View File

@@ -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.gateway.dto.ToolMetadata;
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.common.mcp.security.SecurityProperties;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.client.RestClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -43,15 +47,20 @@ public class McpRouterController {
private final ExecuteService executeService;
private final SecurityProperties securityProperties;
private final ObjectMapper objectMapper;
private final GatewayFallbackProperties gatewayFallbackProperties;
private final RestClient restClient;
public McpRouterController(RedisRegistryService redisRegistryService,
ExecuteService executeService,
SecurityProperties securityProperties,
ObjectMapper objectMapper) {
ObjectMapper objectMapper,
GatewayFallbackProperties gatewayFallbackProperties) {
this.redisRegistryService = redisRegistryService;
this.executeService = executeService;
this.securityProperties = securityProperties;
this.objectMapper = objectMapper;
this.gatewayFallbackProperties = gatewayFallbackProperties;
this.restClient = RestClient.create();
}
@Operation(summary = "MCP 파이프라인 호출", description = "MCP 표준 파이프라인(ExecuteService)을 통해 레거시 툴을 호출합니다.")
@@ -102,6 +111,35 @@ public class McpRouterController {
.stream()
.filter(ToolMetadata::isVisible)
.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();
response.setId(UUID.randomUUID().toString());

View File

@@ -326,11 +326,17 @@
<!-- Tool List Form -->
<div class="tab-pane fade" id="list" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0 text-secondary">현재 시스템에 등록되어 라우팅 가능한 Tool 목록입니다.</h5>
<button class="btn btn-sm btn-outline-primary" onclick="loadToolList()">🔄 새로고침</button>
<div>
<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 class="table-responsive">
<table class="table table-hover align-middle">
<table class="table table-hover align-middle" id="toolTable">
<thead class="table-light">
<tr>
<th style="width: 10%; min-width: 100px;">그룹</th>
@@ -422,20 +428,43 @@
return;
}
tbody.innerHTML = tools.map(t => `
<tr>
<td class="text-nowrap"><span class="badge bg-secondary">${t.domainGroup || 'UNKNOWN'}</span></td>
<td class="fw-bold text-primary text-break">${t.toolName}</td>
tbody.innerHTML = tools.map(t => {
const isReg = t.registered !== false; // default true
const badgeHtml = isReg ? '' : ' <span class="badge bg-warning text-dark border border-warning" title="Redis 미등록 (직접 호출만 가능)">비공개 (미등록)</span>';
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-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>
</tr>
`).join('');
`}).join('');
// Re-apply filter if text is already present
filterTools();
})
.catch(err => {
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>
</body>