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 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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"); + }); + } + } From 68b80804eae807f10ee9d9da37f5e493b10dec18 Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Wed, 19 Nov 2025 15:07:21 +0100 Subject: [PATCH 06/14] refactor(mcp-inmemory-transport): bump to new dev version 0.17.0-SNAPSHOT - use JacksonMcpJsonMapper instead of ObjectMapper - update procotoc version to CP_2025_03_26 --- mcp-inmemory-transport/pom.xml | 12 ++-- .../inmemory/InMemoryClientTransport.java | 55 +++++++++++++------ .../inmemory/InMemoryServerTransport.java | 31 +++++++---- .../InMemoryServerTransportProvider.java | 13 ++++- .../transport/inmemory/InMemoryTransport.java | 21 ++++--- .../inmemory/InMemoryTransportTest.java | 53 ++++++++---------- 6 files changed, 107 insertions(+), 78 deletions(-) diff --git a/mcp-inmemory-transport/pom.xml b/mcp-inmemory-transport/pom.xml index afbe5a2be..8070a05ca 100644 --- a/mcp-inmemory-transport/pom.xml +++ b/mcp-inmemory-transport/pom.xml @@ -7,7 +7,7 @@ io.modelcontextprotocol.sdk mcp-parent - 0.12.0-SNAPSHOT + 0.17.0-SNAPSHOT ../pom.xml @@ -21,6 +21,11 @@ mcp ${project.version} + + io.modelcontextprotocol.sdk + mcp-json-jackson2 + ${project.version} + org.springframework.boot spring-boot-starter-logging @@ -31,11 +36,6 @@ io.projectreactor reactor-core - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - org.junit.jupiter junit-jupiter-api 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 690d05b0f..957592e37 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,61 +1,80 @@ package io.modelcontextprotocol.transport.inmemory; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import io.modelcontextprotocol.spec.ProtocolVersions; import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; +import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; public class InMemoryClientTransport implements McpClientTransport { + private McpJsonMapper mapper; + private final InMemoryTransport transport; + private Consumer exceptionHandler = throwable -> { + }; + private Disposable disposable; - public InMemoryClientTransport( InMemoryTransport transport ) { + public InMemoryClientTransport(InMemoryTransport transport) { this.transport = requireNonNull(transport, "transport cannot be null"); } @Override public Mono connect(Function, Mono> handler) { - disposable = transport.clientSink().asFlux() + disposable = transport.clientSink() + .asFlux() .flatMap(message -> handler.apply(Mono.just(message))) - .subscribe( message -> sendMessage( message ).subscribe() ); + .subscribe(message -> sendMessage(message).subscribe(), exceptionHandler); return Mono.empty(); } @Override public Mono sendMessage(JSONRPCMessage message) { - var result = ofNullable(transport.serverSink()) - .map( s -> s.tryEmitNext(message)) - .orElse( Sinks.EmitResult.FAIL_TERMINATED ); - return switch( result ) { + 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) ); + 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 transport.objectMapper().convertValue(data, typeRef); - } - @Override public Mono closeGracefully() { - if( disposable!=null && !disposable.isDisposed() ) { + if (disposable != null && !disposable.isDisposed()) { disposable.dispose(); } return Mono.empty(); } + @Override + public void setExceptionHandler(Consumer handler) { + exceptionHandler = handler; + } + + @Override + public T unmarshalFrom(Object data, TypeRef typeRef) { + return transport.objectMapper().convertValue(data, typeRef); + } + + @Override + public List protocolVersions() { + return List.of(ProtocolVersions.MCP_2025_03_26); + } + } 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 114fc8d40..7edcec522 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,11 +1,17 @@ package io.modelcontextprotocol.transport.inmemory; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransport; +import io.modelcontextprotocol.spec.ProtocolVersions; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import java.util.List; + +import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; @@ -13,7 +19,7 @@ public class InMemoryServerTransport implements McpServerTransport { private final InMemoryTransport transport; - public InMemoryServerTransport( InMemoryTransport transport ) { + public InMemoryServerTransport(InMemoryTransport transport) { this.transport = requireNonNull(transport, "transport cannot be null"); } @@ -28,22 +34,23 @@ public Mono closeGracefully() { @Override public Mono sendMessage(McpSchema.JSONRPCMessage message) { - var result = ofNullable( transport.clientSink()) - .map( s -> s.tryEmitNext(message) ) - .orElse( Sinks.EmitResult.FAIL_TERMINATED ); - return switch( result ) { + 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) ); - }; + 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) { + public T unmarshalFrom(Object data, TypeRef typeRef) { return transport.objectMapper().convertValue(data, typeRef); } + @Override + public List protocolVersions() { + return List.of(ProtocolVersions.MCP_2025_03_26); + } + } 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 9bcea3cf9..c0d104d91 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 @@ -3,15 +3,19 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import io.modelcontextprotocol.spec.ProtocolVersions; import reactor.core.Disposable; import reactor.core.publisher.Mono; +import java.util.List; + public class InMemoryServerTransportProvider implements McpServerTransportProvider { private final InMemoryServerTransport serverTransport; + private Disposable disposable; - public InMemoryServerTransportProvider( InMemoryTransport transport ) { + public InMemoryServerTransportProvider(InMemoryTransport transport) { serverTransport = new InMemoryServerTransport(transport); } @@ -26,7 +30,7 @@ public void setSessionFactory(McpServerSession.Factory sessionFactory) { @Override public Mono closeGracefully() { - if( disposable!=null && !disposable.isDisposed() ) { + if (disposable != null && !disposable.isDisposed()) { disposable.dispose(); } return Mono.empty(); @@ -38,4 +42,9 @@ public Mono notifyClients(String method, Object params) { return Mono.empty(); } + @Override + public List protocolVersions() { + return List.of(ProtocolVersions.MCP_2025_03_26); + } + } 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 d80f07cae..5ebded339 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 @@ -1,24 +1,23 @@ package io.modelcontextprotocol.transport.inmemory; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import reactor.core.publisher.Sinks; import static java.util.Objects.requireNonNull; -public record InMemoryTransport( - Sinks.Many clientSink, - Sinks.Many serverSink, - ObjectMapper objectMapper -){ +public record InMemoryTransport(Sinks.Many clientSink, + Sinks.Many serverSink, McpJsonMapper objectMapper) { public InMemoryTransport { - requireNonNull(clientSink,"clientSink cannot be null!"); - requireNonNull(serverSink,"serverSink cannot be null!"); - requireNonNull(objectMapper,"objectMapper cannot be null!"); + 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() ); + this(Sinks.many().multicast().onBackpressureBuffer(), Sinks.many().multicast().onBackpressureBuffer(), + new JacksonMcpJsonMapper(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 cb52536e0..3a17953d4 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 @@ -18,31 +18,30 @@ class InMemoryTransportTest { final InMemoryTransport transport = new InMemoryTransport(); @BeforeEach - public void createSyncMCPServer() { + 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(); + .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(); } + @Test void shouldSendMessageFromSyncClientToServer() { var clientTransport = new InMemoryClientTransport(transport); - try(var client = McpClient.sync(clientTransport).build()) { + try (var client = McpClient.sync(clientTransport).build()) { client.initialize(); var toolList = client.listTools(); - assertFalse( toolList.tools().isEmpty() ); - assertEquals( 1, toolList.tools().size() ); + assertFalse(toolList.tools().isEmpty()); + assertEquals(1, toolList.tools().size()); var result = client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of())); @@ -58,23 +57,19 @@ void shouldSendMessageFromAsyncClientToServer() { 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"); - }); + 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"); + }); } } From d0ca9297a30e72412c433a2a80db6694bcc20069 Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Thu, 20 Nov 2025 11:01:30 +0100 Subject: [PATCH 07/14] chore(InMemoryTransport): remove unused import --- .../transport/inmemory/InMemoryClientTransport.java | 3 --- .../transport/inmemory/InMemoryServerTransport.java | 3 --- .../transport/inmemory/InMemoryServerTransportProvider.java | 1 - 3 files changed, 7 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 957592e37..a5b2ba362 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,7 +1,5 @@ package io.modelcontextprotocol.transport.inmemory; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JavaType; import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpClientTransport; @@ -15,7 +13,6 @@ import java.util.function.Consumer; import java.util.function.Function; -import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; 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 7edcec522..69e22d1cd 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,7 +1,5 @@ package io.modelcontextprotocol.transport.inmemory; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JavaType; import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransport; @@ -11,7 +9,6 @@ import java.util.List; -import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; 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 c0d104d91..0e49e19cb 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,6 +1,5 @@ package io.modelcontextprotocol.transport.inmemory; -import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.spec.ProtocolVersions; From 101931f7660e47dab120803c8ab8432d5316558f Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Fri, 20 Feb 2026 15:45:54 +0100 Subject: [PATCH 08/14] refactor(InMemoryTransport): upgrade to mcp-json-jackson3 --- mcp-inmemory-transport/pom.xml | 4 ++-- .../transport/inmemory/InMemoryTransport.java | 6 +++--- .../transport/inmemory/InMemoryTransportTest.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mcp-inmemory-transport/pom.xml b/mcp-inmemory-transport/pom.xml index 8070a05ca..56ec71d85 100644 --- a/mcp-inmemory-transport/pom.xml +++ b/mcp-inmemory-transport/pom.xml @@ -7,7 +7,7 @@ io.modelcontextprotocol.sdk mcp-parent - 0.17.0-SNAPSHOT + 1.0.0-SNAPSHOT ../pom.xml @@ -23,7 +23,7 @@ io.modelcontextprotocol.sdk - mcp-json-jackson2 + mcp-json-jackson3 ${project.version} 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 5ebded339..ed75ed034 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 @@ -1,10 +1,10 @@ package io.modelcontextprotocol.transport.inmemory; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.json.McpJsonMapper; -import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import reactor.core.publisher.Sinks; +import tools.jackson.databind.json.JsonMapper; import static java.util.Objects.requireNonNull; @@ -18,6 +18,6 @@ public record InMemoryTransport(Sinks.Many clientSink, public InMemoryTransport() { this(Sinks.many().multicast().onBackpressureBuffer(), Sinks.many().multicast().onBackpressureBuffer(), - new JacksonMcpJsonMapper(new ObjectMapper())); + new JacksonMcpJsonMapper(new JsonMapper())); } } 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 3a17953d4..1e2b47fdd 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 @@ -25,7 +25,7 @@ public void createSyncMCPServer() { .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(), (exchange, request) -> McpSchema.CallToolResult.builder().addTextContent("test-result").build()) .build(); } From 3db107bac49e3a2ddb0baa69d4f0c42b7c0e0cb3 Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Mon, 20 Apr 2026 16:43:22 +0200 Subject: [PATCH 09/14] refactor(InMemoryClientTransport): Add protected method to expose transport --- .../transport/inmemory/InMemoryClientTransport.java | 4 ++++ 1 file changed, 4 insertions(+) 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 a5b2ba362..87270460e 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 @@ -31,6 +31,10 @@ public InMemoryClientTransport(InMemoryTransport transport) { this.transport = requireNonNull(transport, "transport cannot be null"); } + protected InMemoryTransport transport() { + return transport; + } + @Override public Mono connect(Function, Mono> handler) { disposable = transport.clientSink() From 9b53fe69cd2ea6c07e297542ebd8becd801d4ed2 Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Mon, 20 Apr 2026 16:44:03 +0200 Subject: [PATCH 10/14] test(AbstractMcpAsyncClientResiliencyTests): Add host configuration for async client resiliency tests --- .../client/AbstractMcpAsyncClientResiliencyTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java index e8d24a379..8fb8093ac 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java @@ -45,6 +45,7 @@ public abstract class AbstractMcpAsyncClientResiliencyTests { private static final Logger logger = LoggerFactory.getLogger(AbstractMcpAsyncClientResiliencyTests.class); static Network network = Network.newNetwork(); + public static String host = "http://localhost:3001"; @SuppressWarnings("resource") From 105dd7066fc903c7b1941493ebe1201984aa10db Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Mon, 20 Apr 2026 16:45:30 +0200 Subject: [PATCH 11/14] refactor(InMemoryTransportTest): reformat toolCall method for readability --- .../transport/inmemory/InMemoryTransportTest.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 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 1e2b47fdd..feb82eeea 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 @@ -21,11 +21,13 @@ class InMemoryTransportTest { 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) -> McpSchema.CallToolResult.builder().addTextContent("test-result").build()) + .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) -> McpSchema.CallToolResult.builder().addTextContent("test-result").build()) .build(); } From f0044bd6acf5e4b4ff007e1bf2e7f30827c65692 Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Mon, 20 Apr 2026 16:48:54 +0200 Subject: [PATCH 12/14] Merge remote-tracking branch 'original/main' --- pom.xml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pom.xml b/pom.xml index d79850c57..0128b1074 100644 --- a/pom.xml +++ b/pom.xml @@ -105,18 +105,9 @@ mcp-bom mcp -<<<<<<< HEAD - mcp-inmemory-transport - mcp-core - mcp-json-jackson2 - mcp-json-jackson3 - mcp-spring/mcp-spring-webflux - mcp-spring/mcp-spring-webmvc -======= mcp-core mcp-json-jackson2 mcp-json-jackson3 ->>>>>>> original/main mcp-test conformance-tests From 509aa1f5e41ec43af8ecbd6f4baae435a67c66c1 Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Mon, 20 Apr 2026 17:09:57 +0200 Subject: [PATCH 13/14] build: Update parent version to 2.0.0-SNAPSHOT --- mcp-inmemory-transport/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-inmemory-transport/pom.xml b/mcp-inmemory-transport/pom.xml index 56ec71d85..1659154fe 100644 --- a/mcp-inmemory-transport/pom.xml +++ b/mcp-inmemory-transport/pom.xml @@ -7,7 +7,7 @@ io.modelcontextprotocol.sdk mcp-parent - 1.0.0-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml From 7875f5ff0aa11a416c181637d46df7bd570d1b2d Mon Sep 17 00:00:00 2001 From: bsorrentino Date: Mon, 20 Apr 2026 17:10:35 +0200 Subject: [PATCH 14/14] build: Add mcp-inmemory-transport module to build configuration --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 0128b1074..6a62ce70a 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,7 @@ mcp-json-jackson3 mcp-test conformance-tests + mcp-inmemory-transport