Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/mcpHandler.ts
13405 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 type { Session, SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
7
import type { CancellationToken } from 'vscode';
8
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
9
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
10
import { ILogService } from '../../../../platform/log/common/logService';
11
import { IMcpService } from '../../../../platform/mcp/common/mcpService';
12
import { createServiceIdentifier } from '../../../../util/common/services';
13
import { Disposable, DisposableStore, IDisposable } from '../../../../util/vs/base/common/lifecycle';
14
import { hasKey } from '../../../../util/vs/base/common/types';
15
import { URI } from '../../../../util/vs/base/common/uri';
16
import type { LanguageModelToolInformation } from '../../../../vscodeTypes';
17
import { GitHubMcpDefinitionProvider } from '../../../githubMcp/common/githubMcpDefinitionProvider';
18
19
const toolInvalidCharRe = /[^a-z0-9_-]/gi;
20
21
/** The user-facing display label of an MCP server (from VS Code settings). */
22
export type MCPDisplayName = string;
23
/** The short server name as used in agent definition files (the prefix of `fullReferenceName`). */
24
export type MCPServerName = string;
25
26
/**
27
* A mapping from friendly MCP server names (as defined in custom agent files)
28
* to VS Code MCP server display labels.
29
*/
30
export type McpServerMappings = Map<MCPServerName, MCPDisplayName>;
31
32
export type MCPServerConfig = NonNullable<Session['mcpServers']>[string];
33
34
export interface ICopilotCLIMCPHandler {
35
readonly _serviceBrand: undefined;
36
loadMcpConfig(sessionUri: URI): Promise<{ mcpConfig: Record<string, MCPServerConfig> | undefined; disposable: IDisposable }>;
37
}
38
39
export const ICopilotCLIMCPHandler = createServiceIdentifier<ICopilotCLIMCPHandler>('ICopilotCLIMCPHandler');
40
41
export class CopilotCLIMCPHandler implements ICopilotCLIMCPHandler {
42
declare _serviceBrand: undefined;
43
constructor(
44
@ILogService private readonly logService: ILogService,
45
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
46
@IConfigurationService private readonly configurationService: IConfigurationService,
47
@IMcpService private readonly mcpService: IMcpService,
48
) { }
49
50
public async loadMcpConfig(sessionUri: URI): Promise<{ mcpConfig: Record<string, MCPServerConfig> | undefined; disposable: IDisposable }> {
51
52
// TODO: Sessions window settings override is not honored with extension
53
// configuration API, so this needs to be a core setting
54
const isSessionsWindow = this.configurationService.getNonExtensionConfig<boolean>('chat.experimentalSessionsWindowOverride') ?? false;
55
56
// Sessions window: use the gateway approach which proxies all MCP servers from core
57
if (isSessionsWindow) {
58
return this.loadMcpConfigWithGateway(sessionUri);
59
}
60
61
// Standard path: use the CLIMCPServerEnabled setting
62
const enabled = this.configurationService.getConfig(ConfigKey.Advanced.CLIMCPServerEnabled);
63
this.logService.info(`[CopilotCLIMCPHandler] loadMcpConfig called. CLIMCPServerEnabled=${enabled}`);
64
65
if (enabled) {
66
this.logService.info('[CopilotCLIMCPHandler] MCP server forwarding is enabled, using gateway configuration');
67
return this.loadMcpConfigWithGateway(sessionUri);
68
}
69
70
const processedConfig: Record<string, MCPServerConfig> = {};
71
await this.addBuiltInGitHubServer(processedConfig);
72
return {
73
mcpConfig: Object.keys(processedConfig).length > 0 ? processedConfig : undefined,
74
disposable: Disposable.None
75
};
76
}
77
78
/**
79
* Use the Gateway to handle all connections
80
*/
81
private async loadMcpConfigWithGateway(sessionUri: URI): Promise<{ mcpConfig: Record<string, MCPServerConfig> | undefined; disposable: IDisposable }> {
82
const mcpConfig: Record<string, MCPServerConfig> = {};
83
const disposable = new DisposableStore();
84
try {
85
const gateway = await this.mcpService.startMcpGateway(sessionUri);
86
if (gateway) {
87
disposable.add(gateway);
88
for (const server of gateway.servers) {
89
const serverId = this.normalizeServerName(server.label) ?? `vscode-mcp-server-${Object.keys(mcpConfig).length}`;
90
mcpConfig[serverId] = {
91
type: 'http',
92
url: server.address.toString(),
93
tools: ['*'],
94
displayName: server.label,
95
};
96
}
97
const serverIds = Object.keys(mcpConfig);
98
this.logService.trace(`[CopilotCLIMCPHandler] gateway started, server(s): [${serverIds.join(', ')}]`);
99
} else {
100
this.logService.warn('[CopilotCLIMCPHandler] gateway failed to start');
101
disposable.dispose();
102
}
103
} catch (error) {
104
this.logService.warn(`[CopilotCLIMCPHandler] gateway error: ${error}`);
105
}
106
107
if (Object.keys(mcpConfig).length === 0) {
108
disposable.dispose();
109
return {
110
mcpConfig: undefined,
111
disposable: Disposable.None
112
};
113
} else {
114
return {
115
mcpConfig,
116
disposable
117
};
118
}
119
}
120
121
private normalizeServerName(originalName: string): string | undefined {
122
// Convert to lowercase and replace invalid characters with underscore
123
let normalized = originalName.toLowerCase().replace(toolInvalidCharRe, '_');
124
125
// Trim leading and trailing underscores
126
normalized = normalized.replace(/^_+|_+$/g, '');
127
128
// Return undefined if normalization results in empty string
129
if (!normalized) {
130
this.logService.error(`[CopilotCLIMCPHandler] Failed to normalize server name '${originalName}' - result is empty`);
131
return undefined;
132
}
133
134
if (normalized !== originalName) {
135
this.logService.trace(`[CopilotCLIMCPHandler] Normalized server '${originalName}' to '${normalized}'`);
136
}
137
138
return normalized;
139
}
140
141
private async addBuiltInGitHubServer(config: Record<string, MCPServerConfig>): Promise<void> {
142
try {
143
const githubId = this.normalizeServerName('gitHub');
144
if (!githubId) {
145
return;
146
}
147
148
// Override only if no GitHub MCP server is already configured
149
if (config[githubId] && config[githubId].type === 'http') {
150
// We have headers, do not override
151
if (Object.keys(config[githubId].headers || {}).length > 0) {
152
return;
153
}
154
}
155
156
const definitionProvider = new GitHubMcpDefinitionProvider(
157
this.configurationService,
158
this.authenticationService,
159
this.logService
160
);
161
162
const definitions = definitionProvider.provideMcpServerDefinitions();
163
if (!definitions || definitions.length === 0) {
164
this.logService.trace('[CopilotCLIMCPHandler] No GitHub MCP server definitions available.');
165
return;
166
}
167
168
// Use the first definition
169
const definition = definitions[0];
170
171
// Resolve the definition to get the access token
172
const resolvedDefinition = await definitionProvider.resolveMcpServerDefinition(definition, {} as CancellationToken);
173
174
config[githubId] = {
175
type: 'http',
176
url: resolvedDefinition.uri.toString(),
177
isDefaultServer: true,
178
headers: resolvedDefinition.headers,
179
tools: ['*'],
180
displayName: 'GitHub',
181
};
182
this.logService.trace('[CopilotCLIMCPHandler] Added built-in GitHub MCP server.');
183
} catch (error) {
184
this.logService.warn(`[CopilotCLIMCPHandler] Failed to add built-in GitHub MCP server: ${error}`);
185
}
186
}
187
}
188
189
/**
190
* Builds a mapping from friendly MCP server names (as defined in custom agent files)
191
* to VS Code MCP server labels.
192
*
193
* Iterates through tools that have an MCP source (detected via structural typing using
194
* {@link hasKey}) and a `fullReferenceName` in the format `<server name>/<tool name>`,
195
* extracting the server name portion as the key and the source's `label` as the value.
196
*/
197
export function buildMcpServerMappings(tools: ReadonlyMap<LanguageModelToolInformation, boolean>): McpServerMappings {
198
const mappings = new Map<string, string>();
199
for (const [tool] of tools) {
200
if (!tool.source || !hasKey(tool.source, { name: true }) || !tool.fullReferenceName) {
201
continue;
202
}
203
const slashIndex = tool.fullReferenceName.lastIndexOf('/');
204
if (slashIndex > 0) {
205
const serverName = tool.fullReferenceName.substring(0, slashIndex);
206
if (serverName && !mappings.has(serverName) && tool.source.label) {
207
mappings.set(serverName, tool.source.label);
208
}
209
}
210
}
211
return mappings;
212
}
213
214
/**
215
* Remaps tool references in custom agents from friendly MCP server names to gateway names.
216
*
217
* Agent definition files reference tools as `<friendly server name>/<tool name>`, but the SDK
218
* expects `<gateway name>/<tool name>` where gateway names are the Record keys in the MCP
219
* server config.
220
*
221
* @param customAgents The list of custom agents whose tools will be remapped in place.
222
* @param mcpServerMappings Maps friendly server names (from agent files) → VS Code MCP display labels.
223
* @param mcpServers The MCP server config, keyed by gateway name.
224
* @param selectedAgent Optional selected agent to also remap.
225
*/
226
export function remapCustomAgentTools(
227
customAgents: SweCustomAgent[],
228
mcpServerMappings: McpServerMappings,
229
mcpServers: SessionOptions['mcpServers'],
230
selectedAgent: SweCustomAgent | undefined,
231
): void {
232
if (!mcpServerMappings.size || !mcpServers) {
233
return;
234
}
235
// Build a map from display name → gateway name (the Record key in mcpServers).
236
const displayNameToGatewayName = new Map<string, string>();
237
for (const [gatewayName, config] of Object.entries(mcpServers)) {
238
if (config.displayName) {
239
displayNameToGatewayName.set(config.displayName, gatewayName);
240
}
241
}
242
243
const agentsToRemap = selectedAgent ? [...customAgents, selectedAgent] : customAgents;
244
for (const agent of agentsToRemap) {
245
if (!agent.tools?.length) {
246
continue;
247
}
248
for (let i = 0; i < agent.tools.length; i++) {
249
const tool = agent.tools[i];
250
const slashIndex = tool.lastIndexOf('/'); // Tool names cannot contain '/', so the last slash separates server from tool
251
if (slashIndex < 1) {
252
continue;
253
}
254
const serverName = tool.substring(0, slashIndex);
255
const toolName = tool.substring(slashIndex + 1);
256
if (!serverName || !toolName) {
257
continue;
258
}
259
// First try: map through mcpServerMappings (friendly name → display name) then to gateway name.
260
const displayName = mcpServerMappings.get(serverName);
261
// Also try to look up the server name directly as a display name in the gateway map.
262
const gatewayName = displayName ? displayNameToGatewayName.get(displayName) : displayNameToGatewayName.get(serverName);
263
264
if (gatewayName) {
265
agent.tools[i] = `${gatewayName}/${toolName}`;
266
}
267
}
268
}
269
}
270
271