#AI
zhou zhou
9 小时以前 51877df13075ad10ef51107f15bcd21f1661febe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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<AiMcpToolDescriptor> listTools(AiMcpMount mount) {
        initialize(mount);
        JsonNode result = sendRequest(mount, "tools/list", new LinkedHashMap<String, Object>(), true);
        List<AiMcpToolDescriptor> 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<String, Object> arguments) {
        initialize(mount);
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("name", toolName);
        params.put("arguments", arguments == null ? new LinkedHashMap<String, Object>() : 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<String, Object> params = new LinkedHashMap<>();
        params.put("protocolVersion", AiMcpConstants.PROTOCOL_VERSION);
        params.put("capabilities", new LinkedHashMap<String, Object>());
        Map<String, Object> 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<String, Object>(), 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<String, Object> 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<String, Object>() : 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();
    }
}