@namzu/sdk 14.0.3 → 14.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 14.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - b4a3fa7: `StdioTransport.close()` resolves when the child is gone, not when the signal
8
+ was sent.
9
+
10
+ It called `kill('SIGTERM')` and returned. `kill()` returns as soon as the
11
+ signal is delivered, so an awaited `close()` meant "SIGTERM is on its way", and
12
+ a caller that closed a transport and then deleted the child's working directory
13
+ raced the exit — reported as `EBUSY` from a real integration, not inferred. A
14
+ close that does not mean closed makes every teardown built on it a guess, and
15
+ the guess is only wrong sometimes.
16
+
17
+ `close()` now waits for the child's `exit`. A child ignoring SIGTERM is sent
18
+ SIGKILL after two seconds, and a second timer gives up waiting, so `close()`
19
+ cannot hang; both timers are unreferenced, so neither holds the event loop
20
+ open. A spawn that never produced a process emits `error` and no `exit`, and
21
+ that path settles too rather than waiting for an event that is not coming.
22
+
3
23
  ## 14.0.3
4
24
 
5
25
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=a-close-that-means-closed.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-close-that-means-closed.test.d.ts","sourceRoot":"","sources":["../../../../src/connector/mcp/__tests__/a-close-that-means-closed.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { StdioTransport } from '../stdio.js';
3
+ /**
4
+ * `close()` sent SIGTERM and returned without waiting, so a resolved close
5
+ * meant "the signal is on its way", not "the child is gone". A caller that
6
+ * closed a transport and then deleted the child's working directory raced the
7
+ * exit and saw EBUSY — reported from a real integration, not inferred.
8
+ *
9
+ * A close that does not mean closed makes every teardown built on top of it a
10
+ * guess, and the guess is only wrong sometimes, which is the worst kind.
11
+ *
12
+ * These assert on the operating system rather than on the transport's own
13
+ * state: `process.kill(pid, 0)` throws ESRCH once the pid is gone, so the
14
+ * assertion cannot pass by the transport merely believing it closed.
15
+ */
16
+ /** The child the transport spawned, read before `close()` clears the field. */
17
+ function childOf(transport) {
18
+ const child = transport.process;
19
+ if (!child)
20
+ throw new Error('transport spawned no process');
21
+ return child;
22
+ }
23
+ function pidOf(transport) {
24
+ const pid = childOf(transport).pid;
25
+ if (pid === undefined)
26
+ throw new Error('transport spawned no process');
27
+ return pid;
28
+ }
29
+ function isAlive(pid) {
30
+ try {
31
+ process.kill(pid, 0);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ describe('a close that means closed', () => {
39
+ it('does not resolve until the child is actually gone', async () => {
40
+ const transport = new StdioTransport({
41
+ type: 'stdio',
42
+ command: process.execPath,
43
+ args: ['-e', 'setInterval(() => {}, 1000)'],
44
+ });
45
+ await transport.connect();
46
+ const child = childOf(transport);
47
+ const pid = pidOf(transport);
48
+ expect(isAlive(pid)).toBe(true);
49
+ await transport.close();
50
+ // The reaped-ness of the child, not the liveness of the pid.
51
+ //
52
+ // `isAlive(pid)` alone is sound about the wrong thing: on Windows
53
+ // `kill('SIGTERM')` terminates the process synchronously, so the pid is
54
+ // already gone by the next line whether or not `close()` waited — the
55
+ // assertion passed under a deliberately reintroduced fire-and-forget
56
+ // kill, which is how this was caught. Node fills `exitCode`/`signalCode`
57
+ // only when it reaps the child and emits `exit`, a later tick, so these
58
+ // are non-null here exactly when `close()` awaited that event.
59
+ expect(child.exitCode !== null || child.signalCode !== null).toBe(true);
60
+ expect(isAlive(pid)).toBe(false);
61
+ });
62
+ it('returns rather than hanging when the command does not exist', async () => {
63
+ // A spawn that fails emits `error` and never `exit`. Waiting on `exit`
64
+ // alone would turn a bad command into a shutdown that never completes.
65
+ const transport = new StdioTransport({
66
+ type: 'stdio',
67
+ command: 'namzu-no-such-command-exists-here',
68
+ args: [],
69
+ });
70
+ await transport.connect();
71
+ await expect(transport.close()).resolves.toBeUndefined();
72
+ });
73
+ it('is safe to call twice', async () => {
74
+ const transport = new StdioTransport({
75
+ type: 'stdio',
76
+ command: process.execPath,
77
+ args: ['-e', 'setInterval(() => {}, 1000)'],
78
+ });
79
+ await transport.connect();
80
+ const pid = pidOf(transport);
81
+ await transport.close();
82
+ await transport.close();
83
+ expect(isAlive(pid)).toBe(false);
84
+ });
85
+ it('reports the exit to the handler registered for that session', async () => {
86
+ // The waiting must not swallow the notification the client depends on
87
+ // to learn the session ended.
88
+ const transport = new StdioTransport({
89
+ type: 'stdio',
90
+ command: process.execPath,
91
+ args: ['-e', 'setInterval(() => {}, 1000)'],
92
+ });
93
+ await transport.connect();
94
+ let closes = 0;
95
+ transport.onClose(() => {
96
+ closes++;
97
+ });
98
+ await transport.close();
99
+ // `close` on a ChildProcess follows `exit` by a tick once the stdio
100
+ // streams drain; give the loop that tick rather than asserting on a
101
+ // race.
102
+ await new Promise((resolve) => setTimeout(resolve, 50));
103
+ expect(closes).toBe(1);
104
+ });
105
+ });
106
+ //# sourceMappingURL=a-close-that-means-closed.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a-close-that-means-closed.test.js","sourceRoot":"","sources":["../../../../src/connector/mcp/__tests__/a-close-that-means-closed.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;;;;;;;;GAYG;AAEH,+EAA+E;AAC/E,SAAS,OAAO,CAAC,SAAyB;IACzC,MAAM,KAAK,GAAI,SAAyD,CAAC,OAAO,CAAA;IAChF,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAA;IAC3D,OAAO,KAAK,CAAA;AACb,CAAC;AAED,SAAS,KAAK,CAAC,SAAyB;IACvC,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,CAAA;IAClC,IAAI,GAAG,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAA;IACtE,OAAO,GAAG,CAAA;AACX,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC3B,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,IAAI,CAAA;IACZ,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAA;IACb,CAAC;AACF,CAAC;AAED,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,SAAS,GAAG,IAAI,cAAc,CAAC;YACpC,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,OAAO,CAAC,QAAQ;YACzB,IAAI,EAAE,CAAC,IAAI,EAAE,6BAA6B,CAAC;SAC3C,CAAC,CAAA;QACF,MAAM,SAAS,CAAC,OAAO,EAAE,CAAA;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;QAChC,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAA;QAC5B,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAE/B,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;QAEvB,6DAA6D;QAC7D,EAAE;QACF,kEAAkE;QAClE,wEAAwE;QACxE,sEAAsE;QACtE,qEAAqE;QACrE,yEAAyE;QACzE,wEAAwE;QACxE,+DAA+D;QAC/D,MAAM,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACvE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACjC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC5E,uEAAuE;QACvE,uEAAuE;QACvE,MAAM,SAAS,GAAG,IAAI,cAAc,CAAC;YACpC,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,mCAAmC;YAC5C,IAAI,EAAE,EAAE;SACR,CAAC,CAAA;QACF,MAAM,SAAS,CAAC,OAAO,EAAE,CAAA;QAEzB,MAAM,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAA;IACzD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uBAAuB,EAAE,KAAK,IAAI,EAAE;QACtC,MAAM,SAAS,GAAG,IAAI,cAAc,CAAC;YACpC,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,OAAO,CAAC,QAAQ;YACzB,IAAI,EAAE,CAAC,IAAI,EAAE,6BAA6B,CAAC;SAC3C,CAAC,CAAA;QACF,MAAM,SAAS,CAAC,OAAO,EAAE,CAAA;QACzB,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAA;QAE5B,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;QACvB,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;QAEvB,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACjC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC5E,sEAAsE;QACtE,8BAA8B;QAC9B,MAAM,SAAS,GAAG,IAAI,cAAc,CAAC;YACpC,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,OAAO,CAAC,QAAQ;YACzB,IAAI,EAAE,CAAC,IAAI,EAAE,6BAA6B,CAAC;SAC3C,CAAC,CAAA;QACF,MAAM,SAAS,CAAC,OAAO,EAAE,CAAA;QACzB,IAAI,MAAM,GAAG,CAAC,CAAA;QACd,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE;YACtB,MAAM,EAAE,CAAA;QACT,CAAC,CAAC,CAAA;QAEF,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;QACvB,oEAAoE;QACpE,oEAAoE;QACpE,QAAQ;QACR,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;QAEvD,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACvB,CAAC,CAAC,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,iBAAiB,EACjB,uBAAuB,EACvB,YAAY,EACZ,MAAM,gCAAgC,CAAA;AAGvC,qBAAa,cAAe,YAAW,YAAY;IAWtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVnC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,eAAe,CAAkD;IACzE,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,SAAS,CAAQ;IACzB,6DAA6D;IAC7D,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,GAAG,CAAQ;gBAEU,MAAM,EAAE,uBAAuB;IAItD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB5B;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAMf,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrD,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,GAAG,IAAI;IAI9D,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIlC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAI9C,WAAW,IAAI,OAAO;IAItB,OAAO,CAAC,aAAa;CAerB"}
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,iBAAiB,EACjB,uBAAuB,EACvB,YAAY,EACZ,MAAM,gCAAgC,CAAA;AAUvC,qBAAa,cAAe,YAAW,YAAY;IAWtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVnC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,eAAe,CAAkD;IACzE,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,SAAS,CAAQ;IACzB,6DAA6D;IAC7D,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,GAAG,CAAQ;gBAEU,MAAM,EAAE,uBAAuB;IAItD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkD5B;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAMf,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrD,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,GAAG,IAAI;IAI9D,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIlC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAI9C,WAAW,IAAI,OAAO;IAItB,OAAO,CAAC,aAAa;CAerB"}
@@ -1,5 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { getRootLogger } from '../../utils/logger.js';
3
+ /**
4
+ * How long a child gets to honour SIGTERM before SIGKILL. Two seconds is
5
+ * long enough for a server flushing a response and short enough that a
6
+ * shutdown does not read as a hang.
7
+ */
8
+ const TERMINATE_GRACE_MS = 2_000;
3
9
  export class StdioTransport {
4
10
  config;
5
11
  process = null;
@@ -60,9 +66,40 @@ export class StdioTransport {
60
66
  }
61
67
  this.connected = false;
62
68
  this.exitPending = true;
63
- this.process.kill('SIGTERM');
69
+ const child = this.process;
64
70
  this.process = null;
65
71
  this.buffer = '';
72
+ // Resolve when the child is actually gone, not when the signal was
73
+ // sent. `kill()` returns as soon as the signal is delivered, so an
74
+ // awaited `close()` meant only "SIGTERM is on its way" — a caller that
75
+ // closed and then deleted the child's working directory raced the exit
76
+ // and saw EBUSY. A close that does not mean closed makes every
77
+ // teardown after it a guess.
78
+ //
79
+ // A spawn that never produced a process emits `error` and no `exit`,
80
+ // so both settle this, and two timers make a hang impossible: the
81
+ // first escalates to SIGKILL for a child ignoring SIGTERM, the second
82
+ // gives up waiting. Neither holds the event loop open.
83
+ if (child.pid === undefined)
84
+ return;
85
+ await new Promise((resolve) => {
86
+ let settled = false;
87
+ const finish = () => {
88
+ if (settled)
89
+ return;
90
+ settled = true;
91
+ clearTimeout(escalate);
92
+ clearTimeout(giveUp);
93
+ resolve();
94
+ };
95
+ child.once('exit', finish);
96
+ child.once('error', finish);
97
+ child.kill('SIGTERM');
98
+ const escalate = setTimeout(() => child.kill('SIGKILL'), TERMINATE_GRACE_MS);
99
+ const giveUp = setTimeout(finish, TERMINATE_GRACE_MS * 2);
100
+ escalate.unref?.();
101
+ giveUp.unref?.();
102
+ });
66
103
  // The handlers deliberately survive this call. `process.on('close')`
67
104
  // fires them when the process actually exits, and dropping them here
68
105
  // would leave `MCPClient` believing it is still connected — so its next
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../../src/connector/mcp/stdio.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "14.0.3",
3
+ "version": "14.0.4",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -0,0 +1,119 @@
1
+ import type { ChildProcess } from 'node:child_process'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { StdioTransport } from '../stdio.js'
5
+
6
+ /**
7
+ * `close()` sent SIGTERM and returned without waiting, so a resolved close
8
+ * meant "the signal is on its way", not "the child is gone". A caller that
9
+ * closed a transport and then deleted the child's working directory raced the
10
+ * exit and saw EBUSY — reported from a real integration, not inferred.
11
+ *
12
+ * A close that does not mean closed makes every teardown built on top of it a
13
+ * guess, and the guess is only wrong sometimes, which is the worst kind.
14
+ *
15
+ * These assert on the operating system rather than on the transport's own
16
+ * state: `process.kill(pid, 0)` throws ESRCH once the pid is gone, so the
17
+ * assertion cannot pass by the transport merely believing it closed.
18
+ */
19
+
20
+ /** The child the transport spawned, read before `close()` clears the field. */
21
+ function childOf(transport: StdioTransport): ChildProcess {
22
+ const child = (transport as unknown as { process: ChildProcess | null }).process
23
+ if (!child) throw new Error('transport spawned no process')
24
+ return child
25
+ }
26
+
27
+ function pidOf(transport: StdioTransport): number {
28
+ const pid = childOf(transport).pid
29
+ if (pid === undefined) throw new Error('transport spawned no process')
30
+ return pid
31
+ }
32
+
33
+ function isAlive(pid: number): boolean {
34
+ try {
35
+ process.kill(pid, 0)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ describe('a close that means closed', () => {
43
+ it('does not resolve until the child is actually gone', async () => {
44
+ const transport = new StdioTransport({
45
+ type: 'stdio',
46
+ command: process.execPath,
47
+ args: ['-e', 'setInterval(() => {}, 1000)'],
48
+ })
49
+ await transport.connect()
50
+ const child = childOf(transport)
51
+ const pid = pidOf(transport)
52
+ expect(isAlive(pid)).toBe(true)
53
+
54
+ await transport.close()
55
+
56
+ // The reaped-ness of the child, not the liveness of the pid.
57
+ //
58
+ // `isAlive(pid)` alone is sound about the wrong thing: on Windows
59
+ // `kill('SIGTERM')` terminates the process synchronously, so the pid is
60
+ // already gone by the next line whether or not `close()` waited — the
61
+ // assertion passed under a deliberately reintroduced fire-and-forget
62
+ // kill, which is how this was caught. Node fills `exitCode`/`signalCode`
63
+ // only when it reaps the child and emits `exit`, a later tick, so these
64
+ // are non-null here exactly when `close()` awaited that event.
65
+ expect(child.exitCode !== null || child.signalCode !== null).toBe(true)
66
+ expect(isAlive(pid)).toBe(false)
67
+ })
68
+
69
+ it('returns rather than hanging when the command does not exist', async () => {
70
+ // A spawn that fails emits `error` and never `exit`. Waiting on `exit`
71
+ // alone would turn a bad command into a shutdown that never completes.
72
+ const transport = new StdioTransport({
73
+ type: 'stdio',
74
+ command: 'namzu-no-such-command-exists-here',
75
+ args: [],
76
+ })
77
+ await transport.connect()
78
+
79
+ await expect(transport.close()).resolves.toBeUndefined()
80
+ })
81
+
82
+ it('is safe to call twice', async () => {
83
+ const transport = new StdioTransport({
84
+ type: 'stdio',
85
+ command: process.execPath,
86
+ args: ['-e', 'setInterval(() => {}, 1000)'],
87
+ })
88
+ await transport.connect()
89
+ const pid = pidOf(transport)
90
+
91
+ await transport.close()
92
+ await transport.close()
93
+
94
+ expect(isAlive(pid)).toBe(false)
95
+ })
96
+
97
+ it('reports the exit to the handler registered for that session', async () => {
98
+ // The waiting must not swallow the notification the client depends on
99
+ // to learn the session ended.
100
+ const transport = new StdioTransport({
101
+ type: 'stdio',
102
+ command: process.execPath,
103
+ args: ['-e', 'setInterval(() => {}, 1000)'],
104
+ })
105
+ await transport.connect()
106
+ let closes = 0
107
+ transport.onClose(() => {
108
+ closes++
109
+ })
110
+
111
+ await transport.close()
112
+ // `close` on a ChildProcess follows `exit` by a tick once the stdio
113
+ // streams drain; give the loop that tick rather than asserting on a
114
+ // race.
115
+ await new Promise((resolve) => setTimeout(resolve, 50))
116
+
117
+ expect(closes).toBe(1)
118
+ })
119
+ })
@@ -6,6 +6,13 @@ import type {
6
6
  } from '../../types/connector/index.js'
7
7
  import { type Logger, getRootLogger } from '../../utils/logger.js'
8
8
 
9
+ /**
10
+ * How long a child gets to honour SIGTERM before SIGKILL. Two seconds is
11
+ * long enough for a server flushing a response and short enough that a
12
+ * shutdown does not read as a hang.
13
+ */
14
+ const TERMINATE_GRACE_MS = 2_000
15
+
9
16
  export class StdioTransport implements MCPTransport {
10
17
  private process: ChildProcess | null = null
11
18
  private messageHandlers: Array<(message: MCPJsonRpcMessage) => void> = []
@@ -71,9 +78,39 @@ export class StdioTransport implements MCPTransport {
71
78
  }
72
79
  this.connected = false
73
80
  this.exitPending = true
74
- this.process.kill('SIGTERM')
81
+ const child = this.process
75
82
  this.process = null
76
83
  this.buffer = ''
84
+
85
+ // Resolve when the child is actually gone, not when the signal was
86
+ // sent. `kill()` returns as soon as the signal is delivered, so an
87
+ // awaited `close()` meant only "SIGTERM is on its way" — a caller that
88
+ // closed and then deleted the child's working directory raced the exit
89
+ // and saw EBUSY. A close that does not mean closed makes every
90
+ // teardown after it a guess.
91
+ //
92
+ // A spawn that never produced a process emits `error` and no `exit`,
93
+ // so both settle this, and two timers make a hang impossible: the
94
+ // first escalates to SIGKILL for a child ignoring SIGTERM, the second
95
+ // gives up waiting. Neither holds the event loop open.
96
+ if (child.pid === undefined) return
97
+ await new Promise<void>((resolve) => {
98
+ let settled = false
99
+ const finish = (): void => {
100
+ if (settled) return
101
+ settled = true
102
+ clearTimeout(escalate)
103
+ clearTimeout(giveUp)
104
+ resolve()
105
+ }
106
+ child.once('exit', finish)
107
+ child.once('error', finish)
108
+ child.kill('SIGTERM')
109
+ const escalate = setTimeout(() => child.kill('SIGKILL'), TERMINATE_GRACE_MS)
110
+ const giveUp = setTimeout(finish, TERMINATE_GRACE_MS * 2)
111
+ escalate.unref?.()
112
+ giveUp.unref?.()
113
+ })
77
114
  // The handlers deliberately survive this call. `process.on('close')`
78
115
  // fires them when the process actually exits, and dropping them here
79
116
  // would leave `MCPClient` believing it is still connected — so its next