Skip to content

ServerSession.onInitialized has no state replay, so late registrants silently miss the event #920

Description

@rnett

Affected version / component: io.modelcontextprotocol:kotlin-sdk 0.14.0 — server/ServerSession.kt

ServerSession.onInitialized { } only appends to a callback chain; it has no notion of whether initialization has already happened. If notifications/initialized has already been processed by the time a block is registered, that block never runs, and anything waiting on it hangs forever with no error, timeout, or diagnostic.

The session keeps the callback in a plain field and invokes it once, from the InitializedNotification handler:

private var _onInitialized: (() -> Unit) = {}

setNotificationHandler<InitializedNotification>(Defined.NotificationsInitialized) {
    _onInitialized()
    CompletableDeferred(Unit)
}

public fun onInitialized(block: () -> Unit) {
    val old = _onInitialized
    _onInitialized = { old(); block() }
}

There is no "already initialized" flag and no replay, so a registration that lands after the handshake is dropped silently. This is easy to hit, because Server.createSession() returns as soon as the transport starts, with no ordering guarantee relative to the client handshake — any code that registers onInitialized after other setup is racing the client.

The only observable side effects of the handshake are ServerSession.clientCapabilities and clientVersion, which stay null until the initialize request is processed. There is no awaitInitialized() and no StateFlow/property exposing initialization state in 0.14.0.

Suggested fix

Expose initialization state to late registrants: replay it on registration (invoke the block immediately when the session is already initialized), and/or add a suspend fun awaitInitialized() or a StateFlow of the state so callers can wait idempotently regardless of timing.

Workaround

Register onInitialized before the client can complete the handshake — immediately after createSession()/connect() returns, in the same coroutine, and only then start the client:

val session = server.connect(serverTransport)
session.onInitialized { ready.complete(Unit) }
// only now start the client

This wins the race for an in-process pair, but it is fragile: if the session were already initialized when the block is registered, the wait would suspend forever. Polling clientCapabilities/clientVersion for a non-null value can detect "already initialized", but it busy-waits and gives you no callback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues, valuable feature requestsapiPublic API changesbugSomething isn't workingready for workHas enough information to start

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions