Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/node/mcpStdioStateHandler.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ChildProcessWithoutNullStreams } from 'child_process';
7
import { TimeoutTimer } from '../../../../base/common/async.js';
8
import { IDisposable } from '../../../../base/common/lifecycle.js';
9
import { killTree } from '../../../../base/node/processes.js';
10
import { isWindows } from '../../../../base/common/platform.js';
11
12
const enum McpProcessState {
13
Running,
14
StdinEnded,
15
KilledPolite,
16
KilledForceful,
17
}
18
19
/**
20
* Manages graceful shutdown of MCP stdio connections following the MCP specification.
21
*
22
* Per spec, shutdown should:
23
* 1. Close the input stream to the child process
24
* 2. Wait for the server to exit, or send SIGTERM if it doesn't exit within 10 seconds
25
* 3. Send SIGKILL if the server doesn't exit within 10 seconds after SIGTERM
26
* 4. Allow forceful killing if called twice
27
*/
28
export class McpStdioStateHandler implements IDisposable {
29
private static readonly GRACE_TIME_MS = 10_000;
30
31
private _procState = McpProcessState.Running;
32
private _nextTimeout?: IDisposable;
33
34
public get stopped() {
35
return this._procState !== McpProcessState.Running;
36
}
37
38
constructor(
39
private readonly _child: ChildProcessWithoutNullStreams,
40
private readonly _graceTimeMs: number = McpStdioStateHandler.GRACE_TIME_MS
41
) { }
42
43
/**
44
* Initiates graceful shutdown. If called while shutdown is already in progress,
45
* forces immediate termination.
46
*/
47
public stop(): void {
48
if (this._procState === McpProcessState.Running) {
49
let graceTime = this._graceTimeMs;
50
try {
51
this._child.stdin.end();
52
} catch (error) {
53
// If stdin.end() fails, continue with termination sequence
54
// This can happen if the stream is already in an error state
55
graceTime = 1;
56
}
57
this._procState = McpProcessState.StdinEnded;
58
this._nextTimeout = new TimeoutTimer(() => this.killPolite(), graceTime);
59
} else {
60
this._nextTimeout?.dispose();
61
this.killForceful();
62
}
63
}
64
65
private async killPolite() {
66
this._procState = McpProcessState.KilledPolite;
67
this._nextTimeout = new TimeoutTimer(() => this.killForceful(), this._graceTimeMs);
68
69
if (this._child.pid) {
70
if (!isWindows) {
71
await killTree(this._child.pid, false).catch(() => {
72
this._child.kill('SIGTERM');
73
});
74
}
75
} else {
76
this._child.kill('SIGTERM');
77
}
78
}
79
80
private async killForceful() {
81
this._procState = McpProcessState.KilledForceful;
82
83
if (this._child.pid) {
84
await killTree(this._child.pid, true).catch(() => {
85
this._child.kill('SIGKILL');
86
});
87
} else {
88
this._child.kill();
89
}
90
}
91
92
public write(message: string): void {
93
if (!this.stopped) {
94
this._child.stdin.write(message + '\n');
95
}
96
}
97
98
public dispose() {
99
this._nextTimeout?.dispose();
100
}
101
}
102
103