feat: apply tool schema v17 metadata
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 22s

This commit is contained in:
jade
2026-08-12 15:46:57 +09:00
parent 97a56efddf
commit 017812cd29
45 changed files with 1554 additions and 50 deletions

View File

@@ -98,8 +98,19 @@ public class ScaffoldingController {
if (inputFields.isEmpty()) {
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false));
}
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields, httpApiName);
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
req.get("functionDescription"),
req.get("whenToUse"),
req.get("whenNotToUse"),
req.get("ioLimits"),
req.get("displayDescription"),
parseDelimited(req.get("exampleQueries")),
parseDelimited(req.get("tags")),
req.get("ownerOrg"));
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType,
moduleName, author, date, register, clientSystemCode, inputSchemaResource,
outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
@@ -151,9 +162,11 @@ public class ScaffoldingController {
Generate an MCP Tool scaffold from the user request.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true}]}
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true}]}
categoryKey must be exactly three lowercase letters or digits.
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal.
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
@@ -163,15 +176,7 @@ public class ScaffoldingController {
String response = generateAiContent(prompt, req.get("model"));
ToolDraft draft = objectMapper.readValue(stripCodeFence(response), ToolDraft.class);
ToolDraft validatedDraft = validateToolDraft(draft);
return ResponseEntity.ok(Map.of(
"baseName", validatedDraft.baseName(),
"title", validatedDraft.title(),
"description", validatedDraft.description(),
"categoryKey", validatedDraft.categoryKey(),
"routingType", validatedDraft.routingType(),
"httpApiName", validatedDraft.httpApiName(),
"inputFields", validatedDraft.inputFields(),
"outputFields", validatedDraft.outputFields()));
return ResponseEntity.ok(validatedDraft);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "AI Tool 초안 생성 실패: " + safeMessage(e)));
}
@@ -219,6 +224,17 @@ public class ScaffoldingController {
return objectMapper.readValue(source, new TypeReference<List<ToolScaffolder.FieldDefinition>>() { });
}
private List<String> parseDelimited(String source) {
if (source == null || source.isBlank()) {
return List.of();
}
return Arrays.stream(source.split("[\\r\\n,]+"))
.map(String::trim)
.filter(value -> !value.isBlank())
.distinct()
.toList();
}
private List<ToolScaffolder.FieldDefinition> validateFields(List<ToolScaffolder.FieldDefinition> source) {
if (source == null || source.isEmpty()) {
throw new IllegalArgumentException("AI가 필드를 생성하지 않았습니다.");
@@ -268,10 +284,37 @@ public class ScaffoldingController {
if (title.isBlank() || description.isBlank()) {
throw new IllegalArgumentException("AI가 Tool 제목 또는 설명을 생성하지 않았습니다.");
}
String functionDescription = textOrDefault(draft.functionDescription(), description);
String displayDescription = textOrDefault(draft.displayDescription(), title);
String whenToUse = textOrDefault(draft.whenToUse(), description + " 요청을 처리할 때 사용한다.");
String whenNotToUse = textOrDefault(draft.whenNotToUse(), "필수 입력값이 없거나 다른 업무 요청에는 사용하지 않는다.");
String ioLimits = textOrDefault(draft.ioLimits(), "정의된 입력 필드만 허용하며 정의된 응답 DTO 범위만 반환한다.");
List<String> exampleQueries = normalizedDraftList(draft.exampleQueries(), List.of(
title + " 해줘", title + " 정보를 알려줘", title + " 결과를 확인해줘"));
List<String> tags = normalizedDraftList(draft.tags(), List.of(categoryKey));
String ownerOrg = textOrDefault(draft.ownerOrg(), "MCP_TOOL");
return new ToolDraft(draft.baseName().trim(), title, description, categoryKey, routingType, httpApiName,
functionDescription, displayDescription, whenToUse, whenNotToUse, ioLimits,
exampleQueries, tags, ownerOrg,
validateFields(draft.inputFields()), validateFields(draft.outputFields()));
}
private String textOrDefault(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private List<String> normalizedDraftList(List<String> values, List<String> fallback) {
if (values == null) {
return fallback;
}
List<String> normalized = values.stream()
.filter(value -> value != null && !value.isBlank())
.map(String::trim)
.distinct()
.toList();
return normalized.isEmpty() ? fallback : normalized;
}
private String generateAiContent(String prompt, String requestedModel) {
return chatClientBuilder.build().prompt()
.user(prompt)
@@ -306,7 +349,10 @@ public class ScaffoldingController {
}
private record ToolDraft(String baseName, String title, String description, String categoryKey, String routingType,
String httpApiName, List<ToolScaffolder.FieldDefinition> inputFields,
String httpApiName, String functionDescription, String displayDescription,
String whenToUse, String whenNotToUse, String ioLimits,
List<String> exampleQueries, List<String> tags, String ownerOrg,
List<ToolScaffolder.FieldDefinition> inputFields,
List<ToolScaffolder.FieldDefinition> outputFields) {
}
}

View File

@@ -16,6 +16,7 @@ package io.shinhanlife.dap.mcg.sync;
* </pre>
*/
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.mcp.ToolMetadataMcpMapper;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
@@ -48,18 +49,7 @@ public class RegistryMcpToolSpecificationFactory {
* Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다.
*/
public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) {
McpSchema.Tool tool = McpSchema.Tool.builder()
.name(entry.getName())
.description(description(entry))
.inputSchema(toJsonSchema(inputSchema(entry)))
.annotations(new McpSchema.ToolAnnotations(
entry.getDisplayName(),
entry.getReadOnlyHint(),
entry.getDestructiveHint(),
entry.getIdempotentHint(),
entry.getOpenWorldHint(),
null))
.build();
McpSchema.Tool tool = ToolMetadataMcpMapper.toTool(entry);
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)

View File

@@ -868,6 +868,43 @@
<div class="input-hint">LLM call guidance: purpose, when to use it, required conditions, and exclusions.</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="col-md-6">
@@ -1692,6 +1729,16 @@
form.elements.categoryKey.value = result.categoryKey || '';
form.elements.routingType.value = result.routingType || 'HTTP';
form.elements.httpApiName.value = result.httpApiName || '';
form.elements.functionDescription.value = result.functionDescription || '';
form.elements.displayDescription.value = result.displayDescription || '';
form.elements.whenToUse.value = result.whenToUse || '';
form.elements.whenNotToUse.value = result.whenNotToUse || '';
form.elements.ioLimits.value = result.ioLimits || '';
form.elements.exampleQueries.value = Array.isArray(result.exampleQueries)
? result.exampleQueries.join('\n') : (result.exampleQueries || '');
form.elements.tags.value = Array.isArray(result.tags)
? result.tags.join(', ') : (result.tags || '');
form.elements.ownerOrg.value = result.ownerOrg || 'MCP_TOOL';
document.getElementById('inputFields').value = JSON.stringify(result.inputFields || [], null, 2);
document.getElementById('outputFields').value = JSON.stringify(result.outputFields || [], null, 2);
alert('Tool 초안을 채웠습니다. Legacy Interface ID와 Client System Code는 실제 연계 명세를 확인해 입력해주세요.');