feat: apply tool schema v17 metadata
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 22s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 22s
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`
|
- Tool Scaffold는 Pod 이름을 Tool 함수명에 포함하지 않습니다. 함수명은 `도메인_비즈니스_행위` 형식입니다. 예: `cmm_notification_send`
|
||||||
- Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다.
|
- Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다.
|
||||||
- Tool Source Update 기능은 `dap-was-*` 아래의 `*UseCase.java`를 검색합니다.
|
- 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
|
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 {
|
subprojects {
|
||||||
// 배포용 Spring Boot JAR 생성 전에 Tool 이름 중복 검증을 강제합니다.
|
// 배포용 Spring Boot JAR 생성 전에 Tool 이름 중복 검증을 강제합니다.
|
||||||
tasks.matching { it.name == 'bootJar' }.configureEach {
|
tasks.matching { it.name == 'bootJar' }.configureEach {
|
||||||
dependsOn rootProject.tasks.named('validateMcpToolNames')
|
dependsOn rootProject.tasks.named('validateMcpToolNames')
|
||||||
|
dependsOn rootProject.tasks.named('validateToolSchemaV17')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,19 @@ public class ScaffoldingController {
|
|||||||
if (inputFields.isEmpty()) {
|
if (inputFields.isEmpty()) {
|
||||||
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false));
|
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false));
|
||||||
}
|
}
|
||||||
|
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
|
||||||
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields, httpApiName);
|
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, definitionOptions);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return "오류 발생: " + e.getMessage();
|
return "오류 발생: " + e.getMessage();
|
||||||
}
|
}
|
||||||
@@ -151,9 +162,11 @@ public class ScaffoldingController {
|
|||||||
Generate an MCP Tool scaffold from the user request.
|
Generate an MCP Tool scaffold from the user request.
|
||||||
Return JSON only. Do not add Markdown, explanations, or code fences.
|
Return JSON only. Do not add Markdown, explanations, or code fences.
|
||||||
The response must have this exact shape:
|
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.
|
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.
|
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.
|
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.
|
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.
|
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"));
|
String response = generateAiContent(prompt, req.get("model"));
|
||||||
ToolDraft draft = objectMapper.readValue(stripCodeFence(response), ToolDraft.class);
|
ToolDraft draft = objectMapper.readValue(stripCodeFence(response), ToolDraft.class);
|
||||||
ToolDraft validatedDraft = validateToolDraft(draft);
|
ToolDraft validatedDraft = validateToolDraft(draft);
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(validatedDraft);
|
||||||
"baseName", validatedDraft.baseName(),
|
|
||||||
"title", validatedDraft.title(),
|
|
||||||
"description", validatedDraft.description(),
|
|
||||||
"categoryKey", validatedDraft.categoryKey(),
|
|
||||||
"routingType", validatedDraft.routingType(),
|
|
||||||
"httpApiName", validatedDraft.httpApiName(),
|
|
||||||
"inputFields", validatedDraft.inputFields(),
|
|
||||||
"outputFields", validatedDraft.outputFields()));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.internalServerError().body(Map.of("error", "AI Tool 초안 생성 실패: " + safeMessage(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>>() { });
|
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) {
|
private List<ToolScaffolder.FieldDefinition> validateFields(List<ToolScaffolder.FieldDefinition> source) {
|
||||||
if (source == null || source.isEmpty()) {
|
if (source == null || source.isEmpty()) {
|
||||||
throw new IllegalArgumentException("AI가 필드를 생성하지 않았습니다.");
|
throw new IllegalArgumentException("AI가 필드를 생성하지 않았습니다.");
|
||||||
@@ -268,10 +284,37 @@ public class ScaffoldingController {
|
|||||||
if (title.isBlank() || description.isBlank()) {
|
if (title.isBlank() || description.isBlank()) {
|
||||||
throw new IllegalArgumentException("AI가 Tool 제목 또는 설명을 생성하지 않았습니다.");
|
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,
|
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()));
|
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) {
|
private String generateAiContent(String prompt, String requestedModel) {
|
||||||
return chatClientBuilder.build().prompt()
|
return chatClientBuilder.build().prompt()
|
||||||
.user(prompt)
|
.user(prompt)
|
||||||
@@ -306,7 +349,10 @@ public class ScaffoldingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private record ToolDraft(String baseName, String title, String description, String categoryKey, String routingType,
|
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) {
|
List<ToolScaffolder.FieldDefinition> outputFields) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package io.shinhanlife.dap.mcg.sync;
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
|
import io.shinhanlife.dap.lib.mcp.ToolMetadataMcpMapper;
|
||||||
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
||||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
@@ -48,18 +49,7 @@ public class RegistryMcpToolSpecificationFactory {
|
|||||||
* Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다.
|
* Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다.
|
||||||
*/
|
*/
|
||||||
public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) {
|
public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) {
|
||||||
McpSchema.Tool tool = McpSchema.Tool.builder()
|
McpSchema.Tool tool = ToolMetadataMcpMapper.toTool(entry);
|
||||||
.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();
|
|
||||||
|
|
||||||
return McpServerFeatures.SyncToolSpecification.builder()
|
return McpServerFeatures.SyncToolSpecification.builder()
|
||||||
.tool(tool)
|
.tool(tool)
|
||||||
|
|||||||
@@ -868,6 +868,43 @@
|
|||||||
<div class="input-hint">LLM call guidance: purpose, when to use it, required conditions, and exclusions.</div>
|
<div class="input-hint">LLM call guidance: purpose, when to use it, required conditions, and exclusions.</div>
|
||||||
</div>
|
</div>
|
||||||
</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="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -1692,6 +1729,16 @@
|
|||||||
form.elements.categoryKey.value = result.categoryKey || '';
|
form.elements.categoryKey.value = result.categoryKey || '';
|
||||||
form.elements.routingType.value = result.routingType || 'HTTP';
|
form.elements.routingType.value = result.routingType || 'HTTP';
|
||||||
form.elements.httpApiName.value = result.httpApiName || '';
|
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('inputFields').value = JSON.stringify(result.inputFields || [], null, 2);
|
||||||
document.getElementById('outputFields').value = JSON.stringify(result.outputFields || [], null, 2);
|
document.getElementById('outputFields').value = JSON.stringify(result.outputFields || [], null, 2);
|
||||||
alert('Tool 초안을 채웠습니다. Legacy Interface ID와 Client System Code는 실제 연계 명세를 확인해 입력해주세요.');
|
alert('Tool 초안을 채웠습니다. Legacy Interface ID와 Client System Code는 실제 연계 명세를 확인해 입력해주세요.');
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
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 static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import io.shinhanlife.dap.mcg.sync.RegistryMcpToolSpecificationFactory;
|
import io.shinhanlife.dap.mcg.sync.RegistryMcpToolSpecificationFactory;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
class RegistryMcpToolSpecificationFactoryTest {
|
class RegistryMcpToolSpecificationFactoryTest {
|
||||||
@@ -15,7 +18,15 @@ class RegistryMcpToolSpecificationFactoryTest {
|
|||||||
void exposesMetadataBehaviorHintsInMcpToolSpecification() {
|
void exposesMetadataBehaviorHintsInMcpToolSpecification() {
|
||||||
ToolMetadata metadata = ToolMetadata.builder()
|
ToolMetadata metadata = ToolMetadata.builder()
|
||||||
.name("customer_lookup")
|
.name("customer_lookup")
|
||||||
|
.displayName("고객 조회")
|
||||||
.description("Looks up customer information")
|
.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)
|
.readOnlyHint(true)
|
||||||
.destructiveHint(false)
|
.destructiveHint(false)
|
||||||
.idempotentHint(true)
|
.idempotentHint(true)
|
||||||
@@ -29,5 +40,9 @@ class RegistryMcpToolSpecificationFactoryTest {
|
|||||||
assertFalse(tool.annotations().destructiveHint());
|
assertFalse(tool.annotations().destructiveHint());
|
||||||
assertTrue(tool.annotations().idempotentHint());
|
assertTrue(tool.annotations().idempotentHint());
|
||||||
assertFalse(tool.annotations().openWorldHint());
|
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.options(any(ChatOptions.class))).thenReturn(requestSpec);
|
||||||
when(requestSpec.call()).thenReturn(responseSpec);
|
when(requestSpec.call()).thenReturn(responseSpec);
|
||||||
when(responseSpec.content()).thenReturn("""
|
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(
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||||
new ScaffoldingController(builder, new ObjectMapper()))
|
new ScaffoldingController(builder, new ObjectMapper()))
|
||||||
@@ -43,6 +43,14 @@ class ScaffoldingControllerToolDraftTest {
|
|||||||
.content("{\"description\":\"고객번호로 계약 상태를 조회\"}"))
|
.content("{\"description\":\"고객번호로 계약 상태를 조회\"}"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.baseName").value("CustomerContractStatus"))
|
.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"));
|
.andExpect(jsonPath("$.inputFields[0].name").value("customerId"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ dependencies {
|
|||||||
|
|
||||||
// MCI XML 전문, JSON Tool Schema/Manifest 처리에 사용합니다.
|
// MCI XML 전문, JSON Tool Schema/Manifest 처리에 사용합니다.
|
||||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
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.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||||
api 'com.networknt:json-schema-validator:3.0.0'
|
api 'com.networknt:json-schema-validator:3.0.0'
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public record ToolManifestItem(
|
|||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
Map<String, Object> inputSchema,
|
Map<String, Object> inputSchema,
|
||||||
|
Map<String, Object> outputSchema,
|
||||||
ToolManifestAnnotations annotations,
|
ToolManifestAnnotations annotations,
|
||||||
@JsonProperty("_meta") ToolManifestMeta meta) {
|
@JsonProperty("_meta") ToolManifestMeta meta) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
package io.shinhanlife.dap.lib.manifest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** Operational metadata exposed by the Tool Service manifest. */
|
/** 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();
|
? tool.getName() : tool.getDisplayName();
|
||||||
Map<String, Object> schema = tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema();
|
Map<String, Object> schema = tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema();
|
||||||
return new ToolManifestItem(
|
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()),
|
new ToolManifestAnnotations(title, isTrue(tool.getReadOnlyHint()), isTrue(tool.getDestructiveHint()),
|
||||||
isTrue(tool.getIdempotentHint()), isTrue(tool.getOpenWorldHint())),
|
isTrue(tool.getIdempotentHint()), isTrue(tool.getOpenWorldHint())),
|
||||||
new ToolManifestMeta(defaultString(tool.getSemver(), "1.0.0"),
|
new ToolManifestMeta(defaultString(tool.getSemver(), "1.0.0"),
|
||||||
tool.getTimeoutMillis() == null ? DEFAULT_TIMEOUT_MILLIS : tool.getTimeoutMillis(),
|
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) {
|
private void validate(List<ToolManifestItem> tools) {
|
||||||
@@ -128,4 +130,8 @@ public class ToolManifestService {
|
|||||||
private String defaultString(String value, String fallback) {
|
private String defaultString(String value, String fallback) {
|
||||||
return value == null || value.isBlank() ? fallback : value;
|
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) {
|
private McpServerFeatures.SyncToolSpecification specification(ToolMetadata tool) {
|
||||||
McpSchema.Tool mcpTool = McpSchema.Tool.builder()
|
McpSchema.Tool mcpTool = ToolMetadataMcpMapper.toTool(tool);
|
||||||
.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();
|
|
||||||
return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool)
|
return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool)
|
||||||
.callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build();
|
.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.lib.config.McpProperties;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
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 jakarta.annotation.PostConstruct;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -29,14 +31,15 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.aop.support.AopUtils;
|
import org.springframework.aop.support.AopUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -49,7 +52,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
|||||||
@Component
|
@Component
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@RequiredArgsConstructor
|
|
||||||
@ConditionalOnBean(McpToolExecutionService.class)
|
@ConditionalOnBean(McpToolExecutionService.class)
|
||||||
public class ToolRegistryHeartbeatSender {
|
public class ToolRegistryHeartbeatSender {
|
||||||
|
|
||||||
@@ -58,6 +60,23 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
private final McpProperties mcpProperties;
|
private final McpProperties mcpProperties;
|
||||||
private final RestClient restClient = RestClient.create();
|
private final RestClient restClient = RestClient.create();
|
||||||
private final ToolSchemaResolver toolSchemaResolver;
|
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}")
|
@Value("${axhub.gateway.url:http://localhost:8081}")
|
||||||
private String gatewayUrl;
|
private String gatewayUrl;
|
||||||
@@ -143,11 +162,18 @@ public class ToolRegistryHeartbeatSender {
|
|||||||
// TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction
|
// TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction
|
||||||
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
|
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
|
||||||
meta.setParametersSchema(finalSchema);
|
meta.setParametersSchema(finalSchema);
|
||||||
|
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(
|
||||||
|
functionAnnotation, method.getReturnType(), hintAnnotation);
|
||||||
|
if (!outputSchema.isEmpty()) {
|
||||||
|
meta.setOutputSchema(outputSchema);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to generate schema for {}", subToolName, e);
|
log.error("Failed to generate schema for {}", subToolName, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enrichWithDefinition(meta, rawSubToolName, hintAnnotation);
|
||||||
|
|
||||||
if (isRegister) {
|
if (isRegister) {
|
||||||
registeredTools.add(meta);
|
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)
|
@Scheduled(fixedRate = 30000)
|
||||||
public void sendHeartbeats() {
|
public void sendHeartbeats() {
|
||||||
if (registeredTools.isEmpty()) return;
|
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) {
|
||||||
|
}
|
||||||
@@ -47,6 +47,17 @@ public class ToolScaffolder {
|
|||||||
public record FieldDefinition(String name, String type, String description, String example, boolean required) {
|
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 {
|
public static void main(String[] args) throws IOException {
|
||||||
Scanner scanner = new Scanner(System.in);
|
Scanner scanner = new Scanner(System.in);
|
||||||
|
|
||||||
@@ -125,6 +136,17 @@ public class ToolScaffolder {
|
|||||||
* Generates a Tool using an HTTP API name that is resolved from glow.communication.http.api-list.
|
* 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 {
|
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);
|
baseName = toPascalCase(baseName);
|
||||||
title = title == null || title.isBlank() ? baseName : title.trim();
|
title = title == null || title.isBlank() ? baseName : title.trim();
|
||||||
description = description == null ? "" : description.trim();
|
description = description == null ? "" : description.trim();
|
||||||
@@ -145,6 +167,7 @@ public class ToolScaffolder {
|
|||||||
String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json";
|
String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json";
|
||||||
String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json";
|
String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json";
|
||||||
Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/tool-schemas", group.toLowerCase()));
|
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 inputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + inputSchemaFileName;
|
||||||
String outputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + outputSchemaFileName;
|
String outputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + outputSchemaFileName;
|
||||||
|
|
||||||
@@ -878,11 +901,184 @@ public class ToolScaffolder {
|
|||||||
Files.writeString(generatedTestPath, useCaseTestContent(bizPackage, baseName));
|
Files.writeString(generatedTestPath, useCaseTestContent(bizPackage, baseName));
|
||||||
log.append("[Unit Test] ").append(generatedTestPath).append("\\n");
|
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");
|
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");
|
||||||
|
Files.writeString(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");
|
log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n");
|
||||||
|
|
||||||
return log.toString();
|
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) {
|
private static String toKebabCase(String pascalCase) {
|
||||||
if (pascalCase == null || pascalCase.isEmpty()) return pascalCase;
|
if (pascalCase == null || pascalCase.isEmpty()) return pascalCase;
|
||||||
return pascalCase
|
return pascalCase
|
||||||
|
|||||||
@@ -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 displayName; // 사람이 읽는 라벨 (1-128자)
|
||||||
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
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)
|
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||||
private Map<String, Object> parametersSchema;
|
private Map<String, Object> parametersSchema;
|
||||||
|
private Map<String, Object> outputSchema;
|
||||||
|
|
||||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||||
private Map<String, String> actionPrompts;
|
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 {
|
private static class ValidatedRequest {
|
||||||
@McpToolParam(description = "recipient phone number", required = true)
|
@McpToolParam(description = "recipient phone number", required = true)
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(pattern = "^01[0-9]{8,9}$")
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
|
|
||||||
@McpToolParam(description = "issue amount", required = true)
|
@McpToolParam(description = "issue amount", required = true)
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(minimum = "1")
|
||||||
private Long amount;
|
private Long amount;
|
||||||
|
|
||||||
@McpToolParam(description = "approval result")
|
@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;
|
private String approvalStatus;
|
||||||
|
|
||||||
@McpToolParam(description = "page size")
|
@McpToolParam(description = "page size")
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(maximum = "50", defaultValue = "20")
|
||||||
private Integer pageSize;
|
private Integer pageSize;
|
||||||
|
|
||||||
@McpToolParam(description = "reference")
|
@McpToolParam(description = "reference")
|
||||||
|
@io.swagger.v3.oas.annotations.media.Schema(minLength = 1, maxLength = 30)
|
||||||
private String reference;
|
private String reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +122,9 @@ class JsonSchemaGeneratorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class NestedChild {
|
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;
|
private String businessDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package io.shinhanlife.dap.lib.util;
|
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.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -12,6 +16,53 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
|
|
||||||
class ToolScaffolderTest {
|
class ToolScaffolderTest {
|
||||||
|
|
||||||
|
@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
|
@TempDir
|
||||||
Path root;
|
Path root;
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class ToolManifestServiceTest {
|
|||||||
assertTrue(item.annotations().readOnlyHint());
|
assertTrue(item.annotations().readOnlyHint());
|
||||||
assertEquals("1.2.0", item.meta().version());
|
assertEquals("1.2.0", item.meta().version());
|
||||||
assertEquals(3000, item.meta().timeoutMillis());
|
assertEquals(3000, item.meta().timeoutMillis());
|
||||||
|
assertEquals(List.of("계약 상태를 알려줘", "내 계약을 조회해줘", "계약번호로 찾아줘"),
|
||||||
|
item.meta().exampleQueries());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -86,6 +88,9 @@ class ToolManifestServiceTest {
|
|||||||
.podUrl("http://tool-processing.ax-hub.svc.cluster.local:8080")
|
.podUrl("http://tool-processing.ax-hub.svc.cluster.local:8080")
|
||||||
.displayName("계약 조회")
|
.displayName("계약 조회")
|
||||||
.description("계약번호로 계약 정보를 조회합니다.")
|
.description("계약번호로 계약 정보를 조회합니다.")
|
||||||
|
.exampleQueries(List.of("계약 상태를 알려줘", "내 계약을 조회해줘", "계약번호로 찾아줘"))
|
||||||
|
.tags(List.of("계약"))
|
||||||
|
.ownerOrg("MCP_TOOL")
|
||||||
.parametersSchema(Map.of("type", "object", "properties", Map.of("contractNo", Map.of("type", "string")),
|
.parametersSchema(Map.of("type", "object", "properties", Map.of("contractNo", Map.of("type", "string")),
|
||||||
"required", List.of("contractNo"), "additionalProperties", false))
|
"required", List.of("contractNo"), "additionalProperties", false))
|
||||||
.semver(version)
|
.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>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public interface MetaCommonCodeUseCase {
|
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")
|
@ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
|
||||||
Object execute(MetaCommonCodeRequest req);
|
Object execute(MetaCommonCodeRequest req);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest;
|
|||||||
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse;
|
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse;
|
||||||
|
|
||||||
public interface ExchangeRateToolUseCase {
|
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")
|
@ToolHint(register = false, categoryKey = "smp", mappingId = "EXCHANGE_001")
|
||||||
ExchangeRateResponse execute(ExchangeRateRequest req);
|
ExchangeRateResponse execute(ExchangeRateRequest req);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 검증을 실행한다.
|
||||||
@@ -88,8 +88,8 @@ Manifest Tool 항목에는 `outputSchema`를 추가한다. `_meta`에는 `versio
|
|||||||
|
|
||||||
모든 이름을 영문 소문자 snake_case 3~64자로 통일한다. 현재 확인된 변경 대상은 다음과 같다.
|
모든 이름을 영문 소문자 snake_case 3~64자로 통일한다. 현재 확인된 변경 대상은 다음과 같다.
|
||||||
|
|
||||||
- `smp_exchangeRate_inquiry` -> `smp_exchange_rate_inquiry`
|
- `smp_exchangeRate_inquiry` -> `smp_exchange_inquiry`
|
||||||
- `cmm_commonCode_lookup` -> `cmm_common_code_lookup`
|
- `cmm_commonCode_lookup` -> `cmm_comcode_lookup`
|
||||||
|
|
||||||
나머지 Tool도 같은 정규식과 `category_service_action` 의미 구조로 검증한다. MCI/EIMS 인터페이스 ID는 Tool명으로 사용하지 않고 `legacy_interface_id`에 저장한다.
|
나머지 Tool도 같은 정규식과 `category_service_action` 의미 구조로 검증한다. MCI/EIMS 인터페이스 ID는 Tool명으로 사용하지 않고 `legacy_interface_id`에 저장한다.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user