diff --git a/chaos_test.py b/chaos_test.py new file mode 100644 index 0000000..1262231 --- /dev/null +++ b/chaos_test.py @@ -0,0 +1,161 @@ +import requests +import json +import threading +import time + +BASE_URL = "https://axhub-gateway.onrender.com/mcp/custom/common" +TOOL_NAME = "other_get_current_weather" + +results = [] + +def run_test(test_name, payload_args): + print(f"\n[{test_name}] 시작...") + + try: + # 1. Initialize (서버 슬립 해제 포함, 타임아웃 넉넉하게) + init_resp = requests.post(BASE_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": {"name": "chaos-tester", "version": "1.0.0"} + } + }, stream=True, timeout=60) + + session_id = init_resp.headers.get("Mcp-Session-Id") + if not session_id: + results.append({"name": test_name, "payload": str(payload_args), "result": "세션 수립 실패", "safety": "알 수 없음"}) + print(f" 세션 수립 실패") + return + + print(f" 세션 ID: {session_id[:16]}...") + headers = {"Mcp-Session-Id": session_id} + + sse_result = {"data": None, "raw": []} + + def read_sse(resp): + for line in resp.iter_lines(): + if line: + decoded = line.decode("utf-8") + sse_result["raw"].append(decoded) + if decoded.startswith("data:"): + raw = decoded[5:].strip() + if raw and not raw.startswith("/mcp"): + try: + parsed = json.loads(raw) + # tools/call 응답 (id=2)만 저장 + if parsed.get("id") == 2: + sse_result["data"] = parsed + except: + pass + + sse_thread = threading.Thread(target=read_sse, args=(init_resp,), daemon=True) + sse_thread.start() + time.sleep(1) + + # 2. notifications/initialized + requests.post(BASE_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=headers, timeout=30) + time.sleep(0.5) + + # 3. tools/call with chaos payload + print(f" 페이로드 주입 중: {json.dumps(payload_args, ensure_ascii=False)[:80]}") + call_resp = requests.post(BASE_URL, json={ + "jsonrpc": "2.0", "id": 2, "method": "tools/call", + "params": {"name": TOOL_NAME, "arguments": payload_args} + }, headers=headers, timeout=30) + + print(f" HTTP Status: {call_resp.status_code}") + + # SSE 응답 대기 (최대 5초) + for _ in range(10): + time.sleep(0.5) + if sse_result["data"]: + break + + tool_result = sse_result["data"] + http_status = call_resp.status_code + + # 결과 분석 + server_died = False + is_error = False + response_summary = "SSE 응답 없음 (타임아웃)" + + if tool_result: + result_body = tool_result.get("result", {}) + error_body = tool_result.get("error", {}) + + if error_body: + is_error = True + response_summary = f"MCP Error: {json.dumps(error_body, ensure_ascii=False)[:100]}" + elif isinstance(result_body, dict): + is_err_flag = result_body.get("isError", False) + content = result_body.get("content", []) + text = content[0].get("text", "") if content else str(result_body) + if is_err_flag or "500" in text or "Exception" in text: + server_died = True + response_summary = f"서버 에러: {text[:100]}" + else: + response_summary = f"정상 응답: {text[:100]}" + elif http_status >= 500: + server_died = True + response_summary = f"HTTP {http_status} 서버 에러" + + if server_died: + safety = "위험 - 서버 500 에러" + elif is_error: + safety = "안전 - Validation 에러 반환" + else: + safety = f"실행됨 (HTTP {http_status})" + + results.append({ + "name": test_name, + "payload": json.dumps(payload_args, ensure_ascii=False)[:60], + "result": response_summary, + "safety": safety + }) + + print(f" 결과: {response_summary}") + print(f" 판정: {safety}") + + except requests.exceptions.ReadTimeout: + results.append({"name": test_name, "payload": str(payload_args), "result": "서버 응답 타임아웃", "safety": "판정 불가"}) + print(f" 타임아웃 발생") + except Exception as e: + results.append({"name": test_name, "payload": str(payload_args), "result": f"오류: {str(e)[:80]}", "safety": "판정 불가"}) + print(f" 오류: {e}") + + +# ===== 서버 웨이크업 ===== +print("=" * 60) +print("서버 웨이크업 중... (최초 응답에 시간이 걸릴 수 있습니다)") +try: + r = requests.get("https://axhub-gateway.onrender.com/actuator/health", timeout=60) + print(f"서버 상태: {r.status_code}") +except: + print("헬스체크 엔드포인트 없음 - 바로 테스트 진행") + +print("=" * 60) +print(f"CHAOS TEST: {TOOL_NAME}") +print("=" * 60) + +# 1. 타입 브레이커: city에 10000자 문자열 +run_test("1. 타입 브레이커 (10000자 문자열)", {"city": "A" * 10000}) +time.sleep(2) + +# 2. 필수 파라미터 누락 +run_test("2. 필수 파라미터 누락 (city 없음)", {}) +time.sleep(2) + +# 3. XSS + SQL Injection +run_test("3. 악의적 페이로드 (XSS + SQL Injection)", {"city": "' OR 1=1 --"}) + +# ===== 결과 리포트 ===== +print("\n\n" + "=" * 80) +print("CHAOS TEST 결과 리포트 - other_get_current_weather") +print("=" * 80) +print(f"{'테스트 시나리오':<35} {'판정':<30} {'서버 응답'}") +print("-" * 100) +for r in results: + print(f"{r['name']:<35} {r['safety']:<30} {r['result'][:40]}") +print("=" * 80) diff --git a/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/sync/DynamicMcpControllerRouteTest.java b/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/sync/DynamicMcpControllerRouteTest.java new file mode 100644 index 0000000..074d6b8 --- /dev/null +++ b/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/sync/DynamicMcpControllerRouteTest.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.biz.mcp.gateway.sync; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; + +class DynamicMcpControllerRouteTest { + + @Test + void exposesOnlySupportedCategoryAwareMcpRoutes() { + Set getPaths = Arrays.stream(DynamicMcpController.class.getDeclaredMethods()) + .flatMap(method -> Arrays.stream(method.getAnnotationsByType(GetMapping.class))) + .flatMap(mapping -> Arrays.stream(mapping.value())) + .collect(Collectors.toSet()); + Set postPaths = Arrays.stream(DynamicMcpController.class.getDeclaredMethods()) + .flatMap(method -> Arrays.stream(method.getAnnotationsByType(PostMapping.class))) + .flatMap(mapping -> Arrays.stream(mapping.value())) + .collect(Collectors.toSet()); + + assertTrue(getPaths.contains("/mcp/sse/{category}")); + assertTrue(postPaths.contains("/mcp/message/{category}")); + assertTrue(postPaths.contains("/mcp/custom/{category}")); + assertFalse(postPaths.contains("/mcp")); + assertFalse(postPaths.contains("/mcp/initialize")); + assertFalse(postPaths.contains("/mcp/sse/initialize")); + } +} diff --git a/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactoryTest.java b/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactoryTest.java new file mode 100644 index 0000000..6c7804e --- /dev/null +++ b/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactoryTest.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.biz.mcp.gateway.sync; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpSchema; +import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata; +import org.junit.jupiter.api.Test; + +class RegistryMcpToolSpecificationFactoryTest { + + @Test + void exposesMetadataBehaviorHintsInMcpToolSpecification() { + ToolMetadata metadata = ToolMetadata.builder() + .name("customer_lookup") + .description("Looks up customer information") + .readOnlyHint(true) + .destructiveHint(false) + .idempotentHint(true) + .openWorldHint(false) + .build(); + + RegistryMcpToolSpecificationFactory factory = new RegistryMcpToolSpecificationFactory(null, new ObjectMapper()); + McpSchema.Tool tool = factory.create(metadata).tool(); + + assertTrue(tool.annotations().readOnlyHint()); + assertFalse(tool.annotations().destructiveHint()); + assertTrue(tool.annotations().idempotentHint()); + assertFalse(tool.annotations().openWorldHint()); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/common/integration/mci/config/ShinhanIntegrationProperties.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/common/integration/mci/config/ShinhanIntegrationProperties.java index a98981b..925dfd4 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/common/integration/mci/config/ShinhanIntegrationProperties.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/common/integration/mci/config/ShinhanIntegrationProperties.java @@ -21,7 +21,6 @@ import org.springframework.context.annotation.Configuration; */ @Getter @Setter -@Configuration @ConfigurationProperties(prefix = "shinhan.integration") public class ShinhanIntegrationProperties { diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/common/util/ToolScaffolder.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/common/util/ToolScaffolder.java index dd4a93a..a0ab279 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/common/util/ToolScaffolder.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/common/util/ToolScaffolder.java @@ -161,188 +161,270 @@ public class ToolScaffolder { String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1); - // Generate Service - String serviceContent = """ - package %s.service; + boolean isMci = "MCI".equalsIgnoreCase(routingType); + String serviceContent; - import %s.annotation.McpFunction; - import %s.annotation.McpTool; - import %s.dto.%sReq; - import %s.dto.%sRes; - import %s.%s.converter.%sLegacyConverter; - import org.springframework.stereotype.Service; - import org.mapstruct.factory.Mappers; + if (isMci) { + serviceContent = """ + package %s.service; - /** - * @package %s.service - * @className %sService - * @description AX HUB 시스템 처리 클래스 - * @author %s - * @create %s - *
-             * ---------- 개정이력 ----------
-             * 수정일      수정자    수정내용
-             * ---------- -------- ---------------------------
-             * %s  %s    최초생성
-             * 
-             * 
- */ - @Service - @McpTool( - routingType = "%s", - categoryKey = "%s" - ) - public class %sService extends AbstractMcpToolService { + import %s.annotation.McpFunction; + import %s.annotation.McpTool; + import %s.dto.%sReq; + import %s.dto.%sRes; + import io.shinhanlife.dap.common.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + import org.springframework.stereotype.Service; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; - private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class); - - @McpFunction( - displayName = "%s 툴", - name = "%s", - description = "%s", - prompt = "%s", - mappingId = "%s", - register = %s, - requiresApproval = false + /** + * @package %s.service + * @className %sService + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + @McpTool( + routingType = "%s", + categoryKey = "%s" ) - public Object execute(%sReq req) { - // %sLegacyReq legacyReq = converter.toLegacyReq(req); - return executeLegacy("%s", "%s", req); // Or pass legacyReq + public class %sService { + + private final AxhubMciComponent mci; + + @McpFunction( + displayName = "%s 툴", + name = "%s", + description = "%s", + prompt = "%s", + mappingId = "%s", + register = %s, + requiresApproval = false, + openWorldHint = true + ) + public Object execute(%sReq req) { + log.info("[MCI Tool] {} 요청 수신.", "%s"); + try { + Transfer resTransfer = mci.callTo( + "%s", + null, + req, + Object.class + ); + return resTransfer.getBody() != null ? resTransfer.getBody() : "{\"status\":\"SUCCESS\"}"; + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return "{\"status\":\"ERROR\", \"message\":\"" + e.getMessage() + "\"}"; + } + } } - } - """.formatted( - BASE_PACKAGE, - BASE_PACKAGE, - BASE_PACKAGE, - BASE_PACKAGE, baseName, - BASE_PACKAGE, baseName, - BASE_PACKAGE, shortName, baseName, - BASE_PACKAGE, - baseName, - author, - createDate, - createDate, author, - routingType, group.toLowerCase(), - baseName, - baseName, baseName, - baseName, toolName, description, description + " 해줘.", interfaceId, register, - baseName, - baseName, - routingType, interfaceId - ); + """.formatted( + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + author, + createDate, + createDate, author, + routingType, group.toLowerCase(), + baseName, + baseName, toolName, description, description + " 해줘.", interfaceId, register, + baseName, + baseName, interfaceId + ); + } else { + serviceContent = """ + package %s.service; + + import %s.annotation.McpFunction; + import %s.annotation.McpTool; + import %s.dto.%sReq; + import %s.dto.%sRes; + import %s.%s.converter.%sLegacyConverter; + import org.springframework.stereotype.Service; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.service + * @className %sService + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Service + @McpTool( + routingType = "%s", + categoryKey = "%s" + ) + public class %sService extends AbstractMcpToolService { + + private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class); + + @McpFunction( + displayName = "%s 툴", + name = "%s", + description = "%s", + prompt = "%s", + mappingId = "%s", + register = %s, + requiresApproval = false + ) + public Object execute(%sReq req) { + // %sLegacyReq legacyReq = converter.toLegacyReq(req); + return executeLegacy("%s", "%s", req); // Or pass legacyReq + } + } + """.formatted( + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, shortName, baseName, + BASE_PACKAGE, baseName, + author, + createDate, + createDate, author, + routingType, group.toLowerCase(), + baseName, + baseName, toolName, description, description + " 해줘.", interfaceId, register, + baseName, + baseName, + routingType, interfaceId + ); + } Files.writeString(serviceDir.resolve(baseName + "Service.java"), serviceContent); + if (!isMci) { + String legacyReqContent = """ + package %s.%s.dto; + import lombok.Data; - // Generate Legacy Req DTO - String legacyReqContent = """ - package %s.%s.dto; + /** + * @package %s.%s.dto + * @className %sLegacyReq + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyReq { + // TODO: Add legacy request fields here + } + """.formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent); - import lombok.Data; + String legacyResContent = """ + package %s.%s.dto; - /** - * @package %s.%s.dto - * @className %sLegacyReq - * @description AX HUB 시스템 처리 클래스 - * @author %s - * @create %s - *
-             * ---------- 개정이력 ----------
-             * 수정일      수정자    수정내용
-             * ---------- -------- ---------------------------
-             * %s  %s    최초생성
-             * 
-             * 
- */ - @Data - public class %sLegacyReq { - // TODO: Add legacy request fields here - } - """.formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName); - Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent); + import lombok.Data; - // Generate Legacy Res DTO - String legacyResContent = """ - package %s.%s.dto; + /** + * @package %s.%s.dto + * @className %sLegacyRes + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyRes { + // TODO: Add legacy response fields here + } + """.formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent); - import lombok.Data; + String converterContent = """ + package %s.%s.converter; - /** - * @package %s.%s.dto - * @className %sLegacyRes - * @description AX HUB 시스템 처리 클래스 - * @author %s - * @create %s - *
-             * ---------- 개정이력 ----------
-             * 수정일      수정자    수정내용
-             * ---------- -------- ---------------------------
-             * %s  %s    최초생성
-             * 
-             * 
- */ - @Data - public class %sLegacyRes { - // TODO: Add legacy response fields here - } - """.formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName); - Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent); + import %s.dto.%sReq; + import %s.dto.%sRes; + import %s.%s.dto.%sLegacyReq; + import %s.%s.dto.%sLegacyRes; + import org.mapstruct.Mapper; + import org.mapstruct.Mapping; + import org.mapstruct.factory.Mappers; - // Generate Legacy Converter - String converterContent = """ - package %s.%s.converter; + /** + * @package %s.%s.converter + * @className %sLegacyConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sLegacyConverter { - import %s.dto.%sReq; - import %s.dto.%sRes; - import %s.%s.dto.%sLegacyReq; - import %s.%s.dto.%sLegacyRes; - import org.mapstruct.Mapper; - import org.mapstruct.Mapping; - import org.mapstruct.factory.Mappers; - - /** - * @package %s.%s.converter - * @className %sLegacyConverter - * @description AX HUB 시스템 처리 클래스 - * @author %s - * @create %s - *
-             * ---------- 개정이력 ----------
-             * 수정일      수정자    수정내용
-             * ---------- -------- ---------------------------
-             * %s  %s    최초생성
-             * 
-             * 
- */ - @Mapper(componentModel = "spring") - public interface %sLegacyConverter { - - // @Mapping(source = "sourceField", target = "targetField") - %sLegacyReq toLegacyReq(%sReq req); - - %sRes toRes(%sLegacyRes legacyRes); - } - """.formatted( - BASE_PACKAGE, shortName, - BASE_PACKAGE, baseName, - BASE_PACKAGE, baseName, - BASE_PACKAGE, shortName, baseName, - BASE_PACKAGE, shortName, baseName, - BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, - baseName, baseName, baseName, baseName, baseName, baseName - ); - Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent); + %sLegacyReq toLegacyReq(%sReq req); + + %sRes toRes(%sLegacyRes legacyRes); + } + """.formatted( + BASE_PACKAGE, shortName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, shortName, baseName, + BASE_PACKAGE, shortName, baseName, + BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, + baseName, baseName, baseName, baseName, baseName, baseName + ); + Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent); + } log.append("\n=========================================\n"); - log.append(" Scaffolding Complete!\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); log.append("=========================================\n"); log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).append("\n"); log.append("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\n"); log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.java")).append("\n"); - log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\n"); - log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\n"); - log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n"); + + if (!isMci) { + log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\n"); + log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\n"); + log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n"); + } log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n"); return log.toString(); diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/biz/mcp/tool/annotation/McpFunctionAnnotationsTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/biz/mcp/tool/annotation/McpFunctionAnnotationsTest.java new file mode 100644 index 0000000..dccaa59 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/biz/mcp/tool/annotation/McpFunctionAnnotationsTest.java @@ -0,0 +1,27 @@ +package io.shinhanlife.dap.biz.mcp.tool.annotation; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +class McpFunctionAnnotationsTest { + + @Test + void usesConservativeDefaultsForMcpBehaviorHints() throws NoSuchMethodException { + Method method = SampleTool.class.getDeclaredMethod("execute"); + McpFunction annotation = method.getAnnotation(McpFunction.class); + + assertFalse(annotation.readOnlyHint()); + assertFalse(annotation.destructiveHint()); + assertFalse(annotation.idempotentHint()); + assertFalse(annotation.openWorldHint()); + } + + static class SampleTool { + @McpFunction(displayName = "sample", name = "sample", description = "sample") + void execute() { + } + } +} diff --git a/patch_scaffolder.py b/patch_scaffolder.py new file mode 100644 index 0000000..dc480bd --- /dev/null +++ b/patch_scaffolder.py @@ -0,0 +1,296 @@ +import re +import os + +FILE_PATH = r"c:\Users\Admin\IdeaProjects\axhub-backend-main\dap-tool-core\src\main\java\io\shinhanlife\dap\common\util\ToolScaffolder.java" + +with open(FILE_PATH, 'r', encoding='utf-8') as f: + content = f.read() + +part1_start = content.find('String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);') +part1_end = content.find('log.append("\\n=========================================\\n");') + +if part1_start == -1 or part1_end == -1: + print("Could not find the target block.") + exit(1) + +new_block = '''String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1); + + boolean isMci = "MCI".equalsIgnoreCase(routingType); + String serviceContent; + + if (isMci) { + serviceContent = """ + package %s.service; + + import %s.annotation.McpFunction; + import %s.annotation.McpTool; + import %s.dto.%sReq; + import %s.dto.%sRes; + import io.shinhanlife.dap.common.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + import org.springframework.stereotype.Service; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + + /** + * @package %s.service + * @className %sService + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + @McpTool( + routingType = "%s", + categoryKey = "%s" + ) + public class %sService { + + private final AxhubMciComponent mci; + + @McpFunction( + displayName = "%s 툴", + name = "%s", + description = "%s", + prompt = "%s", + mappingId = "%s", + register = %s, + requiresApproval = false, + openWorldHint = true + ) + public Object execute(%sReq req) { + log.info("[MCI Tool] {} 요청 수신.", "%s"); + try { + Transfer resTransfer = mci.callTo( + "%s", + null, + req, + Object.class + ); + return resTransfer.getBody() != null ? resTransfer.getBody() : "{\\"status\\":\\"SUCCESS\\"}"; + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return "{\\"status\\":\\"ERROR\\", \\"message\\":\\"" + e.getMessage() + "\\"}"; + } + } + } + """.formatted( + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + author, + createDate, + createDate, author, + routingType, group.toLowerCase(), + baseName, + baseName, toolName, description, description + " 해줘.", interfaceId, register, + baseName, + baseName, interfaceId + ); + } else { + serviceContent = """ + package %s.service; + + import %s.annotation.McpFunction; + import %s.annotation.McpTool; + import %s.dto.%sReq; + import %s.dto.%sRes; + import %s.%s.converter.%sLegacyConverter; + import org.springframework.stereotype.Service; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.service + * @className %sService + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Service + @McpTool( + routingType = "%s", + categoryKey = "%s" + ) + public class %sService extends AbstractMcpToolService { + + private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class); + + @McpFunction( + displayName = "%s 툴", + name = "%s", + description = "%s", + prompt = "%s", + mappingId = "%s", + register = %s, + requiresApproval = false + ) + public Object execute(%sReq req) { + // %sLegacyReq legacyReq = converter.toLegacyReq(req); + return executeLegacy("%s", "%s", req); // Or pass legacyReq + } + } + """.formatted( + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, shortName, baseName, + BASE_PACKAGE, baseName, + author, + createDate, + createDate, author, + routingType, group.toLowerCase(), + baseName, + baseName, toolName, description, description + " 해줘.", interfaceId, register, + baseName, + baseName, + routingType, interfaceId + ); + } + + Files.writeString(serviceDir.resolve(baseName + "Service.java"), serviceContent); + + if (!isMci) { + String legacyReqContent = """ + package %s.%s.dto; + + import lombok.Data; + + /** + * @package %s.%s.dto + * @className %sLegacyReq + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyReq { + // TODO: Add legacy request fields here + } + """.formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent); + + String legacyResContent = """ + package %s.%s.dto; + + import lombok.Data; + + /** + * @package %s.%s.dto + * @className %sLegacyRes + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyRes { + // TODO: Add legacy response fields here + } + """.formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent); + + String converterContent = """ + package %s.%s.converter; + + import %s.dto.%sReq; + import %s.dto.%sRes; + import %s.%s.dto.%sLegacyReq; + import %s.%s.dto.%sLegacyRes; + import org.mapstruct.Mapper; + import org.mapstruct.Mapping; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.%s.converter + * @className %sLegacyConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sLegacyConverter { + + %sLegacyReq toLegacyReq(%sReq req); + + %sRes toRes(%sLegacyRes legacyRes); + } + """.formatted( + BASE_PACKAGE, shortName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, baseName, + BASE_PACKAGE, shortName, baseName, + BASE_PACKAGE, shortName, baseName, + BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, + baseName, baseName, baseName, baseName, baseName, baseName + ); + Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent); + } + + ''' + +log_block_start = content.find('log.append("\\n=========================================\\n");') +log_block_end = content.find('return log.toString();') + +new_log_block = '''log.append("\\n=========================================\\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\\n"); + log.append("=========================================\\n"); + log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).append("\\n"); + log.append("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\\n"); + log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.java")).append("\\n"); + + if (!isMci) { + log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\\n"); + log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\\n"); + log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\\n"); + } + log.append("\\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\\n"); + + ''' + +content = content[:part1_start] + new_block + new_log_block + content[log_block_end:] + +with open(FILE_PATH, 'w', encoding='utf-8') as f: + f.write(content) + +print("ToolScaffolder.java patched successfully.")