package com.vincent.rsf.server.ai.service.mcp; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.vincent.rsf.server.ai.constant.AiMcpConstants; import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult; import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor; import com.vincent.rsf.server.system.entity.AiMcpMount; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @Component public class AiMcpHttpClient { @Resource private ObjectMapper objectMapper; @Resource private AiMcpPayloadMapper aiMcpPayloadMapper; /** * 通过 Streamable HTTP 协议加载远程 MCP 工具目录。 */ public List listTools(AiMcpMount mount) { initialize(mount); JsonNode result = sendRequest(mount, "tools/list", new LinkedHashMap(), true); List output = new ArrayList<>(); JsonNode toolsNode = result.path("tools"); if (!toolsNode.isArray()) { return output; } for (JsonNode item : toolsNode) { AiMcpToolDescriptor descriptor = aiMcpPayloadMapper.toExternalToolDescriptor(mount, item); if (descriptor != null) { output.add(descriptor); } } return output; } /** * 通过 Streamable HTTP 协议执行一次远程工具调用。 */ public AiDiagnosticToolResult callTool(AiMcpMount mount, String toolName, Map arguments) { initialize(mount); Map params = new LinkedHashMap<>(); params.put("name", toolName); params.put("arguments", arguments == null ? new LinkedHashMap() : arguments); JsonNode result = sendRequest(mount, "tools/call", params, true); return aiMcpPayloadMapper.toExternalToolResult(mount, toolName, result); } /** * 执行 MCP initialize + notifications/initialized 握手。 */ private void initialize(AiMcpMount mount) { Map params = new LinkedHashMap<>(); params.put("protocolVersion", AiMcpConstants.PROTOCOL_VERSION); params.put("capabilities", new LinkedHashMap()); Map clientInfo = new LinkedHashMap<>(); clientInfo.put("name", "rsf-server"); clientInfo.put("version", AiMcpConstants.SERVER_VERSION); params.put("clientInfo", clientInfo); sendRequest(mount, "initialize", params, true); sendRequest(mount, "notifications/initialized", new LinkedHashMap(), false); } /** * 发送一条 JSON-RPC 请求到远程 MCP HTTP 端点。 */ private JsonNode sendRequest(AiMcpMount mount, String method, Object params, boolean expectResponse) { HttpURLConnection connection = null; try { connection = (HttpURLConnection) new URL(mount.getUrl()).openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setConnectTimeout(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs()); connection.setReadTimeout(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs()); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Accept", "application/json"); applyAuthHeaders(connection, mount); Map body = new LinkedHashMap<>(); body.put("jsonrpc", "2.0"); if (expectResponse) { body.put("id", String.valueOf(System.currentTimeMillis())); } body.put("method", method); body.put("params", params == null ? new LinkedHashMap() : params); try (OutputStream outputStream = connection.getOutputStream()) { outputStream.write(objectMapper.writeValueAsBytes(body)); outputStream.flush(); } int statusCode = connection.getResponseCode(); InputStream inputStream = statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream(); if (!expectResponse) { return null; } if (inputStream == null) { throw new IllegalStateException("MCP服务返回空响应"); } String payload = readPayload(inputStream); JsonNode root = objectMapper.readTree(payload); if (root.has("error") && !root.get("error").isNull()) { throw new IllegalStateException(root.path("error").path("message").asText("MCP调用失败")); } return root.path("result"); } catch (Exception e) { throw new IllegalStateException("MCP请求失败: " + e.getMessage(), e); } finally { if (connection != null) { connection.disconnect(); } } } /** * 按挂载配置写入鉴权请求头。 */ private void applyAuthHeaders(HttpURLConnection connection, AiMcpMount mount) { if (mount == null || mount.getAuthType() == null || mount.getAuthValue() == null || mount.getAuthValue().trim().isEmpty()) { return; } String authType = mount.getAuthType().trim().toUpperCase(); if (AiMcpConstants.AUTH_TYPE_BEARER.equals(authType)) { connection.setRequestProperty("Authorization", "Bearer " + mount.getAuthValue().trim()); } else if (AiMcpConstants.AUTH_TYPE_API_KEY.equals(authType)) { connection.setRequestProperty("X-API-Key", mount.getAuthValue().trim()); } } /** * 读取 HTTP 响应体全文。 */ private String readPayload(InputStream inputStream) throws Exception { StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { output.append(line); } } return output.toString(); } }