feat: use MCP SDK for tool pod calls
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user