From cfad317d73bbca6fdd77c5193bacdd59571d481c Mon Sep 17 00:00:00 2001 From: devmrko Date: Thu, 25 Jun 2026 15:38:17 +0900 Subject: [PATCH] [Developer] #424 add Java MCP client demo tab --- .../domain/mcp/McpClientDemoResult.java | 12 ++ .../service/McpClientDemoService.java | 144 ++++++++++++++++++ .../web/McpClientDemoController.java | 64 ++++++++ .../resources/templates/fragments/layout.html | 1 + .../fragments/mcp-client-demo-result.html | 44 ++++++ .../resources/templates/mcp-client-demo.html | 76 +++++++++ 6 files changed, 341 insertions(+) create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/mcp/McpClientDemoResult.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/service/McpClientDemoService.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/web/McpClientDemoController.java create mode 100644 src/main/resources/templates/fragments/mcp-client-demo-result.html create mode 100644 src/main/resources/templates/mcp-client-demo.html diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/mcp/McpClientDemoResult.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/mcp/McpClientDemoResult.java new file mode 100644 index 0000000..5712118 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/mcp/McpClientDemoResult.java @@ -0,0 +1,12 @@ +package com.cloudhandson.vpdbackoffice.domain.mcp; + +public record McpClientDemoResult( + String contextPath, + String messageUrl, + String selectedTool, + String initializeResponse, + String toolsListResponse, + String toolsCallResponse, + String status +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/McpClientDemoService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/McpClientDemoService.java new file mode 100644 index 0000000..63e60c4 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/McpClientDemoService.java @@ -0,0 +1,144 @@ +package com.cloudhandson.vpdbackoffice.service; + +import com.cloudhandson.vpdbackoffice.config.BackofficeProperties; +import com.cloudhandson.vpdbackoffice.domain.mcp.McpClientDemoResult; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.net.URI; +import java.time.Duration; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +public class McpClientDemoService { + + private final BackofficeProperties properties; + private final ObjectMapper objectMapper; + private final RestTemplate restTemplate; + + public McpClientDemoService( + BackofficeProperties properties, + ObjectMapper objectMapper, + RestTemplateBuilder restTemplateBuilder + ) { + this.properties = properties; + this.objectMapper = objectMapper; + this.restTemplate = restTemplateBuilder + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(15)) + .build(); + } + + public McpClientDemoResult run( + String serverOrigin, + String contextPath, + String toolName, + String bearerToken, + int limit + ) { + String normalizedContextPath = normalizeContextPath(contextPath); + URI messageUri = messageUri(serverOrigin, normalizedContextPath); + + JsonNode initializeResponse = post(messageUri, initializeRequest(1)); + JsonNode toolsListResponse = post(messageUri, toolsListRequest(2)); + JsonNode toolsCallResponse = null; + if (hasText(toolName) && hasText(bearerToken)) { + toolsCallResponse = post(messageUri, toolsCallRequest(3, toolName.trim(), bearerToken.trim(), normalizeLimit(limit))); + } + + return new McpClientDemoResult( + normalizedContextPath, + messageUri.toString(), + hasText(toolName) ? toolName.trim() : "", + pretty(initializeResponse), + pretty(toolsListResponse), + toolsCallResponse == null ? "Bearer Token이 없어 tools/call은 실행하지 않았습니다." : pretty(toolsCallResponse), + toolsCallResponse == null ? "TOOLS_LIST_ONLY" : "SUCCESS" + ); + } + + private JsonNode post(URI uri, ObjectNode request) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBasicAuth(properties.security().adminUser(), properties.security().adminPassword()); + String response = restTemplate.postForObject(uri, new HttpEntity<>(request, headers), String.class); + try { + return objectMapper.readTree(response); + } catch (Exception e) { + throw new AppException("MCP client 응답 JSON 파싱 실패: " + e.getMessage()); + } + } + + private URI messageUri(String serverOrigin, String contextPath) { + String path = "default".equals(contextPath) ? "/mcp/messages" : "/mcp/" + contextPath + "/messages"; + return UriComponentsBuilder.fromUriString(serverOrigin) + .path(path) + .build() + .toUri(); + } + + private ObjectNode initializeRequest(int id) { + ObjectNode request = request(id, "initialize"); + request.set("params", objectMapper.createObjectNode()); + return request; + } + + private ObjectNode toolsListRequest(int id) { + ObjectNode request = request(id, "tools/list"); + request.set("params", objectMapper.createObjectNode()); + return request; + } + + private ObjectNode toolsCallRequest(int id, String toolName, String bearerToken, int limit) { + ObjectNode request = request(id, "tools/call"); + ObjectNode params = objectMapper.createObjectNode(); + params.put("name", toolName); + ObjectNode arguments = objectMapper.createObjectNode(); + arguments.put("bearerToken", bearerToken); + arguments.put("limit", limit); + params.set("arguments", arguments); + request.set("params", params); + return request; + } + + private ObjectNode request(int id, String method) { + ObjectNode request = objectMapper.createObjectNode(); + request.put("jsonrpc", "2.0"); + request.put("id", id); + request.put("method", method); + return request; + } + + private String normalizeContextPath(String contextPath) { + String normalized = contextPath == null || contextPath.isBlank() ? "default" : contextPath.trim(); + if (!normalized.matches("[A-Za-z0-9][A-Za-z0-9_-]{0,63}")) { + throw new AppException("MCP context path는 영문/숫자로 시작하고 영문/숫자/_/-만 사용할 수 있습니다: " + contextPath); + } + return normalized.toLowerCase(); + } + + private int normalizeLimit(int limit) { + if (limit < 1) { + return 50; + } + return Math.min(limit, 500); + } + + private boolean hasText(String value) { + return value != null && !value.isBlank(); + } + + private String pretty(JsonNode node) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(node); + } catch (Exception e) { + return String.valueOf(node); + } + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/McpClientDemoController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/McpClientDemoController.java new file mode 100644 index 0000000..1f92265 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/McpClientDemoController.java @@ -0,0 +1,64 @@ +package com.cloudhandson.vpdbackoffice.web; + +import com.cloudhandson.vpdbackoffice.service.McpClientDemoService; +import com.cloudhandson.vpdbackoffice.service.McpToolRegistry; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import org.springframework.dao.DataAccessException; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@Controller +public class McpClientDemoController { + + private final McpToolRegistry toolRegistry; + private final McpClientDemoService demoService; + + public McpClientDemoController(McpToolRegistry toolRegistry, McpClientDemoService demoService) { + this.toolRegistry = toolRegistry; + this.demoService = demoService; + } + + @GetMapping("/mcp-client-demo") + public String page(Model model) { + addTools(model); + return "mcp-client-demo"; + } + + @PostMapping("/mcp-client-demo") + public String run( + @RequestParam(defaultValue = "vpd-live") String contextPath, + @RequestParam(defaultValue = "") String toolName, + @RequestParam(defaultValue = "") String bearerToken, + @RequestParam(defaultValue = "50") int limit, + HttpServletRequest request, + Model model + ) { + addTools(model); + try { + String serverOrigin = ServletUriComponentsBuilder.fromRequestUri(request) + .replacePath(null) + .replaceQuery(null) + .build() + .toUriString(); + model.addAttribute("result", demoService.run(serverOrigin, contextPath, toolName, bearerToken, limit)); + } catch (Exception e) { + model.addAttribute("errorMessage", e.getMessage()); + } + return "fragments/mcp-client-demo-result :: result"; + } + + private void addTools(Model model) { + try { + model.addAttribute("tools", toolRegistry.listTools()); + } catch (DataAccessException e) { + RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(e); + model.addAttribute("tools", List.of()); + model.addAttribute("runtimeError", message); + } + } +} diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index 0655371..ad2c139 100644 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -37,6 +37,7 @@
Reasoning SSE 서비스 + Client Demo
diff --git a/src/main/resources/templates/fragments/mcp-client-demo-result.html b/src/main/resources/templates/fragments/mcp-client-demo-result.html new file mode 100644 index 0000000..dea4980 --- /dev/null +++ b/src/main/resources/templates/fragments/mcp-client-demo-result.html @@ -0,0 +1,44 @@ + + + +
+
+ Java MCP client 호출 실패 +
error
+
+ +
+
+

