Path: blob/main/extensions/copilot/src/extension/githubMcp/common/githubMcpDefinitionProvider.ts
13399 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 * as l10n from '@vscode/l10n';6import type { CancellationToken, McpHttpServerDefinition, McpServerDefinitionProvider } from 'vscode';7import { authProviderId, IAuthenticationService } from '../../../platform/authentication/common/authentication';8import { AuthProviderId, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';9import { ILogService } from '../../../platform/log/common/logService';10import { Event } from '../../../util/vs/base/common/event';11import { URI } from '../../../util/vs/base/common/uri';1213const EnterpriseURLConfig = 'github-enterprise.uri';1415export class GitHubMcpDefinitionProvider implements McpServerDefinitionProvider<McpHttpServerDefinition> {1617readonly onDidChangeMcpServerDefinitions: Event<void>;1819private _askedForAuth = false;2021constructor(22@IConfigurationService private readonly configurationService: IConfigurationService,23@IAuthenticationService private readonly authenticationService: IAuthenticationService,24@ILogService private readonly logService: ILogService25) {26const configurationEvent = Event.chain(configurationService.onDidChangeConfiguration, $ => $27.filter(e => {28// If they change the toolsets29if (e.affectsConfiguration(ConfigKey.GitHubMcpToolsets.fullyQualifiedId)) {30logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP toolsets.');31return true;32}33// If they change readonly mode34if (e.affectsConfiguration(ConfigKey.GitHubMcpReadonly.fullyQualifiedId)) {35logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP readonly mode.');36return true;37}38// If they change lockdown mode39if (e.affectsConfiguration(ConfigKey.GitHubMcpLockdown.fullyQualifiedId)) {40logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP lockdown mode.');41return true;42}43// If they change the channel44if (e.affectsConfiguration(ConfigKey.GitHubMcpChannel.fullyQualifiedId)) {45logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP channel.');46return true;47}48// If they change to GHE or GitHub.com49if (e.affectsConfiguration(ConfigKey.Shared.AuthProvider.fullyQualifiedId)) {50logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub auth provider.');51return true;52}53// If they change the GHE URL54if (e.affectsConfiguration(EnterpriseURLConfig)) {55logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub Enterprise URL.');56return true;57}58return false;59})60// void event61.map(() => { })62);63let havePermissiveToken = !!this.authenticationService.permissiveGitHubSession;64const authEvent = Event.chain(this.authenticationService.onDidAuthenticationChange, $ => $65.filter(() => {66const hadToken = havePermissiveToken;67havePermissiveToken = !!this.authenticationService.permissiveGitHubSession;68return hadToken !== havePermissiveToken;69})70.map(() => {71this.logService.debug(`GitHubMcpDefinitionProvider: Permissive GitHub session availability changed: ${havePermissiveToken}`);72})73);74this.onDidChangeMcpServerDefinitions = Event.any(configurationEvent, authEvent);75}7677private get toolsets(): string[] {78return this.configurationService.getConfig<string[]>(ConfigKey.GitHubMcpToolsets);79}8081private get readonly(): boolean {82return this.configurationService.getConfig<boolean>(ConfigKey.GitHubMcpReadonly);83}8485private get lockdown(): boolean {86return this.configurationService.getConfig<boolean>(ConfigKey.GitHubMcpLockdown);87}8889private get channel(): ConfigKey.GitHubMcpChannelValue {90return this.configurationService.getConfig<ConfigKey.GitHubMcpChannelValue>(ConfigKey.GitHubMcpChannel);91}9293private get gheConfig(): string | undefined {94return this.configurationService.getNonExtensionConfig<string>(EnterpriseURLConfig);95}9697private getGheUri(): URI {98const uri = this.gheConfig;99if (!uri) {100throw new Error('GitHub Enterprise URI is not configured.');101}102// Prefix with 'copilot-api.'103const url = URI.parse(uri).with({ path: '/mcp/' });104return url.with({ authority: `copilot-api.${url.authority}` });105}106107provideMcpServerDefinitions(): McpHttpServerDefinition[] {108const providerId = authProviderId(this.configurationService);109const toolsets = this.toolsets.sort().join(',');110const readonly = this.readonly;111const lockdown = this.lockdown;112const channel = this.channel;113const isSignedIn = !!this.authenticationService.permissiveGitHubSession;114115const basics = providerId === AuthProviderId.GitHubEnterprise116? { label: 'GitHub Enterprise', uri: this.getGheUri() }117: { label: 'GitHub', uri: URI.parse('https://api.githubcopilot.com/mcp/') };118119// Build headers object conditionally120const headers: Record<string, string> = {};121// Build version string with toolsets and flags122let version: string;123if (isSignedIn) {124version = toolsets.length ? toolsets : '0';125if (toolsets.length > 0) {126headers['X-MCP-Toolsets'] = toolsets;127}128if (readonly) {129headers['X-MCP-Readonly'] = 'true';130version += '|readonly';131}132if (lockdown) {133headers['X-MCP-Lockdown'] = 'true';134version += '|lockdown';135}136if (channel === 'insiders') {137headers['X-MCP-Insiders'] = 'true';138version += '|insiders';139}140} else {141version = 'signedout';142}143return [144{145...basics,146headers,147version148}149];150}151152async resolveMcpServerDefinition(server: McpHttpServerDefinition, token: CancellationToken): Promise<McpHttpServerDefinition> {153const accessToken = this.authenticationService.permissiveGitHubSession?.accessToken;154if (accessToken) {155server.headers['Authorization'] = `Bearer ${accessToken}`;156return server;157}158159if (this._askedForAuth) {160throw new Error('User denied authentication. Cannot connect to GitHub MCP Server.');161}162163try {164const session = await this.authenticationService.getGitHubSession('permissive', {165createIfNone: {166detail: l10n.t('Additional permissions are required to use GitHub MCP Server'),167},168});169server.headers['Authorization'] = `Bearer ${session.accessToken}`;170return server;171} finally {172this._askedForAuth = true;173}174}175}176177178