From a7f1d834e370f8dc660461088703e7e7a096917d Mon Sep 17 00:00:00 2001 From: ningyixi Date: Thu, 15 Oct 2026 04:24:55 +0800 Subject: [PATCH] Fix LoggingMessageNotification deserialization for non-string data The MCP spec allows any JSON value in notifications/message params.data, but the Java SDK typed data as String. FastMCP (and similar servers) send objects such as {"msg":"...","extra":null}, which caused Jackson to fail with MismatchedInputException and drop the log notification. Accept Object in the @JsonCreator and coerce non-string values to JSON text so the public String API is preserved. Fixes #1180. --- .../modelcontextprotocol/spec/McpSchema.java | 36 ++++++++++--- .../spec/McpSchemaTests.java | 53 +++++++++++++++++++ 2 files changed, 83 insertions(+), 6 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..bd513d79e 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.util.Assert; @@ -5355,7 +5356,8 @@ 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 Logging data as text. The wire format may carry any JSON value; on + * deserialization non-string values are coerced to their JSON text form. * @param meta See specification for notes on _meta usage *

* Note: {@code level} and {@code data} are required by the MCP specification. @@ -5375,24 +5377,46 @@ public record LoggingMessageNotification( // @formatter:off Assert.notNull(data, "data must not be null"); } + /** + * Coerce notification {@code data} to a string while keeping the public API as + * {@link String}. The MCP spec allows any JSON value; servers such as FastMCP + * send objects (for example {@code {"msg":"...","extra":null}}). Non-string + * values are serialized to JSON text so deserialization does not fail. + */ + private static String coerceDataToString(Object data) { + if (data == null) { + return null; + } + if (data instanceof String stringData) { + return stringData; + } + try { + return McpJsonDefaults.getMapper().writeValueAsString(data); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to serialize logging notification data to JSON text", ex); + } + } + @JsonCreator static LoggingMessageNotification fromJson(@JsonProperty("level") LoggingLevel level, - @JsonProperty("logger") String loggerName, @JsonProperty("data") String data, + @JsonProperty("logger") String loggerName, @JsonProperty("data") Object data, @JsonProperty("_meta") Map meta) { - if (level == null || data == null) { + String dataText = coerceDataToString(data); + if (level == null || dataText == null) { List missing = new ArrayList<>(); if (level == null) { missing.add("level -> INFO"); level = LoggingLevel.INFO; } - if (data == null) { + if (dataText == null) { missing.add("data -> ''"); - data = ""; + dataText = ""; } McpSchema.logger.warn("LoggingMessageNotification: missing required fields during deserialization: {}", String.join(", ", missing)); } - return new LoggingMessageNotification(level, loggerName, data, meta); + return new LoggingMessageNotification(level, loggerName, dataText, meta); } // backwards compatibility constructor 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..ed4912add 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -2729,6 +2729,59 @@ void testLoggingMessageNotificationDeserializationWithMissingRequiredFields() th assertThat(notification.data()).isEmpty(); } + @Test + void testLoggingMessageNotificationDeserializationWithObjectData() throws Exception { + // FastMCP ctx.info()/ctx.log() sends data as an object; the MCP spec allows any + // JSON + // value. Keep the public API as String by coercing non-string data to JSON text. + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue(""" + {"level":"info","logger":"server","data":{"msg":"working","extra":null}}""", + McpSchema.LoggingMessageNotification.class); + + assertThat(notification).isNotNull(); + assertThat(notification.level()).isEqualTo(McpSchema.LoggingLevel.INFO); + assertThat(notification.logger()).isEqualTo("server"); + assertThatJson(notification.data()).isObject().containsEntry("msg", "working").containsEntry("extra", null); + } + + @Test + void testLoggingMessageNotificationDeserializationWithStringDataStillWorks() throws Exception { + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue(""" + {"level":"warning","logger":"my-logger","data":"plain text"}""", + McpSchema.LoggingMessageNotification.class); + + assertThat(notification.data()).isEqualTo("plain text"); + assertThat(notification.level()).isEqualTo(McpSchema.LoggingLevel.WARNING); + } + + @Test + void testLoggingMessageNotificationDeserializationWithArrayAndNumberData() throws Exception { + McpSchema.LoggingMessageNotification arrayNotification = JSON_MAPPER.readValue(""" + {"level":"debug","data":[1,"two",true]}""", McpSchema.LoggingMessageNotification.class); + assertThatJson(arrayNotification.data()).isArray().containsExactly(1, "two", true); + + McpSchema.LoggingMessageNotification numberNotification = JSON_MAPPER.readValue(""" + {"level":"debug","data":42}""", McpSchema.LoggingMessageNotification.class); + assertThat(numberNotification.data()).isEqualTo("42"); + + McpSchema.LoggingMessageNotification boolNotification = JSON_MAPPER.readValue(""" + {"level":"debug","data":true}""", McpSchema.LoggingMessageNotification.class); + assertThat(boolNotification.data()).isEqualTo("true"); + } + + @Test + void testLoggingMessageNotificationConvertValueWithObjectData() { + Map params = new HashMap<>(); + params.put("level", "info"); + params.put("logger", "server"); + params.put("data", Map.of("msg", "working")); + + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.convertValue(params, + McpSchema.LoggingMessageNotification.class); + + assertThatJson(notification.data()).isObject().containsEntry("msg", "working"); + } + // --- Icon tests (SEP-973) --- @Test