diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java
new file mode 100644
index 00000000..b77bc3e8
--- /dev/null
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java
@@ -0,0 +1,149 @@
+package io.shinhanlife.dap.lib.validation;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @package io.shinhanlife.dap.lib.validation
+ * @className McpToolNameValidator
+ * @description Validates unique MCP function names across tool modules
+ * @author 0986406
+ * @create 2026.07.27
+ *
+ * ---------- revision history ----------
+ * date author description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406 initial creation
+ *
+ */
+public final class McpToolNameValidator {
+
+ private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\"");
+
+ private McpToolNameValidator() {
+ }
+
+ public static void assertUnique(Path projectRoot) {
+ Map> declarationsByName = new LinkedHashMap<>();
+
+ try (var modules = Files.list(projectRoot)) {
+ modules.filter(Files::isDirectory)
+ .filter(path -> path.getFileName().toString().startsWith("dap-tool-"))
+ .filter(path -> !path.getFileName().toString().equals("dap-tool-core"))
+ .sorted()
+ .forEach(module -> collectDeclarations(module, declarationsByName));
+ } catch (IOException exception) {
+ throw new UncheckedIOException("Failed to scan MCP tool modules", exception);
+ }
+
+ List>> duplicates = declarationsByName.entrySet().stream()
+ .filter(entry -> entry.getValue().size() > 1)
+ .sorted(Map.Entry.comparingByKey())
+ .toList();
+
+ if (!duplicates.isEmpty()) {
+ throw new IllegalStateException(buildDuplicateMessage(duplicates));
+ }
+ }
+
+ private static void collectDeclarations(Path module, Map> declarationsByName) {
+ Path sourceDirectory = module.resolve("src/main/java");
+ if (!Files.isDirectory(sourceDirectory)) {
+ return;
+ }
+
+ try (var sources = Files.walk(sourceDirectory)) {
+ sources.filter(path -> path.toString().endsWith(".java"))
+ .sorted()
+ .forEach(source -> collectDeclarations(module.getFileName().toString(), source, declarationsByName));
+ } catch (IOException exception) {
+ throw new UncheckedIOException("Failed to scan module " + module.getFileName(), exception);
+ }
+ }
+
+ private static void collectDeclarations(String moduleName, Path source,
+ Map> declarationsByName) {
+ String content;
+ try {
+ content = Files.readString(source);
+ } catch (IOException exception) {
+ throw new UncheckedIOException("Failed to read " + source, exception);
+ }
+
+ int annotationOffset = content.indexOf("@McpFunction");
+ while (annotationOffset >= 0) {
+ int openingParenthesis = content.indexOf('(', annotationOffset);
+ int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
+ if (openingParenthesis < 0 || closingParenthesis < 0) {
+ break;
+ }
+
+ Matcher matcher = TOOL_NAME_PATTERN.matcher(content.substring(openingParenthesis + 1, closingParenthesis));
+ if (matcher.find()) {
+ String toolName = matcher.group(1);
+ int line = 1 + (int) content.substring(0, annotationOffset).chars().filter(character -> character == '\n').count();
+ declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>())
+ .add(new ToolDeclaration(moduleName, source, line));
+ }
+ annotationOffset = content.indexOf("@McpFunction", closingParenthesis + 1);
+ }
+ }
+
+ private static int findAnnotationEnd(String content, int openingParenthesis) {
+ if (openingParenthesis < 0) {
+ return -1;
+ }
+
+ 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;
+ }
+ continue;
+ }
+ if (character == '\"') {
+ inString = true;
+ } else if (character == '(') {
+ depth++;
+ } else if (character == ')' && --depth == 0) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private static String buildDuplicateMessage(List>> duplicates) {
+ StringBuilder message = new StringBuilder("Duplicate MCP tool name(s):");
+ for (Map.Entry> duplicate : duplicates) {
+ message.append("\n\n").append(duplicate.getKey());
+ duplicate.getValue().stream()
+ .sorted(Comparator.comparing(ToolDeclaration::moduleName).thenComparing(declaration -> declaration.source().toString()))
+ .forEach(declaration -> message.append("\n- ")
+ .append(declaration.moduleName())
+ .append(": ")
+ .append(declaration.source())
+ .append(':').append(declaration.line()));
+ }
+ return message.toString();
+ }
+
+ private record ToolDeclaration(String moduleName, Path source, int line) {
+ }
+}
diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java
new file mode 100644
index 00000000..32cc6052
--- /dev/null
+++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java
@@ -0,0 +1,72 @@
+package io.shinhanlife.dap.lib.validation;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * @package io.shinhanlife.dap.lib.validation
+ * @className McpToolNameValidatorTest
+ * @description MCP tool name uniqueness validation test
+ * @author 0986406
+ * @create 2026.07.27
+ *
+ * ---------- revision history ----------
+ * date author description
+ * ---------- --------- ---------------------------
+ * 2026.07.27 0986406 initial creation
+ *
+ */
+class McpToolNameValidatorTest {
+
+ @TempDir
+ Path temporaryRoot;
+
+ @Test
+ void rejectsDuplicateMcpFunctionNamesAcrossToolModules() throws IOException {
+ writeToolSource("dap-tool-first", "FirstTool.java", "first", "send_sms");
+ writeToolSource("dap-tool-second", "SecondTool.java", "second", "send_sms");
+
+ IllegalStateException exception = assertThrows(IllegalStateException.class,
+ () -> McpToolNameValidator.assertUnique(temporaryRoot));
+
+ assertTrue(exception.getMessage().contains("send_sms"));
+ assertTrue(exception.getMessage().contains("dap-tool-first"));
+ assertTrue(exception.getMessage().contains("dap-tool-second"));
+ }
+
+ @Test
+ void acceptsCurrentProjectToolNames() {
+ assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(findProjectRoot()));
+ }
+
+ private void writeToolSource(String moduleName, String fileName, String className, String toolName) throws IOException {
+ Path source = temporaryRoot.resolve(moduleName).resolve("src/main/java/example").resolve(fileName);
+ Files.createDirectories(source.getParent());
+ Files.writeString(source, """
+ package example;
+
+ class %s {
+ @McpFunction(displayName = "%s", name = "%s", description = "test")
+ void execute() { }
+ }
+ """.formatted(className, className, toolName));
+ }
+
+ private Path findProjectRoot() {
+ Path current = Path.of("").toAbsolutePath();
+ while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
+ current = current.getParent();
+ }
+ if (current == null) {
+ throw new IllegalStateException("Could not locate Gradle project root");
+ }
+ return current;
+ }
+}