Skip to content

Commit 9378f8b

Browse files
ElliotDrelclaude
andcommitted
fix(server): exit when MCP client closes stdin pipe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4fbcfcd commit 9378f8b

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

packages/server/src/server/stdio.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export class StdioServerTransport implements Transport {
4444
// Ignore errors during close — we're already in an error path
4545
});
4646
};
47+
_onstdinclose = () => {
48+
this.close().catch(() => {
49+
// Ignore errors during close — stdin pipe ended
50+
});
51+
};
4752

4853
/**
4954
* Starts listening for messages on `stdin`.
@@ -58,6 +63,7 @@ export class StdioServerTransport implements Transport {
5863
this._started = true;
5964
this._stdin.on('data', this._ondata);
6065
this._stdin.on('error', this._onerror);
66+
this._stdin.on('close', this._onstdinclose);
6167
this._stdout.on('error', this._onstdouterror);
6268
}
6369

@@ -85,6 +91,7 @@ export class StdioServerTransport implements Transport {
8591
// Remove our event listeners first
8692
this._stdin.off('data', this._ondata);
8793
this._stdin.off('error', this._onerror);
94+
this._stdin.off('close', this._onstdinclose);
8895
this._stdout.off('error', this._onstdouterror);
8996

9097
// Check if we were the only data listener

packages/server/test/server/stdio.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,46 @@ test('should fire onerror before onclose on stdout error', async () => {
179179

180180
expect(events).toEqual(['error', 'close']);
181181
});
182+
183+
test('should fire onclose when stdin emits close', async () => {
184+
const server = new StdioServerTransport(input, output);
185+
server.onerror = error => { throw error; };
186+
187+
let closeCount = 0;
188+
server.onclose = () => { closeCount++; };
189+
190+
await server.start();
191+
input.emit('close');
192+
193+
expect(closeCount).toBe(1);
194+
});
195+
196+
test('should fire onclose when stdin emits end', async () => {
197+
const server = new StdioServerTransport(input, output);
198+
server.onerror = error => { throw error; };
199+
200+
let closeCount = 0;
201+
server.onclose = () => { closeCount++; };
202+
203+
await server.start();
204+
input.push(null); // signals end-of-stream
205+
206+
// Allow microtasks to flush
207+
await new Promise(resolve => setTimeout(resolve, 0));
208+
209+
expect(closeCount).toBe(1);
210+
});
211+
212+
test('should not fire onclose twice when close() called after stdin close', async () => {
213+
const server = new StdioServerTransport(input, output);
214+
server.onerror = () => {};
215+
216+
let closeCount = 0;
217+
server.onclose = () => { closeCount++; };
218+
219+
await server.start();
220+
input.emit('close');
221+
await server.close();
222+
223+
expect(closeCount).toBe(1);
224+
});

0 commit comments

Comments
 (0)