feat: implement dynamic mcp server routing to bypass SSE deprecation and 50 tools limit
This commit is contained in:
@@ -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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 김형식 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class DynamicMcpRouterConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RouterFunction<ServerResponse> dynamicMcpRouterFunction(DynamicMcpServerManager manager) {
|
||||||
|
return manager.getDynamicRouter();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 김형식 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DynamicMcpServerManager {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DynamicMcpServerManager.class);
|
||||||
|
|
||||||
|
private final Map<String, McpSyncServer> categoryServers = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, WebMvcSseServerTransportProvider> categoryTransports = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Set<String>> managedToolNamesPerCategory = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final RegistryMcpToolSpecificationFactory specificationFactory;
|
||||||
|
|
||||||
|
public DynamicMcpServerManager(RegistryMcpToolSpecificationFactory specificationFactory) {
|
||||||
|
this.specificationFactory = specificationFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void synchronizeCategory(String categoryKey, List<ToolMetadata> 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<String> managedToolNames = managedToolNamesPerCategory.get(safeCategory);
|
||||||
|
Set<String> activeNames = tools.stream()
|
||||||
|
.map(ToolMetadata::getName)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Set<String> 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<String> getKnownCategories() {
|
||||||
|
return Collections.unmodifiableSet(categoryServers.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouterFunction<ServerResponse> getDynamicRouter() {
|
||||||
|
return request -> {
|
||||||
|
for (WebMvcSseServerTransportProvider transport : categoryTransports.values()) {
|
||||||
|
Optional<HandlerFunction<ServerResponse>> handler = transport.getRouterFunction().route(request);
|
||||||
|
if (handler.isPresent()) {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,27 +13,42 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 김형식 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class RegistryMcpToolSynchronizer {
|
public class RegistryMcpToolSynchronizer {
|
||||||
private static final Logger log = LoggerFactory.getLogger(RegistryMcpToolSynchronizer.class);
|
private static final Logger log = LoggerFactory.getLogger(RegistryMcpToolSynchronizer.class);
|
||||||
|
|
||||||
private final ObjectProvider<McpSyncServer> mcpServerProvider;
|
private final ObjectProvider<McpSyncServer> mcpServerProvider;
|
||||||
|
private final DynamicMcpServerManager dynamicMcpServerManager;
|
||||||
private final RedisRegistryService redisRegistryService;
|
private final RedisRegistryService redisRegistryService;
|
||||||
private final RegistryMcpToolSpecificationFactory specificationFactory;
|
private final RegistryMcpToolSpecificationFactory specificationFactory;
|
||||||
private final Set<String> managedToolNames = new LinkedHashSet<>();
|
private final Set<String> managedToolNames = new LinkedHashSet<>();
|
||||||
private final ReentrantLock lock = new ReentrantLock();
|
private final ReentrantLock lock = new ReentrantLock();
|
||||||
|
|
||||||
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
|
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
|
||||||
|
DynamicMcpServerManager dynamicMcpServerManager,
|
||||||
RedisRegistryService redisRegistryService,
|
RedisRegistryService redisRegistryService,
|
||||||
RegistryMcpToolSpecificationFactory specificationFactory) {
|
RegistryMcpToolSpecificationFactory specificationFactory) {
|
||||||
this.mcpServerProvider = mcpServerProvider;
|
this.mcpServerProvider = mcpServerProvider;
|
||||||
|
this.dynamicMcpServerManager = dynamicMcpServerManager;
|
||||||
this.redisRegistryService = redisRegistryService;
|
this.redisRegistryService = redisRegistryService;
|
||||||
this.specificationFactory = specificationFactory;
|
this.specificationFactory = specificationFactory;
|
||||||
}
|
}
|
||||||
@@ -49,18 +64,29 @@ public class RegistryMcpToolSynchronizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void synchronize() {
|
public void synchronize() {
|
||||||
McpSyncServer server = mcpServerProvider.getIfAvailable();
|
|
||||||
if (server == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools()
|
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools()
|
||||||
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
|
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 1. Dynamic MCP Server 동기화 (카테고리별)
|
||||||
|
Map<String, List<ToolMetadata>> toolsByCategory = activeEntries.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
t -> (t.getCategoryKey() == null || t.getCategoryKey().trim().isEmpty()) ? "common" : t.getCategoryKey()
|
||||||
|
));
|
||||||
|
|
||||||
|
Set<String> allKnownCategories = new LinkedHashSet<>(dynamicMcpServerManager.getKnownCategories());
|
||||||
|
allKnownCategories.addAll(toolsByCategory.keySet());
|
||||||
|
|
||||||
|
for (String category : allKnownCategories) {
|
||||||
|
dynamicMcpServerManager.synchronizeCategory(category, toolsByCategory.getOrDefault(category, List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Global MCP Server 동기화 (기존 하위 호환)
|
||||||
|
McpSyncServer server = mcpServerProvider.getIfAvailable();
|
||||||
|
if (server != null) {
|
||||||
Set<String> activeNames = activeEntries.stream()
|
Set<String> activeNames = activeEntries.stream()
|
||||||
.map(ToolMetadata::getName) // We use 'name' for MCP Tool Name
|
.map(ToolMetadata::getName)
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
|
||||||
Set<String> removedNames = new LinkedHashSet<>(managedToolNames);
|
Set<String> removedNames = new LinkedHashSet<>(managedToolNames);
|
||||||
@@ -68,7 +94,7 @@ public class RegistryMcpToolSynchronizer {
|
|||||||
for (String removedName : removedNames) {
|
for (String removedName : removedNames) {
|
||||||
server.removeTool(removedName);
|
server.removeTool(removedName);
|
||||||
managedToolNames.remove(removedName);
|
managedToolNames.remove(removedName);
|
||||||
log.info("Removed MCP registry tool. tool={}", removedName);
|
log.info("Removed MCP registry tool (Global). tool={}", removedName);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ToolMetadata entry : activeEntries) {
|
for (ToolMetadata entry : activeEntries) {
|
||||||
@@ -78,6 +104,7 @@ public class RegistryMcpToolSynchronizer {
|
|||||||
server.addTool(specificationFactory.create(entry));
|
server.addTool(specificationFactory.create(entry));
|
||||||
managedToolNames.add(entry.getName());
|
managedToolNames.add(entry.getName());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
log.warn("MCP registry tool synchronization failed. message={}", error.getMessage());
|
log.warn("MCP registry tool synchronization failed. message={}", error.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user