feat: standardize tool names by pod domain action
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
This commit is contained in:
@@ -6,6 +6,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
@@ -199,8 +200,7 @@ public class ToolScaffolder {
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent);
|
||||
|
||||
String rawToolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
|
||||
String toolName = group.toLowerCase() + "." + rawToolName;
|
||||
String toolName = toToolName(moduleName, group, baseName);
|
||||
|
||||
String serviceInterfaceContent = """
|
||||
package %s.usecase;
|
||||
@@ -672,6 +672,24 @@ public class ToolScaffolder {
|
||||
return log.toString();
|
||||
}
|
||||
|
||||
private static String toToolName(String moduleName, String group, String baseName) {
|
||||
String pod = moduleName.startsWith("dap-tool-")
|
||||
? moduleName.substring("dap-tool-".length())
|
||||
: "oth";
|
||||
String normalizedName = baseName.replaceAll("([a-z0-9])([A-Z])", "$1 $2")
|
||||
.toLowerCase(Locale.ROOT)
|
||||
.replaceAll("[^a-z0-9]+", " ")
|
||||
.trim();
|
||||
String[] words = normalizedName.split("\\s+");
|
||||
String service = words[0];
|
||||
String action = words.length == 1 ? "execute" : words[words.length - 1];
|
||||
return "%s.%s.%s.%s".formatted(
|
||||
pod.toLowerCase(Locale.ROOT),
|
||||
group.toLowerCase(Locale.ROOT),
|
||||
service,
|
||||
action);
|
||||
}
|
||||
|
||||
private static String toPascalCase(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.regex.Pattern;
|
||||
public final class McpToolNameValidator {
|
||||
|
||||
private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\"");
|
||||
private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$");
|
||||
|
||||
private McpToolNameValidator() {
|
||||
}
|
||||
@@ -45,6 +46,14 @@ public final class McpToolNameValidator {
|
||||
throw new UncheckedIOException("Failed to scan MCP tool modules", exception);
|
||||
}
|
||||
|
||||
List<Map.Entry<String, List<ToolDeclaration>>> invalidNames = declarationsByName.entrySet().stream()
|
||||
.filter(entry -> !TOOL_NAME_CONVENTION.matcher(entry.getKey()).matches())
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.toList();
|
||||
|
||||
if (!invalidNames.isEmpty()) {
|
||||
throw new IllegalStateException(buildInvalidNameMessage(invalidNames));
|
||||
}
|
||||
List<Map.Entry<String, List<ToolDeclaration>>> duplicates = declarationsByName.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().size() > 1)
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
@@ -129,6 +138,21 @@ public final class McpToolNameValidator {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String buildInvalidNameMessage(List<Map.Entry<String, List<ToolDeclaration>>> invalidNames) {
|
||||
StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected pod.domain.service.action using lowercase letters, digits, or hyphens.");
|
||||
for (Map.Entry<String, List<ToolDeclaration>> invalidName : invalidNames) {
|
||||
message.append("\n\n").append(invalidName.getKey());
|
||||
invalidName.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 static String buildDuplicateMessage(List<Map.Entry<String, List<ToolDeclaration>>> duplicates) {
|
||||
StringBuilder message = new StringBuilder("Duplicate MCP tool name(s):");
|
||||
for (Map.Entry<String, List<ToolDeclaration>> duplicate : duplicates) {
|
||||
|
||||
@@ -19,7 +19,7 @@ class ToolScaffolderTest {
|
||||
String useCase = Files.readString(root.resolve("usecase/ClaimSearchUseCase.java"));
|
||||
String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java"));
|
||||
|
||||
assertTrue(useCase.contains("name = \"cmm.claimSearch\""));
|
||||
assertTrue(useCase.contains("name = \"oth.cmm.claim.search\""));
|
||||
assertTrue(useCase.contains("version = \"1.0.0\""));
|
||||
assertTrue(useCase.contains("timeoutMillis = 300000L"));
|
||||
assertTrue(response.contains("@McpOutputSchema"));
|
||||
|
||||
@@ -74,7 +74,7 @@ class ToolSchemaResolverTest {
|
||||
static class InlineSchemaTool {
|
||||
@McpFunction(
|
||||
displayName = "inline",
|
||||
name = "sample.inline",
|
||||
name = "test.schema.inline",
|
||||
description = "inline schema",
|
||||
inputSchema = "{\"type\":\"object\",\"properties\":{\"keyword\":{\"type\":\"string\"}},\"additionalProperties\":false}")
|
||||
void search(AutomaticRequest request) {
|
||||
@@ -82,13 +82,13 @@ class ToolSchemaResolverTest {
|
||||
}
|
||||
|
||||
static class AutomaticSchemaTool {
|
||||
@McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic schema")
|
||||
@McpFunction(displayName = "automatic", name = "test.schema.automatic", description = "automatic schema")
|
||||
void search(AutomaticRequest request) {
|
||||
}
|
||||
}
|
||||
|
||||
static class AutomaticOutputSchemaTool {
|
||||
@McpFunction(displayName = "automatic-output", name = "sample.automatic-output", description = "automatic output")
|
||||
@McpFunction(displayName = "automatic-output", name = "test.schema.automatic-output", description = "automatic output")
|
||||
SimpleResponse search(AutomaticRequest request) {
|
||||
return null;
|
||||
}
|
||||
@@ -106,7 +106,7 @@ class ToolSchemaResolverTest {
|
||||
static class OutputSchemaTool {
|
||||
@McpFunction(
|
||||
displayName = "output",
|
||||
name = "sample.output",
|
||||
name = "test.schema.output",
|
||||
description = "output schema",
|
||||
outputSchema = "{\"type\":\"object\",\"properties\":{\"resultCode\":{\"type\":\"string\"}},\"required\":[\"resultCode\"],\"additionalProperties\":false}")
|
||||
void search(AutomaticRequest request) {
|
||||
|
||||
@@ -31,26 +31,36 @@ class McpToolNameValidatorTest {
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateMcpFunctionNamesAcrossToolModules() throws IOException {
|
||||
writeToolSource("dap-tool-first", "FirstTool.java", "first", "send_sms");
|
||||
writeToolSource("dap-tool-second", "SecondTool.java", "second", "send_sms");
|
||||
writeToolSource("dap-tool-first", "FirstTool.java", "first", "oth.sms.notification.send");
|
||||
writeToolSource("dap-tool-second", "SecondTool.java", "second", "oth.sms.notification.send");
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
||||
() -> McpToolNameValidator.assertUnique(temporaryRoot));
|
||||
|
||||
assertTrue(exception.getMessage().contains("send_sms"));
|
||||
assertTrue(exception.getMessage().contains("oth.sms.notification.send"));
|
||||
assertTrue(exception.getMessage().contains("dap-tool-first"));
|
||||
assertTrue(exception.getMessage().contains("dap-tool-second"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationRunnerRejectsDuplicateMcpFunctionNamesBeforePackaging() throws IOException {
|
||||
writeToolSource("dap-tool-first", "FirstTool.java", "first", "send_sms");
|
||||
writeToolSource("dap-tool-second", "SecondTool.java", "second", "send_sms");
|
||||
writeToolSource("dap-tool-first", "FirstTool.java", "first", "oth.sms.notification.send");
|
||||
writeToolSource("dap-tool-second", "SecondTool.java", "second", "oth.sms.notification.send");
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> McpToolNameValidationRunner.validate(temporaryRoot));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsToolNameOutsidePodDomainServiceActionConvention() throws IOException {
|
||||
writeToolSource("dap-tool-first", "FirstTool.java", "first", "bond_issue");
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
||||
() -> McpToolNameValidator.assertUnique(temporaryRoot));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Invalid MCP tool name(s)"));
|
||||
assertTrue(exception.getMessage().contains("bond_issue"));
|
||||
}
|
||||
@Test
|
||||
void acceptsCurrentProjectToolNames() {
|
||||
assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(findProjectRoot()));
|
||||
@@ -61,11 +71,11 @@ class McpToolNameValidatorTest {
|
||||
Path root = findProjectRoot();
|
||||
|
||||
assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java",
|
||||
"customer_detail", "detail");
|
||||
"oth.cmm.customer.detail", "detail");
|
||||
assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java",
|
||||
"billing_process", "process");
|
||||
"oth.cmm.billing.process", "process");
|
||||
assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java",
|
||||
"bond_issue", "issue");
|
||||
"oth.cmm.bond.issue", "issue");
|
||||
}
|
||||
|
||||
private void assertToolName(Path root, String relativePath, String expectedName, String legacyName) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user