diff --git a/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ExecuteService.java b/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ExecuteService.java index 3b7946e..a86ffbf 100644 --- a/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ExecuteService.java +++ b/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ExecuteService.java @@ -32,7 +32,8 @@ public class ExecuteService { killSwitchService.checkAgent(tenantId); // [비상 차단 2단계] Tool 단위 차단 검사 - String toolName = (String) payload.get("toolName"); + Map params = payload.containsKey("params") ? (Map) payload.get("params") : null; + String toolName = params != null ? (String) params.get("name") : (String) payload.get("toolName"); killSwitchService.checkTool(toolName); // 1. 검증 (Validator) diff --git a/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/controller/BusinessToolController.java b/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/controller/BusinessToolController.java index ba7b4c7..375cd89 100644 --- a/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/controller/BusinessToolController.java +++ b/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/controller/BusinessToolController.java @@ -1,8 +1,5 @@ package io.shinhanlife.axhub.biz.mcp.tool.controller; -import io.shinhanlife.axhub.biz.mcp.tool.model.ToolConfig; -import io.shinhanlife.axhub.biz.mcp.tool.model.ToolFunction; -import io.shinhanlife.axhub.biz.mcp.tool.registry.MockToolAutoRegistrar; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -26,6 +23,7 @@ import org.springframework.context.ApplicationContext; import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool; import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction; import java.lang.reflect.Method; +import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator; import lombok.extern.slf4j.Slf4j; @@ -36,13 +34,12 @@ import lombok.extern.slf4j.Slf4j; public class BusinessToolController { private final ApplicationContext applicationContext; - private final MockToolAutoRegistrar toolRegistrar; private final ObjectMapper objectMapper; - // 단일 동적 라우팅 엔드포인트 - @PostMapping("/execute/{toolName}") - public Map executeDynamicTool(@PathVariable String toolName, @RequestBody(required = false) Map payload) { - log.info("\n [Tool] 동적 툴 실행 요청 수신: {}", toolName); + // 함수명(functionName) 기반의 단일 동적 라우팅 엔드포인트 + @PostMapping("/execute/{functionName}") + public Map executeDynamicTool(@PathVariable String functionName, @RequestBody(required = false) Map payload) { + log.info("\n [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName); if (payload != null) { try { log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(payload)); @@ -51,113 +48,71 @@ public class BusinessToolController { } } - // 1. 등록된 툴 설정 조회 - ToolConfig config = toolRegistrar.getTools().stream() - .filter(t -> t.getName().equals(toolName)) - .findFirst() - .orElse(null); - - if (config == null) { - log.error(" [Tool] 등록되지 않은 툴 호출: {}", toolName); - Map error = new HashMap<>(); - error.put("jsonrpc", "2.0"); - error.put("error", Map.of("code", -32601, "message", "등록되지 않은 툴입니다: " + toolName)); - error.put("id", payload != null ? payload.get("id") : null); - return error; - } - - // 2. 파라미터에서 실행할 함수(action) 추출 및 라우팅 결정 (JSON-RPC) Map params = payload != null ? (Map) payload.get("params") : null; Map arguments = params != null ? (Map) params.get("arguments") : null; - String action = arguments != null && arguments.containsKey("action") ? arguments.get("action").toString() : null; - - ToolFunction targetFunction = null; - if (action != null) { - targetFunction = config.getFunctions().stream() - .filter(f -> f.getName().equals(action)) - .findFirst() - .orElse(null); - } - - // action 파라미터가 없거나 일치하는 함수가 없으면 첫 번째 함수를 기본값으로 사용 - if (targetFunction == null && !config.getFunctions().isEmpty()) { - targetFunction = config.getFunctions().get(0); - log.warn(" [Tool] 지정된 action 파라미터가 없거나 유효하지 않아 기본 함수({})를 사용합니다.", targetFunction.getName()); - } - String interfaceId = targetFunction != null ? targetFunction.getInterfaceId() : "UNKNOWN"; - - // 2-1. 파라미터 유효성 검증 (JSON Schema) - if (targetFunction != null && targetFunction.getParameterSchema() != null && !targetFunction.getParameterSchema().equals("{}")) { - try { - // parameterSchema에는 properties 내용만 들어있으므로 완전한 스키마 형태로 감싸줍니다. - String fullSchemaJson = "{\"type\":\"object\", \"properties\":" + targetFunction.getParameterSchema() + "}"; - - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); - JsonSchema schema = factory.getSchema(fullSchemaJson); - - Set errors = schema.validate(objectMapper.valueToTree(arguments)); - if (!errors.isEmpty()) { - log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); - Map error = new HashMap<>(); - error.put("jsonrpc", "2.0"); - List errorMessages = new ArrayList<>(); - for (ValidationMessage vm : errors) { - errorMessages.add(vm.getMessage()); - } - error.put("error", Map.of("code", -32602, "message", "파라미터 유효성 검증 실패: " + String.join(", ", errorMessages))); - error.put("id", payload != null ? payload.get("id") : null); - return error; - } - } catch (Exception e) { - log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage()); - } - } - - // 3. 대상 Bean 찾기 (ApplicationContext 활용) + // 1. 대상 Bean 및 Method 찾기 (ApplicationContext 활용) Object targetBean = null; - Map toolBeans = applicationContext.getBeansWithAnnotation(McpTool.class); - for (Object bean : toolBeans.values()) { - McpTool mcpToolAnnotation = bean.getClass().getAnnotation(McpTool.class); - if (mcpToolAnnotation != null && mcpToolAnnotation.name().equals(toolName)) { - targetBean = bean; - break; - } - } - - if (targetBean == null) { - log.error("[Tool] 실행할 Tool Bean을 찾을 수 없습니다: {}", toolName); - Map error = new HashMap<>(); - error.put("jsonrpc", "2.0"); - error.put("error", Map.of("code", -32601, "message", "실행할 Tool Bean을 찾을 수 없습니다: " + toolName)); - error.put("id", payload != null ? payload.get("id") : null); - return error; - } - - // 4. 리플렉션을 통해 대상 메서드 직접 호출 - String actionName = targetFunction.getName(); Method targetMethod = null; - for (Method method : targetBean.getClass().getDeclaredMethods()) { - McpFunction mcpFunc = method.getAnnotation(McpFunction.class); - if (mcpFunc != null && mcpFunc.name().equals(actionName)) { - targetMethod = method; - break; + McpFunction targetFunctionAnnotation = null; + + Map toolBeans = applicationContext.getBeansWithAnnotation(McpTool.class); + outerLoop: + for (Object bean : toolBeans.values()) { + for (Method method : bean.getClass().getDeclaredMethods()) { + McpFunction mcpFunc = method.getAnnotation(McpFunction.class); + if (mcpFunc != null && mcpFunc.name().equals(functionName)) { + targetBean = bean; + targetMethod = method; + targetFunctionAnnotation = mcpFunc; + break outerLoop; + } } } - if (targetMethod == null) { - log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}", actionName); + if (targetBean == null || targetMethod == null) { + log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}", functionName); Map error = new HashMap<>(); error.put("jsonrpc", "2.0"); - error.put("error", Map.of("code", -32601, "message", "실행할 함수(Method)를 찾을 수 없습니다: " + actionName)); + error.put("error", Map.of("code", -32601, "message", "실행할 함수를 찾을 수 없습니다: " + functionName)); error.put("id", payload != null ? payload.get("id") : null); return error; } - log.info("[Tool] 리플렉션 직접 실행 -> Tool: {}, Method: {}", toolName, targetMethod.getName()); + // 2. 파라미터 유효성 검증 (JSON Schema) + if (targetMethod.getParameterCount() > 0) { + Class paramType = targetMethod.getParameterTypes()[0]; + if (!Map.class.isAssignableFrom(paramType)) { + try { + Map autoSchema = JsonSchemaGenerator.generatePropertiesSchema(paramType); + String fullSchemaJson = "{\"type\":\"object\", \"properties\":" + objectMapper.writeValueAsString(autoSchema) + "}"; + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); + JsonSchema schema = factory.getSchema(fullSchemaJson); + + Set errors = schema.validate(objectMapper.valueToTree(arguments)); + if (!errors.isEmpty()) { + log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); + Map error = new HashMap<>(); + error.put("jsonrpc", "2.0"); + List errorMessages = new ArrayList<>(); + for (ValidationMessage vm : errors) { + errorMessages.add(vm.getMessage()); + } + error.put("error", Map.of("code", -32602, "message", "파라미터 유효성 검증 실패: " + String.join(", ", errorMessages))); + error.put("id", payload != null ? payload.get("id") : null); + return error; + } + } catch (Exception e) { + log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage()); + } + } + } + + log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName()); try { - // 5. DTO 파라미터 자동 매핑 (Map -> DTO) + // 3. DTO 파라미터 자동 매핑 (Map -> DTO) Object invokeArgument = arguments; if (targetMethod.getParameterCount() > 0 && arguments != null) { Class paramType = targetMethod.getParameterTypes()[0]; @@ -167,10 +122,10 @@ public class BusinessToolController { } } - // 6. 메서드 실행 + // 4. 메서드 실행 Object methodResult = targetMethod.invoke(targetBean, invokeArgument); - // 7. 결과 조립 (JSON-RPC 응답) + // 5. 결과 조립 (JSON-RPC 응답) Map resultPayload = new HashMap<>(); if (methodResult instanceof Map) { resultPayload = (Map) methodResult; diff --git a/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/registry/MockToolAutoRegistrar.java b/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/registry/MockToolAutoRegistrar.java index 879ebf0..a155457 100644 --- a/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/registry/MockToolAutoRegistrar.java +++ b/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/registry/MockToolAutoRegistrar.java @@ -2,8 +2,6 @@ package io.shinhanlife.axhub.biz.mcp.tool.registry; import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction; import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool; -import io.shinhanlife.axhub.biz.mcp.tool.model.ToolConfig; -import io.shinhanlife.axhub.biz.mcp.tool.model.ToolFunction; import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; import com.fasterxml.jackson.databind.ObjectMapper; import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator; @@ -23,7 +21,6 @@ import org.springframework.web.client.RestClient; import jakarta.annotation.PostConstruct; import java.lang.reflect.Method; import java.util.*; -import java.util.stream.Collectors; @Slf4j @Component @@ -46,66 +43,13 @@ public class MockToolAutoRegistrar { @Value("${mcp.tool.host:localhost}") private String toolHost; - private List TOOLS = new ArrayList<>(); + private List registeredTools = new ArrayList<>(); public MockToolAutoRegistrar(ApplicationContext applicationContext, ObjectMapper objectMapper) { this.applicationContext = applicationContext; this.objectMapper = objectMapper; } - @PostConstruct - public void init() { - if (serverPort == 8081) return; // Gateway skip - - log.info(" [AutoDiscovery] @McpTool 어노테이션 스캔 시작..."); - Map toolBeans = applicationContext.getBeansWithAnnotation(McpTool.class); - - for (Object bean : toolBeans.values()) { - McpTool mcpTool = bean.getClass().getAnnotation(McpTool.class); - if (mcpTool == null) continue; - - // Target Group 필터링 - if (!"all".equals(toolTarget) && !mcpTool.name().equals(toolTarget) && !mcpTool.group().equals(toolTarget)) { - continue; - } - - List functions = new ArrayList<>(); - for (Method method : bean.getClass().getDeclaredMethods()) { - if (method.isAnnotationPresent(McpFunction.class)) { - McpFunction mcpFunc = method.getAnnotation(McpFunction.class); - String finalSchema = mcpFunc.parameterSchema(); - if (method.getParameterCount() > 0) { - Class paramType = method.getParameterTypes()[0]; - if (!Map.class.isAssignableFrom(paramType)) { - try { - Map autoSchema = JsonSchemaGenerator.generatePropertiesSchema(paramType); - finalSchema = objectMapper.writeValueAsString(autoSchema); - } catch (Exception e) { - log.error("Failed to generate schema for {}", paramType.getSimpleName(), e); - } - } - } - functions.add(new ToolFunction(mcpFunc.name(), mcpFunc.description(), mcpFunc.mappingId(), finalSchema)); - } - } - - ToolConfig config = new ToolConfig( - mcpTool.name(), - mcpTool.description(), - "/api/tool/execute/" + mcpTool.name(), - mcpTool.group(), - mcpTool.routingType(), - functions - ); - TOOLS.add(config); - log.info(" [AutoDiscovery] 툴 발견: {} (함수 {}개)", mcpTool.name(), functions.size()); - } - } - - public List getTools() { - return TOOLS; - } - private HttpHeaders createHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -118,6 +62,7 @@ public class MockToolAutoRegistrar { if (serverPort == 8081) return; log.info("[MockTool] Tool Pod 기동 완료! (Port: {})", serverPort); + registeredTools.clear(); for (Object bean : applicationContext.getBeansWithAnnotation(McpTool.class).values()) { McpTool mcpTool = bean.getClass().getAnnotation(McpTool.class); @@ -125,37 +70,36 @@ public class MockToolAutoRegistrar { continue; } - ToolMetadata myMetadata = new ToolMetadata(); - myMetadata.setToolName(mcpTool.name()); - myMetadata.setDescription(mcpTool.description()); - myMetadata.setDomainGroup(mcpTool.group()); - myMetadata.setEndpoint("/api/tool/execute/" + mcpTool.name()); - myMetadata.setPodUrl("http://" + toolHost + ":" + serverPort); - myMetadata.setIntegrationType(mcpTool.routingType()); - myMetadata.setFailureRateThreshold(50); - myMetadata.setSlidingWindowSize(20); - myMetadata.setRateLimitForPeriod(100); - - Map actionPrompts = new HashMap<>(); - List actions = new ArrayList<>(); - List actionDescs = new ArrayList<>(); - Map aggregatedProperties = new HashMap<>(); - for (Method method : bean.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(McpFunction.class)) { McpFunction mcpFunc = method.getAnnotation(McpFunction.class); - actions.add(mcpFunc.name()); - actionDescs.add(mcpFunc.name() + "(" + mcpFunc.description() + ")"); + + ToolMetadata myMetadata = new ToolMetadata(); + myMetadata.setToolName(mcpFunc.name()); // Function -> Tool로 승격 + myMetadata.setDescription(mcpFunc.description()); + myMetadata.setDomainGroup(mcpTool.group()); + myMetadata.setEndpoint("/api/tool/execute/" + mcpFunc.name()); + myMetadata.setPodUrl("http://" + toolHost + ":" + serverPort); + myMetadata.setIntegrationType(mcpTool.routingType()); + myMetadata.setFailureRateThreshold(50); + myMetadata.setSlidingWindowSize(20); + myMetadata.setRateLimitForPeriod(100); + + Map actionPrompts = new HashMap<>(); actionPrompts.put(mcpFunc.name(), mcpFunc.prompt()); + myMetadata.setActionPrompts(actionPrompts); + + Map parametersSchema = new HashMap<>(); + parametersSchema.put("type", "object"); + Map aggregatedProperties = new HashMap<>(); if (method.getParameterCount() > 0) { Class paramType = method.getParameterTypes()[0]; if (!Map.class.isAssignableFrom(paramType)) { - // DTO 기반 자동 스키마 생성 + // DTO 기반 스키마 (비즈니스 파라미터만) Map autoSchema = JsonSchemaGenerator.generatePropertiesSchema(paramType); aggregatedProperties.putAll(autoSchema); } else { - // 기존 하드코딩 스키마 사용 String schemaJson = mcpFunc.parameterSchema(); if (!"{}".equals(schemaJson)) { try { @@ -163,44 +107,30 @@ public class MockToolAutoRegistrar { Map schemaMap = objectMapper.readValue(schemaJson, Map.class); aggregatedProperties.putAll(schemaMap); } catch (Exception e) { - log.error(" [MockTool] Failed to parse parameterSchema for {}: {}", mcpFunc.name(), e.getMessage()); + log.error(" [MockTool] 스키마 파싱 실패 {}: {}", mcpFunc.name(), e.getMessage()); } } } } + + parametersSchema.put("properties", aggregatedProperties); + myMetadata.setParametersSchema(parametersSchema); + + try { + String registerUrl = gatewayUrl + "/registry/register"; + restClient.post() + .uri(registerUrl) + .headers(h -> h.addAll(createHeaders())) + .body(myMetadata) + .retrieve() + .body(String.class); + log.info(" [MockTool] 함수 툴 승격 및 등록 성공: {} (Pod: {})", mcpFunc.name(), myMetadata.getPodUrl()); + registeredTools.add(mcpFunc.name()); + } catch (Exception e) { + log.error(" [MockTool] {} 등록 실패: {}", mcpFunc.name(), e.getMessage()); + } } } - - myMetadata.setActionPrompts(actionPrompts); - - Map parametersSchema = new HashMap<>(); - parametersSchema.put("type", "object"); - Map properties = new HashMap<>(); - properties.putAll(aggregatedProperties); - - Map actionProperty = new HashMap<>(); - actionProperty.put("type", "string"); - actionProperty.put("enum", actions); - actionProperty.put("description", "실행할 함수명: " + String.join(", ", actionDescs)); - - properties.put("action", actionProperty); - parametersSchema.put("properties", properties); - parametersSchema.put("required", Arrays.asList("action")); - - myMetadata.setParametersSchema(parametersSchema); - - try { - String registerUrl = gatewayUrl + "/registry/register"; - restClient.post() - .uri(registerUrl) - .headers(h -> h.addAll(createHeaders())) - .body(myMetadata) - .retrieve() - .body(String.class); - log.info(" [MockTool] {} 자동 등록 성공! (Pod URL: {})", mcpTool.name(), myMetadata.getPodUrl()); - } catch (Exception e) { - log.error(" [MockTool] {} 자동 등록 실패: {}", mcpTool.name(), e.getMessage()); - } } log.info(""); } @@ -213,24 +143,24 @@ public class MockToolAutoRegistrar { headers.setContentType(MediaType.TEXT_PLAIN); boolean needsReRegistration = false; - for (ToolConfig tool : TOOLS) { + for (String toolName : registeredTools) { try { String heartbeatUrl = gatewayUrl + "/registry/heartbeat"; restClient.post() .uri(heartbeatUrl) .headers(h -> h.addAll(headers)) - .body(tool.getName()) + .body(toolName) .retrieve() .body(String.class); - log.info(" [MockTool] {} Heartbeat 서버 전송 완료", tool.getName()); + log.info(" [MockTool] {} Heartbeat 전송 완료", toolName); } catch (Exception e) { - log.error(" [MockTool] {} Heartbeat 전송 실패: {}", tool.getName(), e.getMessage()); + log.error(" [MockTool] {} Heartbeat 전송 실패: {}", toolName, e.getMessage()); needsReRegistration = true; } } if (needsReRegistration) { - log.info(" [MockTool] 하트비트 실패로 인해 전체 툴 재등록을 시도합니다."); + log.info(" [MockTool] 하트비트 실패로 재등록을 시도합니다."); registerOnStartup(); } } @@ -239,22 +169,22 @@ public class MockToolAutoRegistrar { public void deregisterOnShutdown() { if (serverPort == 8081) return; - log.info("\n [MockTool] Tool Pod (Port: {}) 종료 중... Gateway에 툴 해제를 요청합니다.", serverPort); + log.info("\n [MockTool] Tool Pod (Port: {}) 종료 중...", serverPort); HttpHeaders headers = createHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); - for (ToolConfig tool : TOOLS) { + for (String toolName : registeredTools) { try { String deregisterUrl = gatewayUrl + "/registry/deregister"; restClient.post() .uri(deregisterUrl) .headers(h -> h.addAll(headers)) - .body(tool.getName()) + .body(toolName) .retrieve() .body(String.class); - log.info(" [MockTool] {} 명시적 해제 성공!", tool.getName()); + log.info(" [MockTool] {} 명시적 해제 성공!", toolName); } catch (Exception e) { - log.error(" [MockTool] {} 명시적 해제 실패: {}", tool.getName(), e.getMessage()); + log.error(" [MockTool] {} 해제 실패: {}", toolName, e.getMessage()); } } } diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 5298e80..6c9ce77 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -192,13 +192,13 @@
- Next-Gen Router + Next-Gen Router (Flattened)

AI MCP Gateway

- 엔터프라이즈 환경을 위한 차세대 통합 라우팅 시스템 + 함수 단위로 독립된 엔터프라이즈 통합 라우팅 시스템

@@ -219,12 +219,12 @@
-
+
- -
- -
-
-
- -