From 2134d719ee17c23699892a8af0ab154c3e5382eb Mon Sep 17 00:00:00 2001 From: hemsej018 Date: Tue, 12 Aug 2025 14:26:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20listTools()=EB=A5=BC=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EB=B0=8F=20=ED=83=80=EC=9E=84=EC=95=84=EC=9B=83=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=97=B0=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/BraveSearchMcpService.java | 73 ++++++++++++------- .../src/main/resources/application.properties | 2 +- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java b/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java index ae595d61..17f9caeb 100644 --- a/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java +++ b/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java @@ -6,6 +6,7 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; +import java.time.Duration; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -18,45 +19,65 @@ public class BraveSearchMcpService { private static final String BRAVE_WEB_TOOL = "brave_web_search"; - + private static final Duration INIT_TIMEOUT = Duration.ofSeconds(60); private final List mcpClients; - private final ObjectMapper objectMapper; public JsonNode search(String query, int count, int offset) { - McpSyncClient braveClient = resolveBraveClient(); - - CallToolRequest request = new CallToolRequest( - BRAVE_WEB_TOOL, - Map.of("query", query, "count", count, "offset", offset) - ); - - CallToolResult result = braveClient.callTool(request); - - JsonNode content = objectMapper.valueToTree(result.content()); - log.info("[Brave MCP Response Raw content]: {}", content.toPrettyString()); - - if (content != null && content.isArray()) { - var root = objectMapper.createObjectNode(); - root.set("results", content); - return root; + try { + McpSyncClient braveClient = resolveBraveClient(); // 내부에서 초기화/툴 확인 + + // 실제 호출 + CallToolRequest request = new CallToolRequest( + BRAVE_WEB_TOOL, + Map.of("query", query, "count", count, "offset", offset) + ); + CallToolResult result = braveClient.callTool(request); + + JsonNode content = objectMapper.valueToTree(result.content()); + log.info("[Brave MCP Response Raw content]: {}", + content != null ? content.toPrettyString() : "null"); + + if (content != null && content.isArray()) { + var root = objectMapper.createObjectNode(); + root.set("results", content); + return root; + } + return content != null ? content : objectMapper.createObjectNode(); + + } catch (Exception e) { + // 폴백: 벡터 검색만 사용 + log.warn("Brave MCP 호출 실패 - 질문: [{}], 벡터 검색만 사용합니다. 사유={}", query, e.toString()); + return objectMapper.createObjectNode(); // 호출부에서 null 체크보다 빈 객체가 안전 } + } - return content != null ? content : objectMapper.createObjectNode(); + private void ensureInitialized(McpSyncClient client) { + if (!client.isInitialized()) { + synchronized (client) { // 다중 스레드 초기화 경합 방지 + if (!client.isInitialized()) { + log.debug("MCP 클라이언트 초기화 시작…"); + client.initialize(); // 매개변수 없는 버전 + log.debug("MCP 클라이언트 초기화 완료"); + } + } + } } private McpSyncClient resolveBraveClient() { for (McpSyncClient client : mcpClients) { - ListToolsResult tools = client.listTools(); - if (tools != null && tools.tools() != null) { - boolean found = tools.tools().stream() - .anyMatch(tool -> BRAVE_WEB_TOOL.equalsIgnoreCase(tool.name())); - if (found) { + try { + ensureInitialized(client); // 초기화 + ListToolsResult tools = client.listTools(); + if (tools != null && tools.tools() != null && + tools.tools().stream() + .anyMatch(t -> BRAVE_WEB_TOOL.equalsIgnoreCase(t.name()))) { return client; } + } catch (Exception e) { + log.debug("Brave MCP 클라이언트 후보 실패: {}", e.toString()); } } - - throw new IllegalStateException("Brave MCP 서버에서 brave_web_search 툴을 찾을 수 없습니다."); + throw new IllegalStateException("Brave MCP 서버에서 '" + BRAVE_WEB_TOOL + "' 툴을 찾을 수 없습니다."); } } diff --git a/cs25-service/src/main/resources/application.properties b/cs25-service/src/main/resources/application.properties index 0ad81543..f5ea13aa 100644 --- a/cs25-service/src/main/resources/application.properties +++ b/cs25-service/src/main/resources/application.properties @@ -65,7 +65,7 @@ spring.ai.chat.client.enabled=false # MCP spring.ai.mcp.client.enabled=true spring.ai.mcp.client.type=SYNC -spring.ai.mcp.client.request-timeout=45s +spring.ai.mcp.client.request-timeout=60s spring.ai.mcp.client.root-change-notification=false # STDIO Connect: Brave Search spring.ai.mcp.client.stdio.connections.brave.command=server-brave-search From 473513f02f5f71c6ef909e539f72523b9e23f148 Mon Sep 17 00:00:00 2001 From: hemsej018 Date: Tue, 12 Aug 2025 14:57:29 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=EA=B8=B0=EC=A1=B4=20MCP=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=82=AC=EC=9A=A9=20=EB=B0=8F=20=EC=95=B1?= =?UTF-8?q?=20=EB=B6=80=ED=8C=85=EC=8B=9C=EC=B4=88=EA=B8=B0=ED=99=94=20tru?= =?UTF-8?q?e=EC=84=A4=EC=A0=95=20=EC=9E=90=EB=8F=99=EB=8F=84=EA=B5=AC?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cs25-service/Dockerfile | 5 +- .../ai/service/BraveSearchMcpService.java | 67 +++++++------------ .../src/main/resources/application.properties | 4 +- 3 files changed, 28 insertions(+), 48 deletions(-) diff --git a/cs25-service/Dockerfile b/cs25-service/Dockerfile index f61197f1..39cb4a4a 100644 --- a/cs25-service/Dockerfile +++ b/cs25-service/Dockerfile @@ -25,7 +25,10 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates gnupg bash \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ - && npm install -g @modelcontextprotocol/server-brave-search \ + && npm install -g @modelcontextprotocol/server-brave-search@0.2.1 \ + && ln -sf "$(npm root -g)/.bin/server-brave-search" /usr/local/bin/server-brave-search \ + && chmod +x /usr/local/bin/server-brave-search \ + && /usr/local/bin/server-brave-search --help || true \ && npm cache clean --force \ && apt-get purge -y gnupg \ && apt-get autoremove -y --purge \ diff --git a/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java b/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java index 17f9caeb..7a31cce3 100644 --- a/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java +++ b/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/BraveSearchMcpService.java @@ -6,7 +6,6 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; -import java.time.Duration; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -19,65 +18,45 @@ public class BraveSearchMcpService { private static final String BRAVE_WEB_TOOL = "brave_web_search"; - private static final Duration INIT_TIMEOUT = Duration.ofSeconds(60); + private final List mcpClients; + private final ObjectMapper objectMapper; public JsonNode search(String query, int count, int offset) { - try { - McpSyncClient braveClient = resolveBraveClient(); // 내부에서 초기화/툴 확인 + McpSyncClient braveClient = resolveBraveClient(); - // 실제 호출 - CallToolRequest request = new CallToolRequest( - BRAVE_WEB_TOOL, - Map.of("query", query, "count", count, "offset", offset) - ); - CallToolResult result = braveClient.callTool(request); + CallToolRequest request = new CallToolRequest( + BRAVE_WEB_TOOL, + Map.of("query", query, "count", count, "offset", offset) + ); - JsonNode content = objectMapper.valueToTree(result.content()); - log.info("[Brave MCP Response Raw content]: {}", - content != null ? content.toPrettyString() : "null"); + CallToolResult result = braveClient.callTool(request); - if (content != null && content.isArray()) { - var root = objectMapper.createObjectNode(); - root.set("results", content); - return root; - } - return content != null ? content : objectMapper.createObjectNode(); + JsonNode content = objectMapper.valueToTree(result.content()); + log.info("[Brave MCP Response Raw content]: {}", content.toPrettyString()); - } catch (Exception e) { - // 폴백: 벡터 검색만 사용 - log.warn("Brave MCP 호출 실패 - 질문: [{}], 벡터 검색만 사용합니다. 사유={}", query, e.toString()); - return objectMapper.createObjectNode(); // 호출부에서 null 체크보다 빈 객체가 안전 + if (content != null && content.isArray()) { + var root = objectMapper.createObjectNode(); + root.set("results", content); + return root; } - } - private void ensureInitialized(McpSyncClient client) { - if (!client.isInitialized()) { - synchronized (client) { // 다중 스레드 초기화 경합 방지 - if (!client.isInitialized()) { - log.debug("MCP 클라이언트 초기화 시작…"); - client.initialize(); // 매개변수 없는 버전 - log.debug("MCP 클라이언트 초기화 완료"); - } - } - } + return content != null ? content : objectMapper.createObjectNode(); } private McpSyncClient resolveBraveClient() { for (McpSyncClient client : mcpClients) { - try { - ensureInitialized(client); // 초기화 - ListToolsResult tools = client.listTools(); - if (tools != null && tools.tools() != null && - tools.tools().stream() - .anyMatch(t -> BRAVE_WEB_TOOL.equalsIgnoreCase(t.name()))) { + ListToolsResult tools = client.listTools(); + if (tools != null && tools.tools() != null) { + boolean found = tools.tools().stream() + .anyMatch(tool -> BRAVE_WEB_TOOL.equalsIgnoreCase(tool.name())); + if (found) { return client; } - } catch (Exception e) { - log.debug("Brave MCP 클라이언트 후보 실패: {}", e.toString()); } } - throw new IllegalStateException("Brave MCP 서버에서 '" + BRAVE_WEB_TOOL + "' 툴을 찾을 수 없습니다."); + + throw new IllegalStateException("Brave MCP 서버에서 brave_web_search 툴을 찾을 수 없습니다."); } -} +} \ No newline at end of file diff --git a/cs25-service/src/main/resources/application.properties b/cs25-service/src/main/resources/application.properties index f5ea13aa..b96ef567 100644 --- a/cs25-service/src/main/resources/application.properties +++ b/cs25-service/src/main/resources/application.properties @@ -68,11 +68,9 @@ spring.ai.mcp.client.type=SYNC spring.ai.mcp.client.request-timeout=60s spring.ai.mcp.client.root-change-notification=false # STDIO Connect: Brave Search -spring.ai.mcp.client.stdio.connections.brave.command=server-brave-search +spring.ai.mcp.client.stdio.connections.brave.command=/usr/local/bin/server-brave-search spring.ai.mcp.client.stdio.connections.brave.args[0]=--stdio spring.ai.mcp.client.stdio.connections.brave.env.BRAVE_API_KEY=${BRAVE_API_KEY} -spring.ai.mcp.client.initialized=false -spring.autoconfigure.exclude=org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration #MAIL spring.mail.host=smtp.gmail.com spring.mail.port=587 From c0c68a9ed9204255d4789f6e8b7f33f6cdbf887e Mon Sep 17 00:00:00 2001 From: hemsej018 Date: Tue, 12 Aug 2025 15:11:54 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=EB=B2=84=EC=A0=BC=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cs25-service/Dockerfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cs25-service/Dockerfile b/cs25-service/Dockerfile index 39cb4a4a..f61197f1 100644 --- a/cs25-service/Dockerfile +++ b/cs25-service/Dockerfile @@ -25,10 +25,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates gnupg bash \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ - && npm install -g @modelcontextprotocol/server-brave-search@0.2.1 \ - && ln -sf "$(npm root -g)/.bin/server-brave-search" /usr/local/bin/server-brave-search \ - && chmod +x /usr/local/bin/server-brave-search \ - && /usr/local/bin/server-brave-search --help || true \ + && npm install -g @modelcontextprotocol/server-brave-search \ && npm cache clean --force \ && apt-get purge -y gnupg \ && apt-get autoremove -y --purge \ From ca8317eacade821c57ea72d26b41e61a91d26a2a Mon Sep 17 00:00:00 2001 From: hemsej018 Date: Tue, 12 Aug 2025 15:34:32 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore=20:=20=EB=8F=84=EC=BB=A4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=9C=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=EB=90=98=EB=8A=94=20=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cs25-service/Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cs25-service/Dockerfile b/cs25-service/Dockerfile index f61197f1..b134928b 100644 --- a/cs25-service/Dockerfile +++ b/cs25-service/Dockerfile @@ -11,7 +11,12 @@ COPY cs25-entity cs25-entity/ COPY cs25-common cs25-common/ # 테스트 생략하여 빌드 안정화 -RUN ./gradlew :cs25-service:bootJar --stacktrace --no-daemon +# (빌드 시 MCP 비활성화 + gradlew 실행 권한 + 테스트 스킵) +ENV SPRING_AI_MCP_CLIENT_ENABLED=false \ + SPRING_AI_MCP_CLIENT_INITIALIZED=false +RUN chmod +x ./gradlew +RUN ./gradlew :cs25-service:bootJar --stacktrace --no-daemon -x test + FROM eclipse-temurin:17-jre-jammy # 메타 정보