forked from kimhyungsik/ax_hub_mcp_tool
툴 검색 시 interface -> 구현체
This commit is contained in:
@@ -2,9 +2,11 @@ package io.shinhanlife.dap.lib.mcp;
|
|||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
|
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springaicommunity.mcp.annotation.McpTool;
|
import org.springaicommunity.mcp.annotation.McpTool;
|
||||||
@@ -14,6 +16,8 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,26 +35,46 @@ public class McpToolMethodRegistry {
|
|||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
Map<String, RegisteredTool> discovered = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
|
Map<String, RegisteredTool> discovered = new LinkedHashMap<>();
|
||||||
for (Object bean : applicationContext.getBeansOfType(Object.class).values()) {
|
for (Object bean : applicationContext.getBeansOfType(Object.class).values()) {
|
||||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||||
for (Method declaredMethod : targetClass.getDeclaredMethods()) {
|
for (Method declaredMethod : targetClass.getDeclaredMethods()) {
|
||||||
McpTool annotation = AnnotationUtils.findAnnotation(declaredMethod, McpTool.class);
|
|
||||||
if (annotation == null) {
|
/*
|
||||||
|
* Tool Annotation 검색
|
||||||
|
*
|
||||||
|
* 1순위 : Interface
|
||||||
|
* 2순위 : 구현체
|
||||||
|
*/
|
||||||
|
ToolAnnotationMetadata metadata = findToolAnnotationMetadata(targetClass, declaredMethod);
|
||||||
|
|
||||||
|
if (metadata == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisteredTool tool = new RegisteredTool(
|
McpTool annotation = metadata.mcpTool();
|
||||||
bean,
|
GrowToolHint hint = metadata.growToolHint();
|
||||||
findInvocableMethod(bean, declaredMethod),
|
|
||||||
annotation,
|
/*
|
||||||
AnnotationUtils.findAnnotation(declaredMethod, GrowToolHint.class));
|
* Annotation은 Interface에서 가져올 수 있지만
|
||||||
|
* 실제 호출 Method는 구현체 Method를 사용한다.
|
||||||
|
*/
|
||||||
|
RegisteredTool tool = new RegisteredTool(bean, findInvocableMethod(bean, declaredMethod), annotation, hint);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 원래 Tool Name 등록
|
||||||
|
*/
|
||||||
register(discovered, annotation.name(), tool);
|
register(discovered, annotation.name(), tool);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* namespace alias 등록
|
||||||
|
*/
|
||||||
registerNamespaceAlias(discovered, annotation.name(), tool);
|
registerNamespaceAlias(discovered, annotation.name(), tool);
|
||||||
|
|
||||||
|
log.info("[Tool Registry] Registered Tool: {} -> {}#{}", annotation.name(), targetClass.getSimpleName(), declaredMethod.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tools = Map.copyOf(discovered);
|
tools = Map.copyOf(discovered);
|
||||||
log.info("[Tool Registry] {} executable tool names cached", tools.size());
|
log.info("[Tool Registry] {} executable tool names cached", tools.size());
|
||||||
}
|
}
|
||||||
@@ -59,22 +83,111 @@ public class McpToolMethodRegistry {
|
|||||||
return tools.get(toolName);
|
return tools.get(toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerNamespaceAlias(Map<String, RegisteredTool> discovered, String toolName,
|
/**
|
||||||
RegisteredTool tool) {
|
* @McpTool / @GrowToolHint 검색
|
||||||
|
* <p>
|
||||||
|
* 우선순위
|
||||||
|
* <p>
|
||||||
|
* 1. Interface Method
|
||||||
|
* 2. 구현체 Method
|
||||||
|
*/
|
||||||
|
private ToolAnnotationMetadata findToolAnnotationMetadata(Class<?> targetClass, Method declaredMethod) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ==========================================
|
||||||
|
* STEP 1.
|
||||||
|
* Interface부터 검색
|
||||||
|
* ==========================================
|
||||||
|
*/
|
||||||
|
for (Class<?> interfaceClass : ClassUtils.getAllInterfacesForClassAsSet(targetClass)) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 구현체 Method와 동일한 Signature를 가진
|
||||||
|
* Interface Method를 찾는다.
|
||||||
|
*/
|
||||||
|
Method interfaceMethod = ReflectionUtils.findMethod(interfaceClass, declaredMethod.getName(), declaredMethod.getParameterTypes());
|
||||||
|
|
||||||
|
if (interfaceMethod == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Interface의 @McpTool 검색
|
||||||
|
*/
|
||||||
|
McpTool mcpTool = AnnotationUtils.findAnnotation(interfaceMethod, McpTool.class);
|
||||||
|
if (mcpTool == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @McpTool을 Interface에서 발견했다면
|
||||||
|
* @GrowToolHint 역시 같은 Interface Method에서 가져온다.
|
||||||
|
*/
|
||||||
|
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(interfaceMethod, GrowToolHint.class);
|
||||||
|
log.debug("[Tool Registry] Interface @McpTool found: " + "{}#{} -> {}", interfaceClass.getSimpleName(), interfaceMethod.getName(), mcpTool.name());
|
||||||
|
return new ToolAnnotationMetadata(mcpTool, growToolHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ==========================================
|
||||||
|
* STEP 2.
|
||||||
|
* Interface에서 발견되지 않은 경우
|
||||||
|
* 구현체 Method 검색
|
||||||
|
* ==========================================
|
||||||
|
*/
|
||||||
|
McpTool mcpTool = AnnotationUtils.findAnnotation(declaredMethod, McpTool.class);
|
||||||
|
|
||||||
|
if (mcpTool == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(declaredMethod, GrowToolHint.class);
|
||||||
|
log.debug("[Tool Registry] Implementation @McpTool found: " + "{}#{} -> {}", targetClass.getSimpleName(), declaredMethod.getName(), mcpTool.name());
|
||||||
|
return new ToolAnnotationMetadata(mcpTool, growToolHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace가 존재하는 경우
|
||||||
|
* <p>
|
||||||
|
* ex)
|
||||||
|
* <p>
|
||||||
|
* pct_notice_list
|
||||||
|
* <p>
|
||||||
|
* namespace = sys
|
||||||
|
* <p>
|
||||||
|
* sys_pct_notice_list
|
||||||
|
* <p>
|
||||||
|
* 두 이름으로 동일 Tool을 조회할 수 있도록 등록
|
||||||
|
*/
|
||||||
|
private void registerNamespaceAlias(Map<String, RegisteredTool> discovered, String toolName, RegisteredTool tool) {
|
||||||
|
|
||||||
String namespace = mcpProperties.getNamespace();
|
String namespace = mcpProperties.getNamespace();
|
||||||
|
|
||||||
if (StringUtils.hasText(namespace)) {
|
if (StringUtils.hasText(namespace)) {
|
||||||
register(discovered, namespace + "_" + toolName, tool);
|
register(discovered, namespace + "_" + toolName, tool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool Registry 등록
|
||||||
|
*/
|
||||||
private void register(Map<String, RegisteredTool> discovered, String toolName, RegisteredTool tool) {
|
private void register(Map<String, RegisteredTool> discovered, String toolName, RegisteredTool tool) {
|
||||||
|
|
||||||
RegisteredTool existing = discovered.putIfAbsent(toolName, tool);
|
RegisteredTool existing = discovered.putIfAbsent(toolName, tool);
|
||||||
|
|
||||||
if (existing != null && existing != tool) {
|
if (existing != null && existing != tool) {
|
||||||
throw new IllegalStateException("Duplicate @McpTool name: " + toolName);
|
throw new IllegalStateException("Duplicate @McpTool name: " + toolName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실제 호출 가능한 Method 확보
|
||||||
|
* <p>
|
||||||
|
* Annotation 검색은 Interface에서 하더라도
|
||||||
|
* Tool 실행은 구현체 Bean을 대상으로 수행해야 한다.
|
||||||
|
*/
|
||||||
private Method findInvocableMethod(Object bean, Method declaredMethod) {
|
private Method findInvocableMethod(Object bean, Method declaredMethod) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return bean.getClass().getMethod(declaredMethod.getName(), declaredMethod.getParameterTypes());
|
return bean.getClass().getMethod(declaredMethod.getName(), declaredMethod.getParameterTypes());
|
||||||
} catch (NoSuchMethodException ignored) {
|
} catch (NoSuchMethodException ignored) {
|
||||||
@@ -82,6 +195,15 @@ public class McpToolMethodRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation 검색 결과
|
||||||
|
*/
|
||||||
|
private record ToolAnnotationMetadata(McpTool mcpTool, GrowToolHint growToolHint) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실제 실행 Registry 정보
|
||||||
|
*/
|
||||||
public record RegisteredTool(Object bean, Method method, McpTool annotation, GrowToolHint hint) {
|
public record RegisteredTool(Object bean, Method method, McpTool annotation, GrowToolHint hint) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,32 +15,36 @@ package io.shinhanlife.dap.lib.mcp;
|
|||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springaicommunity.mcp.annotation.McpTool;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
|
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
|
||||||
import io.shinhanlife.dap.lib.metadata.ToolDefinition;
|
import io.shinhanlife.dap.lib.metadata.ToolDefinition;
|
||||||
import io.shinhanlife.dap.lib.metadata.ToolDefinitionRepository;
|
import io.shinhanlife.dap.lib.metadata.ToolDefinitionRepository;
|
||||||
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springaicommunity.mcp.annotation.McpTool;
|
||||||
import org.springframework.aop.support.AopUtils;
|
import org.springframework.aop.support.AopUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -54,9 +58,8 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
private final ToolDefinitionRepository toolDefinitionRepository;
|
private final ToolDefinitionRepository toolDefinitionRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper,
|
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper, McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver, @Nullable ToolDefinitionRepository toolDefinitionRepository) {
|
||||||
McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver,
|
|
||||||
@Nullable ToolDefinitionRepository toolDefinitionRepository) {
|
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.mcpProperties = mcpProperties;
|
this.mcpProperties = mcpProperties;
|
||||||
@@ -64,8 +67,8 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
this.toolDefinitionRepository = toolDefinitionRepository;
|
this.toolDefinitionRepository = toolDefinitionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper,
|
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper, McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver) {
|
||||||
McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver) {
|
|
||||||
this(applicationContext, objectMapper, mcpProperties, toolSchemaResolver, null);
|
this(applicationContext, objectMapper, mcpProperties, toolSchemaResolver, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,94 +88,187 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void scanAndBuildMetadata() {
|
private void scanAndBuildMetadata() {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 재호출될 가능성을 고려해서
|
||||||
|
* 기존 Metadata 제거
|
||||||
|
*/
|
||||||
|
allScannedTools.clear();
|
||||||
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
||||||
for (Object bean : allBeans.values()) {
|
for (Object bean : allBeans.values()) {
|
||||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||||
|
|
||||||
for (Method method : targetClass.getDeclaredMethods()) {
|
for (Method method : targetClass.getDeclaredMethods()) {
|
||||||
McpTool functionAnnotation = AnnotationUtils.findAnnotation(method, McpTool.class);
|
/*
|
||||||
GrowToolHint hintAnnotation = AnnotationUtils.findAnnotation(method, GrowToolHint.class);
|
* Annotation 검색
|
||||||
|
*
|
||||||
|
* 1순위 : Interface
|
||||||
|
* 2순위 : 구현체
|
||||||
|
*/
|
||||||
|
ToolAnnotationMetadata annotationMetadata = findToolAnnotationMetadata(targetClass, method);
|
||||||
|
|
||||||
if (functionAnnotation != null) {
|
if (annotationMetadata == null) {
|
||||||
String rawSubToolName = functionAnnotation.name();
|
continue;
|
||||||
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
|
||||||
? mcpProperties.getNamespace() + "_" + rawSubToolName
|
|
||||||
: rawSubToolName;
|
|
||||||
// register 값은 외부 Gateway 전송이 아니라 Tool 메타데이터 호환 필드로만 유지합니다.
|
|
||||||
boolean isRegister = hintAnnotation == null || hintAnnotation.register();
|
|
||||||
|
|
||||||
ToolMetadata meta = new ToolMetadata();
|
|
||||||
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
|
|
||||||
String displayName = functionAnnotation.title().isEmpty() ? functionAnnotation.name() : functionAnnotation.title();
|
|
||||||
meta.setDisplayName(displayName);
|
|
||||||
meta.setName(subToolName);
|
|
||||||
meta.setSemver("1.0.0");
|
|
||||||
meta.setTimeoutMillis(5000L);
|
|
||||||
meta.setEnabled(true);
|
|
||||||
meta.setDescription(functionAnnotation.description());
|
|
||||||
meta.setCategoryKey(hintAnnotation == null || hintAnnotation.categoryKey().isBlank()
|
|
||||||
? "common" : hintAnnotation.categoryKey());
|
|
||||||
meta.setIntegrationType("REST");
|
|
||||||
meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : "");
|
|
||||||
meta.setPodUrl(podUrl);
|
|
||||||
meta.setModuleName(applicationName);
|
|
||||||
meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName);
|
|
||||||
|
|
||||||
meta.setVisible(true);
|
|
||||||
meta.setIsRegistered(isRegister);
|
|
||||||
meta.setRequiresApproval(hintAnnotation != null && hintAnnotation.requiresApproval());
|
|
||||||
|
|
||||||
// extract standard hints from @McpTool.annotations()
|
|
||||||
McpTool.McpAnnotations ann = functionAnnotation.annotations();
|
|
||||||
if (ann != null) {
|
|
||||||
meta.setReadOnlyHint(ann.readOnlyHint());
|
|
||||||
meta.setDestructiveHint(ann.destructiveHint());
|
|
||||||
meta.setIdempotentHint(ann.idempotentHint());
|
|
||||||
meta.setOpenWorldHint(ann.openWorldHint());
|
|
||||||
} else {
|
|
||||||
meta.setReadOnlyHint(false);
|
|
||||||
meta.setDestructiveHint(true);
|
|
||||||
meta.setIdempotentHint(false);
|
|
||||||
meta.setOpenWorldHint(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> prompts = new HashMap<>();
|
|
||||||
meta.setActionPrompts(prompts);
|
|
||||||
|
|
||||||
if (method.getParameterCount() > 0) {
|
|
||||||
try {
|
|
||||||
Class<?> paramType = method.getParameterTypes()[0];
|
|
||||||
// TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction
|
|
||||||
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
|
|
||||||
meta.setParametersSchema(finalSchema);
|
|
||||||
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(
|
|
||||||
functionAnnotation, method.getReturnType(), hintAnnotation);
|
|
||||||
if (!outputSchema.isEmpty()) {
|
|
||||||
meta.setOutputSchema(outputSchema);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to generate schema for {}", subToolName, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enrichWithDefinition(meta, rawSubToolName, hintAnnotation);
|
|
||||||
|
|
||||||
allScannedTools.add(meta);
|
|
||||||
log.info(" [ToolScanner] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
McpTool functionAnnotation = annotationMetadata.mcpTool();
|
||||||
|
GrowToolHint hintAnnotation = annotationMetadata.growToolHint();
|
||||||
|
|
||||||
|
String rawSubToolName = functionAnnotation.name();
|
||||||
|
/*
|
||||||
|
* Namespace 적용
|
||||||
|
*/
|
||||||
|
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() ? mcpProperties.getNamespace() + "_" + rawSubToolName : rawSubToolName;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* register 값은 외부 Gateway 전송이 아니라
|
||||||
|
* Tool 메타데이터 호환 필드로 유지
|
||||||
|
*/
|
||||||
|
boolean isRegister = hintAnnotation == null || hintAnnotation.register();
|
||||||
|
ToolMetadata meta = new ToolMetadata();
|
||||||
|
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
|
||||||
|
String displayName = functionAnnotation.title().isEmpty() ? functionAnnotation.name() : functionAnnotation.title();
|
||||||
|
meta.setDisplayName(displayName);
|
||||||
|
meta.setName(subToolName);
|
||||||
|
meta.setSemver("1.0.0");
|
||||||
|
meta.setTimeoutMillis(5000L);
|
||||||
|
meta.setEnabled(true);
|
||||||
|
meta.setDescription(functionAnnotation.description());
|
||||||
|
|
||||||
|
meta.setCategoryKey(hintAnnotation == null || hintAnnotation.categoryKey().isBlank() ? "common" : hintAnnotation.categoryKey());
|
||||||
|
meta.setIntegrationType("REST");
|
||||||
|
meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : "");
|
||||||
|
|
||||||
|
meta.setPodUrl(podUrl);
|
||||||
|
meta.setModuleName(applicationName);
|
||||||
|
|
||||||
|
meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName);
|
||||||
|
|
||||||
|
meta.setVisible(true);
|
||||||
|
meta.setIsRegistered(isRegister);
|
||||||
|
meta.setRequiresApproval(hintAnnotation != null && hintAnnotation.requiresApproval());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MCP standard hint
|
||||||
|
*/
|
||||||
|
McpTool.McpAnnotations ann = functionAnnotation.annotations();
|
||||||
|
|
||||||
|
if (ann != null) {
|
||||||
|
meta.setReadOnlyHint(ann.readOnlyHint());
|
||||||
|
meta.setDestructiveHint(ann.destructiveHint());
|
||||||
|
meta.setIdempotentHint(ann.idempotentHint());
|
||||||
|
meta.setOpenWorldHint(ann.openWorldHint());
|
||||||
|
} else {
|
||||||
|
meta.setReadOnlyHint(false);
|
||||||
|
meta.setDestructiveHint(true);
|
||||||
|
meta.setIdempotentHint(false);
|
||||||
|
meta.setOpenWorldHint(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> prompts = new HashMap<>();
|
||||||
|
|
||||||
|
meta.setActionPrompts(prompts);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Request / Response Schema 생성
|
||||||
|
*
|
||||||
|
* Annotation은 Interface에서 읽어오지만
|
||||||
|
* 실제 Method의 Parameter/Return Type은
|
||||||
|
* 구현체 Method를 기준으로 사용
|
||||||
|
*/
|
||||||
|
if (method.getParameterCount() > 0) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
Class<?> paramType = method.getParameterTypes()[0];
|
||||||
|
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
|
||||||
|
meta.setParametersSchema(finalSchema);
|
||||||
|
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(functionAnnotation, method.getReturnType(), hintAnnotation);
|
||||||
|
if (!outputSchema.isEmpty()) {
|
||||||
|
meta.setOutputSchema(outputSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
|
||||||
|
log.error("Failed to generate schema for {}", subToolName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* YML Tool Definition 병합
|
||||||
|
*/
|
||||||
|
enrichWithDefinition(meta, rawSubToolName, hintAnnotation);
|
||||||
|
allScannedTools.add(meta);
|
||||||
|
log.info(" [ToolScanner] 도구 메타데이터 생성: " + "name={}, class={}#{}, " + "isRegistered={}", subToolName, targetClass.getSimpleName(), method.getName(), isRegister);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(" [ToolScanner] 총 {}개 Tool 메타데이터 생성 완료", allScannedTools.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @McpTool / @GrowToolHint 검색
|
||||||
|
* <p>
|
||||||
|
* 우선순위
|
||||||
|
* <p>
|
||||||
|
* 1. Interface
|
||||||
|
* 2. Implementation
|
||||||
|
*/
|
||||||
|
private ToolAnnotationMetadata findToolAnnotationMetadata(Class<?> targetClass, Method method) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================
|
||||||
|
* STEP 1.
|
||||||
|
* Interface부터 검색
|
||||||
|
* ========================================
|
||||||
|
*/
|
||||||
|
for (Class<?> interfaceClass : ClassUtils.getAllInterfacesForClassAsSet(targetClass)) {
|
||||||
|
|
||||||
|
Method interfaceMethod = ReflectionUtils.findMethod(interfaceClass, method.getName(), method.getParameterTypes());
|
||||||
|
if (interfaceMethod == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
McpTool mcpTool = AnnotationUtils.findAnnotation(interfaceMethod, McpTool.class);
|
||||||
|
if (mcpTool == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(interfaceMethod, GrowToolHint.class);
|
||||||
|
log.debug(" [ToolScanner] Interface @McpTool 발견: " + "{}#{} -> {}", interfaceClass.getSimpleName(), interfaceMethod.getName(), mcpTool.name());
|
||||||
|
|
||||||
|
return new ToolAnnotationMetadata(mcpTool, growToolHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================
|
||||||
|
* STEP 2.
|
||||||
|
* Interface에서 찾지 못한 경우에만
|
||||||
|
* 구현체 Method 검색
|
||||||
|
* ========================================
|
||||||
|
*/
|
||||||
|
McpTool mcpTool = AnnotationUtils.findAnnotation(method, McpTool.class);
|
||||||
|
|
||||||
|
if (mcpTool == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(method, GrowToolHint.class);
|
||||||
|
|
||||||
|
log.debug(" [ToolScanner] Implementation @McpTool 발견: " + "{}#{} -> {}", targetClass.getSimpleName(), method.getName(), mcpTool.name());
|
||||||
|
|
||||||
|
return new ToolAnnotationMetadata(mcpTool, growToolHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enrichWithDefinition(ToolMetadata meta, String rawToolName, GrowToolHint hintAnnotation) {
|
private void enrichWithDefinition(ToolMetadata meta, String rawToolName, GrowToolHint hintAnnotation) {
|
||||||
|
|
||||||
if (toolDefinitionRepository == null) {
|
if (toolDefinitionRepository == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toolDefinitionRepository.findByName(rawToolName)
|
|
||||||
.ifPresent(definition -> applyDefinition(meta, definition, hintAnnotation));
|
toolDefinitionRepository.findByName(rawToolName).ifPresent(definition -> applyDefinition(meta, definition, hintAnnotation));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyDefinition(ToolMetadata meta, ToolDefinition definition, GrowToolHint hintAnnotation) {
|
private void applyDefinition(ToolMetadata meta, ToolDefinition definition, GrowToolHint hintAnnotation) {
|
||||||
|
|
||||||
meta.setDisplayName(definition.displayName());
|
meta.setDisplayName(definition.displayName());
|
||||||
meta.setSemver(definition.version());
|
meta.setSemver(definition.version());
|
||||||
meta.setCategoryKey(definition.categoryKey());
|
meta.setCategoryKey(definition.categoryKey());
|
||||||
@@ -187,17 +283,26 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
meta.setDestructiveHint(definition.destructive());
|
meta.setDestructiveHint(definition.destructive());
|
||||||
meta.setIdempotentHint(definition.idempotent());
|
meta.setIdempotentHint(definition.idempotent());
|
||||||
boolean explicitInputResource = hintAnnotation != null && !hintAnnotation.inputSchemaResource().isBlank();
|
boolean explicitInputResource = hintAnnotation != null && !hintAnnotation.inputSchemaResource().isBlank();
|
||||||
|
|
||||||
boolean explicitOutputResource = hintAnnotation != null && !hintAnnotation.outputSchemaResource().isBlank();
|
boolean explicitOutputResource = hintAnnotation != null && !hintAnnotation.outputSchemaResource().isBlank();
|
||||||
|
|
||||||
if (!explicitInputResource) {
|
if (!explicitInputResource) {
|
||||||
meta.setParametersSchema(definition.parametersSchema());
|
meta.setParametersSchema(definition.parametersSchema());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!explicitOutputResource && definition.outputSchema() != null && !definition.outputSchema().isEmpty()) {
|
if (!explicitOutputResource && definition.outputSchema() != null && !definition.outputSchema().isEmpty()) {
|
||||||
meta.setOutputSchema(definition.outputSchema());
|
meta.setOutputSchema(definition.outputSchema());
|
||||||
}
|
}
|
||||||
|
|
||||||
meta.setTags(definition.tags());
|
meta.setTags(definition.tags());
|
||||||
meta.setMciServiceId(definition.legacyInterfaceId());
|
meta.setMciServiceId(definition.legacyInterfaceId());
|
||||||
meta.setRequiredEnvKeys(definition.requiredEnvKeys());
|
meta.setRequiredEnvKeys(definition.requiredEnvKeys());
|
||||||
meta.setOwnerOrg(definition.ownerOrg());
|
meta.setOwnerOrg(definition.ownerOrg());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation 검색 결과
|
||||||
|
*/
|
||||||
|
private record ToolAnnotationMetadata(McpTool mcpTool, GrowToolHint growToolHint) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user