Skip to content

Commit 0984898

Browse files
committed
fix: #780 onerror and other listener not remove after client close (stdio)
1 parent 8a1b457 commit 0984898

File tree

2 files changed

+57
-25
lines changed

2 files changed

+57
-25
lines changed

packages/client/src/client/stdio.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export class StdioClientTransport implements Transport {
9595
private _readBuffer: ReadBuffer = new ReadBuffer();
9696
private _serverParams: StdioServerParameters;
9797
private _stderrStream: PassThrough | null = null;
98+
private _onServerDataHandler?: (chunk: Buffer) => void;
99+
private _onServerErrorHandler?: (error: Error) => void;
98100

99101
onclose?: () => void;
100102
onerror?: (error: Error) => void;
@@ -130,33 +132,31 @@ export class StdioClientTransport implements Transport {
130132
cwd: this._serverParams.cwd
131133
});
132134

135+
this._onServerDataHandler = (chunk: Buffer) => {
136+
this._readBuffer.append(chunk);
137+
this.processReadBuffer();
138+
};
139+
this._onServerErrorHandler = (error: Error) => {
140+
this.onerror?.(error);
141+
};
142+
143+
this._process.stdout?.on('data', this._onServerDataHandler);
144+
this._process.stdout?.on('error', this._onServerErrorHandler);
145+
this._process.stdin?.on('error', this._onServerErrorHandler);
146+
133147
this._process.on('error', error => {
134148
reject(error);
135149
this.onerror?.(error);
136150
});
137-
138-
this._process.on('spawn', () => {
139-
resolve();
140-
});
141-
142-
this._process.on('close', _code => {
151+
this._process.once('spawn', () => resolve());
152+
this._process.once('close', _code => {
153+
if (this._process) {
154+
this.cleanupListeners(this._process);
155+
}
143156
this._process = undefined;
144157
this.onclose?.();
145158
});
146159

147-
this._process.stdin?.on('error', error => {
148-
this.onerror?.(error);
149-
});
150-
151-
this._process.stdout?.on('data', chunk => {
152-
this._readBuffer.append(chunk);
153-
this.processReadBuffer();
154-
});
155-
156-
this._process.stdout?.on('error', error => {
157-
this.onerror?.(error);
158-
});
159-
160160
if (this._stderrStream && this._process.stderr) {
161161
this._process.stderr.pipe(this._stderrStream);
162162
}
@@ -202,8 +202,19 @@ export class StdioClientTransport implements Transport {
202202
}
203203
}
204204

205+
private cleanupListeners(process: ChildProcess) {
206+
if (this._onServerDataHandler) {
207+
process.stdout?.off('data', this._onServerDataHandler);
208+
}
209+
if (this._onServerErrorHandler) {
210+
process.stdout?.off('error', this._onServerErrorHandler);
211+
process.stdin?.off('error', this._onServerErrorHandler);
212+
}
213+
}
214+
205215
async close(): Promise<void> {
206216
if (this._process) {
217+
this.cleanupListeners(this._process);
207218
const processToClose = this._process;
208219
this._process = undefined;
209220

packages/client/test/client/cross-spawn.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ChildProcess } from 'node:child_process';
33
import type { JSONRPCMessage } from '@modelcontextprotocol/core';
44
import spawn from 'cross-spawn';
55
import type { Mock, MockedFunction } from 'vitest';
6+
import { vi } from 'vitest';
67

78
import { getDefaultEnvironment, StdioClientTransport } from '../../src/client/stdio.js';
89

@@ -16,8 +17,9 @@ describe('StdioClientTransport using cross-spawn', () => {
1617
mockSpawn.mockImplementation(() => {
1718
const mockProcess: {
1819
on: Mock;
19-
stdin?: { on: Mock; write: Mock };
20-
stdout?: { on: Mock };
20+
once: Mock;
21+
stdin?: { on: Mock; write: Mock; off: Mock };
22+
stdout?: { on: Mock; off: Mock };
2123
stderr?: null;
2224
} = {
2325
on: vi.fn((event: string, callback: () => void) => {
@@ -26,12 +28,20 @@ describe('StdioClientTransport using cross-spawn', () => {
2628
}
2729
return mockProcess;
2830
}),
31+
once: vi.fn((event: string, callback: () => void) => {
32+
if (event === 'spawn') {
33+
callback();
34+
}
35+
return mockProcess;
36+
}),
2937
stdin: {
3038
on: vi.fn(),
31-
write: vi.fn().mockReturnValue(true)
39+
write: vi.fn().mockReturnValue(true),
40+
off: vi.fn()
3241
},
3342
stdout: {
34-
on: vi.fn()
43+
on: vi.fn(),
44+
off: vi.fn()
3545
},
3646
stderr: null
3747
};
@@ -109,13 +119,16 @@ describe('StdioClientTransport using cross-spawn', () => {
109119
// get the mock process object
110120
const mockProcess: {
111121
on: Mock;
122+
once: Mock;
112123
stdin: {
113124
on: Mock;
114125
write: Mock;
115126
once: Mock;
127+
off: Mock;
116128
};
117129
stdout: {
118130
on: Mock;
131+
off: Mock;
119132
};
120133
stderr: null;
121134
} = {
@@ -125,13 +138,21 @@ describe('StdioClientTransport using cross-spawn', () => {
125138
}
126139
return mockProcess;
127140
}),
141+
once: vi.fn((event: string, callback: () => void) => {
142+
if (event === 'spawn') {
143+
callback();
144+
}
145+
return mockProcess;
146+
}),
128147
stdin: {
129148
on: vi.fn(),
130149
write: vi.fn().mockReturnValue(true),
131-
once: vi.fn()
150+
once: vi.fn(),
151+
off: vi.fn()
132152
},
133153
stdout: {
134-
on: vi.fn()
154+
on: vi.fn(),
155+
off: vi.fn()
135156
},
136157
stderr: null
137158
};

0 commit comments

Comments
 (0)