feat(mcp): resolve absolute and relative message endpoint URIs (#150)
Improve endpoint URI handling by supporting both relative paths and properly validated absolute URIs. - Implement URI resolution in HttpClientSseClientTransport: - Change baseUri field from String to URI type - Add Utils.resolveUri method to handle both absolute and relative URIs - Resolve relative URIs against the base URI - Validate absolute URIs to ensure they match base URI's scheme, authority, and path - Add parameterized tests for various URI resolution scenarios - Add ByteBuddy dependency for HttpClient mocking and update Mockito Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
734d1732d6
commit
344f1b8383
2 changed files with 136 additions and 10 deletions
|
|
@ -24,6 +24,7 @@ import io.modelcontextprotocol.spec.McpError;
|
|||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import io.modelcontextprotocol.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
|
@ -69,7 +70,7 @@ public class HttpClientSseClientTransport implements McpClientTransport {
|
|||
private static final String DEFAULT_SSE_ENDPOINT = "/sse";
|
||||
|
||||
/** Base URI for the MCP server */
|
||||
private final String baseUri;
|
||||
private final URI baseUri;
|
||||
|
||||
/** SSE endpoint path */
|
||||
private final String sseEndpoint;
|
||||
|
|
@ -178,7 +179,7 @@ public class HttpClientSseClientTransport implements McpClientTransport {
|
|||
Assert.hasText(sseEndpoint, "sseEndpoint must not be empty");
|
||||
Assert.notNull(httpClient, "httpClient must not be null");
|
||||
Assert.notNull(requestBuilder, "requestBuilder must not be null");
|
||||
this.baseUri = baseUri;
|
||||
this.baseUri = URI.create(baseUri);
|
||||
this.sseEndpoint = sseEndpoint;
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = httpClient;
|
||||
|
|
@ -340,7 +341,8 @@ public class HttpClientSseClientTransport implements McpClientTransport {
|
|||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
connectionFuture.set(future);
|
||||
|
||||
sseClient.subscribe(this.baseUri + this.sseEndpoint, new FlowSseClient.SseEventHandler() {
|
||||
URI clientUri = Utils.resolveUri(this.baseUri, this.sseEndpoint);
|
||||
sseClient.subscribe(clientUri.toString(), new FlowSseClient.SseEventHandler() {
|
||||
@Override
|
||||
public void onEvent(SseEvent event) {
|
||||
if (isClosing) {
|
||||
|
|
@ -412,7 +414,8 @@ public class HttpClientSseClientTransport implements McpClientTransport {
|
|||
|
||||
try {
|
||||
String jsonText = this.objectMapper.writeValueAsString(message);
|
||||
HttpRequest request = this.requestBuilder.uri(URI.create(this.baseUri + endpoint))
|
||||
URI requestUri = Utils.resolveUri(baseUri, endpoint);
|
||||
HttpRequest request = this.requestBuilder.uri(requestUri)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonText))
|
||||
.build();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
package io.modelcontextprotocol.util;
|
||||
|
||||
import reactor.util.annotation.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.util.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Miscellaneous utility methods.
|
||||
*
|
||||
|
|
@ -52,4 +53,55 @@ public final class Utils {
|
|||
return (map == null || map.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given endpoint URL against the base URL.
|
||||
* <ul>
|
||||
* <li>If the endpoint URL is relative, it will be resolved against the base URL.</li>
|
||||
* <li>If the endpoint URL is absolute, it will be validated to ensure it matches the
|
||||
* base URL's scheme, authority, and path prefix.</li>
|
||||
* <li>If validation fails for an absolute URL, an {@link IllegalArgumentException} is
|
||||
* thrown.</li>
|
||||
* </ul>
|
||||
* @param baseUrl The base URL (must be absolute)
|
||||
* @param endpointUrl The endpoint URL (can be relative or absolute)
|
||||
* @return The resolved endpoint URI
|
||||
* @throws IllegalArgumentException If the absolute endpoint URL does not match the
|
||||
* base URL or URI is malformed
|
||||
*/
|
||||
public static URI resolveUri(URI baseUrl, String endpointUrl) {
|
||||
URI endpointUri = URI.create(endpointUrl);
|
||||
if (endpointUri.isAbsolute() && !isUnderBaseUri(baseUrl, endpointUri)) {
|
||||
throw new IllegalArgumentException("Absolute endpoint URL does not match the base URL.");
|
||||
}
|
||||
else {
|
||||
return baseUrl.resolve(endpointUri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given absolute endpoint URI falls under the base URI. It validates
|
||||
* the scheme, authority (host and port), and ensures that the base path is a prefix
|
||||
* of the endpoint path.
|
||||
* @param baseUri The base URI
|
||||
* @param endpointUri The endpoint URI to check
|
||||
* @return true if endpointUri is within baseUri's hierarchy, false otherwise
|
||||
*/
|
||||
private static boolean isUnderBaseUri(URI baseUri, URI endpointUri) {
|
||||
if (!baseUri.getScheme().equals(endpointUri.getScheme())
|
||||
|| !baseUri.getAuthority().equals(endpointUri.getAuthority())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
URI normalizedBase = baseUri.normalize();
|
||||
URI normalizedEndpoint = endpointUri.normalize();
|
||||
|
||||
String basePath = normalizedBase.getPath();
|
||||
String endpointPath = normalizedEndpoint.getPath();
|
||||
|
||||
if (basePath.endsWith("/")) {
|
||||
basePath = basePath.substring(0, basePath.length() - 1);
|
||||
}
|
||||
return endpointPath.startsWith(basePath);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue