Add sampling functionality tests to MCP client test suites (#255)
- Add testSampling() methods to AbstractMcpAsyncClientTests and AbstractMcpSyncClientTests Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com> Co-authored-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
8e9857f181
commit
f3b0774a24
5 changed files with 194 additions and 37 deletions
|
|
@ -1,13 +1,13 @@
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
|
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
|
||||||
import org.testcontainers.containers.GenericContainer;
|
import org.testcontainers.containers.GenericContainer;
|
||||||
import org.testcontainers.containers.wait.strategy.Wait;
|
import org.testcontainers.containers.wait.strategy.Wait;
|
||||||
|
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
@Timeout(15)
|
@Timeout(15)
|
||||||
public class WebClientStreamableHttpSyncClientTests extends AbstractMcpSyncClientTests {
|
public class WebClientStreamableHttpSyncClientTests extends AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
@ -40,6 +41,7 @@ import reactor.test.StepVerifier;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test suite for the {@link McpAsyncClient} that can be used with different
|
* Test suite for the {@link McpAsyncClient} that can be used with different
|
||||||
|
|
@ -193,22 +195,6 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSampling() {
|
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("sampleLLM",
|
|
||||||
Map.of("prompt", "Hello MCP Spring AI!"));
|
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest)))
|
|
||||||
.consumeNextWith(callToolResult -> {
|
|
||||||
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
|
||||||
assertThat(result.content()).isNotNull();
|
|
||||||
assertThat(result.isError()).isNull();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.verifyComplete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithInvalidTool() {
|
void testCallToolWithInvalidTool() {
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
|
@ -525,4 +511,52 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSampling() {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
final String message = "Hello, world!";
|
||||||
|
final String response = "Goodbye, world!";
|
||||||
|
final int maxTokens = 100;
|
||||||
|
|
||||||
|
AtomicReference<String> receivedPrompt = new AtomicReference<>();
|
||||||
|
AtomicReference<String> receivedMessage = new AtomicReference<>();
|
||||||
|
AtomicInteger receivedMaxTokens = new AtomicInteger();
|
||||||
|
|
||||||
|
withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build())
|
||||||
|
.sampling(request -> {
|
||||||
|
McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class,
|
||||||
|
request.messages().get(0).content());
|
||||||
|
receivedPrompt.set(request.systemPrompt());
|
||||||
|
receivedMessage.set(messageText.text());
|
||||||
|
receivedMaxTokens.set(request.maxTokens());
|
||||||
|
|
||||||
|
return Mono
|
||||||
|
.just(new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response),
|
||||||
|
"modelId", McpSchema.CreateMessageResult.StopReason.END_TURN));
|
||||||
|
}), client -> {
|
||||||
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
|
||||||
|
StepVerifier.create(client.callTool(
|
||||||
|
new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens))))
|
||||||
|
.consumeNextWith(result -> {
|
||||||
|
// Verify tool response to ensure our sampling response was passed
|
||||||
|
// through
|
||||||
|
assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class);
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
if (!(content instanceof McpSchema.TextContent text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
assertThat(text.text()).endsWith(response); // Prefixed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify sampling request parameters received in our callback
|
||||||
|
assertThat(receivedPrompt.get()).isNotEmpty();
|
||||||
|
assertThat(receivedMessage.get()).endsWith(message); // Prefixed
|
||||||
|
assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens);
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
@ -31,13 +32,12 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Scheduler;
|
|
||||||
import reactor.core.scheduler.Schedulers;
|
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for MCP Client Session functionality.
|
* Unit tests for MCP Client Session functionality.
|
||||||
|
|
@ -438,4 +438,48 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
.hasMessageContaining("Logging level must not be null"));
|
.hasMessageContaining("Logging level must not be null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSampling() {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
final String message = "Hello, world!";
|
||||||
|
final String response = "Goodbye, world!";
|
||||||
|
final int maxTokens = 100;
|
||||||
|
|
||||||
|
AtomicReference<String> receivedPrompt = new AtomicReference<>();
|
||||||
|
AtomicReference<String> receivedMessage = new AtomicReference<>();
|
||||||
|
AtomicInteger receivedMaxTokens = new AtomicInteger();
|
||||||
|
|
||||||
|
withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build())
|
||||||
|
.sampling(request -> {
|
||||||
|
McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class,
|
||||||
|
request.messages().get(0).content());
|
||||||
|
receivedPrompt.set(request.systemPrompt());
|
||||||
|
receivedMessage.set(messageText.text());
|
||||||
|
receivedMaxTokens.set(request.maxTokens());
|
||||||
|
|
||||||
|
return new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response),
|
||||||
|
"modelId", McpSchema.CreateMessageResult.StopReason.END_TURN);
|
||||||
|
}), client -> {
|
||||||
|
client.initialize();
|
||||||
|
|
||||||
|
McpSchema.CallToolResult result = client.callTool(
|
||||||
|
new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens)));
|
||||||
|
|
||||||
|
// Verify tool response to ensure our sampling response was passed through
|
||||||
|
assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class);
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
if (!(content instanceof McpSchema.TextContent text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
assertThat(text.text()).endsWith(response); // Prefixed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify sampling request parameters received in our callback
|
||||||
|
assertThat(receivedPrompt.get()).isNotEmpty();
|
||||||
|
assertThat(receivedMessage.get()).endsWith(message); // Prefixed
|
||||||
|
assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
@ -40,6 +41,7 @@ import reactor.test.StepVerifier;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test suite for the {@link McpAsyncClient} that can be used with different
|
* Test suite for the {@link McpAsyncClient} that can be used with different
|
||||||
|
|
@ -194,22 +196,6 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSampling() {
|
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("sampleLLM",
|
|
||||||
Map.of("prompt", "Hello MCP Spring AI!"));
|
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest)))
|
|
||||||
.consumeNextWith(callToolResult -> {
|
|
||||||
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
|
||||||
assertThat(result.content()).isNotNull();
|
|
||||||
assertThat(result.isError()).isNull();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.verifyComplete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithInvalidTool() {
|
void testCallToolWithInvalidTool() {
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
|
@ -486,7 +472,6 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Logging Tests
|
// Logging Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevelsWithoutInitialization() {
|
void testLoggingLevelsWithoutInitialization() {
|
||||||
verifyNotificationSucceedsWithImplicitInitialization(
|
verifyNotificationSucceedsWithImplicitInitialization(
|
||||||
|
|
@ -526,4 +511,52 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSampling() {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
final String message = "Hello, world!";
|
||||||
|
final String response = "Goodbye, world!";
|
||||||
|
final int maxTokens = 100;
|
||||||
|
|
||||||
|
AtomicReference<String> receivedPrompt = new AtomicReference<>();
|
||||||
|
AtomicReference<String> receivedMessage = new AtomicReference<>();
|
||||||
|
AtomicInteger receivedMaxTokens = new AtomicInteger();
|
||||||
|
|
||||||
|
withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build())
|
||||||
|
.sampling(request -> {
|
||||||
|
McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class,
|
||||||
|
request.messages().get(0).content());
|
||||||
|
receivedPrompt.set(request.systemPrompt());
|
||||||
|
receivedMessage.set(messageText.text());
|
||||||
|
receivedMaxTokens.set(request.maxTokens());
|
||||||
|
|
||||||
|
return Mono
|
||||||
|
.just(new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response),
|
||||||
|
"modelId", McpSchema.CreateMessageResult.StopReason.END_TURN));
|
||||||
|
}), client -> {
|
||||||
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
|
||||||
|
StepVerifier.create(client.callTool(
|
||||||
|
new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens))))
|
||||||
|
.consumeNextWith(result -> {
|
||||||
|
// Verify tool response to ensure our sampling response was passed
|
||||||
|
// through
|
||||||
|
assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class);
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
if (!(content instanceof McpSchema.TextContent text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
assertThat(text.text()).endsWith(response); // Prefixed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify sampling request parameters received in our callback
|
||||||
|
assertThat(receivedPrompt.get()).isNotEmpty();
|
||||||
|
assertThat(receivedMessage.get()).endsWith(message); // Prefixed
|
||||||
|
assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens);
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
@ -38,6 +39,7 @@ import reactor.test.StepVerifier;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for MCP Client Session functionality.
|
* Unit tests for MCP Client Session functionality.
|
||||||
|
|
@ -439,4 +441,48 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
.hasMessageContaining("Logging level must not be null"));
|
.hasMessageContaining("Logging level must not be null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSampling() {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
final String message = "Hello, world!";
|
||||||
|
final String response = "Goodbye, world!";
|
||||||
|
final int maxTokens = 100;
|
||||||
|
|
||||||
|
AtomicReference<String> receivedPrompt = new AtomicReference<>();
|
||||||
|
AtomicReference<String> receivedMessage = new AtomicReference<>();
|
||||||
|
AtomicInteger receivedMaxTokens = new AtomicInteger();
|
||||||
|
|
||||||
|
withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build())
|
||||||
|
.sampling(request -> {
|
||||||
|
McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class,
|
||||||
|
request.messages().get(0).content());
|
||||||
|
receivedPrompt.set(request.systemPrompt());
|
||||||
|
receivedMessage.set(messageText.text());
|
||||||
|
receivedMaxTokens.set(request.maxTokens());
|
||||||
|
|
||||||
|
return new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response),
|
||||||
|
"modelId", McpSchema.CreateMessageResult.StopReason.END_TURN);
|
||||||
|
}), client -> {
|
||||||
|
client.initialize();
|
||||||
|
|
||||||
|
McpSchema.CallToolResult result = client.callTool(
|
||||||
|
new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens)));
|
||||||
|
|
||||||
|
// Verify tool response to ensure our sampling response was passed through
|
||||||
|
assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class);
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
if (!(content instanceof McpSchema.TextContent text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
assertThat(text.text()).endsWith(response); // Prefixed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify sampling request parameters received in our callback
|
||||||
|
assertThat(receivedPrompt.get()).isNotEmpty();
|
||||||
|
assertThat(receivedMessage.get()).endsWith(message); // Prefixed
|
||||||
|
assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue