Skip to content
Merged
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 @@ -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
* <p>
* Note: {@code level} and {@code data} are required by the MCP specification.
Expand All @@ -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<String, Object> meta) {
String data = rawData != null ? rawData.toString() : null;
if (level == null || data == null) {
List<String> missing = new ArrayList<>();
if (level == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,4 +559,31 @@ void testPingMessageRequestHandling() {
asyncMcpClient.closeGracefully();
}

@Test
void testLoggingNotificationWithObjectData() throws IOException {
MockMcpClientTransport transport = initializationEnabledTransport();

List<McpSchema.LoggingMessageNotification> 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
Loading