From 49585082b3939c9db77ddba0fd759069b3f335d7 Mon Sep 17 00:00:00 2001 From: jade Date: Thu, 9 Jul 2026 23:28:22 +0900 Subject: [PATCH] feat: Show unregistered tools and add UI filtering --- .../biz/mcp/gateway/dto/ToolMetadata.java | 7 +++ .../biz/mcp/gateway/dto/ToolMetadata.java | 6 ++- .../presentation/McpRouterController.java | 40 ++++++++++++++++- .../main/resources/static/admin/scaffold.html | 45 +++++++++++++++---- .../axhub/biz/mcp/tool/dto/ToolMetadata.java | 3 ++ .../presentation/BusinessToolController.java | 10 +++++ .../service/ToolRegistryHeartbeatSender.java | 13 ++++-- 7 files changed, 111 insertions(+), 13 deletions(-) diff --git a/axhub-common/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java b/axhub-common/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java index b6a85f8..bf44216 100644 --- a/axhub-common/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java +++ b/axhub-common/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java @@ -54,6 +54,13 @@ public class ToolMetadata { // 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082) private String podUrl; + // 2-4. 가시성 여부 + private boolean visible = true; + + // 2-5. Redis 등록 여부 (UI 표출용) + @Builder.Default + private boolean isRegistered = true; + // 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI) private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI" diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java index af58a40..6b3d808 100644 --- a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java +++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/dto/ToolMetadata.java @@ -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" diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java index f5dde94..5efc440 100644 --- a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java +++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java @@ -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 knownTools = activeTools.stream() + .map(ToolMetadata::getToolName) + .collect(java.util.stream.Collectors.toSet()); + + java.util.Set fallbackUrls = new java.util.HashSet<>(gatewayFallbackProperties.getRoutes().values()); + if (gatewayFallbackProperties.getDefaultUrl() != null) { + fallbackUrls.add(gatewayFallbackProperties.getDefaultUrl()); + } + + for (String url : fallbackUrls) { + try { + List localTools = restClient.get() + .uri(url + "/mcp/api/v1/tools/local") + .retrieve() + .body(new ParameterizedTypeReference>() {}); + + 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()); diff --git a/axhub-gateway/src/main/resources/static/admin/scaffold.html b/axhub-gateway/src/main/resources/static/admin/scaffold.html index 66b00d0..a9cfd2d 100644 --- a/axhub-gateway/src/main/resources/static/admin/scaffold.html +++ b/axhub-gateway/src/main/resources/static/admin/scaffold.html @@ -326,11 +326,17 @@
-
현재 시스템에 등록되어 라우팅 가능한 Tool 목록입니다.
- +
+
현재 시스템에 라우팅 가능한 Tool 목록입니다.
+ Redis에 등록되지 않은(비공개) Tool도 포함되어 조회됩니다. +
+
+ + +
- +
@@ -422,20 +428,43 @@ return; } - tbody.innerHTML = tools.map(t => ` - - - + tbody.innerHTML = tools.map(t => { + const isReg = t.registered !== false; // default true + const badgeHtml = isReg ? '' : ' 비공개 (미등록)'; + + return ` + + + - `).join(''); + `}).join(''); + + // Re-apply filter if text is already present + filterTools(); }) .catch(err => { tbody.innerHTML = ``; }); } + + 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'; + } + }); + } diff --git a/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/dto/ToolMetadata.java b/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/dto/ToolMetadata.java index 8dd09d7..fd57bde 100644 --- a/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/dto/ToolMetadata.java +++ b/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/dto/ToolMetadata.java @@ -39,7 +39,10 @@ public class ToolMetadata { private String mciServiceId; + @Builder.Default private boolean visible = true; + @Builder.Default + private boolean isRegistered = true; } \ No newline at end of file diff --git a/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/presentation/BusinessToolController.java b/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/presentation/BusinessToolController.java index 3df28d0..ec7d67d 100644 --- a/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/presentation/BusinessToolController.java +++ b/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/presentation/BusinessToolController.java @@ -1,6 +1,7 @@ package io.shinhanlife.axhub.biz.mcp.tool.presentation; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; 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.McpFunction; 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 io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator; import org.springframework.util.ClassUtils; @@ -54,6 +57,13 @@ public class BusinessToolController { private final ApplicationContext applicationContext; private final ObjectMapper objectMapper; private final McpProperties mcpProperties; + private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender; + + // 내부 조회용 로컬 Tool 목록 엔드포인트 + @GetMapping("/mcp/api/v1/tools/local") + public List getLocalTools() { + return toolRegistryHeartbeatSender.getAllScannedTools(); + } // JSON RPC 기반 단일 라우팅 엔드포인트 @PostMapping("/mcp/api/v1/tools/call") diff --git a/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/service/ToolRegistryHeartbeatSender.java b/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/service/ToolRegistryHeartbeatSender.java index 4baa49f..e5efa43 100644 --- a/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/service/ToolRegistryHeartbeatSender.java +++ b/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/service/ToolRegistryHeartbeatSender.java @@ -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.util.JsonSchemaGenerator; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -60,6 +61,9 @@ public class ToolRegistryHeartbeatSender { private String podUrl; private List registeredTools = new ArrayList<>(); + + @Getter + private List allScannedTools = new ArrayList<>(); @PostConstruct public void init() { @@ -89,7 +93,6 @@ public class ToolRegistryHeartbeatSender { boolean isRegister = prop != null && prop.getRegister() != null ? prop.getRegister() : functionAnnotation.register(); if (!isRegister) { log.info(" [HeartbeatSender] '{}' 툴은 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, finalName); - continue; } ToolMetadata meta = new ToolMetadata(); @@ -102,6 +105,7 @@ public class ToolRegistryHeartbeatSender { boolean isVisible = prop != null && prop.getVisible() != null ? prop.getVisible() : functionAnnotation.visible(); meta.setVisible(isVisible); + meta.setRegistered(isRegister); Map prompts = new HashMap<>(); String promptText = prop != null && prop.getPrompt() != null ? prop.getPrompt() : functionAnnotation.prompt(); @@ -121,8 +125,11 @@ public class ToolRegistryHeartbeatSender { } } - registeredTools.add(meta); - log.info(" [HeartbeatSender] 도구 메타데이터 생성: {}", meta.getToolName()); + if (isRegister) { + registeredTools.add(meta); + } + allScannedTools.add(meta); + log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getToolName(), isRegister); } } }
그룹
${t.domainGroup || 'UNKNOWN'}${t.toolName}
${t.domainGroup || 'UNKNOWN'}${t.toolName}${badgeHtml} ${t.description} ${t.podUrl} ${t.integrationType} ${t.mciServiceId || '-'}
목록을 불러오는 중 오류가 발생했습니다: ${err.message}