From bdc833326dea6a77d4c6184c8c98b7f36c2e2ddb Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 20 Oct 2026 14:47:27 +0200 Subject: [PATCH] LoggingMessageNotification#data accepts non-String payload Fixes #1180 Signed-off-by: Daniel Garnier-Moiroux --- .../modelcontextprotocol/spec/McpSchema.java | 8 ++- .../McpAsyncClientResponseHandlerTests.java | 27 +++++++++ .../spec/McpSchemaTests.java | 55 +++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index b088396e4..fa467b213 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -5355,7 +5355,10 @@ static ResourcesUpdatedNotification fromJson(@JsonProperty("uri") String uri, * * @param level The severity levels. The minimum log level is set by the client. * @param logger The logger that generated the message. - * @param data JSON-serializable logging data. + * @param data The logging data, as a string. The MCP specification allows any JSON + * value; when a non-string value is received, it is converted with + * {@link Object#toString()}. The resulting format is intended for human readers and + * is not guaranteed to be valid JSON. * @param meta See specification for notes on _meta usage *

* Note: {@code level} and {@code data} are required by the MCP specification. @@ -5377,8 +5380,9 @@ public record LoggingMessageNotification( // @formatter:off @JsonCreator static LoggingMessageNotification fromJson(@JsonProperty("level") LoggingLevel level, - @JsonProperty("logger") String loggerName, @JsonProperty("data") String data, + @JsonProperty("logger") String loggerName, @JsonProperty("data") Object rawData, @JsonProperty("_meta") Map meta) { + String data = rawData != null ? rawData.toString() : null; if (level == null || data == null) { List missing = new ArrayList<>(); if (level == null) { diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index 2f01bb06e..d89171b9f 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -559,4 +559,31 @@ void testPingMessageRequestHandling() { asyncMcpClient.closeGracefully(); } + @Test + void testLoggingNotificationWithObjectData() throws IOException { + MockMcpClientTransport transport = initializationEnabledTransport(); + + List receivedNotifications = new ArrayList<>(); + + McpAsyncClient asyncMcpClient = McpClient.async(transport) + .loggingConsumer(notification -> Mono.fromRunnable(() -> receivedNotifications.add(notification))) + .build(); + + assertThat(asyncMcpClient.initialize().block()).isNotNull(); + + // Logging notification as sent by FastMCP servers, with an object as data + McpSchema.JSONRPCMessage notification = McpSchema.deserializeJsonRpcMessage(JSON_MAPPER, """ + {"jsonrpc":"2.0","method":"notifications/message", + "params":{"level":"info","logger":"server","data":{"msg":"working","extra":null}}}"""); + transport.simulateIncomingMessage(notification); + + assertThat(receivedNotifications).singleElement().satisfies(received -> { + assertThat(received.level()).isEqualTo(McpSchema.LoggingLevel.INFO); + assertThat(received.logger()).isEqualTo("server"); + assertThat(received.data()).isEqualTo("{msg=working, extra=null}"); + }); + + asyncMcpClient.closeGracefully(); + } + } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index ab9bc8643..a9cddf41b 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -17,6 +17,7 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; @@ -2729,6 +2730,60 @@ void testLoggingMessageNotificationDeserializationWithMissingRequiredFields() th assertThat(notification.data()).isEmpty(); } + @Test + void testLoggingMessageNotificationDeserializationWithNullData() throws Exception { + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue(""" + {"level":"info","data":null}""", McpSchema.LoggingMessageNotification.class); + + assertThat(notification.data()).isEmpty(); + } + + @Test + void testLoggingMessageNotificationDeserializationWithStringData() throws Exception { + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue(""" + {"level":"warning","logger":"my-logger","data":"plain text"}""", + McpSchema.LoggingMessageNotification.class); + + assertThat(notification.level()).isEqualTo(McpSchema.LoggingLevel.WARNING); + assertThat(notification.logger()).isEqualTo("my-logger"); + assertThat(notification.data()).isEqualTo("plain text"); + } + + @Test + void testLoggingMessageNotificationDeserializationWithObjectData() throws Exception { + // FastMCP sends {"msg": ..., "extra": ...} for every ctx.log() call + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue(""" + { + "level": "info", + "logger": "server", + "data": { + "msg": "working", + "extra": { + "user": "bob", + "ids": [1, 2 ] + } + } + }""", McpSchema.LoggingMessageNotification.class); + + assertThat(notification.level()).isEqualTo(McpSchema.LoggingLevel.INFO); + assertThat(notification.logger()).isEqualTo("server"); + assertThat(notification.data()).isEqualTo("{msg=working, extra={user=bob, ids=[1, 2]}}"); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', textBlock = """ + [1,"two",true] | [1, two, true] + 42 | 42 + 1.5 | 1.5 + true | true + """) + void testLoggingMessageNotificationDeserializationWithNonStringData(String data, String expected) throws Exception { + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue(""" + {"level":"debug","data":%s}""".formatted(data), McpSchema.LoggingMessageNotification.class); + + assertThat(notification.data()).isEqualTo(expected); + } + // --- Icon tests (SEP-973) --- @Test