diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/DynamicMcpRouterConfig.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/DynamicMcpRouterConfig.java
new file mode 100644
index 0000000..78bf870
--- /dev/null
+++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/DynamicMcpRouterConfig.java
@@ -0,0 +1,29 @@
+package io.shinhanlife.axhub.biz.mcp.gateway.sync;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.function.RouterFunction;
+import org.springframework.web.servlet.function.ServerResponse;
+
+/**
+ * @package io.shinhanlife.axhub.biz.mcp.gateway.sync
+ * @className DynamicMcpRouterConfig
+ * @description AX HUB 시스템 처리 클래스
+ * @author 김형식
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 김형식 최초생성
+ *
+ *
+ */
+@Configuration
+public class DynamicMcpRouterConfig {
+
+ @Bean
+ public RouterFunction dynamicMcpRouterFunction(DynamicMcpServerManager manager) {
+ return manager.getDynamicRouter();
+ }
+}
diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/DynamicMcpServerManager.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/DynamicMcpServerManager.java
new file mode 100644
index 0000000..ed1779d
--- /dev/null
+++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/DynamicMcpServerManager.java
@@ -0,0 +1,117 @@
+package io.shinhanlife.axhub.biz.mcp.gateway.sync;
+
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpSyncServer;
+import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.mcp.server.webmvc.transport.WebMvcSseServerTransportProvider;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.function.HandlerFunction;
+import org.springframework.web.servlet.function.RouterFunction;
+import org.springframework.web.servlet.function.ServerResponse;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * @package io.shinhanlife.axhub.biz.mcp.gateway.sync
+ * @className DynamicMcpServerManager
+ * @description AX HUB 시스템 처리 클래스
+ * @author 김형식
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 김형식 최초생성
+ *
+ *
+ */
+@Component
+public class DynamicMcpServerManager {
+ private static final Logger log = LoggerFactory.getLogger(DynamicMcpServerManager.class);
+
+ private final Map categoryServers = new ConcurrentHashMap<>();
+ private final Map categoryTransports = new ConcurrentHashMap<>();
+ private final Map> managedToolNamesPerCategory = new ConcurrentHashMap<>();
+
+ private final RegistryMcpToolSpecificationFactory specificationFactory;
+
+ public DynamicMcpServerManager(RegistryMcpToolSpecificationFactory specificationFactory) {
+ this.specificationFactory = specificationFactory;
+ }
+
+ public void synchronizeCategory(String categoryKey, List tools) {
+ if (categoryKey == null || categoryKey.trim().isEmpty()) {
+ categoryKey = "common";
+ }
+ String safeCategory = categoryKey.toLowerCase();
+
+ McpSyncServer server = categoryServers.computeIfAbsent(safeCategory, key -> {
+ log.info("Creating dynamic MCP Server for category: {}", key);
+ String ssePath = "common".equals(key) ? "/mcp/sse" : "/mcp/sse/" + key;
+ String msgPath = "common".equals(key) ? "/mcp/message" : "/mcp/message/" + key;
+
+ WebMvcSseServerTransportProvider transport = WebMvcSseServerTransportProvider.builder()
+ .sseEndpoint(ssePath)
+ .messageEndpoint(msgPath)
+ .build();
+
+ McpSyncServer newServer = McpServer.sync(transport)
+ .serverInfo("AXHUB-Gateway-" + key, "1.0.0")
+ .build();
+
+ categoryTransports.put(key, transport);
+ managedToolNamesPerCategory.put(key, ConcurrentHashMap.newKeySet());
+ return newServer;
+ });
+
+ Set managedToolNames = managedToolNamesPerCategory.get(safeCategory);
+ Set activeNames = tools.stream()
+ .map(ToolMetadata::getName)
+ .collect(Collectors.toSet());
+
+ Set removedNames = managedToolNames.stream()
+ .filter(name -> !activeNames.contains(name))
+ .collect(Collectors.toSet());
+
+ for (String removedName : removedNames) {
+ server.removeTool(removedName);
+ managedToolNames.remove(removedName);
+ log.info("[{}] Removed tool: {}", safeCategory, removedName);
+ }
+
+ for (ToolMetadata entry : tools) {
+ if (!managedToolNames.contains(entry.getName())) {
+ server.addTool(specificationFactory.create(entry));
+ managedToolNames.add(entry.getName());
+ log.info("[{}] Added tool: {}", safeCategory, entry.getName());
+ } else {
+ server.removeTool(entry.getName());
+ server.addTool(specificationFactory.create(entry));
+ }
+ }
+ }
+
+ public Set getKnownCategories() {
+ return Collections.unmodifiableSet(categoryServers.keySet());
+ }
+
+ public RouterFunction getDynamicRouter() {
+ return request -> {
+ for (WebMvcSseServerTransportProvider transport : categoryTransports.values()) {
+ Optional> handler = transport.getRouterFunction().route(request);
+ if (handler.isPresent()) {
+ return handler;
+ }
+ }
+ return Optional.empty();
+ };
+ }
+}
diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java
index a551c58..0ac8c46 100644
--- a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java
+++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java
@@ -13,27 +13,42 @@ import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
- * MCP 서버의 tools/list 노출 목록을 Redis Registry의 ACTIVE Tool 목록과 동기화합니다.
+ * @package io.shinhanlife.axhub.biz.mcp.gateway.sync
+ * @className RegistryMcpToolSynchronizer
+ * @description AX HUB 시스템 처리 클래스
+ * @author 김형식
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 김형식 최초생성
+ *
+ *
*/
@Service
public class RegistryMcpToolSynchronizer {
private static final Logger log = LoggerFactory.getLogger(RegistryMcpToolSynchronizer.class);
private final ObjectProvider mcpServerProvider;
+ private final DynamicMcpServerManager dynamicMcpServerManager;
private final RedisRegistryService redisRegistryService;
private final RegistryMcpToolSpecificationFactory specificationFactory;
private final Set managedToolNames = new LinkedHashSet<>();
private final ReentrantLock lock = new ReentrantLock();
public RegistryMcpToolSynchronizer(ObjectProvider mcpServerProvider,
+ DynamicMcpServerManager dynamicMcpServerManager,
RedisRegistryService redisRegistryService,
RegistryMcpToolSpecificationFactory specificationFactory) {
this.mcpServerProvider = mcpServerProvider;
+ this.dynamicMcpServerManager = dynamicMcpServerManager;
this.redisRegistryService = redisRegistryService;
this.specificationFactory = specificationFactory;
}
@@ -49,34 +64,46 @@ public class RegistryMcpToolSynchronizer {
}
public void synchronize() {
- McpSyncServer server = mcpServerProvider.getIfAvailable();
- if (server == null) {
- return;
- }
-
lock.lock();
try {
List activeEntries = redisRegistryService.getAllTools()
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
-
- Set activeNames = activeEntries.stream()
- .map(ToolMetadata::getName) // We use 'name' for MCP Tool Name
- .collect(Collectors.toCollection(LinkedHashSet::new));
-
- Set removedNames = new LinkedHashSet<>(managedToolNames);
- removedNames.removeAll(activeNames);
- for (String removedName : removedNames) {
- server.removeTool(removedName);
- managedToolNames.remove(removedName);
- log.info("Removed MCP registry tool. tool={}", removedName);
+
+ // 1. Dynamic MCP Server 동기화 (카테고리별)
+ Map> toolsByCategory = activeEntries.stream()
+ .collect(Collectors.groupingBy(
+ t -> (t.getCategoryKey() == null || t.getCategoryKey().trim().isEmpty()) ? "common" : t.getCategoryKey()
+ ));
+
+ Set allKnownCategories = new LinkedHashSet<>(dynamicMcpServerManager.getKnownCategories());
+ allKnownCategories.addAll(toolsByCategory.keySet());
+
+ for (String category : allKnownCategories) {
+ dynamicMcpServerManager.synchronizeCategory(category, toolsByCategory.getOrDefault(category, List.of()));
}
- for (ToolMetadata entry : activeEntries) {
- if (managedToolNames.contains(entry.getName())) {
- server.removeTool(entry.getName());
+ // 2. Global MCP Server 동기화 (기존 하위 호환)
+ McpSyncServer server = mcpServerProvider.getIfAvailable();
+ if (server != null) {
+ Set activeNames = activeEntries.stream()
+ .map(ToolMetadata::getName)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+
+ Set removedNames = new LinkedHashSet<>(managedToolNames);
+ removedNames.removeAll(activeNames);
+ for (String removedName : removedNames) {
+ server.removeTool(removedName);
+ managedToolNames.remove(removedName);
+ log.info("Removed MCP registry tool (Global). tool={}", removedName);
+ }
+
+ for (ToolMetadata entry : activeEntries) {
+ if (managedToolNames.contains(entry.getName())) {
+ server.removeTool(entry.getName());
+ }
+ server.addTool(specificationFactory.create(entry));
+ managedToolNames.add(entry.getName());
}
- server.addTool(specificationFactory.create(entry));
- managedToolNames.add(entry.getName());
}
} catch (Exception error) {
log.warn("MCP registry tool synchronization failed. message={}", error.getMessage());