Repository navigation
Conversation
🦋 Changeset detectedLatest commit: fdd1ffb The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
karaaslanz
left a comment
There was a problem hiding this comment.
Exact-source review of draft HEAD fdd1ffb9baef3d93c996224d164d5b2ebaf4db7f. Found a reproducible post-close write race introduced by async outbound interception.
Location: packages/server/src/server/serveStdio.ts, StdioConnectionChannel.send (approx. lines 184?197). It checks this._closed before await this._outboundIntercept?.(message), but not again afterwards. modernOutboundIntercept is now async, introducing an await boundary even for ordinary responses where interception returns undefined. If the channel is closed/discarded during the await, the continuation still forwards the message to the shared wire after close() finished.
Exact-source reproduction: fetched serveStdio.ts from the PR head (blob 4f6d5fc7928b93269c149d25691428022e82c153), transformed the actual StdioConnectionChannel class with Node 24 TypeScript-strip, instantiated it using a deferred async interceptor and a counting fake wire. Sequence:
- Start
channel.send({jsonrpc:'2.0', id:7, result:{ok:true}}); interceptor awaits. await channel.close().- Resolve the interceptor with
undefined; then await the pending send. - Observed
WIRE_WRITES_AFTER_CLOSE = 1.
No repo files changed. This is a deterministic exact-source isolated test, not a full stdio integration suite.
Narrow fix and regression gate: check this._closed again after awaiting interception, before this._wire.send. Add a deferred-interceptor test that closes the channel between the two steps and verifies zero writes to the shared wire. This prevents discarded connection instances from emitting late messages. I did not review the broader admission-completion design end to end.
Transport adapters cannot safely return reusable admission credits from responses or cancellation alone: handlers, response writes and SDK cleanup may still be running. This adds local
MessageExtraInfo.onMessageProcessedmetadata, preserved through the official stdio entry, and a public stdio capability marker. Completion joins ordinary handlers/writes/controller cleanup, asynchronous inbound-response validation, opening/error paths, and subscription ACK/teardown/started notification deliveries.Draft proposal for #3020; maintainer acceptance is still pending. The metadata is never serialized and does not attest remote effects or application work outside returned promises. No alternate MCP transport or protocol implementation is introduced.
Validation:
/usr/bin/tee. No system workaround was installed.Official stdio send now waits for the actual chunk callback even when write() accepts the chunk immediately. Failure and close cannot return an unfinished write credit. Independent review reproduced and closed both missing-event hangs.
The new changeset and documentation describe the local completion boundary. This is a source proposal, not released SDK support or downstream production qualification.