diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpAsyncClientTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpAsyncClientTests.java index 4c80326..f824193 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpAsyncClientTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpAsyncClientTests.java @@ -1,13 +1,12 @@ package io.modelcontextprotocol.client; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; import io.modelcontextprotocol.spec.McpClientTransport; import org.junit.jupiter.api.Timeout; -import org.springframework.web.reactive.function.client.WebClient; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.images.builder.ImageFromDockerfile; + +import org.springframework.web.reactive.function.client.WebClient; @Timeout(15) public class WebClientStreamableHttpAsyncClientTests extends AbstractMcpAsyncClientTests { diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java index f0533cb..0edf4cd 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java @@ -6,13 +6,13 @@ package io.modelcontextprotocol.client; import java.time.Duration; -import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; -import io.modelcontextprotocol.spec.McpClientTransport; import org.junit.jupiter.api.Timeout; +import org.springframework.web.reactive.function.client.WebClient; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; -import org.springframework.web.reactive.function.client.WebClient; +import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; +import io.modelcontextprotocol.spec.McpClientTransport; /** * Tests for the {@link McpAsyncClient} with {@link WebFluxSseClientTransport}. diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index d08bf70..4c77e59 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -4,7 +4,14 @@ package io.modelcontextprotocol.client; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + import java.time.Duration; +import java.util.ArrayList; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -13,9 +20,16 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; @@ -24,28 +38,19 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; import io.modelcontextprotocol.spec.McpSchema.ElicitResult; import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; import io.modelcontextprotocol.spec.McpSchema.Prompt; +import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.Resource; +import io.modelcontextprotocol.spec.McpSchema.ResourceContents; import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; +import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; import io.modelcontextprotocol.spec.McpTransport; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - /** * Test suite for the {@link McpAsyncClient} that can be used with different * {@link McpTransport} implementations. @@ -406,18 +411,59 @@ public abstract class AbstractMcpAsyncClientTests { } @Test - @Disabled void testReadResource() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> { - if (!resources.resources().isEmpty()) { - Resource firstResource = resources.resources().get(0); - StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.contents()).isNotNull(); - }).verifyComplete(); + withClient(createMcpTransport(), client -> { + Flux resources = client.initialize() + .then(client.listResources(null)) + .flatMapMany(r -> Flux.fromIterable(r.resources())) + .flatMap(r -> client.readResource(r)); + + StepVerifier.create(resources).recordWith(ArrayList::new).consumeRecordedWith(readResourceResults -> { + + for (ReadResourceResult result : readResourceResults) { + + assertThat(result).isNotNull(); + assertThat(result.contents()).isNotNull().isNotEmpty(); + + // Validate each content item + for (ResourceContents content : result.contents()) { + assertThat(content).isNotNull(); + assertThat(content.uri()).isNotNull().isNotEmpty(); + assertThat(content.mimeType()).isNotNull().isNotEmpty(); + + // Validate content based on its type with more comprehensive + // checks + switch (content.mimeType()) { + case "text/plain" -> { + TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, + content); + assertThat(textContent.text()).isNotNull().isNotEmpty(); + assertThat(textContent.uri()).isNotEmpty(); + } + case "application/octet-stream" -> { + BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, + content); + assertThat(blobContent.blob()).isNotNull().isNotEmpty(); + assertThat(blobContent.uri()).isNotNull().isNotEmpty(); + // Validate base64 encoding format + assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$"); + } + default -> { + + // Still validate basic properties + if (content instanceof TextResourceContents textContent) { + assertThat(textContent.text()).isNotNull(); + } + else if (content instanceof BlobResourceContents blobContent) { + assertThat(blobContent.blob()).isNotNull(); + } + } + } + } } - }).verifyComplete(); + }) + .expectNextCount(10) // Expect 10 elements + .verifyComplete(); }); } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index d949296..80c1525 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -4,6 +4,12 @@ package io.modelcontextprotocol.client; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + import java.time.Duration; import java.util.List; import java.util.Map; @@ -13,8 +19,17 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; @@ -23,25 +38,17 @@ import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult; import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.Resource; +import io.modelcontextprotocol.spec.McpSchema.ResourceContents; import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; import io.modelcontextprotocol.spec.McpSchema.TextContent; +import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - /** * Unit tests for MCP Client Session functionality. * @@ -50,6 +57,8 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; */ public abstract class AbstractMcpSyncClientTests { + private static final Logger logger = LoggerFactory.getLogger(AbstractMcpSyncClientTests.class); + private static final String TEST_MESSAGE = "Hello MCP Spring AI!"; abstract protected McpClientTransport createMcpTransport(); @@ -124,9 +133,9 @@ public abstract class AbstractMcpSyncClientTests { void verifyCallSucceedsWithImplicitInitialization(Function blockingOperation, String action) { withClient(createMcpTransport(), mcpSyncClient -> { - StepVerifier.create(Mono.fromSupplier(() -> blockingOperation.apply(mcpSyncClient))) - .expectNextCount(1) - .verifyComplete(); + StepVerifier.create(Mono.fromSupplier(() -> blockingOperation.apply(mcpSyncClient)) + // Offload the blocking call to the real scheduler + .subscribeOn(Schedulers.boundedElastic())).expectNextCount(1).verifyComplete(); }); } @@ -186,6 +195,49 @@ public abstract class AbstractMcpSyncClientTests { }); } + @Test + void testPingWithoutInitialization() { + verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server"); + } + + @Test + void testPing() { + withClient(createMcpTransport(), mcpSyncClient -> { + mcpSyncClient.initialize(); + assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException(); + }); + } + + @Test + void testCallToolWithoutInitialization() { + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); + verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools"); + } + + @Test + void testCallTool() { + withClient(createMcpTransport(), mcpSyncClient -> { + mcpSyncClient.initialize(); + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); + + CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest); + + assertThat(callToolResult).isNotNull().satisfies(result -> { + assertThat(result.content()).isNotNull(); + assertThat(result.isError()).isNull(); + }); + }); + } + + @Test + void testCallToolWithInvalidTool() { + withClient(createMcpTransport(), mcpSyncClient -> { + CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE)); + + assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class); + }); + } + @ParameterizedTest @ValueSource(strings = { "success", "error", "debug" }) void testCallToolWithMessageAnnotations(String messageType) { @@ -240,49 +292,6 @@ public abstract class AbstractMcpSyncClientTests { }); } - @Test - void testPingWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server"); - } - - @Test - void testPing() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException(); - }); - } - - @Test - void testCallToolWithoutInitialization() { - CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); - verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools"); - } - - @Test - void testCallTool() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); - - CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest); - - assertThat(callToolResult).isNotNull().satisfies(result -> { - assertThat(result.content()).isNotNull(); - assertThat(result.isError()).isNull(); - }); - }); - } - - @Test - void testCallToolWithInvalidTool() { - withClient(createMcpTransport(), mcpSyncClient -> { - CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE)); - - assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class); - }); - } - @Test void testRootsListChangedWithoutInitialization() { verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(), @@ -388,16 +397,70 @@ public abstract class AbstractMcpSyncClientTests { @Test void testReadResource() { withClient(createMcpTransport(), mcpSyncClient -> { + + int readResourceCount = 0; + mcpSyncClient.initialize(); ListResourcesResult resources = mcpSyncClient.listResources(null); - if (!resources.resources().isEmpty()) { - Resource firstResource = resources.resources().get(0); - ReadResourceResult result = mcpSyncClient.readResource(firstResource); + assertThat(resources).isNotNull(); + assertThat(resources.resources()).isNotNull(); + + assertThat(resources.resources()).isNotNull().isNotEmpty(); + + // Test reading each resource individually for better error isolation + for (Resource resource : resources.resources()) { + ReadResourceResult result = mcpSyncClient.readResource(resource); assertThat(result).isNotNull(); - assertThat(result.contents()).isNotNull(); + assertThat(result.contents()).isNotNull().isNotEmpty(); + + readResourceCount++; + + // Validate each content item + for (ResourceContents content : result.contents()) { + assertThat(content).isNotNull(); + assertThat(content.uri()).isNotNull().isNotEmpty(); + assertThat(content.mimeType()).isNotNull().isNotEmpty(); + + // Validate content based on its type with more comprehensive + // checks + switch (content.mimeType()) { + case "text/plain" -> { + TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, content); + assertThat(textContent.text()).isNotNull().isNotEmpty(); + // Verify URI consistency + assertThat(textContent.uri()).isEqualTo(resource.uri()); + } + case "application/octet-stream" -> { + BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, content); + assertThat(blobContent.blob()).isNotNull().isNotEmpty(); + // Verify URI consistency + assertThat(blobContent.uri()).isEqualTo(resource.uri()); + // Validate base64 encoding format + assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$"); + } + default -> { + // More flexible handling of additional MIME types + // Log the unexpected type for debugging but don't fail + // the test + logger.warn("Warning: Encountered unexpected MIME type: {} for resource: {}", + content.mimeType(), resource.uri()); + + // Still validate basic properties + if (content instanceof TextResourceContents textContent) { + assertThat(textContent.text()).isNotNull(); + } + else if (content instanceof BlobResourceContents blobContent) { + assertThat(blobContent.blob()).isNotNull(); + } + } + } + } } + + // Assert that we read exactly 10 resources + assertThat(readResourceCount).isEqualTo(10); }); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index af130be..8472ec3 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -4,7 +4,14 @@ package io.modelcontextprotocol.client; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + import java.time.Duration; +import java.util.ArrayList; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -13,9 +20,16 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; @@ -24,28 +38,19 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; import io.modelcontextprotocol.spec.McpSchema.ElicitResult; import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; import io.modelcontextprotocol.spec.McpSchema.Prompt; +import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.Resource; +import io.modelcontextprotocol.spec.McpSchema.ResourceContents; import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; +import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; import io.modelcontextprotocol.spec.McpTransport; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - /** * Test suite for the {@link McpAsyncClient} that can be used with different * {@link McpTransport} implementations. @@ -407,18 +412,59 @@ public abstract class AbstractMcpAsyncClientTests { } @Test - @Disabled void testReadResource() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> { - if (!resources.resources().isEmpty()) { - Resource firstResource = resources.resources().get(0); - StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.contents()).isNotNull(); - }).verifyComplete(); + withClient(createMcpTransport(), client -> { + Flux resources = client.initialize() + .then(client.listResources(null)) + .flatMapMany(r -> Flux.fromIterable(r.resources())) + .flatMap(r -> client.readResource(r)); + + StepVerifier.create(resources).recordWith(ArrayList::new).consumeRecordedWith(readResourceResults -> { + + for (ReadResourceResult result : readResourceResults) { + + assertThat(result).isNotNull(); + assertThat(result.contents()).isNotNull().isNotEmpty(); + + // Validate each content item + for (ResourceContents content : result.contents()) { + assertThat(content).isNotNull(); + assertThat(content.uri()).isNotNull().isNotEmpty(); + assertThat(content.mimeType()).isNotNull().isNotEmpty(); + + // Validate content based on its type with more comprehensive + // checks + switch (content.mimeType()) { + case "text/plain" -> { + TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, + content); + assertThat(textContent.text()).isNotNull().isNotEmpty(); + assertThat(textContent.uri()).isNotEmpty(); + } + case "application/octet-stream" -> { + BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, + content); + assertThat(blobContent.blob()).isNotNull().isNotEmpty(); + assertThat(blobContent.uri()).isNotNull().isNotEmpty(); + // Validate base64 encoding format + assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$"); + } + default -> { + + // Still validate basic properties + if (content instanceof TextResourceContents textContent) { + assertThat(textContent.text()).isNotNull(); + } + else if (content instanceof BlobResourceContents blobContent) { + assertThat(blobContent.blob()).isNotNull(); + } + } + } + } } - }).verifyComplete(); + }) + .expectNextCount(10) // Expect 10 elements + .verifyComplete(); }); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index 10ecade..1385149 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -4,6 +4,12 @@ package io.modelcontextprotocol.client; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + import java.time.Duration; import java.util.List; import java.util.Map; @@ -13,8 +19,17 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; @@ -23,27 +38,17 @@ import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult; import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.Resource; +import io.modelcontextprotocol.spec.McpSchema.ResourceContents; import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; import io.modelcontextprotocol.spec.McpSchema.TextContent; +import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - /** * Unit tests for MCP Client Session functionality. * @@ -53,6 +58,8 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; // KEEP IN SYNC with the class in mcp-test module public abstract class AbstractMcpSyncClientTests { + private static final Logger logger = LoggerFactory.getLogger(AbstractMcpSyncClientTests.class); + private static final String TEST_MESSAGE = "Hello MCP Spring AI!"; abstract protected McpClientTransport createMcpTransport(); @@ -391,16 +398,70 @@ public abstract class AbstractMcpSyncClientTests { @Test void testReadResource() { withClient(createMcpTransport(), mcpSyncClient -> { + + int readResourceCount = 0; + mcpSyncClient.initialize(); ListResourcesResult resources = mcpSyncClient.listResources(null); - if (!resources.resources().isEmpty()) { - Resource firstResource = resources.resources().get(0); - ReadResourceResult result = mcpSyncClient.readResource(firstResource); + assertThat(resources).isNotNull(); + assertThat(resources.resources()).isNotNull(); + + assertThat(resources.resources()).isNotNull().isNotEmpty(); + + // Test reading each resource individually for better error isolation + for (Resource resource : resources.resources()) { + ReadResourceResult result = mcpSyncClient.readResource(resource); assertThat(result).isNotNull(); - assertThat(result.contents()).isNotNull(); + assertThat(result.contents()).isNotNull().isNotEmpty(); + + readResourceCount++; + + // Validate each content item + for (ResourceContents content : result.contents()) { + assertThat(content).isNotNull(); + assertThat(content.uri()).isNotNull().isNotEmpty(); + assertThat(content.mimeType()).isNotNull().isNotEmpty(); + + // Validate content based on its type with more comprehensive + // checks + switch (content.mimeType()) { + case "text/plain" -> { + TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, content); + assertThat(textContent.text()).isNotNull().isNotEmpty(); + // Verify URI consistency + assertThat(textContent.uri()).isEqualTo(resource.uri()); + } + case "application/octet-stream" -> { + BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, content); + assertThat(blobContent.blob()).isNotNull().isNotEmpty(); + // Verify URI consistency + assertThat(blobContent.uri()).isEqualTo(resource.uri()); + // Validate base64 encoding format + assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$"); + } + default -> { + // More flexible handling of additional MIME types + // Log the unexpected type for debugging but don't fail + // the test + logger.warn("Warning: Encountered unexpected MIME type: {} for resource: {}", + content.mimeType(), resource.uri()); + + // Still validate basic properties + if (content instanceof TextResourceContents textContent) { + assertThat(textContent.text()).isNotNull(); + } + else if (content instanceof BlobResourceContents blobContent) { + assertThat(blobContent.blob()).isNotNull(); + } + } + } + } } + + // Assert that we read exactly 10 resources + assertThat(readResourceCount).isEqualTo(10); }); }