feat: use MCP SDK for tool pod calls
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s

This commit is contained in:
jade
2026-07-29 23:03:11 +09:00
parent 0a2464d426
commit 5fe8590695
11 changed files with 254 additions and 74 deletions

View File

@@ -261,7 +261,6 @@ public class ExecuteService {
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
targetUrl = metadata.getPodUrl();
}
String executeApiUrl = targetUrl + "/mcp/" + metadata.getName();
Map<String, String> headers = new java.util.HashMap<>();
headers.put("trace-id", context.requestId());
@@ -279,16 +278,16 @@ public class ExecuteService {
JsonNode data = null;
try {
data = toolInvoker.invoke(pagePayload, executeApiUrl, headers);
data = toolInvoker.invoke(metadata.getName(), pagePayload, targetUrl, headers);
} catch (org.springframework.web.client.RestClientResponseException e) {
// HTTP 4xx, 5xx 에러는 연결 오류가 아니라 비즈니스 로직 오류이거나 검증 실패이므로 원본 에러를 그대로 반환
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 에러 (" + e.getStatusCode() + "): " + e.getResponseBodyAsString());
} catch (Exception e) {
if (executeApiUrl.contains("http://tool-")) {
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
if (targetUrl.contains("http://tool-")) {
String fallbackUrl = targetUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
try {
data = toolInvoker.invoke(pagePayload, fallbackUrl, headers);
data = toolInvoker.invoke(metadata.getName(), pagePayload, fallbackUrl, headers);
} catch (Exception ex) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
}
@@ -385,4 +384,4 @@ public class ExecuteService {
public void shutdown() {
executor.shutdownNow();
}
}
}

View File

@@ -1,69 +1,81 @@
package io.shinhanlife.dap.mcg.transport;
/**
* @package io.shinhanlife.dap.mcg.transport
* @className HttpToolInvoker
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.node.TextNode;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/** MCP SDK client used for Gateway-to-Tool-Pod calls over Streamable HTTP. */
@Slf4j
@Component
public class HttpToolInvoker implements ToolInvoker {
private final RestClient restClient;
private final ObjectMapper objectMapper;
public HttpToolInvoker(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.restClient = RestClient.create();
}
@Override
public JsonNode invoke(Map<String, Object> payload, String targetUrl, Map<String, String> headers) {
try {
RestClient.RequestBodySpec requestSpec = restClient.post()
.uri(targetUrl)
.contentType(MediaType.APPLICATION_JSON);
if (headers != null) {
headers.forEach(requestSpec::header);
public JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers) {
String endpoint = podUrl.replaceAll("/+$", "") + "/mcp";
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
if (headers != null) {
headers.forEach(requestBuilder::header);
}
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(endpoint)
.requestBuilder(requestBuilder)
.connectTimeout(Duration.ofSeconds(5))
.build();
try (McpSyncClient client = McpClient.sync(transport)
.clientInfo(new McpSchema.Implementation("dap-gateway", "1.0.0"))
.requestTimeout(Duration.ofSeconds(30))
.build()) {
client.initialize();
McpSchema.CallToolResult result = client.callTool(McpSchema.CallToolRequest.builder()
.name(toolName)
.arguments(arguments)
.build());
if (Boolean.TRUE.equals(result.isError())) {
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool Pod MCP error: " + textContent(result));
}
Object httpResult = requestSpec.body(payload)
.retrieve()
.body(Object.class);
return extractData(objectMapper.valueToTree(httpResult));
} catch (Exception e) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 호출 실패: " + e.getMessage(), e);
return result.structuredContent() != null
? objectMapper.valueToTree(result.structuredContent())
: textContentAsJson(result);
} catch (ToolExecutionException error) {
throw error;
} catch (Exception error) {
throw new ToolExecutionException(FailureType.SERVER_ERROR,
"Tool Pod MCP call failed: " + error.getMessage(), error);
}
}
private JsonNode extractData(JsonNode root) {
if (!root.path("success").asBoolean(true)) {
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool 서버 업무 오류: " + root.path("error").asText());
private JsonNode textContentAsJson(McpSchema.CallToolResult result) {
String text = textContent(result);
try {
return objectMapper.readTree(text);
} catch (Exception ignored) {
return TextNode.valueOf(text);
}
return root.has("data") ? root.get("data") : root;
}
private String textContent(McpSchema.CallToolResult result) {
return result.content().stream()
.filter(McpSchema.TextContent.class::isInstance)
.map(McpSchema.TextContent.class::cast)
.map(McpSchema.TextContent::text)
.findFirst()
.orElse("");
}
}

View File

@@ -22,5 +22,5 @@ import java.util.Map;
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
*/
public interface ToolInvoker {
JsonNode invoke(Map<String, Object> payload, String targetUrl, Map<String, String> headers);
JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers);
}