diff --git a/TestClientTransport.java b/TestClientTransport.java new file mode 100644 index 00000000..fe054ea1 --- /dev/null +++ b/TestClientTransport.java @@ -0,0 +1,7 @@ +import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; +public class TestClientTransport { + public static void main(String[] args) { + HttpClientStreamableHttpTransport t = HttpClientStreamableHttpTransport.builder("http://localhost:8086/mcp").build(); + System.out.println("Wait, I can't print private fields easily, but let's just see if it runs."); + } +} diff --git a/TestMcpCall.java b/TestMcpCall.java new file mode 100644 index 00000000..7835893c --- /dev/null +++ b/TestMcpCall.java @@ -0,0 +1,22 @@ +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class TestMcpCall { + public static void main(String[] args) throws Exception { + HttpClient client = HttpClient.newHttpClient(); + String json = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"iam_team_contact\",\"arguments\":{\"target\":\"전체\"}},\"id\":1}"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:8086/mcp")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer dapms") // just in case + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("STATUS: " + response.statusCode()); + System.out.println("BODY: " + response.body()); + } +} diff --git a/TestMcpClient.java b/TestMcpClient.java new file mode 100644 index 00000000..15b37b62 --- /dev/null +++ b/TestMcpClient.java @@ -0,0 +1,39 @@ +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; +import io.modelcontextprotocol.spec.McpSchema; +import java.net.http.HttpRequest; +import java.time.Duration; +import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TestMcpClient { + public static void main(String[] args) throws Exception { + String endpoint = "http://localhost:8086/mcp"; + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); + requestBuilder.header("Authorization", "Bearer dapms"); + + HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(endpoint) + .requestBuilder(requestBuilder) + .connectTimeout(Duration.ofSeconds(5)) + .build(); + + try (McpSyncClient client = McpClient.sync(transport) + .clientInfo(new McpSchema.Implementation("test-client", "1.0.0")) + .requestTimeout(Duration.ofSeconds(30)) + .build()) { + System.out.println("Initializing client..."); + client.initialize(); + System.out.println("Client initialized."); + + System.out.println("Calling tool..."); + McpSchema.CallToolResult result = client.callTool(McpSchema.CallToolRequest.builder() + .name("iam_team_contact") + .arguments(Map.of("target", "전체")) + .build()); + System.out.println("Result: " + result); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/TestReflection.java b/TestReflection.java new file mode 100644 index 00000000..73eb7dec --- /dev/null +++ b/TestReflection.java @@ -0,0 +1,11 @@ +import java.lang.reflect.Method; +import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; + +public class TestReflection { + public static void main(String[] args) throws Exception { + Class clazz = HttpServletStreamableServerTransportProvider.Builder.class; + for (Method m : clazz.getDeclaredMethods()) { + System.out.println(m.getName()); + } + } +} diff --git a/TestSseEndpoint.java b/TestSseEndpoint.java new file mode 100644 index 00000000..34d8cd8d --- /dev/null +++ b/TestSseEndpoint.java @@ -0,0 +1,20 @@ +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class TestSseEndpoint { + public static void main(String[] args) throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:8086/mcp")) + .header("Accept", "text/event-stream") + .header("X-Tool-Server-API-Key", "test") + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("STATUS: " + response.statusCode()); + System.out.println("BODY: " + response.body()); + } +} diff --git a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java index e91c57c2..3c0c6163 100644 --- a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java +++ b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java @@ -16,6 +16,7 @@ package io.shinhanlife.dat.mcg.presentation; * */ import io.shinhanlife.dat.lib.util.PodScaffolder; +import io.shinhanlife.dat.lib.util.MciResponseScaffolder; import io.shinhanlife.dat.lib.util.ToolScaffolder; import io.shinhanlife.dat.lib.util.ToolSourceUpdater; import com.fasterxml.jackson.core.type.TypeReference; @@ -317,6 +318,73 @@ public class ScaffoldingController { } } + @PostMapping("/mci-response/analyze") + public ResponseEntity analyzeMciResponse(@RequestBody MciResponseAnalyzeRequest request) { + if (request == null || request.source() == null || request.source().isBlank()) { + return ResponseEntity.badRequest().body(Map.of("error", "XXXX_O.java 소스를 입력해주세요.")); + } + try { + MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source()); + String fieldsJson = objectMapper.writeValueAsString(parsed.types().stream() + .flatMap(type -> type.fields().stream().map(field -> Map.of( + "ownerType", type.name(), + "sourceName", field.name(), + "description", field.description(), + "javaType", field.type(), + "sensitive", field.sensitive()))) + .toList()); + String prompt = """ + You rename legacy Korean financial-system response fields for an MCP Tool response DTO. + Return JSON only with this exact shape: + {"mappings":[{"ownerType":"SourceOwnerClass","sourceName":"legacyField","targetName":"businessMeaningInEnglish","include":true}]} + + Rules: + - Return exactly one mapping for every input field, preserving ownerType and sourceName verbatim. + - targetName must be a concise, descriptive English Java camelCase identifier. + - Derive the business meaning primarily from description; use sourceName only as supporting metadata. + - Expand abbreviations: No -> Number, Cd -> Code, Nm -> Name, Ymd/Dt -> Date when the description supports it. + - Do not invent fields, examples, descriptions, values, or business rules. + - Keep include=true. Sensitive fields must still be named accurately; the UI will show a warning for human review. + - targetName values must be unique within each ownerType. + + Source fields: + %s + """.formatted(fieldsJson); + String aiResponse = generateAiContent(prompt, request.model()); + AiMciMappingDraft draft = objectMapper.readValue(stripCodeFence(aiResponse), AiMciMappingDraft.class); + List mappings = normalizeAiMappings(parsed, draft); + return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings)); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(Map.of("error", safeMessage(e))); + } catch (Exception e) { + return ResponseEntity.internalServerError() + .body(Map.of("error", "MCI Response AI 분석 실패: " + safeMessage(e))); + } + } + + @PostMapping("/mci-response/generate") + public ResponseEntity generateMciResponse(@RequestBody MciResponseGenerateRequest request) { + if (request == null || request.source() == null || request.source().isBlank()) { + return ResponseEntity.badRequest().body(Map.of("error", "XXXX_O.java 소스를 입력해주세요.")); + } + try { + MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source()); + MciResponseScaffolder.GeneratedSources generated = MciResponseScaffolder.generate( + parsed, + request.responsePackage(), + request.responseClassName(), + request.converterPackage(), + request.converterClassName(), + request.mappings()); + return ResponseEntity.ok(generated); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(Map.of("error", safeMessage(e))); + } catch (Exception e) { + return ResponseEntity.internalServerError() + .body(Map.of("error", "MCI Response 소스 생성 실패: " + safeMessage(e))); + } + } + @PostMapping("/tool/update") public String updateTool(@RequestBody Map req) { try { @@ -360,6 +428,39 @@ public class ScaffoldingController { return objectMapper.readValue(source, new TypeReference>() { }); } + private List normalizeAiMappings( + MciResponseScaffolder.ParsedSource parsed, AiMciMappingDraft draft) { + Map suggestions = new java.util.LinkedHashMap<>(); + if (draft != null && draft.mappings() != null) { + for (AiMciFieldMapping mapping : draft.mappings()) { + if (mapping == null || mapping.ownerType() == null || mapping.sourceName() == null) continue; + suggestions.put(mapping.ownerType() + "#" + mapping.sourceName(), mapping); + } + } + + List result = new java.util.ArrayList<>(); + Map> usedTargets = new java.util.HashMap<>(); + for (MciResponseScaffolder.ParsedType type : parsed.types()) { + Set used = usedTargets.computeIfAbsent(type.name(), ignored -> new LinkedHashSet<>()); + for (MciResponseScaffolder.ParsedField field : type.fields()) { + AiMciFieldMapping suggestion = suggestions.get(type.name() + "#" + field.name()); + String targetName = suggestion == null ? field.name() : suggestion.targetName(); + targetName = targetName == null ? "" : targetName.trim(); + if (!targetName.matches("^[a-z][A-Za-z0-9]*$") || used.contains(targetName)) { + targetName = field.name(); + } + if (!targetName.matches("^[a-z][A-Za-z0-9]*$") || !used.add(targetName)) { + throw new IllegalArgumentException( + "AI가 중복되거나 올바르지 않은 필드명을 생성했습니다: " + type.name() + "." + field.name()); + } + boolean include = suggestion == null || suggestion.include() == null || suggestion.include(); + result.add(new MciResponseScaffolder.FieldMapping( + type.name(), field.name(), targetName, include)); + } + } + return List.copyOf(result); + } + private List parseDelimited(String source) { if (source == null || source.isBlank()) { return List.of(); @@ -551,6 +652,27 @@ public class ScaffoldingController { private record FieldDraft(List fields) { } + public record MciResponseAnalyzeRequest(String source, String model) { + } + + public record MciResponseAnalyzeResponse(MciResponseScaffolder.ParsedSource parsed, + List mappings) { + } + + public record MciResponseGenerateRequest(String source, + String responsePackage, + String responseClassName, + String converterPackage, + String converterClassName, + List mappings) { + } + + private record AiMciMappingDraft(List mappings) { + } + + private record AiMciFieldMapping(String ownerType, String sourceName, String targetName, Boolean include) { + } + private record ToolDraft(String baseName, String title, String description, String categoryKey, String routingType, String httpApiName, String functionDescription, String displayDescription, String whenToUse, String whenNotToUse, String ioLimits, diff --git a/dat-gateway/src/main/resources/application.yml b/dat-gateway/src/main/resources/application.yml index bb7eb7a4..afe41b92 100644 --- a/dat-gateway/src/main/resources/application.yml +++ b/dat-gateway/src/main/resources/application.yml @@ -50,6 +50,8 @@ mcp: hr: http://was-cus:8084 pro: http://was-pro:8085 sys: http://was-sys:8086 + iam: http://was-sys:8086 + agent-claims-required: false trusted-claims-required: false write-approval-required: false diff --git a/dat-gateway/src/main/resources/static/admin/scaffold.html b/dat-gateway/src/main/resources/static/admin/scaffold.html index b7a88a57..cc2d0f21 100644 --- a/dat-gateway/src/main/resources/static/admin/scaffold.html +++ b/dat-gateway/src/main/resources/static/admin/scaffold.html @@ -787,6 +787,9 @@ + @@ -1131,6 +1134,131 @@ + +
+
+
+
+

Glow MCI 응답을 LLM Response로 변환

+
XXXX_O.java의 전문 구조와 @GlowTrgmField.description은 원문 그대로 읽고, AI는 축약 필드명을 업무 의미가 드러나는 영문 camelCase로만 제안합니다.
+
+ 구조: Parser · 이름: AI · 출력: MapStruct +
+
+ +
+
+ + +
파일을 선택하면 아래 소스 입력란에 UTF-8 텍스트로 불러옵니다. 소스를 직접 붙여넣어도 됩니다.
+
+
+ + +
+
+ +
+ + +
+ +
+ +
+ + + +
+
+
@@ -2483,6 +2611,288 @@ } + +