|
| 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 * as l10n from '@vscode/l10n'; |
| 7 | +import type { CancellationToken, McpHttpServerDefinition, McpServerDefinitionProvider } from 'vscode'; |
| 8 | +import { authProviderId, IAuthenticationService } from '../../../platform/authentication/common/authentication'; |
| 9 | +import { AuthProviderId, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; |
| 10 | +import { ILogService } from '../../../platform/log/common/logService'; |
| 11 | +import { Event } from '../../../util/vs/base/common/event'; |
| 12 | +import { URI } from '../../../util/vs/base/common/uri'; |
| 13 | + |
| 14 | +const EnterpriseURLConfig = 'github-enterprise.uri'; |
| 15 | + |
| 16 | +export class GitHubMcpDefinitionProvider implements McpServerDefinitionProvider<McpHttpServerDefinition> { |
| 17 | + |
| 18 | + readonly onDidChangeMcpServerDefinitions: Event<void>; |
| 19 | + |
| 20 | + constructor( |
| 21 | + @IConfigurationService private readonly configurationService: IConfigurationService, |
| 22 | + @IAuthenticationService private readonly authenticationService: IAuthenticationService, |
| 23 | + @ILogService private readonly logService: ILogService |
| 24 | + ) { |
| 25 | + const configurationEvent = Event.chain(configurationService.onDidChangeConfiguration, $ => $ |
| 26 | + .filter(e => { |
| 27 | + // If they change the toolsets |
| 28 | + if (e.affectsConfiguration(ConfigKey.GitHubMcpToolsets.fullyQualifiedId)) { |
| 29 | + logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP toolsets.'); |
| 30 | + return true; |
| 31 | + } |
| 32 | + // If they change readonly mode |
| 33 | + if (e.affectsConfiguration(ConfigKey.GitHubMcpReadonly.fullyQualifiedId)) { |
| 34 | + logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP readonly mode.'); |
| 35 | + return true; |
| 36 | + } |
| 37 | + // If they change lockdown mode |
| 38 | + if (e.affectsConfiguration(ConfigKey.GitHubMcpLockdown.fullyQualifiedId)) { |
| 39 | + logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP lockdown mode.'); |
| 40 | + return true; |
| 41 | + } |
| 42 | + // If they change to GHE or GitHub.com |
| 43 | + if (e.affectsConfiguration(ConfigKey.Shared.AuthProvider.fullyQualifiedId)) { |
| 44 | + logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub auth provider.'); |
| 45 | + return true; |
| 46 | + } |
| 47 | + // If they change the GHE URL |
| 48 | + if (e.affectsConfiguration(EnterpriseURLConfig)) { |
| 49 | + logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub Enterprise URL.'); |
| 50 | + return true; |
| 51 | + } |
| 52 | + return false; |
| 53 | + }) |
| 54 | + // void event |
| 55 | + .map(() => { }) |
| 56 | + ); |
| 57 | + let havePermissiveToken = !!this.authenticationService.permissiveGitHubSession; |
| 58 | + const authEvent = Event.chain(this.authenticationService.onDidAuthenticationChange, $ => $ |
| 59 | + .filter(() => { |
| 60 | + const hadToken = havePermissiveToken; |
| 61 | + havePermissiveToken = !!this.authenticationService.permissiveGitHubSession; |
| 62 | + return hadToken !== havePermissiveToken; |
| 63 | + }) |
| 64 | + .map(() => { |
| 65 | + this.logService.debug(`GitHubMcpDefinitionProvider: Permissive GitHub session availability changed: ${havePermissiveToken}`); |
| 66 | + }) |
| 67 | + ); |
| 68 | + this.onDidChangeMcpServerDefinitions = Event.any(configurationEvent, authEvent); |
| 69 | + } |
| 70 | + |
| 71 | + private get toolsets(): string[] { |
| 72 | + return this.configurationService.getConfig<string[]>(ConfigKey.GitHubMcpToolsets); |
| 73 | + } |
| 74 | + |
| 75 | + private get readonly(): boolean { |
| 76 | + return this.configurationService.getConfig<boolean>(ConfigKey.GitHubMcpReadonly); |
| 77 | + } |
| 78 | + |
| 79 | + private get lockdown(): boolean { |
| 80 | + return this.configurationService.getConfig<boolean>(ConfigKey.GitHubMcpLockdown); |
| 81 | + } |
| 82 | + |
| 83 | + private get gheConfig(): string | undefined { |
| 84 | + return this.configurationService.getNonExtensionConfig<string>(EnterpriseURLConfig); |
| 85 | + } |
| 86 | + |
| 87 | + private getGheUri(): URI { |
| 88 | + const uri = this.gheConfig; |
| 89 | + if (!uri) { |
| 90 | + throw new Error('GitHub Enterprise URI is not configured.'); |
| 91 | + } |
| 92 | + // Prefix with 'copilot-api.' |
| 93 | + const url = URI.parse(uri).with({ path: '/mcp/' }); |
| 94 | + return url.with({ authority: `copilot-api.${url.authority}` }); |
| 95 | + } |
| 96 | + |
| 97 | + provideMcpServerDefinitions(): McpHttpServerDefinition[] { |
| 98 | + const providerId = authProviderId(this.configurationService); |
| 99 | + const toolsets = this.toolsets.sort().join(','); |
| 100 | + const readonly = this.readonly; |
| 101 | + const lockdown = this.lockdown; |
| 102 | + |
| 103 | + const basics = providerId === AuthProviderId.GitHubEnterprise |
| 104 | + ? { label: 'GitHub Enterprise', uri: this.getGheUri() } |
| 105 | + : { label: 'GitHub', uri: URI.parse('https://api.githubcopilot.com/mcp/') }; |
| 106 | + |
| 107 | + // Build headers object conditionally |
| 108 | + const headers: Record<string, string> = {}; |
| 109 | + // Build version string with toolsets and flags |
| 110 | + let version = toolsets.length ? toolsets : '0'; |
| 111 | + if (toolsets.length > 0) { |
| 112 | + headers['X-MCP-Toolsets'] = toolsets; |
| 113 | + } |
| 114 | + if (readonly) { |
| 115 | + headers['X-MCP-Readonly'] = 'true'; |
| 116 | + version += '|readonly'; |
| 117 | + } |
| 118 | + if (lockdown) { |
| 119 | + headers['X-MCP-Lockdown'] = 'true'; |
| 120 | + version += '|lockdown'; |
| 121 | + } |
| 122 | + return [ |
| 123 | + { |
| 124 | + ...basics, |
| 125 | + headers, |
| 126 | + version |
| 127 | + } |
| 128 | + ]; |
| 129 | + } |
| 130 | + |
| 131 | + async resolveMcpServerDefinition(server: McpHttpServerDefinition, token: CancellationToken): Promise<McpHttpServerDefinition> { |
| 132 | + const session = await this.authenticationService.getPermissiveGitHubSession({ |
| 133 | + createIfNone: { |
| 134 | + detail: l10n.t('Additional permissions are required to use GitHub MCP Server'), |
| 135 | + }, |
| 136 | + }); |
| 137 | + if (!session) { |
| 138 | + throw new Error('Authentication required'); |
| 139 | + } |
| 140 | + server.headers['Authorization'] = `Bearer ${session.accessToken}`; |
| 141 | + return server; |
| 142 | + } |
| 143 | +} |
0 commit comments