Fix tools/call routing and dependency injection for MSA architecture

This commit is contained in:
jade
2026-07-05 18:01:39 +09:00
parent 10f3f0513d
commit 4c718d38f1
22 changed files with 399 additions and 18 deletions

4
axhub-gateway/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY build/libs/axhub-gateway-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -4,7 +4,8 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication @SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.gateway", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@org.springframework.boot.context.properties.ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.gateway", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching @EnableCaching
public class AxHubGatewayApplication { public class AxHubGatewayApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -47,7 +47,13 @@ public class ExecuteService {
// Forward the request to the target tool pod // Forward the request to the target tool pod
try { try {
ResponseEntity<Object> response = restTemplate.postForEntity(executeApiUrl, payload, Object.class); org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
headers.set("X-API-KEY", "SHINHAN_MCP_TEST_KEY_9999"); // TODO: Use actual tenant's key
headers.set("X-Trace-Id", java.util.UUID.randomUUID().toString());
org.springframework.http.HttpEntity<Map<String, Object>> entity = new org.springframework.http.HttpEntity<>(payload, headers);
ResponseEntity<Object> response = restTemplate.postForEntity(executeApiUrl, entity, Object.class);
return response.getBody(); return response.getBody();
} catch (Exception e) { } catch (Exception e) {
log.error(" [ExecuteService] Tool Pod 호출 실패: {}", e.getMessage()); log.error(" [ExecuteService] Tool Pod 호출 실패: {}", e.getMessage());

View File

@@ -29,16 +29,19 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/api/tool") @RequestMapping("/")
@RequiredArgsConstructor @RequiredArgsConstructor
public class BusinessToolController { public class BusinessToolController {
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
// 함수명(functionName) 기반 단일 동적 라우팅 엔드포인트 // JSON RPC 기반 단일 라우팅 엔드포인트
@PostMapping("/execute/{functionName}") @PostMapping("/mcp/api/v1/tools/call")
public Map<String, Object> executeDynamicTool(@PathVariable String functionName, @RequestBody(required = false) Map<String, Object> payload) { public Map<String, Object> executeDynamicTool(@RequestBody(required = false) Map<String, Object> payload) {
Map<String, Object> params = payload != null ? (Map<String, Object>) payload.get("params") : null;
String functionName = params != null ? (String) params.get("name") : null;
log.info("\n [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName); log.info("\n [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
if (payload != null) { if (payload != null) {
try { try {
@@ -48,10 +51,8 @@ public class BusinessToolController {
} }
} }
Map<String, Object> params = payload != null ? (Map<String, Object>) payload.get("params") : null;
Map<String, Object> arguments = params != null ? (Map<String, Object>) params.get("arguments") : null;
// 1. 대상 Bean 및 Method 찾기 (ApplicationContext 활용) // 1. 대상 Bean 및 Method 찾기 (ApplicationContext 활용)
Map<String, Object> arguments = params != null ? (Map<String, Object>) params.get("arguments") : null;
Object targetBean = null; Object targetBean = null;
Method targetMethod = null; Method targetMethod = null;
McpFunction targetFunctionAnnotation = null; McpFunction targetFunctionAnnotation = null;

View File

@@ -0,0 +1,4 @@
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY build/libs/axhub-tool-email-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -4,7 +4,8 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication @SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@org.springframework.boot.context.properties.ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching @EnableCaching
public class AxHubToolEmailApplication { public class AxHubToolEmailApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -0,0 +1,26 @@
# Local ?˜ê²½ ?„ìš© ?¤ì • (H2 메모ë¦?DB ??
spring.datasource.url=jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
# EIMS µ¿Àû ¶ó¿ìÆÃ Á¢¼Ó Á¤º¸
eims.http.url=http://localhost:8081/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
eims.tcp.timeout=5000
eims.jsp.form.url=http://localhost:8081/mock/jsp-form
eims.jsp.json.url=http://localhost:8081/mock/jsp-json
eims.mci.url=http://localhost:8081/mock/esb/api
# API º¸¾È Ű ¼³Á¤
mcp.security.api-keys.SHINHAN_MCP_SECRET_KEY_2026=mcp-client-1
mcp.security.api-keys.SHINHAN_MCP_TEST_KEY_9999=mcp-client-2
mcp.security.tenant-domains.mcp-client-1=CUSTOMER,COMMON
mcp.security.tenant-domains.mcp-client-2=ALL
# Gateway/Tool URLs
mcp.adapter.url=http://localhost:8081/rpc/v1/execute
mcp.gateway.url=http://localhost:8081/mcp/api/v1
mcp.tool.host=localhost

View File

@@ -0,0 +1,4 @@
server.port=8083
spring.application.name=axhub-tool-email
spring.profiles.active=local

View File

@@ -0,0 +1,4 @@
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY build/libs/axhub-tool-other-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -4,7 +4,8 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication @SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@org.springframework.boot.context.properties.ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching @EnableCaching
public class AxHubToolOtherApplication { public class AxHubToolOtherApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -0,0 +1,26 @@
# Local ?˜ê²½ ?„ìš© ?¤ì • (H2 메모ë¦?DB ??
spring.datasource.url=jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
# EIMS µ¿Àû ¶ó¿ìÆÃ Á¢¼Ó Á¤º¸
eims.http.url=http://localhost:8081/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
eims.tcp.timeout=5000
eims.jsp.form.url=http://localhost:8081/mock/jsp-form
eims.jsp.json.url=http://localhost:8081/mock/jsp-json
eims.mci.url=http://localhost:8081/mock/esb/api
# API º¸¾È Ű ¼³Á¤
mcp.security.api-keys.SHINHAN_MCP_SECRET_KEY_2026=mcp-client-1
mcp.security.api-keys.SHINHAN_MCP_TEST_KEY_9999=mcp-client-2
mcp.security.tenant-domains.mcp-client-1=CUSTOMER,COMMON
mcp.security.tenant-domains.mcp-client-2=ALL
# Gateway/Tool URLs
mcp.adapter.url=http://localhost:8081/rpc/v1/execute
mcp.gateway.url=http://localhost:8081/mcp/api/v1
mcp.tool.host=localhost

View File

@@ -0,0 +1,4 @@
server.port=8084
spring.application.name=axhub-tool-other
spring.profiles.active=local

View File

@@ -0,0 +1,4 @@
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY build/libs/axhub-tool-sms-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -4,7 +4,8 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication @SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@org.springframework.boot.context.properties.ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching @EnableCaching
public class AxHubToolSmsApplication { public class AxHubToolSmsApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -0,0 +1,26 @@
# Local ?˜ê²½ ?„ìš© ?¤ì • (H2 메모ë¦?DB ??
spring.datasource.url=jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
# EIMS µ¿Àû ¶ó¿ìÆÃ Á¢¼Ó Á¤º¸
eims.http.url=http://localhost:8081/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
eims.tcp.timeout=5000
eims.jsp.form.url=http://localhost:8081/mock/jsp-form
eims.jsp.json.url=http://localhost:8081/mock/jsp-json
eims.mci.url=http://localhost:8081/mock/esb/api
# API º¸¾È Ű ¼³Á¤
mcp.security.api-keys.SHINHAN_MCP_SECRET_KEY_2026=mcp-client-1
mcp.security.api-keys.SHINHAN_MCP_TEST_KEY_9999=mcp-client-2
mcp.security.tenant-domains.mcp-client-1=CUSTOMER,COMMON
mcp.security.tenant-domains.mcp-client-2=ALL
# Gateway/Tool URLs
mcp.adapter.url=http://localhost:8081/rpc/v1/execute
mcp.gateway.url=http://localhost:8081/mcp/api/v1
mcp.tool.host=localhost

View File

@@ -0,0 +1,4 @@
server.port=8082
spring.application.name=axhub-tool-sms
spring.profiles.active=local

86
create_docker.py Normal file
View File

@@ -0,0 +1,86 @@
import os
modules = {
"axhub-gateway": 8081,
"axhub-tool-sms": 8082,
"axhub-tool-email": 8083,
"axhub-tool-other": 8084
}
# 1. Create Dockerfiles
dockerfile_template = """FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY build/libs/{module}-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
"""
for module in modules:
with open(f"{module}/Dockerfile", "w") as f:
f.write(dockerfile_template.format(module=module))
# 2. Create application.properties for tool modules
for module, port in modules.items():
if module == "axhub-gateway":
continue
res_dir = f"{module}/src/main/resources"
os.makedirs(res_dir, exist_ok=True)
with open(f"{res_dir}/application.properties", "w") as f:
f.write(f"server.port={port}\n")
f.write("spring.application.name=" + module + "\n")
# 3. Create docker-compose.yml
compose_content = """version: '3.8'
services:
redis:
image: redis:latest
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
gateway:
build:
context: ./axhub-gateway
ports:
- "8081:8081"
depends_on:
redis:
condition: service_healthy
environment:
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
tool-sms:
build:
context: ./axhub-tool-sms
ports:
- "8082:8082"
depends_on:
- redis
tool-email:
build:
context: ./axhub-tool-email
ports:
- "8083:8083"
depends_on:
- redis
tool-other:
build:
context: ./axhub-tool-other
ports:
- "8084:8084"
depends_on:
- redis
"""
with open("docker-compose.yml", "w") as f:
f.write(compose_content)
print("Docker configurations generated.")

View File

@@ -3,12 +3,63 @@ version: '3.8'
services: services:
redis: redis:
image: redis:latest image: redis:latest
container_name: axhub-redis
ports: ports:
- "6379:6379" - "6379:6379"
volumes: healthcheck:
- redis_data:/data test: ["CMD", "redis-cli", "ping"]
restart: always interval: 5s
timeout: 3s
retries: 5
volumes: gateway:
redis_data: build:
context: ./axhub-gateway
ports:
- "8081:8081"
depends_on:
redis:
condition: service_healthy
environment:
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_HOST=redis
- SPRING_DATA_REDIS_PORT=6379
tool-sms:
build:
context: ./axhub-tool-sms
ports:
- "8082:8082"
depends_on:
- redis
environment:
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_HOST=redis
- SPRING_DATA_REDIS_PORT=6379
tool-email:
build:
context: ./axhub-tool-email
ports:
- "8083:8083"
depends_on:
- redis
environment:
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_HOST=redis
- SPRING_DATA_REDIS_PORT=6379
tool-other:
build:
context: ./axhub-tool-other
ports:
- "8084:8084"
depends_on:
- redis
environment:
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_HOST=redis
- SPRING_DATA_REDIS_PORT=6379

23
fix_adapter_scan.py Normal file
View File

@@ -0,0 +1,23 @@
import os
import re
files = [
"axhub-tool-sms/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/sms/AxHubToolSmsApplication.java",
"axhub-tool-email/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/email/AxHubToolEmailApplication.java",
"axhub-tool-other/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/other/AxHubToolOtherApplication.java"
]
for file_path in files:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Replace the basePackages
content = content.replace(
'{"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"}',
'{"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"}'
)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
print(f"Fixed {file_path}")

22
fix_scans.py Normal file
View File

@@ -0,0 +1,22 @@
import os
files_to_fix = {
"axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/AxHubGatewayApplication.java": '{"io.shinhanlife.axhub.biz.mcp.gateway", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"}',
"axhub-tool-sms/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/sms/AxHubToolSmsApplication.java": '{"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"}',
"axhub-tool-email/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/email/AxHubToolEmailApplication.java": '{"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"}',
"axhub-tool-other/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/other/AxHubToolOtherApplication.java": '{"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"}',
}
for filepath, packages in files_to_fix.items():
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# regex replace the content in the string
import re
content = re.sub(r'scanBasePackages = \{.*?\}', f'scanBasePackages = {packages}', content)
content = re.sub(r'basePackages = \{.*?\}', f'basePackages = {packages}', content)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print("Fixed component scans")

55
register_tools.py Normal file
View File

@@ -0,0 +1,55 @@
import urllib.request
import urllib.parse
import json
tools = [
{
"toolName": "grade",
"description": "고객등급 조회",
"podUrl": "http://tool-other:8084",
"domainGroup": "biz_support",
"integrationType": "TCP",
"mciServiceId": "CRM_001",
"parametersSchema": {
"type": "object",
"properties": {
"reqId": { "type": "string" }
}
},
"actionPrompts": {
"grade": "이 고객의 VIP 등급을 조회해줘."
}
},
{
"toolName": "detail",
"description": "고객상세 정보 조회",
"podUrl": "http://tool-other:8084",
"domainGroup": "biz_support",
"integrationType": "TCP",
"mciServiceId": "CRM_002",
"parametersSchema": {
"type": "object",
"properties": {
"reqId": { "type": "string" }
}
},
"actionPrompts": {
"detail": "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘."
}
}
]
url = 'http://localhost:8081/mcp/api/v1/registry/register'
headers = {
'Content-Type': 'application/json',
'X-API-KEY': 'SHINHAN_MCP_TEST_KEY_9999'
}
for tool in tools:
data = json.dumps(tool).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers, method='POST')
try:
with urllib.request.urlopen(req) as response:
print(f"Registered {tool['toolName']}: {response.read().decode('utf-8')}")
except Exception as e:
print(f"Error registering {tool['toolName']}: {e}")

27
test_call.py Normal file
View File

@@ -0,0 +1,27 @@
import urllib.request
import json
payload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "grade",
"arguments": { "cust_id": "C001" }
},
"id": 1
}
url = 'http://localhost:8081/mcp/api/v1/tools/call'
headers = {
'Content-Type': 'application/json',
'X-API-KEY': 'SHINHAN_MCP_TEST_KEY_9999'
}
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers, method='POST')
try:
with urllib.request.urlopen(req) as response:
print(f"Response: {response.read().decode('utf-8')}")
except Exception as e:
print(f"Error: {e}")