Skip to content

feat: add ignorable JSON-RPC methods support for both client and server #417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -830,4 +830,35 @@ void testProgressConsumer() {
});
}

// Tests for ignorable JSON-RPC methods feature

@Test
void testIgnorableJsonRpcMethodsBuilderList() {
List<String> 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<String>) 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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -695,4 +695,35 @@ void testProgressConsumer() {
});
}

// Tests for ignorable JSON-RPC methods feature

@Test
void testIgnorableJsonRpcMethodsBuilderList() {
List<String> 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<String>) 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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -522,4 +522,44 @@ void testRootsChangeHandlers() {
.doesNotThrowAnyException();
}

// ---------------------------------------
// Ignorable JSON-RPC Methods Tests
// ---------------------------------------

@Test
void testIgnorableJsonRpcMethodsBuilderList() {
List<String> 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<String>) 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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -492,4 +492,44 @@ void testRootsChangeHandlers() {
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
}

// ---------------------------------------
// Ignorable JSON-RPC Methods Tests
// ---------------------------------------

@Test
void testIgnorableJsonRpcMethodsBuilderList() {
List<String> 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<String>) 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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> ignorableJsonRpcMethods) {

Assert.notNull(transport, "Transport must not be null");
Assert.notNull(requestTimeout, "Request timeout must not be null");
Expand Down Expand Up @@ -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);
}

Expand Down
80 changes: 77 additions & 3 deletions mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<String> ignorableJsonRpcMethods = new ArrayList<>(Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS);

private ClientCapabilities capabilities;

private Implementation clientInfo = new Implementation("Java SDK MCP Client", "1.0.0");
Expand Down Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -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));
}

}
Expand Down Expand Up @@ -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<String> ignorableJsonRpcMethods = new ArrayList<>(Utils.DEFAULT_IGNORABLE_JSON_RPC_METHODS);

private ClientCapabilities capabilities;

private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
Expand Down Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -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);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ public class McpAsyncServer {

private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();

private final List<String> ignorableJsonRpcMethods;

/**
* Create a new McpAsyncServer with the given transport provider and capabilities.
* @param mcpTransportProvider The transport layer implementation for MCP
Expand All @@ -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<String> ignorableJsonRpcMethods) {
this.mcpTransportProvider = mcpTransportProvider;
this.objectMapper = objectMapper;
this.serverInfo = features.serverInfo();
Expand All @@ -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<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();

Expand Down Expand Up @@ -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));
}

// ---------------------------------------
Expand Down
Loading
Loading