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:
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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는 실제 연계 명세를 확인해 입력해주세요.');
|
||||
|
||||
Reference in New Issue
Block a user