fix(stdio): skip non-JSON lines in ReadBuffer (#1762)
Co-authored-by: Sahar Shemesh <sahar.shemesh@zoominfo.com>
This commit is contained in:
parent
a2e503733f
commit
64897f78ce
3 changed files with 106 additions and 11 deletions
5
.changeset/stdio-skip-non-json.md
Normal file
5
.changeset/stdio-skip-non-json.md
Normal 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.
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue