diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java index 1a42b32d7..04fc35c4a 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java @@ -25,6 +25,7 @@ import java.lang.annotation.Target; public @interface ToolHint { boolean register() default false; boolean requiresApproval() default false; + String group() default ""; String mappingId() default ""; String inputSchemaResource() default ""; String outputSchemaResource() default ""; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java index e80d90a46..af2605455 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java @@ -107,7 +107,8 @@ public class ToolRegistryHeartbeatSender { meta.setTimeoutMillis(5000L); meta.setEnabled(true); meta.setDescription(functionAnnotation.description()); - meta.setCategoryKey("default"); + meta.setCategoryKey(hintAnnotation == null || hintAnnotation.group().isBlank() + ? "default" : hintAnnotation.group()); meta.setIntegrationType("REST"); meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : ""); meta.setPodUrl(podUrl); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java index b1a0796f9..290c3b5a1 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java @@ -1,123 +1,140 @@ package io.shinhanlife.dap.lib.util; - -/** - * @package io.shinhanlife.dap.lib.util - * @className ToolSourceUpdater - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Comparator; -public class ToolSourceUpdater { +/** Updates MCP SDK and project-owned metadata in a generated tool source file. */ +public final class ToolSourceUpdater { - public static void updateToolSource(String toolName, String domainGroup, String description, boolean register, Boolean requiresApproval) throws Exception { - // 1. Find all *UseCase.java files in dap-was-* directories - String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); - Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); - - List javaFiles; - try (Stream paths = Files.walk(rootDir)) { - javaFiles = paths - .filter(Files::isRegularFile) - .filter(p -> p.toString().endsWith("UseCase.java")) - .filter(p -> p.toString().contains("dap-was-") || p.toString().contains("axhub-tool-")) - .collect(Collectors.toList()); - } + private ToolSourceUpdater() { + } - Path targetFile = null; - String content = null; - - // 2. Find the specific file for the tool - String functionName = toolName; - if (toolName.contains("_")) { - functionName = toolName.substring(toolName.indexOf("_") + 1); - } - - Pattern namePattern = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"", Pattern.DOTALL); - Pattern namePattern2 = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(functionName) + "\"", Pattern.DOTALL); - - for (Path path : javaFiles) { - String text = Files.readString(path); - if (namePattern.matcher(text).find()) { - targetFile = path; - content = text; - break; - } else if (namePattern2.matcher(text).find()) { - targetFile = path; - content = text; - toolName = functionName; // Use baseName for subsequent replacements - break; - } - } + public static void updateToolSource(String toolName, String domainGroup, String description, + boolean register, Boolean requiresApproval) throws IOException { + String configuredSourceDirectory = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDirectory = configuredSourceDirectory == null || configuredSourceDirectory.isBlank() + ? Paths.get(".") : Paths.get(configuredSourceDirectory); + updateToolSource(rootDirectory, toolName, domainGroup, description, register, requiresApproval); + } + static void updateToolSource(Path rootDirectory, String toolName, String domainGroup, String description, + boolean register, Boolean requiresApproval) throws IOException { + Path targetFile = findToolSource(rootDirectory, toolName); if (targetFile == null) { - throw new Exception("소스 코드를 찾을 수 없습니다: " + toolName); + throw new IllegalArgumentException("Tool source not found: " + toolName); } - // 3. Update @McpTool group - if (domainGroup != null && !domainGroup.trim().isEmpty()) { - Pattern groupPattern = Pattern.compile("(@McpTool\\s*\\([^)]*group\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL); - Matcher groupMatcher = groupPattern.matcher(content); - if (groupMatcher.find()) { - content = groupMatcher.replaceFirst("$1" + domainGroup + "$3"); - } - } - - // 4. Update @McpFunction description - if (description != null) { - Pattern funcPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*description\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL); - Matcher funcMatcher = funcPattern.matcher(content); - if (funcMatcher.find()) { - content = funcMatcher.replaceFirst("$1" + description.replace("\\", "\\\\").replace("$", "\\\\$") + "$3"); - } - } - - // 5. Update register flag - Pattern regPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*register\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL); - Matcher regMatcher = regPattern.matcher(content); - if (regMatcher.find()) { - content = regMatcher.replaceFirst("$1" + register + "$3"); - } else { - Pattern addRegPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL); - Matcher addRegMatcher = addRegPattern.matcher(content); - if (addRegMatcher.find()) { - content = addRegMatcher.replaceFirst("$1, register = " + register); - } - } - - // 5.5 Update requiresApproval flag - if (requiresApproval != null) { - Pattern appPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*requiresApproval\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL); - Matcher appMatcher = appPattern.matcher(content); - if (appMatcher.find()) { - content = appMatcher.replaceFirst("$1" + requiresApproval + "$3"); - } else { - Pattern addAppPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL); - Matcher addAppMatcher = addAppPattern.matcher(content); - if (addAppMatcher.find()) { - content = addAppMatcher.replaceFirst("$1, requiresApproval = " + requiresApproval); - } - } - } - - // 6. Write back to file + String content = Files.readString(targetFile); + content = updateMcpTool(content, toolName, description); + content = updateToolHint(content, domainGroup, register, requiresApproval); Files.writeString(targetFile, content); } + + private static Path findToolSource(Path rootDirectory, String toolName) throws IOException { + try (var paths = Files.walk(rootDirectory)) { + return paths.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith("UseCase.java")) + .filter(path -> path.toString().contains("dap-was-") || path.toString().contains("axhub-tool-")) + .sorted(Comparator.naturalOrder()) + .filter(path -> containsMcpTool(path, toolName)) + .findFirst() + .orElse(null); + } + } + + private static boolean containsMcpTool(Path path, String toolName) { + try { + return annotationArguments(Files.readString(path), "McpTool", toolName) != null; + } catch (IOException exception) { + throw new IllegalStateException("Failed to read tool source: " + path, exception); + } + } + + private static String updateMcpTool(String content, String toolName, String description) { + AnnotationRange range = annotationArguments(content, "McpTool", toolName); + if (range == null) { + throw new IllegalArgumentException("McpTool declaration not found: " + toolName); + } + return description == null ? content : replaceAttribute(content, range, "description", quote(description)); + } + + private static String updateToolHint(String content, String domainGroup, boolean register, Boolean requiresApproval) { + AnnotationRange range = annotationArguments(content, "ToolHint", null); + if (range == null) { + throw new IllegalArgumentException("ToolHint declaration not found next to McpTool"); + } + String updated = replaceAttribute(content, range, "register", Boolean.toString(register)); + range = annotationArguments(updated, "ToolHint", null); + if (requiresApproval != null) { + updated = replaceAttribute(updated, range, "requiresApproval", Boolean.toString(requiresApproval)); + range = annotationArguments(updated, "ToolHint", null); + } + if (domainGroup != null && !domainGroup.isBlank()) { + updated = replaceAttribute(updated, range, "group", quote(domainGroup)); + } + return updated; + } + + private static String replaceAttribute(String content, AnnotationRange range, String attribute, String value) { + String arguments = content.substring(range.argumentsStart(), range.argumentsEnd()); + String pattern = "\\b" + attribute + "\\s*=\\s*(?:true|false|\\\"(?:\\\\.|[^\\\"\\\\])*\\\")"; + String replacement = arguments.replaceFirst(pattern, attribute + " = " + value); + if (replacement.equals(arguments)) { + replacement = arguments.isBlank() ? attribute + " = " + value : arguments + ", " + attribute + " = " + value; + } + return content.substring(0, range.argumentsStart()) + replacement + content.substring(range.argumentsEnd()); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + + private static AnnotationRange annotationArguments(String content, String annotationName, String toolName) { + int offset = content.indexOf("@" + annotationName); + while (offset >= 0) { + int openingParenthesis = content.indexOf('(', offset); + int closingParenthesis = findAnnotationEnd(content, openingParenthesis); + if (openingParenthesis < 0 || closingParenthesis < 0) { + return null; + } + AnnotationRange range = new AnnotationRange(openingParenthesis + 1, closingParenthesis); + if (toolName == null || content.substring(range.argumentsStart(), range.argumentsEnd()) + .matches("(?s).*\\bname\\s*=\\s*\\\"" + java.util.regex.Pattern.quote(toolName) + "\\\".*")) { + return range; + } + offset = content.indexOf("@" + annotationName, closingParenthesis + 1); + } + return null; + } + + private static int findAnnotationEnd(String content, int openingParenthesis) { + int depth = 0; + boolean inString = false; + boolean escaped = false; + for (int index = openingParenthesis; index < content.length(); index++) { + char character = content.charAt(index); + if (inString) { + if (escaped) { + escaped = false; + } else if (character == '\\') { + escaped = true; + } else if (character == '\"') { + inString = false; + } + } else if (character == '\"') { + inString = true; + } else if (character == '(') { + depth++; + } else if (character == ')' && --depth == 0) { + return index; + } + } + return -1; + } + + private record AnnotationRange(int argumentsStart, int argumentsEnd) { + } } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java index f491fd875..89aea24d7 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java @@ -15,7 +15,7 @@ import java.util.regex.Pattern; /** * @package io.shinhanlife.dap.lib.validation * @className McpToolNameValidator - * @description Validates unique MCP function names across tool modules + * @description Validates unique MCP SDK tool names across tool modules * @author 0986406 * @create 2026.07.27 *
@@ -88,7 +88,7 @@ public final class McpToolNameValidator {
             throw new UncheckedIOException("Failed to read " + source, exception);
         }
 
-        int annotationOffset = content.indexOf("@McpFunction");
+        int annotationOffset = content.indexOf("@McpTool");
         while (annotationOffset >= 0) {
             int openingParenthesis = content.indexOf('(', annotationOffset);
             int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
@@ -103,7 +103,7 @@ public final class McpToolNameValidator {
                 declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>())
                         .add(new ToolDeclaration(moduleName, source, line));
             }
-            annotationOffset = content.indexOf("@McpFunction", closingParenthesis + 1);
+            annotationOffset = content.indexOf("@McpTool", closingParenthesis + 1);
         }
     }
 
diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
index 04b19cae4..166c7fad7 100644
--- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
+++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
@@ -8,26 +8,17 @@ import java.lang.reflect.Method;
 import java.util.List;
 import java.util.Map;
 import org.junit.jupiter.api.Test;
+import org.springframework.ai.mcp.annotation.McpTool;
 
 class ToolSchemaResolverTest {
 
     private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper());
 
-    @Test
-    void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
-        Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
-
-        Map schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
-
-        assertEquals(false, schema.get("additionalProperties"));
-        assertTrue(!properties(schema).containsKey("differentField"));
-    }
-
     @Test
     void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
         Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
 
