Skip to content

Commit be64bba

Browse files
committed
Fix: Handle logging/setLevel request for server
1 parent ee341df commit be64bba

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
*/
@@ -160,12 +183,20 @@ public open class ServerSession(
160183

161184
/**
162185
* Sends a logging message notification to the client.
186+
* Messages are filtered based on the current logging level set by the client.
187+
* If no logging level is set, all messages are sent.
163188
*
164189
* @param notification The logging message notification.
165190
*/
166191
public suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
167-
logger.trace { "Sending logging message: ${notification.params.data}" }
168-
notification(notification)
192+
if (serverCapabilities.logging != null) {
193+
if (!isMessageIgnored(notification.params.level)) {
194+
logger.trace { "Sending logging message: ${notification.params.data}" }
195+
notification(notification)
196+
} else {
197+
logger.trace { "Filtering out logging message with level ${notification.params.level}" }
198+
}
199+
}
169200
}
170201

171202
/**
@@ -318,6 +349,7 @@ public open class ServerSession(
318349

319350
Defined.LoggingSetLevel -> {
320351
if (serverCapabilities.logging == null) {
352+
logger.error { "Server does not support logging (required for $method)" }
321353
throw IllegalStateException("Server does not support logging (required for $method)")
322354
}
323355
}
@@ -381,4 +413,19 @@ public open class ServerSession(
381413
instructions = instructions,
382414
)
383415
}
416+
417+
/**
418+
* Checks if a message with the given level should be ignored based on the current logging level.
419+
*
420+
* @param level The level of the message to check.
421+
* @return true if the message should be ignored (filtered out), false otherwise.
422+
*/
423+
private fun isMessageIgnored(level: LoggingLevel): Boolean {
424+
val current = currentLoggingLevel ?: return false // If no level is set, don't filter
425+
426+
val messageSeverity = loggingLevelSeverity[level] ?: return false
427+
val currentSeverity = loggingLevelSeverity[current] ?: return false
428+
429+
return messageSeverity < currentSeverity
430+
}
384431
}

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)