Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/node/extHostMcpNode.ts
3296 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, spawn } from 'child_process';
7
import { readFile } from 'fs/promises';
8
import { homedir } from 'os';
9
import { parseEnvFile } from '../../../base/common/envfile.js';
10
import { untildify } from '../../../base/common/labels.js';
11
import { DisposableMap } from '../../../base/common/lifecycle.js';
12
import * as path from '../../../base/common/path.js';
13
import { StreamSplitter } from '../../../base/node/nodeStreams.js';
14
import { findExecutable } from '../../../base/node/processes.js';
15
import { ILogService, LogLevel } from '../../../platform/log/common/log.js';
16
import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js';
17
import { McpStdioStateHandler } from '../../contrib/mcp/node/mcpStdioStateHandler.js';
18
import { IExtHostInitDataService } from '../common/extHostInitDataService.js';
19
import { ExtHostMcpService } from '../common/extHostMcp.js';
20
import { IExtHostRpcService } from '../common/extHostRpcService.js';
21
22
export class NodeExtHostMpcService extends ExtHostMcpService {
23
constructor(
24
@IExtHostRpcService extHostRpc: IExtHostRpcService,
25
@IExtHostInitDataService initDataService: IExtHostInitDataService,
26
@ILogService logService: ILogService,
27
) {
28
super(extHostRpc, logService, initDataService);
29
}
30
31
private nodeServers = this._register(new DisposableMap<number, McpStdioStateHandler>());
32
33
protected override _startMcp(id: number, launch: McpServerLaunch): void {
34
if (launch.type === McpServerTransportType.Stdio) {
35
this.startNodeMpc(id, launch);
36
} else {
37
super._startMcp(id, launch);
38
}
39
}
40
41
override $stopMcp(id: number): void {
42
const nodeServer = this.nodeServers.get(id);
43
if (nodeServer) {
44
nodeServer.stop(); // will get removed from map when process is fully stopped
45
} else {
46
super.$stopMcp(id);
47
}
48
}
49
50
override $sendMessage(id: number, message: string): void {
51
const nodeServer = this.nodeServers.get(id);
52
if (nodeServer) {
53
nodeServer.write(message);
54
} else {
55
super.$sendMessage(id, message);
56
}
57
}
58
59
private async startNodeMpc(id: number, launch: McpServerTransportStdio) {
60
const onError = (err: Error | string) => this._proxy.$onDidChangeState(id, {
61
state: McpConnectionState.Kind.Error,
62
code: err.hasOwnProperty('code') ? String((err as any).code) : undefined,
63
message: typeof err === 'string' ? err : err.message,
64
});
65
66
// MCP servers are run on the same authority where they are defined, so
67
// reading the envfile based on its path off the filesystem here is fine.
68
const env = { ...process.env };
69
if (launch.envFile) {
70
try {
71
for (const [key, value] of parseEnvFile(await readFile(launch.envFile, 'utf-8'))) {
72
env[key] = value;
73
}
74
} catch (e) {
75
onError(`Failed to read envFile '${launch.envFile}': ${e.message}`);
76
return;
77
}
78
}
79
for (const [key, value] of Object.entries(launch.env)) {
80
env[key] = value === null ? undefined : String(value);
81
}
82
83
let child: ChildProcessWithoutNullStreams;
84
try {
85
const home = homedir();
86
let cwd = launch.cwd ? untildify(launch.cwd, home) : home;
87
if (!path.isAbsolute(cwd)) {
88
cwd = path.join(home, cwd);
89
}
90
91
const { executable, args, shell } = await formatSubprocessArguments(
92
untildify(launch.command, home),
93
launch.args.map(a => untildify(a, home)),
94
cwd,
95
env
96
);
97
98
this._proxy.$onDidPublishLog(id, LogLevel.Debug, `Server command line: ${executable} ${args.join(' ')}`);
99
child = spawn(executable, args, {
100
stdio: 'pipe',
101
cwd,
102
env,
103
shell,
104
});
105
} catch (e) {
106
onError(e);
107
return;
108
}
109
110
// Create the connection manager for graceful shutdown
111
const connectionManager = new McpStdioStateHandler(child);
112
113
this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Starting });
114
115
child.stdout.pipe(new StreamSplitter('\n')).on('data', line => this._proxy.$onDidReceiveMessage(id, line.toString()));
116
117
child.stdin.on('error', onError);
118
child.stdout.on('error', onError);
119
120
// Stderr handling is not currently specified https://github.com/modelcontextprotocol/specification/issues/177
121
// Just treat it as generic log data for now
122
child.stderr.pipe(new StreamSplitter('\n')).on('data', line => this._proxy.$onDidPublishLog(id, LogLevel.Warning, `[server stderr] ${line.toString().trimEnd()}`));
123
124
child.on('spawn', () => this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Running }));
125
126
child.on('error', e => {
127
onError(e);
128
});
129
child.on('exit', code => {
130
this.nodeServers.deleteAndDispose(id);
131
132
if (code === 0 || connectionManager.stopped) {
133
this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Stopped });
134
} else {
135
this._proxy.$onDidChangeState(id, {
136
state: McpConnectionState.Kind.Error,
137
message: `Process exited with code ${code}`,
138
});
139
}
140
});
141
142
this.nodeServers.set(id, connectionManager);
143
}
144
}
145
146
const windowsShellScriptRe = /\.(bat|cmd)$/i;
147
148
/**
149
* Formats arguments to avoid issues on Windows for CVE-2024-27980.
150
*/
151
export const formatSubprocessArguments = async (
152
executable: string,
153
args: ReadonlyArray<string>,
154
cwd: string | undefined,
155
env: Record<string, string | undefined>,
156
) => {
157
if (process.platform !== 'win32') {
158
return { executable, args, shell: false };
159
}
160
161
const found = await findExecutable(executable, cwd, undefined, env);
162
if (found && windowsShellScriptRe.test(found)) {
163
const quote = (s: string) => s.includes(' ') ? `"${s}"` : s;
164
return {
165
executable: quote(found),
166
args: args.map(quote),
167
shell: true,
168
};
169
}
170
171
return { executable, args, shell: false };
172
};
173
174