test: enhance resource reading tests (#314)
- Implement comprehensive resource content validation for text/plain and application/octet-stream MIME types - Validate resource count expectations (10 resources) - Add elicitation capability testing with ElicitRequest/ElicitResult support - Clean up unused imports (ObjectMapper, ImageFromDockerfile) Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
aacc631c96
commit
b701a366c0
6 changed files with 342 additions and 127 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -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<McpSchema.ReadResourceResult> 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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
|
||||
<T> void verifyCallSucceedsWithImplicitInitialization(Function<McpSyncClient, T> 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<McpSchema.ReadResourceResult> 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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue