|
31 | 31 | import io.quarkiverse.langchain4j.mcp.auth.McpClientAuthProvider; |
32 | 32 | import io.smallrye.mutiny.Uni; |
33 | 33 | import io.smallrye.mutiny.infrastructure.Infrastructure; |
| 34 | +import io.vertx.core.Handler; |
34 | 35 | import io.vertx.core.MultiMap; |
| 36 | +import io.vertx.core.buffer.Buffer; |
35 | 37 | import io.vertx.core.http.HttpClient; |
36 | 38 | import io.vertx.core.http.HttpMethod; |
37 | 39 | import io.vertx.core.http.RequestOptions; |
38 | | -import io.vertx.core.parsetools.RecordParser; |
39 | 40 |
|
40 | 41 | public class QuarkusStreamableHttpMcpTransport implements McpTransport { |
41 | 42 |
|
@@ -151,16 +152,34 @@ private Uni<JsonNode> execute(McpClientMessage request, Long id) { |
151 | 152 | log.debug("Assigned MCP session ID: " + mcpSessionId); |
152 | 153 | this.mcpSessionId.set(mcpSessionId); |
153 | 154 | } |
154 | | - |
155 | | - RecordParser sseEventparser = RecordParser.newDelimited("\n\n", bodyBuffer -> { |
156 | | - String responseString = bodyBuffer.toString(); |
157 | | - SseEvent<String> sseEvent = parseSseEvent(responseString); |
158 | | - sseSubscriber.accept(sseEvent); |
159 | | - }); |
160 | | - |
| 155 | + // Handler that splits SSE events whether they are separated by \r\n\r\n or \n\n |
| 156 | + Handler<Buffer> sseEventparser = new Handler<Buffer>() { |
| 157 | + private StringBuffer sb = new StringBuffer(); |
| 158 | + |
| 159 | + @Override |
| 160 | + public void handle(Buffer event) { |
| 161 | + sb.append(event.toString()); |
| 162 | + String str = sb.toString(); |
| 163 | + if (str.contains("\r\n\r\n")) { |
| 164 | + String[] parts = str.split("\r\n\r\n", 2); |
| 165 | + String eventStr = parts[0]; |
| 166 | + sb = new StringBuffer(); |
| 167 | + sb.append(parts[1]); |
| 168 | + SseEvent<String> sseEvent = parseSseEvent(eventStr); |
| 169 | + sseSubscriber.accept(sseEvent); |
| 170 | + } else if (str.contains("\n\n")) { |
| 171 | + String[] parts = str.split("\n\n", 2); |
| 172 | + String eventStr = parts[0]; |
| 173 | + sb = new StringBuffer(); |
| 174 | + sb.append(parts[1]); |
| 175 | + SseEvent<String> sseEvent = parseSseEvent(eventStr); |
| 176 | + sseSubscriber.accept(sseEvent); |
| 177 | + } |
| 178 | + } |
| 179 | + }; |
161 | 180 | String contentType = response.result().getHeader("Content-Type"); |
162 | 181 | if (id != null && contentType != null && contentType.contains("text/event-stream")) { |
163 | | - // the server has started a SSE channel |
| 182 | + // the server has started an SSE channel |
164 | 183 | response.result().handler(sseEventparser); |
165 | 184 | } else { |
166 | 185 | // the server has sent a single regular response |
@@ -202,7 +221,8 @@ private MultivaluedMap<String, Object> toMultivaluedMap(MultiMap multiMap) { |
202 | 221 |
|
203 | 222 | // FIXME: this may be brittle, is there a more standard way to parse SSE events? |
204 | 223 | private SseEvent<String> parseSseEvent(String responseString) { |
205 | | - Map<String, String> entries = Arrays.stream(responseString.split("\\n")) |
| 224 | + // use \\R to match any line ending because some servers use \r\n and some use \n |
| 225 | + Map<String, String> entries = Arrays.stream(responseString.split("\\R")) |
206 | 226 | .collect(Collectors.toMap(s -> s.substring(0, s.indexOf(":")), |
207 | 227 | s -> s.substring(s.indexOf(":") + 2))); |
208 | 228 | return new SseEvent<String>() { |
|
0 commit comments