diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java index 74a1863a..fb7608db 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java @@ -154,6 +154,11 @@ public class McpRouterController { return activeTools; } + /** Report 화면처럼 내부 메타데이터 전체가 필요한 Gateway 컴포넌트에 활성 Tool 목록을 제공합니다. */ + List activeToolsForReport() { + return fetchAllActiveTools(); + } + @GetMapping("/tools/list") public ResponseEntity listTools( @RequestParam(value = "categoryKey", required = false) String categoryKey) { @@ -280,4 +285,4 @@ public class McpRouterController { } -} \ No newline at end of file +} diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ToolReportProxyController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ToolReportProxyController.java index f60c1707..4e10642a 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ToolReportProxyController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ToolReportProxyController.java @@ -1,6 +1,5 @@ package io.shinhanlife.dap.mcg.presentation; -import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse; import io.shinhanlife.dap.mcc.dto.ToolMetadata; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -30,7 +29,7 @@ public class ToolReportProxyController { public ToolReportProxyController( RestClient.Builder restClientBuilder, McpRouterController mcpRouterController, - @Value("${report.service-url:http://127.0.0.1:8092}") String reportServiceUrl) { + @Value("${report.service-url}") String reportServiceUrl) { this.restClient = restClientBuilder.build(); this.mcpRouterController = mcpRouterController; this.reportServiceUrl = reportServiceUrl.replaceAll("/+$", ""); @@ -75,18 +74,7 @@ public class ToolReportProxyController { } private List activeTools() { - ResponseEntity response = mcpRouterController.listTools(null); - JsonRpcResponse body = response.getBody(); - if (body == null || !(body.getResult() instanceof Map result) - || !(result.get("tools") instanceof List tools)) { - throw new IllegalStateException("MCP tools/list response does not contain result.tools"); - } - return tools.stream().map(item -> { - if (!(item instanceof ToolMetadata metadata)) { - throw new IllegalStateException("MCP tools/list contains an invalid tool entry"); - } - return metadata; - }).toList(); + return mcpRouterController.activeToolsForReport(); } private ResponseEntity copy(ResponseEntity response) { diff --git a/dap-gateway/src/main/resources/application-local.yml b/dap-gateway/src/main/resources/application-local.yml index 12f1d531..a167c86f 100644 --- a/dap-gateway/src/main/resources/application-local.yml +++ b/dap-gateway/src/main/resources/application-local.yml @@ -52,3 +52,6 @@ mcp: logging: level: org.apache.kafka: ERROR + +report: + service-url: ${REPORT_SERVICE_URL:http://127.0.0.1:8092} diff --git a/dap-gateway/src/main/resources/application.yml b/dap-gateway/src/main/resources/application.yml index b9f893f9..dda8d4f8 100644 --- a/dap-gateway/src/main/resources/application.yml +++ b/dap-gateway/src/main/resources/application.yml @@ -24,6 +24,9 @@ server: enabled: true shutdown: graceful +report: + service-url: ${REPORT_SERVICE_URL:http://tool-report:8092} + mcp: gateway: fallback: diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html index f63f2cf3..164b70c3 100644 --- a/dap-gateway/src/main/resources/static/admin/scaffold.html +++ b/dap-gateway/src/main/resources/static/admin/scaffold.html @@ -760,6 +760,7 @@ Chat Tester Console + Report
diff --git a/dap-gateway/src/main/resources/static/tool-report.html b/dap-gateway/src/main/resources/static/tool-report.html index 9235858a..aebca121 100644 --- a/dap-gateway/src/main/resources/static/tool-report.html +++ b/dap-gateway/src/main/resources/static/tool-report.html @@ -156,8 +156,15 @@ async function load() { try { const response = await fetch('/report/api/report-tools'); - if (!response.ok) throw new Error('툴 목록 조회에 실패했습니다.'); - state.tools = await response.json(); + const body = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(body?.error || body?.message || `툴 목록 조회에 실패했습니다. (${response.status})`); + } + const tools = Array.isArray(body) ? body : body?.result?.tools; + if (!Array.isArray(tools)) { + throw new Error('툴 목록 응답 형식이 올바르지 않습니다.'); + } + state.tools = tools; [...new Set(state.tools.map(tool => tool.categoryKey))].sort().forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; el('category').appendChild(option); diff --git a/dap-tool-report/build.gradle b/dap-tool-report/build.gradle index 679e803e..7cbe00d7 100644 --- a/dap-tool-report/build.gradle +++ b/dap-tool-report/build.gradle @@ -5,6 +5,7 @@ plugins { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1' implementation 'com.github.javaparser:javaparser-core:3.26.3' implementation 'org.apache.poi:poi-ooxml:5.3.0' diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriter.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriter.java index 5a2356c9..d785dec5 100644 --- a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriter.java +++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriter.java @@ -36,6 +36,7 @@ public class ToolReportExcelWriter { try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { Styles styles = new Styles(workbook); writeSummary(workbook, reports, styles); + writeDefinitions(workbook, reports, styles); writeFields(workbook, reports, styles); writeDiagnostics(workbook, reports, styles); workbook.write(output); @@ -45,6 +46,29 @@ public class ToolReportExcelWriter { } } + private void writeDefinitions(Workbook workbook, List reports, Styles styles) { + String[] headers = {"No.", "툴명", "버전", "표시 설명", "기능 설명", "사용 시점", "사용 제외", + "입출력 제한", "예시 질의", "태그", "필수 환경변수", "소유 조직", "정의 파일"}; + Sheet sheet = workbook.createSheet("툴 정의"); + decorateSheet(sheet, "Tool Definition Report", "tool-definitions YAML 메타데이터", reports, + headers.length, styles); + header(sheet, headers, styles); + int rowIndex = FIRST_DATA_ROW; + int sequence = 1; + for (ToolReportModel report : reports) { + ToolSummary tool = report.tool(); + Row row = sheet.createRow(rowIndex++); + Object[] values = {sequence++, tool.name(), tool.version(), tool.displayDescription(), + tool.functionDescription(), tool.whenToUse(), tool.whenNotToUse(), tool.ioLimits(), + tool.exampleQueries(), tool.tags(), tool.requiredEnvKeys(), tool.ownerOrg(), tool.definitionFile()}; + values(row, values, styles.body); + row.getCell(0).setCellStyle(styles.bodyCenter); + row.getCell(2).setCellStyle(styles.bodyCenter); + } + finishTable(sheet, rowIndex, headers.length, + new int[]{7, 30, 12, 40, 48, 48, 48, 48, 60, 30, 30, 20, 65}); + } + private void writeSummary(Workbook workbook, List reports, Styles styles) { String[] headers = {"No.", "툴명", "제목", "설명", "카테고리", "연계 ID", "등록", "승인 필요", "Read Only", "Destructive", "Idempotent", "Open World", "Request", "Response", "UseCase", "원천 파일"}; diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolSummary.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolSummary.java index 9d7a37ea..65f43689 100644 --- a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolSummary.java +++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/model/ToolSummary.java @@ -18,5 +18,25 @@ public record ToolSummary( String useCaseClass, String sourceFile, String inputSchemaResource, - String outputSchemaResource) { + String outputSchemaResource, + String version, + String functionDescription, + String whenToUse, + String whenNotToUse, + String ioLimits, + String displayDescription, + String exampleQueries, + String tags, + String requiredEnvKeys, + String ownerOrg, + String definitionFile) { + + public ToolSummary(String name, String title, String description, String categoryKey, String mappingId, + boolean register, boolean requiresApproval, boolean readOnlyHint, boolean destructiveHint, + boolean idempotentHint, boolean openWorldHint, String requestType, String responseType, + String useCaseClass, String sourceFile, String inputSchemaResource, String outputSchemaResource) { + this(name, title, description, categoryKey, mappingId, register, requiresApproval, readOnlyHint, + destructiveHint, idempotentHint, openWorldHint, requestType, responseType, useCaseClass, sourceFile, + inputSchemaResource, outputSchemaResource, "", "", "", "", "", "", "", "", "", "", ""); + } } diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolDetailAnalyzer.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolDetailAnalyzer.java index 272172a0..a10a8cc6 100644 --- a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolDetailAnalyzer.java +++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolDetailAnalyzer.java @@ -2,6 +2,7 @@ package io.shinhanlife.dap.report.source; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.FieldDeclaration; @@ -26,10 +27,12 @@ public class ToolDetailAnalyzer { private final ToolSourceDiscovery discovery; private final ObjectMapper objectMapper; + private final ObjectMapper yamlMapper; public ToolDetailAnalyzer(ToolSourceDiscovery discovery) { this.discovery = discovery; this.objectMapper = new ObjectMapper(); + this.yamlMapper = new ObjectMapper(new YAMLFactory()); } public ToolReportModel analyze(ToolSummary tool) { @@ -39,10 +42,12 @@ public class ToolDetailAnalyzer { collectSchema(tool, tool.inputSchemaResource(), "INPUT", fields, diagnostics); collectSchema(tool, tool.outputSchemaResource(), "OUTPUT", fields, diagnostics); - if (tool.inputSchemaResource().isBlank()) { + boolean definitionInput = collectDefinitionSchema(tool, "parameters_schema", "INPUT", fields, diagnostics); + boolean definitionOutput = collectDefinitionSchema(tool, "output_schema", "OUTPUT", fields, diagnostics); + if (tool.inputSchemaResource().isBlank() && !definitionInput) { collectJavaType(tool, javaFiles.get(tool.requestType()), "DTO", "INPUT", fields, diagnostics); } - if (tool.outputSchemaResource().isBlank()) { + if (tool.outputSchemaResource().isBlank() && !definitionOutput) { collectJavaType(tool, javaFiles.get(tool.responseType()), "DTO", "OUTPUT", fields, diagnostics); } if (!tool.mappingId().isBlank()) { @@ -52,6 +57,21 @@ public class ToolDetailAnalyzer { return new ToolReportModel(tool, List.copyOf(fields), List.copyOf(diagnostics)); } + private boolean collectDefinitionSchema(ToolSummary tool, String schemaKey, String direction, + List fields, List diagnostics) { + if (tool.definitionFile() == null || tool.definitionFile().isBlank()) return false; + Path path = discovery.sourceRoot().resolve(tool.definitionFile()).normalize(); + try { + JsonNode schema = yamlMapper.readTree(path.toFile()).path(schemaKey); + if (schema.isMissingNode() || schema.isNull() || schema.isEmpty()) return false; + collectSchemaFields(tool, schema, "TOOL_DEFINITION", direction, "", "", relative(path), fields); + return true; + } catch (Exception exception) { + diagnostics.add("Tool definition schema 분석 실패: " + relative(path) + " (" + schemaKey + ")"); + return false; + } + } + private Map indexJavaFiles() { Map result = new LinkedHashMap<>(); try (var paths = Files.walk(discovery.sourceRoot())) { @@ -83,23 +103,41 @@ public class ToolDetailAnalyzer { Path path = matches.get(0); try { JsonNode root = objectMapper.readTree(path.toFile()); - JsonNode properties = root.path("properties"); - List required = new ArrayList<>(); - root.path("required").forEach(node -> required.add(node.asText())); - Iterator> iterator = properties.fields(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - JsonNode definition = entry.getValue(); - fields.add(new FieldDefinition(tool.name(), "JSON_SCHEMA", direction, "", - entry.getKey(), definition.path("type").asText("object"), - required.contains(entry.getKey()), definition.path("description").asText(""), - constraints(definition), relative(path))); - } + collectSchemaFields(tool, root, "JSON_SCHEMA", direction, "", "", relative(path), fields); } catch (Exception exception) { diagnostics.add("Schema 분석 실패: " + relative(path)); } } + private void collectSchemaFields(ToolSummary tool, JsonNode schema, String sourceKind, String direction, + String ownerPath, String fieldPrefix, String sourceFile, + List fields) { + List required = new ArrayList<>(); + schema.path("required").forEach(node -> required.add(node.asText())); + Iterator> iterator = schema.path("properties").fields(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + JsonNode definition = entry.getValue(); + String fieldName = fieldPrefix.isBlank() ? entry.getKey() : fieldPrefix + "." + entry.getKey(); + String type = schemaType(definition); + fields.add(new FieldDefinition(tool.name(), sourceKind, direction, ownerPath, fieldName, type, + required.contains(entry.getKey()), definition.path("description").asText(""), + constraints(definition), sourceFile)); + JsonNode nested = definition.path("properties").isObject() ? definition : definition.path("items"); + if (nested.path("properties").isObject()) { + collectSchemaFields(tool, nested, sourceKind, direction, fieldName, fieldName, sourceFile, fields); + } + } + } + + private String schemaType(JsonNode definition) { + String type = definition.path("type").asText("object"); + if ("array".equals(type)) { + return "array<" + definition.path("items").path("type").asText("object") + ">"; + } + return type; + } + private void collectJavaType(ToolSummary tool, Path path, String sourceKind, String direction, List fields, List diagnostics) { if (path == null) { diff --git a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolSourceDiscovery.java b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolSourceDiscovery.java index c5dbbcc9..980fd217 100644 --- a/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolSourceDiscovery.java +++ b/dap-tool-report/src/main/java/io/shinhanlife/dap/report/source/ToolSourceDiscovery.java @@ -4,6 +4,9 @@ import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.AnnotationExpr; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.shinhanlife.dap.report.config.ReportProperties; import io.shinhanlife.dap.report.model.ToolSummary; import java.io.IOException; @@ -13,6 +16,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.stereotype.Component; @@ -22,6 +26,7 @@ import org.springframework.stereotype.Component; public class ToolSourceDiscovery { private final Path sourceRoot; + private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); public ToolSourceDiscovery(ReportProperties properties) { String configuredRoot = properties.sourceRoot(); @@ -45,15 +50,89 @@ public class ToolSourceDiscovery { } catch (IOException exception) { throw new IllegalStateException("Failed to scan tool sources: " + sourceRoot, exception); } - return tools.stream() + Map discovered = tools.stream() .sorted(Comparator.comparing(ToolSummary::sourceFile)) .collect(Collectors.toMap(ToolSummary::name, Function.identity(), - (first, duplicate) -> first, LinkedHashMap::new)) - .values().stream() + (first, duplicate) -> first, LinkedHashMap::new)); + discoverDefinitions().forEach((name, definition) -> + discovered.merge(name, definition, this::mergeDefinition)); + return discovered.values().stream() .sorted(Comparator.comparing(ToolSummary::name)) .toList(); } + private Map discoverDefinitions() { + Map definitions = new LinkedHashMap<>(); + try (var paths = Files.walk(sourceRoot)) { + paths.filter(this::isToolDefinition).sorted().forEach(path -> { + try { + JsonNode root = yamlMapper.readTree(path.toFile()); + String name = text(root, "name"); + if (name.isBlank()) return; + JsonNode description = root.path("description"); + String function = text(description, "function"); + definitions.putIfAbsent(name, new ToolSummary( + name, text(root, "display_name"), function, text(root, "category_key"), + text(root, "legacy_interface_id"), true, false, + bool(root, "read_only"), bool(root, "destructive"), bool(root, "idempotent"), false, + "", "", "", "", "", "", text(root, "version"), function, + text(description, "when_to_use"), text(description, "when_not_to_use"), + text(description, "io_limits"), text(root, "display_description"), + joined(root.path("example_queries")), joined(root.path("tags")), + joined(root.path("required_env_keys")), text(root, "owner_org"), relative(path))); + } catch (IOException exception) { + throw new IllegalStateException("Failed to parse tool definition: " + path, exception); + } + }); + } catch (IOException exception) { + throw new IllegalStateException("Failed to scan tool definitions: " + sourceRoot, exception); + } + return definitions; + } + + private ToolSummary mergeDefinition(ToolSummary source, ToolSummary definition) { + return new ToolSummary(source.name(), prefer(definition.title(), source.title()), + prefer(definition.description(), source.description()), + prefer(definition.categoryKey(), source.categoryKey()), + prefer(definition.mappingId(), source.mappingId()), source.register(), source.requiresApproval(), + definition.readOnlyHint(), definition.destructiveHint(), definition.idempotentHint(), + source.openWorldHint(), source.requestType(), source.responseType(), source.useCaseClass(), + source.sourceFile(), source.inputSchemaResource(), source.outputSchemaResource(), definition.version(), + definition.functionDescription(), definition.whenToUse(), definition.whenNotToUse(), + definition.ioLimits(), definition.displayDescription(), definition.exampleQueries(), definition.tags(), + definition.requiredEnvKeys(), definition.ownerOrg(), definition.definitionFile()); + } + + private boolean isToolDefinition(Path path) { + String normalized = path.toString().replace('\\', '/'); + String fileName = path.getFileName().toString().toLowerCase(); + return Files.isRegularFile(path) && (fileName.endsWith(".yml") || fileName.endsWith(".yaml")) + && normalized.contains("/src/main/resources/tool-definitions/"); + } + + private String text(JsonNode node, String field) { + return node.path(field).asText(""); + } + + private boolean bool(JsonNode node, String field) { + return node.path(field).asBoolean(false); + } + + private String joined(JsonNode node) { + if (!node.isArray()) return node.asText(""); + List values = new ArrayList<>(); + node.forEach(value -> values.add(value.asText())); + return String.join(" | ", values); + } + + private String prefer(String primary, String fallback) { + return primary == null || primary.isBlank() ? fallback : primary; + } + + private String relative(Path path) { + return sourceRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'); + } + private Path resolveProjectRoot(Path configuredPath) { Path candidate = configuredPath; while (candidate != null) { @@ -88,7 +167,8 @@ public class ToolSourceDiscovery { } private ToolSummary toSummary(Path path, String owner, MethodDeclaration method, AnnotationExpr tool) { - AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "ToolHint").orElse(null); + AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "GrowToolHint") + .or(() -> JavaAnnotationReader.find(method.getAnnotations(), "ToolHint")).orElse(null); AnnotationExpr annotations = JavaAnnotationReader.nested(tool, "annotations").orElse(null); String name = JavaAnnotationReader.string(tool, "name", method.getNameAsString()); String title = JavaAnnotationReader.string(tool, "title", name); diff --git a/dap-tool-report/src/main/resources/application.yml b/dap-tool-report/src/main/resources/application.yml index 62642200..4d742a5d 100644 --- a/dap-tool-report/src/main/resources/application.yml +++ b/dap-tool-report/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - address: ${REPORT_SERVER_ADDRESS:127.0.0.1} + address: ${REPORT_SERVER_ADDRESS:0.0.0.0} port: ${REPORT_SERVER_PORT:8092} spring: diff --git a/dap-tool-report/src/test/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriterTest.java b/dap-tool-report/src/test/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriterTest.java index 6bc98293..49cb09cb 100644 --- a/dap-tool-report/src/test/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriterTest.java +++ b/dap-tool-report/src/test/java/io/shinhanlife/dap/report/excel/ToolReportExcelWriterTest.java @@ -21,8 +21,9 @@ class ToolReportExcelWriterTest { byte[] content = new ToolReportExcelWriter().write(List.of(new ToolReportModel(tool, List.of(field), List.of()))); try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) { - assertThat(workbook.getNumberOfSheets()).isEqualTo(3); + assertThat(workbook.getNumberOfSheets()).isEqualTo(4); assertThat(workbook.getSheet("툴 기본정보").getRow(8).getCell(1).getStringCellValue()).isEqualTo(tool.name()); + assertThat(workbook.getSheet("툴 정의").getRow(8).getCell(1).getStringCellValue()).isEqualTo(tool.name()); assertThat(workbook.getSheet("수집 필드").getRow(8).getCell(5).getStringCellValue()).isEqualTo("customerNo"); assertThat(workbook.getSheet("툴 기본정보").getRow(0).getCell(0).getCellStyle().getFillForegroundColorColor().getARGBHex()) .endsWith("262626"); diff --git a/dap-tool-report/src/test/java/io/shinhanlife/dap/report/source/ToolSourceDiscoveryTest.java b/dap-tool-report/src/test/java/io/shinhanlife/dap/report/source/ToolSourceDiscoveryTest.java index daa2adaa..161d1ea6 100644 --- a/dap-tool-report/src/test/java/io/shinhanlife/dap/report/source/ToolSourceDiscoveryTest.java +++ b/dap-tool-report/src/test/java/io/shinhanlife/dap/report/source/ToolSourceDiscoveryTest.java @@ -16,6 +16,36 @@ class ToolSourceDiscoveryTest { assertThat(discovery.discover()) .extracting(tool -> tool.name()) - .contains("oth.cmm.customer.detail", "sms.sms.msg.send"); + .contains("cmm_customer_tool", "cmm_claim_search", "iam_system_status"); + + assertThat(discovery.discover().stream() + .filter(tool -> tool.name().equals("cmm_customer_tool")) + .findFirst().orElseThrow()) + .satisfies(tool -> { + assertThat(tool.version()).isEqualTo("1.0.0"); + assertThat(tool.mappingId()).isEqualTo("ONILD0320"); + assertThat(tool.definitionFile()).endsWith("tool-definitions/cmm/cmm_customer_tool.yml"); + assertThat(tool.whenToUse()).contains("고객 ID"); + }); + } + + @Test + void collectsJsonSchemaFieldsEmbeddedInToolDefinitionYaml() { + Path projectRoot = Path.of("..").toAbsolutePath().normalize(); + ToolSourceDiscovery discovery = new ToolSourceDiscovery( + new ReportProperties(projectRoot.toString(), "tool-report")); + var tool = discovery.discover().stream() + .filter(candidate -> candidate.name().equals("cmm_customer_tool")) + .findFirst().orElseThrow(); + + var report = new ToolDetailAnalyzer(discovery).analyze(tool); + + assertThat(report.fields()) + .anySatisfy(field -> { + assertThat(field.sourceKind()).isEqualTo("TOOL_DEFINITION"); + assertThat(field.direction()).isEqualTo("INPUT"); + assertThat(field.fieldName()).isEqualTo("csNo"); + assertThat(field.required()).isTrue(); + }); } } diff --git a/dap-was-lib/src/main/resources/static/tool-test-console.html b/dap-was-lib/src/main/resources/static/tool-test-console.html index 300410cd..85644db9 100644 --- a/dap-was-lib/src/main/resources/static/tool-test-console.html +++ b/dap-was-lib/src/main/resources/static/tool-test-console.html @@ -50,6 +50,7 @@ Chat Tester Console + Report