Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpDevMode.ts
5263 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 { equals as arraysEqual } from '../../../../base/common/arrays.js';
7
import { assertNever } from '../../../../base/common/assert.js';
8
import { Throttler } from '../../../../base/common/async.js';
9
import * as glob from '../../../../base/common/glob.js';
10
import { Disposable } from '../../../../base/common/lifecycle.js';
11
import { equals as objectsEqual } from '../../../../base/common/objects.js';
12
import { autorun, autorunDelta, derivedOpts } from '../../../../base/common/observable.js';
13
import { localize } from '../../../../nls.js';
14
import { ICommandService } from '../../../../platform/commands/common/commands.js';
15
import { FileSystemProviderCapabilities, IFileService } from '../../../../platform/files/common/files.js';
16
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
17
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
18
import { IConfig, IDebugService, IDebugSessionOptions } from '../../debug/common/debug.js';
19
import { IMcpRegistry } from './mcpRegistryTypes.js';
20
import { IMcpServer, McpServerDefinition, McpServerLaunch, McpServerTransportType } from './mcpTypes.js';
21
22
export class McpDevModeServerAttache extends Disposable {
23
constructor(
24
server: IMcpServer,
25
fwdRef: { lastModeDebugged: boolean },
26
@IMcpRegistry registry: IMcpRegistry,
27
@IFileService fileService: IFileService,
28
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
29
) {
30
super();
31
32
const workspaceFolder = server.readDefinitions().map(({ collection }) => collection?.presentation?.origin &&
33
workspaceContextService.getWorkspaceFolder(collection.presentation?.origin)?.uri);
34
35
const restart = async () => {
36
const lastDebugged = fwdRef.lastModeDebugged;
37
await server.stop();
38
await server.start({ debug: lastDebugged });
39
};
40
41
// 1. Auto-start the server, restart if entering debug mode
42
let didAutoStart = false;
43
this._register(autorun(reader => {
44
const defs = server.readDefinitions().read(reader);
45
if (!defs.collection || !defs.server || !defs.server.devMode) {
46
didAutoStart = false;
47
return;
48
}
49
50
// don't keep trying to start the server unless it's a new server or devmode is newly turned on
51
if (didAutoStart) {
52
return;
53
}
54
55
const delegates = registry.delegates.read(reader);
56
if (!delegates.some(d => d.canStart(defs.collection!, defs.server!))) {
57
return;
58
}
59
60
server.start();
61
didAutoStart = true;
62
}));
63
64
const debugMode = server.readDefinitions().map(d => !!d.server?.devMode?.debug);
65
this._register(autorunDelta(debugMode, ({ lastValue, newValue }) => {
66
if (!!newValue && !objectsEqual(lastValue, newValue)) {
67
restart();
68
}
69
}));
70
71
// 2. Watch for file changes
72
const watchObs = derivedOpts<string[] | undefined>({ equalsFn: arraysEqual }, reader => {
73
const def = server.readDefinitions().read(reader);
74
const watch = def.server?.devMode?.watch;
75
return typeof watch === 'string' ? [watch] : watch;
76
});
77
78
const restartScheduler = this._register(new Throttler());
79
80
this._register(autorun(reader => {
81
const pattern = watchObs.read(reader);
82
const wf = workspaceFolder.read(reader);
83
if (!pattern || !wf) {
84
return;
85
}
86
87
const includes = pattern.filter(p => !p.startsWith('!'));
88
const excludes = pattern.filter(p => p.startsWith('!')).map(p => p.slice(1));
89
reader.store.add(fileService.watch(wf, { includes, excludes, recursive: true }));
90
91
const ignoreCase = !fileService.hasCapability(wf, FileSystemProviderCapabilities.PathCaseSensitive);
92
const includeParse = includes.map(p => glob.parse({ base: wf.fsPath, pattern: p }, { ignoreCase }));
93
const excludeParse = excludes.map(p => glob.parse({ base: wf.fsPath, pattern: p }, { ignoreCase }));
94
reader.store.add(fileService.onDidFilesChange(e => {
95
for (const change of [e.rawAdded, e.rawDeleted, e.rawUpdated]) {
96
for (const uri of change) {
97
if (includeParse.some(i => i(uri.fsPath)) && !excludeParse.some(e => e(uri.fsPath))) {
98
restartScheduler.queue(restart);
99
break;
100
}
101
}
102
}
103
}));
104
}));
105
}
106
}
107
108
export interface IMcpDevModeDebugging {
109
readonly _serviceBrand: undefined;
110
111
transform(definition: McpServerDefinition, launch: McpServerLaunch): Promise<McpServerLaunch>;
112
}
113
114
export const IMcpDevModeDebugging = createDecorator<IMcpDevModeDebugging>('mcpDevModeDebugging');
115
116
const DEBUG_HOST = '127.0.0.1';
117
118
export class McpDevModeDebugging implements IMcpDevModeDebugging {
119
declare readonly _serviceBrand: undefined;
120
121
constructor(
122
@IDebugService private readonly _debugService: IDebugService,
123
@ICommandService private readonly _commandService: ICommandService,
124
) { }
125
126
public async transform(definition: McpServerDefinition, launch: McpServerLaunch): Promise<McpServerLaunch> {
127
if (!definition.devMode?.debug || launch.type !== McpServerTransportType.Stdio) {
128
return launch;
129
}
130
131
const port = await this.getDebugPort();
132
const name = `MCP: ${definition.label}`; // for debugging
133
const options: IDebugSessionOptions = { startedByUser: false, suppressDebugView: true };
134
const commonConfig: Partial<IConfig> = {
135
internalConsoleOptions: 'neverOpen',
136
suppressMultipleSessionWarning: true,
137
};
138
139
switch (definition.devMode.debug.type) {
140
case 'node': {
141
if (!/node[0-9]*$/.test(launch.command)) {
142
throw new Error(localize('mcp.debug.nodeBinReq', 'MCP server must be launched with the "node" executable to enable debugging, but was launched with "{0}"', launch.command));
143
}
144
145
// We intentionally assert types as the DA has additional properties beyong IConfig
146
// eslint-disable-next-line local/code-no-dangerous-type-assertions
147
this._debugService.startDebugging(undefined, {
148
type: 'pwa-node',
149
request: 'attach',
150
name,
151
port,
152
host: DEBUG_HOST,
153
timeout: 30_000,
154
continueOnAttach: true,
155
...commonConfig,
156
} as IConfig, options);
157
return { ...launch, args: [`--inspect-brk=${DEBUG_HOST}:${port}`, ...launch.args] };
158
}
159
case 'debugpy': {
160
if (!/python[0-9.]*$/.test(launch.command)) {
161
throw new Error(localize('mcp.debug.pythonBinReq', 'MCP server must be launched with the "python" executable to enable debugging, but was launched with "{0}"', launch.command));
162
}
163
164
let command: string | undefined;
165
let args = ['--wait-for-client', '--connect', `${DEBUG_HOST}:${port}`, ...launch.args];
166
if (definition.devMode.debug.debugpyPath) {
167
command = definition.devMode.debug.debugpyPath;
168
} else {
169
try {
170
// The Python debugger exposes a command to get its bundle debugpy module path. Use that if it's available.
171
const debugPyPath = await this._commandService.executeCommand<string | undefined>('python.getDebugpyPackagePath');
172
if (debugPyPath) {
173
command = launch.command;
174
args = [debugPyPath, ...args];
175
}
176
} catch {
177
// ignored, no Python debugger extension installed or an error therein
178
}
179
}
180
if (!command) {
181
command = 'debugpy';
182
}
183
184
await Promise.race([
185
// eslint-disable-next-line local/code-no-dangerous-type-assertions
186
this._debugService.startDebugging(undefined, {
187
type: 'debugpy',
188
name,
189
request: 'attach',
190
listen: {
191
host: DEBUG_HOST,
192
port
193
},
194
...commonConfig,
195
} as IConfig, options),
196
this.ensureListeningOnPort(port)
197
]);
198
199
return { ...launch, command, args };
200
}
201
default:
202
assertNever(definition.devMode.debug, `Unknown debug type ${JSON.stringify(definition.devMode.debug)}`);
203
}
204
}
205
206
protected ensureListeningOnPort(port: number): Promise<void> {
207
return Promise.resolve();
208
}
209
210
protected getDebugPort() {
211
return Promise.resolve(9230);
212
}
213
}
214
215