Skip to content

Commit ece8a7b

Browse files
committed
Allow registering logging consumers after client initialization
The logging notification handler already iterates the consumer list on every notification, so consumers can be added after build without reinstalling the handler. Switch the list to CopyOnWriteArrayList, since notifications are delivered on transport threads, and expose addLoggingConsumer on both the async and sync clients. This lets callers inspect getServerCapabilities() and only attach a consumer when the server actually advertises logging support. Fixes #938 Signed-off-by: Armen Petrosyan <liotur.mail@gmail.com>
1 parent e6c532e commit ece8a7b

3 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.List;
1414
import java.util.Map;
1515
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.concurrent.CopyOnWriteArrayList;
1617
import java.util.function.Function;
1718

1819
import io.modelcontextprotocol.client.LifecycleInitializer.Initialization;
@@ -185,6 +186,13 @@ public class McpAsyncClient {
185186

186187
private final boolean applyElicitationDefaults;
187188

189+
/**
190+
* Consumers notified for every logging message received from the server. Held as a
191+
* live reference so that consumers registered after the client was built are picked
192+
* up by the already installed notification handler.
193+
*/
194+
private final List<Function<LoggingMessageNotification, Mono<Void>>> loggingConsumers;
195+
188196
/**
189197
* Create a new McpAsyncClient with the given transport and session request-response
190198
* timeout.
@@ -304,11 +312,12 @@ public class McpAsyncClient {
304312
asyncPromptsChangeNotificationHandler(promptsChangeConsumersFinal));
305313

306314
// Utility Logging Notification
307-
List<Function<LoggingMessageNotification, Mono<Void>>> loggingConsumersFinal = new ArrayList<>();
315+
List<Function<LoggingMessageNotification, Mono<Void>>> loggingConsumersFinal = new CopyOnWriteArrayList<>();
308316
loggingConsumersFinal.add((notification) -> Mono.fromRunnable(() -> logger.debug("Logging: {}", notification)));
309317
if (!Utils.isEmpty(features.loggingConsumers())) {
310318
loggingConsumersFinal.addAll(features.loggingConsumers());
311319
}
320+
this.loggingConsumers = loggingConsumersFinal;
312321
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
313322
asyncLoggingNotificationHandler(loggingConsumersFinal));
314323

@@ -1103,6 +1112,19 @@ private NotificationHandler asyncLoggingNotificationHandler(
11031112
};
11041113
}
11051114

1115+
/**
1116+
* Registers an additional consumer for logging messages received from the server.
1117+
* <p>
1118+
* Unlike {@link McpClient.AsyncSpec#loggingConsumer(Function)}, this can be called
1119+
* after the client has been built, so a consumer may be registered only once
1120+
* {@link #getServerCapabilities()} shows the server supports logging.
1121+
* @param loggingConsumer A consumer that receives logging messages. Must not be null.
1122+
*/
1123+
public void addLoggingConsumer(Function<LoggingMessageNotification, Mono<Void>> loggingConsumer) {
1124+
Assert.notNull(loggingConsumer, "Logging consumer must not be null");
1125+
this.loggingConsumers.add(loggingConsumer);
1126+
}
1127+
11061128
/**
11071129
* Sets the minimum logging level for messages received from the server. The client
11081130
* will only receive log messages at or above the specified severity level.

‎mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.time.Duration;
88
import java.util.Map;
9+
import java.util.function.Consumer;
910
import java.util.function.Supplier;
1011

1112
import org.slf4j.Logger;
@@ -19,6 +20,7 @@
1920
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
2021
import io.modelcontextprotocol.util.Assert;
2122
import reactor.core.publisher.Mono;
23+
import reactor.core.scheduler.Schedulers;
2224

2325
/**
2426
* A synchronous client implementation for the Model Context Protocol (MCP) that wraps an
@@ -423,6 +425,20 @@ public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) {
423425
return withProvidedContext(this.delegate.getPrompt(getPromptRequest)).block();
424426
}
425427

428+
/**
429+
* Registers an additional consumer for logging messages received from the server.
430+
* <p>
431+
* Unlike {@link McpClient.SyncSpec#loggingConsumer(Consumer)}, this can be called
432+
* after the client has been built, so a consumer may be registered only once
433+
* {@link #getServerCapabilities()} reports that the server supports logging.
434+
* @param loggingConsumer the consumer that receives logging messages
435+
*/
436+
public void addLoggingConsumer(Consumer<McpSchema.LoggingMessageNotification> loggingConsumer) {
437+
Assert.notNull(loggingConsumer, "Logging consumer must not be null");
438+
this.delegate.addLoggingConsumer(
439+
l -> Mono.<Void>fromRunnable(() -> loggingConsumer.accept(l)).subscribeOn(Schedulers.boundedElastic()));
440+
}
441+
426442
/**
427443
* Client can set the minimum logging level it wants to receive from the server.
428444
* @param loggingLevel the min logging level

‎mcp-core/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTest.java‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,31 @@ void bothHandlersPresent() {
106106

107107
}
108108

109+
@Nested
110+
class LoggingConsumers {
111+
112+
@Test
113+
void addLoggingConsumerAfterBuild() {
114+
McpClientTransport transport = mock(McpClientTransport.class);
115+
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
116+
var client = McpClient.async(transport).jsonSchemaValidator(mock(JsonSchemaValidator.class)).build();
117+
118+
assertThatCode(() -> client.addLoggingConsumer(notification -> Mono.empty()))
119+
.doesNotThrowAnyException();
120+
}
121+
122+
@Test
123+
void addLoggingConsumerRejectsNull() {
124+
McpClientTransport transport = mock(McpClientTransport.class);
125+
when(transport.protocolVersions()).thenReturn(List.of("2024-11-05"));
126+
var client = McpClient.async(transport).jsonSchemaValidator(mock(JsonSchemaValidator.class)).build();
127+
128+
assertThatThrownBy(() -> client.addLoggingConsumer(null)).isInstanceOf(IllegalArgumentException.class)
129+
.hasMessage("Logging consumer must not be null");
130+
}
131+
132+
}
133+
109134
@Nested
110135
class ClientCapabilities {
111136

0 commit comments

Comments
 (0)