feat: Add MCI Scaffolding support and fix bean collision
This commit is contained in:
161
chaos_test.py
Normal file
161
chaos_test.py
Normal file
@@ -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": "<script>alert(1)</script>' 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)
|
||||||
@@ -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<String> getPaths = Arrays.stream(DynamicMcpController.class.getDeclaredMethods())
|
||||||
|
.flatMap(method -> Arrays.stream(method.getAnnotationsByType(GetMapping.class)))
|
||||||
|
.flatMap(mapping -> Arrays.stream(mapping.value()))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Set<String> 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "shinhan.integration")
|
@ConfigurationProperties(prefix = "shinhan.integration")
|
||||||
public class ShinhanIntegrationProperties {
|
public class ShinhanIntegrationProperties {
|
||||||
|
|
||||||
|
|||||||
@@ -161,188 +161,270 @@ public class ToolScaffolder {
|
|||||||
|
|
||||||
String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
|
String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
|
||||||
|
|
||||||
// Generate Service
|
boolean isMci = "MCI".equalsIgnoreCase(routingType);
|
||||||
String serviceContent = """
|
String serviceContent;
|
||||||
package %s.service;
|
|
||||||
|
|
||||||
import %s.annotation.McpFunction;
|
if (isMci) {
|
||||||
import %s.annotation.McpTool;
|
serviceContent = """
|
||||||
import %s.dto.%sReq;
|
package %s.service;
|
||||||
import %s.dto.%sRes;
|
|
||||||
import %s.%s.converter.%sLegacyConverter;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
/**
|
import %s.annotation.McpFunction;
|
||||||
* @package %s.service
|
import %s.annotation.McpTool;
|
||||||
* @className %sService
|
import %s.dto.%sReq;
|
||||||
* @description AX HUB 시스템 처리 클래스
|
import %s.dto.%sRes;
|
||||||
* @author %s
|
import io.shinhanlife.dap.common.integration.mci.component.AxhubMciComponent;
|
||||||
* @create %s
|
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||||
* <pre>
|
import org.springframework.stereotype.Service;
|
||||||
* ---------- 개정이력 ----------
|
import lombok.RequiredArgsConstructor;
|
||||||
* 수정일 수정자 수정내용
|
import lombok.extern.slf4j.Slf4j;
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* %s %s 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@McpTool(
|
|
||||||
routingType = "%s",
|
|
||||||
categoryKey = "%s"
|
|
||||||
)
|
|
||||||
public class %sService extends AbstractMcpToolService {
|
|
||||||
|
|
||||||
private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class);
|
/**
|
||||||
|
* @package %s.service
|
||||||
@McpFunction(
|
* @className %sService
|
||||||
displayName = "%s 툴",
|
* @description AX HUB 시스템 처리 클래스
|
||||||
name = "%s",
|
* @author %s
|
||||||
description = "%s",
|
* @create %s
|
||||||
prompt = "%s",
|
* <pre>
|
||||||
mappingId = "%s",
|
* ---------- 개정이력 ----------
|
||||||
register = %s,
|
* 수정일 수정자 수정내용
|
||||||
requiresApproval = false
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@McpTool(
|
||||||
|
routingType = "%s",
|
||||||
|
categoryKey = "%s"
|
||||||
)
|
)
|
||||||
public Object execute(%sReq req) {
|
public class %sService {
|
||||||
// %sLegacyReq legacyReq = converter.toLegacyReq(req);
|
|
||||||
return executeLegacy("%s", "%s", req); // Or pass legacyReq
|
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<Object> 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(
|
||||||
""".formatted(
|
BASE_PACKAGE,
|
||||||
BASE_PACKAGE,
|
BASE_PACKAGE,
|
||||||
BASE_PACKAGE,
|
BASE_PACKAGE,
|
||||||
BASE_PACKAGE,
|
BASE_PACKAGE, baseName,
|
||||||
BASE_PACKAGE, baseName,
|
BASE_PACKAGE, baseName,
|
||||||
BASE_PACKAGE, baseName,
|
BASE_PACKAGE, baseName,
|
||||||
BASE_PACKAGE, shortName, baseName,
|
author,
|
||||||
BASE_PACKAGE,
|
createDate,
|
||||||
baseName,
|
createDate, author,
|
||||||
author,
|
routingType, group.toLowerCase(),
|
||||||
createDate,
|
baseName,
|
||||||
createDate, author,
|
baseName, toolName, description, description + " 해줘.", interfaceId, register,
|
||||||
routingType, group.toLowerCase(),
|
baseName,
|
||||||
baseName,
|
baseName, interfaceId
|
||||||
baseName, baseName,
|
);
|
||||||
baseName, toolName, description, description + " 해줘.", interfaceId, register,
|
} else {
|
||||||
baseName,
|
serviceContent = """
|
||||||
baseName,
|
package %s.service;
|
||||||
routingType, interfaceId
|
|
||||||
);
|
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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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);
|
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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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;
|
||||||
|
|
||||||
/**
|
import lombok.Data;
|
||||||
* @package %s.%s.dto
|
|
||||||
* @className %sLegacyReq
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author %s
|
|
||||||
* @create %s
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* %s %s 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@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);
|
|
||||||
|
|
||||||
// Generate Legacy Res DTO
|
/**
|
||||||
String legacyResContent = """
|
* @package %s.%s.dto
|
||||||
package %s.%s.dto;
|
* @className %sLegacyRes
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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;
|
||||||
|
|
||||||
/**
|
import %s.dto.%sReq;
|
||||||
* @package %s.%s.dto
|
import %s.dto.%sRes;
|
||||||
* @className %sLegacyRes
|
import %s.%s.dto.%sLegacyReq;
|
||||||
* @description AX HUB 시스템 처리 클래스
|
import %s.%s.dto.%sLegacyRes;
|
||||||
* @author %s
|
import org.mapstruct.Mapper;
|
||||||
* @create %s
|
import org.mapstruct.Mapping;
|
||||||
* <pre>
|
import org.mapstruct.factory.Mappers;
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* %s %s 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@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);
|
|
||||||
|
|
||||||
// Generate Legacy Converter
|
/**
|
||||||
String converterContent = """
|
* @package %s.%s.converter
|
||||||
package %s.%s.converter;
|
* @className %sLegacyConverter
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface %sLegacyConverter {
|
||||||
|
|
||||||
import %s.dto.%sReq;
|
%sLegacyReq toLegacyReq(%sReq req);
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
%sRes toRes(%sLegacyRes legacyRes);
|
||||||
* @package %s.%s.converter
|
}
|
||||||
* @className %sLegacyConverter
|
""".formatted(
|
||||||
* @description AX HUB 시스템 처리 클래스
|
BASE_PACKAGE, shortName,
|
||||||
* @author %s
|
BASE_PACKAGE, baseName,
|
||||||
* @create %s
|
BASE_PACKAGE, baseName,
|
||||||
* <pre>
|
BASE_PACKAGE, shortName, baseName,
|
||||||
* ---------- 개정이력 ----------
|
BASE_PACKAGE, shortName, baseName,
|
||||||
* 수정일 수정자 수정내용
|
BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author,
|
||||||
* ---------- -------- ---------------------------
|
baseName, baseName, baseName, baseName, baseName, baseName
|
||||||
* %s %s 최초생성
|
);
|
||||||
*
|
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
|
||||||
* </pre>
|
}
|
||||||
*/
|
|
||||||
@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);
|
|
||||||
|
|
||||||
log.append("\n=========================================\n");
|
log.append("\n=========================================\n");
|
||||||
log.append(" Scaffolding Complete!\n");
|
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
|
||||||
log.append("=========================================\n");
|
log.append("=========================================\n");
|
||||||
log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).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("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\n");
|
||||||
log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.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");
|
if (!isMci) {
|
||||||
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.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");
|
||||||
|
}
|
||||||
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
|
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
|
||||||
|
|
||||||
return log.toString();
|
return log.toString();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
296
patch_scaffolder.py
Normal file
296
patch_scaffolder.py
Normal file
@@ -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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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<Object> 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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@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.")
|
||||||
Reference in New Issue
Block a user