refactor: remove redis and switch to in-memory, rollback incomplete Spring AI 2.0 upgrade
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 4m31s

This commit is contained in:
jade
2026-08-13 16:37:30 +09:00
parent 1cb1d90a7e
commit f1ca0d6df1
21 changed files with 1243 additions and 531 deletions

View File

@@ -59,7 +59,7 @@ jobs:
# 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어! # 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어!
cd /app cd /app
docker system prune -f docker system prune -f
ACTIVE_PROFILE=dev docker compose up -d --build --pull always --remove-orphans gateway redis mci-mock dozzle was-sms was-oth ACTIVE_PROFILE=dev docker compose up -d --build --pull always --remove-orphans gateway mci-mock dozzle was-sms was-oth
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 및 빌드 캐시 자동 소거 청소! # 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 및 빌드 캐시 자동 소거 청소!
docker image prune -a -f docker image prune -a -f

View File

@@ -13,11 +13,9 @@ dependencies {
// 등록된 Excel 양식을 보존하면서 Tool 문서를 생성합니다. // 등록된 Excel 양식을 보존하면서 Tool 문서를 생성합니다.
implementation 'org.apache.poi:poi-ooxml:5.5.1' implementation 'org.apache.poi:poi-ooxml:5.5.1'
// Tool Registry 및 분산 캐시 연동에 사용합니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Tool/Registry 관련 DB 조회와 MyBatis Mapper 실행에 사용합니다. // Tool/Registry 관련 DB 조회와 MyBatis Mapper 실행에 사용합니다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
// Gateway가 /mcp Endpoint를 MCP Server로 노출하도록 지원합니다. // Gateway가 /mcp Endpoint를 MCP Server로 노출하도록 지원합니다.

View File

@@ -37,8 +37,8 @@ public class McpGatewayProperties {
private Boolean agentClaimsRequired; private Boolean agentClaimsRequired;
private Boolean trustedClaimsRequired; private Boolean trustedClaimsRequired;
private Boolean writeApprovalRequired; private Boolean writeApprovalRequired;
private Boolean redisTraceEnabled; private Boolean traceEnabled;
private Long redisTraceTtlSeconds; private Long traceRetentionSize;
private Long toolTimeoutMillis; private Long toolTimeoutMillis;
private Integer retryMaxAttempts; private Integer retryMaxAttempts;
private Long retryInitialBackoffMillis; private Long retryInitialBackoffMillis;
@@ -70,8 +70,8 @@ public class McpGatewayProperties {
public void setAgentClaimsRequired(Boolean agentClaimsRequired) { this.agentClaimsRequired = agentClaimsRequired; } public void setAgentClaimsRequired(Boolean agentClaimsRequired) { this.agentClaimsRequired = agentClaimsRequired; }
public void setTrustedClaimsRequired(Boolean trustedClaimsRequired) { this.trustedClaimsRequired = trustedClaimsRequired; } public void setTrustedClaimsRequired(Boolean trustedClaimsRequired) { this.trustedClaimsRequired = trustedClaimsRequired; }
public void setWriteApprovalRequired(Boolean writeApprovalRequired) { this.writeApprovalRequired = writeApprovalRequired; } public void setWriteApprovalRequired(Boolean writeApprovalRequired) { this.writeApprovalRequired = writeApprovalRequired; }
public void setRedisTraceEnabled(Boolean redisTraceEnabled) { this.redisTraceEnabled = redisTraceEnabled; } public void setTraceEnabled(Boolean traceEnabled) { this.traceEnabled = traceEnabled; }
public void setRedisTraceTtlSeconds(Long redisTraceTtlSeconds) { this.redisTraceTtlSeconds = redisTraceTtlSeconds; } public void setTraceRetentionSize(Long traceRetentionSize) { this.traceRetentionSize = traceRetentionSize; }
public void setToolTimeoutMillis(Long toolTimeoutMillis) { this.toolTimeoutMillis = toolTimeoutMillis; } public void setToolTimeoutMillis(Long toolTimeoutMillis) { this.toolTimeoutMillis = toolTimeoutMillis; }
public void setRetryMaxAttempts(Integer retryMaxAttempts) { this.retryMaxAttempts = retryMaxAttempts; } public void setRetryMaxAttempts(Integer retryMaxAttempts) { this.retryMaxAttempts = retryMaxAttempts; }
public void setRetryInitialBackoffMillis(Long retryInitialBackoffMillis) { this.retryInitialBackoffMillis = retryInitialBackoffMillis; } public void setRetryInitialBackoffMillis(Long retryInitialBackoffMillis) { this.retryInitialBackoffMillis = retryInitialBackoffMillis; }
@@ -103,8 +103,8 @@ public class McpGatewayProperties {
public boolean agentClaimsRequired() { return agentClaimsRequired == null || agentClaimsRequired; } public boolean agentClaimsRequired() { return agentClaimsRequired == null || agentClaimsRequired; }
public boolean trustedClaimsRequired() { return trustedClaimsRequired == null || trustedClaimsRequired; } public boolean trustedClaimsRequired() { return trustedClaimsRequired == null || trustedClaimsRequired; }
public boolean writeApprovalRequired() { return writeApprovalRequired == null || writeApprovalRequired; } public boolean writeApprovalRequired() { return writeApprovalRequired == null || writeApprovalRequired; }
public boolean redisTraceEnabled() { return redisTraceEnabled == null || redisTraceEnabled; } public boolean traceEnabled() { return traceEnabled == null || traceEnabled; }
public long redisTraceTtlSeconds() { return redisTraceTtlSeconds == null || redisTraceTtlSeconds < 1 ? 3_600 : redisTraceTtlSeconds; } public long traceRetentionSize() { return traceRetentionSize == null || traceRetentionSize < 1 ? 1000 : traceRetentionSize; }
public long toolTimeoutMillis() { return toolTimeoutMillis == null || toolTimeoutMillis < 1 ? 5_000 : toolTimeoutMillis; } public long toolTimeoutMillis() { return toolTimeoutMillis == null || toolTimeoutMillis < 1 ? 5_000 : toolTimeoutMillis; }
public int retryMaxAttempts() { return retryMaxAttempts == null || retryMaxAttempts < 1 ? 3 : retryMaxAttempts; } public int retryMaxAttempts() { return retryMaxAttempts == null || retryMaxAttempts < 1 ? 3 : retryMaxAttempts; }
public long retryInitialBackoffMillis() { return retryInitialBackoffMillis == null || retryInitialBackoffMillis < 1 ? 1_000 : retryInitialBackoffMillis; } public long retryInitialBackoffMillis() { return retryInitialBackoffMillis == null || retryInitialBackoffMillis < 1 ? 1_000 : retryInitialBackoffMillis; }

View File

@@ -1,46 +0,0 @@
package io.shinhanlife.dap.mcg.config;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @package io.shinhanlife.dap.mcg.config
* @className RedisConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, ToolMetadata> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, ToolMetadata> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 1. Key는 무조건 String
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 2. Value는 Generic 직렬화기 사용 (생성자 인자 없음)
Jackson2JsonRedisSerializer<ToolMetadata> serializer = new Jackson2JsonRedisSerializer<>(ToolMetadata.class);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.presentation;
*/ */
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse; import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService; import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -41,7 +41,7 @@ public class ChatController {
private final ExecuteService executeService; private final ExecuteService executeService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final RedisRegistryService registryService; private final InMemoryRegistryService registryService;
private final ChatClient.Builder chatClientBuilder; private final ChatClient.Builder chatClientBuilder;
private final McpRouterController mcpRouterController; private final McpRouterController mcpRouterController;

View File

@@ -21,7 +21,7 @@ import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.adapter.dto.Params; import io.shinhanlife.dap.lib.adapter.dto.Params;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService; import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties; import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -49,19 +49,19 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
@Tag(name = "MCP Router API", description = "AI Agent의 요청을 받아 Adapter 시스템으로 라우팅하는 게이트웨이 API") @Tag(name = "MCP Router API", description = "AI Agent의 요청을 받아 Adapter 시스템으로 라우팅하는 게이트웨이 API")
public class McpRouterController { public class McpRouterController {
private final RedisRegistryService redisRegistryService; private final InMemoryRegistryService registryService;
private final ExecuteService executeService; private final ExecuteService executeService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final GatewayFallbackProperties gatewayFallbackProperties; private final GatewayFallbackProperties gatewayFallbackProperties;
private final RestClient restClient; private final RestClient restClient;
public McpRouterController(RedisRegistryService redisRegistryService, public McpRouterController(InMemoryRegistryService registryService,
ExecuteService executeService, ExecuteService executeService,
SecurityProperties securityProperties, SecurityProperties securityProperties,
ObjectMapper objectMapper, ObjectMapper objectMapper,
GatewayFallbackProperties gatewayFallbackProperties) { GatewayFallbackProperties gatewayFallbackProperties) {
this.redisRegistryService = redisRegistryService; this.registryService = registryService;
this.executeService = executeService; this.executeService = executeService;
this.securityProperties = securityProperties; this.securityProperties = securityProperties;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
@@ -115,12 +115,12 @@ public class McpRouterController {
private List<ToolMetadata> fetchAllActiveTools() { private List<ToolMetadata> fetchAllActiveTools() {
List<ToolMetadata> activeTools = new ArrayList<>(); List<ToolMetadata> activeTools = new ArrayList<>();
try { try {
activeTools.addAll(redisRegistryService.getAllTools() activeTools.addAll(registryService.getAllTools()
.stream() .stream()
.filter(ToolMetadata::getVisible) .filter(ToolMetadata::getVisible)
.collect(Collectors.toList())); .collect(Collectors.toList()));
} catch (org.springframework.data.redis.RedisConnectionFailureException exception) { } catch (Exception exception) {
log.warn("Redis is unavailable. Fetching tools from configured fallback Tool Pods instead."); log.warn("Registry is unavailable. Fetching tools from configured fallback Tool Pods instead.");
} }
Set<String> knownTools = activeTools.stream() Set<String> knownTools = activeTools.stream()
@@ -240,19 +240,19 @@ public class McpRouterController {
@PostMapping("/registry/register") @PostMapping("/registry/register")
public ResponseEntity<String> registerTool(@RequestBody ToolMetadata meta) { public ResponseEntity<String> registerTool(@RequestBody ToolMetadata meta) {
redisRegistryService.saveTool(meta); registryService.saveTool(meta);
return ResponseEntity.ok("Registered"); return ResponseEntity.ok("Registered");
} }
@PostMapping("/registry/deregister") @PostMapping("/registry/deregister")
public ResponseEntity<String> deregisterTool(@RequestBody String uid) { public ResponseEntity<String> deregisterTool(@RequestBody String uid) {
redisRegistryService.removeTool(uid); registryService.removeTool(uid);
return ResponseEntity.ok("Deregistered"); return ResponseEntity.ok("Deregistered");
} }
@PostMapping("/registry/heartbeat") @PostMapping("/registry/heartbeat")
public ResponseEntity<String> heartbeat(@RequestBody String uid) { public ResponseEntity<String> heartbeat(@RequestBody String uid) {
boolean success = redisRegistryService.refreshHeartbeat(uid); boolean success = registryService.refreshHeartbeat(uid);
if (success) { if (success) {
return ResponseEntity.ok("Heartbeat updated"); return ResponseEntity.ok("Heartbeat updated");
} else { } else {

View File

@@ -1,264 +0,0 @@
package io.shinhanlife.dap.mcg.redis;
/**
* @package io.shinhanlife.dap.mcg.redis
* @className RedisToolTraceService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcc.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.ArrayList;
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 Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
public RedisToolTraceService(McpGatewayProperties properties,
ObjectProvider<StringRedisTemplate> redisProvider,
ObjectMapper json,
McpMonitorEventService monitorEvents) {
this.properties = properties;
this.redisProvider = redisProvider;
this.json = json;
this.monitorEvents = monitorEvents;
}
/**
* 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("")));
List<String> argNames = new ArrayList<>();
arguments.fieldNames().forEachRemaining(argNames::add);
trace.put("argumentNames", argNames);
trace.put("arguments", 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) {
}
}

View File

@@ -0,0 +1,111 @@
package io.shinhanlife.dap.mcg.registry;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.dap.mcg.registry
* @className InMemoryRegistryService
* @description AX HUB 인메모리 툴 레지스트리
* @author 0986406
* @create 2026.09.01
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class InMemoryRegistryService {
private final Map<String, ToolMetadata> toolCache = new ConcurrentHashMap<>();
private static final long DEFAULT_TTL_MILLIS = 45000; // 45초
/**
* 툴 등록 및 갱신
*/
public void saveTool(ToolMetadata meta) {
meta.setLastHeartbeat(System.currentTimeMillis());
toolCache.put(meta.getUid(), meta);
log.info(" [InMemoryRegistry] 툴 등록 완료: {}", meta.getUid());
}
/**
* 하트비트 갱신 (TTL 초기화)
*/
public boolean refreshHeartbeat(String uid) {
ToolMetadata meta = toolCache.get(uid);
if (meta != null) {
meta.setLastHeartbeat(System.currentTimeMillis());
log.debug(" [InMemoryRegistry] 하트비트 갱신: {}", uid);
return true;
} else {
log.warn(" [InMemoryRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
return false;
}
}
public List<String> getAvailablePods(String uid) {
ToolMetadata tool = getTool(uid);
if (tool != null && tool.getPodUrl() != null) {
return List.of(tool.getPodUrl());
}
return List.of();
}
/**
* 실행 시 툴 정보 조회
*/
public ToolMetadata getTool(String uid) {
return toolCache.get(uid);
}
/**
* 툴 이름으로 정보 조회
*/
public ToolMetadata getToolByName(String name) {
for (ToolMetadata tool : toolCache.values()) {
if (name.equals(tool.getName())) {
return tool;
}
}
return null;
}
/**
* 등록된 모든 활성 툴 목록 조회
*/
public List<ToolMetadata> getAllTools() {
return List.copyOf(toolCache.values());
}
/**
* 툴 명시적 제거 (Deregister)
*/
public void removeTool(String uid) {
toolCache.remove(uid);
log.info(" [InMemoryRegistry] 툴 삭제 완료: {}", uid);
}
/**
* 만료된 툴을 스케줄러로 삭제합니다. (10초마다 실행)
*/
@Scheduled(fixedRate = 10000)
public void evictExpiredTools() {
long now = System.currentTimeMillis();
List<String> expiredKeys = toolCache.entrySet().stream()
.filter(entry -> (now - entry.getValue().getLastHeartbeat()) > DEFAULT_TTL_MILLIS)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
for (String key : expiredKeys) {
toolCache.remove(key);
log.info(" [InMemoryRegistry] TTL 초과로 툴 자동 삭제: {}", key);
}
}
}

View File

@@ -1,110 +0,0 @@
package io.shinhanlife.dap.mcg.registry;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.Objects;
/**
* @package io.shinhanlife.dap.mcg.registry
* @className RedisRegistryService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RedisRegistryService {
private final RedisTemplate<String, ToolMetadata> redisTemplate;
private static final String KEY_PREFIX = "mcp:tool:";
private static final Duration DEFAULT_TTL = Duration.ofSeconds(45); // 실무 하트비트 주기 반영
/**
* 툴 등록 및 갱신 (TTL 기반으로 60초 뒤 자동 만료)
*/
public void saveTool(ToolMetadata meta) {
String key = KEY_PREFIX + meta.getUid();
meta.setLastHeartbeat(System.currentTimeMillis());
redisTemplate.opsForValue().set(key, meta, DEFAULT_TTL);
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getUid());
}
/**
* 하트비트 갱신 (TTL 초기화)
*/
public boolean refreshHeartbeat(String uid) {
String key = KEY_PREFIX + uid;
Boolean exists = redisTemplate.expire(key, DEFAULT_TTL);
if (Boolean.TRUE.equals(exists)) {
log.debug(" [RedisRegistry] 하트비트 갱신: {}", uid);
return true;
} else {
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
return false;
}
}
public List<String> getAvailablePods(String uid) {
ToolMetadata tool = getTool(uid);
if (tool != null && tool.getPodUrl() != null) {
return List.of(tool.getPodUrl());
}
return List.of();
}
/**
* 실행 시 툴 정보 조회 (Tool Execution 시 참조)
*/
public ToolMetadata getTool(String uid) {
return redisTemplate.opsForValue().get(KEY_PREFIX + uid);
}
/**
* 툴 이름으로 정보 조회
*/
public ToolMetadata getToolByName(String name) {
List<ToolMetadata> allTools = getAllTools();
for (ToolMetadata tool : allTools) {
if (name.equals(tool.getName())) {
return tool;
}
}
return null;
}
/**
* 등록된 모든 활성 툴 목록 조회
*/
public List<ToolMetadata> getAllTools() {
Set<String> keys = redisTemplate.keys(KEY_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return List.of();
}
List<ToolMetadata> tools = redisTemplate.opsForValue().multiGet(keys);
return tools != null ? tools.stream().filter(Objects::nonNull).collect(Collectors.toList()) : List.of();
}
/**
* 툴 명시적 제거 (Deregister)
*/
public void removeTool(String uid) {
redisTemplate.delete(KEY_PREFIX + uid);
log.info(" [RedisRegistry] 툴 삭제 완료: {}", uid);
}
}

View File

@@ -30,7 +30,7 @@ import io.shinhanlife.dap.mcg.audit.AuditLogService;
import io.shinhanlife.dap.mcg.resilience.CircuitBreaker; import io.shinhanlife.dap.mcg.resilience.CircuitBreaker;
import io.shinhanlife.dap.mcg.resilience.CircuitBreakerService; import io.shinhanlife.dap.mcg.resilience.CircuitBreakerService;
import io.shinhanlife.dap.mcg.security.ToolAuthorizationService; import io.shinhanlife.dap.mcg.security.ToolAuthorizationService;
import io.shinhanlife.dap.mcg.redis.RedisToolTraceService; import io.shinhanlife.dap.mcg.trace.InMemoryToolTraceService;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import java.util.Map; import java.util.Map;
@@ -62,7 +62,7 @@ public class ExecuteService {
private final AuditLogService auditLogService; private final AuditLogService auditLogService;
private final CircuitBreakerService circuitBreakerService; private final CircuitBreakerService circuitBreakerService;
private final ToolAuthorizationService authorizationService; private final ToolAuthorizationService authorizationService;
private final RedisToolTraceService redisTrace; private final InMemoryToolTraceService traceService;
private final McpGatewayProperties properties; private final McpGatewayProperties properties;
private final LargeToolResponseService largeResponses; private final LargeToolResponseService largeResponses;
private final PaginationRequestValidator paginationValidator; private final PaginationRequestValidator paginationValidator;
@@ -79,7 +79,7 @@ public class ExecuteService {
AuditLogService auditLogService, AuditLogService auditLogService,
CircuitBreakerService circuitBreakerService, CircuitBreakerService circuitBreakerService,
ToolAuthorizationService authorizationService, ToolAuthorizationService authorizationService,
RedisToolTraceService redisTrace, InMemoryToolTraceService traceService,
McpGatewayProperties properties, McpGatewayProperties properties,
LargeToolResponseService largeResponses, LargeToolResponseService largeResponses,
PaginationRequestValidator paginationValidator, PaginationRequestValidator paginationValidator,
@@ -94,7 +94,7 @@ public class ExecuteService {
this.auditLogService = auditLogService; this.auditLogService = auditLogService;
this.circuitBreakerService = circuitBreakerService; this.circuitBreakerService = circuitBreakerService;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.redisTrace = redisTrace; this.traceService = traceService;
this.properties = properties; this.properties = properties;
this.largeResponses = largeResponses; this.largeResponses = largeResponses;
this.paginationValidator = paginationValidator; this.paginationValidator = paginationValidator;
@@ -133,7 +133,7 @@ public class ExecuteService {
long startedAt = System.nanoTime(); long startedAt = System.nanoTime();
auditLogService.toolStarted(context, toolName, argumentsNode); auditLogService.toolStarted(context, toolName, argumentsNode);
redisTrace.started(context, metadata, argumentsNode); traceService.started(context, metadata, argumentsNode);
try { try {
authorizationService.authorize(context, metadata); authorizationService.authorize(context, metadata);
@@ -174,13 +174,13 @@ public class ExecuteService {
finalResult.put("original_size", originalSize); finalResult.put("original_size", originalSize);
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, ""); auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText); traceService.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return finalResult; return finalResult;
} catch (ToolExecutionException error) { } catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), ""); traceService.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
Map<String, Object> errorResult = new java.util.LinkedHashMap<>(); Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error"); errorResult.put("status", "error");
@@ -194,7 +194,7 @@ public class ExecuteService {
} catch (Exception error) { } catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), ""); traceService.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
Map<String, Object> errorResult = new java.util.LinkedHashMap<>(); Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error"); errorResult.put("status", "error");
@@ -225,7 +225,7 @@ public class ExecuteService {
ToolExecutionException lastError = null; ToolExecutionException lastError = null;
for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) { for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) {
try { try {
redisTrace.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts()); traceService.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
circuitBreaker.beforeCall(); circuitBreaker.beforeCall();
Object result = executeOnce(context, metadata, arguments, payload); Object result = executeOnce(context, metadata, arguments, payload);
circuitBreaker.recordSuccess(); circuitBreaker.recordSuccess();
@@ -237,7 +237,7 @@ public class ExecuteService {
throw error; throw error;
} }
long backoffMillis = retryPolicy.backoffMillis(attempt); long backoffMillis = retryPolicy.backoffMillis(attempt);
redisTrace.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis, traceService.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis,
error.failureType().name()); error.failureType().name());
sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType()); sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType());
} }

View File

@@ -2,34 +2,29 @@ package io.shinhanlife.dap.mcg.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @package io.shinhanlife.dap.mcg.service * @package io.shinhanlife.dap.mcg.service
* @className KillSwitchService * @className KillSwitchService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 킬 스위치 서비스 (인메모리)
* @author 0986406 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class KillSwitchService { public class KillSwitchService {
// 기본 제공되는 StringRedisTemplate 사용 // 인메모리 맵으로 변경
private final StringRedisTemplate stringRedisTemplate; private final Map<String, String> killSwitchMap = new ConcurrentHashMap<>();
public void checkAgent(String tenantId) { public void checkAgent(String tenantId) {
String key = "mcp:kill:agent:" + tenantId; String key = "mcp:kill:agent:" + tenantId;
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) { if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
log.warn(" [KillSwitch] 차단된 에이전트 접근 시도: {}", tenantId); log.warn(" [KillSwitch] 차단된 에이전트 접근 시도: {}", tenantId);
throw new SecurityException(" 비상 차단: 해당 테넌트(" + tenantId + ")의 접근이 관리자에 의해 차단되었습니다."); throw new SecurityException(" 비상 차단: 해당 테넌트(" + tenantId + ")의 접근이 관리자에 의해 차단되었습니다.");
} }
@@ -39,7 +34,7 @@ public class KillSwitchService {
if (toolName == null || toolName.isBlank()) return; if (toolName == null || toolName.isBlank()) return;
String key = "mcp:kill:tool:" + toolName; String key = "mcp:kill:tool:" + toolName;
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) { if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
log.warn(" [KillSwitch] 차단된 툴 실행 시도: {}", toolName); log.warn(" [KillSwitch] 차단된 툴 실행 시도: {}", toolName);
throw new SecurityException(" 비상 차단: 해당 툴(" + toolName + ")의 실행이 관리자에 의해 차단되었습니다."); throw new SecurityException(" 비상 차단: 해당 툴(" + toolName + ")의 실행이 관리자에 의해 차단되었습니다.");
} }
@@ -49,7 +44,7 @@ public class KillSwitchService {
if (integrationType == null || integrationType.isBlank()) return; if (integrationType == null || integrationType.isBlank()) return;
String key = "mcp:kill:route:" + integrationType; String key = "mcp:kill:route:" + integrationType;
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) { if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
log.warn(" [KillSwitch] 차단된 라우트 통신 시도: {}", integrationType); log.warn(" [KillSwitch] 차단된 라우트 통신 시도: {}", integrationType);
throw new SecurityException(" 비상 차단: 해당 레거시 라우트(" + integrationType + ")로의 통신이 관리자에 의해 차단되었습니다."); throw new SecurityException(" 비상 차단: 해당 레거시 라우트(" + integrationType + ")로의 통신이 관리자에 의해 차단되었습니다.");
} }
@@ -58,14 +53,14 @@ public class KillSwitchService {
// 관리자 API용 토글 메서드 // 관리자 API용 토글 메서드
public void toggleKillSwitch(String type, String target, boolean state) { public void toggleKillSwitch(String type, String target, boolean state) {
String key = "mcp:kill:" + type + ":" + target; String key = "mcp:kill:" + type + ":" + target;
stringRedisTemplate.opsForValue().set(key, String.valueOf(state)); killSwitchMap.put(key, String.valueOf(state));
log.info(" [KillSwitch] 상태 변경: {} -> {} (차단 상태: {})", type, target, state); log.info(" [KillSwitch] 상태 변경: {} -> {} (차단 상태: {})", type, target, state);
} }
// 관리자 API용 삭제 메서드 (Redis 키 자체를 완전 삭제) // 관리자 API용 삭제 메서드 (맵 항목 삭제)
public void removeKillSwitch(String type, String target) { public void removeKillSwitch(String type, String target) {
String key = "mcp:kill:" + type + ":" + target; String key = "mcp:kill:" + type + ":" + target;
stringRedisTemplate.delete(key); killSwitchMap.remove(key);
log.info(" [KillSwitch] 차단 키 완전 삭제: {} -> {}", type, target); log.info(" [KillSwitch] 차단 키 완전 삭제: {} -> {}", type, target);
} }
} }

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcg.service; package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties; import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
import io.shinhanlife.dap.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -14,14 +14,14 @@ import java.util.Map;
/** /**
* @package io.shinhanlife.dap.mcg.service * @package io.shinhanlife.dap.mcg.service
* @className ToolPlanner * @className ToolPlanner
* @description AX HUB 시스템 처리 클래스 * @description AX HUB ?<3F>스??처리 ?<3F>래??
* @author 0986406 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정력 ---------- * ---------- 개정?<3F>력 ----------
* 수정일 수정자 수정내 * ?<3F>정?? ?<3F>정?? ?<3F>정?<3F>
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 0986406 최초 * 2026.09.01 0986406 최초?<3F>
* *
* </pre> * </pre>
*/ */
@@ -30,30 +30,30 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ToolPlanner { public class ToolPlanner {
private final RedisRegistryService redisRegistryService; private final InMemoryRegistryService InMemoryRegistryService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final GatewayFallbackProperties fallbackProperties; private final GatewayFallbackProperties fallbackProperties;
/** /**
* 청(payload)을 분석하여 실행해야 할 Tool의 계획을 생성합니다. * ?<3F>청(payload)??분석?<3F>여 ?<3F>행?<3F>야 ??Tool??계획???<3F>성?<3F>니??
*/ */
public Object createPlan(Map<String, Object> payload, String tenantId) { public Object createPlan(Map<String, Object> payload, String tenantId) {
log.info(" [Planner] 청 분석 및 실행 계획 수립 시"); log.info(" [Planner] ?<3F>청 분석 <EFBFBD>??<3F>행 계획 ?<3F>립 ?<3F>");
// 1. 요청에서 호출하려는 툴 이름 추출 (JSON-RPC params.name) // 1. ?<3F>청?<3F>서 ?<3F>출?<3F>려?????<3F>름 추출 (JSON-RPC params.name)
Map<String, Object> params = (Map<String, Object>) payload.get("params"); Map<String, Object> params = (Map<String, Object>) payload.get("params");
String toolName = params != null ? (String) params.get("name") : null; String toolName = params != null ? (String) params.get("name") : null;
if (toolName == null || toolName.isEmpty()) { if (toolName == null || toolName.isEmpty()) {
throw new IllegalArgumentException("요청에 toolName이 포함되어 있지 않습니다."); throw new IllegalArgumentException("?<3F>청??toolName???<3F>함?<3F>어 ?<3F><>? ?<3F>습?<3F>다.");
} }
// 2. RedisRegistry에서 해당 툴의 메타데이터 조회 // 2. RedisRegistry?<3F>서 ?<3F>당 ?<3F>의 메<>??<3F>이??조회
// (제로는 이 메타데이터가 실행 계획의 핵심이 됩니다) // (?<3F>제로는 ??메<>??<3F>이?<3F><>? ?<3F>행 계획???<3F>심???<3F>니??
var toolMetadata = redisRegistryService.getToolByName(toolName); var toolMetadata = InMemoryRegistryService.getToolByName(toolName);
if (toolMetadata == null) { if (toolMetadata == null) {
log.warn(" [Planner] 등록되지 않은 툴 요청: {}. Fallback 라우팅 규칙을 확인합니다.", toolName); log.warn(" [Planner] ?<3F>록?<3F><>? ?<3F><>? ???<3F>청: {}. Fallback ?<3F>우??규칙???<3F>인?<3F>니??", toolName);
String fallbackPodUrl = fallbackProperties.getDefaultUrl(); String fallbackPodUrl = fallbackProperties.getDefaultUrl();
if (fallbackProperties.getRoutes() != null) { if (fallbackProperties.getRoutes() != null) {
@@ -66,11 +66,11 @@ public class ToolPlanner {
} }
if (fallbackPodUrl == null || fallbackPodUrl.isEmpty()) { if (fallbackPodUrl == null || fallbackPodUrl.isEmpty()) {
log.error(" [Planner] Fallback 라우팅 대상이 아닙니다. 툴: {}", toolName); log.error(" [Planner] Fallback ?<3F>우???<3F>?<3F>이 ?<3F>닙?<3F>다. ?? {}", toolName);
throw new RuntimeException("해당 툴(" + toolName + ")이 레지스트리에 존재하지 않습니다."); throw new RuntimeException("?<3F>당 ??" + toolName + ")???<3F><>??<3F>트리에 존재?<3F><>? ?<3F>습?<3F>다.");
} }
log.info(" [Planner] Fallback 라우팅 매칭됨: {} -> {}", toolName, fallbackPodUrl); log.info(" [Planner] Fallback ?<3F>우??매칭?? {} -> {}", toolName, fallbackPodUrl);
toolMetadata = ToolMetadata.builder() toolMetadata = ToolMetadata.builder()
.uid(toolName) .uid(toolName)
@@ -80,26 +80,26 @@ public class ToolPlanner {
.build(); .build();
} }
// 2-1. [규] 도메인 그룹핑 기반 권한 검 // 2-1. [?<3F>규] ?<3F>메??그룹??기반 권한 검<EFBFBD>?
if (tenantId != null && !tenantId.equalsIgnoreCase("system") && toolMetadata.getCategoryKey() != null) { if (tenantId != null && !tenantId.equalsIgnoreCase("system") && toolMetadata.getCategoryKey() != null) {
String normalizedTenantId = tenantId.toLowerCase(); String normalizedTenantId = tenantId.toLowerCase();
List<String> allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId); List<String> allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId);
// 만약 대소문자 변환 후에도 없으면 원래 값으로 한 번 더 시도 (하위 호환성) // 만약 ?<3F>?<3F>문??변???<3F>에???<3F><EFBFBD>??<3F>래 값으<EAB092>???<3F>????<3F>도 (?<3F>위 ?<3F>환??
if (allowedDomains == null) { if (allowedDomains == null) {
allowedDomains = securityProperties.getTenantDomains().get(tenantId); allowedDomains = securityProperties.getTenantDomains().get(tenantId);
} }
if (allowedDomains == null || if (allowedDomains == null ||
(!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getCategoryKey()))) { (!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getCategoryKey()))) {
log.warn(" [Planner] 권한 거 - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getCategoryKey()); log.warn(" [Planner] 권한 거<EFBFBD>? - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getCategoryKey());
throw new SecurityException("해당 도메인(" + toolMetadata.getCategoryKey() + ")의 툴을 실행할 권한이 없습니다."); throw new SecurityException("?<3F>당 ?<3F>메??" + toolMetadata.getCategoryKey() + ")???<3F>을 ?<3F>행??권한???<3F>습?<3F>다.");
} }
} }
log.info(" [Planner] '{}'에 대한 실행 계획 수립 완", toolName); log.info(" [Planner] ??'{}'???<3F>???<3F>행 계획 ?<3F>립 ?<3F>", toolName);
// 3. 수립된 계획(툴 메타데이터) 반환 // 3. ?<3F>립??계획(??메<>??<3F>이?? 반환
return toolMetadata; return toolMetadata;
} }
} }

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcg.sync; package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.dap.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.InMemoryRegistryService;
import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -38,18 +38,18 @@ public class RegistryMcpToolSynchronizer {
private final ObjectProvider<McpSyncServer> mcpServerProvider; private final ObjectProvider<McpSyncServer> mcpServerProvider;
private final DynamicMcpServerManager dynamicMcpServerManager; private final DynamicMcpServerManager dynamicMcpServerManager;
private final RedisRegistryService redisRegistryService; private final InMemoryRegistryService InMemoryRegistryService;
private final RegistryMcpToolSpecificationFactory specificationFactory; private final RegistryMcpToolSpecificationFactory specificationFactory;
private final Set<String> managedToolNames = new LinkedHashSet<>(); private final Set<String> managedToolNames = new LinkedHashSet<>();
private final ReentrantLock lock = new ReentrantLock(); private final ReentrantLock lock = new ReentrantLock();
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider, public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
DynamicMcpServerManager dynamicMcpServerManager, DynamicMcpServerManager dynamicMcpServerManager,
RedisRegistryService redisRegistryService, InMemoryRegistryService InMemoryRegistryService,
RegistryMcpToolSpecificationFactory specificationFactory) { RegistryMcpToolSpecificationFactory specificationFactory) {
this.mcpServerProvider = mcpServerProvider; this.mcpServerProvider = mcpServerProvider;
this.dynamicMcpServerManager = dynamicMcpServerManager; this.dynamicMcpServerManager = dynamicMcpServerManager;
this.redisRegistryService = redisRegistryService; this.InMemoryRegistryService = InMemoryRegistryService;
this.specificationFactory = specificationFactory; this.specificationFactory = specificationFactory;
} }
@@ -66,7 +66,7 @@ public class RegistryMcpToolSynchronizer {
public void synchronize() { public void synchronize() {
lock.lock(); lock.lock();
try { try {
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools() List<ToolMetadata> activeEntries = InMemoryRegistryService.getAllTools()
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList()); .stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
// 1. Dynamic MCP Server 동기화 (카테고리별) // 1. Dynamic MCP Server 동기화 (카테고리별)

View File

@@ -0,0 +1,172 @@
package io.shinhanlife.dap.mcg.trace;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Instant;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @package io.shinhanlife.dap.mcg.trace
* @className InMemoryToolTraceService
* @description AX HUB 시스템 인메모리 툴 트레이스 서비스
* @author 0986406
* @create 2026.09.01
*/
@Service
public class InMemoryToolTraceService {
private static final Logger log = LoggerFactory.getLogger(InMemoryToolTraceService.class);
private final McpGatewayProperties properties;
private final ObjectMapper json;
private final McpMonitorEventService monitorEvents;
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
// LRU Cache for recent traces
private final Map<String, String> recentTraces;
public InMemoryToolTraceService(McpGatewayProperties properties,
ObjectMapper json,
McpMonitorEventService monitorEvents) {
this.properties = properties;
this.json = json;
this.monitorEvents = monitorEvents;
final int maxSize = (int) properties.traceRetentionSize();
this.recentTraces = new LinkedHashMap<String, String>(maxSize, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > maxSize;
}
};
}
public void started(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments) {
save(context, metadata, arguments, "STARTED", 0, "", 0, 0, 0,
"Agent request entered MCP", "");
}
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", "");
}
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", "");
}
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);
}
public List<String> getRecentTraces(int limit) {
if (!properties.traceEnabled()) {
return List.of("Trace is disabled. Set traceEnabled=true.");
}
List<String> result = new ArrayList<>();
synchronized (recentTraces) {
List<String> values = new ArrayList<>(recentTraces.values());
// reverse to get latest first
for (int i = values.size() - 1; i >= 0 && result.size() < limit; i--) {
result.add(values.get(i));
}
}
return result;
}
public String getTraceDetail(String requestId) {
if (!properties.traceEnabled()) {
return "Trace is disabled.";
}
synchronized (recentTraces) {
return recentTraces.get(requestId);
}
}
private void save(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
String state, long elapsedMillis, String failureType, int attempt, int maxAttempts, long backoffMillis,
String message, String responseText) {
if (!properties.traceEnabled()) {
return; // trace is off
}
try {
AttemptState ast = attemptStates.computeIfAbsent(context.requestId(), k -> new AttemptState(0));
if (attempt > 0) ast.currentAttempt = attempt;
ObjectNode root = json.createObjectNode();
root.put("requestId", context.requestId());
root.put("agentId", context.agentId());
root.put("tenantId", context.traceGroupId());
root.put("toolName", metadata.getName());
root.put("targetPod", metadata.getPodUrl());
root.put("state", state);
root.put("timestamp", Instant.now().toString());
root.put("elapsedMillis", elapsedMillis);
root.put("attempt", ast.currentAttempt);
root.put("maxAttempts", maxAttempts > 0 ? maxAttempts : properties.retryMaxAttempts());
root.put("backoffMillis", backoffMillis);
if (failureType != null && !failureType.isEmpty()) {
root.put("failureType", failureType);
}
if (message != null && !message.isEmpty()) {
root.put("message", message);
}
// Mask arguments if needed, for simplicity we just put them
root.set("arguments", arguments);
if (responseText != null && !responseText.isEmpty()) {
if (responseText.length() > 500) {
root.put("responsePreview", responseText.substring(0, 500) + "...");
} else {
root.put("responsePreview", responseText);
}
}
String payload = json.writeValueAsString(root);
synchronized (recentTraces) {
recentTraces.put(context.requestId(), payload);
}
monitorEvents.publish(payload);
if ("SUCCESS".equals(state) || "FAILED".equals(state)) {
attemptStates.remove(context.requestId());
}
} catch (Exception e) {
log.error("Failed to save trace: {}", e.getMessage());
}
}
private static class AttemptState {
int currentAttempt;
AttemptState(int currentAttempt) {
this.currentAttempt = currentAttempt;
}
}
}

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dap.mcg.redis; package io.shinhanlife.dap.mcg.trace;
/** /**
* @package io.shinhanlife.dap.mcg.redis * @package io.shinhanlife.dap.mcg.trace
* @className McpMonitorEventService * @className McpMonitorEventService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406

View File

@@ -1,17 +1,17 @@
package io.shinhanlife.dap.mcg; package io.shinhanlife.dap.mcg;
/** /**
* @package io.shinhanlife.dap.mcg * @package io.shinhanlife.dap.mcg
* @className DapGatewayApplicationTests * @className DapGatewayApplicationTests
* @description AX HUB 시스템 처리 클래스 * @description AX HUB ?<3F>스??처리 ?<3F>래??
* @author 0986406 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정력 ---------- * ---------- 개정?<3F>력 ----------
* 수정일 수정자 수정내 * ?<3F>정?? ?<3F>정?? ?<3F>정?<3F>
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 0986406 최초 * 2026.09.01 0986406 최초?<3F>
* *
* </pre> * </pre>
*/ */
@@ -20,7 +20,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import io.shinhanlife.dap.mcg.audit.AuditLogService; import io.shinhanlife.dap.mcg.audit.AuditLogService;
import io.shinhanlife.dap.mcg.redis.RedisToolTraceService; import io.shinhanlife.dap.mcg.trace.InMemoryToolTraceService;
@SpringBootTest @SpringBootTest
@ActiveProfiles("test") @ActiveProfiles("test")
@@ -28,7 +28,7 @@ class DapGatewayApplicationTests {
// Mock components that might require external dependencies (like Redis/DB) to pass the context load // Mock components that might require external dependencies (like Redis/DB) to pass the context load
@MockitoBean @MockitoBean
private RedisToolTraceService redisToolTraceService; private InMemoryToolTraceService InMemoryToolTraceService;
@MockitoBean @MockitoBean
private AuditLogService auditLogService; private AuditLogService auditLogService;

View File

@@ -0,0 +1,20 @@
package io.shinhanlife.dap.lib.mcp;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Test;
import org.springframework.ai.mcp.annotation.McpTool;
class McpSdk2CompatibilityTest {
@Test
void usesOfficialSpringAiMcpAnnotations() {
assertEquals("org.springframework.ai.mcp.annotation", McpTool.class.getPackageName());
}
@Test
void usesMcpJavaSdkTwo() {
assertEquals("2.0.0", McpSchema.class.getPackage().getImplementationVersion());
}
}

BIN
deps.txt Normal file

Binary file not shown.

View File

@@ -1,15 +1,7 @@
name: ax_hub_mcp_tool name: ax_hub_mcp_tool
services: services:
redis:
image: redis:latest
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
gateway: gateway:
build: build:
@@ -20,16 +12,10 @@ services:
volumes: volumes:
- ./:/src - ./:/src
depends_on: depends_on:
redis:
condition: service_healthy
tool-report: tool-report:
condition: service_started condition: service_started
environment: environment:
- TZ=Asia/Seoul - TZ=Asia/Seoul
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_HOST=redis
- SPRING_DATA_REDIS_PORT=6379
- AXHUB_SOURCE_DIR=/src - AXHUB_SOURCE_DIR=/src
- JAVA_TOOL_OPTIONS=-Xms64m -Xmx384m - JAVA_TOOL_OPTIONS=-Xms64m -Xmx384m
- SPRING_PROFILES_ACTIVE=${ACTIVE_PROFILE:-local} - SPRING_PROFILES_ACTIVE=${ACTIVE_PROFILE:-local}
@@ -67,13 +53,8 @@ services:
dockerfile: dap-was-sms/Dockerfile dockerfile: dap-was-sms/Dockerfile
ports: ports:
- "8282:8082" - "8282:8082"
depends_on:
- redis
environment: environment:
- TZ=Asia/Seoul - TZ=Asia/Seoul
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_PORT=6379
- AXHUB_GATEWAY_URL=http://gateway:8081 - AXHUB_GATEWAY_URL=http://gateway:8081
- AXHUB_SOURCE_DIR=/src - AXHUB_SOURCE_DIR=/src
- AXHUB_TOOL_URL=http://was-sms:8082 - AXHUB_TOOL_URL=http://was-sms:8082
@@ -91,13 +72,8 @@ services:
dockerfile: dap-was-oth/Dockerfile dockerfile: dap-was-oth/Dockerfile
ports: ports:
- "8284:8084" - "8284:8084"
depends_on:
- redis
environment: environment:
- TZ=Asia/Seoul - TZ=Asia/Seoul
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_PORT=6379
- AXHUB_GATEWAY_URL=http://gateway:8081 - AXHUB_GATEWAY_URL=http://gateway:8081
- AXHUB_TOOL_URL=http://was-oth:8084 - AXHUB_TOOL_URL=http://was-oth:8084
- GLOW_COMMUNICATION_MCI_HOMT=http://mci-mock - GLOW_COMMUNICATION_MCI_HOMT=http://mci-mock

859
smp_tools.json Normal file
View File

@@ -0,0 +1,859 @@
{
"value": [
{
"uid": "080f2207-f98a-31b4-9a46-5bc7a8e41f90",
"semver": "1.0.0",
"displayName": "고객 통합 안내이력 조회",
"name": "cmm_customer_tool",
"description": "고객의 통합 안내 이력을 조회하는 도구ìž\u0085니다.\n사용 시점: 고객 ID와 조회 기간을 기반으로 해당 기간 동안의 안내 이력을 확인할 때 사용합니다.\n사용 제외: 안내 이력을 생성하거나 수정할 때는 사용하지 않습니다.\nìž\u0085출력 제한: 안내 이력 조회만 수행하며 데이터를 변경하지 않습니다.",
"functionDescription": "고객의 통합 안내 이력을 조회하는 도구ìž\u0085니다.",
"whenToUse": "고객 ID와 조회 기간을 기반으로 해당 기간 동안의 안내 이력을 확인할 때 사용합니다.",
"whenNotToUse": "안내 이력을 생성하거나 수정할 때는 사용하지 않습니다.",
"ioLimits": "안내 이력 조회만 수행하며 데이터를 변경하지 않습니다.",
"displayDescription": "고객의 통합 안내 이력을 조회합니다.",
"exampleQueries": [
"고객 통합 안내이력 조회해줘",
"특정 기간의 안내 이력을 확인해줘",
"고객 번호로 안내 이력을 찾아줘"
],
"tags": [
"고객",
"통합안내이력"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"csNo": {
"type": "string",
"description": "고객번호"
},
"ntleCd": {
"type": "string",
"description": "안내장코드"
},
"notiPmlMdCd": {
"type": "string",
"description": "안내발송방법코드"
},
"inqrStrYmd": {
"type": "string",
"description": "조회시작일자"
},
"inqrEndYmd": {
"type": "string",
"description": "조회ì¢\u0085료일자"
}
},
"required": [
"csNo"
],
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "cmm",
"endpoint": "http://localhost:8084/mcp/cmm_customer_tool",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": "ONILD0320",
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "26c5b305-a75d-3092-9252-16849187090d",
"semver": "1.0.0",
"displayName": "메타 공통코드 조회",
"name": "cmm_comcode_lookup",
"description": "통합코드 그룹과 코드ëª\u0085 조건으로 메타 공통코드 목록을 조회한다.\n사용 시점: ì—\u0085무 코드의 값과 표시ëª\u0085을 확인하거나 유효한 코드 목록이 필요한 경우 사용한다.\n사용 제외: 공통코드를 신규 등록하거나 변경 또는 삭제하려는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: 검색 조건에 맞는 코드와 코드ëª\u0085만 반환하며 코드 데이터는 변경하지 않는다.",
"functionDescription": "통합코드 그룹과 코드ëª\u0085 조건으로 메타 공통코드 목록을 조회한다.",
"whenToUse": "ì—\u0085무 코드의 값과 표시ëª\u0085을 확인하거나 유효한 코드 목록이 필요한 경우 사용한다.",
"whenNotToUse": "공통코드를 신규 등록하거나 변경 또는 삭제하려는 경우에는 사용하지 않는다.",
"ioLimits": "검색 조건에 맞는 코드와 코드ëª\u0085만 반환하며 코드 데이터는 변경하지 않는다.",
"displayDescription": "메타 시스í\u0085œì˜ 공통코드 목록을 조회합니다.",
"exampleQueries": [
"사용 상태 코드 목록을 알려줘",
"고객 구분 공통코드를 찾아줘",
"사용 중인 통합코드를 조회해줘"
],
"tags": [
"메타",
"공통코드"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"groupCode": {
"type": "string",
"description": "조회할 통합코드 그룹 ID"
},
"codeName": {
"type": "string",
"description": "코드ëª\u0085 검색 키워드"
},
"useYn": {
"type": "string",
"description": "사용 여부 Y 또는 N",
"enum": [
"Y",
"N"
]
}
},
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "cmm",
"endpoint": "http://localhost:8084/mcp/cmm_comcode_lookup",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": "CLCNNB00001",
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "bf437712-8e47-3db6-9e56-2d1995dc0609",
"semver": "1.0.0",
"displayName": "메타 í\u0085Œì´ë¸” 조회",
"name": "cmm_meta_table",
"description": "물리ëª\u0085, ë\u0085¼ë¦¬ëª\u0085 또는 소유자 조건으로 메타 í\u0085Œì´ë¸” 정보를 조회한다.\n사용 시점: 사용자가 ì—\u0085무 데이터의 í\u0085Œì´ë¸”ëª\u0085이나 소유 스키마를 확인하려는 경우 사용한다.\n사용 제외: í\u0085Œì´ë¸”을 생성하거나 구조를 변경하려는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: 메타에 등록된 í\u0085Œì´ë¸” 설ëª\u0085 정보만 반환하며 실제 í\u0085Œì´ë¸” 데이터는 조회하지 않는다.",
"functionDescription": "물리ëª\u0085, ë\u0085¼ë¦¬ëª\u0085 또는 소유자 조건으로 메타 í\u0085Œì´ë¸” 정보를 조회한다.",
"whenToUse": "사용자가 ì—\u0085무 데이터의 í\u0085Œì´ë¸”ëª\u0085이나 소유 스키마를 확인하려는 경우 사용한다.",
"whenNotToUse": "í\u0085Œì´ë¸”을 생성하거나 구조를 변경하려는 경우에는 사용하지 않는다.",
"ioLimits": "메타에 등록된 í\u0085Œì´ë¸” 설ëª\u0085 정보만 반환하며 실제 í\u0085Œì´ë¸” 데이터는 조회하지 않는다.",
"displayDescription": "메타 시스í\u0085œì— 등록된 í\u0085Œì´ë¸” 정보를 조회합니다.",
"exampleQueries": [
"고객 기본 í\u0085Œì´ë¸”을 찾아줘",
"계약 관련 í\u0085Œì´ë¸” 목록을 보여줘",
"특정 스키마의 í\u0085Œì´ë¸”을 조회해줘"
],
"tags": [
"메타",
"í\u0085Œì´ë¸”"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"tableName": {
"type": "string",
"description": "í\u0085Œì´ë¸” 물리ëª\u0085 검색어"
},
"tableLogicalName": {
"type": "string",
"description": "í\u0085Œì´ë¸” ë\u0085¼ë¦¬ëª\u0085 검색어"
},
"owner": {
"type": "string",
"description": "í\u0085Œì´ë¸” 소유 스키마ëª\u0085"
}
},
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "cmm",
"endpoint": "http://localhost:8084/mcp/cmm_meta_table",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": null,
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "4914953b-ea25-389b-a6a9-c3c407f4d5bb",
"semver": "1.0.0",
"displayName": "ì—\u0085무 í\u0085œí”Œë¦¿ 다운로드 URL 조회",
"name": "cmm_template_url",
"description": "요청한 ì—\u0085무 í\u0085œí”Œë¦¿ 파일을 내려받을 수 있는 URL을 반환한다.\n사용 시점: 사용자가 엑ì\u0085€ì´ë‚˜ 워드 ì—\u0085무 양식의 다운로드 위치를 요청한 경우 사용한다.\n사용 제외: í\u0085œí”Œë¦¿ 내용을 작성하거나 ì—\u0085로드 또는 변경하려는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: 등록된 í\u0085œí”Œë¦¿ ID에 대한 다운로드 URL만 반환하며 파일 자체는 반환하지 않는다.",
"functionDescription": "요청한 ì—\u0085무 í\u0085œí”Œë¦¿ 파일을 내려받을 수 있는 URL을 반환한다.",
"whenToUse": "사용자가 엑ì\u0085€ì´ë‚˜ 워드 ì—\u0085무 양식의 다운로드 위치를 요청한 경우 사용한다.",
"whenNotToUse": "í\u0085œí”Œë¦¿ 내용을 작성하거나 ì—\u0085로드 또는 변경하려는 경우에는 사용하지 않는다.",
"ioLimits": "등록된 í\u0085œí”Œë¦¿ ID에 대한 다운로드 URL만 반환하며 파일 자체는 반환하지 않는다.",
"displayDescription": "ì—\u0085무 í\u0085œí”Œë¦¿ì„ 다운로드할 수 있는 URL을 제공합니다.",
"exampleQueries": [
"청구 양식 다운로드 링크를 알려줘",
"ì—\u0085무용 엑ì\u0085€ í\u0085œí”Œë¦¿ì„ 받고 싶어",
"등록된 문서 양식 위치를 찾아줘"
],
"tags": [
"í\u0085œí”Œë¦¿",
"다운로드"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"templateId": {
"type": "string",
"description": "다운로드할 í\u0085œí”Œë¦¿ 식별자"
}
},
"required": [
"templateId"
],
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "cmm",
"endpoint": "http://localhost:8084/mcp/cmm_template_url",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": null,
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "e14f5dc9-38c5-3424-bc73-69052c28f660",
"semver": "1.0.0",
"displayName": "보험금 청구 처리",
"name": "ins_insurance_processor",
"description": "보험금 청구번호, 청구금액과 청구일을 받아 청구 처리 요청을 수행한다.\n사용 시점: 사용자가 확인된 보험금 청구 정보를 실제 처리계에 접수하려는 경우 사용한다.\n사용 제외: 청구 상태만 조회하거나 필수 정보가 확인되지 않은 경우에는 사용하지 않는다.\nìž\u0085출력 제한: 실행 시 ì—\u0085무 상태가 변경될 수 있으므로 호출 전에 ìž\u0085력값과 사용자 의사를 확인해야 한다.",
"functionDescription": "보험금 청구번호, 청구금액과 청구일을 받아 청구 처리 요청을 수행한다.",
"whenToUse": "사용자가 확인된 보험금 청구 정보를 실제 처리계에 접수하려는 경우 사용한다.",
"whenNotToUse": "청구 상태만 조회하거나 필수 정보가 확인되지 않은 경우에는 사용하지 않는다.",
"ioLimits": "실행 시 ì—\u0085무 상태가 변경될 수 있으므로 호출 전에 ìž\u0085력값과 사용자 의사를 확인해야 한다.",
"displayDescription": "확인된 보험금 청구 요청을 처리계에 전달합니다.",
"exampleQueries": [
"확인한 내용으로 보험금 청구를 접수해줘",
"이 청구번호의 보험금 처리를 진행해줘",
"오늘 날짜로 보험금 청구 요청을 보내줘"
],
"tags": [
"보험금",
"청구처리"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"claimNumber": {
"type": "string",
"description": "처리할 보험금 청구번호"
},
"claimAmount": {
"type": "number",
"description": "처리할 보험금 청구금액"
},
"claimDate": {
"type": "string",
"description": "청구일자 YYYYMMDD",
"pattern": "^[0-9]{8}$"
}
},
"required": [
"claimNumber",
"claimAmount",
"claimDate"
],
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "ins",
"endpoint": "http://localhost:8084/mcp/ins_insurance_processor",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": "CLAIM0000001",
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "a503bf94-adc5-3bdc-b9b0-9c6fcb59838d",
"semver": "1.0.0",
"displayName": "ONNBA3011 보험 ì—\u0085무 조회",
"name": "oth_onnba3011_call",
"description": "ONNBA3011 ìž\u0085력정보를 MCI 전문으로 변환해 보험 ì—\u0085무 결과를 조회한다.\n사용 시점: 사용자가 ONNBA3011 인터페이스 기준의 보험 계약 또는 지급 정보를 조회하려는 경우 사용한다.\n사용 제외: 인터페이스 ìž\u0085력값을 확인할 수 없거나 보험 정보를 변경하려는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: CLCNNB00001 인터페이스 규격에 맞는 값만 전달하며 조회 결과를 ì—\u0085무 응답으로 변환한다.",
"functionDescription": "ONNBA3011 ìž\u0085력정보를 MCI 전문으로 변환해 보험 ì—\u0085무 결과를 조회한다.",
"whenToUse": "사용자가 ONNBA3011 인터페이스 기준의 보험 계약 또는 지급 정보를 조회하려는 경우 사용한다.",
"whenNotToUse": "인터페이스 ìž\u0085력값을 확인할 수 없거나 보험 정보를 변경하려는 경우에는 사용하지 않는다.",
"ioLimits": "CLCNNB00001 인터페이스 규격에 맞는 값만 전달하며 조회 결과를 ì—\u0085무 응답으로 변환한다.",
"displayDescription": "ONNBA3011 ì—\u0085무 정보를 MCI로 조회합니다.",
"exampleQueries": [
"고객의 보험 ì—\u0085무 정보를 조회해줘",
"ONNBA3011 기준으로 계약 정보를 확인해줘",
"ìž\u0085력한 고객번호의 보험 결과를 알려줘"
],
"tags": [
"보험",
"MCI"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
"GLOW_COMMUNICATION_MCI_HOST",
"GLOW_COMMUNICATION_MCI_PORT"
],
"parametersSchema": {
"type": "object",
"properties": {
"dalScCd": {
"type": "string",
"description": "거래 구분 코드"
},
"cstSucoRltyCd": {
"type": "string",
"description": "고객 성공 관계 코드"
},
"csNo": {
"type": "string",
"description": "고객 번호"
},
"rdreNo": {
"type": "string",
"description": "설계사 번호"
},
"unfcPvsCalReqYn": {
"type": "string",
"description": "미확정 지급 계산 요청 여부"
},
"kcisPymmTnnrRequest": {
"type": "string",
"description": "KCIS 납ìž\u0085 기간 요청값"
},
"lmovYn": {
"type": "string",
"description": "계약 이동 여부"
},
"genPsthApvTrgtYn": {
"type": "string",
"description": "일반 사후 승인 대상 여부"
},
"ircoLmovEcpbTrgtYn": {
"type": "string",
"description": "계약 이동 예외 대상 여부"
},
"digCalYn": {
"type": "string",
"description": "디지털 계산 여부"
},
"prbuIciDigCalYn": {
"type": "string",
"description": "상품별 디지털 계산 여부"
},
"unfcPrbuIrcoAddu": {
"type": "object",
"description": "미확정 상품 추가 정보",
"additionalProperties": true
},
"sucoIspaBasDto": {
"type": "object",
"description": "성공 심사 기본 정보",
"additionalProperties": true
}
},
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "oth",
"endpoint": "http://localhost:8084/mcp/oth_onnba3011_call",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": "CLCNNB00001",
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "850ea7c1-f54f-38d9-8b4f-958602271fb2",
"semver": "1.0.0",
"displayName": "오늘의 ëª\u0085언 조회",
"name": "smp_quote_daily",
"description": "선택한 ì¹´í\u0085Œê³ ë¦¬ì— 맞는 오늘의 ëª\u0085언 한 건을 조회한다.\n사용 시점: 사용자가 ëª\u0085언이나 짧은 동기부여 문구를 요청한 경우 사용한다.\n사용 제외: ì—\u0085무 데이터 조회나 사실 검증이 필요한 질문에는 사용하지 않는다.\nìž\u0085출력 제한: 등록된 ëª\u0085언 데이터 중 한 건만 반환하며 출처 정보가 없을 수 있다.",
"functionDescription": "선택한 ì¹´í\u0085Œê³ ë¦¬ì— 맞는 오늘의 ëª\u0085언 한 건을 조회한다.",
"whenToUse": "사용자가 ëª\u0085언이나 짧은 동기부여 문구를 요청한 경우 사용한다.",
"whenNotToUse": "ì—\u0085무 데이터 조회나 사실 검증이 필요한 질문에는 사용하지 않는다.",
"ioLimits": "등록된 ëª\u0085언 데이터 중 한 건만 반환하며 출처 정보가 없을 수 있다.",
"displayDescription": "ì¹´í\u0085Œê³ ë¦¬ì— 맞는 오늘의 ëª\u0085언을 제공합니다.",
"exampleQueries": [
"오늘 힘이 되는 말을 알려줘",
"ì—\u0085무 시작 전에 ëª\u0085언 하나 보여줘",
"성공에 관한 짧은 문구를 추천해줘"
],
"tags": [
"ëª\u0085언",
"콘í\u0085ì¸ "
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "조회할 ëª\u0085언 ì¹´í\u0085Œê³ ë¦¬"
}
},
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "smp",
"endpoint": "http://localhost:8084/mcp/smp_quote_daily",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": null,
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "ef86d147-785e-3007-a158-f7e6ffd275de",
"semver": "1.0.0",
"displayName": "실시간 환율 조회",
"name": "smp_exchange_inquiry",
"description": "통화코드를 기준으로 현재 환율 정보를 조회한다.\n사용 시점: 사용자가 USD, EUR, JPY 등 특정 통화의 환율을 확인하려는 경우 사용한다.\n사용 제외: 환전 거래를 실행하거나 과거 환율 통계를 분석하려는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: 지원되는 통화코드의 조회 시점 환율만 반환하며 실제 환전 기능은 제공하지 않는다.",
"functionDescription": "통화코드를 기준으로 현재 환율 정보를 조회한다.",
"whenToUse": "사용자가 USD, EUR, JPY 등 특정 통화의 환율을 확인하려는 경우 사용한다.",
"whenNotToUse": "환전 거래를 실행하거나 과거 환율 통계를 분석하려는 경우에는 사용하지 않는다.",
"ioLimits": "지원되는 통화코드의 조회 시점 환율만 반환하며 실제 환전 기능은 제공하지 않는다.",
"displayDescription": "지정한 통화의 현재 환율을 조회합니다.",
"exampleQueries": [
"오늘 달러 환율을 알려줘",
"엔화 환율이 얼마인지 조회해줘",
"유로 환율을 확인해줘"
],
"tags": [
"환율",
"금융"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"currencyCode": {
"type": "string",
"description": "조회할 ISO 통화코드",
"pattern": "^[A-Z]{3}$"
}
},
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "smp",
"endpoint": "http://localhost:8084/mcp/smp_exchange_inquiry",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": null,
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "44508c3e-89a8-3fa8-92ec-ae236ca2cd88",
"semver": "1.0.0",
"displayName": "MCP·TOOL 파트 구성원 조회",
"name": "smp_team_list",
"description": "신한라이프 AX 추진팀과 MCP·TOOL 파트 구성원 및 직급을 조회한다.\n사용 시점: 사용자가 프로젝트 담당자나 팀별 구성원 정보를 묻는 경우 사용한다.\n사용 제외: 인사 개인정보나 연락처 또는 조직 변경 이력을 요청하는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: 사전에 등록된 구성원의 이름과 직급만 반환하며 민감한 인사정보는 포함하지 않는다.",
"functionDescription": "신한라이프 AX 추진팀과 MCP·TOOL 파트 구성원 및 직급을 조회한다.",
"whenToUse": "사용자가 프로젝트 담당자나 팀별 구성원 정보를 묻는 경우 사용한다.",
"whenNotToUse": "인사 개인정보나 연락처 또는 조직 변경 이력을 요청하는 경우에는 사용하지 않는다.",
"ioLimits": "사전에 등록된 구성원의 이름과 직급만 반환하며 민감한 인사정보는 포함하지 않는다.",
"displayDescription": "신한라이프 MCP·TOOL 파트 담당자와 구성원을 조회합니다.",
"exampleQueries": [
"MCP 팀 담당자를 알려줘",
"TOOL 파트 구성원이 누구인지 보여줘",
"AX 추진팀 담당자를 찾아줘"
],
"tags": [
"조직",
"담당자"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"teamName": {
"type": "string",
"description": "조회할 팀 이름 또는 전체"
}
},
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "smp",
"endpoint": "http://localhost:8084/mcp/smp_team_list",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": null,
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "bc0f227e-3a9b-36a1-af6f-3f20c1964969",
"semver": "1.0.0",
"displayName": "도시 날씨 조회",
"name": "smp_weather_inquiry",
"description": "도시ëª\u0085을 기준으로 현재 날씨, 온도와 풍속을 조회한다.\n사용 시점: 사용자가 특정 도시의 현재 기상 정보를 요청한 경우 사용한다.\n사용 제외: 장기 예보나 기상 특보 또는 공식 재난정보가 필요한 경우에는 사용하지 않는다.\nìž\u0085출력 제한: ìž\u0085력한 도시의 현재 관측 기반 샘플 정보만 반환한다.",
"functionDescription": "도시ëª\u0085을 기준으로 현재 날씨, 온도와 풍속을 조회한다.",
"whenToUse": "사용자가 특정 도시의 현재 기상 정보를 요청한 경우 사용한다.",
"whenNotToUse": "장기 예보나 기상 특보 또는 공식 재난정보가 필요한 경우에는 사용하지 않는다.",
"ioLimits": "ìž\u0085력한 도시의 현재 관측 기반 샘플 정보만 반환한다.",
"displayDescription": "지정한 도시의 현재 날씨 정보를 조회합니다.",
"exampleQueries": [
"서울 날씨를 알려줘",
"부산의 현재 온도를 조회해줘",
"제주도 바람이 얼마나 부는지 알려줘"
],
"tags": [
"날씨",
"조회"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "날씨를 조회할 도시ëª\u0085"
}
},
"required": [
"city"
],
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "smp",
"endpoint": "http://localhost:8084/mcp/smp_weather_inquiry",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": null,
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "5fd282b9-6472-3fc3-a562-fe23e000ab7e",
"semver": "1.0.0",
"displayName": "SOL 의뢰서 상세 조회",
"name": "sol_request_detail",
"description": "SOL 의뢰서 식별자를 기준으로 의뢰서 상세 내용을 조회한다.\n사용 시점: 사용자가 특정 SOL 의뢰서의 상세 내용과 진행 정보를 확인하려는 경우 사용한다.\n사용 제외: 의뢰서 목록만 찾거나 의뢰서를 변경 또는 처리하려는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: 정확한 의뢰서 ID가 필요하며 등록된 상세 정보만 반환한다.",
"functionDescription": "SOL 의뢰서 식별자를 기준으로 의뢰서 상세 내용을 조회한다.",
"whenToUse": "사용자가 특정 SOL 의뢰서의 상세 내용과 진행 정보를 확인하려는 경우 사용한다.",
"whenNotToUse": "의뢰서 목록만 찾거나 의뢰서를 변경 또는 처리하려는 경우에는 사용하지 않는다.",
"ioLimits": "정확한 의뢰서 ID가 필요하며 등록된 상세 정보만 반환한다.",
"displayDescription": "SOL 의뢰서 한 건의 상세 정보를 조회합니다.",
"exampleQueries": [
"이 SOL 의뢰서 상세를 보여줘",
"의뢰서 ID로 처리 내용을 확인해줘",
"선택한 의뢰서의 상세 정보를 알려줘"
],
"tags": [
"SOL",
"의뢰서"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"srId": {
"type": "string",
"description": "상세 조회할 SOL 의뢰서 ID"
}
},
"required": [
"srId"
],
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "sol",
"endpoint": "http://localhost:8084/mcp/sol_request_detail",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": "SOLG00000002",
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
},
{
"uid": "332361bb-488a-388a-974c-c28ec243c253",
"semver": "1.0.0",
"displayName": "SOL 의뢰서 목록 조회",
"name": "sol_request_list",
"description": "진행상태, 조회기간과 조회대상 조건으로 SOL 의뢰서 목록을 조회한다.\n사용 시점: 사용자가 자신의 SOL 의뢰서나 상태별 의뢰서 목록을 확인하려는 경우 사용한다.\n사용 제외: 특정 의뢰서의 상세 내용만 보거나 의뢰서를 변경하려는 경우에는 사용하지 않는다.\nìž\u0085출력 제한: ìž\u0085ë ¥ 조건에 해당하는 의뢰서 요약 목록만 반환한다.",
"functionDescription": "진행상태, 조회기간과 조회대상 조건으로 SOL 의뢰서 목록을 조회한다.",
"whenToUse": "사용자가 자신의 SOL 의뢰서나 상태별 의뢰서 목록을 확인하려는 경우 사용한다.",
"whenNotToUse": "특정 의뢰서의 상세 내용만 보거나 의뢰서를 변경하려는 경우에는 사용하지 않는다.",
"ioLimits": "ìž\u0085ë ¥ 조건에 해당하는 의뢰서 요약 목록만 반환한다.",
"displayDescription": "조건에 맞는 SOL 의뢰서 목록을 조회합니다.",
"exampleQueries": [
"진행 중인 SOL 의뢰서를 보여줘",
"최근 한 달간 내 의뢰서를 조회해줘",
"완료된 의뢰서 목록을 알려줘"
],
"tags": [
"SOL",
"의뢰서"
],
"ownerOrg": "MCP_TOOL",
"requiredEnvKeys": [
],
"parametersSchema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "조회할 의뢰서 진행상태"
},
"period": {
"type": "string",
"description": "조회할 기간 조건"
},
"target": {
"type": "string",
"description": "나의 ì—\u0085무 또는 전체 조회대상"
}
},
"additionalProperties": false
},
"outputSchema": null,
"actionPrompts": {
},
"categoryKey": "sol",
"endpoint": "http://localhost:8084/mcp/sol_request_list",
"podUrl": "http://localhost:8084",
"visible": true,
"enabled": true,
"isRegistered": false,
"requiresApproval": false,
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true,
"integrationType": "REST",
"mciServiceId": "SOLG00000001",
"lastHeartbeat": null,
"failureRateThreshold": null,
"slidingWindowSize": null,
"rateLimitForPeriod": null,
"operationType": "READ",
"retryEnabled": true,
"circuitBreakerFailureThreshold": 0,
"circuitBreakerOpenMillis": 0,
"timeoutMillis": 5000
}
],
"Count": 12
}