feat(core): concurrent inbound dispatch behind an initialization gate (#884)

Make inbound message handling concurrent after the initialization
handshake, so a slow or peer-awaiting handler no longer blocks other
messages on the same connection

fixes #176, #572 

## Motivation and Context
Inbound messages were processed serially on the transport read loop. One
slow or peer-awaiting request handler (sampling, elicitation, roots)
blocked every later message on that connection, including the responses
and `notifications/cancelled` those handlers depend on which could
deadlock request/response flows.

## How Has This Been Tested?
New and old tests

## Breaking Changes
- after the handshake, inbound handlers may run concurrently (processing
stays serial during initialization). There is no opt-out flag by design,
concurrency is bounded by `maxConcurrentHandlers` /
`maxInFlightHandlers`.
- `ProtocolOptions` / `ClientOptions` / `ServerOptions` gain new
parameters with defaults
- `RequestHandlerExtra` is now a coroutine-context element and is no
longer user-constructible

## Types of changes
- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] 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
This commit is contained in:
Pavel Gorgulov 2026-07-15 12:33:29 +02:00 • committed by GitHub
commit beb3400064
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -73,11 +73,13 @@ import kotlinx.collections.immutable.minus
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.cancellation.CancellationException
private val logger = KotlinLogging.logger {}
@ -87,11 +89,16 @@ private val logger = KotlinLogging.logger {}
*
* @property capabilities The capabilities this client supports.
* @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server.
* @param handlerCoroutineContext Coroutine context for inbound handlers. See [ProtocolOptions.handlerCoroutineContext].
*/
public class ClientOptions(
public val capabilities: ClientCapabilities = ClientCapabilities(),
enforceStrictCapabilities: Boolean = true,
) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities)
handlerCoroutineContext: CoroutineContext = Dispatchers.Default,
) : ProtocolOptions(
enforceStrictCapabilities = enforceStrictCapabilities,
handlerCoroutineContext = handlerCoroutineContext,
)
/**
* Initializes and connects an MCP client using the provided clientInfo [Implementation], client options,
@ -216,6 +223,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
serverInstructions = result.instructions
notification(InitializedNotification())
enableConcurrentDispatch()
} catch (error: Throwable) {
logger.error(error) { "Failed to initialize client: ${error.message}" }
close()