fix(stdio): skip non-JSON lines in ReadBuffer (#1762)

Co-authored-by: Sahar Shemesh <sahar.shemesh@zoominfo.com>
This commit is contained in:
Felix Weinberger 2026-03-27 13:32:51 +00:00 • committed by GitHub
commit 64897f78ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 106 additions and 11 deletions

View file

@ -0,0 +1,5 @@
---
'@modelcontextprotocol/core': patch
---
`ReadBuffer.readMessage()` now silently skips non-JSON lines instead of throwing `SyntaxError`. This prevents noisy `onerror` callbacks when hot-reload tools (tsx, nodemon) write debug output like "Gracefully restarting..." to stdout. Lines that parse as JSON but fail JSONRPC schema validation still throw.

View file

@ -12,18 +12,28 @@ export class ReadBuffer {
}
readMessage(): JSONRPCMessage | null {
if (!this._buffer) {
return null;
}
while (this._buffer) {
const index = this._buffer.indexOf('\n');
if (index === -1) {
return null;
}
const index = this._buffer.indexOf('\n');
if (index === -1) {
return null;
}
const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, '');
this._buffer = this._buffer.subarray(index + 1);
const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, '');
this._buffer = this._buffer.subarray(index + 1);
return deserializeMessage(line);
try {
return deserializeMessage(line);
} catch (error) {
// Skip non-JSON lines (e.g., debug output from hot-reload tools like
// tsx or nodemon that write to stdout). Schema validation errors still
// throw so malformed-but-valid-JSON messages surface via onerror.
if (error instanceof SyntaxError) {
continue;
}
throw error;
}
}
return null;
}
clear(): void {

View file

@ -33,3 +33,83 @@ test('should be reusable after clearing', () => {
readBuffer.append(Buffer.from('\n'));
expect(readBuffer.readMessage()).toEqual(testMessage);
});
describe('non-JSON line filtering', () => {
test('should skip empty lines', () => {
const readBuffer = new ReadBuffer();
readBuffer.append(Buffer.from('\n\n' + JSON.stringify(testMessage) + '\n\n'));
expect(readBuffer.readMessage()).toEqual(testMessage);
expect(readBuffer.readMessage()).toBeNull();
});
test('should skip non-JSON lines before a valid message', () => {
const readBuffer = new ReadBuffer();
readBuffer.append(Buffer.from('Debug: Starting server\n' + 'Warning: Something happened\n' + JSON.stringify(testMessage) + '\n'));
expect(readBuffer.readMessage()).toEqual(testMessage);
expect(readBuffer.readMessage()).toBeNull();
});
test('should skip non-JSON lines interleaved with multiple valid messages', () => {
const readBuffer = new ReadBuffer();
const message1: JSONRPCMessage = { jsonrpc: '2.0', method: 'method1' };
const message2: JSONRPCMessage = { jsonrpc: '2.0', method: 'method2' };
readBuffer.append(
Buffer.from(
'Debug line 1\n' +
JSON.stringify(message1) +
'\n' +
'Debug line 2\n' +
'Another non-JSON line\n' +
JSON.stringify(message2) +
'\n'
)
);
expect(readBuffer.readMessage()).toEqual(message1);
expect(readBuffer.readMessage()).toEqual(message2);
expect(readBuffer.readMessage()).toBeNull();
});
test('should preserve incomplete JSON at end of buffer until completed', () => {
const readBuffer = new ReadBuffer();
readBuffer.append(Buffer.from('{"jsonrpc": "2.0", "method": "test"'));
expect(readBuffer.readMessage()).toBeNull();
readBuffer.append(Buffer.from('}\n'));
expect(readBuffer.readMessage()).toEqual({ jsonrpc: '2.0', method: 'test' });
});
test('should skip lines with unbalanced braces', () => {
const readBuffer = new ReadBuffer();
readBuffer.append(Buffer.from('{incomplete\n' + 'incomplete}\n' + JSON.stringify(testMessage) + '\n'));
expect(readBuffer.readMessage()).toEqual(testMessage);
expect(readBuffer.readMessage()).toBeNull();
});
test('should skip lines that look like JSON but fail to parse', () => {
const readBuffer = new ReadBuffer();
readBuffer.append(Buffer.from('{invalidJson: true}\n' + JSON.stringify(testMessage) + '\n'));
expect(readBuffer.readMessage()).toEqual(testMessage);
expect(readBuffer.readMessage()).toBeNull();
});
test('should tolerate leading/trailing whitespace around valid JSON', () => {
const readBuffer = new ReadBuffer();
const message: JSONRPCMessage = { jsonrpc: '2.0', method: 'test' };
readBuffer.append(Buffer.from(' ' + JSON.stringify(message) + ' \n'));
expect(readBuffer.readMessage()).toEqual(message);
});
test('should still throw on valid JSON that fails schema validation', () => {
const readBuffer = new ReadBuffer();
readBuffer.append(Buffer.from('{"not": "a jsonrpc message"}\n'));
expect(() => readBuffer.readMessage()).toThrow();
});
});