From 274aeb76957ccc201742e2ca739b9e78b95585da Mon Sep 17 00:00:00 2001 From: matevip Date: Thu, 7 May 2026 14:39:27 +0800 Subject: [PATCH] fix(tool/mcp): split full URL into base + endpoint for HTTP transports (#70) --- .../tool/mcp/runtime/McpClientManager.java | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/tool/mcp/runtime/McpClientManager.java b/mateclaw-server/src/main/java/vip/mate/tool/mcp/runtime/McpClientManager.java index e55245aa..ad835a4f 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/mcp/runtime/McpClientManager.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/mcp/runtime/McpClientManager.java @@ -17,6 +17,8 @@ import org.springframework.stereotype.Component; import vip.mate.tool.mcp.model.McpServerEntity; import jakarta.annotation.PreDestroy; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Duration; import java.time.LocalDateTime; import java.util.*; @@ -371,7 +373,9 @@ public class McpClientManager { Duration connectTimeout = Duration.ofSeconds( server.getConnectTimeoutSeconds() != null ? server.getConnectTimeoutSeconds() : 30); - var builder = HttpClientSseClientTransport.builder(server.getUrl()) + HttpEndpointConfig endpointConfig = splitHttpUrl(server.getUrl(), "/sse"); + var builder = HttpClientSseClientTransport.builder(endpointConfig.baseUrl()) + .sseEndpoint(endpointConfig.endpoint()) .connectTimeout(connectTimeout); // Add headers via request customizer @@ -391,7 +395,9 @@ public class McpClientManager { Duration connectTimeout = Duration.ofSeconds( server.getConnectTimeoutSeconds() != null ? server.getConnectTimeoutSeconds() : 30); - var builder = HttpClientStreamableHttpTransport.builder(server.getUrl()) + HttpEndpointConfig endpointConfig = splitHttpUrl(server.getUrl(), "/mcp"); + var builder = HttpClientStreamableHttpTransport.builder(endpointConfig.baseUrl()) + .endpoint(endpointConfig.endpoint()) .connectTimeout(connectTimeout); // Add headers via request customizer @@ -407,6 +413,45 @@ public class McpClientManager { return builder.build(); } + /** + * Splits a full HTTP MCP URL into a {@code scheme://authority} base and a + * {@code path[?query]} endpoint suffix. The underlying SDK builders take + * the two halves separately and resolve them via {@link URI#resolve(URI)}, + * which replaces the base URL's path with the endpoint when the endpoint + * starts with {@code /}. Passing a full URL as the base would therefore + * silently route every request to the SDK's default endpoint + * (e.g. {@code /mcp}) and drop any user-configured path or query string. + * + * @param url the user-configured full URL + * @param defaultEndpoint endpoint to use when the URL has no path + */ + static HttpEndpointConfig splitHttpUrl(String url, String defaultEndpoint) { + String trimmed = url != null ? url.trim() : ""; + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("MCP server URL must not be empty"); + } + URI uri; + try { + uri = new URI(trimmed); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid MCP server URL: " + url, e); + } + if (uri.getScheme() == null || uri.getRawAuthority() == null) { + throw new IllegalArgumentException("MCP server URL must include scheme and host: " + url); + } + String path = uri.getRawPath(); + String endpoint = (path == null || path.isEmpty() || "/".equals(path)) ? defaultEndpoint : path; + String query = uri.getRawQuery(); + if (query != null && !query.isEmpty()) { + endpoint += "?" + query; + } + String baseUrl = uri.getScheme() + "://" + uri.getRawAuthority(); + return new HttpEndpointConfig(baseUrl, endpoint); + } + + record HttpEndpointConfig(String baseUrl, String endpoint) { + } + private Map parseHeaders(McpServerEntity server) { if (server.getHeadersJson() != null && !server.getHeadersJson().isBlank()) { Map headers = JSONUtil.toBean(server.getHeadersJson(),