modelcontextprotocol / modelcontextprotocol/kotlin-sdk
ServerSession.onInitialized has no state replay, so late registrants silently miss the event
Nobody has claimed this yet.
- Dominant language
- Kotlin
- Stars
- 1.5k
- Forks
- 248
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 23
Description
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in server/ServerSession.kt, reading the onInitialized registration method and the InitializedNotification handler. Reproduce the race by registering after the handshake has been processed, then verify that the chosen initialization-state behavior lets late registrants complete reliably without hanging.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 56/100