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
|
|
@ -7,12 +7,13 @@ package io.modelcontextprotocol.client.transport;
|
|||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
|
|
@ -21,6 +22,8 @@ import org.junit.jupiter.api.AfterEach;
|
|||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
|
@ -31,6 +34,9 @@ import org.springframework.http.codec.ServerSentEvent;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
|
|
@ -364,4 +370,25 @@ class HttpClientSseClientTransportTests {
|
|||
customizedTransport.closeGracefully().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testResolvingClientEndpoint() {
|
||||
HttpClient httpClient = Mockito.mock(HttpClient.class);
|
||||
HttpResponse<Void> httpResponse = Mockito.mock(HttpResponse.class);
|
||||
CompletableFuture<HttpResponse<Void>> future = new CompletableFuture<>();
|
||||
future.complete(httpResponse);
|
||||
when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
|
||||
|
||||
HttpClientSseClientTransport transport = new HttpClientSseClientTransport(httpClient, HttpRequest.newBuilder(),
|
||||
"http://example.com", "http://example.com/sse", new ObjectMapper());
|
||||
|
||||
transport.connect(Function.identity());
|
||||
|
||||
ArgumentCaptor<HttpRequest> httpRequestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
verify(httpClient).sendAsync(httpRequestCaptor.capture(), any(HttpResponse.BodyHandler.class));
|
||||
assertThat(httpRequestCaptor.getValue().uri()).isEqualTo(URI.create("http://example.com/sse"));
|
||||
|
||||
transport.closeGracefully().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,17 @@ package io.modelcontextprotocol.util;
|
|||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
class UtilsTests {
|
||||
|
||||
|
|
@ -37,4 +42,28 @@ class UtilsTests {
|
|||
assertFalse(Utils.isEmpty(Map.of("key", "value")));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
// relative endpoints
|
||||
"http://localhost:8080/root, /api/v1, http://localhost:8080/api/v1",
|
||||
"http://localhost:8080/root/, api, http://localhost:8080/root/api",
|
||||
"http://localhost:8080, /api, http://localhost:8080/api",
|
||||
// absolute endpoints matching base
|
||||
"http://localhost:8080/root, http://localhost:8080/root/api/v1, http://localhost:8080/root/api/v1",
|
||||
"http://localhost:8080/root, http://localhost:8080/root, http://localhost:8080/root" })
|
||||
void testValidUriResolution(String baseUrl, String endpoint, String expectedResult) {
|
||||
URI result = Utils.resolveUri(URI.create(baseUrl), endpoint);
|
||||
assertThat(result.toString()).isEqualTo(expectedResult);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({ "http://localhost:8080/root, http://localhost:8080/other/api",
|
||||
"http://localhost:8080/root, http://otherhost/api",
|
||||
"http://localhost:8080/root, http://localhost:9090/root/api" })
|
||||
void testAbsoluteUriNotMatchingBase(String baseUrl, String endpoint) {
|
||||
assertThatThrownBy(() -> Utils.resolveUri(URI.create(baseUrl), endpoint))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("does not match the base URL");
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue