diff --git a/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/router/RequestValidator.java b/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/router/RequestValidator.java index a61f0ff..df579e0 100644 --- a/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/router/RequestValidator.java +++ b/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/router/RequestValidator.java @@ -22,14 +22,32 @@ public class RequestValidator { throw new IllegalArgumentException("요청 페이로드가 존재하지 않습니다."); } - // 2. 필수 파라미터 누락 및 빈 값 검증 + // JSON-RPC 2.0 껍데기 검증 + if (!"2.0".equals(payload.get("jsonrpc"))) { + throw new IllegalArgumentException("지원하지 않는 규격입니다. 'jsonrpc': '2.0' 이 필요합니다."); + } + if (!"tools/call".equals(payload.get("method"))) { + throw new IllegalArgumentException("지원하지 않는 method 입니다. 'tools/call' 이 필요합니다."); + } + + Map params = (Map) payload.get("params"); + if (params == null || params.isEmpty()) { + throw new IllegalArgumentException("params 객체가 누락되었습니다."); + } + + Map arguments = (Map) params.get("arguments"); + if (arguments == null) { + throw new IllegalArgumentException("params.arguments 객체가 누락되었습니다."); + } + + // 2. 필수 파라미터 누락 및 빈 값 검증 (arguments 내에서 검증) for (String field : REQUIRED_FIELDS) { - if (!payload.containsKey(field)) { + if (!arguments.containsKey(field)) { log.error(" [Validator] 검증 실패: 필수 키 누락 [{}]", field); throw new IllegalArgumentException("필수 파라미터가 누락되었습니다: " + field); } - Object value = payload.get(field); + Object value = arguments.get(field); if (value == null || value.toString().trim().isEmpty()) { log.error(" [Validator] 검증 실패: 필수 키의 값이 비어 있음 [{}]", field); throw new IllegalArgumentException("필수 파라미터의 값이 비어있을 수 없습니다: " + field); @@ -37,8 +55,8 @@ public class RequestValidator { } // 3. 비즈니스 로직에 따른 추가 데이터 길이 또는 타입 검증 (예: 프롬프트 길이) - String traceId = payload.get("traceId").toString(); - String userPrompt = payload.get("userPrompt").toString(); + String traceId = arguments.get("traceId").toString(); + String userPrompt = arguments.get("userPrompt").toString(); if (userPrompt.length() > 2000) { log.warn(" [Validator] 프롬프트 길이 초과 (Trace ID: {})", traceId); diff --git a/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ToolPlanner.java b/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ToolPlanner.java index d3495f0..dab3f77 100644 --- a/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ToolPlanner.java +++ b/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/service/ToolPlanner.java @@ -23,8 +23,9 @@ public class ToolPlanner { public Object createPlan(Map payload, String tenantId) { log.info("📝 [Planner] 요청 분석 및 실행 계획 수립 시작"); - // 1. 요청에서 호출하려는 툴 이름 추출 - String toolName = (String) payload.get("toolName"); + // 1. 요청에서 호출하려는 툴 이름 추출 (JSON-RPC params.name) + Map params = (Map) payload.get("params"); + String toolName = params != null ? (String) params.get("name") : null; if (toolName == null || toolName.isEmpty()) { throw new IllegalArgumentException("요청에 toolName이 포함되어 있지 않습니다."); 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 95abf45..db355f3 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 @@ -60,13 +60,16 @@ public class BusinessToolController { if (config == null) { log.error(" [Tool] 등록되지 않은 툴 호출: {}", toolName); Map error = new HashMap<>(); - error.put("status", "ERROR"); - error.put("message", "등록되지 않은 툴입니다: " + toolName); + 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) 추출 및 라우팅 결정 - String action = payload != null && payload.containsKey("action") ? payload.get("action").toString() : null; + // 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) { @@ -93,16 +96,17 @@ public class BusinessToolController { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); JsonSchema schema = factory.getSchema(fullSchemaJson); - Set errors = schema.validate(objectMapper.valueToTree(payload)); + Set errors = schema.validate(objectMapper.valueToTree(arguments)); if (!errors.isEmpty()) { log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); Map error = new HashMap<>(); - error.put("status", "ERROR"); + error.put("jsonrpc", "2.0"); List errorMessages = new ArrayList<>(); for (ValidationMessage vm : errors) { errorMessages.add(vm.getMessage()); } - error.put("message", "파라미터 유효성 검증 실패: " + String.join(", ", errorMessages)); + 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) { @@ -124,8 +128,9 @@ public class BusinessToolController { if (targetBean == null) { log.error("[Tool] 실행할 Tool Bean을 찾을 수 없습니다: {}", toolName); Map error = new HashMap<>(); - error.put("status", "ERROR"); - error.put("message", "실행할 Tool Bean을 찾을 수 없습니다: " + toolName); + 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; } @@ -143,8 +148,9 @@ public class BusinessToolController { if (targetMethod == null) { log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}", actionName); Map error = new HashMap<>(); - error.put("status", "ERROR"); - error.put("message", "실행할 함수(Method)를 찾을 수 없습니다: " + actionName); + error.put("jsonrpc", "2.0"); + error.put("error", Map.of("code", -32601, "message", "실행할 함수(Method)를 찾을 수 없습니다: " + actionName)); + error.put("id", payload != null ? payload.get("id") : null); return error; } @@ -152,11 +158,11 @@ public class BusinessToolController { try { // 5. DTO 파라미터 자동 매핑 (Map -> DTO) - Object invokeArgument = payload; - if (targetMethod.getParameterCount() > 0) { + Object invokeArgument = arguments; + if (targetMethod.getParameterCount() > 0 && arguments != null) { Class paramType = targetMethod.getParameterTypes()[0]; if (!Map.class.isAssignableFrom(paramType)) { - invokeArgument = objectMapper.convertValue(payload, paramType); + invokeArgument = objectMapper.convertValue(arguments, paramType); log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName()); } } @@ -164,24 +170,28 @@ public class BusinessToolController { // 6. 메서드 실행 Object methodResult = targetMethod.invoke(targetBean, invokeArgument); - // 6. 결과 반환 (Map 타입으로 캐스팅 보장) + // 7. 결과 조립 (JSON-RPC 응답) + Map resultPayload = new HashMap<>(); if (methodResult instanceof Map) { - @SuppressWarnings("unchecked") - Map resultMap = (Map) methodResult; - resultMap.put("ai_insight", "다이렉트 메서드(" + targetMethod.getName() + ") 통신이 성공적으로 수행되었습니다."); - return resultMap; + resultPayload = (Map) methodResult; } else { - Map resultMap = new HashMap<>(); - resultMap.put("status", "SUCCESS"); - resultMap.put("legacy_response", methodResult); - resultMap.put("ai_insight", "다이렉트 메서드(" + targetMethod.getName() + ") 통신이 성공적으로 수행되었습니다."); - return resultMap; + resultPayload.put("status", "SUCCESS"); + resultPayload.put("legacy_response", methodResult); } + resultPayload.put("ai_insight", "다이렉트 메서드(" + targetMethod.getName() + ") 통신이 성공적으로 수행되었습니다."); + + Map rpcResponse = new HashMap<>(); + rpcResponse.put("jsonrpc", "2.0"); + rpcResponse.put("result", resultPayload); + rpcResponse.put("id", payload != null ? payload.get("id") : null); + return rpcResponse; + } catch (Exception e) { log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage()); Map error = new HashMap<>(); - error.put("status", "ERROR"); - error.put("message", "메서드 실행 중 예외 발생: " + e.getCause().getMessage()); + error.put("jsonrpc", "2.0"); + error.put("error", Map.of("code", -32000, "message", "메서드 실행 중 예외 발생: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()))); + error.put("id", payload != null ? payload.get("id") : null); return error; } }