diff --git a/bridged/src/main/java/dev/ltms/bridged/logging/McpCancelledNotificationFilter.java b/bridged/src/main/java/dev/ltms/bridged/logging/McpCancelledNotificationFilter.java new file mode 100644 index 0000000..d6f0463 --- /dev/null +++ b/bridged/src/main/java/dev/ltms/bridged/logging/McpCancelledNotificationFilter.java @@ -0,0 +1,30 @@ +package dev.ltms.bridged.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.turbo.TurboFilter; +import ch.qos.logback.core.spi.FilterReply; +import io.modelcontextprotocol.spec.McpSchema; +import org.slf4j.Marker; + +/** Suppresses only the SDK warning for the normal MCP cancellation notification. */ +public final class McpCancelledNotificationFilter extends TurboFilter { + + static final String LOGGER = "io.modelcontextprotocol.spec.McpStreamableServerSession"; + static final String UNHANDLED_NOTIFICATION = "No handler registered for notification method: {}"; + + @Override + public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, + Throwable throwable) { + if (level == Level.WARN + && LOGGER.equals(logger.getName()) + && UNHANDLED_NOTIFICATION.equals(format) + && params != null + && params.length == 1 + && params[0] instanceof McpSchema.JSONRPCNotification notification + && "notifications/cancelled".equals(notification.method())) { + return FilterReply.DENY; + } + return FilterReply.NEUTRAL; + } +} diff --git a/bridged/src/main/resources/logback.xml b/bridged/src/main/resources/logback.xml index afa8ab5..beda6f1 100644 --- a/bridged/src/main/resources/logback.xml +++ b/bridged/src/main/resources/logback.xml @@ -1,4 +1,13 @@ + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{28} - %msg%n diff --git a/bridged/src/test/java/dev/ltms/bridged/logging/McpCancelledNotificationFilterTest.java b/bridged/src/test/java/dev/ltms/bridged/logging/McpCancelledNotificationFilterTest.java new file mode 100644 index 0000000..ef205bc --- /dev/null +++ b/bridged/src/test/java/dev/ltms/bridged/logging/McpCancelledNotificationFilterTest.java @@ -0,0 +1,37 @@ +package dev.ltms.bridged.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class McpCancelledNotificationFilterTest { + + @Test + void suppressesOnlyTheCancelledNotificationWarning() { + Logger logger = (Logger) LoggerFactory.getLogger(McpCancelledNotificationFilter.LOGGER); + ListAppender appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + try { + logger.warn(McpCancelledNotificationFilter.UNHANDLED_NOTIFICATION, + new McpSchema.JSONRPCNotification("notifications/cancelled", Map.of("requestId", 7))); + logger.warn(McpCancelledNotificationFilter.UNHANDLED_NOTIFICATION, + new McpSchema.JSONRPCNotification("notifications/progress", Map.of("progress", 1))); + + assertEquals(1, appender.list.size()); + assertEquals(Level.WARN, appender.list.getFirst().getLevel()); + assertTrue(appender.list.getFirst().getFormattedMessage().contains("notifications/progress")); + } finally { + logger.detachAppender(appender); + } + } +} diff --git a/bridged/src/test/resources/logback-test.xml b/bridged/src/test/resources/logback-test.xml index 5ab6fee..85f49d4 100644 --- a/bridged/src/test/resources/logback-test.xml +++ b/bridged/src/test/resources/logback-test.xml @@ -16,6 +16,9 @@ AuditLogTest, which attaches its own ListAppender and asserts on emitted records. --> + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n @@ -34,4 +37,4 @@ - \ No newline at end of file +