feat: refactor imports in ExecuteService and MCP Gateway items
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.axhub.biz.mcp.gateway.dto
|
||||||
|
* @className OperationType
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 김형식
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 김형식 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public enum OperationType {
|
||||||
|
READ,
|
||||||
|
WRITE
|
||||||
|
}
|
||||||
@@ -85,6 +85,22 @@ public class ToolMetadata {
|
|||||||
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
|
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
|
||||||
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
|
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
|
||||||
|
|
||||||
|
// 7. Gateway 코어 제어용 설정 필드 추가 (재시도, 타임아웃, 오퍼레이션 타입)
|
||||||
|
@Builder.Default
|
||||||
|
private OperationType operationType = OperationType.READ;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean retryEnabled = true;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Integer circuitBreakerFailureThreshold = 0;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Long circuitBreakerOpenMillis = 0L;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Long timeoutMillis = 0L;
|
||||||
|
|
||||||
// --- Guardrail 호환성을 위한 메서드 추가 ---
|
// --- Guardrail 호환성을 위한 메서드 추가 ---
|
||||||
public java.util.Set<String> allowedArguments() {
|
public java.util.Set<String> allowedArguments() {
|
||||||
if (parametersSchema == null || !parametersSchema.containsKey("properties")) return java.util.Set.of();
|
if (parametersSchema == null || !parametersSchema.containsKey("properties")) return java.util.Set.of();
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.redis;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class McpMonitorEventService {
|
||||||
|
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Monitor browser screen subscribes to this emitter.
|
||||||
|
*/
|
||||||
|
public SseEmitter subscribe() {
|
||||||
|
SseEmitter emitter = new SseEmitter(0L);
|
||||||
|
emitters.add(emitter);
|
||||||
|
emitter.onCompletion(() -> emitters.remove(emitter));
|
||||||
|
emitter.onTimeout(() -> emitters.remove(emitter));
|
||||||
|
emitter.onError(error -> emitters.remove(emitter));
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends one monitor event only when an Agent tool request changes state inside MCP.
|
||||||
|
*/
|
||||||
|
public void publish(String payload) {
|
||||||
|
for (SseEmitter emitter : emitters) {
|
||||||
|
try {
|
||||||
|
emitter.send(SseEmitter.event()
|
||||||
|
.name("mcp-tool-trace")
|
||||||
|
.data(payload));
|
||||||
|
} catch (IOException | IllegalStateException error) {
|
||||||
|
emitters.remove(emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.redis;
|
||||||
|
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContext;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class RedisToolTraceService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RedisToolTraceService.class);
|
||||||
|
private static final String KEY_PREFIX = "mcp:request:";
|
||||||
|
private static final String RECENT_KEY = "mcp:request:recent";
|
||||||
|
|
||||||
|
private final McpGatewayProperties properties;
|
||||||
|
private final ObjectProvider<StringRedisTemplate> redisProvider;
|
||||||
|
private final ObjectMapper json;
|
||||||
|
private final McpMonitorEventService monitorEvents;
|
||||||
|
private final SensitiveDataMasker masker;
|
||||||
|
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public RedisToolTraceService(McpGatewayProperties properties,
|
||||||
|
ObjectProvider<StringRedisTemplate> redisProvider,
|
||||||
|
ObjectMapper json,
|
||||||
|
McpMonitorEventService monitorEvents,
|
||||||
|
SensitiveDataMasker masker) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.redisProvider = redisProvider;
|
||||||
|
this.json = json;
|
||||||
|
this.monitorEvents = monitorEvents;
|
||||||
|
this.masker = masker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent request has entered the MCP tool gateway.
|
||||||
|
*/
|
||||||
|
public void started(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments) {
|
||||||
|
save(context, metadata, arguments, "STARTED", 0, "", 0, 0, 0,
|
||||||
|
"Agent request entered MCP", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP is attempting to call the target Tool server.
|
||||||
|
*/
|
||||||
|
public void attemptStarted(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||||
|
int attempt, int maxAttempts) {
|
||||||
|
save(context, metadata, arguments, "ATTEMPTING", 0, "", attempt, maxAttempts, 0,
|
||||||
|
"Calling tool server", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP will retry the Tool call after backoff.
|
||||||
|
*/
|
||||||
|
public void retryWaiting(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||||
|
int failedAttempt, int maxAttempts, long backoffMillis, String failureType) {
|
||||||
|
save(context, metadata, arguments, "RETRY_WAITING", 0, failureType, failedAttempt, maxAttempts, backoffMillis,
|
||||||
|
"Retry will be attempted after backoff", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool execution has finished.
|
||||||
|
*/
|
||||||
|
public void finished(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
|
||||||
|
long elapsedMillis, boolean success, String failureType, String responseText) {
|
||||||
|
save(context, metadata, arguments, success ? "SUCCESS" : "FAILED", elapsedMillis, failureType, 0, 0, 0,
|
||||||
|
success ? "Tool call completed" : "Tool call failed", responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recent live trace entries. One current entry is kept per request id.
|
||||||
|
*/
|
||||||
|
public List<String> recent() {
|
||||||
|
if (!properties.redisTraceEnabled()) {
|
||||||
|
return List.of("Redis trace is disabled. Set MCP_REDIS_TRACE_ENABLED=true.");
|
||||||
|
}
|
||||||
|
StringRedisTemplate redis = redisProvider.getIfAvailable();
|
||||||
|
if (redis == null) {
|
||||||
|
return List.of("RedisTemplate is not available.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<String> requestIds = redis.opsForList().range(RECENT_KEY, 0, 49);
|
||||||
|
if (requestIds == null || requestIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return requestIds.stream()
|
||||||
|
.map(requestId -> redis.opsForValue().get(KEY_PREFIX + requestId))
|
||||||
|
.filter(value -> value != null && !value.isBlank())
|
||||||
|
.toList();
|
||||||
|
} catch (Exception error) {
|
||||||
|
log.warn("Redis trace read failed. message={}", error.getMessage());
|
||||||
|
return List.of("Redis trace read failed: " + error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Historical accumulation is intentionally disabled.
|
||||||
|
*
|
||||||
|
* The MCP monitor is used to observe the current Agent request flow only,
|
||||||
|
* so this method returns the same live entries as recent().
|
||||||
|
*/
|
||||||
|
public List<String> history() {
|
||||||
|
return recent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
|
||||||
|
long elapsedMillis, String failureType, int attempt, int maxAttempts,
|
||||||
|
long backoffMillis, String message, String responseText) {
|
||||||
|
String stateKey = stateKey(context, metadata);
|
||||||
|
if (attempt > 0 && maxAttempts > 0) {
|
||||||
|
attemptStates.put(stateKey, new AttemptState(attempt, maxAttempts));
|
||||||
|
}
|
||||||
|
String payload;
|
||||||
|
try {
|
||||||
|
payload = tracePayload(context, metadata, arguments, status, elapsedMillis, failureType, attempt,
|
||||||
|
maxAttempts, backoffMillis, message, responseText);
|
||||||
|
monitorEvents.publish(payload);
|
||||||
|
if ("SUCCESS".equals(status) || "FAILED".equals(status)) {
|
||||||
|
attemptStates.remove(stateKey);
|
||||||
|
}
|
||||||
|
} catch (Exception error) {
|
||||||
|
log.warn("MCP monitor event publish failed. requestId={} tool={} message={}",
|
||||||
|
context.requestId(), metadata.getName(), error.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!properties.redisTraceEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringRedisTemplate redis = redisProvider.getIfAvailable();
|
||||||
|
if (redis == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String requestId = context.requestId();
|
||||||
|
Duration ttl = Duration.ofSeconds(properties.redisTraceTtlSeconds());
|
||||||
|
redis.opsForValue().set(KEY_PREFIX + requestId, payload, ttl);
|
||||||
|
redis.opsForList().remove(RECENT_KEY, 0, requestId);
|
||||||
|
redis.opsForList().leftPush(RECENT_KEY, requestId);
|
||||||
|
redis.opsForList().trim(RECENT_KEY, 0, 49);
|
||||||
|
redis.expire(RECENT_KEY, ttl);
|
||||||
|
} catch (Exception error) {
|
||||||
|
log.warn("Redis trace save failed. requestId={} tool={} message={}",
|
||||||
|
context.requestId(), metadata.getName(), error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String tracePayload(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
|
||||||
|
long elapsedMillis, String failureType, int attempt, int maxAttempts,
|
||||||
|
long backoffMillis, String message, String responseText) throws Exception {
|
||||||
|
AttemptState state = attemptStates.get(stateKey(context, metadata));
|
||||||
|
int displayAttempt = attempt > 0 ? attempt : state == null ? 0 : state.attempt();
|
||||||
|
int displayMaxAttempts = maxAttempts > 0 ? maxAttempts : state == null ? 0 : state.maxAttempts();
|
||||||
|
int retryCount = Math.max(0, displayAttempt - 1);
|
||||||
|
Map<String, Object> trace = new LinkedHashMap<>();
|
||||||
|
trace.put("requestId", context.requestId());
|
||||||
|
trace.put("traceGroupId", context.traceGroupId());
|
||||||
|
trace.put("agentId", context.agentId());
|
||||||
|
trace.put("userId", context.userId());
|
||||||
|
trace.put("clientAddress", context.clientAddress());
|
||||||
|
trace.put("toolName", metadata.getName());
|
||||||
|
trace.put("operationType", metadata.getOperationType() != null ? metadata.getOperationType().name() : "READ");
|
||||||
|
trace.put("status", status);
|
||||||
|
trace.put("message", message == null ? "" : message);
|
||||||
|
trace.put("failureType", failureType == null ? "" : failureType);
|
||||||
|
trace.put("attempt", displayAttempt);
|
||||||
|
trace.put("maxAttempts", displayMaxAttempts);
|
||||||
|
trace.put("retryCount", retryCount);
|
||||||
|
trace.put("backoffMillis", backoffMillis);
|
||||||
|
trace.put("elapsedMillis", elapsedMillis);
|
||||||
|
trace.put("idempotencyKeyHash", hashText(arguments.path("idempotencyKey").asText("")));
|
||||||
|
|
||||||
|
java.util.List<String> argNames = new java.util.ArrayList<>();
|
||||||
|
arguments.fieldNames().forEachRemaining(argNames::add);
|
||||||
|
trace.put("argumentNames", argNames);
|
||||||
|
|
||||||
|
trace.put("arguments", masker.mask(arguments).toString());
|
||||||
|
trace.put("responseSummary", responseSummary(responseText));
|
||||||
|
trace.put("timestamp", Instant.now().toString());
|
||||||
|
return json.writeValueAsString(trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis Trace에는 Tool 응답 원문을 저장하지 않고 size/hash/상태/카운트만 저장합니다.
|
||||||
|
*/
|
||||||
|
Map<String, Object> responseSummary(String responseText) {
|
||||||
|
Map<String, Object> summary = new LinkedHashMap<>();
|
||||||
|
if (responseText == null || responseText.isBlank()) {
|
||||||
|
summary.put("bytes", 0);
|
||||||
|
summary.put("sha256", "");
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
byte[] bytes = responseText.getBytes(StandardCharsets.UTF_8);
|
||||||
|
summary.put("bytes", bytes.length);
|
||||||
|
summary.put("sha256", sha256(bytes));
|
||||||
|
try {
|
||||||
|
JsonNode root = json.readTree(responseText);
|
||||||
|
summary.put("success", root.path("success").asBoolean(false));
|
||||||
|
summary.put("status", root.path("status").asText(""));
|
||||||
|
summary.put("toolName", root.path("toolName").asText(""));
|
||||||
|
summary.put("pageSize", root.path("pageSize").asInt(0));
|
||||||
|
summary.put("pageCount", root.path("pageCount").asInt(0));
|
||||||
|
summary.put("returnedCount", root.path("returnedCount").asInt(0));
|
||||||
|
summary.put("totalCount", root.path("totalCount").asLong(0));
|
||||||
|
summary.put("hasMore", root.path("hasMore").asBoolean(false));
|
||||||
|
summary.put("nextCursor", root.path("nextCursor").asText(""));
|
||||||
|
summary.put("message", root.path("message").asText(""));
|
||||||
|
summary.put("resultRef", root.path("resultRef").asText(""));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
summary.put("parseable", false);
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sha256(byte[] bytes) {
|
||||||
|
try {
|
||||||
|
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||||
|
} catch (Exception error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hashText(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return sha256(value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stateKey(McpRequestContext context, ToolMetadata metadata) {
|
||||||
|
return context.requestId() + ":" + metadata.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AttemptState(int attempt, int maxAttempts) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.resilience;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 호출 재시도 정책입니다.
|
||||||
|
*
|
||||||
|
* 최대 재시도 횟수, 최초 대기 시간, Backoff 배율, 최대 대기 시간을 담습니다.
|
||||||
|
*/
|
||||||
|
public record RetryPolicy(int maxAttempts, long initialBackoffMillis, double backoffMultiplier, long maxBackoffMillis) {
|
||||||
|
/**
|
||||||
|
* WRITE 요청처럼 재시도하면 위험한 경우 사용하는 비활성 정책입니다.
|
||||||
|
*/
|
||||||
|
public static RetryPolicy disabled() {
|
||||||
|
return new RetryPolicy(1, 0, 1.0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실패한 시도 횟수에 따라 다음 재시도 전 대기 시간을 계산합니다.
|
||||||
|
*/
|
||||||
|
public long backoffMillis(int failedAttempt) {
|
||||||
|
if (failedAttempt < 1 || initialBackoffMillis < 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double backoff = initialBackoffMillis * Math.pow(backoffMultiplier, failedAttempt - 1);
|
||||||
|
if (maxBackoffMillis > 0) {
|
||||||
|
backoff = Math.min(backoff, maxBackoffMillis);
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.round(backoff));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package io.shinhanlife.axhub.biz.mcp.gateway.security;
|
||||||
|
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
|
||||||
|
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.OperationType;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
/**
|
||||||
|
* Agent가 특정 Tool을 호출할 수 있는지 판단하는 권한 검증 서비스입니다.
|
||||||
|
*
|
||||||
|
* 서버 전체 허용 Tool, Agent가 보낸 allowed-tools 헤더, WRITE 승인 여부를 함께 확인합니다.
|
||||||
|
*/
|
||||||
|
public class ToolAuthorizationService {
|
||||||
|
private final McpGatewayProperties properties;
|
||||||
|
|
||||||
|
public ToolAuthorizationService(McpGatewayProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool 실행 직전에 호출 가능 여부를 검사하고, 거부 시 ToolExecutionException을 발생시킵니다.
|
||||||
|
*/
|
||||||
|
public void authorize(McpRequestContext context, ToolMetadata metadata) {
|
||||||
|
if (properties.agentClaimsRequired() && context.allowedTools().isEmpty()) {
|
||||||
|
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Missing agent authorization claims");
|
||||||
|
}
|
||||||
|
if (properties.trustedClaimsRequired() && !context.claimsTrusted()) {
|
||||||
|
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Agent authorization claims are not trusted");
|
||||||
|
}
|
||||||
|
Set<String> serverAllowedTools = properties.allowedToolSet();
|
||||||
|
if (!isAllowed(serverAllowedTools, metadata.getName())) {
|
||||||
|
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Tool is not allowed by server policy: " + metadata.getName());
|
||||||
|
}
|
||||||
|
if (!isAllowed(context.allowedTools(), metadata.getName())) {
|
||||||
|
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Tool is not allowed by agent claims: " + metadata.getName());
|
||||||
|
}
|
||||||
|
if (properties.writeApprovalRequired()
|
||||||
|
&& (metadata.getOperationType() == OperationType.WRITE || (metadata.getRequiresApproval() != null && metadata.getRequiresApproval()))
|
||||||
|
&& !context.writeApproved()) {
|
||||||
|
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Write tool requires approval: " + metadata.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 빈 목록은 제한 없음, "*"는 모든 Tool 허용을 의미합니다.
|
||||||
|
*/
|
||||||
|
private boolean isAllowed(Set<String> allowedTools, String toolName) {
|
||||||
|
return allowedTools.isEmpty() || allowedTools.contains("*") || allowedTools.contains(toolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,82 @@
|
|||||||
package io.shinhanlife.axhub.biz.mcp.gateway.service;
|
package io.shinhanlife.axhub.biz.mcp.gateway.service;
|
||||||
|
|
||||||
import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params;
|
|
||||||
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||||
import java.util.List;
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.RetryPolicy;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.dto.OperationType;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.GuardrailService;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContext;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContextResolver;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.audit.AuditLogService;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.CircuitBreaker;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.CircuitBreakerService;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.security.ToolAuthorizationService;
|
||||||
|
import io.shinhanlife.axhub.biz.mcp.gateway.redis.RedisToolTraceService;
|
||||||
|
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import java.util.concurrent.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ExecuteService {
|
public class ExecuteService {
|
||||||
|
|
||||||
private final ToolPlanner planner;
|
private final ToolPlanner planner;
|
||||||
private final KillSwitchService killSwitchService;
|
private final KillSwitchService killSwitchService;
|
||||||
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker dataMasker;
|
private final SensitiveDataMasker dataMasker;
|
||||||
private final io.shinhanlife.axhub.biz.mcp.gateway.guardrail.GuardrailService guardrailService;
|
private final GuardrailService guardrailService;
|
||||||
private final io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContextResolver contextResolver;
|
private final McpRequestContextResolver contextResolver;
|
||||||
private final io.shinhanlife.axhub.biz.mcp.gateway.audit.AuditLogService auditLogService;
|
private final AuditLogService auditLogService;
|
||||||
private final io.shinhanlife.axhub.biz.mcp.gateway.resilience.CircuitBreakerService circuitBreakerService;
|
private final CircuitBreakerService circuitBreakerService;
|
||||||
|
private final ToolAuthorizationService authorizationService;
|
||||||
|
private final RedisToolTraceService redisTrace;
|
||||||
|
private final McpGatewayProperties properties;
|
||||||
private final RestClient restClient = RestClient.create();
|
private final RestClient restClient = RestClient.create();
|
||||||
|
private final ExecutorService executor;
|
||||||
|
|
||||||
|
public ExecuteService(ToolPlanner planner,
|
||||||
|
KillSwitchService killSwitchService,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
SensitiveDataMasker dataMasker,
|
||||||
|
GuardrailService guardrailService,
|
||||||
|
McpRequestContextResolver contextResolver,
|
||||||
|
AuditLogService auditLogService,
|
||||||
|
CircuitBreakerService circuitBreakerService,
|
||||||
|
ToolAuthorizationService authorizationService,
|
||||||
|
RedisToolTraceService redisTrace,
|
||||||
|
McpGatewayProperties properties) {
|
||||||
|
this.planner = planner;
|
||||||
|
this.killSwitchService = killSwitchService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.dataMasker = dataMasker;
|
||||||
|
this.guardrailService = guardrailService;
|
||||||
|
this.contextResolver = contextResolver;
|
||||||
|
this.auditLogService = auditLogService;
|
||||||
|
this.circuitBreakerService = circuitBreakerService;
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.redisTrace = redisTrace;
|
||||||
|
this.properties = properties;
|
||||||
|
|
||||||
|
this.executor = new ThreadPoolExecutor(
|
||||||
|
properties.toolExecutorCorePoolSize(),
|
||||||
|
properties.toolExecutorMaxPoolSize(),
|
||||||
|
properties.toolExecutorKeepAliveSeconds(),
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new ArrayBlockingQueue<>(properties.toolExecutorQueueCapacity()),
|
||||||
|
new ThreadPoolExecutor.AbortPolicy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public Object execute(Map<String, Object> payload, String tenantId) {
|
public Object execute(Map<String, Object> payload, String tenantId) {
|
||||||
log.info(" [ExecuteService] 전체 실행 흐름 제어 시작");
|
log.info(" [ExecuteService] 전체 실행 흐름 제어 시작");
|
||||||
@@ -36,72 +88,144 @@ public class ExecuteService {
|
|||||||
killSwitchService.checkTool(toolName);
|
killSwitchService.checkTool(toolName);
|
||||||
|
|
||||||
var planObj = planner.createPlan(payload, tenantId);
|
var planObj = planner.createPlan(payload, tenantId);
|
||||||
ToolMetadata plan = (ToolMetadata) planObj;
|
ToolMetadata metadata = (ToolMetadata) planObj;
|
||||||
|
|
||||||
io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContext context = contextResolver.current();
|
McpRequestContext context = contextResolver.current();
|
||||||
|
|
||||||
// --- Guardrail 검증 (Admin UI 및 AI 요청 모두 적용) ---
|
ObjectNode argumentsNode = objectMapper.createObjectNode();
|
||||||
com.fasterxml.jackson.databind.node.ObjectNode argumentsNode = objectMapper.createObjectNode();
|
|
||||||
if (params != null && params.containsKey("arguments")) {
|
if (params != null && params.containsKey("arguments")) {
|
||||||
argumentsNode = objectMapper.valueToTree(params.get("arguments"));
|
argumentsNode = objectMapper.valueToTree(params.get("arguments"));
|
||||||
guardrailService.validate(plan, argumentsNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long startedAt = System.nanoTime();
|
||||||
auditLogService.toolStarted(context, toolName, argumentsNode);
|
auditLogService.toolStarted(context, toolName, argumentsNode);
|
||||||
|
redisTrace.started(context, metadata, argumentsNode);
|
||||||
|
|
||||||
if (plan.getIntegrationType() != null) {
|
try {
|
||||||
killSwitchService.checkRoute(plan.getIntegrationType());
|
authorizationService.authorize(context, metadata);
|
||||||
|
if (params != null && params.containsKey("arguments")) {
|
||||||
|
guardrailService.validate(metadata, argumentsNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
io.shinhanlife.axhub.biz.mcp.gateway.resilience.CircuitBreaker circuitBreaker = circuitBreakerService.breaker("tool:" + toolName, 0, 0);
|
if (metadata.getIntegrationType() != null) {
|
||||||
|
killSwitchService.checkRoute(metadata.getIntegrationType());
|
||||||
|
}
|
||||||
|
|
||||||
|
Object result = executeWithResilience(context, metadata, argumentsNode, payload);
|
||||||
|
long elapsedMillis = elapsedMillis(startedAt);
|
||||||
|
|
||||||
|
String responseText = "";
|
||||||
|
try { responseText = objectMapper.writeValueAsString(result); } catch (Exception ignore) {}
|
||||||
|
|
||||||
|
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
|
||||||
|
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (ToolExecutionException error) {
|
||||||
|
long elapsedMillis = elapsedMillis(startedAt);
|
||||||
|
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
|
||||||
|
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
|
||||||
|
throw error;
|
||||||
|
} catch (Exception error) {
|
||||||
|
long elapsedMillis = elapsedMillis(startedAt);
|
||||||
|
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
|
||||||
|
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
|
||||||
|
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + toolName, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object executeWithResilience(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, Map<String, Object> payload) {
|
||||||
|
RetryPolicy retryPolicy = retryPolicy(metadata, arguments);
|
||||||
|
CircuitBreaker circuitBreaker = circuitBreakerService.breaker("tool:" + metadata.getName(),
|
||||||
|
metadata.getCircuitBreakerFailureThreshold(),
|
||||||
|
metadata.getCircuitBreakerOpenMillis());
|
||||||
|
|
||||||
|
ToolExecutionException lastError = null;
|
||||||
|
for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) {
|
||||||
|
try {
|
||||||
|
redisTrace.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
|
||||||
circuitBreaker.beforeCall();
|
circuitBreaker.beforeCall();
|
||||||
|
Object result = executeOnce(metadata, payload);
|
||||||
// Dynamic Routing: Find the target Pod from the Redis registry
|
circuitBreaker.recordSuccess();
|
||||||
String targetUrl = "http://localhost:8084"; // Fallback (tool-other)
|
return result;
|
||||||
if (plan.getPodUrl() != null && !plan.getPodUrl().isEmpty()) {
|
} catch (ToolExecutionException error) {
|
||||||
targetUrl = plan.getPodUrl();
|
lastError = error;
|
||||||
|
circuitBreaker.recordFailure(error.failureType());
|
||||||
|
if (!shouldRetry(metadata, arguments, error.failureType(), attempt, retryPolicy)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
long backoffMillis = retryPolicy.backoffMillis(attempt);
|
||||||
|
redisTrace.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis,
|
||||||
|
error.failureType().name());
|
||||||
|
sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError == null
|
||||||
|
? new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + metadata.getName())
|
||||||
|
: lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(" [ExecuteService] 라우팅 목적지: {}", targetUrl);
|
private Object executeOnce(ToolMetadata metadata, Map<String, Object> payload) {
|
||||||
|
CompletableFuture<Object> future;
|
||||||
|
|
||||||
|
try {
|
||||||
|
future = CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
String targetUrl = "http://localhost:8084"; // Fallback
|
||||||
|
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
|
||||||
|
targetUrl = metadata.getPodUrl();
|
||||||
|
}
|
||||||
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
|
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
boolean success = false;
|
|
||||||
String errorCode = null;
|
|
||||||
|
|
||||||
// Forward the request to the target tool pod using RestClient with fallback
|
|
||||||
try {
|
|
||||||
try {
|
try {
|
||||||
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(payload))));
|
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(payload))));
|
||||||
} catch (Exception ignore) {}
|
} catch (Exception ignore) {}
|
||||||
|
|
||||||
Object result = executeWithUrl(payload, executeApiUrl);
|
try {
|
||||||
circuitBreaker.recordSuccess();
|
return executeWithUrl(payload, executeApiUrl);
|
||||||
success = true;
|
|
||||||
return result;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
errorCode = io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType.SERVER_ERROR.name();
|
|
||||||
circuitBreaker.recordFailure(io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType.SERVER_ERROR);
|
|
||||||
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 result = executeWithUrl(payload, fallbackUrl);
|
return executeWithUrl(payload, fallbackUrl);
|
||||||
circuitBreaker.recordSuccess();
|
|
||||||
success = true;
|
|
||||||
errorCode = null;
|
|
||||||
return result;
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
circuitBreaker.recordFailure(io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType.SERVER_ERROR);
|
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
|
||||||
log.error(" [ExecuteService] localhost 재시도 실패: {}", ex.getMessage());
|
|
||||||
throw new io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException(io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.error(" [ExecuteService] Tool Pod 호출 실패: {}", e.getMessage());
|
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패: " + e.getMessage());
|
||||||
throw new io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException(io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType.SERVER_ERROR, "Tool Pod 호출 실패: " + e.getMessage());
|
}
|
||||||
} finally {
|
} catch (ToolExecutionException error) {
|
||||||
long elapsedMillis = System.currentTimeMillis() - startTime;
|
throw error;
|
||||||
auditLogService.toolFinished(context, toolName, elapsedMillis, success, errorCode);
|
} catch (Exception error) {
|
||||||
|
throw new ToolExecutionException(
|
||||||
|
FailureType.INTERNAL_ERROR,
|
||||||
|
"Tool execution failed: " + metadata.getName(),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, executor);
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
throw new ToolExecutionException(
|
||||||
|
FailureType.SERVER_ERROR,
|
||||||
|
"MCP Tool 실행 큐가 가득 찼습니다: " + metadata.getName(),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return future.get(timeoutMillis(metadata), TimeUnit.MILLISECONDS);
|
||||||
|
} catch (TimeoutException error) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new ToolExecutionException(FailureType.TIMEOUT, "Tool execution timed out: " + metadata.getName(), error);
|
||||||
|
} catch (ExecutionException error) {
|
||||||
|
if (error.getCause() instanceof ToolExecutionException toolError) {
|
||||||
|
throw toolError;
|
||||||
|
}
|
||||||
|
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + metadata.getName(), error);
|
||||||
|
} catch (InterruptedException error) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool 실행이 중단되었습니다: " + metadata.getName(), error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,4 +239,52 @@ public class ExecuteService {
|
|||||||
.retrieve()
|
.retrieve()
|
||||||
.body(Object.class);
|
.body(Object.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RetryPolicy retryPolicy(ToolMetadata metadata, ObjectNode arguments) {
|
||||||
|
if (!retryAllowedByOperation(metadata, arguments)) {
|
||||||
|
return RetryPolicy.disabled();
|
||||||
|
}
|
||||||
|
return new RetryPolicy(properties.retryMaxAttempts(), properties.retryInitialBackoffMillis(),
|
||||||
|
properties.retryBackoffMultiplier(), properties.retryMaxBackoffMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean retryAllowedByOperation(ToolMetadata metadata, ObjectNode arguments) {
|
||||||
|
if (metadata.getOperationType() == OperationType.READ) {
|
||||||
|
return metadata.getRetryEnabled() != null ? metadata.getRetryEnabled() : true;
|
||||||
|
}
|
||||||
|
return arguments.hasNonNull("idempotencyKey") && !arguments.path("idempotencyKey").asText("").isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldRetry(ToolMetadata metadata, ObjectNode arguments, FailureType failureType, int attempt, RetryPolicy retryPolicy) {
|
||||||
|
if (attempt >= retryPolicy.maxAttempts() || !retryAllowedByOperation(metadata, arguments)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return failureType == FailureType.TIMEOUT || failureType == FailureType.NETWORK_ERROR || failureType == FailureType.SERVER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleepBeforeRetry(String toolName, int attempt, long backoffMillis, FailureType failureType) {
|
||||||
|
if (backoffMillis <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.warn("Retrying tool call. tool={} failedAttempt={} failureType={} backoffMillis={}", toolName, attempt, failureType, backoffMillis);
|
||||||
|
try {
|
||||||
|
Thread.sleep(backoffMillis);
|
||||||
|
} catch (InterruptedException error) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool 재시도가 중단되었습니다: " + toolName, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long timeoutMillis(ToolMetadata metadata) {
|
||||||
|
return (metadata.getTimeoutMillis() != null && metadata.getTimeoutMillis() > 0) ? metadata.getTimeoutMillis() : properties.toolTimeoutMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long elapsedMillis(long startedAt) {
|
||||||
|
return (System.nanoTime() - startedAt) / 1_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
32
fix.py
Normal file
32
fix.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# Fix ToolAuthorizationService
|
||||||
|
auth_path = r"C:\eGovFrameDev-4.3.1-64bit\workspace-egov\axhub-backend-main\axhub-gateway\src\main\java\io\shinhanlife\axhub\biz\mcp\gateway\security\ToolAuthorizationService.java"
|
||||||
|
with open(auth_path, "r", encoding="utf-8") as f:
|
||||||
|
auth_content = f.read()
|
||||||
|
|
||||||
|
auth_content = auth_content.replace("metadata.name()", "metadata.getName()")
|
||||||
|
auth_content = auth_content.replace("metadata.operationType()", "metadata.getOperationType()")
|
||||||
|
auth_content = auth_content.replace("metadata.requiresApproval()", "(metadata.getRequiresApproval() != null && metadata.getRequiresApproval())")
|
||||||
|
|
||||||
|
with open(auth_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(auth_content)
|
||||||
|
|
||||||
|
# Fix RedisToolTraceService
|
||||||
|
redis_path = r"C:\eGovFrameDev-4.3.1-64bit\workspace-egov\axhub-backend-main\axhub-gateway\src\main\java\io\shinhanlife\axhub\biz\mcp\gateway\redis\RedisToolTraceService.java"
|
||||||
|
with open(redis_path, "r", encoding="utf-8") as f:
|
||||||
|
redis_content = f.read()
|
||||||
|
|
||||||
|
redis_content = redis_content.replace("metadata.name()", "metadata.getName()")
|
||||||
|
redis_content = redis_content.replace("metadata.operationType()", "metadata.getOperationType()")
|
||||||
|
|
||||||
|
arg_names_old = 'trace.put("argumentNames", arguments.propertyNames());'
|
||||||
|
arg_names_new = ''' java.util.List<String> argNames = new java.util.ArrayList<>();
|
||||||
|
arguments.fieldNames().forEachRemaining(argNames::add);
|
||||||
|
trace.put("argumentNames", argNames);'''
|
||||||
|
redis_content = redis_content.replace(arg_names_old, arg_names_new)
|
||||||
|
|
||||||
|
with open(redis_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(redis_content)
|
||||||
|
|
||||||
|
print("Files fixed!")
|
||||||
13
payload.json
Normal file
13
payload.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "other_register_vacation",
|
||||||
|
"arguments": {
|
||||||
|
"date": "2026-07-15",
|
||||||
|
"employeeId": "010-9999-1111",
|
||||||
|
"unknownData": "이건 차단되어야 해"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
test.py
Normal file
24
test.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
|
||||||
|
url = 'http://localhost:8281/mcp'
|
||||||
|
data = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "other_register_vacation",
|
||||||
|
"arguments": {
|
||||||
|
"date": "2026-07-15",
|
||||||
|
"employeeId": "010-9999-1111",
|
||||||
|
"unknownData": "이건 차단되어야 해"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers={'Content-Type': 'application/json'})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as response:
|
||||||
|
print(response.read().decode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
Reference in New Issue
Block a user