modelcontextprotocol / modelcontextprotocol/typescript-sdk
stdio.ts/ReadBuffer retains consumed input and copies the next stdio chunk
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
ReadBuffer.readMessage() currently advances the buffer with:
this._buffer = this._buffer.subarray(index + 1);
When the newline is the final byte, subarray() returns an empty Buffer view. The view still references the backing allocation of the accumulated input buffer.
That allocation can contain more than the final message, for example when one input chunk contains multiple messages or uses a pooled buffer. It stays referenced until another chunk is appended, clear() is called, or the transport closes.
There is also a cost on the next append. An empty Buffer is truthy, so this code still takes the concat path:
https://github.com/modelcontextprotocol/typescript-sdk/blob/69749aa5081ddfe675d36da8d96c7e27d83742b8/src/shared/stdio.ts#L22
The next append therefore copies the incoming chunk even though no buffered bytes remain. During that copy, the retained backing allocation, the incoming chunk, and the new concat destination are all live.
This is not an unbounded leak, but it unnecessarily increases idle external memory, peak memory, and copying for long-lived stdio processes. The extra peak memory matters when large messages are allowed or the process runs under a memory limit.
I confirmed this behavior in:
- the currently published @modelcontextprotocol/sdk v1.29.0
- the current v1.x branch: https://github.com/modelcontextprotocol/typescript-sdk/blob/69749aa5081ddfe675d36da8d96c7e27d83742b8/src/shared/stdio.ts#L25-L38
- the current main branch used by the v2 packages: https://github.com/modelcontextprotocol/typescript-sdk/blob/1e1392e3f91583884fe82a0b4b91335875c3fba6/packages/core-internal/src/shared/stdio.ts#L26-L49
A small fix would clear the buffer when no bytes remain:
const remainder = this._buffer.subarray(index + 1);
this._buffer = remainder.length === 0 ? undefined : remainder;
This does not copy non-empty remainders or change the public API. It releases the consumed backing allocation and lets the next append assign its chunk directly instead of using Buffer.concat().
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 with ReadBuffer.readMessage() and the append logic in src/shared/stdio.ts, then compare the corresponding entry point in packages/core-internal/src/shared/stdio.ts. Verify that an exhausted buffer releases its backing allocation and that the next append avoids concatenation, while non-empty remainders remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- performance
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100