From 870412f0c77adce95e039094fc6dd04712cbb0af Mon Sep 17 00:00:00 2001 From: jade Date: Wed, 19 Aug 2026 13:50:49 +0900 Subject: [PATCH] fix: resolve workspace path resolution and enhance GrowToolHint generation in ToolScaffolder --- .../presentation/ScaffoldingController.java | 27 ++- .../main/resources/static/admin/scaffold.html | 55 ++++-- .../dap/lib/util/ToolScaffolder.java | 161 ++++++++++++++---- 3 files changed, 192 insertions(+), 51 deletions(-) diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java index 6c8932e0..ce888fa9 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java @@ -108,6 +108,13 @@ public class ScaffoldingController { parseDelimited(req.get("tags")), req.get("ownerOrg")); + String defaultWorkspace = "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dap-was-dapmt"; + String workspacePath = req.getOrDefault("workspacePath", defaultWorkspace); + if (workspacePath.trim().isEmpty()) { + workspacePath = defaultWorkspace; + } + System.setProperty("AXHUB_SOURCE_DIR", workspacePath); + return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions); @@ -129,6 +136,9 @@ public class ScaffoldingController { ? System.getProperty("user.name") : request.author().trim(); String date = request.date() == null || request.date().isBlank() ? LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")) : request.date().trim(); + String workspacePath = request.workspacePath() != null && !request.workspacePath().isBlank() + ? request.workspacePath().trim() : "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dap-was-dapmt"; + System.setProperty("AXHUB_SOURCE_DIR", workspacePath); return ToolScaffolder.scaffoldUseCase(request.useCaseName().trim(), moduleName, author, date, request.tools() == null ? List.of() : request.tools()); } catch (Exception e) { @@ -137,14 +147,15 @@ public class ScaffoldingController { } @GetMapping("/usecases") - public List listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey) { + public List listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey, @RequestParam(required = false) String workspacePath) { if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) { throw new IllegalArgumentException("Invalid target module."); } if (categoryKey == null || !categoryKey.matches("^[a-z0-9]{3}$")) { throw new IllegalArgumentException("Invalid domain category."); } - String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); + String sourceDir = (workspacePath != null && !workspacePath.isBlank()) ? workspacePath : System.getProperty("AXHUB_SOURCE_DIR"); + if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getenv("AXHUB_SOURCE_DIR"); if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir"); File useCaseDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz/" + categoryKey + "/usecase"); @@ -156,11 +167,12 @@ public class ScaffoldingController { } @GetMapping("/categories") - public List listCategories(@RequestParam String moduleName) { + public List listCategories(@RequestParam String moduleName, @RequestParam(required = false) String workspacePath) { if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) { throw new IllegalArgumentException("Invalid target module."); } - String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); + String sourceDir = (workspacePath != null && !workspacePath.isBlank()) ? workspacePath : System.getProperty("AXHUB_SOURCE_DIR"); + if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getenv("AXHUB_SOURCE_DIR"); if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir"); File bizDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz"); File[] dirs = bizDir.listFiles(File::isDirectory); @@ -256,9 +268,10 @@ public class ScaffoldingController { } @GetMapping("/modules") - public List listModules() { + public List listModules(@RequestParam(required = false) String workspacePath) { try { - String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); + String sourceDir = (workspacePath != null && !workspacePath.isBlank()) ? workspacePath : System.getProperty("AXHUB_SOURCE_DIR"); + if (sourceDir == null) sourceDir = System.getenv("AXHUB_SOURCE_DIR"); if (sourceDir == null) sourceDir = System.getProperty("user.dir"); File dir = new File(sourceDir); @@ -426,7 +439,7 @@ public class ScaffoldingController { List outputFields) { } - private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date, + private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date, String workspacePath, List tools) { } } diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html index 164b70c3..b61aea09 100644 --- a/dap-gateway/src/main/resources/static/admin/scaffold.html +++ b/dap-gateway/src/main/resources/static/admin/scaffold.html @@ -825,6 +825,20 @@
+
+
+ +
+ + + +
+
직접 경로를 붙여넣거나 우측 [폴더 선택] 버튼을 눌러 선택하세요.
+
+
@@ -1521,18 +1535,27 @@ } document.addEventListener('DOMContentLoaded', function() { - fetch('/api/v1/scaffold/modules') - .then(res => res.json()) - .then(modules => { - const select = document.getElementById('targetModuleSelect'); - if (modules && modules.length > 0) { - select.innerHTML = modules.map(m => ``).join(''); - } - if (typeof loadCategoriesForSelection === 'function') { - loadCategoriesForSelection(); - } - }) - .catch(err => console.error('Failed to load modules:', err)); + const loadModules = () => { + const workspacePath = document.querySelector('input[name="workspacePath"]')?.value || ''; + fetch(`/api/v1/scaffold/modules?workspacePath=${encodeURIComponent(workspacePath)}`) + .then(res => res.json()) + .then(modules => { + const select = document.getElementById('targetModuleSelect'); + if (modules && modules.length > 0) { + select.innerHTML = modules.map(m => ``).join(''); + } + if (typeof loadCategoriesForSelection === 'function') { + loadCategoriesForSelection(); + } + }) + .catch(err => console.error('Failed to load modules:', err)); + }; + loadModules(); + const wsInput = document.querySelector('input[name="workspacePath"]'); + if (wsInput) { + wsInput.addEventListener('change', loadModules); + wsInput.addEventListener('blur', loadModules); // Typing custom path and clicking away + } }); handleFormSubmit('podForm', '/api/v1/scaffold/pod'); @@ -1603,12 +1626,13 @@ async function loadUseCasesForSelection() { const moduleName = document.getElementById('targetModuleSelect').value; + const workspacePath = document.querySelector('input[name="workspacePath"]')?.value || ''; const categoryKey = document.querySelector('#toolForm [name="categoryKey"]').value.trim(); const select = document.getElementById('toolGroupUseCaseSelect'); select.innerHTML = ''; if (!/^[a-z0-9]{3}$/.test(categoryKey)) return; try { - const response = await fetch(`/api/v1/scaffold/usecases?moduleName=${encodeURIComponent(moduleName)}&categoryKey=${encodeURIComponent(categoryKey)}`); + const response = await fetch(`/api/v1/scaffold/usecases?moduleName=${encodeURIComponent(moduleName)}&categoryKey=${encodeURIComponent(categoryKey)}&workspacePath=${encodeURIComponent(workspacePath)}`); if (!response.ok) throw new Error('UseCase 목록 조회 실패'); const useCases = await response.json(); useCases.forEach(useCaseName => select.add(new Option(`기존 ${useCaseName}에 함수 추가`, useCaseName))); @@ -1619,11 +1643,12 @@ async function loadCategoriesForSelection() { const moduleName = document.getElementById('targetModuleSelect').value; + const workspacePath = document.querySelector('input[name="workspacePath"]')?.value || ''; const select = document.getElementById('domainCategorySelect'); const nameInput = document.getElementById('domainCategoryName'); select.innerHTML = ''; try { - const response = await fetch(`/api/v1/scaffold/categories?moduleName=${encodeURIComponent(moduleName)}`); + const response = await fetch(`/api/v1/scaffold/categories?moduleName=${encodeURIComponent(moduleName)}&workspacePath=${encodeURIComponent(workspacePath)}`); if (!response.ok) throw new Error('Category 목록 조회 실패'); const categories = await response.json(); categories.forEach(cat => select.add(new Option(`기존 ${cat} 도메인`, cat))); @@ -1687,7 +1712,7 @@ fetch('/api/v1/scaffold/tool-group', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({useCaseName, moduleName: data.moduleName, author: data.author, date: data.date, tools}) + body: JSON.stringify({useCaseName, moduleName: data.moduleName, author: data.author, date: data.date, workspacePath: data.workspacePath, tools}) }).then(response => response.text()).then(result => { const resultBox = document.getElementById('resultBox'); resultBox.style.display = 'block'; resultBox.className = result.startsWith('Error:') ? 'error' : 'success'; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java index 5fdfbf4b..7be1a191 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.Scanner; +import java.util.stream.Collectors; /** * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스입니다. @@ -79,8 +80,11 @@ public class ToolScaffolder { String group = tools.getFirst().group().toLowerCase(Locale.ROOT); validateToolMethods(tools, group); - String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); - Path rootDir = sourceDir == null ? Paths.get(".") : Paths.get(sourceDir); + String sourceDir = System.getProperty("AXHUB_SOURCE_DIR"); + if (sourceDir == null) { + sourceDir = System.getenv("AXHUB_SOURCE_DIR"); + } + Path rootDir = sourceDir != null ? Paths.get(sourceDir) : Paths.get("."); Path configuredModule = Paths.get(moduleName); Path moduleRoot = configuredModule.isAbsolute() ? configuredModule : rootDir.resolve(configuredModule); Path sourceRoot = moduleRoot.resolve(BASE_PACKAGE_PATH); @@ -199,9 +203,6 @@ public class ToolScaffolder { } String toolName = toToolName(moduleName, tool.group(), baseName); - writeUtf8(definitionDir.resolve(toolName + ".yml"), toolDefinitionContentV17(toolName, - option(tool.title(), baseName), tool.description(), tool.group(), tool.interfaceId(), - tool.inputFields(), isMutationTool(baseName), tool.definitionOptions())); writeUtf8(mockDir.resolve(toolName + ".json"), mockResponseContent(tool.outputFields())); log.append("[Tool] ").append(toolName).append(" -> ").append(clientDir.resolve(baseName + "Client.java")).append("\n"); } @@ -214,12 +215,43 @@ public class ToolScaffolder { String baseName = toPascalCase(tool.baseName()); imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n") .append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\n"); + boolean isMutation = isMutationTool(baseName); + ToolDefinitionOptions opts = tool.definitionOptions() == null ? new ToolDefinitionOptions(null, null, null, null, null, null, null, null) : tool.definitionOptions(); + methods.append(" @McpTool(name = \"").append(toToolName(moduleName, tool.group(), baseName)) .append("\", title = \"").append(javaText(option(tool.title(), baseName))) .append("\", description = \"").append(javaText(option(tool.description(), ""))).append("\")\n") - .append(" @GrowToolHint(register = ").append(tool.register()).append(", categoryKey = \"") - .append(tool.group().toLowerCase(Locale.ROOT)).append("\", mappingId = \"") - .append(javaText(tool.interfaceId())).append("\")\n") + .append(" @GrowToolHint(\n") + .append(" requiresApproval = ").append(isMutation).append(",\n") + .append(" categoryKey = \"").append(tool.group().toLowerCase(Locale.ROOT)).append("\",\n"); + if (tool.interfaceId() != null && !tool.interfaceId().isBlank()) { + methods.append(" mappingId = \"").append(javaText(tool.interfaceId())).append("\",\n"); + } + methods.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 기능을 수행합니다.")).append("\",\n") + .append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n") + .append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n") + .append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n") + .append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " 정보를 처리합니다.")).append("\",\n"); + + List examples = opts.exampleQueries(); + if (examples == null || examples.isEmpty()) { + examples = List.of(option(tool.title(), baseName) + " 정보를 보여줘", option(tool.title(), baseName) + " 확인해줘", "현재 " + option(tool.title(), baseName) + " 알려줘"); + } + methods.append(" exampleQueries = {") + .append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" destructive = ").append(isMutation).append(",\n") + .append(" idempotent = ").append(!isMutation).append(",\n"); + + List tags = opts.tags(); + if (tags == null || tags.isEmpty()) { + tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회"); + } + methods.append(" tags = {") + .append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" ownerOrg = \"").append(javaText(opts.ownerOrg() != null && !opts.ownerOrg().isBlank() ? opts.ownerOrg() : "MCP_TOOL")).append("\"\n") + .append(" )\n") .append(" ").append(baseName).append("Response ").append(tool.methodName()).append("(") .append(baseName).append("Request req);\n\n"); } @@ -327,11 +359,47 @@ public class ToolScaffolder { useCase = addImport(useCase, "import " + bizPackage + ".dto." + requestType + ";") ; useCase = addImport(useCase, "import " + bizPackage + ".dto." + responseType + ";") ; - String declaration = "\n @McpTool(name = \"" + toolName + "\", title = \"" + javaText(option(tool.title(), baseName)) - + "\", description = \"" + javaText(option(tool.description(), "")) + "\")\n" - + " @GrowToolHint(register = " + tool.register() + ", categoryKey = \"" + tool.group().toLowerCase(Locale.ROOT) - + "\", mappingId = \"" + javaText(option(tool.interfaceId(), tool.httpApiName())) + "\")\n" - + " " + responseType + " " + methodName + "(" + requestType + " req);\n"; + boolean isMutation = isMutationTool(baseName); + ToolDefinitionOptions opts = tool.definitionOptions() == null ? new ToolDefinitionOptions(null, null, null, null, null, null, null, null) : tool.definitionOptions(); + + StringBuilder declBuilder = new StringBuilder("\n @McpTool(name = \"").append(toolName) + .append("\", title = \"").append(javaText(option(tool.title(), baseName))) + .append("\", description = \"").append(javaText(option(tool.description(), ""))).append("\")\n") + .append(" @GrowToolHint(\n") + .append(" requiresApproval = ").append(isMutation).append(",\n") + .append(" categoryKey = \"").append(tool.group().toLowerCase(Locale.ROOT)).append("\",\n"); + String mappingId = option(tool.interfaceId(), tool.httpApiName()); + if (mappingId != null && !mappingId.isBlank()) { + declBuilder.append(" mappingId = \"").append(javaText(mappingId)).append("\",\n"); + } + declBuilder.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 기능을 수행합니다.")).append("\",\n") + .append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n") + .append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n") + .append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n") + .append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " 정보를 처리합니다.")).append("\",\n"); + + List examples = opts.exampleQueries(); + if (examples == null || examples.isEmpty()) { + examples = List.of(option(tool.title(), baseName) + " 정보를 보여줘", option(tool.title(), baseName) + " 확인해줘", "현재 " + option(tool.title(), baseName) + " 알려줘"); + } + declBuilder.append(" exampleQueries = {") + .append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" destructive = ").append(isMutation).append(",\n") + .append(" idempotent = ").append(!isMutation).append(",\n"); + + List tags = opts.tags(); + if (tags == null || tags.isEmpty()) { + tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회"); + } + declBuilder.append(" tags = {") + .append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" ownerOrg = \"").append(javaText(opts.ownerOrg() != null && !opts.ownerOrg().isBlank() ? opts.ownerOrg() : "MCP_TOOL")).append("\"\n") + .append(" )\n") + .append(" ").append(responseType).append(" ").append(methodName).append("(").append(requestType).append(" req);\n"); + + String declaration = declBuilder.toString(); useCase = insertBeforeLastBrace(useCase, declaration); implementation = addImport(implementation, "import " + bizPackage + ".dto." + requestType + ";"); @@ -456,6 +524,12 @@ public class ToolScaffolder { String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/"; String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null; String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null; + String defaultWorkspace = "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dap-was-dapmt"; + String workspace = getOrAsk(args, 10, scanner, "11. 대상 프로젝트 워크스페이스 경로 (default: " + defaultWorkspace + "): "); + if (workspace.trim().isEmpty()) { + workspace = defaultWorkspace; + } + System.setProperty("AXHUB_SOURCE_DIR", workspace); String result = scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource, List.of(new FieldDefinition("query", "String", "Search query", List.of("example"), "", false)), List.of()); System.out.println(result); @@ -513,7 +587,10 @@ public class ToolScaffolder { title = title == null || title.isBlank() ? baseName : title.trim(); description = description == null ? "" : description.trim(); httpApiName = httpApiName == null || httpApiName.isBlank() ? toKebabCase(baseName) : httpApiName.trim(); - String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + String envSourceDir = System.getProperty("AXHUB_SOURCE_DIR"); + if (envSourceDir == null) { + envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + } Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase")); @@ -654,14 +731,45 @@ public class ToolScaffolder { String toolName = toToolName(moduleName, group, baseName); - String toolHintLine; + boolean isMutation = isMutationTool(baseName); + StringBuilder sb = new StringBuilder(" @GrowToolHint(\n"); if (useSchemaResource) { - toolHintLine = (" @GrowToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" + - " inputSchemaResource = \"%s\",\n" + - " outputSchemaResource = \"%s\")").formatted(register, group.toLowerCase(Locale.ROOT), interfaceId, inputSchemaClasspath, outputSchemaClasspath); - } else { - toolHintLine = " @GrowToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\")".formatted(register, group.toLowerCase(Locale.ROOT), interfaceId); + sb.append(" inputSchemaResource = \"").append(inputSchemaClasspath).append("\",\n"); + sb.append(" outputSchemaResource = \"").append(outputSchemaClasspath).append("\",\n"); } + sb.append(" requiresApproval = ").append(isMutation).append(",\n"); + sb.append(" categoryKey = \"").append(group.toLowerCase(Locale.ROOT)).append("\",\n"); + if (interfaceId != null && !interfaceId.isBlank()) { + sb.append(" mappingId = \"").append(interfaceId).append("\",\n"); + } + sb.append(" functionDescription = \"").append(javaText(definitionOptions.functionDescription() != null && !definitionOptions.functionDescription().isBlank() ? definitionOptions.functionDescription() : title + " 기능을 수행합니다.")).append("\",\n"); + sb.append(" whenToUse = \"").append(javaText(definitionOptions.whenToUse() != null && !definitionOptions.whenToUse().isBlank() ? definitionOptions.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n"); + sb.append(" whenNotToUse = \"").append(javaText(definitionOptions.whenNotToUse() != null && !definitionOptions.whenNotToUse().isBlank() ? definitionOptions.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n"); + sb.append(" ioLimits = \"").append(javaText(definitionOptions.ioLimits() != null && !definitionOptions.ioLimits().isBlank() ? definitionOptions.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n"); + sb.append(" displayDescription = \"").append(javaText(definitionOptions.displayDescription() != null && !definitionOptions.displayDescription().isBlank() ? definitionOptions.displayDescription() : title + " 정보를 처리합니다.")).append("\",\n"); + + List examples = definitionOptions.exampleQueries(); + if (examples == null || examples.isEmpty()) { + examples = List.of(title + " 정보를 보여줘", title + " 확인해줘", "현재 " + title + " 알려줘"); + } + sb.append(" exampleQueries = {") + .append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", "))) + .append("},\n"); + + sb.append(" destructive = ").append(isMutation).append(",\n"); + sb.append(" idempotent = ").append(!isMutation).append(",\n"); + + List tags = definitionOptions.tags(); + if (tags == null || tags.isEmpty()) { + tags = List.of(group.toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회"); + } + sb.append(" tags = {") + .append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", "))) + .append("},\n"); + + sb.append(" ownerOrg = \"").append(javaText(definitionOptions.ownerOrg() != null && !definitionOptions.ownerOrg().isBlank() ? definitionOptions.ownerOrg() : "MCP_TOOL")).append("\"\n"); + sb.append(" )"); + String toolHintLine = sb.toString(); String serviceInterfaceContent = """ package %s.usecase; @@ -1269,11 +1377,8 @@ public class ToolScaffolder { writeUtf8(generatedTestPath, useCaseTestContent(bizPackage, baseName)); log.append("[Unit Test] ").append(generatedTestPath).append("\\n"); log.append("[Test Command] .\\gradlew.bat :").append(moduleName.substring(moduleName.lastIndexOf(java.io.File.separator) + 1)).append(":test --tests \"*").append(baseName).append("UseCaseTest\"\\n"); - Files.createDirectories(definitionDir); - Path definitionPath = definitionDir.resolve(toolName + ".yml"); - writeUtf8(definitionPath, toolDefinitionContentV17(toolName, title, description, group, - interfaceId, inputFields, isMutationTool(baseName), definitionOptions)); - log.append("[V17 Tool Definition] ").append(definitionPath).append("\n"); + // Tool Definition YML is no longer generated. We use @GrowToolHint instead. + // log.append("[V17 Tool Definition] ...\n"); log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n"); return log.toString(); @@ -1894,10 +1999,8 @@ public class ToolScaffolder { return supportedType(itemType); } private static String supportedType(String type) { - return switch (type == null ? "String" : type) { - case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type; - default -> throw new IllegalArgumentException("Unsupported field type: " + type); - }; + if (type == null || type.isBlank()) return "String"; + return type; }