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