From 1e7095f0a9d18259faf94055c9bba0e55f49edd9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:17:43 +0000 Subject: [PATCH 1/5] feat: Add in-memory transport for MCP This commit introduces a new Maven module, `mcp-inmemory-transport`, which provides an in-memory transport implementation for the Model Context Protocol (MCP). The primary goal of this new module is to facilitate testing of MCP clients and servers without requiring a full network setup. It uses Project Reactor's `Sinks` to simulate the bidirectional communication between a client and a server. The implementation includes: - `InMemoryTransport`: A container for the shared communication sinks. - `InMemoryClientTransport`: An implementation of `McpClientTransport`. - `InMemoryServerTransportProvider`: An implementation of `McpServerTransportProvider`. - `InMemoryTransportTest`: A test class that verifies the end-to-end communication using the new in-memory transport. --- mcp-inmemory-transport/pom.xml | 69 +++++++++++++++++++ .../inmemory/InMemoryClientTransport.java | 51 ++++++++++++++ .../inmemory/InMemoryServerTransport.java | 38 ++++++++++ .../InMemoryServerTransportProvider.java | 44 ++++++++++++ .../transport/inmemory/InMemoryTransport.java | 15 ++++ .../inmemory/InMemoryTransportTest.java | 44 ++++++++++++ pom.xml | 5 +- 7 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 mcp-inmemory-transport/pom.xml create mode 100644 mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java create mode 100644 mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java create mode 100644 mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java create mode 100644 mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java create mode 100644 mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java diff --git a/mcp-inmemory-transport/pom.xml b/mcp-inmemory-transport/pom.xml new file mode 100644 index 000000000..afbe5a2be --- /dev/null +++ b/mcp-inmemory-transport/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + io.modelcontextprotocol.sdk + mcp-parent + 0.12.0-SNAPSHOT + ../pom.xml + + + mcp-inmemory-transport + Java SDK MCP In-Memory Transport + In-memory transport implementation for the Model Context Protocol (MCP) + + + + io.modelcontextprotocol.sdk + mcp + ${project.version} + + + org.springframework.boot + spring-boot-starter-logging + 3.4.0-SNAPSHOT + provided + + + io.projectreactor + reactor-core + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.assertj + assertj-core + ${assert4j.version} + test + + + io.projectreactor + reactor-test + test + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java new file mode 100644 index 000000000..1a69a16b5 --- /dev/null +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java @@ -0,0 +1,51 @@ +package io.modelcontextprotocol.transport.inmemory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpClientTransport; +import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +import java.util.function.Function; + +public class InMemoryClientTransport implements McpClientTransport { + + private final Sinks.Many toClientSink; + + private final Sinks.Many toServerSink; + + private final ObjectMapper objectMapper; + + public InMemoryClientTransport(Sinks.Many toClientSink, Sinks.Many toServerSink, + ObjectMapper objectMapper) { + this.toClientSink = toClientSink; + this.toServerSink = toServerSink; + this.objectMapper = objectMapper; + } + + @Override + public Mono connect(Function, Mono> handler) { + toClientSink.asFlux() + .flatMap(message -> handler.apply(Mono.just(message))) + .subscribe(toServerSink::tryEmitNext); + return Mono.empty(); + } + + @Override + public Mono sendMessage(JSONRPCMessage message) { + toServerSink.tryEmitNext(message); + return Mono.empty(); + } + + @Override + public T unmarshalFrom(Object data, TypeReference typeRef) { + return this.objectMapper.convertValue(data, typeRef); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + +} diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java new file mode 100644 index 000000000..b7e9f65d3 --- /dev/null +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java @@ -0,0 +1,38 @@ +package io.modelcontextprotocol.transport.inmemory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerTransport; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +public class InMemoryServerTransport implements McpServerTransport { + + private final Sinks.Many toClientSink; + + private final ObjectMapper objectMapper; + + public InMemoryServerTransport(Sinks.Many toClientSink, + Sinks.Many toServerSink, ObjectMapper objectMapper) { + this.toClientSink = toClientSink; + this.objectMapper = objectMapper; + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono sendMessage(McpSchema.JSONRPCMessage message) { + toClientSink.tryEmitNext(message); + return Mono.empty(); + } + + @Override + public T unmarshalFrom(Object data, TypeReference typeRef) { + return this.objectMapper.convertValue(data, typeRef); + } + +} diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java new file mode 100644 index 000000000..3d172e5f6 --- /dev/null +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java @@ -0,0 +1,44 @@ +package io.modelcontextprotocol.transport.inmemory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.spec.McpServerTransportProvider; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +public class InMemoryServerTransportProvider implements McpServerTransportProvider { + + private final Sinks.Many toClientSink; + + private final Sinks.Many toServerSink; + + private final ObjectMapper objectMapper; + + public InMemoryServerTransportProvider(Sinks.Many toClientSink, + Sinks.Many toServerSink, ObjectMapper objectMapper) { + this.toClientSink = toClientSink; + this.toServerSink = toServerSink; + this.objectMapper = objectMapper; + } + + @Override + public void setSessionFactory(McpServerSession.Factory sessionFactory) { + var session = sessionFactory.create(new InMemoryServerTransport(toClientSink, toServerSink, objectMapper)); + toServerSink.asFlux().subscribe(message -> { + session.handle(message).subscribe(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono notifyClients(String method, Object params) { + // Not implemented for in-memory transport + return Mono.empty(); + } + +} diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java new file mode 100644 index 000000000..2e70c122d --- /dev/null +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java @@ -0,0 +1,15 @@ +package io.modelcontextprotocol.transport.inmemory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpSchema; +import reactor.core.publisher.Sinks; + +public class InMemoryTransport { + + final Sinks.Many toClientSink = Sinks.many().multicast().onBackpressureBuffer(); + + final Sinks.Many toServerSink = Sinks.many().multicast().onBackpressureBuffer(); + + final ObjectMapper objectMapper = new ObjectMapper(); + +} diff --git a/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java b/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java new file mode 100644 index 000000000..5cb182b7f --- /dev/null +++ b/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java @@ -0,0 +1,44 @@ +package io.modelcontextprotocol.transport.inmemory; + +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class InMemoryTransportTest { + + @Test + void shouldSendMessageFromClientToServer() { + var transport = new InMemoryTransport(); + var serverProvider = new InMemoryServerTransportProvider(transport.toClientSink, transport.toServerSink, + transport.objectMapper); + var clientTransport = new InMemoryClientTransport(transport.toClientSink, transport.toServerSink, + transport.objectMapper); + + var server = McpServer.sync(serverProvider) + .toolCall(McpSchema.Tool.builder() + .name("test-tool") + .description("a test tool") + .inputSchema(new McpSchema.JsonSchema("object", Map.of(), List.of(), true, null, null)) + .build(), (exchange, request) -> { + return new McpSchema.CallToolResult("test-result", false); + }) + .build(); + + var client = McpClient.sync(clientTransport).build(); + + client.initialize(); + + var result = client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of())); + + assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); + McpSchema.TextContent textContent = (McpSchema.TextContent) result.content().get(0); + assertThat(textContent.text()).isEqualTo("test-result"); + } + +} diff --git a/pom.xml b/pom.xml index c0b1f7a44..3859fbf57 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ 3.26.3 5.10.2 - 5.17.0 + 5.2.0 1.20.4 1.17.5 1.21.0 @@ -103,6 +103,7 @@ mcp-bom mcp + mcp-inmemory-transport mcp-spring/mcp-spring-webflux mcp-spring/mcp-spring-webmvc mcp-test @@ -182,7 +183,7 @@ maven-surefire-plugin ${maven-surefire-plugin.version} - ${surefireArgLine} -javaagent:${org.mockito:mockito-core:jar} + ${surefireArgLine} false false From a66e5127cc068d2d0c693f47b263403a2057ff70 Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Fri, 22 Aug 2025 13:17:23 +0200 Subject: [PATCH 2/5] feat: add support for in-memory-transport --- .../inmemory/InMemoryClientTransport.java | 40 ++++++++++++------- .../inmemory/InMemoryServerTransport.java | 31 +++++++++----- .../InMemoryServerTransportProvider.java | 25 +++++------- .../transport/inmemory/InMemoryTransport.java | 23 +++++++---- .../inmemory/InMemoryTransportTest.java | 27 +++++++------ 5 files changed, 87 insertions(+), 59 deletions(-) diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java index 1a69a16b5..690d05b0f 100644 --- a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryClientTransport.java @@ -1,50 +1,60 @@ package io.modelcontextprotocol.transport.inmemory; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import java.util.function.Function; -public class InMemoryClientTransport implements McpClientTransport { +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; - private final Sinks.Many toClientSink; +public class InMemoryClientTransport implements McpClientTransport { - private final Sinks.Many toServerSink; + private final InMemoryTransport transport; - private final ObjectMapper objectMapper; + private Disposable disposable; - public InMemoryClientTransport(Sinks.Many toClientSink, Sinks.Many toServerSink, - ObjectMapper objectMapper) { - this.toClientSink = toClientSink; - this.toServerSink = toServerSink; - this.objectMapper = objectMapper; + public InMemoryClientTransport( InMemoryTransport transport ) { + this.transport = requireNonNull(transport, "transport cannot be null"); } @Override public Mono connect(Function, Mono> handler) { - toClientSink.asFlux() + disposable = transport.clientSink().asFlux() .flatMap(message -> handler.apply(Mono.just(message))) - .subscribe(toServerSink::tryEmitNext); + .subscribe( message -> sendMessage( message ).subscribe() ); return Mono.empty(); } @Override public Mono sendMessage(JSONRPCMessage message) { - toServerSink.tryEmitNext(message); - return Mono.empty(); + var result = ofNullable(transport.serverSink()) + .map( s -> s.tryEmitNext(message)) + .orElse( Sinks.EmitResult.FAIL_TERMINATED ); + return switch( result ) { + case OK -> Mono.empty(); + case FAIL_TERMINATED, + FAIL_NON_SERIALIZED, + FAIL_OVERFLOW, + FAIL_CANCELLED, + FAIL_ZERO_SUBSCRIBER -> Mono.error( () -> new Sinks.EmissionException(result) ); + }; } @Override public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + return transport.objectMapper().convertValue(data, typeRef); } @Override public Mono closeGracefully() { + if( disposable!=null && !disposable.isDisposed() ) { + disposable.dispose(); + } return Mono.empty(); } diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java index b7e9f65d3..114fc8d40 100644 --- a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransport.java @@ -1,22 +1,24 @@ package io.modelcontextprotocol.transport.inmemory; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransport; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + public class InMemoryServerTransport implements McpServerTransport { - private final Sinks.Many toClientSink; + private final InMemoryTransport transport; - private final ObjectMapper objectMapper; + public InMemoryServerTransport( InMemoryTransport transport ) { + this.transport = requireNonNull(transport, "transport cannot be null"); + } - public InMemoryServerTransport(Sinks.Many toClientSink, - Sinks.Many toServerSink, ObjectMapper objectMapper) { - this.toClientSink = toClientSink; - this.objectMapper = objectMapper; + public Sinks.Many serverSink() { + return transport.serverSink(); } @Override @@ -26,13 +28,22 @@ public Mono closeGracefully() { @Override public Mono sendMessage(McpSchema.JSONRPCMessage message) { - toClientSink.tryEmitNext(message); - return Mono.empty(); + var result = ofNullable( transport.clientSink()) + .map( s -> s.tryEmitNext(message) ) + .orElse( Sinks.EmitResult.FAIL_TERMINATED ); + return switch( result ) { + case OK -> Mono.empty(); + case FAIL_TERMINATED, + FAIL_NON_SERIALIZED, + FAIL_OVERFLOW, + FAIL_CANCELLED, + FAIL_ZERO_SUBSCRIBER -> Mono.error( () -> new Sinks.EmissionException(result) ); + }; } @Override public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + return transport.objectMapper().convertValue(data, typeRef); } } diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java index 3d172e5f6..9bcea3cf9 100644 --- a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryServerTransportProvider.java @@ -1,37 +1,34 @@ package io.modelcontextprotocol.transport.inmemory; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import reactor.core.Disposable; import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; public class InMemoryServerTransportProvider implements McpServerTransportProvider { - private final Sinks.Many toClientSink; + private final InMemoryServerTransport serverTransport; + private Disposable disposable; - private final Sinks.Many toServerSink; - - private final ObjectMapper objectMapper; - - public InMemoryServerTransportProvider(Sinks.Many toClientSink, - Sinks.Many toServerSink, ObjectMapper objectMapper) { - this.toClientSink = toClientSink; - this.toServerSink = toServerSink; - this.objectMapper = objectMapper; + public InMemoryServerTransportProvider( InMemoryTransport transport ) { + serverTransport = new InMemoryServerTransport(transport); } @Override public void setSessionFactory(McpServerSession.Factory sessionFactory) { - var session = sessionFactory.create(new InMemoryServerTransport(toClientSink, toServerSink, objectMapper)); - toServerSink.asFlux().subscribe(message -> { + + var session = sessionFactory.create(serverTransport); + disposable = serverTransport.serverSink().asFlux().subscribe(message -> { session.handle(message).subscribe(); }); } @Override public Mono closeGracefully() { + if( disposable!=null && !disposable.isDisposed() ) { + disposable.dispose(); + } return Mono.empty(); } diff --git a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java index 2e70c122d..d80f07cae 100644 --- a/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java +++ b/mcp-inmemory-transport/src/main/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransport.java @@ -4,12 +4,21 @@ import io.modelcontextprotocol.spec.McpSchema; import reactor.core.publisher.Sinks; -public class InMemoryTransport { - - final Sinks.Many toClientSink = Sinks.many().multicast().onBackpressureBuffer(); - - final Sinks.Many toServerSink = Sinks.many().multicast().onBackpressureBuffer(); - - final ObjectMapper objectMapper = new ObjectMapper(); +import static java.util.Objects.requireNonNull; +public record InMemoryTransport( + Sinks.Many clientSink, + Sinks.Many serverSink, + ObjectMapper objectMapper +){ + public InMemoryTransport { + requireNonNull(clientSink,"clientSink cannot be null!"); + requireNonNull(serverSink,"serverSink cannot be null!"); + requireNonNull(objectMapper,"objectMapper cannot be null!"); + } + public InMemoryTransport() { + this( Sinks.many().multicast().onBackpressureBuffer(), + Sinks.many().multicast().onBackpressureBuffer(), + new ObjectMapper() ); + } } diff --git a/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java b/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java index 5cb182b7f..86c0345f4 100644 --- a/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java +++ b/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import static org.assertj.core.api.Assertions.assertThat; @@ -15,30 +16,30 @@ class InMemoryTransportTest { @Test void shouldSendMessageFromClientToServer() { var transport = new InMemoryTransport(); - var serverProvider = new InMemoryServerTransportProvider(transport.toClientSink, transport.toServerSink, - transport.objectMapper); - var clientTransport = new InMemoryClientTransport(transport.toClientSink, transport.toServerSink, - transport.objectMapper); + var serverProvider = new InMemoryServerTransportProvider(transport); + var clientTransport = new InMemoryClientTransport(transport); var server = McpServer.sync(serverProvider) .toolCall(McpSchema.Tool.builder() .name("test-tool") .description("a test tool") .inputSchema(new McpSchema.JsonSchema("object", Map.of(), List.of(), true, null, null)) - .build(), (exchange, request) -> { - return new McpSchema.CallToolResult("test-result", false); - }) + .build(), (exchange, request) -> + new McpSchema.CallToolResult("test-result", false) + ) .build(); - var client = McpClient.sync(clientTransport).build(); - client.initialize(); + try(var client = McpClient.sync(clientTransport).build()) { - var result = client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of())); + CompletableFuture.supplyAsync(client::initialize); - assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - McpSchema.TextContent textContent = (McpSchema.TextContent) result.content().get(0); - assertThat(textContent.text()).isEqualTo("test-result"); + var result = client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of())); + + assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); + McpSchema.TextContent textContent = (McpSchema.TextContent) result.content().get(0); + assertThat(textContent.text()).isEqualTo("test-result"); + } } } From 795d2e6318a1105cadfa4cc4af9b419cf7754adf Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Fri, 22 Aug 2025 13:26:24 +0200 Subject: [PATCH 3/5] docs: add README.md --- mcp-inmemory-transport/README.md | 223 +++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 mcp-inmemory-transport/README.md diff --git a/mcp-inmemory-transport/README.md b/mcp-inmemory-transport/README.md new file mode 100644 index 000000000..003a22511 --- /dev/null +++ b/mcp-inmemory-transport/README.md @@ -0,0 +1,223 @@ +# MCP In-Memory Transport + +[![Maven Central](https://img.shields.io/maven-central/v/io.modelcontextprotocol.sdk/mcp-inmemory-transport)](https://central.sonatype.com/artifact/io.modelcontextprotocol.sdk/mcp-inmemory-transport) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +In-memory transport implementation for the Model Context Protocol (MCP) Java SDK. + +## Overview + +The `mcp-inmemory-transport` module provides an in-memory transport layer for the Model Context Protocol (MCP) Java SDK. This transport is particularly useful for testing, local development, and scenarios where you need to establish direct communication between MCP client and server components without network overhead. + +## Features + +- **In-Memory Communication**: Enables direct communication between MCP client and server without network calls +- **Reactive Streams**: Built on Project Reactor for non-blocking, reactive communication +- **Easy Integration**: Seamlessly integrates with the MCP Java SDK +- **Testing-Friendly**: Ideal for unit and integration testing of MCP implementations + +## Getting Started + +### Prerequisites + +- Java 17 or higher +- Maven or Gradle build system +- MCP Java SDK core dependency + +### Installation + +#### Maven + +Add the following dependency to your `pom.xml`: + +```xml + + io.modelcontextprotocol.sdk + mcp-inmemory-transport + 0.12.0-SNAPSHOT + +``` + +#### Gradle + +Add the following to your `build.gradle`: + +```gradle +implementation 'io.modelcontextprotocol.sdk:mcp-inmemory-transport:0.12.0-SNAPSHOT' +``` + +### Usage + +#### Basic Setup + +To use the in-memory transport, you'll need to create an `InMemoryTransport` instance and use it to create both client and server transports: + +```java +// Create the shared in-memory transport +InMemoryTransport transport = new InMemoryTransport(); + +// Create server transport provider +InMemoryServerTransportProvider serverProvider = new InMemoryServerTransportProvider(transport); + +// Create client transport +InMemoryClientTransport clientTransport = new InMemoryClientTransport(transport); +``` + +#### Creating an MCP Server + +```java +McpServer server = McpServer.sync(serverProvider) + .toolCall(McpSchema.Tool.builder() + .name("echo") + .description("Echoes the input") + .inputSchema(new McpSchema.JsonSchema( + "object", + Map.of("message", Map.of("type", "string")), + List.of("message"), + true, + null, + null)) + .build(), (exchange, request) -> { + String message = (String) request.arguments().get("message"); + return new McpSchema.CallToolResult("Echo: " + message, false); + }) + .build(); +``` + +#### Creating an MCP Client + +```java +try (McpClient client = McpClient.sync(clientTransport).build()) { + // Initialize the client + client.initialize(); + + // Call a tool + McpSchema.CallToolRequest request = new McpSchema.CallToolRequest( + "echo", + Map.of("message", "Hello, MCP!") + ); + + McpSchema.CallToolResult result = client.callTool(request); + System.out.println("Result: " + result.content()); +} +``` + +### Complete Example + +Here's a complete example showing how to set up and use the in-memory transport: + +```java +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.transport.inmemory.InMemoryTransport; +import io.modelcontextprotocol.transport.inmemory.InMemoryClientTransport; +import io.modelcontextprotocol.transport.inmemory.InMemoryServerTransportProvider; + +import java.util.List; +import java.util.Map; + +public class InMemoryTransportExample { + public static void main(String[] args) { + // Create the shared in-memory transport + InMemoryTransport transport = new InMemoryTransport(); + + // Create server transport provider + InMemoryServerTransportProvider serverProvider = new InMemoryServerTransportProvider(transport); + + // Create client transport + InMemoryClientTransport clientTransport = new InMemoryClientTransport(transport); + + // Set up the server + McpServer server = McpServer.sync(serverProvider) + .toolCall(McpSchema.Tool.builder() + .name("calculate") + .description("Performs a calculation") + .inputSchema(new McpSchema.JsonSchema( + "object", + Map.of( + "operation", Map.of("type", "string", "enum", List.of("add", "subtract")), + "a", Map.of("type", "number"), + "b", Map.of("type", "number") + ), + List.of("operation", "a", "b"), + true, + null, + null)) + .build(), (exchange, request) -> { + String operation = (String) request.arguments().get("operation"); + Number a = (Number) request.arguments().get("a"); + Number b = (Number) request.arguments().get("b"); + + double result; + switch (operation) { + case "add": + result = a.doubleValue() + b.doubleValue(); + break; + case "subtract": + result = a.doubleValue() - b.doubleValue(); + break; + default: + throw new IllegalArgumentException("Unknown operation: " + operation); + } + + return new McpSchema.CallToolResult(String.valueOf(result), false); + }) + .build(); + + // Use the client + try (McpClient client = McpClient.sync(clientTransport).build()) { + // Initialize the client + client.initialize(); + + // Call the calculate tool + McpSchema.CallToolRequest addRequest = new McpSchema.CallToolRequest( + "calculate", + Map.of("operation", "add", "a", 10, "b", 5) + ); + + McpSchema.CallToolResult addResult = client.callTool(addRequest); + System.out.println("10 + 5 = " + addResult.content().get(0)); + + McpSchema.CallToolRequest subtractRequest = new McpSchema.CallToolRequest( + "calculate", + Map.of("operation", "subtract", "a", 10, "b", 3) + ); + + McpSchema.CallToolResult subtractResult = client.callTool(subtractRequest); + System.out.println("10 - 3 = " + subtractResult.content().get(0)); + } + } +} +``` + +## Architecture + +The in-memory transport consists of three main components: + +1. **InMemoryTransport**: The core transport that manages the communication channels between client and server +2. **InMemoryClientTransport**: Implements the client-side transport interface +3. **InMemoryServerTransportProvider**: Provides the server-side transport implementation + +The transport uses Reactor's `Sinks.Many` to create multicast channels for message passing between client and server. + +## Testing + +The module includes comprehensive unit tests. To run them: + +```bash +mvn test +``` + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](../CONTRIBUTING.md) for details on how to contribute to this project. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Related Projects + +- [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) +- [Model Context Protocol Specification](https://github.com/modelcontextprotocol/specification) \ No newline at end of file From 77455162a4157ec0e6eb8b8816b7fb48a0a4545a Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Sat, 23 Aug 2025 16:51:12 +0200 Subject: [PATCH 4/5] docs: add README.md --- mcp-inmemory-transport/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mcp-inmemory-transport/README.md b/mcp-inmemory-transport/README.md index 003a22511..c1d1dec47 100644 --- a/mcp-inmemory-transport/README.md +++ b/mcp-inmemory-transport/README.md @@ -1,13 +1,11 @@ # MCP In-Memory Transport -[![Maven Central](https://img.shields.io/maven-central/v/io.modelcontextprotocol.sdk/mcp-inmemory-transport)](https://central.sonatype.com/artifact/io.modelcontextprotocol.sdk/mcp-inmemory-transport) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) - In-memory transport implementation for the Model Context Protocol (MCP) Java SDK. ## Overview -The `mcp-inmemory-transport` module provides an in-memory transport layer for the Model Context Protocol (MCP) Java SDK. This transport is particularly useful for testing, local development, and scenarios where you need to establish direct communication between MCP client and server components without network overhead. +The `mcp-inmemory-transport` module provides an in-memory transport layer for the Model Context Protocol (MCP) Java SDK. +This transport is particularly useful for testing, local development, and scenarios where you need to establish direct communication between MCP client and server components without network overhead. ## Features From cf58f672c6258c15d75306288f3b0270a5cf19af Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Sat, 23 Aug 2025 16:53:16 +0200 Subject: [PATCH 5/5] tests(InMemoryTransportTest): Refactor InMemoryTransport to add async client tests --- .../inmemory/InMemoryTransportTest.java | 65 ++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java b/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java index 86c0345f4..cb52536e0 100644 --- a/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java +++ b/mcp-inmemory-transport/src/test/java/io/modelcontextprotocol/transport/inmemory/InMemoryTransportTest.java @@ -3,36 +3,46 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; class InMemoryTransportTest { - @Test - void shouldSendMessageFromClientToServer() { - var transport = new InMemoryTransport(); - var serverProvider = new InMemoryServerTransportProvider(transport); - var clientTransport = new InMemoryClientTransport(transport); + final InMemoryTransport transport = new InMemoryTransport(); - var server = McpServer.sync(serverProvider) - .toolCall(McpSchema.Tool.builder() - .name("test-tool") - .description("a test tool") - .inputSchema(new McpSchema.JsonSchema("object", Map.of(), List.of(), true, null, null)) - .build(), (exchange, request) -> - new McpSchema.CallToolResult("test-result", false) + @BeforeEach + public void createSyncMCPServer() { + var serverProvider = new InMemoryServerTransportProvider(transport); + McpServer.sync(serverProvider) + .toolCall(McpSchema.Tool.builder() + .name("test-tool") + .description("a test tool") + .inputSchema(new McpSchema.JsonSchema("object", Map.of(), List.of(), true, null, null)) + .build(), (exchange, request) -> + new McpSchema.CallToolResult("test-result", false) ) - .build(); + .build(); + } + @Test + void shouldSendMessageFromSyncClientToServer() { + + var clientTransport = new InMemoryClientTransport(transport); try(var client = McpClient.sync(clientTransport).build()) { - CompletableFuture.supplyAsync(client::initialize); + client.initialize(); + var toolList = client.listTools(); + + assertFalse( toolList.tools().isEmpty() ); + assertEquals( 1, toolList.tools().size() ); var result = client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of())); @@ -42,4 +52,29 @@ void shouldSendMessageFromClientToServer() { } } + @Test + void shouldSendMessageFromAsyncClientToServer() { + var clientTransport = new InMemoryClientTransport(transport); + + var client = McpClient.async(clientTransport).build(); + + client.initialize() + .flatMap( initResult -> client.listTools() ) + .flatMap( toolList -> { + assertFalse(toolList.tools().isEmpty()); + assertEquals(1, toolList.tools().size()); + + return client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of())); + }) + .doFinally(signalType -> { + client.closeGracefully().subscribe(); + }) + .subscribe( result -> { + + assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); + McpSchema.TextContent textContent = (McpSchema.TextContent) result.content().get(0); + assertThat(textContent.text()).isEqualTo("test-result"); + }); + } + }