package com.vincent.rsf.server.ai.service.impl.mcp; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.ai.config.AiDefaults; import com.vincent.rsf.server.ai.entity.AiMcpMount; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.time.Duration; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @Slf4j @Component @RequiredArgsConstructor public class McpClientFactory { private final ObjectMapper objectMapper; public McpSyncClient createClient(AiMcpMount mount) { Duration timeout = Duration.ofMillis(mount.getRequestTimeoutMs() == null ? AiDefaults.DEFAULT_TIMEOUT_MS : mount.getRequestTimeoutMs()); JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(objectMapper); if (AiDefaults.MCP_TRANSPORT_STDIO.equals(mount.getTransportType())) { ServerParameters.Builder parametersBuilder = ServerParameters.builder(mount.getCommand()); List args = readStringList(mount.getArgsJson()); if (!args.isEmpty()) { parametersBuilder.args(args); } Map env = readStringMap(mount.getEnvJson()); if (!env.isEmpty()) { parametersBuilder.env(env); } StdioClientTransport transport = new StdioClientTransport(parametersBuilder.build(), jsonMapper); transport.setStdErrorHandler(message -> log.warn("MCP STDIO stderr [{}]: {}", mount.getName(), message)); return McpClient.sync(transport) .requestTimeout(timeout) .initializationTimeout(timeout) .clientInfo(new McpSchema.Implementation("rsf-ai-client", "RSF AI Client", "1.0.0")) .build(); } if (!AiDefaults.MCP_TRANSPORT_SSE_HTTP.equals(mount.getTransportType())) { throw new CoolException("不支持的 MCP 传输类型: " + mount.getTransportType()); } if (!StringUtils.hasText(mount.getServerUrl())) { throw new CoolException("MCP 服务地址不能为空"); } HttpClientSseClientTransport.Builder transportBuilder = HttpClientSseClientTransport.builder(mount.getServerUrl()) .jsonMapper(jsonMapper) .connectTimeout(timeout); if (StringUtils.hasText(mount.getEndpoint())) { transportBuilder.sseEndpoint(mount.getEndpoint()); } Map headers = readStringMap(mount.getHeadersJson()); if (!headers.isEmpty()) { transportBuilder.customizeRequest(builder -> headers.forEach(builder::header)); } return McpClient.sync(transportBuilder.build()) .requestTimeout(timeout) .initializationTimeout(timeout) .clientInfo(new McpSchema.Implementation("rsf-ai-client", "RSF AI Client", "1.0.0")) .build(); } private List readStringList(String json) { if (!StringUtils.hasText(json)) { return Collections.emptyList(); } try { return objectMapper.readValue(json, new TypeReference>() { }); } catch (Exception e) { throw new CoolException("解析 MCP 列表配置失败: " + e.getMessage()); } } private Map readStringMap(String json) { if (!StringUtils.hasText(json)) { return Collections.emptyMap(); } try { Map result = objectMapper.readValue(json, new TypeReference>() { }); return result == null ? Collections.emptyMap() : result; } catch (Exception e) { throw new CoolException("解析 MCP Map 配置失败: " + e.getMessage()); } } }