Skip to content

Commit a5dd29e

Browse files
committed
Fix: Handle logging/setLevel request for server
1 parent 458a49a commit a5dd29e

File tree

3 files changed

+156
-19
lines changed
  • kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client
  • kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server
  • kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client

3 files changed

+156
-19
lines changed

kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
315315
* @throws IllegalStateException If the server does not support logging.
316316
*/
317317
public suspend fun setLoggingLevel(level: LoggingLevel, options: RequestOptions? = null): EmptyRequestResult =
318-
request<EmptyRequestResult>(SetLevelRequest(level), options)
318+
request(SetLevelRequest(level), options)
319319

320320
/**
321321
* Retrieves a prompt by name from the server.

kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
1616
import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION
1717
import io.modelcontextprotocol.kotlin.sdk.ListRootsRequest
1818
import io.modelcontextprotocol.kotlin.sdk.ListRootsResult
19+
import io.modelcontextprotocol.kotlin.sdk.LoggingLevel
1920
import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification
2021
import io.modelcontextprotocol.kotlin.sdk.Method
2122
import io.modelcontextprotocol.kotlin.sdk.Method.Defined
@@ -43,22 +44,6 @@ public open class ServerSession(
4344
@Suppress("ktlint:standard:backing-property-naming")
4445
private var _onClose: () -> Unit = {}
4546

46-
init {
47-
// Core protocol handlers
48-
setRequestHandler<InitializeRequest>(Method.Defined.Initialize) { request, _ ->
49-
handleInitialize(request)
50-
}
51-
setNotificationHandler<InitializedNotification>(Method.Defined.NotificationsInitialized) {
52-
_onInitialized()
53-
CompletableDeferred(Unit)
54-
}
55-
}
56-
57-
/**
58-
* The capabilities supported by the server, related to the session.
59-
*/
60-
private val serverCapabilities = options.capabilities
61-
6247
/**
6348
* The client's reported capabilities after initialization.
6449
*/
@@ -71,6 +56,44 @@ public open class ServerSession(
7156
public var clientVersion: Implementation? = null
7257
private set
7358

59+
/**
60+
* The capabilities supported by the server, related to the session.
61+
*/
62+
private val serverCapabilities = options.capabilities
63+
64+
/**
65+
* The current logging level set by the client.
66+
* When null, all messages are sent (no filtering).
67+
*/
68+
private var currentLoggingLevel: LoggingLevel? = null
69+
70+
/**
71+
* Map of LoggingLevel to severity index for comparison.
72+
* Higher index means higher severity.
73+
*/
74+
private val loggingLevelSeverity: Map<LoggingLevel, Int> = LoggingLevel.entries.withIndex()
75+
.associate { (index, level) -> level to index }
76+
77+
init {
78+
// Core protocol handlers
79+
setRequestHandler<InitializeRequest>(Defined.Initialize) { request, _ ->
80+
handleInitialize(request)
81+
}
82+
setNotificationHandler<InitializedNotification>(Defined.NotificationsInitialized) {
83+
_onInitialized()
84+
CompletableDeferred(Unit)
85+
}
86+
87+
// Logging level handler
88+
if (options.capabilities.logging != null) {
89+
setRequestHandler<LoggingMessageNotification.SetLevelRequest>(Defined.LoggingSetLevel) { request, _ ->
90+
currentLoggingLevel = request.level
91+
logger.debug { "Logging level set to: ${request.level}" }
92+
EmptyRequestResult()
93+
}
94+
}
95+
}
96+
7497
/**
7598
* Registers a callback to be invoked when the server has completed initialization.
7699
*/
@@ -152,12 +175,20 @@ public open class ServerSession(
152175

153176
/**
154177
* Sends a logging message notification to the client.
178+
* Messages are filtered based on the current logging level set by the client.
179+
* If no logging level is set, all messages are sent.
155180
*
156181
* @param notification The logging message notification.
157182
*/
158183
public suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
159-
logger.trace { "Sending logging message: ${notification.params.data}" }
160-
notification(notification)
184+
if (serverCapabilities.logging != null) {
185+
if (!isMessageIgnored(notification.params.level)) {
186+
logger.trace { "Sending logging message: ${notification.params.data}" }
187+
notification(notification)
188+
} else {
189+
logger.trace { "Filtering out logging message with level ${notification.params.level}" }
190+
}
191+
}
161192
}
162193

163194
/**
@@ -310,6 +341,7 @@ public open class ServerSession(
310341

311342
Defined.LoggingSetLevel -> {
312343
if (serverCapabilities.logging == null) {
344+
logger.error { "Server does not support logging (required for $method)" }
313345
throw IllegalStateException("Server does not support logging (required for $method)")
314346
}
315347
}
@@ -373,4 +405,19 @@ public open class ServerSession(
373405
instructions = instructions,
374406
)
375407
}
408+
409+
/**
410+
* Checks if a message with the given level should be ignored based on the current logging level.
411+
*
412+
* @param level The level of the message to check.
413+
* @return true if the message should be ignored (filtered out), false otherwise.
414+
*/
415+
private fun isMessageIgnored(level: LoggingLevel): Boolean {
416+
val current = currentLoggingLevel ?: return false // If no level is set, don't filter
417+
418+
val messageSeverity = loggingLevelSeverity[level] ?: return false
419+
val currentSeverity = loggingLevelSeverity[current] ?: return false
420+
421+
return messageSeverity < currentSeverity
422+
}
376423
}

kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,96 @@ class ClientTest {
886886
)
887887
}
888888

889+
@Test
890+
fun `should handle logging setLevel request`() = runTest {
891+
val server = Server(
892+
Implementation(name = "test server", version = "1.0"),
893+
ServerOptions(
894+
capabilities = ServerCapabilities(
895+
logging = EmptyJsonObject,
896+
),
897+
),
898+
)
899+
900+
val client = Client(
901+
clientInfo = Implementation(name = "test client", version = "1.0"),
902+
options = ClientOptions(
903+
capabilities = ClientCapabilities(),
904+
),
905+
)
906+
907+
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
908+
909+
val receivedMessages = mutableListOf<LoggingMessageNotification>()
910+
client.setNotificationHandler<LoggingMessageNotification>(Method.Defined.NotificationsMessage) { notification ->
911+
receivedMessages.add(notification)
912+
CompletableDeferred(Unit)
913+
}
914+
915+
val serverSessionResult = CompletableDeferred<ServerSession>()
916+
917+
listOf(
918+
launch {
919+
client.connect(clientTransport)
920+
println("Client connected")
921+
},
922+
launch {
923+
serverSessionResult.complete(server.connect(serverTransport))
924+
println("Server connected")
925+
},
926+
).joinAll()
927+
928+
val serverSession = serverSessionResult.await()
929+
930+
// Set logging level to warning
931+
val result = client.setLoggingLevel(LoggingLevel.warning)
932+
assertEquals(EmptyJsonObject, result._meta)
933+
934+
// Send messages of different levels
935+
serverSession.sendLoggingMessage(
936+
LoggingMessageNotification(
937+
params = LoggingMessageNotification.Params(
938+
level = LoggingLevel.debug,
939+
data = buildJsonObject { put("message", "Debug - should be filtered") },
940+
),
941+
),
942+
)
943+
944+
serverSession.sendLoggingMessage(
945+
LoggingMessageNotification(
946+
params = LoggingMessageNotification.Params(
947+
level = LoggingLevel.info,
948+
data = buildJsonObject { put("message", "Info - should be filtered") },
949+
),
950+
),
951+
)
952+
953+
serverSession.sendLoggingMessage(
954+
LoggingMessageNotification(
955+
params = LoggingMessageNotification.Params(
956+
level = LoggingLevel.warning,
957+
data = buildJsonObject { put("message", "Warning - should pass") },
958+
),
959+
),
960+
)
961+
962+
serverSession.sendLoggingMessage(
963+
LoggingMessageNotification(
964+
params = LoggingMessageNotification.Params(
965+
level = LoggingLevel.error,
966+
data = buildJsonObject { put("message", "Error - should pass") },
967+
),
968+
),
969+
)
970+
971+
// Only warning and error should be received
972+
assertEquals(2, receivedMessages.size, "Should receive only 2 messages (warning and error)")
973+
974+
val levels = receivedMessages.map { it.params.level }
975+
assertTrue(levels.contains(LoggingLevel.warning), "Should receive warning message")
976+
assertTrue(levels.contains(LoggingLevel.error), "Should receive error message")
977+
}
978+
889979
@Test
890980
fun `should handle server elicitation`() = runTest {
891981
val client = Client(

0 commit comments

Comments
 (0)