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