feat: 이식 완료 - MCP Gateway 응답 구조화, 가드레일, 통신 계층 추상화 적용
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.guardrail;
|
||||||
|
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 서버 응답을 Agent에게 전달하기 전에 환각 방어 규칙을 적용하는 서비스입니다.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ToolResponseGuardrailService {
|
||||||
|
private final ObjectMapper json;
|
||||||
|
|
||||||
|
public ToolResponseGuardrailService(ObjectMapper json) {
|
||||||
|
this.json = json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 응답 data를 검증한 뒤 표준 MCP 응답 문자열로 감쌉니다.
|
||||||
|
*/
|
||||||
|
public String validateAndWrap(ToolMetadata metadata, String requestId, JsonNode data) {
|
||||||
|
validateData(metadata.getName(), data);
|
||||||
|
|
||||||
|
ObjectNode response = json.createObjectNode();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("toolName", metadata.getName());
|
||||||
|
response.put("requestId", requestId);
|
||||||
|
response.put("source", "registered-tool-server");
|
||||||
|
response.set("data", data);
|
||||||
|
response.set("answerPolicy", answerPolicy());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return json.writerWithDefaultPrettyPrinter().writeValueAsString(response);
|
||||||
|
} catch (Exception error) {
|
||||||
|
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "검증된 Tool 응답 직렬화에 실패했습니다.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 응답 data를 Agent에게 전달하기 전에 응답 규격으로 검증합니다.
|
||||||
|
*/
|
||||||
|
public void validateData(String toolName, JsonNode data) {
|
||||||
|
if (data == null || data.isMissingNode() || data.isNull()) {
|
||||||
|
throw new ToolExecutionException(FailureType.HALLUCINATION_GUARDRAIL, "Tool 응답 data가 비어 있습니다. tool=" + toolName);
|
||||||
|
}
|
||||||
|
// 향후 ToolMetadata에 responseAllowedFields 등이 추가되면 스키마 검증 로직 추가
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent가 Tool 결과 밖의 내용을 추측하지 않도록 응답 정책을 함께 내려줍니다.
|
||||||
|
*/
|
||||||
|
private ObjectNode answerPolicy() {
|
||||||
|
ObjectNode policy = json.createObjectNode();
|
||||||
|
policy.put("allowOnlyToolData", true);
|
||||||
|
policy.put("noGuessing", true);
|
||||||
|
policy.put("onMissingData", "answer_unknown_or_request_more_information");
|
||||||
|
policy.put("instruction", "Tool이 반환한 data 필드만 사용하세요. data에 없는 값은 추측해서 만들지 마세요.");
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import io.shinhanlife.axhub.biz.mcp.gateway.tool.large.LargeToolResponseService;
|
import io.shinhanlife.axhub.biz.mcp.gateway.tool.large.LargeToolResponseService;
|
||||||
import io.shinhanlife.axhub.biz.mcp.gateway.tool.large.PaginationRequestValidator;
|
import io.shinhanlife.axhub.biz.mcp.gateway.tool.large.PaginationRequestValidator;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.tool.result.ToolExecutionResultFormatter;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.tool.result.ToolExecutionResult;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.ToolResponseGuardrailService;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.transport.ToolInvoker;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -47,7 +51,9 @@ public class ExecuteService {
|
|||||||
private final McpGatewayProperties properties;
|
private final McpGatewayProperties properties;
|
||||||
private final LargeToolResponseService largeResponses;
|
private final LargeToolResponseService largeResponses;
|
||||||
private final PaginationRequestValidator paginationValidator;
|
private final PaginationRequestValidator paginationValidator;
|
||||||
private final RestClient restClient = RestClient.create();
|
private final ToolExecutionResultFormatter resultFormatter;
|
||||||
|
private final ToolResponseGuardrailService responseGuardrail;
|
||||||
|
private final ToolInvoker toolInvoker;
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
|
|
||||||
public ExecuteService(ToolPlanner planner,
|
public ExecuteService(ToolPlanner planner,
|
||||||
@@ -62,7 +68,10 @@ public class ExecuteService {
|
|||||||
RedisToolTraceService redisTrace,
|
RedisToolTraceService redisTrace,
|
||||||
McpGatewayProperties properties,
|
McpGatewayProperties properties,
|
||||||
LargeToolResponseService largeResponses,
|
LargeToolResponseService largeResponses,
|
||||||
PaginationRequestValidator paginationValidator) {
|
PaginationRequestValidator paginationValidator,
|
||||||
|
ToolExecutionResultFormatter resultFormatter,
|
||||||
|
ToolResponseGuardrailService responseGuardrail,
|
||||||
|
ToolInvoker toolInvoker) {
|
||||||
this.planner = planner;
|
this.planner = planner;
|
||||||
this.killSwitchService = killSwitchService;
|
this.killSwitchService = killSwitchService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
@@ -76,6 +85,9 @@ public class ExecuteService {
|
|||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.largeResponses = largeResponses;
|
this.largeResponses = largeResponses;
|
||||||
this.paginationValidator = paginationValidator;
|
this.paginationValidator = paginationValidator;
|
||||||
|
this.resultFormatter = resultFormatter;
|
||||||
|
this.responseGuardrail = responseGuardrail;
|
||||||
|
this.toolInvoker = toolInvoker;
|
||||||
|
|
||||||
this.executor = new ThreadPoolExecutor(
|
this.executor = new ThreadPoolExecutor(
|
||||||
properties.toolExecutorCorePoolSize(),
|
properties.toolExecutorCorePoolSize(),
|
||||||
@@ -123,7 +135,12 @@ public class ExecuteService {
|
|||||||
Object result = executeWithResilience(context, metadata, argumentsNode, payload);
|
Object result = executeWithResilience(context, metadata, argumentsNode, payload);
|
||||||
|
|
||||||
if (result instanceof com.fasterxml.jackson.databind.JsonNode) {
|
if (result instanceof com.fasterxml.jackson.databind.JsonNode) {
|
||||||
result = objectMapper.convertValue(result, Object.class);
|
// Apply Output Guardrail
|
||||||
|
String wrappedResponse = responseGuardrail.validateAndWrap(metadata, context.requestId(), (com.fasterxml.jackson.databind.JsonNode) result);
|
||||||
|
|
||||||
|
// Format the result
|
||||||
|
ToolExecutionResult formattedResult = resultFormatter.fromRawResponse(metadata.getName(), wrappedResponse);
|
||||||
|
result = formattedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
long elapsedMillis = elapsedMillis(startedAt);
|
long elapsedMillis = elapsedMillis(startedAt);
|
||||||
@@ -210,15 +227,13 @@ public class ExecuteService {
|
|||||||
|
|
||||||
JsonNode data = null;
|
JsonNode data = null;
|
||||||
try {
|
try {
|
||||||
Object httpResult = executeWithUrl(pagePayload, executeApiUrl);
|
data = toolInvoker.invoke(pagePayload, executeApiUrl);
|
||||||
data = extractData(objectMapper.valueToTree(httpResult));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (executeApiUrl.contains("http://tool-")) {
|
if (executeApiUrl.contains("http://tool-")) {
|
||||||
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
|
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
|
||||||
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
|
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
|
||||||
try {
|
try {
|
||||||
Object httpResult = executeWithUrl(pagePayload, fallbackUrl);
|
data = toolInvoker.invoke(pagePayload, fallbackUrl);
|
||||||
data = extractData(objectMapper.valueToTree(httpResult));
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
|
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
|
||||||
}
|
}
|
||||||
@@ -267,24 +282,6 @@ public class ExecuteService {
|
|||||||
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool 실행이 중단되었습니다: " + metadata.getName(), error);
|
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool 실행이 중단되었습니다: " + metadata.getName(), error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object executeWithUrl(Map<String, Object> payload, String url) {
|
|
||||||
return restClient.post()
|
|
||||||
.uri(url)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
// TODO: Use actual tenant's key
|
|
||||||
.header("X-Trace-Id", UUID.randomUUID().toString())
|
|
||||||
.body(payload)
|
|
||||||
.retrieve()
|
|
||||||
.body(Object.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonNode extractData(JsonNode root) {
|
|
||||||
if (!root.path("success").asBoolean(true)) {
|
|
||||||
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool 서버 업무 오류: " + root.path("error").asText());
|
|
||||||
}
|
|
||||||
return root.has("data") ? root.get("data") : root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RetryPolicy retryPolicy(ToolMetadata metadata, ObjectNode arguments) {
|
private RetryPolicy retryPolicy(ToolMetadata metadata, ObjectNode arguments) {
|
||||||
if (!retryAllowedByOperation(metadata, arguments)) {
|
if (!retryAllowedByOperation(metadata, arguments)) {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.tool.result;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Tool 실행 결과를 text 요약과 structuredContent로 분리해서 표현합니다.
|
||||||
|
*/
|
||||||
|
public record ToolExecutionResult(
|
||||||
|
ResultType resultType,
|
||||||
|
List<ContentItem> content,
|
||||||
|
JsonNode structuredContent,
|
||||||
|
List<ResourceLink> resourceLinks,
|
||||||
|
Map<String, Object> metadata,
|
||||||
|
boolean isError,
|
||||||
|
boolean truncated,
|
||||||
|
long originalSizeBytes
|
||||||
|
) {
|
||||||
|
public record ContentItem(String type, String text) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ResourceLink(String uri, String name, String mimeType, long sizeBytes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ResultType {
|
||||||
|
JSON_OBJECT,
|
||||||
|
JSON_ARRAY,
|
||||||
|
PLAIN_TEXT,
|
||||||
|
BINARY_FILE,
|
||||||
|
EMPTY,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.tool.result;
|
||||||
|
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 서버 원시 응답을 MCP의 content 요약과 structuredContent로 분리합니다.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ToolExecutionResultFormatter {
|
||||||
|
private static final int TEXT_PREVIEW_LIMIT = 2_000;
|
||||||
|
|
||||||
|
private final ObjectMapper json;
|
||||||
|
private final SensitiveDataMasker masker;
|
||||||
|
|
||||||
|
public ToolExecutionResultFormatter(ObjectMapper json, SensitiveDataMasker masker) {
|
||||||
|
this.json = json;
|
||||||
|
this.masker = masker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 문자열을 content.text에 그대로 넣지 않고 structuredContent로 분리합니다.
|
||||||
|
*/
|
||||||
|
public ToolExecutionResult fromRawResponse(String toolName, String rawResponse) {
|
||||||
|
long sizeBytes = rawResponse == null ? 0 : rawResponse.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
if (rawResponse == null || rawResponse.isBlank()) {
|
||||||
|
return result(ToolExecutionResult.ResultType.EMPTY, "Tool 응답이 비어 있습니다.",
|
||||||
|
json.createObjectNode(), Map.of("toolName", toolName), false, false, sizeBytes);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonNode parsed = json.readTree(rawResponse);
|
||||||
|
return fromJson(toolName, parsed, sizeBytes);
|
||||||
|
} catch (Exception parseError) {
|
||||||
|
ObjectNode metadata = json.createObjectNode();
|
||||||
|
metadata.put("toolName", toolName);
|
||||||
|
metadata.put("parseError", "JSON_PARSE_FAILED");
|
||||||
|
metadata.put("preview", preview(rawResponse));
|
||||||
|
return new ToolExecutionResult(
|
||||||
|
ToolExecutionResult.ResultType.PLAIN_TEXT,
|
||||||
|
List.of(new ToolExecutionResult.ContentItem("text", "Tool 응답이 JSON 형식이 아니어서 제한된 preview만 제공합니다.")),
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
Map.of("toolName", toolName, "parseError", "JSON_PARSE_FAILED", "preview", preview(rawResponse)),
|
||||||
|
false,
|
||||||
|
rawResponse.length() > TEXT_PREVIEW_LIMIT,
|
||||||
|
sizeBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToolExecutionResult fromJson(String toolName, JsonNode parsed, long sizeBytes) {
|
||||||
|
JsonNode masked = masker.mask(parsed);
|
||||||
|
if (masked.isObject()) {
|
||||||
|
ObjectNode object = (ObjectNode) masked;
|
||||||
|
if (object.path("isError").asBoolean(false) || object.has("error") || object.has("failureType")) {
|
||||||
|
return result(ToolExecutionResult.ResultType.ERROR, errorSummary(toolName, object), object,
|
||||||
|
metadata(toolName, sizeBytes), true, object.path("truncated").asBoolean(false), sizeBytes);
|
||||||
|
}
|
||||||
|
JsonNode structured = object.has("structuredContent") ? object.path("structuredContent") : structuredFromObject(object);
|
||||||
|
return result(ToolExecutionResult.ResultType.JSON_OBJECT, summary(toolName, object, structured), structured,
|
||||||
|
metadata(toolName, sizeBytes), false, object.path("truncated").asBoolean(false), sizeBytes);
|
||||||
|
}
|
||||||
|
if (masked.isArray()) {
|
||||||
|
ObjectNode structured = json.createObjectNode();
|
||||||
|
structured.set("items", masked);
|
||||||
|
structured.put("count", count(masked));
|
||||||
|
return result(ToolExecutionResult.ResultType.JSON_ARRAY,
|
||||||
|
toolName + " 결과 " + count(masked) + "건을 조회했습니다.",
|
||||||
|
structured,
|
||||||
|
metadata(toolName, sizeBytes),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
sizeBytes);
|
||||||
|
}
|
||||||
|
if (masked.isTextual()) {
|
||||||
|
String text = masked.asText("");
|
||||||
|
return result(ToolExecutionResult.ResultType.PLAIN_TEXT, preview(text), null,
|
||||||
|
metadata(toolName, sizeBytes), false, text.length() > TEXT_PREVIEW_LIMIT, sizeBytes);
|
||||||
|
}
|
||||||
|
ObjectNode structured = json.createObjectNode();
|
||||||
|
structured.set("value", masked);
|
||||||
|
return result(ToolExecutionResult.ResultType.JSON_OBJECT, toolName + " 결과를 조회했습니다.",
|
||||||
|
structured, metadata(toolName, sizeBytes), false, false, sizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode structuredFromObject(ObjectNode object) {
|
||||||
|
if (object.has("data")) {
|
||||||
|
return object.path("data");
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String summary(String toolName, ObjectNode object, JsonNode structured) {
|
||||||
|
String message = object.path("message").asText("");
|
||||||
|
if (!message.isBlank()) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
if (structured != null && structured.isObject()) {
|
||||||
|
long totalCount = structured.path("totalCount").asLong(-1);
|
||||||
|
if (totalCount >= 0) {
|
||||||
|
return toolName + " 대량 결과 preview 조회가 완료되었습니다. totalCount=" + totalCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (structured != null && structured.isArray()) {
|
||||||
|
return toolName + " 결과 " + count(structured) + "건을 조회했습니다.";
|
||||||
|
}
|
||||||
|
return toolName + " 조회가 완료되었습니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String errorSummary(String toolName, ObjectNode object) {
|
||||||
|
String message = object.path("message").asText(object.path("error").asText(""));
|
||||||
|
return message.isBlank() ? toolName + " 호출 중 오류가 발생했습니다." : message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToolExecutionResult result(ToolExecutionResult.ResultType type, String text, JsonNode structuredContent,
|
||||||
|
Map<String, Object> metadata, boolean error, boolean truncated, long sizeBytes) {
|
||||||
|
return new ToolExecutionResult(
|
||||||
|
type,
|
||||||
|
List.of(new ToolExecutionResult.ContentItem("text", text)),
|
||||||
|
structuredContent,
|
||||||
|
List.of(),
|
||||||
|
metadata,
|
||||||
|
error,
|
||||||
|
truncated,
|
||||||
|
sizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> metadata(String toolName, long sizeBytes) {
|
||||||
|
return Map.of("toolName", toolName, "originalSizeBytes", sizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int count(JsonNode node) {
|
||||||
|
int count = 0;
|
||||||
|
for (JsonNode ignored : node) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String preview(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value.length() <= TEXT_PREVIEW_LIMIT ? value : value.substring(0, TEXT_PREVIEW_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.transport;
|
||||||
|
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.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 java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
try {
|
||||||
|
Object httpResult = restClient.post()
|
||||||
|
.uri(targetUrl)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
// TODO: Use actual tenant's key
|
||||||
|
.header("X-Trace-Id", UUID.randomUUID().toString())
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode extractData(JsonNode root) {
|
||||||
|
if (!root.path("success").asBoolean(true)) {
|
||||||
|
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool 서버 업무 오류: " + root.path("error").asText());
|
||||||
|
}
|
||||||
|
return root.has("data") ? root.get("data") : root;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.transport;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
|
||||||
|
*/
|
||||||
|
public interface ToolInvoker {
|
||||||
|
JsonNode invoke(Map<String, Object> payload, String targetUrl);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user