feat: Flatten MCP Tool architecture and decouple metadata from arguments
This commit is contained in:
@@ -32,7 +32,8 @@ public class ExecuteService {
|
||||
killSwitchService.checkAgent(tenantId);
|
||||
|
||||
// [비상 차단 2단계] Tool 단위 차단 검사
|
||||
String toolName = (String) payload.get("toolName");
|
||||
Map<String, Object> params = payload.containsKey("params") ? (Map<String, Object>) payload.get("params") : null;
|
||||
String toolName = params != null ? (String) params.get("name") : (String) payload.get("toolName");
|
||||
killSwitchService.checkTool(toolName);
|
||||
|
||||
// 1. 검증 (Validator)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package io.shinhanlife.axhub.biz.mcp.tool.controller;
|
||||
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.model.ToolConfig;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.model.ToolFunction;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.registry.MockToolAutoRegistrar;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -26,6 +23,7 @@ import org.springframework.context.ApplicationContext;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction;
|
||||
import java.lang.reflect.Method;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -36,13 +34,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class BusinessToolController {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final MockToolAutoRegistrar toolRegistrar;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// 단일 동적 라우팅 엔드포인트
|
||||
@PostMapping("/execute/{toolName}")
|
||||
public Map<String, Object> executeDynamicTool(@PathVariable String toolName, @RequestBody(required = false) Map<String, Object> payload) {
|
||||
log.info("\n [Tool] 동적 툴 실행 요청 수신: {}", toolName);
|
||||
// 함수명(functionName) 기반의 단일 동적 라우팅 엔드포인트
|
||||
@PostMapping("/execute/{functionName}")
|
||||
public Map<String, Object> executeDynamicTool(@PathVariable String functionName, @RequestBody(required = false) Map<String, Object> payload) {
|
||||
log.info("\n [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
|
||||
if (payload != null) {
|
||||
try {
|
||||
log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(payload));
|
||||
@@ -51,47 +48,44 @@ public class BusinessToolController {
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 등록된 툴 설정 조회
|
||||
ToolConfig config = toolRegistrar.getTools().stream()
|
||||
.filter(t -> t.getName().equals(toolName))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
Map<String, Object> params = payload != null ? (Map<String, Object>) payload.get("params") : null;
|
||||
Map<String, Object> arguments = params != null ? (Map<String, Object>) params.get("arguments") : null;
|
||||
|
||||
if (config == null) {
|
||||
log.error(" [Tool] 등록되지 않은 툴 호출: {}", toolName);
|
||||
// 1. 대상 Bean 및 Method 찾기 (ApplicationContext 활용)
|
||||
Object targetBean = null;
|
||||
Method targetMethod = null;
|
||||
McpFunction targetFunctionAnnotation = null;
|
||||
|
||||
Map<String, Object> toolBeans = applicationContext.getBeansWithAnnotation(McpTool.class);
|
||||
outerLoop:
|
||||
for (Object bean : toolBeans.values()) {
|
||||
for (Method method : bean.getClass().getDeclaredMethods()) {
|
||||
McpFunction mcpFunc = method.getAnnotation(McpFunction.class);
|
||||
if (mcpFunc != null && mcpFunc.name().equals(functionName)) {
|
||||
targetBean = bean;
|
||||
targetMethod = method;
|
||||
targetFunctionAnnotation = mcpFunc;
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetBean == null || targetMethod == null) {
|
||||
log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}", functionName);
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("jsonrpc", "2.0");
|
||||
error.put("error", Map.of("code", -32601, "message", "등록되지 않은 툴입니다: " + toolName));
|
||||
error.put("error", Map.of("code", -32601, "message", "실행할 함수를 찾을 수 없습니다: " + functionName));
|
||||
error.put("id", payload != null ? payload.get("id") : null);
|
||||
return error;
|
||||
}
|
||||
|
||||
// 2. 파라미터에서 실행할 함수(action) 추출 및 라우팅 결정 (JSON-RPC)
|
||||
Map<String, Object> params = payload != null ? (Map<String, Object>) payload.get("params") : null;
|
||||
Map<String, Object> arguments = params != null ? (Map<String, Object>) params.get("arguments") : null;
|
||||
String action = arguments != null && arguments.containsKey("action") ? arguments.get("action").toString() : null;
|
||||
|
||||
ToolFunction targetFunction = null;
|
||||
if (action != null) {
|
||||
targetFunction = config.getFunctions().stream()
|
||||
.filter(f -> f.getName().equals(action))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
// action 파라미터가 없거나 일치하는 함수가 없으면 첫 번째 함수를 기본값으로 사용
|
||||
if (targetFunction == null && !config.getFunctions().isEmpty()) {
|
||||
targetFunction = config.getFunctions().get(0);
|
||||
log.warn(" [Tool] 지정된 action 파라미터가 없거나 유효하지 않아 기본 함수({})를 사용합니다.", targetFunction.getName());
|
||||
}
|
||||
|
||||
String interfaceId = targetFunction != null ? targetFunction.getInterfaceId() : "UNKNOWN";
|
||||
|
||||
// 2-1. 파라미터 유효성 검증 (JSON Schema)
|
||||
if (targetFunction != null && targetFunction.getParameterSchema() != null && !targetFunction.getParameterSchema().equals("{}")) {
|
||||
// 2. 파라미터 유효성 검증 (JSON Schema)
|
||||
if (targetMethod.getParameterCount() > 0) {
|
||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
||||
if (!Map.class.isAssignableFrom(paramType)) {
|
||||
try {
|
||||
// parameterSchema에는 properties 내용만 들어있으므로 완전한 스키마 형태로 감싸줍니다.
|
||||
String fullSchemaJson = "{\"type\":\"object\", \"properties\":" + targetFunction.getParameterSchema() + "}";
|
||||
Map<String, Object> autoSchema = JsonSchemaGenerator.generatePropertiesSchema(paramType);
|
||||
String fullSchemaJson = "{\"type\":\"object\", \"properties\":" + objectMapper.writeValueAsString(autoSchema) + "}";
|
||||
|
||||
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
||||
JsonSchema schema = factory.getSchema(fullSchemaJson);
|
||||
@@ -113,51 +107,12 @@ public class BusinessToolController {
|
||||
log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 대상 Bean 찾기 (ApplicationContext 활용)
|
||||
Object targetBean = null;
|
||||
Map<String, Object> toolBeans = applicationContext.getBeansWithAnnotation(McpTool.class);
|
||||
for (Object bean : toolBeans.values()) {
|
||||
McpTool mcpToolAnnotation = bean.getClass().getAnnotation(McpTool.class);
|
||||
if (mcpToolAnnotation != null && mcpToolAnnotation.name().equals(toolName)) {
|
||||
targetBean = bean;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetBean == null) {
|
||||
log.error("[Tool] 실행할 Tool Bean을 찾을 수 없습니다: {}", toolName);
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("jsonrpc", "2.0");
|
||||
error.put("error", Map.of("code", -32601, "message", "실행할 Tool Bean을 찾을 수 없습니다: " + toolName));
|
||||
error.put("id", payload != null ? payload.get("id") : null);
|
||||
return error;
|
||||
}
|
||||
|
||||
// 4. 리플렉션을 통해 대상 메서드 직접 호출
|
||||
String actionName = targetFunction.getName();
|
||||
Method targetMethod = null;
|
||||
for (Method method : targetBean.getClass().getDeclaredMethods()) {
|
||||
McpFunction mcpFunc = method.getAnnotation(McpFunction.class);
|
||||
if (mcpFunc != null && mcpFunc.name().equals(actionName)) {
|
||||
targetMethod = method;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetMethod == null) {
|
||||
log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}", actionName);
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("jsonrpc", "2.0");
|
||||
error.put("error", Map.of("code", -32601, "message", "실행할 함수(Method)를 찾을 수 없습니다: " + actionName));
|
||||
error.put("id", payload != null ? payload.get("id") : null);
|
||||
return error;
|
||||
}
|
||||
|
||||
log.info("[Tool] 리플렉션 직접 실행 -> Tool: {}, Method: {}", toolName, targetMethod.getName());
|
||||
log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName());
|
||||
|
||||
try {
|
||||
// 5. DTO 파라미터 자동 매핑 (Map -> DTO)
|
||||
// 3. DTO 파라미터 자동 매핑 (Map -> DTO)
|
||||
Object invokeArgument = arguments;
|
||||
if (targetMethod.getParameterCount() > 0 && arguments != null) {
|
||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
||||
@@ -167,10 +122,10 @@ public class BusinessToolController {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 메서드 실행
|
||||
// 4. 메서드 실행
|
||||
Object methodResult = targetMethod.invoke(targetBean, invokeArgument);
|
||||
|
||||
// 7. 결과 조립 (JSON-RPC 응답)
|
||||
// 5. 결과 조립 (JSON-RPC 응답)
|
||||
Map<String, Object> resultPayload = new HashMap<>();
|
||||
if (methodResult instanceof Map) {
|
||||
resultPayload = (Map<String, Object>) methodResult;
|
||||
|
||||
@@ -2,8 +2,6 @@ package io.shinhanlife.axhub.biz.mcp.tool.registry;
|
||||
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.model.ToolConfig;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.model.ToolFunction;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.axhub.biz.mcp.tool.util.JsonSchemaGenerator;
|
||||
@@ -23,7 +21,6 @@ import org.springframework.web.client.RestClient;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -46,66 +43,13 @@ public class MockToolAutoRegistrar {
|
||||
@Value("${mcp.tool.host:localhost}")
|
||||
private String toolHost;
|
||||
|
||||
private List<ToolConfig> TOOLS = new ArrayList<>();
|
||||
private List<String> registeredTools = new ArrayList<>();
|
||||
|
||||
public MockToolAutoRegistrar(ApplicationContext applicationContext, ObjectMapper objectMapper) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (serverPort == 8081) return; // Gateway skip
|
||||
|
||||
log.info(" [AutoDiscovery] @McpTool 어노테이션 스캔 시작...");
|
||||
Map<String, Object> toolBeans = applicationContext.getBeansWithAnnotation(McpTool.class);
|
||||
|
||||
for (Object bean : toolBeans.values()) {
|
||||
McpTool mcpTool = bean.getClass().getAnnotation(McpTool.class);
|
||||
if (mcpTool == null) continue;
|
||||
|
||||
// Target Group 필터링
|
||||
if (!"all".equals(toolTarget) && !mcpTool.name().equals(toolTarget) && !mcpTool.group().equals(toolTarget)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<ToolFunction> functions = new ArrayList<>();
|
||||
for (Method method : bean.getClass().getDeclaredMethods()) {
|
||||
if (method.isAnnotationPresent(McpFunction.class)) {
|
||||
McpFunction mcpFunc = method.getAnnotation(McpFunction.class);
|
||||
String finalSchema = mcpFunc.parameterSchema();
|
||||
if (method.getParameterCount() > 0) {
|
||||
Class<?> paramType = method.getParameterTypes()[0];
|
||||
if (!Map.class.isAssignableFrom(paramType)) {
|
||||
try {
|
||||
Map<String, Object> autoSchema = JsonSchemaGenerator.generatePropertiesSchema(paramType);
|
||||
finalSchema = objectMapper.writeValueAsString(autoSchema);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to generate schema for {}", paramType.getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
functions.add(new ToolFunction(mcpFunc.name(), mcpFunc.description(), mcpFunc.mappingId(), finalSchema));
|
||||
}
|
||||
}
|
||||
|
||||
ToolConfig config = new ToolConfig(
|
||||
mcpTool.name(),
|
||||
mcpTool.description(),
|
||||
"/api/tool/execute/" + mcpTool.name(),
|
||||
mcpTool.group(),
|
||||
mcpTool.routingType(),
|
||||
functions
|
||||
);
|
||||
TOOLS.add(config);
|
||||
log.info(" [AutoDiscovery] 툴 발견: {} (함수 {}개)", mcpTool.name(), functions.size());
|
||||
}
|
||||
}
|
||||
|
||||
public List<ToolConfig> getTools() {
|
||||
return TOOLS;
|
||||
}
|
||||
|
||||
private HttpHeaders createHeaders() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
@@ -118,6 +62,7 @@ public class MockToolAutoRegistrar {
|
||||
if (serverPort == 8081) return;
|
||||
|
||||
log.info("[MockTool] Tool Pod 기동 완료! (Port: {})", serverPort);
|
||||
registeredTools.clear();
|
||||
|
||||
for (Object bean : applicationContext.getBeansWithAnnotation(McpTool.class).values()) {
|
||||
McpTool mcpTool = bean.getClass().getAnnotation(McpTool.class);
|
||||
@@ -125,11 +70,15 @@ public class MockToolAutoRegistrar {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (Method method : bean.getClass().getDeclaredMethods()) {
|
||||
if (method.isAnnotationPresent(McpFunction.class)) {
|
||||
McpFunction mcpFunc = method.getAnnotation(McpFunction.class);
|
||||
|
||||
ToolMetadata myMetadata = new ToolMetadata();
|
||||
myMetadata.setToolName(mcpTool.name());
|
||||
myMetadata.setDescription(mcpTool.description());
|
||||
myMetadata.setToolName(mcpFunc.name()); // Function -> Tool로 승격
|
||||
myMetadata.setDescription(mcpFunc.description());
|
||||
myMetadata.setDomainGroup(mcpTool.group());
|
||||
myMetadata.setEndpoint("/api/tool/execute/" + mcpTool.name());
|
||||
myMetadata.setEndpoint("/api/tool/execute/" + mcpFunc.name());
|
||||
myMetadata.setPodUrl("http://" + toolHost + ":" + serverPort);
|
||||
myMetadata.setIntegrationType(mcpTool.routingType());
|
||||
myMetadata.setFailureRateThreshold(50);
|
||||
@@ -137,25 +86,20 @@ public class MockToolAutoRegistrar {
|
||||
myMetadata.setRateLimitForPeriod(100);
|
||||
|
||||
Map<String, String> actionPrompts = new HashMap<>();
|
||||
List<String> actions = new ArrayList<>();
|
||||
List<String> actionDescs = new ArrayList<>();
|
||||
Map<String, Object> aggregatedProperties = new HashMap<>();
|
||||
|
||||
for (Method method : bean.getClass().getDeclaredMethods()) {
|
||||
if (method.isAnnotationPresent(McpFunction.class)) {
|
||||
McpFunction mcpFunc = method.getAnnotation(McpFunction.class);
|
||||
actions.add(mcpFunc.name());
|
||||
actionDescs.add(mcpFunc.name() + "(" + mcpFunc.description() + ")");
|
||||
actionPrompts.put(mcpFunc.name(), mcpFunc.prompt());
|
||||
myMetadata.setActionPrompts(actionPrompts);
|
||||
|
||||
Map<String, Object> parametersSchema = new HashMap<>();
|
||||
parametersSchema.put("type", "object");
|
||||
Map<String, Object> aggregatedProperties = new HashMap<>();
|
||||
|
||||
if (method.getParameterCount() > 0) {
|
||||
Class<?> paramType = method.getParameterTypes()[0];
|
||||
if (!Map.class.isAssignableFrom(paramType)) {
|
||||
// DTO 기반 자동 스키마 생성
|
||||
// DTO 기반 스키마 (비즈니스 파라미터만)
|
||||
Map<String, Object> autoSchema = JsonSchemaGenerator.generatePropertiesSchema(paramType);
|
||||
aggregatedProperties.putAll(autoSchema);
|
||||
} else {
|
||||
// 기존 하드코딩 스키마 사용
|
||||
String schemaJson = mcpFunc.parameterSchema();
|
||||
if (!"{}".equals(schemaJson)) {
|
||||
try {
|
||||
@@ -163,30 +107,13 @@ public class MockToolAutoRegistrar {
|
||||
Map<String, Object> schemaMap = objectMapper.readValue(schemaJson, Map.class);
|
||||
aggregatedProperties.putAll(schemaMap);
|
||||
} catch (Exception e) {
|
||||
log.error(" [MockTool] Failed to parse parameterSchema for {}: {}", mcpFunc.name(), e.getMessage());
|
||||
}
|
||||
}
|
||||
log.error(" [MockTool] 스키마 파싱 실패 {}: {}", mcpFunc.name(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
myMetadata.setActionPrompts(actionPrompts);
|
||||
|
||||
Map<String, Object> parametersSchema = new HashMap<>();
|
||||
parametersSchema.put("type", "object");
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.putAll(aggregatedProperties);
|
||||
|
||||
Map<String, Object> actionProperty = new HashMap<>();
|
||||
actionProperty.put("type", "string");
|
||||
actionProperty.put("enum", actions);
|
||||
actionProperty.put("description", "실행할 함수명: " + String.join(", ", actionDescs));
|
||||
|
||||
properties.put("action", actionProperty);
|
||||
parametersSchema.put("properties", properties);
|
||||
parametersSchema.put("required", Arrays.asList("action"));
|
||||
|
||||
parametersSchema.put("properties", aggregatedProperties);
|
||||
myMetadata.setParametersSchema(parametersSchema);
|
||||
|
||||
try {
|
||||
@@ -197,9 +124,12 @@ public class MockToolAutoRegistrar {
|
||||
.body(myMetadata)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
log.info(" [MockTool] {} 자동 등록 성공! (Pod URL: {})", mcpTool.name(), myMetadata.getPodUrl());
|
||||
log.info(" [MockTool] 함수 툴 승격 및 등록 성공: {} (Pod: {})", mcpFunc.name(), myMetadata.getPodUrl());
|
||||
registeredTools.add(mcpFunc.name());
|
||||
} catch (Exception e) {
|
||||
log.error(" [MockTool] {} 자동 등록 실패: {}", mcpTool.name(), e.getMessage());
|
||||
log.error(" [MockTool] {} 등록 실패: {}", mcpFunc.name(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("");
|
||||
@@ -213,24 +143,24 @@ public class MockToolAutoRegistrar {
|
||||
headers.setContentType(MediaType.TEXT_PLAIN);
|
||||
|
||||
boolean needsReRegistration = false;
|
||||
for (ToolConfig tool : TOOLS) {
|
||||
for (String toolName : registeredTools) {
|
||||
try {
|
||||
String heartbeatUrl = gatewayUrl + "/registry/heartbeat";
|
||||
restClient.post()
|
||||
.uri(heartbeatUrl)
|
||||
.headers(h -> h.addAll(headers))
|
||||
.body(tool.getName())
|
||||
.body(toolName)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
log.info(" [MockTool] {} Heartbeat 서버 전송 완료", tool.getName());
|
||||
log.info(" [MockTool] {} Heartbeat 전송 완료", toolName);
|
||||
} catch (Exception e) {
|
||||
log.error(" [MockTool] {} Heartbeat 전송 실패: {}", tool.getName(), e.getMessage());
|
||||
log.error(" [MockTool] {} Heartbeat 전송 실패: {}", toolName, e.getMessage());
|
||||
needsReRegistration = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsReRegistration) {
|
||||
log.info(" [MockTool] 하트비트 실패로 인해 전체 툴 재등록을 시도합니다.");
|
||||
log.info(" [MockTool] 하트비트 실패로 재등록을 시도합니다.");
|
||||
registerOnStartup();
|
||||
}
|
||||
}
|
||||
@@ -239,22 +169,22 @@ public class MockToolAutoRegistrar {
|
||||
public void deregisterOnShutdown() {
|
||||
if (serverPort == 8081) return;
|
||||
|
||||
log.info("\n [MockTool] Tool Pod (Port: {}) 종료 중... Gateway에 툴 해제를 요청합니다.", serverPort);
|
||||
log.info("\n [MockTool] Tool Pod (Port: {}) 종료 중...", serverPort);
|
||||
HttpHeaders headers = createHeaders();
|
||||
headers.setContentType(MediaType.TEXT_PLAIN);
|
||||
|
||||
for (ToolConfig tool : TOOLS) {
|
||||
for (String toolName : registeredTools) {
|
||||
try {
|
||||
String deregisterUrl = gatewayUrl + "/registry/deregister";
|
||||
restClient.post()
|
||||
.uri(deregisterUrl)
|
||||
.headers(h -> h.addAll(headers))
|
||||
.body(tool.getName())
|
||||
.body(toolName)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
log.info(" [MockTool] {} 명시적 해제 성공!", tool.getName());
|
||||
log.info(" [MockTool] {} 명시적 해제 성공!", toolName);
|
||||
} catch (Exception e) {
|
||||
log.error(" [MockTool] {} 명시적 해제 실패: {}", tool.getName(), e.getMessage());
|
||||
log.error(" [MockTool] {} 해제 실패: {}", toolName, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,13 +192,13 @@
|
||||
</div>
|
||||
<div class="inline-flex items-center px-3 py-1 rounded-full border border-brand-500/30 bg-brand-500/10 text-brand-400 text-[10px] font-bold uppercase tracking-[0.2em] mb-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-brand-400 mr-2 animate-pulse"></span>
|
||||
Next-Gen Router
|
||||
Next-Gen Router (Flattened)
|
||||
</div>
|
||||
<h1 class="text-5xl font-outfit font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-br from-white via-slate-200 to-slate-500 drop-shadow-sm">
|
||||
AI MCP Gateway
|
||||
</h1>
|
||||
<p class="text-sm text-slate-400 font-medium tracking-wide">
|
||||
엔터프라이즈 환경을 위한 차세대 통합 라우팅 시스템
|
||||
함수 단위로 독립된 엔터프라이즈 통합 라우팅 시스템
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -219,12 +219,12 @@
|
||||
|
||||
<form id="mcpForm" class="space-y-6">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<!-- Target Tool Selection -->
|
||||
<div class="group">
|
||||
<label for="toolName" class="flex items-center text-xs font-bold text-slate-400 mb-2 uppercase tracking-widest transition-colors group-focus-within:text-brand-400">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
대상 툴 (Target Tool)
|
||||
대상 함수 (Function Tool)
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select id="toolName" name="toolName" class="input-premium block w-full pl-4 pr-10 py-3 text-sm rounded-xl appearance-none cursor-pointer font-medium">
|
||||
@@ -235,24 +235,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Target Action Selection -->
|
||||
<div class="group">
|
||||
<label for="toolAction" class="flex items-center text-xs font-bold text-slate-400 mb-2 uppercase tracking-widest transition-colors group-focus-within:text-brand-400">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||
실행 함수 (Action)
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select id="toolAction" name="toolAction" class="input-premium block w-full pl-4 pr-10 py-3 text-sm rounded-xl appearance-none cursor-pointer font-medium">
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-500">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent ID moved to Header -->
|
||||
|
||||
<!-- User Prompt -->
|
||||
<div class="group">
|
||||
@@ -304,11 +287,9 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let toolFunctions = {};
|
||||
let actionPrompts = {};
|
||||
|
||||
const toolSelect = document.getElementById('toolName');
|
||||
const toolActionSelect = document.getElementById('toolAction');
|
||||
|
||||
async function fetchTools() {
|
||||
try {
|
||||
@@ -318,14 +299,12 @@
|
||||
const data = await response.json();
|
||||
|
||||
toolSelect.innerHTML = '';
|
||||
toolFunctions = {};
|
||||
actionPrompts = {};
|
||||
|
||||
const tools = data.result?.tools || [];
|
||||
|
||||
if (tools.length === 0) {
|
||||
toolSelect.innerHTML = '<option value="">등록된 툴이 없습니다</option>';
|
||||
updateActionDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -354,31 +333,13 @@
|
||||
|
||||
groupedTools[group].forEach(tool => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tool.toolName;
|
||||
option.value = tool.toolName; // Now the function name
|
||||
const commType = tool.integrationType ? tool.integrationType : 'HTTP';
|
||||
option.textContent = tool.description ? `[${commType}] ${tool.description} (${tool.toolName})` : `[${commType}] ${tool.toolName}`;
|
||||
option.dataset.schema = JSON.stringify(tool.parametersSchema);
|
||||
option.dataset.prompts = JSON.stringify(tool.actionPrompts || {});
|
||||
optgroup.appendChild(option);
|
||||
|
||||
const enums = tool.parametersSchema?.properties?.action?.enum || [];
|
||||
const descStr = tool.parametersSchema?.properties?.action?.description || "";
|
||||
|
||||
let descMap = {};
|
||||
if(descStr.includes(":")) {
|
||||
const items = descStr.split(":")[1].split(",");
|
||||
items.forEach(item => {
|
||||
const parts = item.trim().split("(");
|
||||
if(parts.length > 1) {
|
||||
descMap[parts[0]] = parts[1].replace(")","");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toolFunctions[tool.toolName] = enums.map(e => {
|
||||
return { value: e, label: descMap[e] || e };
|
||||
});
|
||||
|
||||
if (tool.actionPrompts) {
|
||||
Object.assign(actionPrompts, tool.actionPrompts);
|
||||
}
|
||||
@@ -387,50 +348,19 @@
|
||||
toolSelect.appendChild(optgroup);
|
||||
});
|
||||
|
||||
updateActionDropdown();
|
||||
updatePrompt();
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch tools", err);
|
||||
toolSelect.innerHTML = '<option value="">데이터 로드 실패</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateActionDropdown() {
|
||||
function updatePrompt() {
|
||||
const toolName = toolSelect.value;
|
||||
const actions = toolFunctions[toolName] || [];
|
||||
|
||||
toolActionSelect.innerHTML = '';
|
||||
|
||||
if (actions.length === 0) {
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.value = 'default';
|
||||
defaultOption.textContent = '기본 동작';
|
||||
toolActionSelect.appendChild(defaultOption);
|
||||
document.getElementById('userPrompt').value = "";
|
||||
} else {
|
||||
actions.forEach(action => {
|
||||
const option = document.createElement('option');
|
||||
option.value = action.value;
|
||||
option.textContent = action.label;
|
||||
toolActionSelect.appendChild(option);
|
||||
});
|
||||
|
||||
updatePrompt(actions[0].value, toolName);
|
||||
}
|
||||
document.getElementById('userPrompt').value = actionPrompts[toolName] || "작업을 실행해주세요.";
|
||||
}
|
||||
|
||||
function updatePrompt(actionValue, toolName) {
|
||||
let promptKey = actionValue;
|
||||
if (toolName === "customer_info_tool" && actionValue === "detail") {
|
||||
promptKey = "detail_cust";
|
||||
}
|
||||
document.getElementById('userPrompt').value = actionPrompts[promptKey] || "작업을 실행해주세요.";
|
||||
}
|
||||
|
||||
toolSelect.addEventListener('change', updateActionDropdown);
|
||||
|
||||
toolActionSelect.addEventListener('change', function() {
|
||||
updatePrompt(this.value, toolSelect.value);
|
||||
});
|
||||
toolSelect.addEventListener('change', updatePrompt);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', fetchTools);
|
||||
|
||||
@@ -454,10 +384,9 @@
|
||||
params: {
|
||||
name: document.getElementById('toolName').value,
|
||||
arguments: {
|
||||
action: document.getElementById('toolAction').value,
|
||||
traceId: "MCP-REQ-" + Math.floor(Math.random() * 90000 + 10000),
|
||||
agentId: document.getElementById('agentId').value,
|
||||
userPrompt: document.getElementById('userPrompt').value || document.getElementById('userPrompt').placeholder
|
||||
// arguments는 이제 순수 DTO이므로 테스트 UI에서는 빈 객체 또는 Mock 데이터를 보냅니다.
|
||||
// 실제 AI Agent는 parameterSchema를 보고 채워줍니다.
|
||||
ui_test_call: true
|
||||
}
|
||||
},
|
||||
id: Math.floor(Math.random() * 10000)
|
||||
@@ -467,14 +396,19 @@
|
||||
const startTime = Date.now();
|
||||
const response = await fetch('/mcp/api/v1/tools/call', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-KEY': 'SHINHAN_MCP_TEST_KEY_9999' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': 'SHINHAN_MCP_TEST_KEY_9999',
|
||||
'X-Trace-Id': "MCP-REQ-" + Math.floor(Math.random() * 90000 + 10000),
|
||||
'X-Agent-Id': document.getElementById('agentId').value,
|
||||
'X-User-Prompt': encodeURIComponent(document.getElementById('userPrompt').value || document.getElementById('userPrompt').placeholder)
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
// Dark theme optimized syntax highlighting
|
||||
let formattedJson = JSON.stringify(data, null, 4)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||
@@ -517,7 +451,7 @@
|
||||
resultContainer.classList.remove('hidden');
|
||||
void resultContainer.offsetWidth; // Force reflow
|
||||
resultContainer.classList.remove('opacity-0', 'translate-y-8');
|
||||
}, 800); // slightly longer loading effect for premium feel
|
||||
}, 800);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user