Skip to content

Commit 6cea1c0

Browse files
Support GitHub MCP in-box (#2051)
* Support GitHub MCP inbox Fixes microsoft/vscode#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
1 parent a1c9fcc commit 6cea1c0

File tree

11 files changed

+667
-17
lines changed

11 files changed

+667
-17
lines changed

package.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,7 @@
10611061
"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.",
10621062
"userDescription": "%github.copilot.tools.githubRepo.userDescription%",
10631063
"icon": "$(repo)",
1064+
"when": "!config.github.copilot.chat.githubMcpServer.enabled",
10641065
"inputSchema": {
10651066
"type": "object",
10661067
"properties": {
@@ -1841,6 +1842,12 @@
18411842
"when": "!github.copilot.interactiveSession.disabled"
18421843
}
18431844
],
1845+
"mcpServerDefinitionProviders": [
1846+
{
1847+
"id": "github",
1848+
"label": "GitHub"
1849+
}
1850+
],
18441851
"viewsWelcome": [
18451852
{
18461853
"view": "debug",
@@ -2745,6 +2752,41 @@
27452752
{
27462753
"id": "experimental",
27472754
"properties": {
2755+
"github.copilot.chat.githubMcpServer.enabled": {
2756+
"type": "boolean",
2757+
"default": false,
2758+
"markdownDescription": "%github.copilot.config.githubMcpServer.enabled%",
2759+
"tags": [
2760+
"experimental"
2761+
]
2762+
},
2763+
"github.copilot.chat.githubMcpServer.toolsets": {
2764+
"type": "array",
2765+
"default": ["default"],
2766+
"markdownDescription": "%github.copilot.config.githubMcpServer.toolsets%",
2767+
"items": {
2768+
"type": "string"
2769+
},
2770+
"tags": [
2771+
"experimental"
2772+
]
2773+
},
2774+
"github.copilot.chat.githubMcpServer.readonly": {
2775+
"type": "boolean",
2776+
"default": false,
2777+
"markdownDescription": "%github.copilot.config.githubMcpServer.readonly%",
2778+
"tags": [
2779+
"experimental"
2780+
]
2781+
},
2782+
"github.copilot.chat.githubMcpServer.lockdown": {
2783+
"type": "boolean",
2784+
"default": false,
2785+
"markdownDescription": "%github.copilot.config.githubMcpServer.lockdown%",
2786+
"tags": [
2787+
"experimental"
2788+
]
2789+
},
27482790
"github.copilot.chat.imageUpload.enabled": {
27492791
"type": "boolean",
27502792
"default": true,

package.nls.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,5 +416,9 @@
416416
"github.copilot.cli.sessions.newTerminalSession": "New Agent Session in Terminal",
417417
"github.copilot.command.openCopilotAgentSessionsInBrowser": "Open in Browser",
418418
"github.copilot.command.closeChatSessionPullRequest.title": "Close Pull Request",
419-
"github.copilot.command.applyCopilotCLIAgentSessionChanges": "Apply Changes"
419+
"github.copilot.command.applyCopilotCLIAgentSessionChanges": "Apply Changes",
420+
"github.copilot.config.githubMcpServer.enabled": "Enable built-in support for the GitHub MCP Server.",
421+
"github.copilot.config.githubMcpServer.toolsets": "Specify toolsets to use from the GitHub MCP Server.",
422+
"github.copilot.config.githubMcpServer.readonly": "Enable read-only mode for the GitHub MCP Server. When enabled, only read tools are available.",
423+
"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."
420424
}

src/extension/extension/vscode-node/contributions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { DiagnosticsContextContribution } from '../../diagnosticsContext/vscode/
2424
import { LanguageModelProxyContrib } from '../../externalAgents/vscode-node/lmProxyContrib';
2525
import { WalkthroughCommandContribution } from '../../getting-started/vscode-node/commands';
2626
import * as newWorkspaceContribution from '../../getting-started/vscode-node/newWorkspace.contribution';
27+
import { GitHubMcpContrib } from '../../githubMcp/vscode-node/githubMcp.contribution';
2728
import { IgnoredFileProviderContribution } from '../../ignore/vscode-node/ignoreProvider';
2829
import { InlineEditProviderFeature } from '../../inlineEdits/vscode-node/inlineEditProviderFeature';
2930
import { FixTestFailureContribution } from '../../intents/vscode-node/fixTestFailureContributions';
@@ -86,7 +87,8 @@ export const vscodeNodeContributions: IExtensionContributionFactory[] = [
8687
asContributionFactory(CompletionsCoreContribution),
8788
asContributionFactory(CompletionsUnificationContribution),
8889
workspaceIndexingContribution,
89-
asContributionFactory(ChatSessionsContrib)
90+
asContributionFactory(ChatSessionsContrib),
91+
asContributionFactory(GitHubMcpContrib)
9092
];
9193

9294
/**
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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

Comments
 (0)