-        Map schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
+        Map schema = resolver.resolve(method.getAnnotation(McpTool.class), null, AutomaticRequest.class);
 
         assertTrue(properties(schema).containsKey("differentField"));
     }
@@ -35,7 +26,7 @@ class ToolSchemaResolverTest {
     @Test
     void resolvesExplicitOutputSchema() throws Exception {
         Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
-        Map schema = resolver.resolveOutput(method.getAnnotation(McpFunction.class));
+        Map schema = resolver.resolveOutput(method.getAnnotation(McpTool.class));
         assertEquals(false, schema.get("additionalProperties"));
         assertTrue(properties(schema).containsKey("resultCode"));
     }
@@ -45,7 +36,7 @@ class ToolSchemaResolverTest {
         Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
 
         Map schema = resolver.resolveOutput(
-                method.getAnnotation(McpFunction.class), SimpleResponse.class);
+                method.getAnnotation(McpTool.class), SimpleResponse.class);
 
         assertEquals(List.of("resultCode"), schema.get("required"));
         assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum"));
@@ -55,7 +46,7 @@ class ToolSchemaResolverTest {
     void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception {
         Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
         Map schema = resolver.resolveOutput(
-                method.getAnnotation(McpFunction.class), AutomaticRequest.class);
+                method.getAnnotation(McpTool.class), AutomaticRequest.class);
         assertTrue(schema.isEmpty());
     }
     @SuppressWarnings("unchecked")
@@ -68,17 +59,14 @@ class ToolSchemaResolverTest {
         return (Map) properties(schema).get(name);
     }
 
-    static class InlineSchemaTool {
-        void search(AutomaticRequest request) {
-        }
-    }
-
     static class AutomaticSchemaTool {
+        @McpTool(name = "oth.test.automatic.search")
         void search(AutomaticRequest request) {
         }
     }
 
     static class AutomaticOutputSchemaTool {
+        @McpTool(name = "oth.test.output.search")
         SimpleResponse search(AutomaticRequest request) {
             return null;
         }
@@ -91,6 +79,7 @@ class ToolSchemaResolverTest {
     }
 
     static class OutputSchemaTool {
+        @McpTool(name = "oth.test.explicit.search")
         void search(AutomaticRequest request) {
         }
     }
diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java
new file mode 100644
index 000000000..032dc1b65
--- /dev/null
+++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java
@@ -0,0 +1,37 @@
+package io.shinhanlife.dap.lib.util;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class ToolSourceUpdaterTest {
+
+    @TempDir
+    Path temporaryRoot;
+
+    @Test
+    void updatesSdkAndProjectOwnedMetadataOnTheSameToolMethod() throws Exception {
+        Path source = temporaryRoot.resolve("dap-was-oth/src/main/java/example/SampleUseCase.java");
+        Files.createDirectories(source.getParent());
+        Files.writeString(source, """
+                package example;
+                import org.springframework.ai.mcp.annotation.McpTool;
+                import io.shinhanlife.dap.lib.annotation.ToolHint;
+                interface SampleUseCase {
+                    @McpTool(name = "oth.cmm.sample.search", description = "old")
+                    @ToolHint(register = false, requiresApproval = false)
+                    void search();
+                }
+                """);
+
+        ToolSourceUpdater.updateToolSource(temporaryRoot, "oth.cmm.sample.search", "customer", "new", true, true);
+
+        String updated = Files.readString(source);
+        assertTrue(updated.contains("@McpTool(name = \"oth.cmm.sample.search\", description = \"new\")"));
+        assertTrue(updated.contains("@ToolHint(register = true, requiresApproval = true"));
+        assertTrue(updated.contains("group = \"customer\""), updated);
+    }
+}
diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java
index 234c19721..93e37aeed 100644
--- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java
+++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java
@@ -30,7 +30,7 @@ class McpToolNameValidatorTest {
     Path temporaryRoot;
 
     @Test
-    void rejectsDuplicateMcpFunctionNamesAcrossToolModules() throws IOException {
+    void rejectsDuplicateMcpToolNamesAcrossToolModules() throws IOException {
         writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
         writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
 
@@ -43,7 +43,7 @@ class McpToolNameValidatorTest {
     }
 
     @Test
-    void validationRunnerRejectsDuplicateMcpFunctionNamesBeforePackaging() throws IOException {
+    void validationRunnerRejectsDuplicateMcpToolNamesBeforePackaging() throws IOException {
         writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
         writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
 
@@ -90,10 +90,13 @@ class McpToolNameValidatorTest {
         Files.writeString(source, """
                 package example;
 
+                import org.springframework.ai.mcp.annotation.McpTool;
+
                 class %s {
+                    @McpTool(name = "%s")
                     void execute() { }
                 }
-                """.formatted(className, className, toolName));
+                """.formatted(className, toolName));
     }
 
     private Path findProjectRoot() {
diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java
index 8d09ca369..95a3f2f5b 100644
--- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java
+++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java
@@ -1,4 +1,4 @@
-package io.shinhanlife.dap.mcc.manifest;
+package io.shinhanlife.dap.lib.manifest;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java
index cdbce3ac3..82fe97962 100644
--- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java
+++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/ToolMcpServerConfigurationTest.java
@@ -2,6 +2,7 @@ package io.shinhanlife.dap.mcc.mcp;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
 import io.shinhanlife.dap.lib.mcp.ToolMcpServerConfiguration;
diff --git a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
index a90126042..10f92c571 100644
--- a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
+++ b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
@@ -6,12 +6,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
 import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
+import io.shinhanlife.dap.lib.annotation.ToolHint;
 import io.shinhanlife.dap.mcc.biz.cmm.usecase.impl.ClaimSearchSchemaSampleUseCaseImpl;
 import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
 import java.lang.reflect.Method;
 import java.util.List;
 import java.util.Map;
 import org.junit.jupiter.api.Test;
+import org.springframework.ai.mcp.annotation.McpTool;
 
 class ClaimSearchRequestSchemaTest {
 
@@ -33,10 +35,11 @@ class ClaimSearchRequestSchemaTest {
     void resolvesSchemaFromToolModuleResource() throws Exception {
         Method method = ClaimSearchSchemaSampleUseCase.class
                 .getDeclaredMethod("search", ClaimSearchRequest.class);
-        McpFunction function = method.getAnnotation(McpFunction.class);
+        McpTool function = method.getAnnotation(McpTool.class);
+        ToolHint hint = method.getAnnotation(ToolHint.class);
 
         Map schema = new ToolSchemaResolver(new ObjectMapper())
-                .resolve(function, ClaimSearchRequest.class);
+                .resolve(function, hint, ClaimSearchRequest.class);
 
         assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
         assertTrue(schema.containsKey("anyOf"));
@@ -47,8 +50,10 @@ class ClaimSearchRequestSchemaTest {
     void resolvesOutputSchemaFromToolModuleResource() throws Exception {
         Method method = ClaimSearchSchemaSampleUseCase.class
                 .getDeclaredMethod("search", ClaimSearchRequest.class);
-        McpFunction function = method.getAnnotation(McpFunction.class);
-        Map schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function);
+        McpTool function = method.getAnnotation(McpTool.class);
+        ToolHint hint = method.getAnnotation(ToolHint.class);
+        Map schema = new ToolSchemaResolver(new ObjectMapper())
+                .resolveOutput(function, ClaimSearchResponse.class, hint);
         assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
         assertTrue(properties(schema).containsKey("resultCode"));
         assertTrue(properties(schema).containsKey("statusLabel"));
@@ -61,7 +66,8 @@ class ClaimSearchRequestSchemaTest {
         Method method = ClaimSearchSchemaSampleUseCase.class
                 .getDeclaredMethod("search", ClaimSearchRequest.class);
         Map schema = new ToolSchemaResolver(new ObjectMapper())
-                .resolveOutput(method.getAnnotation(McpFunction.class));
+                .resolveOutput(method.getAnnotation(McpTool.class), ClaimSearchResponse.class,
+                        method.getAnnotation(ToolHint.class));
 
         ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest());
         ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
diff --git a/docs/superpowers/plans/2026-08-06-mcp-sdk-validator-updater.md b/docs/superpowers/plans/2026-08-06-mcp-sdk-validator-updater.md
new file mode 100644
index 000000000..30e9988a0
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-06-mcp-sdk-validator-updater.md
@@ -0,0 +1,61 @@
+# MCP SDK Validator and Source Updater Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Keep tool-name validation and source metadata updates functional after replacing the custom `@McpFunction` annotation with Spring AI's `@McpTool`.
+
+**Architecture:** `McpToolNameValidator` will scan `@McpTool(name = "...")` declarations for naming and duplication rules. `ToolSourceUpdater` will update SDK-owned fields (`name`, `description`) on `@McpTool` and project-owned fields (`group`, `register`, `requiresApproval`) on adjacent `@ToolHint`, without trying to write unsupported fields to the SDK annotation.
+
+**Tech Stack:** Java 21, JUnit 5, Spring AI MCP annotations, Gradle.
+
+## Global Constraints
+
+- Preserve the existing tool-name convention and duplicate-name build validation.
+- Do not reintroduce the removed custom `McpFunction` annotation.
+- Keep project-specific metadata in `io.shinhanlife.dap.lib.annotation.ToolHint`.
+- Verify with focused tests and `gradlew.bat test`.
+
+---
+
+### Task 1: Migrate tool-name validation
+
+**Files:**
+- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java`
+- Modify: `dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java`
+
+**Interfaces:**
+- Consumes: Java source files containing `@McpTool(name = "...")`.
+- Produces: `McpToolNameValidator.assertUnique(Path)` that rejects duplicate or invalid SDK tool names.
+
+- [ ] Write tests using `@McpTool` source snippets for duplicate, invalid, and valid names.
+- [ ] Run `:dap-was-lib:test --tests *McpToolNameValidatorTest` and confirm current implementation does not detect those annotations.
+- [ ] Replace the annotation scan target from `@McpFunction` to `@McpTool`.
+- [ ] Re-run the focused test and confirm it passes.
+
+### Task 2: Migrate source metadata update behavior
+
+**Files:**
+- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java`
+- Create: `dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java`
+
+**Interfaces:**
+- Consumes: a tool interface source containing `@McpTool` and `@ToolHint`.
+- Produces: `ToolSourceUpdater.updateToolSource(String, String, String, boolean, Boolean)` that updates SDK name/description and project metadata without emitting invalid `@McpTool` attributes.
+
+- [ ] Write a temporary-source test defining one SDK annotation and one `ToolHint` annotation.
+- [ ] Run the focused test and confirm the legacy updater cannot locate `@McpTool` declarations.
+- [ ] Update annotation matching and replacements: `description` belongs to `@McpTool`; `group`, `register`, and `requiresApproval` belong to `@ToolHint`.
+- [ ] Re-run the focused test and confirm it passes.
+
+### Task 3: Restore test-suite compilation after SDK migration
+
+**Files:**
+- Modify: stale tests referencing `McpFunction` or old package locations.
+
+**Interfaces:**
+- Consumes: `McpTool`, `ToolHint`, current `ToolSchemaResolver`, and current package names.
+- Produces: compilation and behavioral coverage aligned with the SDK-based production code.
+
+- [ ] Replace stale annotation and resolver signatures in tests.
+- [ ] Fix package relocation and missing assertion imports without widening production visibility.
+- [ ] Run `gradlew.bat test`.