-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmcp-provider.ts
More file actions
161 lines (147 loc) · 5.59 KB
/
mcp-provider.ts
File metadata and controls
161 lines (147 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import * as vscode from 'vscode';
import { DisposableObject } from '../common/disposable';
import type { Logger } from '../common/logger';
import type { ServerManager } from './server-manager';
import type { EnvironmentBuilder } from '../bridge/environment-builder';
/**
* Implements `McpServerDefinitionProvider` to programmatically register
* the `codeql-development-mcp-server` as an MCP server in VS Code.
*
* The server is launched from the bundled copy inside the VSIX by default
* (`node server/dist/codeql-development-mcp-server.js`). Falls back to
* `npx -y codeql-development-mcp-server` if the bundle is missing.
* Override via `codeql-mcp.serverCommand` / `codeql-mcp.serverArgs` settings.
*/
export class McpProvider
extends DisposableObject
implements vscode.McpServerDefinitionProvider<vscode.McpStdioServerDefinition>
{
private readonly _onDidChange = new vscode.EventEmitter<void>();
readonly onDidChangeMcpServerDefinitions = this._onDidChange.event;
/**
* Monotonically increasing counter, bumped by `requestRestart()`.
* Appended to the version string so that VS Code sees a genuinely new
* server definition and triggers a stop → restart cycle.
*/
private _revision = 0;
/**
* Cached extension version to avoid repeated synchronous `readFileSync`
* calls on every `getEffectiveVersion()` invocation.
*/
private readonly _extensionVersion: string;
/**
* Handle for the pending debounced `fireDidChange()` timer.
* Used to coalesce rapid file-system events into a single notification.
*/
private _debounceTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
constructor(
private readonly serverManager: ServerManager,
private readonly envBuilder: EnvironmentBuilder,
private readonly logger: Logger,
) {
super();
this._extensionVersion = serverManager.getExtensionVersion();
this.push(this._onDidChange);
}
override dispose(): void {
if (this._debounceTimer !== undefined) {
globalThis.clearTimeout(this._debounceTimer);
this._debounceTimer = undefined;
}
super.dispose();
}
/**
* Soft notification: tell VS Code that definitions may have changed.
*
* Does NOT bump the version, so VS Code will re-query
* `provideMcpServerDefinitions()` / `resolveMcpServerDefinition()` but
* will NOT restart the server. Use for lightweight updates (file watcher
* events, extension changes, background install completion) where the
* running server can continue with its current environment.
*
* Debounced: rapid-fire calls (e.g. from file-system watchers during
* a build) are coalesced into a single notification after a short delay.
*/
fireDidChange(): void {
if (this._debounceTimer !== undefined) {
globalThis.clearTimeout(this._debounceTimer);
}
this._debounceTimer = globalThis.setTimeout(() => {
this._debounceTimer = undefined;
this._onDidChange.fire();
}, 1_000);
}
/**
* Request that VS Code restart the MCP server with a fresh environment.
*
* Invalidates the cached environment and bumps the internal revision counter
* so that the next call to `provideMcpServerDefinitions()` returns a
* definition with a different `version` string. VS Code compares the new
* version to the running server's version and, seeing a change, triggers a
* stop → start cycle.
*
* Use for changes that require a server restart (configuration changes).
*/
requestRestart(): void {
// Cancel any pending debounced fireDidChange — the restart supersedes it.
if (this._debounceTimer !== undefined) {
globalThis.clearTimeout(this._debounceTimer);
this._debounceTimer = undefined;
}
this.envBuilder.invalidate();
this._revision++;
this.logger.info(
`Requesting ql-mcp restart (revision ${this._revision})...`,
);
this._onDidChange.fire();
}
async provideMcpServerDefinitions(
_token: vscode.CancellationToken,
): Promise<vscode.McpStdioServerDefinition[]> {
const command = this.serverManager.getCommand();
const args = this.serverManager.getArgs();
const env = await this.envBuilder.build();
const version = this.getEffectiveVersion();
this.logger.info(
`Providing MCP server definition: ${command} ${args.join(' ')}`,
);
const definition = new vscode.McpStdioServerDefinition(
'ql-mcp',
command,
args,
env,
version,
);
return [definition];
}
async resolveMcpServerDefinition(
server: vscode.McpStdioServerDefinition,
_token: vscode.CancellationToken,
): Promise<vscode.McpStdioServerDefinition | undefined> {
// Refresh environment in case it changed since provideMcpServerDefinitions
const env = await this.envBuilder.build();
server.env = env;
return server;
}
/**
* Computes the version string for the MCP server definition.
*
* Always returns a concrete string so that VS Code has a reliable
* baseline for version comparison. When `serverManager.getVersion()`
* returns `undefined` (the "latest" / unpinned case), the extension
* version is used as the base instead.
*
* After one or more `requestRestart()` calls, a `+rN` revision suffix
* is appended so that the version is always different from the
* previous one. VS Code uses the version to decide whether to
* restart a running server: a changed version triggers a stop → start
* cycle.
*/
private getEffectiveVersion(): string {
const base = this.serverManager.getVersion() ?? this._extensionVersion;
if (this._revision === 0) {
return base;
}
return `${base}+r${this._revision}`;
}
}