diff --git a/mcp/src/main/java/spring/ai/experimental/mcp/client/McpClient.java b/mcp/src/main/java/spring/ai/experimental/mcp/client/McpClient.java
index 28ce6ce..3bf20a4 100644
--- a/mcp/src/main/java/spring/ai/experimental/mcp/client/McpClient.java
+++ b/mcp/src/main/java/spring/ai/experimental/mcp/client/McpClient.java
@@ -1,18 +1,18 @@
/*
-* Copyright 2024 - 2024 the original author or authors.
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* https://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
+ * Copyright 2024 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package spring.ai.experimental.mcp.client;
import java.time.Duration;
@@ -21,41 +21,85 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import spring.ai.experimental.mcp.spec.McpTransport;
/**
- * The MCP client is the main entry point for interacting with the Model Context Protocol
- * (MCP) server.
+ * Factory class providing static methods for creating Model Context Protocol (MCP) clients.
+ * This class serves as the main entry point for establishing connections with MCP servers,
+ * offering both synchronous and asynchronous client implementations.
+ *
+ * The class provides factory methods to create either:
+ *
+ * - {@link McpAsyncClient} for non-blocking operations
+ *
- {@link McpSyncClient} for blocking operations
+ *
+ *
+ * Each client type can be instantiated with default settings or with custom configuration
+ * including request timeout and JSON object mapping.
+ *
+ *
Future implementations will introduce a builder pattern for more flexible client configuration.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
public class McpClient {
- private McpClient() {
- }
+ /**
+ * Private constructor to prevent instantiation as this is a utility class
+ * containing only static factory methods.
+ */
+ private McpClient() {
+ }
- // TODO: introduce a builder like:
- // McpClient.using(transport)
- // .withRequestTimeout(Duration.ofSeconds(5))
- // .withObjectMapper(objectMapper); <- or even a more sophisticated
- // JSONtoPOJOCodec type
- // .sync();
+ // TODO: introduce a builder like:
+ // McpClient.using(transport)
+ // .withRequestTimeout(Duration.ofSeconds(5))
+ // .withObjectMapper(objectMapper); <- or even a more sophisticated
+ // JSONtoPOJOCodec type
+ // .sync();
- public static McpAsyncClient async(McpTransport transport) {
- return new McpAsyncClient(transport);
- }
+ /**
+ * Creates an asynchronous MCP client with default configuration.
+ *
+ * @param transport The transport layer implementation for MCP communication
+ * @return A new instance of {@link McpAsyncClient}
+ */
+ public static McpAsyncClient async(McpTransport transport) {
+ return new McpAsyncClient(transport);
+ }
- public static McpAsyncClient async(McpTransport transport, Duration requestTimeout,
- ObjectMapper objectMapper) {
- return new McpAsyncClient(transport, requestTimeout, objectMapper);
- }
-
- public static McpSyncClient sync(McpTransport transport) {
- return new McpSyncClient(async(transport));
- }
-
- public static McpSyncClient sync(McpTransport transport, Duration requestTimeout,
- ObjectMapper objectMapper) {
- return new McpSyncClient(async(transport, requestTimeout, objectMapper));
- }
+ /**
+ * Creates an asynchronous MCP client with custom configuration.
+ *
+ * @param transport The transport layer implementation for MCP communication
+ * @param requestTimeout The duration to wait before timing out requests
+ * @param objectMapper The Jackson object mapper for JSON serialization/deserialization
+ * @return A new instance of {@link McpAsyncClient}
+ */
+ public static McpAsyncClient async(McpTransport transport, Duration requestTimeout,
+ ObjectMapper objectMapper) {
+ return new McpAsyncClient(transport, requestTimeout, objectMapper);
+ }
+ /**
+ * Creates a synchronous MCP client with default configuration.
+ * This method wraps an asynchronous client to provide synchronous operations.
+ *
+ * @param transport The transport layer implementation for MCP communication
+ * @return A new instance of {@link McpSyncClient}
+ */
+ public static McpSyncClient sync(McpTransport transport) {
+ return new McpSyncClient(async(transport));
+ }
+ /**
+ * Creates a synchronous MCP client with custom configuration.
+ * This method wraps an asynchronous client to provide synchronous operations.
+ *
+ * @param transport The transport layer implementation for MCP communication
+ * @param requestTimeout The duration to wait before timing out requests
+ * @param objectMapper The Jackson object mapper for JSON serialization/deserialization
+ * @return A new instance of {@link McpSyncClient}
+ */
+ public static McpSyncClient sync(McpTransport transport, Duration requestTimeout,
+ ObjectMapper objectMapper) {
+ return new McpSyncClient(async(transport, requestTimeout, objectMapper));
+ }
}
diff --git a/mcp/src/main/java/spring/ai/experimental/mcp/client/stdio/StdioServerTransport.java b/mcp/src/main/java/spring/ai/experimental/mcp/client/stdio/StdioServerTransport.java
index ee05901..0ed8607 100644
--- a/mcp/src/main/java/spring/ai/experimental/mcp/client/stdio/StdioServerTransport.java
+++ b/mcp/src/main/java/spring/ai/experimental/mcp/client/stdio/StdioServerTransport.java
@@ -10,10 +10,11 @@ import java.util.concurrent.Executors;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
+import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import spring.ai.experimental.mcp.client.util.Assert;
-import spring.ai.experimental.mcp.spec.DefaultMcpTransport;
+import spring.ai.experimental.mcp.spec.AbstractMcpTransport;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCMessage;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCNotification;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCRequest;
@@ -25,7 +26,7 @@ import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCResponse;
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
-public class StdioServerTransport extends DefaultMcpTransport {
+public class StdioServerTransport extends AbstractMcpTransport {
private final static TypeReference> MAP_TYPE_REF = new TypeReference>() {
};
@@ -194,47 +195,46 @@ public class StdioServerTransport extends DefaultMcpTransport {
.subscribe();
}
- // TODO: provide a non-blocking variant with graceful option
- public void stop() {
- this.outboundScheduler.dispose();
- this.inboundScheduler.dispose();
- this.errorScheduler.dispose();
-
- // Destroy process
- if (this.process != null) {
- this.process.destroyForcibly();
- }
- }
-
@Override
- public void close() {
+ public Mono closeGracefully() {
- this.stop();
+ this.isRunning = false;
- super.close(); // Do we need this?
-
- // Close resources
- if (this.processReader != null) {
- try {
- this.processReader.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (this.processWriter != null) {
- try {
- this.processWriter.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (this.processErrorReader != null) {
- try {
- this.processErrorReader.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
+ return Mono.whenDelayError(
+ Mono.fromRunnable(() -> {
+ try {
+ this.processErrorReader.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).subscribeOn(errorScheduler),
+ Mono.fromRunnable(() -> {
+ try {
+ this.processReader.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).subscribeOn(inboundScheduler),
+ Mono.fromRunnable(() -> {
+ try {
+ this.processWriter.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).subscribeOn(outboundScheduler))
+ .then(Mono.whenDelayError(inboundScheduler.disposeGracefully(),
+ outboundScheduler.disposeGracefully(),
+ errorScheduler.disposeGracefully()))
+ .then(Mono.fromRunnable(() -> {
+ if (this.process != null) {
+ var process = this.process.destroyForcibly();
+ if (process.exitValue() != 0) {
+ throw new RuntimeException("Failed to destroy process");
+ }
+ }
+ }))
+ .then()
+ .subscribeOn(Schedulers.boundedElastic());
}
}
\ No newline at end of file
diff --git a/mcp/src/main/java/spring/ai/experimental/mcp/spec/DefaultMcpTransport.java b/mcp/src/main/java/spring/ai/experimental/mcp/spec/AbstractMcpTransport.java
similarity index 93%
rename from mcp/src/main/java/spring/ai/experimental/mcp/spec/DefaultMcpTransport.java
rename to mcp/src/main/java/spring/ai/experimental/mcp/spec/AbstractMcpTransport.java
index ac93903..db99e26 100644
--- a/mcp/src/main/java/spring/ai/experimental/mcp/spec/DefaultMcpTransport.java
+++ b/mcp/src/main/java/spring/ai/experimental/mcp/spec/AbstractMcpTransport.java
@@ -27,7 +27,7 @@ import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCMessage;
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
-public class DefaultMcpTransport implements McpTransport {
+public abstract class AbstractMcpTransport implements McpTransport {
protected final ObjectMapper objectMapper;
@@ -39,11 +39,11 @@ public class DefaultMcpTransport implements McpTransport {
.println("Received message: " + message);
private Consumer errorHandler = error -> System.err.println("Received error: " + error);
- public DefaultMcpTransport() {
+ public AbstractMcpTransport() {
this(new ObjectMapper());
}
- public DefaultMcpTransport(ObjectMapper objectMapper) {
+ public AbstractMcpTransport(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
@@ -73,7 +73,7 @@ public class DefaultMcpTransport implements McpTransport {
private void handleIncomingErrors() {
this.errorSink.asFlux().subscribe(e -> {
this.errorHandler.accept(e);
- System.err.println(e);
+ // System.err.println(e); TODO: log the error
});
}
@@ -103,12 +103,6 @@ public class DefaultMcpTransport implements McpTransport {
this.handleIncomingMessages();
}
- // Close the transport
- @Override
- public void close() {
- // this.onClose();
- }
-
@Override
public Mono sendMessage(JSONRPCMessage message) {
if (this.outboundSink.tryEmitNext(message).isSuccess()) {
diff --git a/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpSession.java b/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpSession.java
index b0faeab..894a0ee 100644
--- a/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpSession.java
+++ b/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpSession.java
@@ -111,7 +111,6 @@ public class McpSession {
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
method, params);
try {
- // TODO: make it non-blocking
this.transport.sendMessage(jsonrpcNotification);
} catch (Exception e) {
return Mono.error(new McpError(e));
diff --git a/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpTransport.java b/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpTransport.java
index ea06c3f..8751fff 100644
--- a/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpTransport.java
+++ b/mcp/src/main/java/spring/ai/experimental/mcp/spec/McpTransport.java
@@ -12,67 +12,104 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
- */
+ */
package spring.ai.experimental.mcp.spec;
+import java.time.Duration;
import java.util.function.Consumer;
import reactor.core.publisher.Mono;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCMessage;
/**
- * Defines the asynchronous transport layer for the Model Context Protocol (MCP).
+ * Defines the asynchronous transport layer for the Model Context Protocol
+ * (MCP).
*
- * The McpTransport interface provides the foundation for implementing custom transport
- * mechanisms in the Model Context Protocol. It handles the bidirectional communication between
- * the client and server components, supporting asynchronous message exchange using JSON-RPC format.
+ *
+ * The McpTransport interface provides the foundation for implementing custom
+ * transport
+ * mechanisms in the Model Context Protocol. It handles the bidirectional
+ * communication between
+ * the client and server components, supporting asynchronous message exchange
+ * using JSON-RPC format.
+ *
*
- * Implementations of this interface are responsible for:
+ *
+ * Implementations of this interface are responsible for:
+ *
*
- * - Managing the lifecycle of the transport connection
- * - Handling incoming messages and errors from the server
- * - Sending outbound messages to the server
+ * - Managing the lifecycle of the transport connection
+ * - Handling incoming messages and errors from the server
+ * - Sending outbound messages to the server
*
*
- * The transport layer is designed to be protocol-agnostic, allowing for various implementations
- * such as WebSocket, HTTP, or custom protocols.
+ *
+ * The transport layer is designed to be protocol-agnostic, allowing for various
+ * implementations
+ * such as WebSocket, HTTP, or custom protocols.
+ *
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
-public interface McpTransport extends AutoCloseable {
-
+public interface McpTransport {
+
/**
* Initializes and starts the transport connection.
*
- * This method should be called before any message exchange can occur. It sets up
- * the necessary resources and establishes the connection to the server.
+ *
+ * This method should be called before any message exchange can occur. It sets
+ * up
+ * the necessary resources and establishes the connection to the server.
+ *
*/
void start();
/**
* Closes the transport connection and releases any associated resources.
*
- * This method ensures proper cleanup of resources when the transport is no longer needed.
- * It should handle the graceful shutdown of any active connections.
+ *
+ * This method ensures proper cleanup of resources when the transport is no
+ * longer needed.
+ * It should handle the graceful shutdown of any active connections.
+ *
*/
- void close();
+ default void close() {
+ this.closeGracefully().subscribe();
+ }
+
+ /**
+ * Closes the transport connection and releases any associated resources
+ * asynchronously.
+ *
+ * @return a {@link Mono} that completes when the connection has been
+ * closed.
+ */
+ Mono closeGracefully();
/**
* Sets the handler for processing incoming messages from the server.
*
- * The provided handler will be called whenever a new message is received from the server.
- * Messages are expected to follow the JSON-RPC format as defined in the MCP specification.
+ *
+ * The provided handler will be called whenever a new message is received from
+ * the server.
+ * Messages are expected to follow the JSON-RPC format as defined in the MCP
+ * specification.
+ *
*
- * @param inboundMessageHandler a consumer that processes incoming {@link JSONRPCMessage}s
+ * @param inboundMessageHandler a consumer that processes incoming
+ * {@link JSONRPCMessage}s
*/
void setInboudMessageHandler(Consumer inboundMessageHandler);
/**
* Sets the handler for processing transport-level errors.
*
- * The provided handler will be called when errors occur during transport operations,
- * such as connection failures or protocol violations.
+ *
+ * The provided handler will be called when errors occur during transport
+ * operations,
+ * such as connection failures or protocol violations.
+ *
*
* @param inboundErrorHandler a consumer that processes error messages
*/
@@ -81,11 +118,14 @@ public interface McpTransport extends AutoCloseable {
/**
* Sends a message to the server asynchronously.
*
- * This method handles the transmission of messages to the server in an asynchronous manner.
- * Messages are sent in JSON-RPC format as specified by the MCP protocol.
+ *
+ * This method handles the transmission of messages to the server in an
+ * asynchronous manner.
+ * Messages are sent in JSON-RPC format as specified by the MCP protocol.
+ *
*
* @param message the {@link JSONRPCMessage} to be sent to the server
* @return a {@link Mono} that completes when the message has been sent
*/
- Mono sendMessage(JSONRPCMessage message);
+ Mono sendMessage(JSONRPCMessage message);
}
diff --git a/mcp/src/test/java/spring/ai/experimental/mcp/spec/DefaultMcpTransportTests.java b/mcp/src/test/java/spring/ai/experimental/mcp/spec/DefaultMcpTransportTests.java
index 182edd7..0a2abac 100644
--- a/mcp/src/test/java/spring/ai/experimental/mcp/spec/DefaultMcpTransportTests.java
+++ b/mcp/src/test/java/spring/ai/experimental/mcp/spec/DefaultMcpTransportTests.java
@@ -15,148 +15,162 @@
*/
package spring.ai.experimental.mcp.spec;
+import java.util.concurrent.atomic.AtomicReference;
+
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
-import spring.ai.experimental.mcp.spec.DefaultMcpTransport;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCMessage;
import spring.ai.experimental.mcp.spec.McpSchema.JSONRPCRequest;
-import java.util.concurrent.atomic.AtomicReference;
-
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class DefaultMcpTransportTests {
- private DefaultMcpTransport transport;
- private ObjectMapper objectMapper;
+ private AbstractMcpTransport transport;
+ private ObjectMapper objectMapper;
- @BeforeEach
- void setUp() {
- objectMapper = new ObjectMapper();
- transport = new DefaultMcpTransport(objectMapper);
- }
+ @BeforeEach
+ void setUp() {
+ objectMapper = new ObjectMapper();
+ transport = new AbstractMcpTransport(objectMapper) {
+ @Override
+ public Mono closeGracefully() {
+ return null;
+ }
+ };
+ }
- @Test
- void constructorShouldValidateObjectMapper() {
- assertThatThrownBy(() -> new DefaultMcpTransport(null))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("ObjectMapper must not be null");
- }
+ @Test
+ void constructorShouldValidateObjectMapper() {
+ assertThatThrownBy(() -> new AbstractMcpTransport(null) {
+ @Override
+ public Mono closeGracefully() {
+ return null;
+ }
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("ObjectMapper must not be null");
+ }
- @Test
- void defaultConstructorShouldCreateValidInstance() {
- DefaultMcpTransport defaultTransport = new DefaultMcpTransport();
- assertThat(defaultTransport.getObjectMapper()).isNotNull();
- }
+ @Test
+ void defaultConstructorShouldCreateValidInstance() {
+ AbstractMcpTransport defaultTransport = new AbstractMcpTransport() {
+ @Override
+ public Mono closeGracefully() {
+ return null;
+ }
+ };
+ assertThat(defaultTransport.getObjectMapper()).isNotNull();
+ }
- @Test
- void customObjectMapperConstructorShouldCreateValidInstance() {
- assertThat(transport.getObjectMapper()).isSameAs(objectMapper);
- }
+ @Test
+ void customObjectMapperConstructorShouldCreateValidInstance() {
+ assertThat(transport.getObjectMapper()).isSameAs(objectMapper);
+ }
- @Test
- void sendMessageShouldEmitToOutboundSink() {
- JSONRPCRequest message = new JSONRPCRequest(
- "2.0",
- "test",
- 1,
- null);
+ @Test
+ void sendMessageShouldEmitToOutboundSink() {
+ JSONRPCRequest message = new JSONRPCRequest(
+ "2.0",
+ "test",
+ 1,
+ null);
- AtomicReference receivedMessage = new AtomicReference<>();
- transport.getOutboundSink().asFlux().subscribe(receivedMessage::set);
+ AtomicReference receivedMessage = new AtomicReference<>();
+ transport.getOutboundSink().asFlux().subscribe(receivedMessage::set);
- Mono result = transport.sendMessage(message);
+ Mono result = transport.sendMessage(message);
- StepVerifier.create(result)
- .verifyComplete();
+ StepVerifier.create(result)
+ .verifyComplete();
- assertThat(receivedMessage.get())
- .isNotNull()
- .satisfies(msg -> {
- assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0");
- assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test");
- });
- }
+ assertThat(receivedMessage.get())
+ .isNotNull()
+ .satisfies(msg -> {
+ assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0");
+ assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test");
+ });
+ }
- @Test
- void customInboundMessageHandlerShouldReceiveMessages() {
- AtomicReference receivedMessage = new AtomicReference<>();
- transport.setInboudMessageHandler(receivedMessage::set);
- transport.start();
+ @Test
+ void customInboundMessageHandlerShouldReceiveMessages() {
+ AtomicReference receivedMessage = new AtomicReference<>();
+ transport.setInboudMessageHandler(receivedMessage::set);
+ transport.start();
- JSONRPCRequest message = new JSONRPCRequest(
- "2.0",
- "test",
- 1,
- null);
+ JSONRPCRequest message = new JSONRPCRequest(
+ "2.0",
+ "test",
+ 1,
+ null);
- transport.getInboundSink().tryEmitNext(message);
+ transport.getInboundSink().tryEmitNext(message);
- assertThat(receivedMessage.get())
- .isNotNull()
- .satisfies(msg -> {
- assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0");
- assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test");
- });
- }
+ assertThat(receivedMessage.get())
+ .isNotNull()
+ .satisfies(msg -> {
+ assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0");
+ assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test");
+ });
+ }
- @Test
- void customErrorHandlerShouldReceiveErrors() {
- AtomicReference receivedError = new AtomicReference<>();
- transport.setInboundErrorHandler(receivedError::set);
- transport.start();
+ @Test
+ void customErrorHandlerShouldReceiveErrors() {
+ AtomicReference receivedError = new AtomicReference<>();
+ transport.setInboundErrorHandler(receivedError::set);
+ transport.start();
- String errorMessage = "Test error";
- transport.getErrorSink().tryEmitNext(errorMessage);
+ String errorMessage = "Test error";
+ transport.getErrorSink().tryEmitNext(errorMessage);
- assertThat(receivedError.get())
- .isNotNull()
- .isEqualTo(errorMessage);
- }
+ assertThat(receivedError.get())
+ .isNotNull()
+ .isEqualTo(errorMessage);
+ }
- @Test
- void startShouldInitializeMessageAndErrorHandling() {
- AtomicReference receivedMessage = new AtomicReference<>();
- AtomicReference receivedError = new AtomicReference<>();
+ @Test
+ void startShouldInitializeMessageAndErrorHandling() {
+ AtomicReference receivedMessage = new AtomicReference<>();
+ AtomicReference receivedError = new AtomicReference<>();
- transport.setInboudMessageHandler(receivedMessage::set);
- transport.setInboundErrorHandler(receivedError::set);
- transport.start();
+ transport.setInboudMessageHandler(receivedMessage::set);
+ transport.setInboundErrorHandler(receivedError::set);
+ transport.start();
- JSONRPCRequest message = new JSONRPCRequest(
- "2.0",
- "test",
- 1,
- null);
+ JSONRPCRequest message = new JSONRPCRequest(
+ "2.0",
+ "test",
+ 1,
+ null);
- String errorMessage = "Test error";
+ String errorMessage = "Test error";
- transport.getInboundSink().tryEmitNext(message);
- transport.getErrorSink().tryEmitNext(errorMessage);
+ transport.getInboundSink().tryEmitNext(message);
+ transport.getErrorSink().tryEmitNext(errorMessage);
- assertThat(receivedMessage.get()).isNotNull();
- assertThat(receivedError.get()).isEqualTo(errorMessage);
- }
+ assertThat(receivedMessage.get()).isNotNull();
+ assertThat(receivedError.get()).isEqualTo(errorMessage);
+ }
- @Test
- void sendMessageShouldReturnErrorWhenSinkFails() {
- JSONRPCRequest message = new JSONRPCRequest(
- "2.0",
- "test",
- 1,
- null);
- // Close the sink to simulate failure
- transport.getOutboundSink().tryEmitComplete();
+ @Test
+ void sendMessageShouldReturnErrorWhenSinkFails() {
+ JSONRPCRequest message = new JSONRPCRequest(
+ "2.0",
+ "test",
+ 1,
+ null);
+ // Close the sink to simulate failure
+ transport.getOutboundSink().tryEmitComplete();
- Mono result = transport.sendMessage(message);
+ Mono result = transport.sendMessage(message);
- StepVerifier.create(result)
- .verifyErrorSatisfies(error -> assertThat(error)
- .isInstanceOf(RuntimeException.class)
- .hasMessage("Failed to enqueue message"));
- }
+ StepVerifier.create(result)
+ .verifyErrorSatisfies(error -> assertThat(error)
+ .isInstanceOf(RuntimeException.class)
+ .hasMessage("Failed to enqueue message"));
+ }
}