From 808c88928e9e34ffba101e6867fde06eff56df2a Mon Sep 17 00:00:00 2001 From: Paul Wang <8560030+pwang347@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:23:25 -0700 Subject: [PATCH] agentHost: preserve static MCP OAuth client IDs --- .../node/copilot/copilotAgentSession.ts | 8 +++--- .../node/copilot/copilotPluginConverters.ts | 1 + .../test/node/copilotAgentSession.test.ts | 5 ++-- .../test/node/copilotPluginConverters.test.ts | 22 +++++++++++++++ .../agentPlugins/common/pluginParsers.ts | 6 +++- .../test/common/pluginParsers.test.ts | 28 +++++++++++++++++++ 6 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index 41c954ea0716d6..51c87a92bf5e07 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -2262,10 +2262,10 @@ export class CopilotAgentSession extends Disposable { } const resource = this._protectedResourceFromMcpAuthRequest(request); const requiredScopes = this._scopesFromChallenge(request.wwwAuthenticateParams?.scope); - const oauthClient: McpAuthRequirement['oauthClient'] = request.staticClientConfig?.publicClient - ? { clientId: request.staticClientConfig.clientId } - : request.staticClientConfig?.clientSecret - ? { clientId: request.staticClientConfig.clientId, clientSecret: request.staticClientConfig.clientSecret } + const oauthClient: McpAuthRequirement['oauthClient'] = request.staticClientConfig?.clientSecret + ? { clientId: request.staticClientConfig.clientId, clientSecret: request.staticClientConfig.clientSecret } + : request.staticClientConfig && request.staticClientConfig.publicClient !== false + ? { clientId: request.staticClientConfig.clientId } : undefined; const auth: McpAuthRequirement = { reason: this._mcpAuthRequiredReason(request.reason), diff --git a/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts b/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts index 3722986e3a1170..fb4d176e8ccf25 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotPluginConverters.ts @@ -95,6 +95,7 @@ function toSdkMcpServer(_name: string, config: IMcpServerConfiguration, defaultC url: config.url, tools: ['*'], ...(config.headers && { headers: { ...config.headers } }), + ...(config.oauth?.clientId && { oauthClientId: config.oauth.clientId }), }; } diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index a73d72095decda..2ce6b7ca9dfa16 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -11971,7 +11971,7 @@ Use the attached image as context. }); }); - test('MCP auth request publishes authRequired state and resolves with authenticate token', async () => { + test('MCP auth request prioritizes a static client secret when publishing authRequired state', async () => { const { session, runtime, waitForSignal } = await createAgentSession(disposables, { githubToken: 'existing-token' }); const authPromise = runtime.handleMcpAuthRequest({ @@ -11982,7 +11982,7 @@ Use the attached image as context. staticClientConfig: { clientId: 'configured-client-id', clientSecret: 'configured-client-secret', - publicClient: false, + publicClient: true, }, resourceMetadata: JSON.stringify({ resource: 'https://api.githubcopilot.com/mcp', @@ -12035,7 +12035,6 @@ Use the attached image as context. reason: 'initial', staticClientConfig: { clientId: 'public-client-id', - publicClient: true, }, resourceMetadata: JSON.stringify({ resource: 'https://mcp.example.com', diff --git a/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts b/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts index 12e0a3a1898733..a358f96d76ed78 100644 --- a/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotPluginConverters.test.ts @@ -118,6 +118,28 @@ suite('copilotPluginConverters', () => { }); }); + test('converts remote OAuth client configuration', () => { + const defs: IMcpServerDefinition[] = [{ + name: 'slack', + uri: URI.file('/plugin'), + configuration: { + type: McpServerType.REMOTE, + url: 'https://mcp.slack.com/mcp', + oauth: { clientId: 'public-client-id' }, + }, + customization: stubMcpCustomization('slack'), + }]; + + assert.deepStrictEqual(toSdkMcpServers(defs), { + slack: { + type: 'http', + url: 'https://mcp.slack.com/mcp', + tools: ['*'], + oauthClientId: 'public-client-id', + }, + }); + }); + test('handles empty definitions', () => { const result = toSdkMcpServers([]); assert.deepStrictEqual(result, {}); diff --git a/src/vs/platform/agentPlugins/common/pluginParsers.ts b/src/vs/platform/agentPlugins/common/pluginParsers.ts index 0e88228ea2eb24..759cec5398ff65 100644 --- a/src/vs/platform/agentPlugins/common/pluginParsers.ts +++ b/src/vs/platform/agentPlugins/common/pluginParsers.ts @@ -500,6 +500,10 @@ export function normalizeMcpServerConfiguration(rawConfig: unknown): IMcpServerC .filter(([, value]) => typeof value === 'string') .map(([key, value]) => [key, value as string])) : undefined; + const rawOAuth = candidate['oauth'] && typeof candidate['oauth'] === 'object' ? candidate['oauth'] as Record : undefined; + const oauthClientId = (typeof rawOAuth?.['clientId'] === 'string' ? rawOAuth['clientId'] : undefined) + ?? (typeof candidate['oauthClientId'] === 'string' ? candidate['oauthClientId'] : undefined); + const oauth = oauthClientId ? { clientId: oauthClientId } : undefined; const dev = candidate['dev'] && typeof candidate['dev'] === 'object' ? candidate['dev'] as IMcpStdioServerConfiguration['dev'] : undefined; if (type === 'ws') { @@ -517,7 +521,7 @@ export function normalizeMcpServerConfiguration(rawConfig: unknown): IMcpServerC if (!url) { return undefined; } - return { type: McpServerType.REMOTE, ...(type === 'sse' || transport === 'sse' ? { transport: 'sse' as const } : {}), url, headers, dev }; + return { type: McpServerType.REMOTE, ...(type === 'sse' || transport === 'sse' ? { transport: 'sse' as const } : {}), url, headers, ...(oauth ? { oauth } : {}), dev }; } return undefined; diff --git a/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts b/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts index 111864a3a5f129..0145a68e012275 100644 --- a/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts +++ b/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts @@ -206,6 +206,34 @@ suite('pluginParsers', () => { }); }); + test('preserves VS Code OAuth client configuration', () => { + assert.deepStrictEqual(normalizeMcpServerConfiguration({ + type: 'http', + url: 'https://mcp.slack.com/mcp', + oauth: { clientId: 'vscode-client-id' }, + }), { + type: McpServerType.REMOTE, + url: 'https://mcp.slack.com/mcp', + headers: undefined, + oauth: { clientId: 'vscode-client-id' }, + dev: undefined, + }); + }); + + test('normalizes Copilot SDK OAuth client configuration', () => { + assert.deepStrictEqual(normalizeMcpServerConfiguration({ + type: 'http', + url: 'https://mcp.slack.com/mcp', + oauthClientId: 'sdk-client-id', + }), { + type: McpServerType.REMOTE, + url: 'https://mcp.slack.com/mcp', + headers: undefined, + oauth: { clientId: 'sdk-client-id' }, + dev: undefined, + }); + }); + test('infers remote type from url without explicit type', () => { const result = normalizeMcpServerConfiguration({ url: 'https://example.com' }); assert.ok(result);