Merge remote-tracking branch 'origin/main'
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
This commit is contained in:
35
README.md
35
README.md
@@ -269,3 +269,38 @@ Tool 관련 공통 기능은 `dap-was-*` 모듈명만 기준으로 동작합니
|
||||
- Tool Scaffold는 Pod 이름을 Tool 함수명에 포함하지 않습니다. 함수명은 `도메인_비즈니스_행위` 형식입니다. 예: `cmm_notification_send`
|
||||
- Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다.
|
||||
- Tool Source Update 기능은 `dap-was-*` 아래의 `*UseCase.java`를 검색합니다.
|
||||
|
||||
## 14. BC-DAB-STD-003 Tool Schema V17 적용
|
||||
|
||||
각 Tool의 표준 명세는 Tool Pod별 다음 경로에서 관리합니다.
|
||||
|
||||
```text
|
||||
dap-was-{pod}/src/main/resources/tool-definitions/{categoryKey}/{toolName}.yml
|
||||
```
|
||||
|
||||
Tool 이름은 Pod 정보를 포함하지 않는 `도메인_서비스_행위` 형태의 영문 소문자 snake_case를 사용하며,
|
||||
정규식 `^[a-z][a-z0-9_]{2,63}$`을 만족해야 합니다. 예: `cmm_claim_search`.
|
||||
|
||||
필수 항목은 `name`, `display_name`, `version`, `category_key`, 설명 4개 요소(function, when_to_use,
|
||||
when_not_to_use, io_limits), `display_description`, 예시 질의 3~10건, 동작 힌트 3개(read_only,
|
||||
destructive, idempotent), `parameters_schema`입니다. 입력 Schema는 루트 `type: object`, 각 property의
|
||||
`description`, `additionalProperties: false`를 갖춰야 합니다. 선택 운영 항목은 `tags`,
|
||||
`legacy_interface_id`, `required_env_keys`, `owner_org`입니다.
|
||||
|
||||
기동 시 `tool-definitions/**/*.yml`을 한 번 읽어 이름 기준으로 캐시하고, `@McpTool` 실행 정보와 결합한
|
||||
동일한 `ToolMetadata`를 `/tool-manifest`, Tool Pod MCP, Gateway MCP에 사용합니다.
|
||||
|
||||
입력 Schema 우선순위는 `inputSchemaResource` → V17 `parameters_schema` → DTO 자동 생성이고, 출력은
|
||||
`outputSchemaResource` → 명시 Output Schema → `@McpOutputSchema` 기반 생성입니다. Output Schema를
|
||||
명시한 Tool만 최종 응답 검증을 수행합니다.
|
||||
|
||||
Scaffold 화면의 `Tool Schema V17 Metadata` 영역에서는 기능 설명, 사용/비사용 조건, 입출력 제한,
|
||||
표시 설명, 예시 질의, 태그와 소유 조직을 입력합니다. Java 소스와 함께 V17 YAML이 생성되며, 기본값은
|
||||
배포 전에 업무 담당자가 실제 의미에 맞게 검토해야 합니다.
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat validateMcpToolNames validateToolSchemaV17
|
||||
```
|
||||
|
||||
`bootJar`는 두 검증에 의존하므로 이름 중복, 필수 항목 누락, Java Tool과 YAML 명세 불일치가 있으면
|
||||
Docker 이미지 생성 전에 빌드가 실패합니다.
|
||||
|
||||
10
build.gradle
10
build.gradle
@@ -89,9 +89,19 @@ tasks.register('validateMcpToolNames', JavaExec) {
|
||||
args rootProject.projectDir.absolutePath
|
||||
}
|
||||
|
||||
tasks.register('validateToolSchemaV17', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Validates BC-DAB-STD-003 V17 definitions for every @McpTool.'
|
||||
dependsOn toolCoreProject.tasks.named('classes')
|
||||
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
|
||||
mainClass.set('io.shinhanlife.dap.lib.validation.ToolSchemaV17ValidationRunner')
|
||||
args rootProject.projectDir.absolutePath
|
||||
}
|
||||
|
||||
subprojects {
|
||||
// 배포용 Spring Boot JAR 생성 전에 Tool 이름 중복 검증을 강제합니다.
|
||||
tasks.matching { it.name == 'bootJar' }.configureEach {
|
||||
dependsOn rootProject.tasks.named('validateMcpToolNames')
|
||||
dependsOn rootProject.tasks.named('validateToolSchemaV17')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,19 @@ public class ScaffoldingController {
|
||||
if (inputFields.isEmpty()) {
|
||||
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false));
|
||||
}
|
||||
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
|
||||
req.get("functionDescription"),
|
||||
req.get("whenToUse"),
|
||||
req.get("whenNotToUse"),
|
||||
req.get("ioLimits"),
|
||||
req.get("displayDescription"),
|
||||
parseDelimited(req.get("exampleQueries")),
|
||||
parseDelimited(req.get("tags")),
|
||||
req.get("ownerOrg"));
|
||||
|
||||
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields, httpApiName);
|
||||
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType,
|
||||
moduleName, author, date, register, clientSystemCode, inputSchemaResource,
|
||||
outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions);
|
||||
} catch (Exception e) {
|
||||
return "오류 발생: " + e.getMessage();
|
||||
}
|
||||
@@ -151,9 +162,11 @@ public class ScaffoldingController {
|
||||
Generate an MCP Tool scaffold from the user request.
|
||||
Return JSON only. Do not add Markdown, explanations, or code fences.
|
||||
The response must have this exact shape:
|
||||
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true}]}
|
||||
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true}]}
|
||||
categoryKey must be exactly three lowercase letters or digits.
|
||||
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
|
||||
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
|
||||
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
|
||||
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal.
|
||||
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
|
||||
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
|
||||
@@ -163,15 +176,7 @@ public class ScaffoldingController {
|
||||
String response = generateAiContent(prompt, req.get("model"));
|
||||
ToolDraft draft = objectMapper.readValue(stripCodeFence(response), ToolDraft.class);
|
||||
ToolDraft validatedDraft = validateToolDraft(draft);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"baseName", validatedDraft.baseName(),
|
||||
"title", validatedDraft.title(),
|
||||
"description", validatedDraft.description(),
|
||||
"categoryKey", validatedDraft.categoryKey(),
|
||||
"routingType", validatedDraft.routingType(),
|
||||
"httpApiName", validatedDraft.httpApiName(),
|
||||
"inputFields", validatedDraft.inputFields(),
|
||||
"outputFields", validatedDraft.outputFields()));
|
||||
return ResponseEntity.ok(validatedDraft);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("error", "AI Tool 초안 생성 실패: " + safeMessage(e)));
|
||||
}
|
||||
@@ -219,6 +224,17 @@ public class ScaffoldingController {
|
||||
return objectMapper.readValue(source, new TypeReference<List<ToolScaffolder.FieldDefinition>>() { });
|
||||
}
|
||||
|
||||
private List<String> parseDelimited(String source) {
|
||||
if (source == null || source.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(source.split("[\\r\\n,]+"))
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isBlank())
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<ToolScaffolder.FieldDefinition> validateFields(List<ToolScaffolder.FieldDefinition> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
throw new IllegalArgumentException("AI가 필드를 생성하지 않았습니다.");
|
||||
@@ -268,10 +284,37 @@ public class ScaffoldingController {
|
||||
if (title.isBlank() || description.isBlank()) {
|
||||
throw new IllegalArgumentException("AI가 Tool 제목 또는 설명을 생성하지 않았습니다.");
|
||||
}
|
||||
String functionDescription = textOrDefault(draft.functionDescription(), description);
|
||||
String displayDescription = textOrDefault(draft.displayDescription(), title);
|
||||
String whenToUse = textOrDefault(draft.whenToUse(), description + " 요청을 처리할 때 사용한다.");
|
||||
String whenNotToUse = textOrDefault(draft.whenNotToUse(), "필수 입력값이 없거나 다른 업무 요청에는 사용하지 않는다.");
|
||||
String ioLimits = textOrDefault(draft.ioLimits(), "정의된 입력 필드만 허용하며 정의된 응답 DTO 범위만 반환한다.");
|
||||
List<String> exampleQueries = normalizedDraftList(draft.exampleQueries(), List.of(
|
||||
title + " 해줘", title + " 정보를 알려줘", title + " 결과를 확인해줘"));
|
||||
List<String> tags = normalizedDraftList(draft.tags(), List.of(categoryKey));
|
||||
String ownerOrg = textOrDefault(draft.ownerOrg(), "MCP_TOOL");
|
||||
return new ToolDraft(draft.baseName().trim(), title, description, categoryKey, routingType, httpApiName,
|
||||
functionDescription, displayDescription, whenToUse, whenNotToUse, ioLimits,
|
||||
exampleQueries, tags, ownerOrg,
|
||||
validateFields(draft.inputFields()), validateFields(draft.outputFields()));
|
||||
}
|
||||
|
||||
private String textOrDefault(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value.trim();
|
||||
}
|
||||
|
||||
private List<String> normalizedDraftList(List<String> values, List<String> fallback) {
|
||||
if (values == null) {
|
||||
return fallback;
|
||||
}
|
||||
List<String> normalized = values.stream()
|
||||
.filter(value -> value != null && !value.isBlank())
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
return normalized.isEmpty() ? fallback : normalized;
|
||||
}
|
||||
|
||||
private String generateAiContent(String prompt, String requestedModel) {
|
||||
return chatClientBuilder.build().prompt()
|
||||
.user(prompt)
|
||||
@@ -306,7 +349,10 @@ public class ScaffoldingController {
|
||||
}
|
||||
|
||||
private record ToolDraft(String baseName, String title, String description, String categoryKey, String routingType,
|
||||
String httpApiName, List<ToolScaffolder.FieldDefinition> inputFields,
|
||||
String httpApiName, String functionDescription, String displayDescription,
|
||||
String whenToUse, String whenNotToUse, String ioLimits,
|
||||
List<String> exampleQueries, List<String> tags, String ownerOrg,
|
||||
List<ToolScaffolder.FieldDefinition> inputFields,
|
||||
List<ToolScaffolder.FieldDefinition> outputFields) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package io.shinhanlife.dap.mcg.sync;
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.mcp.ToolMetadataMcpMapper;
|
||||
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
@@ -48,18 +49,7 @@ public class RegistryMcpToolSpecificationFactory {
|
||||
* Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다.
|
||||
*/
|
||||
public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) {
|
||||
McpSchema.Tool tool = McpSchema.Tool.builder()
|
||||
.name(entry.getName())
|
||||
.description(description(entry))
|
||||
.inputSchema(toJsonSchema(inputSchema(entry)))
|
||||
.annotations(new McpSchema.ToolAnnotations(
|
||||
entry.getDisplayName(),
|
||||
entry.getReadOnlyHint(),
|
||||
entry.getDestructiveHint(),
|
||||
entry.getIdempotentHint(),
|
||||
entry.getOpenWorldHint(),
|
||||
null))
|
||||
.build();
|
||||
McpSchema.Tool tool = ToolMetadataMcpMapper.toTool(entry);
|
||||
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
|
||||
@@ -869,6 +869,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 p-3 rounded" style="background:#18181b; border:1px solid #3f3f46;">
|
||||
<div class="form-label mb-2">Tool Schema V17 Metadata</div>
|
||||
<div class="input-hint mb-3">LLM이 Tool을 올바르게 선택하도록 기능·사용 조건·제외 조건·입출력 제한을 분리해 입력합니다.</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Function Description</label>
|
||||
<textarea class="form-control" name="functionDescription" rows="2" placeholder="이 Tool이 수행하는 핵심 기능"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Display Description</label>
|
||||
<textarea class="form-control" name="displayDescription" rows="2" placeholder="Portal에 보여줄 짧은 설명"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">When To Use</label>
|
||||
<textarea class="form-control" name="whenToUse" rows="2" placeholder="어떤 사용자 요청에서 이 Tool을 사용하는지"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">When Not To Use</label>
|
||||
<textarea class="form-control" name="whenNotToUse" rows="2" placeholder="이 Tool을 사용하면 안 되는 조건"></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">I/O Limits</label>
|
||||
<textarea class="form-control" name="ioLimits" rows="2" placeholder="허용되는 입력, 반환 범위, 건수 제한 등"></textarea>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Example Queries (3~10)</label>
|
||||
<textarea class="form-control" name="exampleQueries" rows="3" placeholder="한 줄에 하나씩 입력\n예: 사번 10001 직원 정보를 조회해줘"></textarea>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Tags</label>
|
||||
<input type="text" class="form-control" name="tags" placeholder="employee, search">
|
||||
<label class="form-label mt-3">Owner Organization</label>
|
||||
<input type="text" class="form-control" name="ownerOrg" value="MCP_TOOL">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Domain Category</label>
|
||||
@@ -1692,6 +1729,16 @@
|
||||
form.elements.categoryKey.value = result.categoryKey || '';
|
||||
form.elements.routingType.value = result.routingType || 'HTTP';
|
||||
form.elements.httpApiName.value = result.httpApiName || '';
|
||||
form.elements.functionDescription.value = result.functionDescription || '';
|
||||
form.elements.displayDescription.value = result.displayDescription || '';
|
||||
form.elements.whenToUse.value = result.whenToUse || '';
|
||||
form.elements.whenNotToUse.value = result.whenNotToUse || '';
|
||||
form.elements.ioLimits.value = result.ioLimits || '';
|
||||
form.elements.exampleQueries.value = Array.isArray(result.exampleQueries)
|
||||
? result.exampleQueries.join('\n') : (result.exampleQueries || '');
|
||||
form.elements.tags.value = Array.isArray(result.tags)
|
||||
? result.tags.join(', ') : (result.tags || '');
|
||||
form.elements.ownerOrg.value = result.ownerOrg || 'MCP_TOOL';
|
||||
document.getElementById('inputFields').value = JSON.stringify(result.inputFields || [], null, 2);
|
||||
document.getElementById('outputFields').value = JSON.stringify(result.outputFields || [], null, 2);
|
||||
alert('Tool 초안을 채웠습니다. Legacy Interface ID와 Client System Code는 실제 연계 명세를 확인해 입력해주세요.');
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.sync.RegistryMcpToolSpecificationFactory;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RegistryMcpToolSpecificationFactoryTest {
|
||||
@@ -15,7 +18,15 @@ class RegistryMcpToolSpecificationFactoryTest {
|
||||
void exposesMetadataBehaviorHintsInMcpToolSpecification() {
|
||||
ToolMetadata metadata = ToolMetadata.builder()
|
||||
.name("customer_lookup")
|
||||
.displayName("고객 조회")
|
||||
.description("Looks up customer information")
|
||||
.displayDescription("고객 기본 정보 조회")
|
||||
.semver("1.2.0")
|
||||
.categoryKey("cus")
|
||||
.exampleQueries(List.of("고객 10001을 조회해줘"))
|
||||
.tags(List.of("customer", "search"))
|
||||
.ownerOrg("CUSTOMER_TEAM")
|
||||
.outputSchema(Map.of("type", "object", "properties", Map.of()))
|
||||
.readOnlyHint(true)
|
||||
.destructiveHint(false)
|
||||
.idempotentHint(true)
|
||||
@@ -29,5 +40,9 @@ class RegistryMcpToolSpecificationFactoryTest {
|
||||
assertFalse(tool.annotations().destructiveHint());
|
||||
assertTrue(tool.annotations().idempotentHint());
|
||||
assertFalse(tool.annotations().openWorldHint());
|
||||
assertEquals("고객 조회", tool.title());
|
||||
assertEquals("object", tool.outputSchema().get("type"));
|
||||
assertEquals("1.2.0", tool.meta().get("version"));
|
||||
assertEquals("CUSTOMER_TEAM", tool.meta().get("owner_org"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class ScaffoldingControllerToolDraftTest {
|
||||
when(requestSpec.options(any(ChatOptions.class))).thenReturn(requestSpec);
|
||||
when(requestSpec.call()).thenReturn(responseSpec);
|
||||
when(responseSpec.content()).thenReturn("""
|
||||
{"baseName":"CustomerContractStatus","title":"계약 상태 조회","description":"고객번호로 계약 상태를 조회합니다.","categoryKey":"cmm","routingType":"HTTP","httpApiName":"contract-status","inputFields":[{"name":"customerId","type":"String","description":"고객번호","example":"C123","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"결과 코드","example":"SUCCESS","required":true}]}
|
||||
{"baseName":"CustomerContractStatus","title":"계약 상태 조회","description":"고객번호로 계약 상태를 조회합니다.","categoryKey":"cmm","routingType":"HTTP","httpApiName":"contract-status","functionDescription":"고객 계약의 현재 상태를 조회한다.","displayDescription":"고객 계약 상태 조회","whenToUse":"고객번호로 계약 상태 확인을 요청할 때 사용한다.","whenNotToUse":"계약 변경 또는 해지를 요청할 때는 사용하지 않는다.","ioLimits":"고객번호 한 건을 입력받아 계약 상태 한 건을 반환한다.","exampleQueries":["고객 C123의 계약 상태를 알려줘","C123 계약이 정상인지 확인해줘","고객번호 C123 계약 조회해줘"],"tags":["contract","status","search"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"customerId","type":"String","description":"고객번호","example":"C123","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"결과 코드","example":"SUCCESS","required":true}]}
|
||||
""");
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
new ScaffoldingController(builder, new ObjectMapper()))
|
||||
@@ -43,6 +43,14 @@ class ScaffoldingControllerToolDraftTest {
|
||||
.content("{\"description\":\"고객번호로 계약 상태를 조회\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.baseName").value("CustomerContractStatus"))
|
||||
.andExpect(jsonPath("$.functionDescription").value("고객 계약의 현재 상태를 조회한다."))
|
||||
.andExpect(jsonPath("$.displayDescription").value("고객 계약 상태 조회"))
|
||||
.andExpect(jsonPath("$.whenToUse").value("고객번호로 계약 상태 확인을 요청할 때 사용한다."))
|
||||
.andExpect(jsonPath("$.whenNotToUse").value("계약 변경 또는 해지를 요청할 때는 사용하지 않는다."))
|
||||
.andExpect(jsonPath("$.ioLimits").value("고객번호 한 건을 입력받아 계약 상태 한 건을 반환한다."))
|
||||
.andExpect(jsonPath("$.exampleQueries[0]").value("고객 C123의 계약 상태를 알려줘"))
|
||||
.andExpect(jsonPath("$.tags[0]").value("contract"))
|
||||
.andExpect(jsonPath("$.ownerOrg").value("MCP_TOOL"))
|
||||
.andExpect(jsonPath("$.inputFields[0].name").value("customerId"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ dependencies {
|
||||
|
||||
// MCI XML 전문, JSON Tool Schema/Manifest 처리에 사용합니다.
|
||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
|
||||
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||
api 'com.networknt:json-schema-validator:3.0.0'
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ public record ToolManifestItem(
|
||||
String title,
|
||||
String description,
|
||||
Map<String, Object> inputSchema,
|
||||
Map<String, Object> outputSchema,
|
||||
ToolManifestAnnotations annotations,
|
||||
@JsonProperty("_meta") ToolManifestMeta meta) {
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
package io.shinhanlife.dap.lib.manifest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Operational metadata exposed by the Tool Service manifest. */
|
||||
public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) {
|
||||
public record ToolManifestMeta(
|
||||
String version,
|
||||
long timeoutMillis,
|
||||
boolean enabled,
|
||||
List<String> exampleQueries,
|
||||
List<String> tags,
|
||||
String legacyInterfaceId,
|
||||
List<String> requiredEnvKeys,
|
||||
String ownerOrg) {
|
||||
}
|
||||
@@ -61,12 +61,14 @@ public class ToolManifestService {
|
||||
? tool.getName() : tool.getDisplayName();
|
||||
Map<String, Object> schema = tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema();
|
||||
return new ToolManifestItem(
|
||||
tool.getName(), endpoint(tool), title, tool.getDescription(), schema,
|
||||
tool.getName(), endpoint(tool), title, tool.getDescription(), schema, tool.getOutputSchema(),
|
||||
new ToolManifestAnnotations(title, isTrue(tool.getReadOnlyHint()), isTrue(tool.getDestructiveHint()),
|
||||
isTrue(tool.getIdempotentHint()), isTrue(tool.getOpenWorldHint())),
|
||||
new ToolManifestMeta(defaultString(tool.getSemver(), "1.0.0"),
|
||||
tool.getTimeoutMillis() == null ? DEFAULT_TIMEOUT_MILLIS : tool.getTimeoutMillis(),
|
||||
tool.getEnabled() == null || tool.getEnabled()));
|
||||
tool.getEnabled() == null || tool.getEnabled(),
|
||||
defaultList(tool.getExampleQueries()), defaultList(tool.getTags()),
|
||||
tool.getMciServiceId(), defaultList(tool.getRequiredEnvKeys()), tool.getOwnerOrg()));
|
||||
}
|
||||
|
||||
private void validate(List<ToolManifestItem> tools) {
|
||||
@@ -128,4 +130,8 @@ public class ToolManifestService {
|
||||
private String defaultString(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private List<String> defaultList(List<String> value) {
|
||||
return value == null ? List.of() : List.copyOf(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Registry 메타데이터를 MCP SDK Tool 명세로 일관되게 변환합니다. */
|
||||
public final class ToolMetadataMcpMapper {
|
||||
private ToolMetadataMcpMapper() {
|
||||
}
|
||||
|
||||
public static McpSchema.Tool toTool(ToolMetadata metadata) {
|
||||
McpSchema.Tool.Builder builder = McpSchema.Tool.builder()
|
||||
.name(metadata.getName())
|
||||
.title(defaultText(metadata.getDisplayName(), metadata.getName()))
|
||||
.description(defaultText(metadata.getDescription(), metadata.getName() + " Tool"))
|
||||
.inputSchema(toJsonSchema(metadata.getParametersSchema()))
|
||||
.annotations(new McpSchema.ToolAnnotations(
|
||||
metadata.getDisplayName(), metadata.getReadOnlyHint(), metadata.getDestructiveHint(),
|
||||
metadata.getIdempotentHint(), metadata.getOpenWorldHint(), null))
|
||||
.meta(meta(metadata));
|
||||
if (metadata.getOutputSchema() != null && !metadata.getOutputSchema().isEmpty()) {
|
||||
builder.outputSchema(metadata.getOutputSchema());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Map<String, Object> meta(ToolMetadata metadata) {
|
||||
Map<String, Object> meta = new LinkedHashMap<>();
|
||||
put(meta, "version", metadata.getSemver());
|
||||
put(meta, "category_key", metadata.getCategoryKey());
|
||||
put(meta, "display_description", metadata.getDisplayDescription());
|
||||
put(meta, "example_queries", metadata.getExampleQueries());
|
||||
put(meta, "tags", metadata.getTags());
|
||||
put(meta, "legacy_interface_id", metadata.getMciServiceId());
|
||||
put(meta, "required_env_keys", metadata.getRequiredEnvKeys());
|
||||
put(meta, "owner_org", metadata.getOwnerOrg());
|
||||
return Map.copyOf(meta);
|
||||
}
|
||||
|
||||
private static void put(Map<String, Object> target, String key, Object value) {
|
||||
if (value instanceof String text && !text.isBlank()) {
|
||||
target.put(key, text);
|
||||
} else if (value instanceof List<?> list && !list.isEmpty()) {
|
||||
target.put(key, List.copyOf(list));
|
||||
}
|
||||
}
|
||||
|
||||
private static String defaultText(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static McpSchema.JsonSchema toJsonSchema(Map<String, Object> source) {
|
||||
Map<String, Object> schema = source == null ? emptySchema() : source;
|
||||
return new McpSchema.JsonSchema(
|
||||
String.valueOf(schema.getOrDefault("type", "object")),
|
||||
schema.get("properties") instanceof Map<?, ?> properties
|
||||
? (Map<String, Object>) properties : Map.of(),
|
||||
schema.get("required") instanceof List<?> required ? (List<String>) required : List.of(),
|
||||
schema.get("additionalProperties") instanceof Boolean additionalProperties
|
||||
? additionalProperties : Boolean.TRUE,
|
||||
schema.get("$defs") instanceof Map<?, ?> defs ? (Map<String, Object>) defs : Map.of(),
|
||||
schema.get("definitions") instanceof Map<?, ?> definitions
|
||||
? (Map<String, Object>) definitions : Map.of());
|
||||
}
|
||||
|
||||
private static Map<String, Object> emptySchema() {
|
||||
return Map.of("type", "object", "properties", Map.of(), "additionalProperties", false);
|
||||
}
|
||||
}
|
||||
@@ -39,18 +39,7 @@ public class ToolPodMcpToolSynchronizer {
|
||||
}
|
||||
|
||||
private McpServerFeatures.SyncToolSpecification specification(ToolMetadata tool) {
|
||||
McpSchema.Tool mcpTool = McpSchema.Tool.builder()
|
||||
.name(tool.getName())
|
||||
.description(tool.getDescription() == null || tool.getDescription().isBlank() ? tool.getName() + " Tool" : tool.getDescription())
|
||||
.inputSchema(toJsonSchema(tool.getParametersSchema()))
|
||||
.annotations(new McpSchema.ToolAnnotations(
|
||||
tool.getDisplayName(),
|
||||
tool.getReadOnlyHint(),
|
||||
tool.getDestructiveHint(),
|
||||
tool.getIdempotentHint(),
|
||||
tool.getOpenWorldHint(),
|
||||
null))
|
||||
.build();
|
||||
McpSchema.Tool mcpTool = ToolMetadataMcpMapper.toTool(tool);
|
||||
return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool)
|
||||
.callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinition;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinitionRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
@@ -29,14 +31,15 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -49,7 +52,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
@Component
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(McpToolExecutionService.class)
|
||||
public class ToolRegistryHeartbeatSender {
|
||||
|
||||
@@ -58,6 +60,23 @@ public class ToolRegistryHeartbeatSender {
|
||||
private final McpProperties mcpProperties;
|
||||
private final RestClient restClient = RestClient.create();
|
||||
private final ToolSchemaResolver toolSchemaResolver;
|
||||
private final ToolDefinitionRepository toolDefinitionRepository;
|
||||
|
||||
@Autowired
|
||||
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper,
|
||||
McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver,
|
||||
@Nullable ToolDefinitionRepository toolDefinitionRepository) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.objectMapper = objectMapper;
|
||||
this.mcpProperties = mcpProperties;
|
||||
this.toolSchemaResolver = toolSchemaResolver;
|
||||
this.toolDefinitionRepository = toolDefinitionRepository;
|
||||
}
|
||||
|
||||
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper,
|
||||
McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver) {
|
||||
this(applicationContext, objectMapper, mcpProperties, toolSchemaResolver, null);
|
||||
}
|
||||
|
||||
@Value("${axhub.gateway.url:http://localhost:8081}")
|
||||
private String gatewayUrl;
|
||||
@@ -143,11 +162,18 @@ public class ToolRegistryHeartbeatSender {
|
||||
// TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction
|
||||
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
|
||||
meta.setParametersSchema(finalSchema);
|
||||
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(
|
||||
functionAnnotation, method.getReturnType(), hintAnnotation);
|
||||
if (!outputSchema.isEmpty()) {
|
||||
meta.setOutputSchema(outputSchema);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to generate schema for {}", subToolName, e);
|
||||
}
|
||||
}
|
||||
|
||||
enrichWithDefinition(meta, rawSubToolName, hintAnnotation);
|
||||
|
||||
if (isRegister) {
|
||||
registeredTools.add(meta);
|
||||
}
|
||||
@@ -158,6 +184,45 @@ public class ToolRegistryHeartbeatSender {
|
||||
}
|
||||
}
|
||||
|
||||
private void enrichWithDefinition(ToolMetadata meta, String rawToolName, ToolHint hintAnnotation) {
|
||||
if (toolDefinitionRepository == null) {
|
||||
return;
|
||||
}
|
||||
toolDefinitionRepository.findByName(rawToolName)
|
||||
.ifPresent(definition -> applyDefinition(meta, definition, hintAnnotation));
|
||||
}
|
||||
|
||||
private void applyDefinition(ToolMetadata meta, ToolDefinition definition, ToolHint hintAnnotation) {
|
||||
meta.setDisplayName(definition.displayName());
|
||||
meta.setSemver(definition.version());
|
||||
meta.setCategoryKey(definition.categoryKey());
|
||||
meta.setFunctionDescription(definition.description().function());
|
||||
meta.setWhenToUse(definition.description().whenToUse());
|
||||
meta.setWhenNotToUse(definition.description().whenNotToUse());
|
||||
meta.setIoLimits(definition.description().ioLimits());
|
||||
meta.setDescription(String.join("\n", definition.description().function(),
|
||||
"사용 시점: " + definition.description().whenToUse(),
|
||||
"사용 제외: " + definition.description().whenNotToUse(),
|
||||
"입출력 제한: " + definition.description().ioLimits()));
|
||||
meta.setDisplayDescription(definition.displayDescription());
|
||||
meta.setExampleQueries(definition.exampleQueries());
|
||||
meta.setReadOnlyHint(definition.readOnly());
|
||||
meta.setDestructiveHint(definition.destructive());
|
||||
meta.setIdempotentHint(definition.idempotent());
|
||||
boolean explicitInputResource = hintAnnotation != null && !hintAnnotation.inputSchemaResource().isBlank();
|
||||
boolean explicitOutputResource = hintAnnotation != null && !hintAnnotation.outputSchemaResource().isBlank();
|
||||
if (!explicitInputResource) {
|
||||
meta.setParametersSchema(definition.parametersSchema());
|
||||
}
|
||||
if (!explicitOutputResource && definition.outputSchema() != null && !definition.outputSchema().isEmpty()) {
|
||||
meta.setOutputSchema(definition.outputSchema());
|
||||
}
|
||||
meta.setTags(definition.tags());
|
||||
meta.setMciServiceId(definition.legacyInterfaceId());
|
||||
meta.setRequiredEnvKeys(definition.requiredEnvKeys());
|
||||
meta.setOwnerOrg(definition.ownerOrg());
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 30000)
|
||||
public void sendHeartbeats() {
|
||||
if (registeredTools.isEmpty()) return;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.shinhanlife.dap.lib.metadata;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** BC-DAB-STD-003 V17 Tool 정의 파일의 불변 모델입니다. */
|
||||
public record ToolDefinition(
|
||||
String name,
|
||||
@JsonProperty("display_name") String displayName,
|
||||
String version,
|
||||
@JsonProperty("category_key") String categoryKey,
|
||||
ToolDescription description,
|
||||
@JsonProperty("display_description") String displayDescription,
|
||||
@JsonProperty("example_queries") List<String> exampleQueries,
|
||||
@JsonProperty("read_only") Boolean readOnly,
|
||||
Boolean destructive,
|
||||
Boolean idempotent,
|
||||
@JsonProperty("parameters_schema") Map<String, Object> parametersSchema,
|
||||
@JsonProperty("output_schema") Map<String, Object> outputSchema,
|
||||
List<String> tags,
|
||||
@JsonProperty("legacy_interface_id") String legacyInterfaceId,
|
||||
@JsonProperty("required_env_keys") List<String> requiredEnvKeys,
|
||||
@JsonProperty("owner_org") String ownerOrg) {
|
||||
|
||||
public ToolDefinition withExampleQueries(List<String> queries) {
|
||||
return new ToolDefinition(name, displayName, version, categoryKey, description, displayDescription,
|
||||
queries, readOnly, destructive, idempotent, parametersSchema, outputSchema, tags, legacyInterfaceId,
|
||||
requiredEnvKeys, ownerOrg);
|
||||
}
|
||||
|
||||
public ToolDefinition withName(String value) {
|
||||
return new ToolDefinition(value, displayName, version, categoryKey, description, displayDescription,
|
||||
exampleQueries, readOnly, destructive, idempotent, parametersSchema, outputSchema, tags, legacyInterfaceId,
|
||||
requiredEnvKeys, ownerOrg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package io.shinhanlife.dap.lib.metadata;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** classpath의 Tool 정의를 기동 시 한 번 읽어 name 기준으로 캐시합니다. */
|
||||
@Component
|
||||
public class ToolDefinitionRepository {
|
||||
public static final String DEFAULT_LOCATION = "classpath*:tool-definitions/**/*.yml";
|
||||
|
||||
private final Map<String, ToolDefinition> definitions;
|
||||
|
||||
@Autowired
|
||||
public ToolDefinitionRepository(ResourceLoader resourceLoader) {
|
||||
this(new ObjectMapper(new YAMLFactory()), resourceLoader, DEFAULT_LOCATION);
|
||||
}
|
||||
|
||||
public ToolDefinitionRepository(ObjectMapper yamlMapper, ResourceLoader resourceLoader, String location) {
|
||||
this(yamlMapper, new PathMatchingResourcePatternResolver(resourceLoader), location);
|
||||
}
|
||||
|
||||
private ToolDefinitionRepository(ObjectMapper yamlMapper, ResourcePatternResolver resolver, String location) {
|
||||
this.definitions = Collections.unmodifiableMap(load(yamlMapper, resolver, location));
|
||||
}
|
||||
|
||||
public Optional<ToolDefinition> findByName(String name) {
|
||||
return Optional.ofNullable(definitions.get(name));
|
||||
}
|
||||
|
||||
public Map<String, ToolDefinition> findAll() {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private Map<String, ToolDefinition> load(ObjectMapper mapper, ResourcePatternResolver resolver, String location) {
|
||||
Map<String, ToolDefinition> loaded = new LinkedHashMap<>();
|
||||
try {
|
||||
for (Resource resource : resolver.getResources(location)) {
|
||||
ToolDefinition definition = mapper.readValue(resource.getInputStream(), ToolDefinition.class);
|
||||
String source = resource.getDescription();
|
||||
ToolDefinitionValidator.validate(definition, source);
|
||||
ToolDefinition previous = loaded.putIfAbsent(definition.name(), definition);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Duplicate Tool definition name: " + definition.name());
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Failed to load Tool definitions from " + location, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.shinhanlife.dap.lib.metadata;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Tool 정의가 BC-DAB-STD-003 V17 필수 규칙을 만족하는지 검증합니다. */
|
||||
public final class ToolDefinitionValidator {
|
||||
private static final Pattern TOOL_NAME = Pattern.compile("^[a-z][a-z0-9_]{2,63}$");
|
||||
|
||||
private ToolDefinitionValidator() {
|
||||
}
|
||||
|
||||
public static void validate(ToolDefinition definition, String source) {
|
||||
if (definition == null) {
|
||||
fail(source, "definition", "문서가 비어 있습니다");
|
||||
}
|
||||
if (isBlank(definition.name()) || !TOOL_NAME.matcher(definition.name()).matches()) {
|
||||
fail(source, "name", "^[a-z][a-z0-9_]{2,63}$ 형식이어야 합니다");
|
||||
}
|
||||
requireText(source, "display_name", definition.displayName());
|
||||
requireText(source, "version", definition.version());
|
||||
requireText(source, "category_key", definition.categoryKey());
|
||||
requireText(source, "display_description", definition.displayDescription());
|
||||
if (definition.description() == null) {
|
||||
fail(source, "description", "필수입니다");
|
||||
}
|
||||
requireText(source, "description.function", definition.description().function());
|
||||
requireText(source, "description.when_to_use", definition.description().whenToUse());
|
||||
requireText(source, "description.when_not_to_use", definition.description().whenNotToUse());
|
||||
requireText(source, "description.io_limits", definition.description().ioLimits());
|
||||
List<String> examples = definition.exampleQueries();
|
||||
if (examples == null || examples.size() < 3 || examples.size() > 10
|
||||
|| examples.stream().anyMatch(ToolDefinitionValidator::isBlank)) {
|
||||
fail(source, "example_queries", "비어 있지 않은 자연어 질의가 3~10개 필요합니다");
|
||||
}
|
||||
if (examples.stream().anyMatch(query -> query.contains(definition.name()))) {
|
||||
fail(source, "example_queries", "Tool name을 직접 포함할 수 없습니다");
|
||||
}
|
||||
if (definition.readOnly() == null || definition.destructive() == null || definition.idempotent() == null) {
|
||||
fail(source, "annotations", "read_only, destructive, idempotent는 필수입니다");
|
||||
}
|
||||
validateSchema(definition.parametersSchema(), source);
|
||||
if (definition.outputSchema() != null && !definition.outputSchema().isEmpty()) {
|
||||
validateSchema(definition.outputSchema(), source + " output_schema");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void validateSchema(Map<String, Object> schema, String source) {
|
||||
if (schema == null || !"object".equals(schema.get("type"))) {
|
||||
fail(source, "parameters_schema.type", "object여야 합니다");
|
||||
}
|
||||
if (!Boolean.FALSE.equals(schema.get("additionalProperties"))) {
|
||||
fail(source, "parameters_schema.additionalProperties", "false여야 합니다");
|
||||
}
|
||||
Object propertiesValue = schema.get("properties");
|
||||
if (!(propertiesValue instanceof Map<?, ?>)) {
|
||||
fail(source, "parameters_schema.properties", "object여야 합니다");
|
||||
}
|
||||
Map<?, ?> properties = (Map<?, ?>) propertiesValue;
|
||||
for (Map.Entry<?, ?> entry : properties.entrySet()) {
|
||||
if (!(entry.getValue() instanceof Map<?, ?> property)
|
||||
|| isBlank(String.valueOf(property.containsKey("description")
|
||||
? property.get("description") : ""))) {
|
||||
fail(source, "parameters_schema.properties." + entry.getKey() + ".description", "필수입니다");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireText(String source, String field, String value) {
|
||||
if (isBlank(value)) {
|
||||
fail(source, field, "필수입니다");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private static void fail(String source, String field, String message) {
|
||||
throw new IllegalStateException("Invalid Tool definition [" + source + "] " + field + ": " + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.shinhanlife.dap.lib.metadata;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/** LLM이 Tool 선택 여부를 판단할 때 사용하는 V17 목적 설명입니다. */
|
||||
public record ToolDescription(
|
||||
String function,
|
||||
@JsonProperty("when_to_use") String whenToUse,
|
||||
@JsonProperty("when_not_to_use") String whenNotToUse,
|
||||
@JsonProperty("io_limits") String ioLimits) {
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -72,7 +73,7 @@ public class PodScaffolder {
|
||||
implementation project(':dap-was-lib')
|
||||
}
|
||||
""";
|
||||
Files.writeString(modulePath.resolve("build.gradle"), buildGradle);
|
||||
writeUtf8(modulePath.resolve("build.gradle"), buildGradle);
|
||||
|
||||
log.append("[3/6] Dockerfile 생성 중...\n");
|
||||
String dockerfile = """
|
||||
@@ -84,7 +85,7 @@ public class PodScaffolder {
|
||||
EXPOSE %s
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
""".formatted(moduleName, portStr);
|
||||
Files.writeString(modulePath.resolve("Dockerfile"), dockerfile);
|
||||
writeUtf8(modulePath.resolve("Dockerfile"), dockerfile);
|
||||
|
||||
log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n");
|
||||
Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName);
|
||||
@@ -121,7 +122,7 @@ public class PodScaffolder {
|
||||
}
|
||||
}
|
||||
""".formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName));
|
||||
Files.writeString(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass);
|
||||
writeUtf8(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass);
|
||||
|
||||
Path resPath = modulePath.resolve("src/main/resources");
|
||||
Files.createDirectories(resPath);
|
||||
@@ -145,7 +146,7 @@ public class PodScaffolder {
|
||||
tenant-domains:
|
||||
TESTER-DEV: ALL
|
||||
""".formatted(portStr, moduleName, moduleName.replace("dap-", ""));
|
||||
Files.writeString(resPath.resolve("application.yml"), applicationYml);
|
||||
writeUtf8(resPath.resolve("application.yml"), applicationYml);
|
||||
|
||||
String applicationLocalYml = """
|
||||
# Local 환경 전용 설정 (H2 메모리 DB 등)
|
||||
@@ -194,7 +195,7 @@ public class PodScaffolder {
|
||||
tool:
|
||||
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
|
||||
""";
|
||||
Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml);
|
||||
writeUtf8(resPath.resolve("application-local.yml"), applicationLocalYml);
|
||||
|
||||
String applicationDevYml = """
|
||||
# OCI 클라우드 환경 전용 설정
|
||||
@@ -244,7 +245,7 @@ public class PodScaffolder {
|
||||
externalMci:
|
||||
url: http://10.176.32.176
|
||||
""".formatted(portStr, portStr);
|
||||
Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml);
|
||||
writeUtf8(resPath.resolve("application-dev.yml"), applicationDevYml);
|
||||
|
||||
String applicationTestYml = """
|
||||
server:
|
||||
@@ -264,7 +265,7 @@ public class PodScaffolder {
|
||||
tool:
|
||||
url: ${AXHUB_TOOL_URL}
|
||||
""".formatted(portStr);
|
||||
Files.writeString(resPath.resolve("application-test.yml"), applicationTestYml);
|
||||
writeUtf8(resPath.resolve("application-test.yml"), applicationTestYml);
|
||||
|
||||
String applicationProdYml = """
|
||||
server:
|
||||
@@ -284,7 +285,7 @@ public class PodScaffolder {
|
||||
tool:
|
||||
url: ${AXHUB_TOOL_URL}
|
||||
""".formatted(portStr);
|
||||
Files.writeString(resPath.resolve("application-prod.yml"), applicationProdYml);
|
||||
writeUtf8(resPath.resolve("application-prod.yml"), applicationProdYml);
|
||||
|
||||
String logbackXml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -312,21 +313,21 @@ public class PodScaffolder {
|
||||
<logger name="io.shinhanlife" level="DEBUG" />
|
||||
</configuration>
|
||||
""".formatted(moduleName, moduleName);
|
||||
Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml);
|
||||
writeUtf8(resPath.resolve("logback-spring.xml"), logbackXml);
|
||||
|
||||
log.append("[5/6] settings.gradle 에 모듈 등록 중...\n");
|
||||
Path settingsPath = rootDir.resolve(Paths.get("settings.gradle"));
|
||||
if (Files.exists(settingsPath)) {
|
||||
String settings = Files.readString(settingsPath);
|
||||
String settings = Files.readString(settingsPath, StandardCharsets.UTF_8);
|
||||
if (!settings.contains("include '" + moduleName + "'")) {
|
||||
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND);
|
||||
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardCharsets.UTF_8, StandardOpenOption.APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n");
|
||||
Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml"));
|
||||
if (Files.exists(dockerComposePath)) {
|
||||
String compose = Files.readString(dockerComposePath);
|
||||
String compose = Files.readString(dockerComposePath, StandardCharsets.UTF_8);
|
||||
String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment
|
||||
if (!compose.contains(" " + serviceName + ":")) {
|
||||
String newService = """
|
||||
@@ -352,7 +353,7 @@ public class PodScaffolder {
|
||||
- GLOW_COMMUNICATION_EAI_HOST=http://mci-mock
|
||||
- GLOW_COMMUNICATION_EAI_PORT=8080
|
||||
""".formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr);
|
||||
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND);
|
||||
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardCharsets.UTF_8, StandardOpenOption.APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +366,10 @@ public class PodScaffolder {
|
||||
return log.toString();
|
||||
}
|
||||
|
||||
private static void writeUtf8(Path path, String content) throws IOException {
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String capitalize(String str) {
|
||||
if (str == null || str.isEmpty()) return str;
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -14,28 +15,28 @@ import java.util.Set;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* MCP Tool 肄붾뱶瑜??먮룞 ?앹꽦(Scaffolding)?섎뒗 ?좏떥由ы떚 ?대옒??
|
||||
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스입니다.
|
||||
*
|
||||
* [?ㅽ뻾 諛⑸쾿]
|
||||
* 諛⑸쾿 1. IDE(IntelliJ ???먯꽌 吏곸젒 ?ㅽ뻾 (??뷀삎 紐⑤뱶 異붿쿇 狩?
|
||||
* - ???대옒??ToolScaffolder.java)瑜??닿퀬 main 硫붿꽌?쒕? 吏곸젒 ?ㅽ뻾(Run)?⑸땲??
|
||||
* - 肄섏넄 李쎌뿉 ?⑤뒗 吏덈Ц??李⑤??濡?媛믪쓣 ?낅젰?섍린留??섎㈃ ?뚯씪???앹꽦?⑸땲??
|
||||
* [실행 방법]
|
||||
* 방법 1. IDE(IntelliJ 등)에서 직접 실행
|
||||
* - ToolScaffolder.java의 main 메서드를 실행합니다.
|
||||
* - 콘솔 질문에 차례대로 값을 입력하면 파일이 생성됩니다.
|
||||
*
|
||||
* 諛⑸쾿 2. 而ㅻ㎤?쒕씪???곕????먯꽌 ?ㅽ뻾 (紐낅졊??湲곕컲)
|
||||
* - 而댄뙆?? javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java
|
||||
* - ?ㅽ뻾: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [?대쫫] [ID] "[?ㅻ챸]" "[洹몃9]" "[?듭떊諛⑹떇]" "[紐⑤뱢紐?"
|
||||
* 방법 2. 명령줄에서 실행
|
||||
* - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java
|
||||
* - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.util
|
||||
* @className ToolScaffolder
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 理쒖큹?앹꽦
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -47,6 +48,17 @@ public class ToolScaffolder {
|
||||
public record FieldDefinition(String name, String type, String description, String example, boolean required) {
|
||||
}
|
||||
|
||||
public record ToolDefinitionOptions(
|
||||
String functionDescription,
|
||||
String whenToUse,
|
||||
String whenNotToUse,
|
||||
String ioLimits,
|
||||
String displayDescription,
|
||||
List<String> exampleQueries,
|
||||
List<String> tags,
|
||||
String ownerOrg) {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
@@ -54,8 +66,8 @@ public class ToolScaffolder {
|
||||
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
|
||||
System.out.println("=========================================\n");
|
||||
|
||||
String baseName = getOrAsk(args, 0, scanner, "1. ?앹꽦??Tool??湲곕낯 ?대쫫 (?? ExchangeRate) [?곷Ц PascalCase]: ");
|
||||
String interfaceId = getOrAsk(args, 1, scanner, "2. ?덇굅??API ?명꽣?섏씠??ID (?? EXCH_001): ");
|
||||
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
|
||||
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
|
||||
String title = getOrAsk(args, 2, scanner, "3. Tool title: ");
|
||||
String description = getOrAsk(args, 3, scanner, "4. Tool description for LLM: ");
|
||||
String group = getOrAsk(args, 4, scanner, "5. Tool category: ");
|
||||
@@ -72,12 +84,12 @@ public class ToolScaffolder {
|
||||
String defaultAuthor = System.getProperty("user.name");
|
||||
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||
|
||||
String author = getOrAsk(args, 7, scanner, "7. ?묒꽦??(?뷀꽣 ?낅젰 ??'" + defaultAuthor + "'): ");
|
||||
String author = getOrAsk(args, 7, scanner, "8. 작성자(Enter 입력 시 '" + defaultAuthor + "'): ");
|
||||
if (author.trim().isEmpty()) author = defaultAuthor;
|
||||
String createDate = getOrAsk(args, 8, scanner, "8. ?묒꽦??(?뷀꽣 ?낅젰 ??'" + defaultDate + "'): ");
|
||||
String createDate = getOrAsk(args, 8, scanner, "9. 작성일(Enter 입력 시 '" + defaultDate + "'): ");
|
||||
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||
|
||||
String useSchemaResourceStr = getOrAsk(args, 9, scanner, "9. input/output JSON Schema ?뚯씪 ?먮룞 ?앹꽦 ?щ? (y/N): ");
|
||||
String useSchemaResourceStr = getOrAsk(args, 9, scanner, "10. input/output JSON Schema 파일 자동 생성 여부 (y/N): ");
|
||||
boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim());
|
||||
|
||||
String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/";
|
||||
@@ -125,6 +137,17 @@ public class ToolScaffolder {
|
||||
* Generates a Tool using an HTTP API name that is resolved from glow.communication.http.api-list.
|
||||
*/
|
||||
public static String scaffold(String baseName, String interfaceId, String title, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List<FieldDefinition> inputFields, List<FieldDefinition> outputFields, String httpApiName) throws IOException {
|
||||
return scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, createDate,
|
||||
register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields,
|
||||
httpApiName, null);
|
||||
}
|
||||
|
||||
public static String scaffold(String baseName, String interfaceId, String title, String description, String group,
|
||||
String routingType, String moduleName, String author, String createDate,
|
||||
boolean register, String clientSystemCode, String inputSchemaResource,
|
||||
String outputSchemaResource, List<FieldDefinition> inputFields,
|
||||
List<FieldDefinition> outputFields, String httpApiName,
|
||||
ToolDefinitionOptions definitionOptions) throws IOException {
|
||||
baseName = toPascalCase(baseName);
|
||||
title = title == null || title.isBlank() ? baseName : title.trim();
|
||||
description = description == null ? "" : description.trim();
|
||||
@@ -139,12 +162,13 @@ public class ToolScaffolder {
|
||||
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy"));
|
||||
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter"));
|
||||
|
||||
// schema resource ?뚯씪 寃쎈줈 (useSchemaResource=true ???뚮쭔 ?앹꽦)
|
||||
// Schema Resource 파일 경로(useSchemaResource=true일 때만 생성)
|
||||
boolean useSchemaResource = (inputSchemaResource != null && !inputSchemaResource.trim().isEmpty()) || (outputSchemaResource != null && !outputSchemaResource.trim().isEmpty());
|
||||
String schemaBaseName = toKebabCase(baseName);
|
||||
String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json";
|
||||
String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json";
|
||||
Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/tool-schemas", group.toLowerCase()));
|
||||
Path definitionDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/tool-definitions", group.toLowerCase()));
|
||||
String inputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + inputSchemaFileName;
|
||||
String outputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + outputSchemaFileName;
|
||||
|
||||
@@ -199,25 +223,25 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sRequest
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %sRequest {
|
||||
@McpToolParam(description = "?섏떊???꾪솕踰덊샇", required = true)
|
||||
@McpToolParam(description = "수신자 전화번호", required = true)
|
||||
-(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"})
|
||||
private String phoneNumber;
|
||||
|
||||
@McpToolParam(description = "?꾩넚??硫붿떆吏 ?댁슜", required = true)
|
||||
@McpToolParam(description = "전송할 메시지 내용", required = true)
|
||||
private String message;
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
@@ -227,9 +251,9 @@ public class ToolScaffolder {
|
||||
.replaceAll("(?m)^\\s*@McpToolParam\\([^\\r\\n]*\\)\\R", "")
|
||||
.replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "")
|
||||
.replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;")
|
||||
.replace("private String message;", "@Schema(example = \"?뚯뒪??硫붿떆吏?낅땲??\")\n private String message;");
|
||||
.replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;");
|
||||
reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent);
|
||||
writeUtf8(dtoDir.resolve(baseName + "Request.java"), reqContent);
|
||||
|
||||
// Generate Response DTO
|
||||
String resContent = """
|
||||
@@ -241,14 +265,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sResponse
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -263,7 +287,7 @@ public class ToolScaffolder {
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
resContent = dtoContent(bizPackage + ".dto", baseName + "Response", outputFields, author, createDate, false);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent);
|
||||
writeUtf8(dtoDir.resolve(baseName + "Response.java"), resContent);
|
||||
|
||||
String toolName = toToolName(moduleName, group, baseName);
|
||||
|
||||
@@ -287,14 +311,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.usecase
|
||||
* @className %sUseCase
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -315,7 +339,7 @@ public class ToolScaffolder {
|
||||
baseName, baseName
|
||||
);
|
||||
|
||||
Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
|
||||
writeUtf8(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
|
||||
|
||||
String serviceImplContent;
|
||||
|
||||
@@ -338,14 +362,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.usecase.impl
|
||||
* @className %sUseCaseImpl
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -359,9 +383,9 @@ public class ToolScaffolder {
|
||||
|
||||
@Override
|
||||
public %sResponse execute(%sRequest req) {
|
||||
log.info("[MCI Tool] {} ?붿껌 ?섏떊.", "%s");
|
||||
log.info("[MCI Tool] {} 요청 수신.", "%s");
|
||||
try {
|
||||
// MapStruct瑜??댁슜???먮룞 留ㅽ븨 (AI DTO -> MCI DTO)
|
||||
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
|
||||
%s_I mciReq = converter.toLegacyRequest(req);
|
||||
|
||||
Transfer<Object> resTransfer = mci.callTo(
|
||||
@@ -377,7 +401,7 @@ public class ToolScaffolder {
|
||||
: "MCI call completed without a response body.");
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("[MCI Tool] ?곕룞 以??ㅻ쪟 諛쒖깮: {}", e.getMessage(), e);
|
||||
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||
%sResponse response = new %sResponse();
|
||||
response.setResultCode("ERROR");
|
||||
response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||
@@ -437,14 +461,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.usecase.impl
|
||||
* @className %sUseCaseImpl
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -490,7 +514,7 @@ public class ToolScaffolder {
|
||||
);
|
||||
}
|
||||
|
||||
Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent);
|
||||
writeUtf8(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent);
|
||||
|
||||
if (isMci) {
|
||||
String mciReqContent = """
|
||||
@@ -501,32 +525,32 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.%s.io
|
||||
* @className %s_I
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %s_I {
|
||||
/**
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 ?섏떊??踰덊샇 ?뚮씪誘명꽣紐?
|
||||
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 硫붿떆吏 ?댁슜 ?뚮씪誘명꽣紐?
|
||||
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||
mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate);
|
||||
Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
|
||||
writeUtf8(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
|
||||
|
||||
String mciResContent = """
|
||||
package %s.%s.io;
|
||||
@@ -536,14 +560,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.%s.io
|
||||
* @className %s_O
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -553,7 +577,7 @@ public class ToolScaffolder {
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||
mciResContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_O", outputFields, author, createDate);
|
||||
Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
|
||||
writeUtf8(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
|
||||
|
||||
String converterContent = """
|
||||
package %s.converter;
|
||||
@@ -569,14 +593,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.converter
|
||||
* @className %sConverter
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -605,7 +629,7 @@ public class ToolScaffolder {
|
||||
baseName, interfaceId
|
||||
);
|
||||
converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId);
|
||||
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
|
||||
@@ -630,14 +654,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.%s
|
||||
* @className Mci%sClient
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -654,7 +678,7 @@ public class ToolScaffolder {
|
||||
BASE_PACKAGE, mciGroupPath.replace("/", "."),
|
||||
BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap
|
||||
);
|
||||
Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
|
||||
writeUtf8(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
|
||||
log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n");
|
||||
}
|
||||
|
||||
@@ -664,13 +688,13 @@ public class ToolScaffolder {
|
||||
String httpResponseClass = baseName + "HttpResponse";
|
||||
String httpClientClass = httpApiClass + "Client";
|
||||
|
||||
Files.writeString(httpIoDir.resolve(httpRequestClass + ".java"),
|
||||
writeUtf8(httpIoDir.resolve(httpRequestClass + ".java"),
|
||||
dtoContent(httpPackage + ".io", httpRequestClass, inputFields, author, createDate, true));
|
||||
Files.writeString(httpIoDir.resolve(httpResponseClass + ".java"),
|
||||
writeUtf8(httpIoDir.resolve(httpResponseClass + ".java"),
|
||||
dtoContent(httpPackage + ".io", httpResponseClass, outputFields, author, createDate, false));
|
||||
Files.writeString(httpClientDir.resolve(httpClientClass + ".java"),
|
||||
writeUtf8(httpClientDir.resolve(httpClientClass + ".java"),
|
||||
httpClientContent(httpPackage, httpClientClass, httpApiName));
|
||||
Files.writeString(converterDir.resolve(baseName + "Converter.java"),
|
||||
writeUtf8(converterDir.resolve(baseName + "Converter.java"),
|
||||
httpConverterContent(bizPackage, baseName, httpPackage));
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
@@ -693,32 +717,32 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.legacy
|
||||
* @className %sLegacyRequest
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %sLegacyRequest {
|
||||
/**
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 ?섏떊??踰덊샇 ?뚮씪誘명꽣紐?
|
||||
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 硫붿떆吏 ?댁슜 ?뚮씪誘명꽣紐?
|
||||
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
legacyReqContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyRequest", inputFields, author, createDate, true);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
|
||||
writeUtf8(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
|
||||
|
||||
String legacyResContent = """
|
||||
package %s.legacy;
|
||||
@@ -728,14 +752,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.legacy
|
||||
* @className %sLegacyResponse
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -745,7 +769,7 @@ public class ToolScaffolder {
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
legacyResContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyResponse", outputFields, author, createDate, true);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
|
||||
writeUtf8(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
|
||||
|
||||
String converterContent = """
|
||||
package %s.converter;
|
||||
@@ -761,14 +785,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.converter
|
||||
* @className %sConverter
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -795,7 +819,7 @@ public class ToolScaffolder {
|
||||
baseName, baseName, baseName, baseName, baseName, baseName, baseName
|
||||
);
|
||||
converterContent = legacyConverterContent(bizPackage, baseName);
|
||||
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
|
||||
@@ -808,7 +832,7 @@ public class ToolScaffolder {
|
||||
log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n");
|
||||
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
|
||||
}
|
||||
// schema resource ?뚯씪 ?앹꽦 (useSchemaResource=true ????
|
||||
// Schema Resource 파일 생성(useSchemaResource=true일 때)
|
||||
if (useSchemaResource) {
|
||||
Files.createDirectories(schemaDir);
|
||||
String inputSchema = """
|
||||
@@ -818,7 +842,7 @@ public class ToolScaffolder {
|
||||
"properties": {
|
||||
"TODO_FIELD": {
|
||||
"type": "string",
|
||||
"description": "TODO: ?뚮씪誘명꽣 ?ㅻ챸???낅젰?섏꽭??"
|
||||
"description": "TODO: 파라미터 설명을 입력하세요."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@@ -831,19 +855,19 @@ public class ToolScaffolder {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "泥섎━ 寃곌낵 ?곹깭 (SUCCESS / FAILURE)",
|
||||
"description": "처리 결과 상태 (SUCCESS / FAILURE)",
|
||||
"enum": ["SUCCESS", "FAILURE"]
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "泥섎━ 寃곌낵 硫붿떆吏"
|
||||
"description": "처리 결과 메시지"
|
||||
}
|
||||
},
|
||||
"required": ["status"]
|
||||
}
|
||||
""";
|
||||
Files.writeString(schemaDir.resolve(inputSchemaFileName), inputSchema);
|
||||
Files.writeString(schemaDir.resolve(outputSchemaFileName), outputSchema);
|
||||
writeUtf8(schemaDir.resolve(inputSchemaFileName), inputSchema);
|
||||
writeUtf8(schemaDir.resolve(outputSchemaFileName), outputSchema);
|
||||
log.append("[Input Schema] ").append(schemaDir.resolve(inputSchemaFileName)).append("\n");
|
||||
log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n");
|
||||
}
|
||||
@@ -858,9 +882,9 @@ public class ToolScaffolder {
|
||||
Files.createDirectories(wireMockBodyPath.getParent());
|
||||
Files.createDirectories(wireMockMappingPath.getParent());
|
||||
Files.createDirectories(podMockResponsePath.getParent());
|
||||
Files.writeString(wireMockBodyPath, mockResponse);
|
||||
Files.writeString(wireMockMappingPath, wireMockMappingContent(interfaceId, wireMockBodyPath.getFileName().toString()));
|
||||
Files.writeString(podMockResponsePath, mockResponse);
|
||||
writeUtf8(wireMockBodyPath, mockResponse);
|
||||
writeUtf8(wireMockMappingPath, wireMockMappingContent(interfaceId, wireMockBodyPath.getFileName().toString()));
|
||||
writeUtf8(podMockResponsePath, mockResponse);
|
||||
ensureLocalHttpApiConfiguration(projectRoot, httpApiName, toolName);
|
||||
log.append("[WireMock Response] ").append(wireMockBodyPath).append("\n");
|
||||
log.append("[WireMock Mapping] ").append(wireMockMappingPath).append("\n");
|
||||
@@ -868,21 +892,194 @@ public class ToolScaffolder {
|
||||
} else {
|
||||
Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json"));
|
||||
Files.createDirectories(mockResponsePath.getParent());
|
||||
Files.writeString(mockResponsePath, mockResponse);
|
||||
writeUtf8(mockResponsePath, mockResponse);
|
||||
log.append("[Mock Response] ").append(mockResponsePath).append("\n");
|
||||
}
|
||||
|
||||
Path generatedTestDir = rootDir.resolve(Paths.get(moduleName, "src/test/java/io/shinhanlife/dap/mcc/biz", group.toLowerCase(), "usecase"));
|
||||
Files.createDirectories(generatedTestDir);
|
||||
Path generatedTestPath = generatedTestDir.resolve(baseName + "UseCaseTest.java");
|
||||
Files.writeString(generatedTestPath, useCaseTestContent(bizPackage, baseName));
|
||||
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");
|
||||
log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n");
|
||||
|
||||
return log.toString();
|
||||
}
|
||||
|
||||
private static String toolDefinitionContent(String toolName, String title, String description,
|
||||
String categoryKey, String interfaceId,
|
||||
List<FieldDefinition> inputFields, boolean mutation) {
|
||||
String safeDescription = description == null || description.isBlank()
|
||||
? title + " 기능을 수행한다." : description;
|
||||
StringBuilder properties = new StringBuilder();
|
||||
StringBuilder required = new StringBuilder();
|
||||
Set<String> generatedNames = new LinkedHashSet<>();
|
||||
for (FieldDefinition field : inputFields == null ? List.<FieldDefinition>of() : inputFields) {
|
||||
if (field == null || field.name() == null || field.name().isBlank()
|
||||
|| !generatedNames.add(field.name().trim())) {
|
||||
continue;
|
||||
}
|
||||
properties.append(" ").append(field.name()).append(":\n")
|
||||
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
|
||||
.append(" description: ").append(yamlText(field.description())).append("\n");
|
||||
if (field.required()) {
|
||||
required.append(" - ").append(field.name()).append("\n");
|
||||
}
|
||||
}
|
||||
if (properties.isEmpty()) {
|
||||
properties.append(" {}\n");
|
||||
}
|
||||
String requiredBlock = required.isEmpty() ? "" : " required:\n" + required;
|
||||
String legacyLine = interfaceId == null || interfaceId.isBlank()
|
||||
? "" : "legacy_interface_id: " + yamlText(interfaceId) + "\n";
|
||||
return """
|
||||
name: %s
|
||||
display_name: %s
|
||||
version: 1.0.0
|
||||
category_key: %s
|
||||
description:
|
||||
function: %s
|
||||
when_to_use: 사용자가 이 업무 기능의 실행 또는 조회를 명확히 요청한 경우 사용한다.
|
||||
when_not_to_use: 입력값이 확인되지 않았거나 다른 업무 기능이 더 적합한 경우에는 사용하지 않는다.
|
||||
io_limits: 정의된 입력 항목만 허용하며 응답 DTO에 정의된 업무 결과만 반환한다.
|
||||
display_description: %s
|
||||
example_queries:
|
||||
- %s 처리해줘
|
||||
- %s 정보를 확인해줘
|
||||
- %s 업무 결과를 알려줘
|
||||
read_only: %s
|
||||
destructive: %s
|
||||
idempotent: %s
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
%s%s additionalProperties: false
|
||||
tags: [%s]
|
||||
%srequired_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
""".formatted(toolName, yamlText(title), categoryKey.toLowerCase(Locale.ROOT),
|
||||
yamlText(safeDescription), yamlText(title), yamlText(title), yamlText(title), yamlText(title),
|
||||
!mutation, mutation, !mutation, properties, requiredBlock,
|
||||
categoryKey.toLowerCase(Locale.ROOT), legacyLine);
|
||||
}
|
||||
|
||||
private static String toolDefinitionContentV17(String toolName, String title, String description,
|
||||
String categoryKey, String interfaceId,
|
||||
List<FieldDefinition> inputFields, boolean mutation,
|
||||
ToolDefinitionOptions options) {
|
||||
String function = option(options == null ? null : options.functionDescription(),
|
||||
option(description, title + " 기능을 수행한다."));
|
||||
String whenToUse = option(options == null ? null : options.whenToUse(),
|
||||
"사용자가 해당 업무 기능의 실행 또는 조회를 명확히 요청한 경우 사용한다.");
|
||||
String whenNotToUse = option(options == null ? null : options.whenNotToUse(),
|
||||
"필수 입력값이 확인되지 않았거나 다른 업무 기능이 더 적합한 경우에는 사용하지 않는다.");
|
||||
String ioLimits = option(options == null ? null : options.ioLimits(),
|
||||
"정의된 입력 항목만 허용하며 응답 DTO에 정의된 업무 결과만 반환한다.");
|
||||
String displayDescription = option(options == null ? null : options.displayDescription(), title);
|
||||
List<String> examples = normalizedList(options == null ? null : options.exampleQueries(), List.of(
|
||||
title + " 처리해줘", title + " 정보를 확인해줘", title + " 업무 결과를 알려줘"));
|
||||
List<String> tags = normalizedList(options == null ? null : options.tags(),
|
||||
List.of(categoryKey.toLowerCase(Locale.ROOT)));
|
||||
String ownerOrg = option(options == null ? null : options.ownerOrg(), "MCP_TOOL");
|
||||
|
||||
StringBuilder properties = new StringBuilder();
|
||||
StringBuilder required = new StringBuilder();
|
||||
Set<String> generatedNames = new LinkedHashSet<>();
|
||||
for (FieldDefinition field : inputFields == null ? List.<FieldDefinition>of() : inputFields) {
|
||||
if (field == null || field.name() == null || field.name().isBlank()
|
||||
|| !generatedNames.add(field.name().trim())) {
|
||||
continue;
|
||||
}
|
||||
properties.append(" ").append(field.name().trim()).append(":\n")
|
||||
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
|
||||
.append(" description: ").append(yamlText(field.description())).append("\n");
|
||||
if (field.required()) {
|
||||
required.append(" - ").append(field.name().trim()).append("\n");
|
||||
}
|
||||
}
|
||||
if (properties.isEmpty()) {
|
||||
properties.append(" {}\n");
|
||||
}
|
||||
String requiredBlock = required.isEmpty() ? "" : " required:\n" + required;
|
||||
String legacyLine = interfaceId == null || interfaceId.isBlank()
|
||||
? "" : "legacy_interface_id: " + yamlText(interfaceId) + "\n";
|
||||
String exampleBlock = examples.stream().map(value -> " - " + yamlText(value))
|
||||
.collect(java.util.stream.Collectors.joining("\n"));
|
||||
String tagBlock = tags.stream().map(ToolScaffolder::yamlText)
|
||||
.collect(java.util.stream.Collectors.joining(", "));
|
||||
|
||||
return """
|
||||
name: %s
|
||||
display_name: %s
|
||||
version: 1.0.0
|
||||
category_key: %s
|
||||
description:
|
||||
function: %s
|
||||
when_to_use: %s
|
||||
when_not_to_use: %s
|
||||
io_limits: %s
|
||||
display_description: %s
|
||||
example_queries:
|
||||
%s
|
||||
read_only: %s
|
||||
destructive: %s
|
||||
idempotent: %s
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
%s%s additionalProperties: false
|
||||
tags: [%s]
|
||||
%srequired_env_keys: []
|
||||
owner_org: %s
|
||||
""".formatted(toolName, yamlText(title), categoryKey.toLowerCase(Locale.ROOT),
|
||||
yamlText(function), yamlText(whenToUse), yamlText(whenNotToUse), yamlText(ioLimits),
|
||||
yamlText(displayDescription), exampleBlock, !mutation, mutation, !mutation,
|
||||
properties, requiredBlock, tagBlock, legacyLine, yamlText(ownerOrg));
|
||||
}
|
||||
|
||||
private static String option(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value.trim();
|
||||
}
|
||||
|
||||
private static List<String> normalizedList(List<String> values, List<String> fallback) {
|
||||
if (values == null) {
|
||||
return fallback;
|
||||
}
|
||||
List<String> normalized = values.stream()
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isBlank())
|
||||
.distinct()
|
||||
.toList();
|
||||
return normalized.isEmpty() ? fallback : normalized;
|
||||
}
|
||||
|
||||
private static boolean isMutationTool(String baseName) {
|
||||
String value = baseName.toLowerCase(Locale.ROOT);
|
||||
return value.matches(".*(create|add|update|delete|remove|send|process|approve|reject|register|issue).*");
|
||||
}
|
||||
|
||||
private static String jsonSchemaType(String javaType) {
|
||||
return switch (javaType == null ? "String" : javaType) {
|
||||
case "Integer", "Long" -> "integer";
|
||||
case "Double", "BigDecimal" -> "number";
|
||||
case "Boolean" -> "boolean";
|
||||
default -> "string";
|
||||
};
|
||||
}
|
||||
|
||||
private static String yamlText(String value) {
|
||||
String safe = value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
.replace("\r", " ").replace("\n", " ");
|
||||
return "\"" + safe + "\"";
|
||||
}
|
||||
|
||||
private static String toKebabCase(String pascalCase) {
|
||||
if (pascalCase == null || pascalCase.isEmpty()) return pascalCase;
|
||||
return pascalCase
|
||||
@@ -893,7 +1090,7 @@ public class ToolScaffolder {
|
||||
private static void ensureLocalHttpApiConfiguration(Path projectRoot, String httpApiName, String toolName) throws IOException {
|
||||
Path localConfigPath = projectRoot.resolve("dap-was-lib/src/main/resources/glow/application-glow-local.yml");
|
||||
Files.createDirectories(localConfigPath.getParent());
|
||||
String existing = Files.exists(localConfigPath) ? Files.readString(localConfigPath) : "";
|
||||
String existing = Files.exists(localConfigPath) ? Files.readString(localConfigPath, StandardCharsets.UTF_8) : "";
|
||||
if (java.util.regex.Pattern.compile("(?m)^\\s*-\\s+name:\\s*"
|
||||
+ java.util.regex.Pattern.quote(httpApiName) + "\\s*$").matcher(existing).find()) {
|
||||
return;
|
||||
@@ -937,7 +1134,11 @@ public class ToolScaffolder {
|
||||
enabled: true
|
||||
""";
|
||||
}
|
||||
Files.writeString(localConfigPath, existing);
|
||||
writeUtf8(localConfigPath, existing);
|
||||
}
|
||||
|
||||
private static void writeUtf8(Path path, String content) throws IOException {
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String dtoContent(String packageName, String className, List<FieldDefinition> fields,
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Comparator;
|
||||
|
||||
/** Updates MCP SDK and project-owned metadata in a generated tool source file. */
|
||||
@@ -27,10 +28,10 @@ public final class ToolSourceUpdater {
|
||||
throw new IllegalArgumentException("Tool source not found: " + toolName);
|
||||
}
|
||||
|
||||
String content = Files.readString(targetFile);
|
||||
String content = Files.readString(targetFile, StandardCharsets.UTF_8);
|
||||
content = updateMcpTool(content, toolName, description);
|
||||
content = updateToolHint(content, categoryKey, register, requiresApproval);
|
||||
Files.writeString(targetFile, content);
|
||||
Files.writeString(targetFile, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static Path findToolSource(Path rootDirectory, String toolName) throws IOException {
|
||||
@@ -47,7 +48,7 @@ public final class ToolSourceUpdater {
|
||||
|
||||
private static boolean containsMcpTool(Path path, String toolName) {
|
||||
try {
|
||||
return annotationArguments(Files.readString(path), "McpTool", toolName) != null;
|
||||
return annotationArguments(Files.readString(path, StandardCharsets.UTF_8), "McpTool", toolName) != null;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Failed to read tool source: " + path, exception);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package io.shinhanlife.dap.lib.validation;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinition;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinitionValidator;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** CI/CD에서 모든 Java MCP Tool과 V17 정의 파일의 1:1 대응을 검증합니다. */
|
||||
public final class ToolSchemaV17ValidationRunner {
|
||||
private static final Pattern MCP_TOOL_NAME = Pattern.compile(
|
||||
"@McpTool\\s*\\(\\s*name\\s*=\\s*\"([^\"]+)\"");
|
||||
|
||||
private ToolSchemaV17ValidationRunner() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 1) {
|
||||
throw new IllegalArgumentException("Usage: ToolSchemaV17ValidationRunner <project-root>");
|
||||
}
|
||||
validate(Path.of(args[0]));
|
||||
}
|
||||
|
||||
static void validate(Path projectRoot) {
|
||||
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
Map<String, Path> definitions = new LinkedHashMap<>();
|
||||
Set<String> toolNames = new LinkedHashSet<>();
|
||||
try (Stream<Path> files = Files.walk(projectRoot)) {
|
||||
for (Path file : files.filter(Files::isRegularFile).toList()) {
|
||||
String normalized = file.toString().replace('\\', '/');
|
||||
if (normalized.contains("/build/") || normalized.contains("/.gradle/")
|
||||
|| normalized.contains("/tmp_") || normalized.contains("/org/")) {
|
||||
continue;
|
||||
}
|
||||
if (normalized.endsWith(".java") && normalized.contains("/dap-was-")
|
||||
&& !normalized.contains("/dap-was-lib/")) {
|
||||
Matcher matcher = MCP_TOOL_NAME.matcher(Files.readString(file, StandardCharsets.UTF_8));
|
||||
while (matcher.find()) {
|
||||
toolNames.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
if (normalized.contains("/src/main/resources/tool-definitions/")
|
||||
&& (normalized.endsWith(".yml") || normalized.endsWith(".yaml"))) {
|
||||
ToolDefinition definition = yamlMapper.readValue(file.toFile(), ToolDefinition.class);
|
||||
ToolDefinitionValidator.validate(definition, file.toString());
|
||||
Path previous = definitions.putIfAbsent(definition.name(), file);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Duplicate V17 Tool definition: " + definition.name()
|
||||
+ " [" + previous + ", " + file + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Failed to scan Tool schema V17 files", exception);
|
||||
}
|
||||
Set<String> missing = new LinkedHashSet<>(toolNames);
|
||||
missing.removeAll(definitions.keySet());
|
||||
if (!missing.isEmpty()) {
|
||||
throw new IllegalStateException("Missing V17 Tool definitions: " + missing);
|
||||
}
|
||||
Set<String> orphan = new LinkedHashSet<>(definitions.keySet());
|
||||
orphan.removeAll(toolNames);
|
||||
if (!orphan.isEmpty()) {
|
||||
throw new IllegalStateException("V17 definitions without matching @McpTool: " + orphan);
|
||||
}
|
||||
System.out.println("Tool schema V17 validation passed: " + toolNames.size() + " tools");
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,19 @@ public class ToolMetadata {
|
||||
private String displayName; // 사람이 읽는 라벨 (1-128자)
|
||||
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||
private String functionDescription;
|
||||
private String whenToUse;
|
||||
private String whenNotToUse;
|
||||
private String ioLimits;
|
||||
private String displayDescription;
|
||||
private List<String> exampleQueries;
|
||||
private List<String> tags;
|
||||
private String ownerOrg;
|
||||
private List<String> requiredEnvKeys;
|
||||
|
||||
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||
private Map<String, Object> parametersSchema;
|
||||
private Map<String, Object> outputSchema;
|
||||
|
||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||
private Map<String, String> actionPrompts;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.shinhanlife.dap.lib.metadata;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
|
||||
class ToolDefinitionRepositoryTest {
|
||||
|
||||
@Test
|
||||
void springCanCreateRepositoryWithoutDefaultConstructor() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(ToolDefinitionRepository.class);
|
||||
context.refresh();
|
||||
|
||||
assertTrue(context.getBean(ToolDefinitionRepository.class)
|
||||
.findByName("cmm_claim_search").isPresent());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadsAndCachesAValidV17DefinitionByToolName() {
|
||||
ToolDefinitionRepository repository = new ToolDefinitionRepository(
|
||||
new ObjectMapper(new YAMLFactory()), new DefaultResourceLoader(),
|
||||
"classpath*:tool-definitions/**/*.yml");
|
||||
|
||||
ToolDefinition definition = repository.findByName("cmm_claim_search").orElseThrow();
|
||||
|
||||
assertEquals("보험금 청구 상태 조회", definition.displayName());
|
||||
assertEquals("cmm", definition.categoryKey());
|
||||
assertEquals(3, definition.exampleQueries().size());
|
||||
assertEquals(false, definition.parametersSchema().get("additionalProperties"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDefinitionWithFewerThanThreeExampleQueries() {
|
||||
ToolDefinition invalid = validDefinition().withExampleQueries(java.util.List.of("청구 상태 알려줘"));
|
||||
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> ToolDefinitionValidator.validate(invalid, "memory:invalid"));
|
||||
|
||||
assertTrue(error.getMessage().contains("example_queries"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonStandardToolName() {
|
||||
ToolDefinition invalid = validDefinition().withName("cmm_claim.Search");
|
||||
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> ToolDefinitionValidator.validate(invalid, "memory:invalid"));
|
||||
|
||||
assertTrue(error.getMessage().contains("name"));
|
||||
}
|
||||
|
||||
private ToolDefinition validDefinition() {
|
||||
return new ToolDefinition(
|
||||
"cmm_claim_search", "보험금 청구 상태 조회", "1.0.0", "cmm",
|
||||
new ToolDescription("청구 상태를 조회한다.", "상태 확인 시 사용한다.",
|
||||
"청구 접수 시 사용하지 않는다.", "청구번호가 필요하다."),
|
||||
"보험금 청구 상태를 조회합니다.",
|
||||
java.util.List.of("청구 상태 알려줘", "심사 결과 조회해줘", "계약번호로 청구를 찾아줘"),
|
||||
true, false, true,
|
||||
java.util.Map.of("type", "object", "properties", java.util.Map.of(),
|
||||
"additionalProperties", false),
|
||||
null,
|
||||
java.util.List.of("보험금"), null, java.util.List.of(), "MCP_TOOL");
|
||||
}
|
||||
}
|
||||
@@ -91,18 +91,25 @@ class JsonSchemaGeneratorTest {
|
||||
|
||||
private static class ValidatedRequest {
|
||||
@McpToolParam(description = "recipient phone number", required = true)
|
||||
@io.swagger.v3.oas.annotations.media.Schema(pattern = "^01[0-9]{8,9}$")
|
||||
private String phoneNumber;
|
||||
|
||||
@McpToolParam(description = "issue amount", required = true)
|
||||
@io.swagger.v3.oas.annotations.media.Schema(minimum = "1")
|
||||
private Long amount;
|
||||
|
||||
@McpToolParam(description = "approval result")
|
||||
@io.swagger.v3.oas.annotations.media.Schema(
|
||||
requiredMode = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED,
|
||||
allowableValues = {"APPROVE", "REJECT"})
|
||||
private String approvalStatus;
|
||||
|
||||
@McpToolParam(description = "page size")
|
||||
@io.swagger.v3.oas.annotations.media.Schema(maximum = "50", defaultValue = "20")
|
||||
private Integer pageSize;
|
||||
|
||||
@McpToolParam(description = "reference")
|
||||
@io.swagger.v3.oas.annotations.media.Schema(minLength = 1, maxLength = 30)
|
||||
private String reference;
|
||||
}
|
||||
|
||||
@@ -115,6 +122,9 @@ class JsonSchemaGeneratorTest {
|
||||
}
|
||||
|
||||
private static class NestedChild {
|
||||
@io.swagger.v3.oas.annotations.media.Schema(
|
||||
requiredMode = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED,
|
||||
pattern = "^\\d{8}$")
|
||||
private String businessDate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,109 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinition;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinitionValidator;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ToolScaffolderTest {
|
||||
|
||||
@Test
|
||||
void generatesEveryToolSourceAsUtf8WithoutBrokenKoreanOrBom() throws Exception {
|
||||
String moduleName = root.resolve("dap-was-korean").toString();
|
||||
|
||||
ToolScaffolder.scaffold("analysis data query", "CLYMCI00001", "분석 데이터 조회",
|
||||
"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.",
|
||||
"cmm", "MCI", moduleName, "테스터", "2026.08.12",
|
||||
false, null, null, null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("query", "String", "조회 조건", "계약 분석", true)),
|
||||
List.of(new ToolScaffolder.FieldDefinition("analysisResult", "String", "분석 결과", "정상", false)));
|
||||
|
||||
Path sourceRoot = root.resolve("dap-was-korean/src/main/java/io/shinhanlife/dap/mcc");
|
||||
Path useCase = sourceRoot.resolve("biz/cmm/usecase/AnalysisDataQueryUseCase.java");
|
||||
Path implementation = sourceRoot.resolve("biz/cmm/usecase/impl/AnalysisDataQueryUseCaseImpl.java");
|
||||
|
||||
String useCaseSource = Files.readString(useCase, StandardCharsets.UTF_8);
|
||||
String implementationSource = Files.readString(implementation, StandardCharsets.UTF_8);
|
||||
assertTrue(useCaseSource.contains("title = \"분석 데이터 조회\""), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("description = \"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.\""), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("AX HUB 시스템 처리 클래스"), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("개정이력"), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("최초생성"), useCaseSource);
|
||||
assertTrue(implementationSource.contains("요청 수신"), implementationSource);
|
||||
assertTrue(implementationSource.contains("MapStruct를 이용한 자동 매핑"), implementationSource);
|
||||
assertTrue(implementationSource.contains("연동 중 오류 발생"), implementationSource);
|
||||
|
||||
try (var files = Files.walk(root.resolve("dap-was-korean"))) {
|
||||
for (Path file : files.filter(Files::isRegularFile).toList()) {
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
assertFalse(bytes.length >= 3
|
||||
&& (bytes[0] & 0xff) == 0xef
|
||||
&& (bytes[1] & 0xff) == 0xbb
|
||||
&& (bytes[2] & 0xff) == 0xbf,
|
||||
"UTF-8 BOM must not be generated: " + file);
|
||||
String content = Files.readString(file, StandardCharsets.UTF_8);
|
||||
assertFalse(content.contains("<EFBFBD>"), "Invalid replacement character: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesV17ToolDefinitionTogetherWithToolSources() throws Exception {
|
||||
String moduleName = root.resolve("dap-was-v17-definition").toString();
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회",
|
||||
"사번으로 재직 중인 직원을 조회한다.", "smp", "HTTP", moduleName,
|
||||
"tester", "2026.08.12", false, null, null, null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", "09860000", true)),
|
||||
List.of(), "employee");
|
||||
|
||||
Path definition = root.resolve("dap-was-v17-definition/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||
String yaml = Files.readString(definition);
|
||||
|
||||
assertTrue(yaml.contains("name: smp_employee_search"), yaml);
|
||||
assertTrue(yaml.contains("when_to_use:"), yaml);
|
||||
assertTrue(yaml.contains("example_queries:"), yaml);
|
||||
assertTrue(yaml.contains("additionalProperties: false"), yaml);
|
||||
assertTrue(yaml.contains("owner_org: \"MCP_TOOL\""), yaml);
|
||||
ToolDefinition parsed = new ObjectMapper(new YAMLFactory()).readValue(yaml, ToolDefinition.class);
|
||||
ToolDefinitionValidator.validate(parsed, definition.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesV17MetadataEnteredByScaffoldUser() throws Exception {
|
||||
String moduleName = root.resolve("dap-was-v17-options").toString();
|
||||
ToolScaffolder.ToolDefinitionOptions options = new ToolScaffolder.ToolDefinitionOptions(
|
||||
"사번으로 직원을 조회한다.",
|
||||
"직원 정보 조회 요청에 사용한다.",
|
||||
"사번이 없으면 사용하지 않는다.",
|
||||
"최대 1건만 반환한다.",
|
||||
"직원 기본 정보 조회",
|
||||
List.of("사번 10001을 조회해줘", "직원 10001 소속을 알려줘", "10001 직원을 찾아줘"),
|
||||
List.of("employee", "search"), "HR_TEAM");
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회", "직원을 조회한다.",
|
||||
"smp", "HTTP", moduleName, "tester", "2026.08.12", false, null, null, null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", "10001", true)),
|
||||
List.of(), "employee", options);
|
||||
|
||||
Path definition = root.resolve("dap-was-v17-options/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||
ToolDefinition parsed = new ObjectMapper(new YAMLFactory()).readValue(Files.readString(definition), ToolDefinition.class);
|
||||
assertEquals("HR_TEAM", parsed.ownerOrg());
|
||||
assertEquals("10001 직원을 찾아줘", parsed.exampleQueries().get(2));
|
||||
assertEquals(2, parsed.tags().size());
|
||||
ToolDefinitionValidator.validate(parsed, definition.toString());
|
||||
}
|
||||
|
||||
@TempDir
|
||||
Path root;
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ class ToolManifestServiceTest {
|
||||
assertTrue(item.annotations().readOnlyHint());
|
||||
assertEquals("1.2.0", item.meta().version());
|
||||
assertEquals(3000, item.meta().timeoutMillis());
|
||||
assertEquals(List.of("계약 상태를 알려줘", "내 계약을 조회해줘", "계약번호로 찾아줘"),
|
||||
item.meta().exampleQueries());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -86,6 +88,9 @@ class ToolManifestServiceTest {
|
||||
.podUrl("http://tool-processing.ax-hub.svc.cluster.local:8080")
|
||||
.displayName("계약 조회")
|
||||
.description("계약번호로 계약 정보를 조회합니다.")
|
||||
.exampleQueries(List.of("계약 상태를 알려줘", "내 계약을 조회해줘", "계약번호로 찾아줘"))
|
||||
.tags(List.of("계약"))
|
||||
.ownerOrg("MCP_TOOL")
|
||||
.parametersSchema(Map.of("type", "object", "properties", Map.of("contractNo", Map.of("type", "string")),
|
||||
"required", List.of("contractNo"), "additionalProperties", false))
|
||||
.semver(version)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
name: cmm_claim_search
|
||||
display_name: 보험금 청구 상태 조회
|
||||
version: 1.0.0
|
||||
category_key: cmm
|
||||
description:
|
||||
function: 청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.
|
||||
when_to_use: 사용자가 보험금 청구 진행 상태나 심사 결과를 확인하려는 경우 사용한다.
|
||||
when_not_to_use: 보험금 청구를 새로 접수하거나 기존 청구를 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 청구번호 또는 계약번호 중 하나 이상이 필요하며 조회 결과만 반환한다.
|
||||
display_description: 보험금 청구 상태와 심사 결과를 조회합니다.
|
||||
example_queries:
|
||||
- 내 보험금 청구가 어디까지 진행됐는지 알려줘
|
||||
- 계약번호로 최근 청구 상태를 확인해줘
|
||||
- 청구 심사 결과가 나왔는지 조회해줘
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
claimNo:
|
||||
type: string
|
||||
description: 조회할 보험금 청구번호
|
||||
required:
|
||||
- claimNo
|
||||
additionalProperties: false
|
||||
tags:
|
||||
- 보험금
|
||||
- 청구조회
|
||||
owner_org: MCP_TOOL
|
||||
@@ -20,7 +20,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
|
||||
* </pre>
|
||||
*/
|
||||
public interface MetaCommonCodeUseCase {
|
||||
@McpTool(name = "cmm_commonCode_lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
|
||||
@McpTool(name = "cmm_comcode_lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
|
||||
@ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
|
||||
Object execute(MetaCommonCodeRequest req);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import io.shinhanlife.dap.mcc.biz.ins.dto.InsuranceClaimProcessorResponse;
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcc.biz.ins.usecase
|
||||
* @className InsuranceClaimProcessorUseCase
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author Admin
|
||||
* @create 2026.08.11
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.08.11 Admin 理쒖큹?앹꽦
|
||||
* 2026.08.11 Admin 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest;
|
||||
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse;
|
||||
|
||||
public interface ExchangeRateToolUseCase {
|
||||
@McpTool(name = "smp_exchangeRate_inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)")
|
||||
@McpTool(name = "smp_exchange_inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)")
|
||||
@ToolHint(register = false, categoryKey = "smp", mappingId = "EXCHANGE_001")
|
||||
ExchangeRateResponse execute(ExchangeRateRequest req);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# OCI ?대씪?곕뱶 ?섍꼍 ?꾩슜 ?ㅼ젙
|
||||
# OCI 클라우드 개발 환경 전용 설정
|
||||
server:
|
||||
port: ${PORT:8084}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: cmm_comcode_lookup
|
||||
display_name: 메타 공통코드 조회
|
||||
version: 1.0.0
|
||||
category_key: cmm
|
||||
description:
|
||||
function: 통합코드 그룹과 코드명 조건으로 메타 공통코드 목록을 조회한다.
|
||||
when_to_use: 업무 코드의 값과 표시명을 확인하거나 유효한 코드 목록이 필요한 경우 사용한다.
|
||||
when_not_to_use: 공통코드를 신규 등록하거나 변경 또는 삭제하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 검색 조건에 맞는 코드와 코드명만 반환하며 코드 데이터는 변경하지 않는다.
|
||||
display_description: 메타 시스템의 공통코드 목록을 조회합니다.
|
||||
example_queries: ["사용 상태 코드 목록을 알려줘", "고객 구분 공통코드를 찾아줘", "사용 중인 통합코드를 조회해줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
groupCode: {type: string, description: 조회할 통합코드 그룹 ID}
|
||||
codeName: {type: string, description: 코드명 검색 키워드}
|
||||
useYn: {type: string, description: 사용 여부 Y 또는 N, enum: [Y, N]}
|
||||
additionalProperties: false
|
||||
tags: [메타, 공통코드]
|
||||
legacy_interface_id: CLCNNB00001
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,24 @@
|
||||
name: cmm_meta_table
|
||||
display_name: 메타 테이블 조회
|
||||
version: 1.0.0
|
||||
category_key: cmm
|
||||
description:
|
||||
function: 물리명, 논리명 또는 소유자 조건으로 메타 테이블 정보를 조회한다.
|
||||
when_to_use: 사용자가 업무 데이터의 테이블명이나 소유 스키마를 확인하려는 경우 사용한다.
|
||||
when_not_to_use: 테이블을 생성하거나 구조를 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 메타에 등록된 테이블 설명 정보만 반환하며 실제 테이블 데이터는 조회하지 않는다.
|
||||
display_description: 메타 시스템에 등록된 테이블 정보를 조회합니다.
|
||||
example_queries: ["고객 기본 테이블을 찾아줘", "계약 관련 테이블 목록을 보여줘", "특정 스키마의 테이블을 조회해줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
tableName: {type: string, description: 테이블 물리명 검색어}
|
||||
tableLogicalName: {type: string, description: 테이블 논리명 검색어}
|
||||
owner: {type: string, description: 테이블 소유 스키마명}
|
||||
additionalProperties: false
|
||||
tags: [메타, 테이블]
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,23 @@
|
||||
name: cmm_template_url
|
||||
display_name: 업무 템플릿 다운로드 URL 조회
|
||||
version: 1.0.0
|
||||
category_key: cmm
|
||||
description:
|
||||
function: 요청한 업무 템플릿 파일을 내려받을 수 있는 URL을 반환한다.
|
||||
when_to_use: 사용자가 엑셀이나 워드 업무 양식의 다운로드 위치를 요청한 경우 사용한다.
|
||||
when_not_to_use: 템플릿 내용을 작성하거나 업로드 또는 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 등록된 템플릿 ID에 대한 다운로드 URL만 반환하며 파일 자체는 반환하지 않는다.
|
||||
display_description: 업무 템플릿을 다운로드할 수 있는 URL을 제공합니다.
|
||||
example_queries: ["청구 양식 다운로드 링크를 알려줘", "업무용 엑셀 템플릿을 받고 싶어", "등록된 문서 양식 위치를 찾아줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
templateId: {type: string, description: 다운로드할 템플릿 식별자}
|
||||
required: [templateId]
|
||||
additionalProperties: false
|
||||
tags: [템플릿, 다운로드]
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,26 @@
|
||||
name: ins_insurance_processor
|
||||
display_name: 보험금 청구 처리
|
||||
version: 1.0.0
|
||||
category_key: ins
|
||||
description:
|
||||
function: 보험금 청구번호, 청구금액과 청구일을 받아 청구 처리 요청을 수행한다.
|
||||
when_to_use: 사용자가 확인된 보험금 청구 정보를 실제 처리계에 접수하려는 경우 사용한다.
|
||||
when_not_to_use: 청구 상태만 조회하거나 필수 정보가 확인되지 않은 경우에는 사용하지 않는다.
|
||||
io_limits: 실행 시 업무 상태가 변경될 수 있으므로 호출 전에 입력값과 사용자 의사를 확인해야 한다.
|
||||
display_description: 확인된 보험금 청구 요청을 처리계에 전달합니다.
|
||||
example_queries: ["확인한 내용으로 보험금 청구를 접수해줘", "이 청구번호의 보험금 처리를 진행해줘", "오늘 날짜로 보험금 청구 요청을 보내줘"]
|
||||
read_only: false
|
||||
destructive: true
|
||||
idempotent: false
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
claimNumber: {type: string, description: 처리할 보험금 청구번호}
|
||||
claimAmount: {type: number, description: 처리할 보험금 청구금액}
|
||||
claimDate: {type: string, description: 청구일자 YYYYMMDD, pattern: "^[0-9]{8}$"}
|
||||
required: [claimNumber, claimAmount, claimDate]
|
||||
additionalProperties: false
|
||||
tags: [보험금, 청구처리]
|
||||
legacy_interface_id: CLAIM0000001
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,35 @@
|
||||
name: oth_onnba3011_call
|
||||
display_name: ONNBA3011 보험 업무 조회
|
||||
version: 1.0.0
|
||||
category_key: oth
|
||||
description:
|
||||
function: ONNBA3011 입력정보를 MCI 전문으로 변환해 보험 업무 결과를 조회한다.
|
||||
when_to_use: 사용자가 ONNBA3011 인터페이스 기준의 보험 계약 또는 지급 정보를 조회하려는 경우 사용한다.
|
||||
when_not_to_use: 인터페이스 입력값을 확인할 수 없거나 보험 정보를 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: CLCNNB00001 인터페이스 규격에 맞는 값만 전달하며 조회 결과를 업무 응답으로 변환한다.
|
||||
display_description: ONNBA3011 업무 정보를 MCI로 조회합니다.
|
||||
example_queries: ["고객의 보험 업무 정보를 조회해줘", "ONNBA3011 기준으로 계약 정보를 확인해줘", "입력한 고객번호의 보험 결과를 알려줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
dalScCd: {type: string, description: 거래 구분 코드}
|
||||
cstSucoRltyCd: {type: string, description: 고객 성공 관계 코드}
|
||||
csNo: {type: string, description: 고객 번호}
|
||||
rdreNo: {type: string, description: 설계사 번호}
|
||||
unfcPvsCalReqYn: {type: string, description: 미확정 지급 계산 요청 여부}
|
||||
kcisPymmTnnrRequest: {type: string, description: KCIS 납입 기간 요청값}
|
||||
lmovYn: {type: string, description: 계약 이동 여부}
|
||||
genPsthApvTrgtYn: {type: string, description: 일반 사후 승인 대상 여부}
|
||||
ircoLmovEcpbTrgtYn: {type: string, description: 계약 이동 예외 대상 여부}
|
||||
digCalYn: {type: string, description: 디지털 계산 여부}
|
||||
prbuIciDigCalYn: {type: string, description: 상품별 디지털 계산 여부}
|
||||
unfcPrbuIrcoAddu: {type: object, description: 미확정 상품 추가 정보, additionalProperties: true}
|
||||
sucoIspaBasDto: {type: object, description: 성공 심사 기본 정보, additionalProperties: true}
|
||||
additionalProperties: false
|
||||
tags: [보험, MCI]
|
||||
legacy_interface_id: CLCNNB00001
|
||||
required_env_keys: [GLOW_COMMUNICATION_MCI_HOST, GLOW_COMMUNICATION_MCI_PORT]
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,22 @@
|
||||
name: smp_exchange_inquiry
|
||||
display_name: 실시간 환율 조회
|
||||
version: 1.0.0
|
||||
category_key: smp
|
||||
description:
|
||||
function: 통화코드를 기준으로 현재 환율 정보를 조회한다.
|
||||
when_to_use: 사용자가 USD, EUR, JPY 등 특정 통화의 환율을 확인하려는 경우 사용한다.
|
||||
when_not_to_use: 환전 거래를 실행하거나 과거 환율 통계를 분석하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 지원되는 통화코드의 조회 시점 환율만 반환하며 실제 환전 기능은 제공하지 않는다.
|
||||
display_description: 지정한 통화의 현재 환율을 조회합니다.
|
||||
example_queries: ["오늘 달러 환율을 알려줘", "엔화 환율이 얼마인지 조회해줘", "유로 환율을 확인해줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
currencyCode: {type: string, description: 조회할 ISO 통화코드, pattern: "^[A-Z]{3}$"}
|
||||
additionalProperties: false
|
||||
tags: [환율, 금융]
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,22 @@
|
||||
name: smp_quote_daily
|
||||
display_name: 오늘의 명언 조회
|
||||
version: 1.0.0
|
||||
category_key: smp
|
||||
description:
|
||||
function: 선택한 카테고리에 맞는 오늘의 명언 한 건을 조회한다.
|
||||
when_to_use: 사용자가 명언이나 짧은 동기부여 문구를 요청한 경우 사용한다.
|
||||
when_not_to_use: 업무 데이터 조회나 사실 검증이 필요한 질문에는 사용하지 않는다.
|
||||
io_limits: 등록된 명언 데이터 중 한 건만 반환하며 출처 정보가 없을 수 있다.
|
||||
display_description: 카테고리에 맞는 오늘의 명언을 제공합니다.
|
||||
example_queries: ["오늘 힘이 되는 말을 알려줘", "업무 시작 전에 명언 하나 보여줘", "성공에 관한 짧은 문구를 추천해줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: false
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
category: {type: string, description: 조회할 명언 카테고리}
|
||||
additionalProperties: false
|
||||
tags: [명언, 콘텐츠]
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,22 @@
|
||||
name: smp_team_list
|
||||
display_name: MCP·TOOL 파트 구성원 조회
|
||||
version: 1.0.0
|
||||
category_key: smp
|
||||
description:
|
||||
function: 신한라이프 AX 추진팀과 MCP·TOOL 파트 구성원 및 직급을 조회한다.
|
||||
when_to_use: 사용자가 프로젝트 담당자나 팀별 구성원 정보를 묻는 경우 사용한다.
|
||||
when_not_to_use: 인사 개인정보나 연락처 또는 조직 변경 이력을 요청하는 경우에는 사용하지 않는다.
|
||||
io_limits: 사전에 등록된 구성원의 이름과 직급만 반환하며 민감한 인사정보는 포함하지 않는다.
|
||||
display_description: 신한라이프 MCP·TOOL 파트 담당자와 구성원을 조회합니다.
|
||||
example_queries: ["MCP 팀 담당자를 알려줘", "TOOL 파트 구성원이 누구인지 보여줘", "AX 추진팀 담당자를 찾아줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
teamName: {type: string, description: 조회할 팀 이름 또는 전체}
|
||||
additionalProperties: false
|
||||
tags: [조직, 담당자]
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,23 @@
|
||||
name: smp_weather_inquiry
|
||||
display_name: 도시 날씨 조회
|
||||
version: 1.0.0
|
||||
category_key: smp
|
||||
description:
|
||||
function: 도시명을 기준으로 현재 날씨, 온도와 풍속을 조회한다.
|
||||
when_to_use: 사용자가 특정 도시의 현재 기상 정보를 요청한 경우 사용한다.
|
||||
when_not_to_use: 장기 예보나 기상 특보 또는 공식 재난정보가 필요한 경우에는 사용하지 않는다.
|
||||
io_limits: 입력한 도시의 현재 관측 기반 샘플 정보만 반환한다.
|
||||
display_description: 지정한 도시의 현재 날씨 정보를 조회합니다.
|
||||
example_queries: ["서울 날씨를 알려줘", "부산의 현재 온도를 조회해줘", "제주도 바람이 얼마나 부는지 알려줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
city: {type: string, description: 날씨를 조회할 도시명}
|
||||
required: [city]
|
||||
additionalProperties: false
|
||||
tags: [날씨, 조회]
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,24 @@
|
||||
name: sol_request_detail
|
||||
display_name: SOL 의뢰서 상세 조회
|
||||
version: 1.0.0
|
||||
category_key: sol
|
||||
description:
|
||||
function: SOL 의뢰서 식별자를 기준으로 의뢰서 상세 내용을 조회한다.
|
||||
when_to_use: 사용자가 특정 SOL 의뢰서의 상세 내용과 진행 정보를 확인하려는 경우 사용한다.
|
||||
when_not_to_use: 의뢰서 목록만 찾거나 의뢰서를 변경 또는 처리하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 정확한 의뢰서 ID가 필요하며 등록된 상세 정보만 반환한다.
|
||||
display_description: SOL 의뢰서 한 건의 상세 정보를 조회합니다.
|
||||
example_queries: ["이 SOL 의뢰서 상세를 보여줘", "의뢰서 ID로 처리 내용을 확인해줘", "선택한 의뢰서의 상세 정보를 알려줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
srId: {type: string, description: 상세 조회할 SOL 의뢰서 ID}
|
||||
required: [srId]
|
||||
additionalProperties: false
|
||||
tags: [SOL, 의뢰서]
|
||||
legacy_interface_id: SOLG00000002
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,25 @@
|
||||
name: sol_request_list
|
||||
display_name: SOL 의뢰서 목록 조회
|
||||
version: 1.0.0
|
||||
category_key: sol
|
||||
description:
|
||||
function: 진행상태, 조회기간과 조회대상 조건으로 SOL 의뢰서 목록을 조회한다.
|
||||
when_to_use: 사용자가 자신의 SOL 의뢰서나 상태별 의뢰서 목록을 확인하려는 경우 사용한다.
|
||||
when_not_to_use: 특정 의뢰서의 상세 내용만 보거나 의뢰서를 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 입력 조건에 해당하는 의뢰서 요약 목록만 반환한다.
|
||||
display_description: 조건에 맞는 SOL 의뢰서 목록을 조회합니다.
|
||||
example_queries: ["진행 중인 SOL 의뢰서를 보여줘", "최근 한 달간 내 의뢰서를 조회해줘", "완료된 의뢰서 목록을 알려줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
status: {type: string, description: 조회할 의뢰서 진행상태}
|
||||
period: {type: string, description: 조회할 기간 조건}
|
||||
target: {type: string, description: 나의 업무 또는 전체 조회대상}
|
||||
additionalProperties: false
|
||||
tags: [SOL, 의뢰서]
|
||||
legacy_interface_id: SOLG00000001
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -8,14 +8,14 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MemoListRetrieverResponse;
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
||||
* @className MemoListRetrieverUseCase
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author Admin
|
||||
* @create 2026.08.11
|
||||
* <pre>
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.08.11 Admin 理쒖큹?앹꽦
|
||||
* 2026.08.11 Admin 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# OCI ?대씪?곕뱶 ?섍꼍 ?꾩슜 ?ㅼ젙
|
||||
# OCI 클라우드 개발 환경 전용 설정
|
||||
server:
|
||||
port: ${PORT:8082}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: cmm_claim_search
|
||||
display_name: 보험금 청구 상태 조회
|
||||
version: 1.0.0
|
||||
category_key: cmm
|
||||
description:
|
||||
function: 청구번호 또는 계약번호로 보험금 청구 상태와 심사 결과를 조회한다.
|
||||
when_to_use: 사용자가 기존 보험금 청구의 진행 상태나 지급 결과를 확인하려는 경우 사용한다.
|
||||
when_not_to_use: 보험금 청구를 새로 접수하거나 기존 청구 내용을 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 청구번호 또는 계약번호 중 하나 이상이 필요하며 조회 결과만 반환한다.
|
||||
display_description: 보험금 청구 상태와 심사 결과를 조회합니다.
|
||||
example_queries: ["내 보험금 청구가 어디까지 진행됐는지 알려줘", "계약번호로 최근 청구 상태를 확인해줘", "청구 심사 결과가 나왔는지 조회해줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
claimNo: {type: string, description: 조회할 보험금 청구번호}
|
||||
contractNo: {type: string, description: 조회할 보험계약 번호}
|
||||
additionalProperties: false
|
||||
tags: [보험금, 청구조회]
|
||||
legacy_interface_id: CLCNNB00001
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,24 @@
|
||||
name: cmm_memo_retriever
|
||||
display_name: 의뢰서 목록 조회
|
||||
version: 1.0.0
|
||||
category_key: cmm
|
||||
description:
|
||||
function: 상태와 검색어 조건으로 의뢰서 목록을 조회한다.
|
||||
when_to_use: 사용자가 등록된 의뢰서의 목록이나 처리 현황을 찾으려는 경우 사용한다.
|
||||
when_not_to_use: 의뢰서를 신규 등록하거나 내용을 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 선택 조건에 해당하는 의뢰서 요약 목록만 반환한다.
|
||||
display_description: 조건에 맞는 의뢰서 목록을 조회합니다.
|
||||
example_queries: ["처리 중인 의뢰서를 보여줘", "지난번 계약 관련 의뢰서를 찾아줘", "완료된 의뢰서 목록을 조회해줘"]
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
memoStatus: {type: string, description: 조회할 의뢰서 처리 상태}
|
||||
searchKeyword: {type: string, description: 의뢰서 제목 또는 내용 검색어}
|
||||
additionalProperties: false
|
||||
tags: [의뢰서, 목록조회]
|
||||
legacy_interface_id: MEMO0000001
|
||||
required_env_keys: []
|
||||
owner_org: MCP_TOOL
|
||||
@@ -0,0 +1,58 @@
|
||||
# Scaffold AI V17 Metadata 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:** AI Tool 초안 생성 시 Tool Schema V17 Metadata 전체를 생성하고 Scaffold 폼에 자동 입력한다.
|
||||
|
||||
**Architecture:** 기존 `/tool-draft` 응답 DTO를 확장하고 서버에서 값을 정규화한다. 화면은 서버 응답의 필드를 기존 V17 폼 요소에 직접 매핑하며, 실제 Scaffold 생성은 기존 `ToolDefinitionOptions` 경로를 그대로 사용한다.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot MVC, Jackson, JUnit 5, MockMvc, HTML/JavaScript
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 기존 Tool 생성 경로와 Legacy 연동 정보 입력 정책을 유지한다.
|
||||
- Tool Schema V17 필드명은 기존 `ToolDefinitionOptions`와 동일하게 유지한다.
|
||||
- 기존 작업 트리 변경사항을 되돌리거나 포함 범위 밖에서 수정하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: AI Tool Draft 서버 계약 확장
|
||||
|
||||
**Files:**
|
||||
- Modify: `dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingControllerToolDraftTest.java`
|
||||
- Modify: `dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: AI가 반환한 JSON Tool 초안
|
||||
- Produces: V17 Metadata가 포함된 `/api/v1/scaffold/tool-draft` JSON 응답
|
||||
|
||||
- [ ] Mock AI 응답과 API assertion에 V17 필드를 추가한다.
|
||||
- [ ] 테스트를 실행해 현재 응답에서 V17 필드가 누락되어 실패하는지 확인한다.
|
||||
- [ ] `ToolDraft`와 프롬프트 및 정규화 로직을 확장한다.
|
||||
- [ ] 테스트가 통과하는지 확인한다.
|
||||
|
||||
### Task 2: Scaffold 화면 자동 매핑
|
||||
|
||||
**Files:**
|
||||
- Modify: `dap-gateway/src/main/resources/static/admin/scaffold.html`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1의 V17 Metadata JSON 필드
|
||||
- Produces: 같은 이름을 가진 V17 폼 입력값
|
||||
|
||||
- [ ] `createAiToolDraft()`에 문자열 필드 매핑을 추가한다.
|
||||
- [ ] `exampleQueries`는 줄바꿈, `tags`는 쉼표 구분 문자열로 변환한다.
|
||||
- [ ] 누락된 값은 빈 값 또는 `MCP_TOOL` 기본값으로 처리한다.
|
||||
|
||||
### Task 3: 통합 검증
|
||||
|
||||
**Files:**
|
||||
- Verify: Gateway와 V17 관련 전체 변경
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1~2 결과
|
||||
- Produces: 컴파일 및 표준 검증 증거
|
||||
|
||||
- [ ] Gateway 대상 테스트를 실행한다.
|
||||
- [ ] `validateToolSchemaV17`을 실행한다.
|
||||
- [ ] `git diff --check`로 문법적 공백 오류를 확인한다.
|
||||
162
docs/superpowers/plans/2026-08-12-tool-schema-v17-migration.md
Normal file
162
docs/superpowers/plans/2026-08-12-tool-schema-v17-migration.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Tool Schema V17 Migration 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:** BC-DAB-STD-003 V17의 Tool 스키마 필수 항목을 Tool Pod의 등록, Manifest, MCP 노출, Scaffold 및 빌드 검증 전 구간에 일관되게 적용한다.
|
||||
|
||||
**Architecture:** 각 Tool의 업무 명세는 `tool-definitions/{category}/{tool-name}.yml`에서 관리하고, 기동 시 공통 로더가 이를 읽어 기존 어노테이션 정보와 결합한다. 결합된 `ToolMetadata`를 단일 원천으로 Manifest와 MCP Tool을 생성해 Portal/Gateway/직접 MCP 연결 간 메타데이터 차이를 제거한다.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 3.5.11, Spring AI MCP 1.1.8, Jackson YAML/JSON, Gradle, JUnit 5, AssertJ
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Tool `name`은 `^[a-z][a-z0-9_]{2,63}$`를 만족한다.
|
||||
- V17 필수 14개 항목을 누락 없이 제공한다.
|
||||
- `example_queries`는 3~10개이며 Tool 이름을 직접 포함하지 않는다.
|
||||
- `parameters_schema`는 루트 `type: object`, `additionalProperties: false`이고 모든 property에 description을 둔다.
|
||||
- `outputSchema`를 명시한 Tool만 출력 검증하며 기존 우선순위를 유지한다.
|
||||
- 기존 SSE/Streamable HTTP 전송, trace-id/request-id/employee-id, MCI/HTTP 호출 흐름은 변경하지 않는다.
|
||||
- 기존 미추적 WCM/HMCI 파일을 삭제하거나 덮어쓰지 않는다.
|
||||
- `register=false`인 Tool을 임의로 운영 등록 상태로 변경하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: V17 Tool Definition 모델과 로더
|
||||
|
||||
**Files:**
|
||||
- Create: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/metadata/ToolDescription.java`
|
||||
- Create: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/metadata/ToolDefinition.java`
|
||||
- Create: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/metadata/ToolDefinitionRepository.java`
|
||||
- Create: `dap-was-lib/src/test/java/io/shinhanlife/dap/lib/metadata/ToolDefinitionRepositoryTest.java`
|
||||
- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `Optional<ToolDefinition> findByName(String name)` 및 `ToolMetadata`의 V17 필드 접근자.
|
||||
- Consumes: classpath `tool-definitions/**/*.yml`과 Jackson YAML.
|
||||
|
||||
- [ ] **Step 1: Write failing loader and validation tests**
|
||||
|
||||
빈 필수 설명, 2개 이하 예시 질의, 잘못된 name, object가 아닌 schema를 거부하고 정상 YAML을 로드하는 테스트를 작성한다.
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `./gradlew.bat :dap-was-lib:test --tests "*ToolDefinitionRepositoryTest"`
|
||||
Expected: FAIL because V17 model/repository does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement minimal immutable models and classpath loader**
|
||||
|
||||
`ToolDescription(function, whenToUse, whenNotToUse, ioLimits)`와 V17 전체 필드를 가진 `ToolDefinition`을 만들고 기동 시 한 번 로드·검증해 name 기준 불변 Map으로 캐시한다.
|
||||
|
||||
- [ ] **Step 4: Run tests and verify GREEN**
|
||||
|
||||
Run: `./gradlew.bat :dap-was-lib:test --tests "*ToolDefinitionRepositoryTest"`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 2: Registry 수집과 Manifest 표준 매핑
|
||||
|
||||
**Files:**
|
||||
- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java`
|
||||
- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java`
|
||||
- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java`
|
||||
- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java`
|
||||
- Modify: `dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSenderTest.java`
|
||||
- Modify: `dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ToolDefinitionRepository.findByName`.
|
||||
- Produces: V17 description, display description, examples, owner/version/interface/environment metadata가 포함된 `ToolMetadata`와 `/manifest` 응답.
|
||||
|
||||
- [ ] **Step 1: Add failing enrichment and manifest mapping tests**
|
||||
- [ ] **Step 2: Run targeted tests and verify RED**
|
||||
|
||||
Run: `./gradlew.bat :dap-was-lib:test --tests "*ToolRegistryHeartbeatSenderTest" --tests "*ToolManifestServiceTest"`
|
||||
|
||||
- [ ] **Step 3: Merge annotation runtime data with definition YAML**
|
||||
|
||||
호출 주소·상태는 런타임 값, 업무 설명·예시·소유조직은 YAML 값을 사용하며 필수 정의가 없는 등록 대상 Tool은 기동 검증에서 실패시킨다.
|
||||
|
||||
- [ ] **Step 4: Map Manifest title/description/inputSchema/outputSchema/annotations/_meta**
|
||||
- [ ] **Step 5: Run targeted tests and verify GREEN**
|
||||
|
||||
### Task 3: MCP Tool 직접 노출과 Gateway 동기화
|
||||
|
||||
**Files:**
|
||||
- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java`
|
||||
- Create or Modify: `dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizerTest.java`
|
||||
- Modify: `dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/RegistryMcpToolSpecificationFactory.java`
|
||||
- Modify: `dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactoryTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: enriched `ToolMetadata`.
|
||||
- Produces: 동일한 MCP `name`, `title`, 합성 description, input/output schema, readOnly/destructive/idempotent annotations, `_meta`.
|
||||
|
||||
- [ ] **Step 1: Add failing parity tests for Pod and Gateway MCP specs**
|
||||
- [ ] **Step 2: Run both module tests and verify RED**
|
||||
- [ ] **Step 3: Implement shared metadata-to-MCP mapping without changing transport**
|
||||
- [ ] **Step 4: Run both module tests and verify GREEN**
|
||||
|
||||
### Task 4: 기존 Tool 전체 정의와 이름 정규화
|
||||
|
||||
**Files:**
|
||||
- Create: `dap-was-sms/src/main/resources/tool-definitions/**/*.yml`
|
||||
- Create: `dap-was-oth/src/main/resources/tool-definitions/**/*.yml`
|
||||
- Modify: all `dap-was-sms/src/main/java/**/*UseCase*.java` containing `@McpTool`
|
||||
- Modify: all `dap-was-oth/src/main/java/**/*UseCase*.java` containing `@McpTool`
|
||||
- Modify: matching tests and mock fixture keys
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 YAML format.
|
||||
- Produces: every discovered Tool has one unique V17 definition.
|
||||
|
||||
- [ ] **Step 1: Add failing repository-wide compliance test**
|
||||
|
||||
모든 `@McpTool` name에 정확히 하나의 정의가 있고 필수 필드/예시 수/name 규칙을 만족하는지 검사한다.
|
||||
|
||||
- [ ] **Step 2: Run compliance test and verify RED**
|
||||
- [ ] **Step 3: Rename invalid names and add complete definitions**
|
||||
|
||||
`smp_exchangeRate_inquiry`는 `smp_exchange_inquiry`, `cmm_commonCode_lookup`는 `cmm_comcode_lookup`로 바꾸고 나머지 호출명은 호왘성을 위해 유지한다.
|
||||
|
||||
- [ ] **Step 4: Update tests/fixtures and verify GREEN**
|
||||
|
||||
### Task 5: Scaffold V17 생성 지원
|
||||
|
||||
**Files:**
|
||||
- Modify: `dap-gateway/src/main/resources/static/admin/scaffold.html`
|
||||
- Modify: Scaffold request DTO/controller files discovered under `dap-gateway/src/main/java`
|
||||
- Modify: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java`
|
||||
- Modify: `dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 입력한 function/when-to-use/when-not-to-use/io-limits/display-description/examples/owner/tags/hints.
|
||||
- Produces: Java 소스, schemas, V17 tool-definition YAML.
|
||||
|
||||
- [ ] **Step 1: Add failing scaffold generation tests**
|
||||
- [ ] **Step 2: Verify RED**
|
||||
- [ ] **Step 3: Add form fields, validation, preview and YAML generation**
|
||||
- [ ] **Step 4: Verify GREEN including duplicate DTO-field regression tests**
|
||||
|
||||
### Task 6: Gradle 품질 게이트와 문서
|
||||
|
||||
**Files:**
|
||||
- Create: `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolSchemaV17ValidationRunner.java`
|
||||
- Create: `dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/ToolSchemaV17ValidationRunnerTest.java`
|
||||
- Modify: `build.gradle`
|
||||
- Modify: `README.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `validateToolSchemaV17` Gradle task; `bootJar` before validation; file/Tool/field가 표시되는 실패 메시지.
|
||||
|
||||
- [ ] **Step 1: Add failing validation runner tests**
|
||||
- [ ] **Step 2: Verify RED**
|
||||
- [ ] **Step 3: Implement runner and wire `bootJar.dependsOn(validateToolSchemaV17)`**
|
||||
- [ ] **Step 4: Document V17 fields, examples, Scaffold, compatibility and commands**
|
||||
- [ ] **Step 5: Run targeted and full verification**
|
||||
|
||||
Run:
|
||||
- `./gradlew.bat :dap-was-lib:test`
|
||||
- `./gradlew.bat :dap-was-sms:test :dap-was-oth:test :dap-gateway:test`
|
||||
- `./gradlew.bat validateMcpToolNames validateToolSchemaV17`
|
||||
- `./gradlew.bat clean build`
|
||||
|
||||
Expected: all tasks succeed with zero test failures.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Scaffold AI V17 Metadata Design
|
||||
|
||||
## Goal
|
||||
|
||||
`AI로 Tool 채우기` 실행 시 Tool 기본 정보와 입출력 필드뿐 아니라 Tool Schema V17 Metadata도 함께 생성하고 화면에 반영한다.
|
||||
|
||||
## Design
|
||||
|
||||
- `/api/v1/scaffold/tool-draft`의 AI JSON 계약에 `functionDescription`, `displayDescription`, `whenToUse`, `whenNotToUse`, `ioLimits`, `exampleQueries`, `tags`, `ownerOrg`를 추가한다.
|
||||
- 서버는 문자열을 trim하고 목록 필드는 비어 있는 항목을 제거하여 반환한다.
|
||||
- `scaffold.html`은 응답받은 V17 값을 같은 이름의 폼 필드에 채운다. 목록은 화면의 기존 입력 규칙에 맞게 줄바꿈 또는 쉼표 문자열로 변환한다.
|
||||
- Legacy Interface ID와 Client System Code는 기존 방침대로 AI가 생성하지 않는다.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- V17 필드가 누락된 과거 형식의 AI 응답도 역직렬화할 수 있게 하되, 서버에서 업무 설명을 기반으로 안전한 기본값을 생성한다.
|
||||
- AI가 빈 배열 또는 공백 항목을 반환하면 기본 예시 질의, 카테고리 태그, 기본 담당 조직을 적용한다.
|
||||
|
||||
## Verification
|
||||
|
||||
- Mock AI 응답을 사용하는 MVC 테스트로 모든 V17 필드가 API 응답에 포함되는지 검증한다.
|
||||
- Gateway 테스트와 Tool Schema V17 검증을 실행한다.
|
||||
@@ -0,0 +1,170 @@
|
||||
# Tool Schema V17 일괄 전환 설계
|
||||
|
||||
## 1. 목표
|
||||
|
||||
BC-DAB-STD-003 V17의 도구 스키마 요구사항을 현재 Java Tool Pod 구조에 일괄 적용한다. 표준 메타데이터를 단일 원본으로 관리하고, 동일 정보가 MCP Tool 정의, Tool Manifest, Gateway Registry, Portal 검색, Scaffold 및 빌드 검증에 일관되게 사용되도록 한다.
|
||||
|
||||
기존 Tool 호출명보다 V17 표준 준수를 우선한다. 이름이 변경되는 Tool은 Agent와 Portal 등록도 함께 갱신해야 한다.
|
||||
|
||||
## 2. 표준 원본
|
||||
|
||||
각 Tool은 모듈의 `src/main/resources/tool-definitions/{category}/{tool-name}.yml`에 표준 정의 파일을 가진다.
|
||||
|
||||
필수 항목은 다음과 같다.
|
||||
|
||||
- `name`: `^[a-z][a-z0-9_]{2,63}$`
|
||||
- `display_name`
|
||||
- `version`
|
||||
- `category_key`
|
||||
- `description.function`
|
||||
- `description.when_to_use`
|
||||
- `description.when_not_to_use`
|
||||
- `description.io_limits`
|
||||
- `display_description`
|
||||
- `example_queries`: 실제 사용자 발화 3~10건
|
||||
- `read_only`
|
||||
- `destructive`
|
||||
- `idempotent`
|
||||
- `parameters_schema`
|
||||
|
||||
권장·조건부 항목은 `tags`, `legacy_interface_id`, `required_env_keys`, `owner_org`, `output_schema`로 한다.
|
||||
|
||||
Java `@McpTool`은 실행 메서드를 식별하는 용도로 유지한다. 등록 시 표준 정의 파일을 우선 적용하고, 정의 파일이 없는 Tool은 빌드 품질 게이트에서 실패시킨다.
|
||||
|
||||
## 3. 런타임 데이터 흐름
|
||||
|
||||
1. Tool Pod 기동 시 `@McpTool` 메서드를 스캔한다.
|
||||
2. Tool 이름으로 표준 정의 파일을 읽는다.
|
||||
3. annotation의 이름과 표준 정의의 이름이 다르면 기동 실패한다.
|
||||
4. 입력 Schema는 기존 우선순위를 유지한다.
|
||||
- `ToolHint.inputSchemaResource`
|
||||
- 표준 정의의 `parameters_schema`
|
||||
- DTO 자동 생성
|
||||
5. 출력 Schema는 다음 우선순위를 사용한다.
|
||||
- `ToolHint.outputSchemaResource`
|
||||
- 표준 정의의 `output_schema`
|
||||
- `@McpOutputSchema` DTO 자동 생성
|
||||
6. Tool Metadata와 Manifest를 생성한다.
|
||||
7. Tool Pod MCP 서버와 Gateway MCP 서버에 동일한 title, description, inputSchema, outputSchema, annotations, `_meta`를 등록한다.
|
||||
|
||||
모델에 전달되는 `description`은 설명 4요소를 순서대로 결합한다. `example_queries`, 운영 조직, 기간계 ID와 버전은 `_meta`에 두어 모델 프롬프트 토큰을 늘리지 않는다.
|
||||
|
||||
## 4. 메타데이터 및 Manifest 확장
|
||||
|
||||
`ToolMetadata`에 다음 값을 추가한다.
|
||||
|
||||
- `displayDescription`
|
||||
- `descriptionFunction`
|
||||
- `whenToUse`
|
||||
- `whenNotToUse`
|
||||
- `ioLimits`
|
||||
- `exampleQueries`
|
||||
- `tags`
|
||||
- `legacyInterfaceId`
|
||||
- `requiredEnvKeys`
|
||||
- `ownerOrg`
|
||||
- `outputSchema`
|
||||
|
||||
Manifest Tool 항목에는 `outputSchema`를 추가한다. `_meta`에는 `version`, `categoryKey`, `exampleQueries`, `tags`, `legacyInterfaceId`, `requiredEnvKeys`, `ownerOrg`, `timeoutMillis`, `enabled`를 제공한다.
|
||||
|
||||
기존 `mciServiceId`는 호환을 위해 유지하되, 표준 Manifest에서는 `legacyInterfaceId`로 노출한다.
|
||||
|
||||
## 5. MCP 매핑
|
||||
|
||||
- `name` -> `Tool.name`
|
||||
- `display_name` -> `Tool.title` 및 `annotations.title`
|
||||
- 설명 4요소 -> `Tool.description`
|
||||
- `parameters_schema` -> `Tool.inputSchema`
|
||||
- `output_schema` -> `Tool.outputSchema`
|
||||
- `read_only` -> `readOnlyHint`
|
||||
- `destructive` -> `destructiveHint`
|
||||
- `idempotent` -> `idempotentHint`
|
||||
- 폐쇄망 Tool의 `openWorldHint` -> `false`
|
||||
- 예시 질의와 운영 메타 -> `Tool._meta` 및 Manifest `_meta`
|
||||
|
||||
행위 힌트는 인가 수단으로 사용하지 않는다. 기존 Gateway/Tool 권한 검증이 실제 접근을 차단한다.
|
||||
|
||||
## 6. Tool 이름 전환
|
||||
|
||||
모든 이름을 영문 소문자 snake_case 3~64자로 통일한다. 현재 확인된 변경 대상은 다음과 같다.
|
||||
|
||||
- `smp_exchangeRate_inquiry` -> `smp_exchange_inquiry`
|
||||
- `cmm_commonCode_lookup` -> `cmm_comcode_lookup`
|
||||
|
||||
나머지 Tool도 같은 정규식과 `category_service_action` 의미 구조로 검증한다. MCI/EIMS 인터페이스 ID는 Tool명으로 사용하지 않고 `legacy_interface_id`에 저장한다.
|
||||
|
||||
## 7. 설명과 유사 Tool 경계
|
||||
|
||||
현재 모든 Tool에 설명 4요소를 작성한다. `when_not_to_use`는 유사 Tool이 있으면 실제 Tool명을 양방향으로 기재한다. 유사 Tool이 없으면 `없음`을 명시한다.
|
||||
|
||||
자기 홍보 문구와 강제 선택 문구는 금지한다. 예시 질의에는 Tool 이름을 포함하지 않는다.
|
||||
|
||||
## 8. 품질 게이트
|
||||
|
||||
Gradle의 기존 Tool 이름 검증을 V17 검증으로 확장한다. `check`와 `bootJar`가 V17 검증에 의존하도록 한다.
|
||||
|
||||
다음을 위반하면 파일과 항목을 표시하고 빌드를 실패시킨다.
|
||||
|
||||
- 필수 14개 항목 누락
|
||||
- 이름 정규식 위반 또는 중복
|
||||
- annotation 이름과 정의 파일 이름 불일치
|
||||
- 설명 4요소 누락
|
||||
- 예시 질의 3~10건 위반 또는 Tool명 포함
|
||||
- 자기 홍보 문구 포함
|
||||
- 입력 필드 description 누락
|
||||
- `additionalProperties: false` 미설정
|
||||
- `read_only=true`와 `destructive=true` 동시 설정
|
||||
- 등록 Tool의 정의 파일 누락 또는 사용되지 않는 정의 파일 존재
|
||||
- outputSchema가 선언된 경우 유효하지 않은 구조
|
||||
|
||||
플랫폼이 주입하는 trace ID, request ID, employee ID 및 인증 값은 Tool 입력 Schema에 포함하지 않는다.
|
||||
|
||||
## 9. Scaffold
|
||||
|
||||
Scaffold 화면과 생성기는 다음 값을 받는다.
|
||||
|
||||
- Tool명 또는 Base Name
|
||||
- 표시명과 화면 설명
|
||||
- 설명 4요소
|
||||
- 예시 질의 3~10건
|
||||
- category, tags, 담당 조직, 기간계 ID
|
||||
- 읽기 전용·파괴적·멱등 힌트
|
||||
- 입력·출력 필드와 Schema 제약
|
||||
|
||||
생성 결과에 Java UseCase/DTO/Converter/Client, Tool 정의 YAML, 필요 시 JSON Schema Resource, 테스트 및 Mock 응답을 포함한다. 생성 직후 V17 검증을 실행하고 결과를 화면에 표시한다.
|
||||
|
||||
## 10. 기존 Tool 일괄 마이그레이션
|
||||
|
||||
SMS와 OTH 모듈의 모든 `@McpTool`을 대상으로 정의 파일을 생성한다. 기존 title, description, DTO, 연동 코드와 테스트를 참고하여 필드를 작성한다. 정보가 소스에서 확정되지 않는 경우 다음 보수적 기본값을 사용한다.
|
||||
|
||||
- `owner_org`: `MCP_TOOL`
|
||||
- 유사 Tool 없음: `when_not_to_use: 없음`
|
||||
- 내부 연동: `openWorldHint: false`
|
||||
- 조회성 이름·구현: 읽기 전용 true, 파괴적 false, 멱등 true
|
||||
- 등록·발송·처리성 구현: 읽기 전용 false, 파괴적 true, 멱등 false
|
||||
|
||||
`register=false`는 초안/로컬 Tool 상태로 유지한다. 중앙 Registry에 올릴 Tool만 승인 후 `register=true`로 전환하며, 이번 변경에서 임의로 운영 등록을 활성화하지 않는다.
|
||||
|
||||
사용자가 별도로 작업 중인 미추적 WCM/HMCI 파일은 덮어쓰지 않는다. 스캔된 WCM Tool용 정의 파일과 검증 지원만 추가한다.
|
||||
|
||||
## 11. 호환성과 영향
|
||||
|
||||
이름이 바뀐 Tool은 기존 호출명으로 호출할 수 없다. Portal, DeepAgentBuilder, Agent 설정, 테스트 데이터 및 Mock mapping에서 이름을 함께 변경한다.
|
||||
|
||||
MCP SSE/Streamable HTTP 전송 경로, MCI/HTTP 연동 로직, trace/request ID 흐름은 변경하지 않는다. 이번 범위는 Tool 정의·등록·검색용 메타데이터와 검증에 한정한다.
|
||||
|
||||
기존 JSON input/output Schema Resource 방식은 유지한다. 복잡한 Tool은 Resource를 사용하고 단순 Tool은 DTO 기반 자동 생성을 계속 사용할 수 있다.
|
||||
|
||||
## 12. 테스트 기준
|
||||
|
||||
- 표준 정의 로더 단위 테스트
|
||||
- 필수 필드 및 반려 조건별 검증 테스트
|
||||
- description 렌더링과 `_meta` 매핑 테스트
|
||||
- input/output Schema 우선순위 테스트
|
||||
- Tool Manifest 직렬화 테스트
|
||||
- Tool Pod 및 Gateway MCP Tool 정의 동등성 테스트
|
||||
- Scaffold 생성물 컴파일 및 V17 검증 테스트
|
||||
- 전체 모듈 컴파일·테스트
|
||||
- 전체 Tool 정의 V17 검증 통과
|
||||
|
||||
완료 기준은 모든 등록 대상 Tool이 정의 파일을 보유하고, `check`와 `bootJar`에서 V17 검증을 통과하며, MCP `tools/list`와 Manifest에 동일한 표준 정보가 노출되는 것이다.
|
||||
Reference in New Issue
Block a user