Support GitHub MCP in-box (#2051)

* Support GitHub MCP inbox

Fixes https://github.com/microsoft/vscode/issues/254836

* Be auth provider aware, and bump version on every config change

* tests and fixed ghe url

* Move things around so the layers make more sense

* move settings to experimental section

* move def prov

* who needs new APIs anyway?

* Ask for auth

* fix tests

* disable `#githubRepo` when GH MCP is enabled

* Include settings for readonly & lockdown

* misc

* fix test
This commit is contained in:
Tyler James Leonhardt
2025-11-20 16:00:35 +00:00
committed by GitHub
parent 8aaacb4574
commit 2db395aa66
11 changed files with 667 additions and 17 deletions
+42
View File
@@ -1061,6 +1061,7 @@
"modelDescription": "Searches a GitHub repository for relevant source code snippets. Only use this tool if the user is very clearly asking for code snippets from a specific GitHub repository. Do not use this tool for Github repos that the user has open in their workspace.",
"userDescription": "%github.copilot.tools.githubRepo.userDescription%",
"icon": "$(repo)",
"when": "!config.github.copilot.chat.githubMcpServer.enabled",
"inputSchema": {
"type": "object",
"properties": {
@@ -1841,6 +1842,12 @@
"when": "!github.copilot.interactiveSession.disabled"
}
],
"mcpServerDefinitionProviders": [
{
"id": "github",
"label": "GitHub"
}
],
"viewsWelcome": [
{
"view": "debug",
@@ -2745,6 +2752,41 @@
{
"id": "experimental",
"properties": {
"github.copilot.chat.githubMcpServer.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.githubMcpServer.enabled%",
"tags": [
"experimental"
]
},
"github.copilot.chat.githubMcpServer.toolsets": {
"type": "array",
"default": ["default"],
"markdownDescription": "%github.copilot.config.githubMcpServer.toolsets%",
"items": {
"type": "string"
},
"tags": [
"experimental"
]
},
"github.copilot.chat.githubMcpServer.readonly": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.githubMcpServer.readonly%",
"tags": [
"experimental"
]
},
"github.copilot.chat.githubMcpServer.lockdown": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.githubMcpServer.lockdown%",
"tags": [
"experimental"
]
},
"github.copilot.chat.imageUpload.enabled": {
"type": "boolean",
"default": true,
+5 -1
View File
@@ -416,5 +416,9 @@
"github.copilot.cli.sessions.newTerminalSession": "New Agent Session in Terminal",
"github.copilot.command.openCopilotAgentSessionsInBrowser": "Open in Browser",
"github.copilot.command.closeChatSessionPullRequest.title": "Close Pull Request",
"github.copilot.command.applyCopilotCLIAgentSessionChanges": "Apply Changes"
"github.copilot.command.applyCopilotCLIAgentSessionChanges": "Apply Changes",
"github.copilot.config.githubMcpServer.enabled": "Enable built-in support for the GitHub MCP Server.",
"github.copilot.config.githubMcpServer.toolsets": "Specify toolsets to use from the GitHub MCP Server.",
"github.copilot.config.githubMcpServer.readonly": "Enable read-only mode for the GitHub MCP Server. When enabled, only read tools are available.",
"github.copilot.config.githubMcpServer.lockdown": "Enable lockdown mode for the GitHub MCP Server. When enabled, hides public issue details created by users without push access."
}
@@ -24,6 +24,7 @@ import { DiagnosticsContextContribution } from '../../diagnosticsContext/vscode/
import { LanguageModelProxyContrib } from '../../externalAgents/vscode-node/lmProxyContrib';
import { WalkthroughCommandContribution } from '../../getting-started/vscode-node/commands';
import * as newWorkspaceContribution from '../../getting-started/vscode-node/newWorkspace.contribution';
import { GitHubMcpContrib } from '../../githubMcp/vscode-node/githubMcp.contribution';
import { IgnoredFileProviderContribution } from '../../ignore/vscode-node/ignoreProvider';
import { InlineEditProviderFeature } from '../../inlineEdits/vscode-node/inlineEditProviderFeature';
import { FixTestFailureContribution } from '../../intents/vscode-node/fixTestFailureContributions';
@@ -86,7 +87,8 @@ export const vscodeNodeContributions: IExtensionContributionFactory[] = [
asContributionFactory(CompletionsCoreContribution),
asContributionFactory(CompletionsUnificationContribution),
workspaceIndexingContribution,
asContributionFactory(ChatSessionsContrib)
asContributionFactory(ChatSessionsContrib),
asContributionFactory(GitHubMcpContrib)
];
/**
@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as l10n from '@vscode/l10n';
import type { CancellationToken, McpHttpServerDefinition, McpServerDefinitionProvider } from 'vscode';
import { authProviderId, IAuthenticationService } from '../../../platform/authentication/common/authentication';
import { AuthProviderId, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { ILogService } from '../../../platform/log/common/logService';
import { Event } from '../../../util/vs/base/common/event';
import { URI } from '../../../util/vs/base/common/uri';
const EnterpriseURLConfig = 'github-enterprise.uri';
export class GitHubMcpDefinitionProvider implements McpServerDefinitionProvider<McpHttpServerDefinition> {
readonly onDidChangeMcpServerDefinitions: Event<void>;
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@ILogService private readonly logService: ILogService
) {
const configurationEvent = Event.chain(configurationService.onDidChangeConfiguration, $ => $
.filter(e => {
// If they change the toolsets
if (e.affectsConfiguration(ConfigKey.GitHubMcpToolsets.fullyQualifiedId)) {
logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP toolsets.');
return true;
}
// If they change readonly mode
if (e.affectsConfiguration(ConfigKey.GitHubMcpReadonly.fullyQualifiedId)) {
logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP readonly mode.');
return true;
}
// If they change lockdown mode
if (e.affectsConfiguration(ConfigKey.GitHubMcpLockdown.fullyQualifiedId)) {
logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub MCP lockdown mode.');
return true;
}
// If they change to GHE or GitHub.com
if (e.affectsConfiguration(ConfigKey.Shared.AuthProvider.fullyQualifiedId)) {
logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub auth provider.');
return true;
}
// If they change the GHE URL
if (e.affectsConfiguration(EnterpriseURLConfig)) {
logService.debug('GitHubMcpDefinitionProvider: Configuration change affects GitHub Enterprise URL.');
return true;
}
return false;
})
// void event
.map(() => { })
);
let havePermissiveToken = !!this.authenticationService.permissiveGitHubSession;
const authEvent = Event.chain(this.authenticationService.onDidAuthenticationChange, $ => $
.filter(() => {
const hadToken = havePermissiveToken;
havePermissiveToken = !!this.authenticationService.permissiveGitHubSession;
return hadToken !== havePermissiveToken;
})
.map(() => {
this.logService.debug(`GitHubMcpDefinitionProvider: Permissive GitHub session availability changed: ${havePermissiveToken}`);
})
);
this.onDidChangeMcpServerDefinitions = Event.any(configurationEvent, authEvent);
}
private get toolsets(): string[] {
return this.configurationService.getConfig<string[]>(ConfigKey.GitHubMcpToolsets);
}
private get readonly(): boolean {
return this.configurationService.getConfig<boolean>(ConfigKey.GitHubMcpReadonly);
}
private get lockdown(): boolean {
return this.configurationService.getConfig<boolean>(ConfigKey.GitHubMcpLockdown);
}
private get gheConfig(): string | undefined {
return this.configurationService.getNonExtensionConfig<string>(EnterpriseURLConfig);
}
private getGheUri(): URI {
const uri = this.gheConfig;
if (!uri) {
throw new Error('GitHub Enterprise URI is not configured.');
}
// Prefix with 'copilot-api.'
const url = URI.parse(uri).with({ path: '/mcp/' });
return url.with({ authority: `copilot-api.${url.authority}` });
}
provideMcpServerDefinitions(): McpHttpServerDefinition[] {
const providerId = authProviderId(this.configurationService);
const toolsets = this.toolsets.sort().join(',');
const readonly = this.readonly;
const lockdown = this.lockdown;
const basics = providerId === AuthProviderId.GitHubEnterprise
? { label: 'GitHub Enterprise', uri: this.getGheUri() }
: { label: 'GitHub', uri: URI.parse('https://api.githubcopilot.com/mcp/') };
// Build headers object conditionally
const headers: Record<string, string> = {};
// Build version string with toolsets and flags
let version = toolsets.length ? toolsets : '0';
if (toolsets.length > 0) {
headers['X-MCP-Toolsets'] = toolsets;
}
if (readonly) {
headers['X-MCP-Readonly'] = 'true';
version += '|readonly';
}
if (lockdown) {
headers['X-MCP-Lockdown'] = 'true';
version += '|lockdown';
}
return [
{
...basics,
headers,
version
}
];
}
async resolveMcpServerDefinition(server: McpHttpServerDefinition, token: CancellationToken): Promise<McpHttpServerDefinition> {
const session = await this.authenticationService.getPermissiveGitHubSession({
createIfNone: {
detail: l10n.t('Additional permissions are required to use GitHub MCP Server'),
},
});
if (!session) {
throw new Error('Authentication required');
}
server.headers['Authorization'] = `Bearer ${session.accessToken}`;
return server;
}
}
@@ -0,0 +1,392 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { beforeEach, describe, expect, test } from 'vitest';
import type { AuthenticationGetSessionOptions, AuthenticationSession } from 'vscode';
import { BaseAuthenticationService, IAuthenticationService } from '../../../../platform/authentication/common/authentication';
import { CopilotToken } from '../../../../platform/authentication/common/copilotToken';
import { ICopilotTokenManager } from '../../../../platform/authentication/common/copilotTokenManager';
import { CopilotTokenStore, ICopilotTokenStore } from '../../../../platform/authentication/common/copilotTokenStore';
import { SimulationTestCopilotTokenManager } from '../../../../platform/authentication/test/node/simulationTestCopilotTokenManager';
import { AuthProviderId, ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService';
import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService';
import { ILogService, LogServiceImpl } from '../../../../platform/log/common/logService';
import { TestingServiceCollection } from '../../../../platform/test/node/services';
import { raceTimeout } from '../../../../util/vs/base/common/async';
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
import { Emitter, Event } from '../../../../util/vs/base/common/event';
import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors';
import { GitHubMcpDefinitionProvider } from '../../common/githubMcpDefinitionProvider';
/**
* Test implementation of authentication service that allows setting sessions dynamically
*/
class TestAuthenticationService extends BaseAuthenticationService {
private readonly _onDidChange = new Emitter<void>();
constructor(
@ILogService logService: ILogService,
@ICopilotTokenStore tokenStore: ICopilotTokenStore,
@ICopilotTokenManager tokenManager: ICopilotTokenManager,
@IConfigurationService configurationService: IConfigurationService
) {
super(logService, tokenStore, tokenManager, configurationService);
this._register(this._onDidChange);
}
setPermissiveGitHubSession(session: AuthenticationSession | undefined): void {
this._permissiveGitHubSession = session;
this._onDidAuthenticationChange.fire();
}
getAnyGitHubSession(_options?: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
return Promise.resolve(this._anyGitHubSession);
}
getPermissiveGitHubSession(options?: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
if (options?.createIfNone && !this._permissiveGitHubSession) {
throw new Error('No permissive GitHub session available');
}
return Promise.resolve(this._permissiveGitHubSession);
}
override getAnyAdoSession(_options?: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
return Promise.resolve(undefined);
}
override getAdoAccessTokenBase64(_options?: AuthenticationGetSessionOptions): Promise<string | undefined> {
return Promise.resolve(undefined);
}
override async getCopilotToken(_force?: boolean): Promise<CopilotToken> {
return await super.getCopilotToken(_force);
}
}
describe('GitHubMcpDefinitionProvider', () => {
let configService: InMemoryConfigurationService;
let authService: TestAuthenticationService;
let provider: GitHubMcpDefinitionProvider;
/**
* Helper to create a provider with specific configuration values.
*/
async function createProvider(configOverrides?: {
authProvider?: AuthProviderId;
gheUri?: string;
toolsets?: string[];
readonly?: boolean;
lockdown?: boolean;
hasPermissiveToken?: boolean;
}): Promise<GitHubMcpDefinitionProvider> {
const serviceCollection = new TestingServiceCollection();
configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());
// Set configuration values before creating the provider
if (configOverrides?.authProvider) {
await configService.setConfig(ConfigKey.Shared.AuthProvider, configOverrides.authProvider);
}
if (configOverrides?.gheUri) {
await configService.setNonExtensionConfig('github-enterprise.uri', configOverrides.gheUri);
}
if (configOverrides?.toolsets) {
await configService.setConfig(ConfigKey.GitHubMcpToolsets, configOverrides.toolsets);
}
if (configOverrides?.readonly !== undefined) {
await configService.setConfig(ConfigKey.GitHubMcpReadonly, configOverrides.readonly);
}
if (configOverrides?.lockdown !== undefined) {
await configService.setConfig(ConfigKey.GitHubMcpLockdown, configOverrides.lockdown);
}
serviceCollection.define(IConfigurationService, configService);
serviceCollection.define(ICopilotTokenStore, new SyncDescriptor(CopilotTokenStore));
serviceCollection.define(ICopilotTokenManager, new SyncDescriptor(SimulationTestCopilotTokenManager));
serviceCollection.define(IAuthenticationService, new SyncDescriptor(TestAuthenticationService));
serviceCollection.define(ILogService, new LogServiceImpl([]));
const accessor = serviceCollection.createTestingAccessor();
// Get the auth service and set up permissive token if needed
authService = accessor.get(IAuthenticationService) as TestAuthenticationService;
if (configOverrides?.hasPermissiveToken !== false) {
authService.setPermissiveGitHubSession({ accessToken: 'test-token', id: 'test-id', account: { id: 'test-account', label: 'test' }, scopes: [] });
}
return new GitHubMcpDefinitionProvider(
accessor.get(IConfigurationService),
accessor.get(IAuthenticationService),
accessor.get(ILogService)
);
}
beforeEach(async () => {
provider = await createProvider();
});
describe('provideMcpServerDefinitions', () => {
test('returns GitHub.com configuration by default', () => {
const definitions = provider.provideMcpServerDefinitions();
expect(definitions).toHaveLength(1);
expect(definitions[0].label).toBe('GitHub');
expect(definitions[0].uri.toString()).toBe('https://api.githubcopilot.com/mcp/');
});
test('returns GitHub Enterprise configuration when auth provider is set to GHE', async () => {
const gheUri = 'https://github.enterprise.com';
const gheProvider = await createProvider({
authProvider: AuthProviderId.GitHubEnterprise,
gheUri
});
const definitions = gheProvider.provideMcpServerDefinitions();
expect(definitions).toHaveLength(1);
expect(definitions[0].label).toBe('GitHub Enterprise');
// Should include the copilot-api. prefix
expect(definitions[0].uri.toString()).toBe('https://copilot-api.github.enterprise.com/mcp/');
});
test('includes configured toolsets in headers', async () => {
const toolsets = ['code_search', 'issues', 'pull_requests'];
const providerWithToolsets = await createProvider({ toolsets });
const definitions = providerWithToolsets.provideMcpServerDefinitions();
expect(definitions[0].headers['X-MCP-Toolsets']).toBe('code_search,issues,pull_requests');
});
test('handles empty toolsets configuration', async () => {
const providerWithEmptyToolsets = await createProvider({ toolsets: [] });
const definitions = providerWithEmptyToolsets.provideMcpServerDefinitions();
expect(definitions[0].headers['X-MCP-Toolsets']).toBeUndefined();
});
test('version is the sorted toolset string', async () => {
const toolsets = ['pull_requests', 'code_search', 'issues'];
const providerWithToolsets = await createProvider({ toolsets });
const definitions = providerWithToolsets.provideMcpServerDefinitions();
// Sorted toolsets string
expect(definitions[0].version).toBe('code_search,issues,pull_requests');
});
test('throws when GHE is configured but URI is missing', async () => {
const gheProviderWithoutUri = await createProvider({
authProvider: AuthProviderId.GitHubEnterprise
// Don't set the GHE URI
});
expect(() => gheProviderWithoutUri.provideMcpServerDefinitions()).toThrow('GitHub Enterprise URI is not configured.');
});
test('includes X-MCP-Readonly header when readonly is true', async () => {
const readonlyProvider = await createProvider({ readonly: true });
const definitions = readonlyProvider.provideMcpServerDefinitions();
expect(definitions[0].headers['X-MCP-Readonly']).toBe('true');
});
test('does not include X-MCP-Readonly header when readonly is false', async () => {
const nonReadonlyProvider = await createProvider({ readonly: false });
const definitions = nonReadonlyProvider.provideMcpServerDefinitions();
expect(definitions[0].headers['X-MCP-Readonly']).toBeUndefined();
});
test('includes X-MCP-Lockdown header when lockdown is true', async () => {
const lockdownProvider = await createProvider({ lockdown: true });
const definitions = lockdownProvider.provideMcpServerDefinitions();
expect(definitions[0].headers['X-MCP-Lockdown']).toBe('true');
});
test('does not include X-MCP-Lockdown header when lockdown is false', async () => {
const nonLockdownProvider = await createProvider({ lockdown: false });
const definitions = nonLockdownProvider.provideMcpServerDefinitions();
expect(definitions[0].headers['X-MCP-Lockdown']).toBeUndefined();
});
test('includes both readonly and lockdown headers when both are true', async () => {
const bothProvider = await createProvider({ readonly: true, lockdown: true });
const definitions = bothProvider.provideMcpServerDefinitions();
expect(definitions[0].headers['X-MCP-Readonly']).toBe('true');
expect(definitions[0].headers['X-MCP-Lockdown']).toBe('true');
});
test('version includes readonly flag when readonly is true', async () => {
const readonlyProvider = await createProvider({ readonly: true });
const definitions = readonlyProvider.provideMcpServerDefinitions();
expect(definitions[0].version).toBe('default|readonly');
});
test('version includes lockdown flag when lockdown is true', async () => {
const lockdownProvider = await createProvider({ lockdown: true });
const definitions = lockdownProvider.provideMcpServerDefinitions();
expect(definitions[0].version).toBe('default|lockdown');
});
test('version includes both flags when both readonly and lockdown are true', async () => {
const bothProvider = await createProvider({ readonly: true, lockdown: true });
const definitions = bothProvider.provideMcpServerDefinitions();
expect(definitions[0].version).toBe('default|readonly|lockdown');
});
test('version is just toolsets when readonly and lockdown are false', async () => {
const toolsets = ['issues', 'pull_requests'];
const normalProvider = await createProvider({ toolsets, readonly: false, lockdown: false });
const definitions = normalProvider.provideMcpServerDefinitions();
expect(definitions[0].version).toBe('issues,pull_requests');
});
test('version with empty toolsets and readonly', async () => {
const readonlyEmptyProvider = await createProvider({ toolsets: [], readonly: true });
const definitions = readonlyEmptyProvider.provideMcpServerDefinitions();
expect(definitions[0].version).toBe('0|readonly');
});
});
describe('onDidChangeMcpServerDefinitions', () => {
test('fires when toolsets configuration changes', async () => {
const eventPromise = Event.toPromise(provider.onDidChangeMcpServerDefinitions);
await configService.setConfig(ConfigKey.GitHubMcpToolsets, ['new_toolset']);
await eventPromise;
});
test('fires when auth provider configuration changes', async () => {
const eventPromise = Event.toPromise(provider.onDidChangeMcpServerDefinitions);
await configService.setConfig(ConfigKey.Shared.AuthProvider, AuthProviderId.GitHubEnterprise);
await eventPromise;
});
test('fires when GHE URI configuration changes', async () => {
await configService.setConfig(ConfigKey.Shared.AuthProvider, AuthProviderId.GitHubEnterprise);
await configService.setNonExtensionConfig('github-enterprise.uri', 'https://old.enterprise.com');
const eventPromise = Event.toPromise(provider.onDidChangeMcpServerDefinitions);
await configService.setNonExtensionConfig('github-enterprise.uri', 'https://new.enterprise.com');
await eventPromise;
});
test('does not fire for unrelated configuration changes', async () => {
let eventFired = false;
const handler = () => {
eventFired = true;
};
const disposable = provider.onDidChangeMcpServerDefinitions(handler);
await configService.setNonExtensionConfig('some.unrelated.config', 'value');
await raceTimeout(Promise.resolve(), 50);
expect(eventFired).toBe(false);
disposable.dispose();
});
test('fires when readonly configuration changes', async () => {
const eventPromise = Event.toPromise(provider.onDidChangeMcpServerDefinitions);
await configService.setConfig(ConfigKey.GitHubMcpReadonly, true);
await eventPromise;
});
test('fires when lockdown configuration changes', async () => {
const eventPromise = Event.toPromise(provider.onDidChangeMcpServerDefinitions);
await configService.setConfig(ConfigKey.GitHubMcpLockdown, true);
await eventPromise;
});
});
describe('edge cases', () => {
test('uses default toolsets value when not configured', () => {
const definitions = provider.provideMcpServerDefinitions();
expect(definitions).toHaveLength(1);
expect(definitions[0].headers['X-MCP-Toolsets']).toBe('default');
expect(definitions[0].version).toBe('default');
});
});
describe('resolveMcpServerDefinition', () => {
test('adds authorization header when permissive token is available', async () => {
const definitions = provider.provideMcpServerDefinitions();
const resolved = await provider.resolveMcpServerDefinition(definitions[0], CancellationToken.None);
expect(resolved).toBeDefined();
expect(resolved.headers['Authorization']).toBe('Bearer test-token');
});
test('throws when no permissive token is available and session cannot be created', async () => {
const providerWithoutToken = await createProvider({ hasPermissiveToken: false });
const definitions = providerWithoutToken.provideMcpServerDefinitions();
// Since the mock returns undefined and the implementation uses session!.accessToken,
// this will throw when trying to access accessToken on undefined
await expect(providerWithoutToken.resolveMcpServerDefinition(definitions[0], CancellationToken.None)).rejects.toThrow();
});
});
describe('authentication change events', () => {
test('fires onDidChangeMcpServerDefinitions when token becomes available', async () => {
const providerWithoutToken = await createProvider({ hasPermissiveToken: false });
const eventPromise = Event.toPromise(providerWithoutToken.onDidChangeMcpServerDefinitions);
authService.setPermissiveGitHubSession({ accessToken: 'new-token', id: 'new-id', account: { id: 'new-account', label: 'new' }, scopes: [] });
await eventPromise;
});
test('fires onDidChangeMcpServerDefinitions when token is removed', async () => {
const eventPromise = Event.toPromise(provider.onDidChangeMcpServerDefinitions);
authService.setPermissiveGitHubSession(undefined);
await eventPromise;
});
test('does not fire when token changes but availability remains the same', async () => {
let eventFired = false;
const handler = () => {
eventFired = true;
};
const disposable = provider.onDidChangeMcpServerDefinitions(handler);
// Change the token value but keep it defined
authService.setPermissiveGitHubSession({ accessToken: 'different-token', id: 'different-id', account: { id: 'different-account', label: 'different' }, scopes: [] });
await raceTimeout(Promise.resolve(), 50);
expect(eventFired).toBe(false);
disposable.dispose();
});
});
});
@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { lm } from 'vscode';
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { ILogService } from '../../../platform/log/common/logService';
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { Disposable, IDisposable } from '../../../util/vs/base/common/lifecycle';
import { GitHubMcpDefinitionProvider } from '../common/githubMcpDefinitionProvider';
export class GitHubMcpContrib extends Disposable {
private disposable?: IDisposable;
private definitionProvider?: GitHubMcpDefinitionProvider;
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExperimentationService private readonly experimentationService: IExperimentationService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@ILogService private readonly logService: ILogService
) {
super();
this._registerConfigurationListener();
if (this.enabled) {
void this._registerGitHubMcpDefinitionProvider();
}
}
private _registerConfigurationListener() {
this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ConfigKey.GitHubMcpEnabled.fullyQualifiedId)) {
if (this.enabled) {
void this._registerGitHubMcpDefinitionProvider();
} else {
this.disposable?.dispose();
this.disposable = undefined;
this.definitionProvider = undefined;
}
}
});
}
private async _registerGitHubMcpDefinitionProvider() {
if (!this.definitionProvider) {
// Register the GitHub MCP Definition Provider
this.definitionProvider = new GitHubMcpDefinitionProvider(this.configurationService, this.authenticationService, this.logService);
this.disposable = lm.registerMcpServerDefinitionProvider('github', this.definitionProvider);
}
}
private get enabled(): boolean {
return this.configurationService.getExperimentBasedConfig(ConfigKey.GitHubMcpEnabled, this.experimentationService);
}
}
@@ -7,7 +7,7 @@ import { createServiceIdentifier } from '../../../util/common/services';
import { Emitter, Event } from '../../../util/vs/base/common/event';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { derived } from '../../../util/vs/base/common/observableInternal';
import { AuthPermissionMode, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
import { AuthPermissionMode, AuthProviderId, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
import { ILogService } from '../../log/common/logService';
import { CopilotToken } from './copilotToken';
import { ICopilotTokenManager } from './copilotTokenManager';
@@ -305,3 +305,11 @@ export abstract class BaseAuthenticationService extends Disposable implements IA
this._logService.debug('Finished handling auth change event.');
}
}
export function authProviderId(configurationService: IConfigurationService): AuthProviderId {
return (
configurationService.getConfig(ConfigKey.Shared.AuthProvider) === AuthProviderId.GitHubEnterprise
? AuthProviderId.GitHubEnterprise
: AuthProviderId.GitHub
);
}
@@ -8,10 +8,10 @@ import { TaskSingler } from '../../../util/common/taskSingler';
import { AuthProviderId, IConfigurationService } from '../../configuration/common/configurationService';
import { IDomainService } from '../../endpoint/common/domainService';
import { ILogService } from '../../log/common/logService';
import { BaseAuthenticationService } from '../common/authentication';
import { authProviderId, BaseAuthenticationService } from '../common/authentication';
import { ICopilotTokenManager } from '../common/copilotTokenManager';
import { ICopilotTokenStore } from '../common/copilotTokenStore';
import { authProviderId, getAlignedSession, getAnyAuthSession } from './session';
import { getAlignedSession, getAnyAuthSession } from './session';
export class AuthenticationService extends BaseAuthenticationService {
private _taskSingler = new TaskSingler<AuthenticationSession | undefined>();
@@ -3,11 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AuthenticationGetSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, authentication } from 'vscode';
import { URI } from '../../../util/vs/base/common/uri';
import { AuthPermissionMode, AuthProviderId, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
import { GITHUB_SCOPE_ALIGNED, GITHUB_SCOPE_READ_USER, GITHUB_SCOPE_USER_EMAIL, MinimalModeError } from '../common/authentication';
import { authentication, AuthenticationGetSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vscode';
import { mixin } from '../../../util/vs/base/common/objects';
import { URI } from '../../../util/vs/base/common/uri';
import { AuthPermissionMode, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
import { authProviderId, GITHUB_SCOPE_ALIGNED, GITHUB_SCOPE_READ_USER, GITHUB_SCOPE_USER_EMAIL, MinimalModeError } from '../common/authentication';
export const SESSION_LOGIN_MESSAGE = 'You are not signed in to GitHub. Please sign in to use Copilot.';
// These types are subsets of the "real" types AuthenticationSessionAccountInformation and
@@ -22,14 +22,6 @@ export type CopilotAuthenticationSession = {
account: CopilotAuthenticationSessionAccountInformation;
};
export function authProviderId(configurationService: IConfigurationService): AuthProviderId {
return (
configurationService.getConfig(ConfigKey.Shared.AuthProvider) === AuthProviderId.GitHubEnterprise
? AuthProviderId.GitHubEnterprise
: AuthProviderId.GitHub
);
}
async function getAuthSession(providerId: string, defaultScopes: string[], getSilentSession: () => Promise<AuthenticationSession | undefined>, options: AuthenticationGetSessionOptions = {}) {
const accounts = await authentication.getAccounts(providerId);
if (!accounts.length) {
@@ -822,6 +822,11 @@ export namespace ConfigKey {
export const CompletionsFetcher = defineSetting<FetcherId | undefined>('chat.completionsFetcher', ConfigType.ExperimentBased, undefined);
export const NextEditSuggestionsFetcher = defineSetting<FetcherId | undefined>('chat.nesFetcher', ConfigType.ExperimentBased, undefined);
export const GitHubMcpEnabled = defineSetting<boolean>('chat.githubMcpServer.enabled', ConfigType.ExperimentBased, false);
export const GitHubMcpToolsets = defineSetting<string[]>('chat.githubMcpServer.toolsets', ConfigType.Simple, ['default']);
export const GitHubMcpReadonly = defineSetting<boolean>('chat.githubMcpServer.readonly', ConfigType.Simple, false);
export const GitHubMcpLockdown = defineSetting<boolean>('chat.githubMcpServer.lockdown', ConfigType.Simple, false);
}
export function getAllConfigKeys(): string[] {
@@ -46,11 +46,17 @@ export class InMemoryConfigurationService extends AbstractConfigurationService {
override setConfig<T>(key: BaseConfig<T>, value: T): Promise<void> {
this.overrides.set(key, value);
this._onDidChangeConfiguration.fire({
affectsConfiguration: (section: string) => section === key.fullyQualifiedId || key.fullyQualifiedId.startsWith(section + '.')
});
return Promise.resolve();
}
setNonExtensionConfig<T>(key: string, value: T): Promise<void> {
this.nonExtensionOverrides.set(key, value);
this._onDidChangeConfiguration.fire({
affectsConfiguration: (section: string) => section === key || key.startsWith(section + '.')
});
return Promise.resolve();
}