Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/mcpHandler.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type { Session, SessionOptions, SweCustomAgent } from '@github/copilot/sdk';6import type { CancellationToken } from 'vscode';7import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';8import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';9import { ILogService } from '../../../../platform/log/common/logService';10import { IMcpService } from '../../../../platform/mcp/common/mcpService';11import { createServiceIdentifier } from '../../../../util/common/services';12import { Disposable, DisposableStore, IDisposable } from '../../../../util/vs/base/common/lifecycle';13import { hasKey } from '../../../../util/vs/base/common/types';14import { URI } from '../../../../util/vs/base/common/uri';15import type { LanguageModelToolInformation } from '../../../../vscodeTypes';16import { GitHubMcpDefinitionProvider } from '../../../githubMcp/common/githubMcpDefinitionProvider';1718const toolInvalidCharRe = /[^a-z0-9_-]/gi;1920/** The user-facing display label of an MCP server (from VS Code settings). */21export type MCPDisplayName = string;22/** The short server name as used in agent definition files (the prefix of `fullReferenceName`). */23export type MCPServerName = string;2425/**26* A mapping from friendly MCP server names (as defined in custom agent files)27* to VS Code MCP server display labels.28*/29export type McpServerMappings = Map<MCPServerName, MCPDisplayName>;3031export type MCPServerConfig = NonNullable<Session['mcpServers']>[string];3233export interface ICopilotCLIMCPHandler {34readonly _serviceBrand: undefined;35loadMcpConfig(sessionUri: URI): Promise<{ mcpConfig: Record<string, MCPServerConfig> | undefined; disposable: IDisposable }>;36}3738export const ICopilotCLIMCPHandler = createServiceIdentifier<ICopilotCLIMCPHandler>('ICopilotCLIMCPHandler');3940export class CopilotCLIMCPHandler implements ICopilotCLIMCPHandler {41declare _serviceBrand: undefined;42constructor(43@ILogService private readonly logService: ILogService,44@IAuthenticationService private readonly authenticationService: IAuthenticationService,45@IConfigurationService private readonly configurationService: IConfigurationService,46@IMcpService private readonly mcpService: IMcpService,47) { }4849public async loadMcpConfig(sessionUri: URI): Promise<{ mcpConfig: Record<string, MCPServerConfig> | undefined; disposable: IDisposable }> {5051// TODO: Sessions window settings override is not honored with extension52// configuration API, so this needs to be a core setting53const isSessionsWindow = this.configurationService.getNonExtensionConfig<boolean>('chat.experimentalSessionsWindowOverride') ?? false;5455// Sessions window: use the gateway approach which proxies all MCP servers from core56if (isSessionsWindow) {57return this.loadMcpConfigWithGateway(sessionUri);58}5960// Standard path: use the CLIMCPServerEnabled setting61const enabled = this.configurationService.getConfig(ConfigKey.Advanced.CLIMCPServerEnabled);62this.logService.info(`[CopilotCLIMCPHandler] loadMcpConfig called. CLIMCPServerEnabled=${enabled}`);6364if (enabled) {65this.logService.info('[CopilotCLIMCPHandler] MCP server forwarding is enabled, using gateway configuration');66return this.loadMcpConfigWithGateway(sessionUri);67}6869const processedConfig: Record<string, MCPServerConfig> = {};70await this.addBuiltInGitHubServer(processedConfig);71return {72mcpConfig: Object.keys(processedConfig).length > 0 ? processedConfig : undefined,73disposable: Disposable.None74};75}7677/**78* Use the Gateway to handle all connections79*/80private async loadMcpConfigWithGateway(sessionUri: URI): Promise<{ mcpConfig: Record<string, MCPServerConfig> | undefined; disposable: IDisposable }> {81const mcpConfig: Record<string, MCPServerConfig> = {};82const disposable = new DisposableStore();83try {84const gateway = await this.mcpService.startMcpGateway(sessionUri);85if (gateway) {86disposable.add(gateway);87for (const server of gateway.servers) {88const serverId = this.normalizeServerName(server.label) ?? `vscode-mcp-server-${Object.keys(mcpConfig).length}`;89mcpConfig[serverId] = {90type: 'http',91url: server.address.toString(),92tools: ['*'],93displayName: server.label,94};95}96const serverIds = Object.keys(mcpConfig);97this.logService.trace(`[CopilotCLIMCPHandler] gateway started, server(s): [${serverIds.join(', ')}]`);98} else {99this.logService.warn('[CopilotCLIMCPHandler] gateway failed to start');100disposable.dispose();101}102} catch (error) {103this.logService.warn(`[CopilotCLIMCPHandler] gateway error: ${error}`);104}105106if (Object.keys(mcpConfig).length === 0) {107disposable.dispose();108return {109mcpConfig: undefined,110disposable: Disposable.None111};112} else {113return {114mcpConfig,115disposable116};117}118}119120private normalizeServerName(originalName: string): string | undefined {121// Convert to lowercase and replace invalid characters with underscore122let normalized = originalName.toLowerCase().replace(toolInvalidCharRe, '_');123124// Trim leading and trailing underscores125normalized = normalized.replace(/^_+|_+$/g, '');126127// Return undefined if normalization results in empty string128if (!normalized) {129this.logService.error(`[CopilotCLIMCPHandler] Failed to normalize server name '${originalName}' - result is empty`);130return undefined;131}132133if (normalized !== originalName) {134this.logService.trace(`[CopilotCLIMCPHandler] Normalized server '${originalName}' to '${normalized}'`);135}136137return normalized;138}139140private async addBuiltInGitHubServer(config: Record<string, MCPServerConfig>): Promise<void> {141try {142const githubId = this.normalizeServerName('gitHub');143if (!githubId) {144return;145}146147// Override only if no GitHub MCP server is already configured148if (config[githubId] && config[githubId].type === 'http') {149// We have headers, do not override150if (Object.keys(config[githubId].headers || {}).length > 0) {151return;152}153}154155const definitionProvider = new GitHubMcpDefinitionProvider(156this.configurationService,157this.authenticationService,158this.logService159);160161const definitions = definitionProvider.provideMcpServerDefinitions();162if (!definitions || definitions.length === 0) {163this.logService.trace('[CopilotCLIMCPHandler] No GitHub MCP server definitions available.');164return;165}166167// Use the first definition168const definition = definitions[0];169170// Resolve the definition to get the access token171const resolvedDefinition = await definitionProvider.resolveMcpServerDefinition(definition, {} as CancellationToken);172173config[githubId] = {174type: 'http',175url: resolvedDefinition.uri.toString(),176isDefaultServer: true,177headers: resolvedDefinition.headers,178tools: ['*'],179displayName: 'GitHub',180};181this.logService.trace('[CopilotCLIMCPHandler] Added built-in GitHub MCP server.');182} catch (error) {183this.logService.warn(`[CopilotCLIMCPHandler] Failed to add built-in GitHub MCP server: ${error}`);184}185}186}187188/**189* Builds a mapping from friendly MCP server names (as defined in custom agent files)190* to VS Code MCP server labels.191*192* Iterates through tools that have an MCP source (detected via structural typing using193* {@link hasKey}) and a `fullReferenceName` in the format `<server name>/<tool name>`,194* extracting the server name portion as the key and the source's `label` as the value.195*/196export function buildMcpServerMappings(tools: ReadonlyMap<LanguageModelToolInformation, boolean>): McpServerMappings {197const mappings = new Map<string, string>();198for (const [tool] of tools) {199if (!tool.source || !hasKey(tool.source, { name: true }) || !tool.fullReferenceName) {200continue;201}202const slashIndex = tool.fullReferenceName.lastIndexOf('/');203if (slashIndex > 0) {204const serverName = tool.fullReferenceName.substring(0, slashIndex);205if (serverName && !mappings.has(serverName) && tool.source.label) {206mappings.set(serverName, tool.source.label);207}208}209}210return mappings;211}212213/**214* Remaps tool references in custom agents from friendly MCP server names to gateway names.215*216* Agent definition files reference tools as `<friendly server name>/<tool name>`, but the SDK217* expects `<gateway name>/<tool name>` where gateway names are the Record keys in the MCP218* server config.219*220* @param customAgents The list of custom agents whose tools will be remapped in place.221* @param mcpServerMappings Maps friendly server names (from agent files) → VS Code MCP display labels.222* @param mcpServers The MCP server config, keyed by gateway name.223* @param selectedAgent Optional selected agent to also remap.224*/225export function remapCustomAgentTools(226customAgents: SweCustomAgent[],227mcpServerMappings: McpServerMappings,228mcpServers: SessionOptions['mcpServers'],229selectedAgent: SweCustomAgent | undefined,230): void {231if (!mcpServerMappings.size || !mcpServers) {232return;233}234// Build a map from display name → gateway name (the Record key in mcpServers).235const displayNameToGatewayName = new Map<string, string>();236for (const [gatewayName, config] of Object.entries(mcpServers)) {237if (config.displayName) {238displayNameToGatewayName.set(config.displayName, gatewayName);239}240}241242const agentsToRemap = selectedAgent ? [...customAgents, selectedAgent] : customAgents;243for (const agent of agentsToRemap) {244if (!agent.tools?.length) {245continue;246}247for (let i = 0; i < agent.tools.length; i++) {248const tool = agent.tools[i];249const slashIndex = tool.lastIndexOf('/'); // Tool names cannot contain '/', so the last slash separates server from tool250if (slashIndex < 1) {251continue;252}253const serverName = tool.substring(0, slashIndex);254const toolName = tool.substring(slashIndex + 1);255if (!serverName || !toolName) {256continue;257}258// First try: map through mcpServerMappings (friendly name → display name) then to gateway name.259const displayName = mcpServerMappings.get(serverName);260// Also try to look up the server name directly as a display name in the gateway map.261const gatewayName = displayName ? displayNameToGatewayName.get(displayName) : displayNameToGatewayName.get(serverName);262263if (gatewayName) {264agent.tools[i] = `${gatewayName}/${toolName}`;265}266}267}268}269270271