Java Client 결과

+ SUCCESS +
+ +
+
Context Path
+
vpd-live
+
Message URL
+
http://localhost:8082/mcp/vpd-live/messages
+
Selected Tool
+
tool
+
+ +
+
+

initialize Response

+
{}
+
+
+

tools/list Response

+
{}
+
+
+

tools/call Response

+
{}
+
+
+
+
+ + diff --git a/src/main/resources/templates/mcp-client-demo.html b/src/main/resources/templates/mcp-client-demo.html new file mode 100644 index 0000000..4186764 --- /dev/null +++ b/src/main/resources/templates/mcp-client-demo.html @@ -0,0 +1,76 @@ + + + + + +
+
+

MCP Client Demo

+

백오피스 Java client가 MCP message endpoint를 호출해 initialize, tools/list, tools/call 결과를 확인합니다.

+
+ +
+ DB 연결 설정이 필요합니다. +
DB 연결을 확인할 수 없습니다.
+
+ +
+

Java Client 호출

+
+ + + + + + +
+
+ +
+
+

호출 흐름

+ HTTP JSON-RPC +
+
+
+ 1 + initialize + context path가 반영된 MCP serverInfo와 tools capability를 확인합니다. +
+
+ 2 + tools/list + 현재 보호 객체에서 생성된 ORDS query tool 목록을 조회합니다. +
+
+ 3 + tools/call + Bearer Token이 입력된 경우 선택한 tool을 호출해 ORDS/VPD 결과를 받습니다. +
+
+
+ +
+
Java MCP client 실행 결과가 여기에 표시됩니다.
+
+
+ +