diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 067fbac2..25645d20 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -830,4 +830,35 @@ void testProgressConsumer() { }); } + // Tests for ignorable JSON-RPC methods feature + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + withClient(createMcpTransport(), builder -> builder.ignorableJsonRpcMethods(customIgnorables), client -> { + StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); + }); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + withClient(createMcpTransport(), + builder -> builder.ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3"), + client -> { + StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); + }); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy(() -> McpClient.async(createMcpTransport()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpClient.async(createMcpTransport()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index 175a0107..3f7177cb 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -695,4 +695,35 @@ void testProgressConsumer() { }); } + // Tests for ignorable JSON-RPC methods feature + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + withClient(createMcpTransport(), builder -> builder.ignorableJsonRpcMethods(customIgnorables), client -> { + assertThatCode(() -> client.initialize()).doesNotThrowAnyException(); + }); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + withClient(createMcpTransport(), + builder -> builder.ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3"), + client -> { + assertThatCode(() -> client.initialize()).doesNotThrowAnyException(); + }); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy(() -> McpClient.sync(createMcpTransport()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpClient.sync(createMcpTransport()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index eb08bdcd..6ef1d574 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -522,4 +522,44 @@ void testRootsChangeHandlers() { .doesNotThrowAnyException(); } + // --------------------------------------- + // Ignorable JSON-RPC Methods Tests + // --------------------------------------- + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + var server = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods(customIgnorables) + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + var server = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3") + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy( + () -> McpServer.async(createMcpTransportProvider()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 4d5f9f77..9d064f61 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -492,4 +492,44 @@ void testRootsChangeHandlers() { assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); } + // --------------------------------------- + // Ignorable JSON-RPC Methods Tests + // --------------------------------------- + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + var server = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods(customIgnorables) + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + var server = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3") + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy( + () -> McpServer.sync(createMcpTransportProvider()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 9e861deb..cfeda594 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -159,7 +159,7 @@ public class McpAsyncClient { * @param features the MCP Client supported features. */ McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout, - McpClientFeatures.Async features) { + McpClientFeatures.Async features, List ignorableJsonRpcMethods) { Assert.notNull(transport, "Transport must not be null"); Assert.notNull(requestTimeout, "Request timeout must not be null"); @@ -269,7 +269,7 @@ public class McpAsyncClient { this.initializer = new LifecycleInitializer(clientCapabilities, clientInfo, List.of(McpSchema.LATEST_PROTOCOL_VERSION), initializationTimeout, ctx -> new McpClientSession(requestTimeout, transport, requestHandlers, notificationHandlers, - con -> con.contextWrite(ctx))); + con -> con.contextWrite(ctx), ignorableJsonRpcMethods)); this.transport.setExceptionHandler(this.initializer::handleException); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java index c8af28ac..1623bb94 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java @@ -23,6 +23,7 @@ import io.modelcontextprotocol.spec.McpSchema.Implementation; import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.util.Utils; import reactor.core.publisher.Mono; /** @@ -161,6 +162,12 @@ class SyncSpec { private Duration initializationTimeout = Duration.ofSeconds(20); + /** + * List of JSON-RPC methods that can be ignored. These methods will not be + * processed and will not generate errors if received + */ + private final List ignorableJsonRpcMethods = new ArrayList<>(Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS); + private ClientCapabilities capabilities; private Implementation clientInfo = new Implementation("Java SDK MCP Client", "1.0.0"); @@ -215,6 +222,36 @@ public SyncSpec initializationTimeout(Duration initializationTimeout) { return this; } + /** + * Sets the list of JSON-RPC methods that can be ignored by the client. These + * methods will not be processed and will not generate errors if received. + * @param ignorableJsonRpcMethods A list of JSON-RPC method names to ignore. Must + * not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public SyncSpec ignorableJsonRpcMethods(List ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + this.ignorableJsonRpcMethods.addAll(ignorableJsonRpcMethods); + return this; + } + + /** + * Sets the list of JSON-RPC methods that can be ignored by the client. These + * methods will not be processed and will not generate errors if received. + * @param ignorableJsonRpcMethods An array of JSON-RPC method names to ignore. + * Must not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public SyncSpec ignorableJsonRpcMethods(String... ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + for (String method : ignorableJsonRpcMethods) { + this.ignorableJsonRpcMethods.add(method); + } + return this; + } + /** * Sets the client capabilities that will be advertised to the server during * connection initialization. Capabilities define what features the client @@ -422,8 +459,8 @@ public McpSyncClient build() { McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures); - return new McpSyncClient( - new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout, asyncFeatures)); + return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout, + asyncFeatures, this.ignorableJsonRpcMethods)); } } @@ -452,6 +489,12 @@ class AsyncSpec { private Duration initializationTimeout = Duration.ofSeconds(20); + /** + * List of JSON-RPC methods that can be ignored. These methods will not be + * processed and will not generate errors if received + */ + private final List ignorableJsonRpcMethods = new ArrayList<>(Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS); + private ClientCapabilities capabilities; private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1"); @@ -506,6 +549,36 @@ public AsyncSpec initializationTimeout(Duration initializationTimeout) { return this; } + /** + * Sets the list of JSON-RPC methods that can be ignored by the client. These + * methods will not be processed and will not generate errors if received. + * @param ignorableJsonRpcMethods A list of JSON-RPC method names to ignore. Must + * not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public AsyncSpec ignorableJsonRpcMethods(List ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + this.ignorableJsonRpcMethods.addAll(ignorableJsonRpcMethods); + return this; + } + + /** + * Sets the list of JSON-RPC methods that can be ignored by the client. These + * methods will not be processed and will not generate errors if received. + * @param ignorableJsonRpcMethods An array of JSON-RPC method names to ignore. + * Must not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public AsyncSpec ignorableJsonRpcMethods(String... ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + for (String method : ignorableJsonRpcMethods) { + this.ignorableJsonRpcMethods.add(method); + } + return this; + } + /** * Sets the client capabilities that will be advertised to the server during * connection initialization. Capabilities define what features the client @@ -730,7 +803,8 @@ public McpAsyncClient build() { new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers, this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, - this.samplingHandler, this.elicitationHandler)); + this.samplingHandler, this.elicitationHandler), + this.ignorableJsonRpcMethods); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 7131b10f..deb72425 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -116,6 +116,8 @@ public class McpAsyncServer { private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); + private final List ignorableJsonRpcMethods; + /** * Create a new McpAsyncServer with the given transport provider and capabilities. * @param mcpTransportProvider The transport layer implementation for MCP @@ -126,6 +128,22 @@ public class McpAsyncServer { McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper, McpServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) { + this(mcpTransportProvider, objectMapper, features, requestTimeout, uriTemplateManagerFactory, + jsonSchemaValidator, Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS); + } + + /** + * Create a new McpAsyncServer with the given transport provider and capabilities. + * @param mcpTransportProvider The transport layer implementation for MCP + * communication. + * @param features The MCP server supported features. + * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * @param ignorableJsonRpcMethods List of JSON-RPC method names that should be ignored + */ + McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper, + McpServerFeatures.Async features, Duration requestTimeout, + McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator, + List ignorableJsonRpcMethods) { this.mcpTransportProvider = mcpTransportProvider; this.objectMapper = objectMapper; this.serverInfo = features.serverInfo(); @@ -138,6 +156,8 @@ public class McpAsyncServer { this.completions.putAll(features.completions()); this.uriTemplateManagerFactory = uriTemplateManagerFactory; this.jsonSchemaValidator = jsonSchemaValidator; + this.ignorableJsonRpcMethods = ignorableJsonRpcMethods != null ? List.copyOf(ignorableJsonRpcMethods) + : List.of(); Map> requestHandlers = new HashMap<>(); @@ -190,9 +210,9 @@ public class McpAsyncServer { notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, asyncRootsListChangedNotificationHandler(rootsChangeConsumers)); - mcpTransportProvider.setSessionFactory( - transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport, - this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers)); + mcpTransportProvider.setSessionFactory(transport -> new McpServerSession(UUID.randomUUID().toString(), + requestTimeout, transport, this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, + notificationHandlers, this.ignorableJsonRpcMethods)); } // --------------------------------------- diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index d4b8addf..eda40f35 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -24,6 +24,7 @@ import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory; import io.modelcontextprotocol.util.McpUriTemplateManagerFactory; +import io.modelcontextprotocol.util.Utils; import reactor.core.publisher.Mono; /** @@ -212,6 +213,8 @@ class AsyncSpecification { private Duration requestTimeout = Duration.ofSeconds(10); // Default timeout + private final List ignorableJsonRpcMethods = new ArrayList<>(Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS); + private AsyncSpecification(McpServerTransportProvider transportProvider) { Assert.notNull(transportProvider, "Transport provider must not be null"); this.transportProvider = transportProvider; @@ -689,6 +692,34 @@ public AsyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonSchemaVali return this; } + /** + * Adds JSON-RPC method names that should be ignored by the server. Ignored + * methods will not generate error responses when received from clients, allowing + * for graceful handling of unknown or unwanted methods. + * @param ignorableJsonRpcMethods List of method names to ignore. Must not be + * null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public AsyncSpecification ignorableJsonRpcMethods(List ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + this.ignorableJsonRpcMethods.addAll(ignorableJsonRpcMethods); + return this; + } + + /** + * Adds JSON-RPC method names that should be ignored by the server using varargs. + * This is a convenience method for {@link #ignorableJsonRpcMethods(List)}. + * @param ignorableJsonRpcMethods Method names to ignore. Must not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public AsyncSpecification ignorableJsonRpcMethods(String... ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + this.ignorableJsonRpcMethods.addAll(List.of(ignorableJsonRpcMethods)); + return this; + } + /** * Builds an asynchronous MCP server that provides non-blocking operations. * @return A new instance of {@link McpAsyncServer} configured with this builder's @@ -701,8 +732,9 @@ public McpAsyncServer build() { var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator : new DefaultJsonSchemaValidator(mapper); + return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout, - this.uriTemplateManagerFactory, jsonSchemaValidator); + this.uriTemplateManagerFactory, jsonSchemaValidator, this.ignorableJsonRpcMethods); } } @@ -766,6 +798,8 @@ class SyncSpecification { private boolean immediateExecution = false; + private final List ignorableJsonRpcMethods = new ArrayList<>(Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS); + private SyncSpecification(McpServerTransportProvider transportProvider) { Assert.notNull(transportProvider, "Transport provider must not be null"); this.transportProvider = transportProvider; @@ -1251,6 +1285,34 @@ public SyncSpecification immediateExecution(boolean immediateExecution) { return this; } + /** + * Adds JSON-RPC method names that should be ignored by the server. Ignored + * methods will not generate error responses when received from clients, allowing + * for graceful handling of unknown or unwanted methods. + * @param ignorableJsonRpcMethods List of method names to ignore. Must not be + * null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public SyncSpecification ignorableJsonRpcMethods(List ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + this.ignorableJsonRpcMethods.addAll(ignorableJsonRpcMethods); + return this; + } + + /** + * Adds JSON-RPC method names that should be ignored by the server using varargs. + * This is a convenience method for {@link #ignorableJsonRpcMethods(List)}. + * @param ignorableJsonRpcMethods Method names to ignore. Must not be null. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if ignorableJsonRpcMethods is null + */ + public SyncSpecification ignorableJsonRpcMethods(String... ignorableJsonRpcMethods) { + Assert.notNull(ignorableJsonRpcMethods, "Ignorable JSON-RPC methods must not be null"); + this.ignorableJsonRpcMethods.addAll(List.of(ignorableJsonRpcMethods)); + return this; + } + /** * Builds a synchronous MCP server that provides blocking operations. * @return A new instance of {@link McpSyncServer} configured with this builder's @@ -1267,7 +1329,7 @@ public McpSyncServer build() { : new DefaultJsonSchemaValidator(mapper); var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout, - this.uriTemplateManagerFactory, jsonSchemaValidator); + this.uriTemplateManagerFactory, jsonSchemaValidator, this.ignorableJsonRpcMethods); return new McpSyncServer(asyncServer, this.immediateExecution); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java index cc7d2abf..0ccb32f7 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java @@ -4,21 +4,24 @@ package io.modelcontextprotocol.spec; -import com.fasterxml.jackson.core.type.TypeReference; -import io.modelcontextprotocol.util.Assert; -import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; - import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; + +import io.modelcontextprotocol.util.Assert; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoSink; + /** * Default implementation of the MCP (Model Context Protocol) session that manages * bidirectional JSON-RPC communication between clients and servers. This implementation @@ -61,6 +64,12 @@ public class McpClientSession implements McpSession { /** Atomic counter for generating unique request IDs */ private final AtomicLong requestCounter = new AtomicLong(0); + /** + * List of JSON-RPC methods that can be ignored. These methods will not be processed + * and will not generate errors if received + */ + private final List ignorableJsonRpcMethods; + /** * Functional interface for handling incoming JSON-RPC requests. Implementations * should process the request parameters and return a response. @@ -101,10 +110,7 @@ public interface NotificationHandler { * @param transport Transport implementation for message exchange * @param requestHandlers Map of method names to request handlers * @param notificationHandlers Map of method names to notification handlers - * @deprecated Use - * {@link #McpClientSession(Duration, McpClientTransport, Map, Map, Function)} */ - @Deprecated public McpClientSession(Duration requestTimeout, McpClientTransport transport, Map> requestHandlers, Map notificationHandlers) { this(requestTimeout, transport, requestHandlers, notificationHandlers, Function.identity()); @@ -118,20 +124,41 @@ public McpClientSession(Duration requestTimeout, McpClientTransport transport, * @param notificationHandlers Map of method names to notification handlers * @param connectHook Hook that allows transforming the connection Publisher prior to * subscribing + * @deprecated Use + * {@link #McpClientSession(Duration, McpClientTransport, Map, Map, Function, List)} + * instead. */ public McpClientSession(Duration requestTimeout, McpClientTransport transport, Map> requestHandlers, Map notificationHandlers, Function, ? extends Publisher> connectHook) { + this(requestTimeout, transport, requestHandlers, notificationHandlers, connectHook, List.of()); + } + + /** + * Creates a new McpClientSession with the specified configuration and handlers. + * @param requestTimeout Duration to wait for responses + * @param transport Transport implementation for message exchange + * @param requestHandlers Map of method names to request handlers + * @param notificationHandlers Map of method names to notification handlers + * @param connectHook Hook that allows transforming the connection Publisher prior to + * subscribing + * @param ignorableJsonRpcMethods List of JSON-RPC methods that can be ignored + */ + public McpClientSession(Duration requestTimeout, McpClientTransport transport, + Map> requestHandlers, Map notificationHandlers, + Function, ? extends Publisher> connectHook, List ignorableJsonRpcMethods) { Assert.notNull(requestTimeout, "The requestTimeout can not be null"); Assert.notNull(transport, "The transport can not be null"); Assert.notNull(requestHandlers, "The requestHandlers can not be null"); Assert.notNull(notificationHandlers, "The notificationHandlers can not be null"); + Assert.notNull(ignorableJsonRpcMethods, "The ignorableJsonRpcMethods can not be null"); this.requestTimeout = requestTimeout; this.transport = transport; this.requestHandlers.putAll(requestHandlers); this.notificationHandlers.putAll(notificationHandlers); + this.ignorableJsonRpcMethods = ignorableJsonRpcMethods; this.transport.connect(mono -> mono.doOnNext(this::handle)).transform(connectHook).subscribe(); } @@ -186,6 +213,12 @@ else if (message instanceof McpSchema.JSONRPCNotification notification) { */ private Mono handleIncomingRequest(McpSchema.JSONRPCRequest request) { return Mono.defer(() -> { + + if (this.ignorableJsonRpcMethods.contains(request.method())) { + logger.debug("Ignoring JSON-RPC request: {}", request); + return Mono.empty(); + } + var handler = this.requestHandlers.get(request.method()); if (handler == null) { MethodNotFoundError error = getMethodNotFoundError(request.method()); @@ -221,7 +254,12 @@ private Mono handleIncomingNotification(McpSchema.JSONRPCNotification noti return Mono.defer(() -> { var handler = notificationHandlers.get(notification.method()); if (handler == null) { - logger.error("No handler registered for notification method: {}", notification.method()); + if (this.ignorableJsonRpcMethods.contains(notification.method())) { + logger.debug("Ignoring JSON-RPC notification: {}", notification); + } + else { + logger.error("No handler registered for notification: {}", notification); + } return Mono.empty(); } return handler.handle(notification.params()); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java index 86906d85..c9efe8f9 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java @@ -1,6 +1,7 @@ package io.modelcontextprotocol.spec; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -56,6 +57,8 @@ public class McpServerSession implements McpSession { private final AtomicInteger state = new AtomicInteger(STATE_UNINITIALIZED); + private final List ignorableJsonRpcMethods; + /** * Creates a new server session with the given parameters and the transport to use. * @param id session id @@ -72,6 +75,28 @@ public class McpServerSession implements McpSession { public McpServerSession(String id, Duration requestTimeout, McpServerTransport transport, InitRequestHandler initHandler, InitNotificationHandler initNotificationHandler, Map> requestHandlers, Map notificationHandlers) { + this(id, requestTimeout, transport, initHandler, initNotificationHandler, requestHandlers, notificationHandlers, + List.of()); + } + + /** + * Creates a new server session with the given parameters and the transport to use. + * @param id session id + * @param transport the transport to use + * @param initHandler called when a + * {@link io.modelcontextprotocol.spec.McpSchema.InitializeRequest} is received by the + * server + * @param initNotificationHandler called when a + * {@link io.modelcontextprotocol.spec.McpSchema#METHOD_NOTIFICATION_INITIALIZED} is + * received. + * @param requestHandlers map of request handlers to use + * @param notificationHandlers map of notification handlers to use + * @param ignorableJsonRpcMethods list of JSON-RPC method names that should be ignored + */ + public McpServerSession(String id, Duration requestTimeout, McpServerTransport transport, + InitRequestHandler initHandler, InitNotificationHandler initNotificationHandler, + Map> requestHandlers, Map notificationHandlers, + List ignorableJsonRpcMethods) { this.id = id; this.requestTimeout = requestTimeout; this.transport = transport; @@ -79,6 +104,8 @@ public McpServerSession(String id, Duration requestTimeout, McpServerTransport t this.initNotificationHandler = initNotificationHandler; this.requestHandlers = requestHandlers; this.notificationHandlers = notificationHandlers; + this.ignorableJsonRpcMethods = ignorableJsonRpcMethods != null ? List.copyOf(ignorableJsonRpcMethods) + : List.of(); } /** @@ -200,6 +227,12 @@ else if (message instanceof McpSchema.JSONRPCNotification notification) { */ private Mono handleIncomingRequest(McpSchema.JSONRPCRequest request) { return Mono.defer(() -> { + + if (ignorableJsonRpcMethods.contains(request.method())) { + logger.debug("Ignoring JSON-RPC request: {}", request); + return Mono.empty(); + } + Mono resultMono; if (McpSchema.METHOD_INITIALIZE.equals(request.method())) { // TODO handle situation where already initialized! @@ -248,7 +281,13 @@ private Mono handleIncomingNotification(McpSchema.JSONRPCNotification noti var handler = notificationHandlers.get(notification.method()); if (handler == null) { - logger.error("No handler registered for notification method: {}", notification.method()); + if (ignorableJsonRpcMethods.contains(notification.method())) { + logger.debug("Ignoring JSON-RPC notification: {}", notification); + } + else { + logger.error("No handler registered for notification: {}", notification); + } + return Mono.empty(); } return this.exchangeSink.asMono().flatMap(exchange -> handler.handle(exchange, notification.params())); diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java b/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java index 8e654e59..882ba2f6 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java +++ b/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java @@ -8,6 +8,7 @@ import java.net.URI; import java.util.Collection; +import java.util.List; import java.util.Map; /** @@ -18,6 +19,14 @@ public final class Utils { + /** + * Default list of JSON-RPC methods that can be ignored by the client. These methods + * will not be processed and will not generate errors if received. This includes + * notifications like "notifications/cancelled" and "notifications/stderr". + */ + public static List DEFAULT_IGNORABLE_JSON_RPC_METHODS = List.of("notifications/cancelled", + "notifications/stderr"); + /** * Check whether the given {@code String} contains actual text. *

diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index e912e1dd..63ad2d60 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -831,4 +831,35 @@ void testProgressConsumer() { }); } + // Tests for ignorable JSON-RPC methods feature + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + withClient(createMcpTransport(), builder -> builder.ignorableJsonRpcMethods(customIgnorables), client -> { + StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); + }); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + withClient(createMcpTransport(), + builder -> builder.ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3"), + client -> { + StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); + }); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy(() -> McpClient.async(createMcpTransport()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpClient.async(createMcpTransport()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index c7425506..7d6ac3de 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -696,4 +696,35 @@ void testProgressConsumer() { }); } + // Tests for ignorable JSON-RPC methods feature + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + withClient(createMcpTransport(), builder -> builder.ignorableJsonRpcMethods(customIgnorables), client -> { + assertThatCode(() -> client.initialize()).doesNotThrowAnyException(); + }); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + withClient(createMcpTransport(), + builder -> builder.ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3"), + client -> { + assertThatCode(() -> client.initialize()).doesNotThrowAnyException(); + }); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy(() -> McpClient.sync(createMcpTransport()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpClient.sync(createMcpTransport()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index b5841e75..919160ab 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -523,4 +523,44 @@ void testRootsChangeHandlers() { .doesNotThrowAnyException(); } + // --------------------------------------- + // Ignorable JSON-RPC Methods Tests + // --------------------------------------- + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + var server = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods(customIgnorables) + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + var server = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3") + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy( + () -> McpServer.async(createMcpTransportProvider()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 208d2e74..74ef4cdb 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -493,4 +493,44 @@ void testRootsChangeHandlers() { assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); } + // --------------------------------------- + // Ignorable JSON-RPC Methods Tests + // --------------------------------------- + + @Test + void testIgnorableJsonRpcMethodsBuilderList() { + List customIgnorables = List.of("custom/method1", "custom/method2"); + + var server = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods(customIgnorables) + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + void testIgnorableJsonRpcMethodsBuilderVarargs() { + var server = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .ignorableJsonRpcMethods("custom/method1", "custom/method2", "custom/method3") + .build(); + + assertThat(server).isNotNull(); + assertThatCode(() -> server.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + void testIgnorableMethodsBuilderNullValidation() { + assertThatThrownBy( + () -> McpServer.sync(createMcpTransportProvider()).ignorableJsonRpcMethods((List) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()).ignorableJsonRpcMethods((String[]) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Ignorable JSON-RPC methods must not be null"); + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java index 85dcd26c..a7342265 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java @@ -5,10 +5,13 @@ package io.modelcontextprotocol.spec; import java.time.Duration; +import java.util.List; import java.util.Map; +import java.util.function.Function; import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.MockMcpClientTransport; +import io.modelcontextprotocol.client.McpClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -195,4 +198,43 @@ void testGracefulShutdown() { StepVerifier.create(session.closeGracefully()).verifyComplete(); } + // Tests for ignorable JSON-RPC methods feature + + @Test + void testIgnorableRequestsAreIgnored() { + List ignorableMethods = List.of("ignorable/request"); + transport = new MockMcpClientTransport(); + session = new McpClientSession(TIMEOUT, transport, Map.of(), Map.of(), Function.identity(), ignorableMethods); + + // Simulate incoming ignorable request + McpSchema.JSONRPCRequest ignorableRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, + "ignorable/request", "test-id", null); + transport.simulateIncomingMessage(ignorableRequest); + + // Verify no response was sent (ignored) + assertThat(transport.getLastSentMessage()).isNull(); + } + + @Test + void testIgnorableNotificationsAreIgnored() { + List ignorableMethods = List.of("ignorable/notification"); + transport = new MockMcpClientTransport(); + session = new McpClientSession(TIMEOUT, transport, Map.of(), Map.of(), Function.identity(), ignorableMethods); + + // Simulate incoming ignorable notification + McpSchema.JSONRPCNotification ignorableNotification = new McpSchema.JSONRPCNotification( + McpSchema.JSONRPC_VERSION, "ignorable/notification", Map.of("data", "test")); + transport.simulateIncomingMessage(ignorableNotification); + + assertThat(transport.getLastSentMessage()).isNull(); + } + + @Test + void testIgnorableMethodsConstructorNullValidation() { + assertThatThrownBy( + () -> new McpClientSession(TIMEOUT, transport, Map.of(), Map.of(), Function.identity(), null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ignorableJsonRpcMethods can not be null"); + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpServerSessionTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpServerSessionTests.java new file mode 100644 index 00000000..2a84f5f5 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpServerSessionTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.spec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import io.modelcontextprotocol.util.Utils; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Test suite for {@link McpServerSession} focusing on ignorable JSON-RPC methods + * functionality. + * + * @author Christian Tzolov + */ +class McpServerSessionTests { + + private McpServerTransport mockTransport; + + private McpServerSession.InitRequestHandler mockInitRequestHandler; + + private McpServerSession.InitNotificationHandler mockInitNotificationHandler; + + private Map> requestHandlers; + + private Map notificationHandlers; + + @BeforeEach + void setUp() { + mockTransport = mock(McpServerTransport.class); + mockInitRequestHandler = mock(McpServerSession.InitRequestHandler.class); + mockInitNotificationHandler = mock(McpServerSession.InitNotificationHandler.class); + requestHandlers = Map.of(); + notificationHandlers = Map.of(); + + // Setup default mock behavior + when(mockTransport.sendMessage(any())).thenReturn(Mono.empty()); + when(mockInitRequestHandler.handle(any())) + .thenReturn(Mono.just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, + McpSchema.ServerCapabilities.builder().build(), + new McpSchema.Implementation("test-server", "1.0.0"), null))); + when(mockInitNotificationHandler.handle()).thenReturn(Mono.empty()); + } + + // --------------------------------------- + // Ignorable Request Tests + // --------------------------------------- + + @Test + void testIgnorableRequestIsIgnored() { + List ignorableMethods = List.of("notifications/cancelled"); + + var session = new McpServerSession("test-session", Duration.ofSeconds(10), mockTransport, + mockInitRequestHandler, mockInitNotificationHandler, requestHandlers, notificationHandlers, + ignorableMethods); + + var ignorableRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "notifications/cancelled", + "req-1", null); + + StepVerifier.create(session.handle(ignorableRequest)).verifyComplete(); + + // Verify no response was sent + verify(mockTransport, never()).sendMessage(any()); + } + + @Test + void testNonIgnorableRequestGeneratesError() { + List ignorableMethods = List.of("custom/method"); + + var session = new McpServerSession("test-session", Duration.ofSeconds(10), mockTransport, + mockInitRequestHandler, mockInitNotificationHandler, requestHandlers, notificationHandlers, + ignorableMethods); + + var nonIgnorableRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "unknown/method", "req-1", + null); + + StepVerifier.create(session.handle(nonIgnorableRequest)).verifyComplete(); + + // Verify error response was sent + ArgumentCaptor messageCaptor = ArgumentCaptor + .forClass(McpSchema.JSONRPCMessage.class); + verify(mockTransport).sendMessage(messageCaptor.capture()); + + McpSchema.JSONRPCMessage sentMessage = messageCaptor.getValue(); + assertThat(sentMessage).isInstanceOf(McpSchema.JSONRPCResponse.class); + + McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sentMessage; + assertThat(response.error()).isNotNull(); + assertThat(response.error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND); + assertThat(response.error().message()).contains("Method not found: unknown/method"); + } + + // --------------------------------------- + // Ignorable Notification Tests + // --------------------------------------- + + @Test + void testIgnorableNotificationIsIgnored() { + List ignorableMethods = List.of("notifications/cancelled"); + + var session = new McpServerSession("test-session", Duration.ofSeconds(10), mockTransport, + mockInitRequestHandler, mockInitNotificationHandler, requestHandlers, notificationHandlers, + ignorableMethods); + + var ignorableNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, + "notifications/cancelled", null); + + StepVerifier.create(session.handle(ignorableNotification)).verifyComplete(); + + // Verify no message was sent (notifications don't generate responses anyway) + verify(mockTransport, never()).sendMessage(any()); + } + + // --------------------------------------- + // Default Ignorable Methods Constants Tests + // --------------------------------------- + @Test + void testDefaultIgnorableMethodsConstants() { + List defaults = Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS; + + assertThat(defaults).isNotNull(); + assertThat(defaults).contains("notifications/cancelled"); + assertThat(defaults).contains("notifications/stderr"); + assertThat(defaults).hasSize(2); + } + +}