Skip to content

Commit ab9ba54

Browse files
authored
Fix: Handle logging/setLevel request for server (#358)
Fix #293 Also fixed a bug with initialization: properties must be declared before the init block; otherwise, the initialization order is violated and an NPE will occur in the init block. ## Motivation and Context After initialization, MCP clients send a `logging/setLevel` request if the logging capability is enabled. This request was previously not handled, leading to an error that blocked the use of the MCP server in the inspector and other clients. ## How Has This Been Tested? Added integration test ## Breaking Changes None ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed
1 parent a75e7a8 commit ab9ba54

File tree

3 files changed

+146
-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

+146
-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
@@ -27,6 +28,8 @@ import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS
2728
import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification
2829
import io.modelcontextprotocol.kotlin.sdk.shared.Protocol
2930
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
31+
import kotlinx.atomicfu.AtomicRef
32+
import kotlinx.atomicfu.atomic
3033
import kotlinx.coroutines.CompletableDeferred
3134
import kotlinx.serialization.json.JsonObject
3235

@@ -43,22 +46,6 @@ public open class ServerSession(
4346
@Suppress("ktlint:standard:backing-property-naming")
4447
private var _onClose: () -> Unit = {}
4548

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-
6249
/**
6350
* The client's reported capabilities after initialization.
6451
*/
@@ -71,6 +58,37 @@ public open class ServerSession(
7158
public var clientVersion: Implementation? = null
7259
private set
7360

61+
/**
62+
* The capabilities supported by the server, related to the session.
63+
*/
64+
private val serverCapabilities = options.capabilities
65+
66+
/**
67+
* The current logging level set by the client.
68+
* When null, all messages are sent (no filtering).
69+
*/
70+
private val currentLoggingLevel: AtomicRef<LoggingLevel?> = atomic(null)
71+
72+
init {
73+
// Core protocol handlers
74+
setRequestHandler<InitializeRequest>(Defined.Initialize) { request, _ ->
75+
handleInitialize(request)
76+
}
77+
setNotificationHandler<InitializedNotification>(Defined.NotificationsInitialized) {
78+
_onInitialized()
79+
CompletableDeferred(Unit)
80+
}
81+
82+
// Logging level handler
83+
if (options.capabilities.logging != null) {
84+
setRequestHandler<LoggingMessageNotification.SetLevelRequest>(Defined.LoggingSetLevel) { request, _ ->
85+
currentLoggingLevel.value = request.level
86+
logger.debug { "Logging level set to: ${request.level}" }
87+
EmptyRequestResult()
88+
}
89+
}
90+
}
91+
7492
/**
7593
* Registers a callback to be invoked when the server has completed initialization.
7694
*/
@@ -160,12 +178,20 @@ public open class ServerSession(
160178

161179
/**
162180
* Sends a logging message notification to the client.
181+
* Messages are filtered based on the current logging level set by the client.
182+
* If no logging level is set, all messages are sent.
163183
*
164184
* @param notification The logging message notification.
165185
*/
166186
public suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
167-
logger.trace { "Sending logging message: ${notification.params.data}" }
168-
notification(notification)
187+
if (serverCapabilities.logging != null) {
188+
if (isMessageAccepted(notification.params.level)) {
189+
logger.trace { "Sending logging message: ${notification.params.data}" }
190+
notification(notification)
191+
} else {
192+
logger.trace { "Filtering out logging message with level ${notification.params.level}" }
193+
}
194+
}
169195
}
170196

171197
/**
@@ -318,6 +344,7 @@ public open class ServerSession(
318344

319345
Defined.LoggingSetLevel -> {
320346
if (serverCapabilities.logging == null) {
347+
logger.error { "Server does not support logging (required for $method)" }
321348
throw IllegalStateException("Server does not support logging (required for $method)")
322349
}
323350
}
@@ -381,4 +408,24 @@ public open class ServerSession(
381408
instructions = instructions,
382409
)
383410
}
411+
412+
/**
413+
* Checks if a message with the given level should be ignored based on the current logging level.
414+
*
415+
* @param level The level of the message to check.
416+
* @return true if the message should be ignored (filtered out), false otherwise.
417+
*/
418+
private fun isMessageIgnored(level: LoggingLevel): Boolean {
419+
val current = currentLoggingLevel.value ?: return false // If no level is set, don't filter
420+
421+
return level.ordinal < current.ordinal
422+
}
423+
424+
/**
425+
* Checks if a message with the given level should be accepted based on the current logging level.
426+
*
427+
* @param level The level of the message to check.
428+
* @return true if the message should be accepted (not filtered out), false otherwise.
429+
*/
430+
private fun isMessageAccepted(level: LoggingLevel): Boolean = !isMessageIgnored(level)
384431
}

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,86 @@ 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 minLevel = LoggingLevel.warning
932+
val result = client.setLoggingLevel(minLevel)
933+
assertEquals(EmptyJsonObject, result._meta)
934+
935+
// Send messages of different levels
936+
val testMessages = listOf(
937+
LoggingLevel.debug to "Debug - should be filtered",
938+
LoggingLevel.info to "Info - should be filtered",
939+
LoggingLevel.warning to "Warning - should pass",
940+
LoggingLevel.error to "Error - should pass",
941+
)
942+
943+
testMessages.forEach { (level, message) ->
944+
serverSession.sendLoggingMessage(
945+
LoggingMessageNotification(
946+
params = LoggingMessageNotification.Params(
947+
level = level,
948+
data = buildJsonObject { put("message", message) },
949+
),
950+
),
951+
)
952+
}
953+
954+
delay(100)
955+
956+
// Only warning and error should be received
957+
assertEquals(2, receivedMessages.size, "Should receive only 2 messages (warning and error)")
958+
959+
// Verify all received messages have severity >= minLevel
960+
receivedMessages.forEach { message ->
961+
val messageSeverity = message.params.level.ordinal
962+
assertTrue(
963+
messageSeverity >= minLevel.ordinal,
964+
"Received message with level ${message.params.level} should have severity >= $minLevel",
965+
)
966+
}
967+
}
968+
889969
@Test
890970
fun `should handle server elicitation`() = runTest {
891971
val client = Client(

0 commit comments

Comments
 (0)