Skip to content

Conversation

@devcrocod
Copy link
Contributor

@devcrocod devcrocod commented Nov 19, 2025

Motivation and Context

This change adds automatic debouncing for specified notification methods in the MCP protocol layer. This is needed to:

  • Reduce redundant notification traffic for high-frequency events
  • Improve performance when multiple notifications of the same type occur rapidly
  • Provide configurable debouncing at the protocol level

How Has This Been Tested?

Breaking Changes

None

Types of changes

  • 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

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@devcrocod devcrocod force-pushed the devcrocod/change-protocol branch from 094c294 to 474a21e Compare November 19, 2025 17:37
Copilot finished reviewing on behalf of devcrocod November 19, 2025 17:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR updates the transport send method signature to support additional options and implements debounced notifications in the Protocol class, aligning with the MCP specification changes.

Key Changes:

  • Added TransportSendOptions class to encapsulate optional parameters for transport send operations (related request ID, resumption token, and resumption token callback)
  • Refactored RequestOptions to extend TransportSendOptions and removed timeout field from ProtocolOptions
  • Added debouncedNotificationMethods to ProtocolOptions with notification debouncing implementation
  • Updated all transport implementations to accept the new optional options parameter

Reviewed Changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt New class defining options for transport send operations
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt Updated send method signature to include optional TransportSendOptions parameter
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt Refactored ProtocolOptions and RequestOptions, implemented notification debouncing logic
kotlin-sdk-core/api/kotlin-sdk-core.api Updated binary API to reflect changes in Transport, ProtocolOptions, RequestOptions, and new TransportSendOptions class
kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt Updated send method signature to match Transport interface
kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt Updated send method signature to match Transport interface
kotlin-sdk-server/api/kotlin-sdk-server.api Updated binary API for server transport implementations
kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt Updated send method signature to match Transport interface
kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt Updated send method signature to match Transport interface
kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt Updated to extract resumption token and callback from TransportSendOptions
kotlin-sdk-client/api/kotlin-sdk-client.api Updated binary API for client transport implementations
kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt Updated test transport send method signature
kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaInMemoryTransport.kt Updated test transport send method signature
kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt Updated mock transport send method signature
kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockTransport.kt Updated mock transport send method signature
kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt Updated anonymous transport implementations in tests
kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaClientTest.kt Updated anonymous transport implementations in tests
kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt Updated HTTP server transport send method signature
kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinServerForTsClientSse.kt Updated HTTP server transport send method signature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 581 to 571
try {
yield()
} finally {
_pendingDebouncedNotifications.update { it.remove(notification.method) }
}

val activeTransport = this@Protocol.transport ?: return@launch

try {
activeTransport.send(jsonRpcNotification, sendOptions)
} catch (cause: Throwable) {
logger.error(cause) { "Error sending debounced notification: ${notification.method}" }
onError(cause)
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debouncing logic has a potential race condition. The finally block removes the method from _pendingDebouncedNotifications (line 584) before the notification is actually sent (line 590). This means:

  1. After yield() completes and the finally block executes, the method is removed from the pending set
  2. If another call to notification() with the same method happens between the removal (line 584) and the send (line 590), it won't be blocked
  3. This could result in multiple notifications being sent instead of being properly debounced

The removal should happen AFTER the send completes to maintain proper debouncing semantics. Consider restructuring the code like:

notificationScope.launch {
    yield()
    
    val activeTransport = this@Protocol.transport ?: run {
        _pendingDebouncedNotifications.update { it.remove(notification.method) }
        return@launch
    }
    
    try {
        activeTransport.send(jsonRpcNotification, sendOptions)
    } catch (cause: Throwable) {
        logger.error(cause) { "Error sending debounced notification: ${notification.method}" }
        onError(cause)
    } finally {
        _pendingDebouncedNotifications.update { it.remove(notification.method) }
    }
}
Suggested change
try {
yield()
} finally {
_pendingDebouncedNotifications.update { it.remove(notification.method) }
}
val activeTransport = this@Protocol.transport ?: return@launch
try {
activeTransport.send(jsonRpcNotification, sendOptions)
} catch (cause: Throwable) {
logger.error(cause) { "Error sending debounced notification: ${notification.method}" }
onError(cause)
yield()
val activeTransport = this@Protocol.transport
if (activeTransport == null) {
_pendingDebouncedNotifications.update { it.remove(notification.method) }
return@launch
}
try {
activeTransport.send(jsonRpcNotification, sendOptions)
} catch (cause: Throwable) {
logger.error(cause) { "Error sending debounced notification: ${notification.method}" }
onError(cause)
} finally {
_pendingDebouncedNotifications.update { it.remove(notification.method) }

Copilot uses AI. Check for mistakes.
public suspend fun start()

/**
* Sends a JSON-RPC message (request or response).
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for the send method should describe the options parameter. Consider adding documentation like:

/**
 * Sends a JSON-RPC message (request or response).
 * 
 * @param message The JSON-RPC message to send
 * @param options Optional transport-specific options for sending the message, such as
 *                related request ID for associating this message with an incoming request,
 *                or resumption token for continuing interrupted long-running requests
 */
public suspend fun send(message: JSONRPCMessage, options: TransportSendOptions? = null)
Suggested change
* Sends a JSON-RPC message (request or response).
* Sends a JSON-RPC message (request or response).
*
* @param message The JSON-RPC message to send.
* @param options Optional transport-specific options for sending the message, such as
* related request ID for associating this message with an incoming request,
* or resumption token for continuing interrupted long-running requests.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@kpavlov kpavlov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure source compatibility, we should define default values for all *Options parameters.

The current pattern of nullable *Options parameters is suboptimal because it necessitates null checks. To address this, we should make *Options parameters mandatory in all methods. This approach will enable the provision of mandatory configuration parameters, such as timeouts, and maintain logical simplicity. Adhering to a consistent pattern across all methods enhances consistency.

Is it indeed impossible to add tests for the change?


public open fun copy(
enforceStrictCapabilities: Boolean = this.enforceStrictCapabilities,
debouncedNotificationMethods: List<Method> = this.debouncedNotificationMethods,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should implement debouncing (dropping some events) from the start/ever. The specification states that the implementation should support rate limiting, not debouncing.
I would remove the word debounced from the name, because it's very implementation-specific

Comment on lines 572 to 573
if (isDebounced) {
if (notification.method in _pendingDebouncedNotifications.value) {
logger.trace { "Skipping debounced notification: ${notification.method}" }
return
}

_pendingDebouncedNotifications.update { it.add(notification.method) }

notificationScope.launch {
try {
yield()
} finally {
_pendingDebouncedNotifications.update { it.remove(notification.method) }
}

val activeTransport = this@Protocol.transport ?: return@launch

try {
activeTransport.send(jsonRpcNotification, sendOptions)
} catch (cause: Throwable) {
logger.error(cause) { "Error sending debounced notification: ${notification.method}" }
onError(cause)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How were positive and negative scenarios tested? It seems worth separating into dedicated PR

@devcrocod
Copy link
Contributor Author

@devcrocod devcrocod marked this pull request as draft November 20, 2025 13:06
@devcrocod devcrocod force-pushed the devcrocod/change-protocol branch from 474a21e to e9700be Compare November 20, 2025 18:33
@devcrocod devcrocod force-pushed the devcrocod/change-protocol branch from e9700be to ab075b9 Compare November 20, 2025 18:44
@devcrocod devcrocod changed the title Update transport send and fix protocol notification Add notification debouncing support to Protocol Nov 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants