diff --git a/axhub-gateway/build.gradle b/axhub-gateway/build.gradle index 4e74997..f1534de 100644 --- a/axhub-gateway/build.gradle +++ b/axhub-gateway/build.gradle @@ -22,7 +22,7 @@ dependencies { annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0" annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final" - implementation 'com.networknt:json-schema-validator:1.4.0' + // implementation 'com.networknt:json-schema-validator:1.4.0' // Spring AI 내장 버전과 충돌 방지를 위해 주석 처리 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' } 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 5d9a141..1601968 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 @@ -60,8 +60,84 @@ public class McpRouterController { this.restClient = RestClient.builder().requestFactory(factory).build(); } + @PostMapping("/tools/call") + public ResponseEntity callTool(@RequestBody Map payload, + @RequestHeader(value = "X-Tenant-Id", required = false, defaultValue = "system") String tenantId) { + + try { + log.info("[Admin UI -> MCP Gateway] 동적 툴 실행 요청 수신: {}", objectMapper.writeValueAsString(payload)); + } catch (Exception ex) { + log.info("[Admin UI -> MCP Gateway] 동적 툴 실행 요청 수신: {}", payload); + } + try { + Object result = executeService.execute(payload, tenantId); + + try { + log.info("[MCP Gateway -> Admin UI] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(result)); + } catch (Exception ex) { + log.info("[MCP Gateway -> Admin UI] 동적 툴 실행 결과 반환: {}", result); + } + + return ResponseEntity.ok(result); + } catch (SecurityException se) { + log.warn(" [보안 차단] 권한 오류: {}", se.getMessage()); + return ResponseEntity.status(403).body(Map.of("error", se.getMessage())); + } catch (Exception e) { + log.error(" 파이프라인 실행 중 오류: {}", e.getMessage()); + return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); + } + } + @GetMapping("/tools/list") + public ResponseEntity listTools( + @RequestParam(value = "categoryKey", required = false) String categoryKey) { + List activeTools = redisRegistryService.getAllTools() + .stream() + .filter(ToolMetadata::getVisible) + .collect(Collectors.toList()); + + Set knownTools = activeTools.stream() + .map(ToolMetadata::getUid) + .collect(Collectors.toSet()); + + Set fallbackUrls = new 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 (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getUid())) { + activeTools.add(t); + knownTools.add(t.getUid()); + } + } + } + } catch (Exception e) { + log.warn("Failed to fetch local tools from fallback URL: {}", url); + } + } + + if (categoryKey != null && !categoryKey.trim().isEmpty()) { + activeTools = activeTools.stream() + .filter(t -> categoryKey.equals(t.getCategoryKey())) + .collect(Collectors.toList()); + } + + JsonRpcResponse response = new JsonRpcResponse(); + response.setId(UUID.randomUUID().toString()); + response.setResult(Map.of("tools", activeTools)); + + return ResponseEntity.ok(response); + } @GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8") public ResponseEntity generateToolsMarkdown() { List tools = redisRegistryService.getAllTools()