src/vs/workbench/api/common/extHostMcp.ts

1202 LOC · 480 covered · 722 uncovered · 78 ranges · 30 concepts · 16 introducers · 26 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/workbench/api/common/extHostDocumentData.ts · 312 LOCcommon/extHostDocumentDa…src/vs/workbench/api/common/extHostDocumentsAndEditors.ts · 197 LOCcommon/extHostDocumentsA…src/vs/workbench/api/common/extHostEditorTabs.ts · 444 LOCcommon/extHostEditorTabs…src/vs/workbench/api/common/extHostTextEditor.ts · 690 LOCcommon/extHostTextEditor…src/vs/workbench/api/common/extHostVariableResolverService.ts · 176 LOCcommon/extHostVariableRe…src/vs/workbench/services/configurationResolver/common/variableResolver.ts · 345 LOCcommon/variableResolver.…extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1, extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 3 introduced LOCextHostMcp.ts ×1extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 1 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 6 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 4 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×2 · 4 introduced LOCextHostMcp.ts ×2extHostMcp.ts ×1 · 1 introduced LOCextHostMcp.ts ×1extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×4 · 6 introduced LOCextHostMcp.ts ×4extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1, extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 6 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×4 · 13 introduced LOCextHostMcp.ts ×4extHostMcpNode.ts ×8 · 45 introduced LOCextHostMcpNode.ts ×8extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×5 · 25 introduced LOCextHostMcp.ts ×5extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×3 · 23 introduced LOCextHostMcp.ts ×3extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×8 · 60 introduced LOCextHostMcp.ts ×8extHostMcp.ts ×43 · 874 introduced LOCextHostMcp.ts ×43extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should allow undefined scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should allow undefined scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when no WWW-Authenticate header and scopes are already undefined|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when no WWW-Authenticate header and scopes are already undefined|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle empty scope string in WWW-Authenticate header|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle empty scope string in WWW-Authenticate header|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle malformed WWW-Authenticate header gracefully|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle malformed WWW-Authenticate header gracefully|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use resource_metadata challenge URL from WWW-Authenticate header|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use resource_metadata challenge URL from WWW-Authenticate header|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1extHostMcp.test|title=Ex…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg does not add stray ^ to argument values|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg does not add stray ^ to argument values|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg doubles embedded double quotes (cmd.exe convention)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg doubles embedded double quotes (cmd.exe convention)|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg neutralizes cmd.exe metacharacters inside quotes (CVE-2024-27980)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg neutralizes cmd.exe metacharacters inside quotes (CVE-2024-27980)|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with parentheses without injecting ^|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with parentheses without injecting ^|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with spaces|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with spaces|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg wraps simple values in double quotes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg wraps simple values in double quotes|occurrence=1extHostMcpNode.test|titl…Focused file · src/vs/workbench/api/common/extHostMcp.ts · 1202 LOCcommon/extHostMcp.ts

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- extHostMcp.ts ×43
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 vscode from 'vscode';
7 > import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../../../base/common/async.js';
8 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
9 > import { CancellationError } from '../../../base/common/errors.js';
10 > import { Emitter, Event } from '../../../base/common/event.js';
11 > import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
12 > import { AUTH_SCOPE_SEPARATOR, fetchAuthorizationServerMetadata, fetchResourceMetadata, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, parseWWWAuthenticateHeader, scopesMatch } from '../../../base/common/oauth.js';
13 > import { SSEParser } from '../../../base/common/sseParser.js';
14 > import { URI, UriComponents } from '../../../base/common/uri.js';
15 > import { vArray, vNumber, vObj, vObjAny, vOptionalProp, vString } from '../../../base/common/validation.js';
16 > import { ConfigurationTarget } from '../../../platform/configuration/common/configuration.js';
17 > import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
18 > import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
19 > import { canLog, ILogService, LogLevel } from '../../../platform/log/common/log.js';
20 > import product from '../../../platform/product/common/product.js';
21 > import { StorageScope } from '../../../platform/storage/common/storage.js';
22 > import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerStaticMetadata, McpServerStaticToolAvailability, McpServerTransportHTTP, McpServerTransportType, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js';
23 > import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js';
24 > import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
25 > import { ExtHostMcpShape, IMcpAuthenticationDetails, IAuthMetadataSource, IStartMcpOptions, MainContext, MainThreadMcpShape, IAuthResourceMetadataSource, IAuthServerMetadataSource } from './extHost.protocol.js';
26 > import { IExtHostInitDataService } from './extHostInitDataService.js';
27 > import { IExtHostRpcService } from './extHostRpcService.js';
28 > import * as Convert from './extHostTypeConverters.js';
29 > import { McpHttpServerDefinition, McpStdioServerDefinition, McpToolAvailability } from './extHostTypes.js';
30 > import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js';
31 > import { IExtHostWorkspace } from './extHostWorkspace.js';
32 >
33 > export const IExtHostMpcService = createDecorator<IExtHostMpcService>('IExtHostMpcService');
34 >
35 > export interface IExtHostMpcService extends ExtHostMcpShape {
36 > registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable;
37 >
38 > /** Event that fires when the set of MCP server definitions changes. */
39 > readonly onDidChangeMcpServerDefinitions: Event<void>;
40 >
41 > /** Returns all MCP server definitions known to the editor. */
42 > readonly mcpServerDefinitions: readonly vscode.McpServerDefinition[];
43 >
44 > /** Starts an MCP gateway that exposes MCP servers via HTTP endpoints. */
45 > startMcpGateway(chatSessionResource?: URI): Promise<vscode.McpGateway | undefined>;
46 > }
47 >
48 > const serverDataValidation = vObj({
49 > label: vString(),
50 > version: vOptionalProp(vString()),
51 > metadata: vOptionalProp(vObj({
52 > capabilities: vOptionalProp(vObjAny()),
53 > serverInfo: vOptionalProp(vObjAny()),
54 > tools: vOptionalProp(vArray(vObj({
55 > availability: vNumber(),
56 > definition: vObjAny(),
57 > }))),
58 > })),
59 > authentication: vOptionalProp(vObj({
60 > providerId: vString(),
61 > scopes: vArray(vString()),
62 > }))
63 > });
64 >
65 > // Can be validated with:
66 > // declare const _serverDataValidationTest: vscode.McpStdioServerDefinition | vscode.McpHttpServerDefinition;
67 > // const _serverDataValidationProd: ValidatorType<typeof serverDataValidation> = _serverDataValidationTest;
68 >
69 > export class ExtHostMcpService extends Disposable implements IExtHostMpcService {
70 > protected _proxy: MainThreadMcpShape;
71 > private readonly _initialProviderPromises = new Set<Promise<void>>();
72 > protected readonly _sseEventSources = this._register(new DisposableMap<number, McpHTTPHandle>());
73 > private readonly _unresolvedMcpServers = new Map</* collectionId */ string, {
74 > provider: vscode.McpServerDefinitionProvider;
75 > servers: vscode.McpServerDefinition[];
76 > }>();
77 >
78 > // MCP server definitions synced from main thread
79 > private readonly _onDidChangeMcpServerDefinitions = this._register(new Emitter<void>());
80 > readonly onDidChangeMcpServerDefinitions: Event<void> = this._onDidChangeMcpServerDefinitions.event;
81 > private _mcpServerDefinitions: readonly vscode.McpServerDefinition[] = [];
82 >
83 > // Active gateways with their server emitters for dynamic updates
84 > private readonly _activeGateways = new Map<string, {
85 > servers: vscode.McpGatewayServer[];
86 > onDidChangeServers: Emitter<readonly vscode.McpGatewayServer[]>;
87 > }>();
88 >
89 > constructor(
90 @IExtHostRpcService extHostRpc: IExtHostRpcService,
91 @ILogService protected readonly _logService: ILogService,
92 @IExtHostInitDataService private readonly _extHostInitData: IExtHostInitDataService,
93 @IExtHostWorkspace protected readonly _workspaceService: IExtHostWorkspace,
94 @IExtHostVariableResolverProvider private readonly _variableResolver: IExtHostVariableResolverProvider,
95 ) {
96 super();
97 this._proxy = extHostRpc.getProxy(MainContext.MainThreadMcp);
98 }
100 > /** Returns all MCP server definitions known to the editor. */
101 > get mcpServerDefinitions(): readonly vscode.McpServerDefinition[] {
102 return this._mcpServerDefinitions;
103 }
105 > /** Called by main thread to notify that MCP server definitions have changed. */
106 > $onDidChangeMcpServerDefinitions(servers: McpServerDefinition.Serialized[]): void {
107 this._mcpServerDefinitions = servers.map(dto => Convert.McpServerDefinition.to(dto));
108 this._onDidChangeMcpServerDefinitions.fire();
109 }
111 > $startMcp(id: number, opts: IStartMcpOptions): void {
112 this._startMcp(id, McpServerLaunch.fromSerialized(opts.launch), opts.defaultCwd && URI.revive(opts.defaultCwd), opts.errorOnUserInteraction);
113 }
115 > protected _startMcp(id: number, launch: McpServerLaunch, _defaultCwd?: URI, errorOnUserInteraction?: boolean): void {
116 if (launch.type === McpServerTransportType.HTTP) {
117 this._sseEventSources.set(id, new McpHTTPHandle(id, launch, this._proxy, this._logService, errorOnUserInteraction));
118 return;
119 }
120
121 throw new Error('not implemented');
122 }
124 > async $substituteVariables<T>(_workspaceFolder: UriComponents | undefined, value: T): Promise<T> {
125 const folderURI = URI.revive(_workspaceFolder);
126 const folder = folderURI && await this._workspaceService.resolveWorkspaceFolder(folderURI);
127 const variableResolver = await this._variableResolver.getResolver();
128 return variableResolver.resolveAsync(folder && {
129 uri: folder.uri,
130 name: folder.name,
131 index: folder.index,
132 }, value) as T;
133 }
135 > $stopMcp(id: number): void {
136 this._sseEventSources.get(id)
137 ?.close()
138 .then(() => this._didClose(id));
139 }
141 > private _didClose(id: number) {
142 this._sseEventSources.deleteAndDispose(id);
143 }
145 > $sendMessage(id: number, message: string): void {
146 this._sseEventSources.get(id)?.send(message);
147 }
149 > async $waitForInitialCollectionProviders(): Promise<void> {
150 await Promise.all(this._initialProviderPromises);
151 }
153 > async $resolveMcpLaunch(collectionId: string, label: string): Promise<McpServerLaunch.Serialized | undefined> {
154 const rec = this._unresolvedMcpServers.get(collectionId);
155 if (!rec) {
156 return;
157 }
158
159 const server = rec.servers.find(s => s.label === label);
160 if (!server) {
161 return;
162 }
163 if (!rec.provider.resolveMcpServerDefinition) {
164 return Convert.McpServerDefinition.from(server);
165 }
166
167 const resolved = await rec.provider.resolveMcpServerDefinition(server, CancellationToken.None);
168 return resolved ? Convert.McpServerDefinition.from(resolved) : undefined;
169 }
171 > /** {@link vscode.lm.registerMcpServerDefinitionProvider} */
172 > public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable {
173 const store = new DisposableStore();
174
175 const metadata = extension.contributes?.mcpServerDefinitionProviders?.find(m => m.id === id);
176 if (!metadata) {
177 throw new Error(`MCP configuration providers must be registered in the contributes.mcpServerDefinitionProviders array within your package.json, but "${id}" was not`);
178 }
179
180 const mcp: McpCollectionDefinition.FromExtHost = {
181 id: extensionPrefixedIdentifier(extension.identifier, id),
182 isTrustedByDefault: true,
183 label: metadata?.label ?? extension.displayName ?? extension.name,
184 scope: StorageScope.WORKSPACE,
185 canResolveLaunch: typeof provider.resolveMcpServerDefinition === 'function',
186 extensionId: extension.identifier.value,
187 configTarget: this._extHostInitData.remote.isRemote ? ConfigurationTarget.USER_REMOTE : ConfigurationTarget.USER,
188 };
189
190 const update = async () => {
191 const list = await provider.provideMcpServerDefinitions(CancellationToken.None);
192 this._unresolvedMcpServers.set(mcp.id, { servers: list ?? [], provider });
193
194 const servers: McpServerDefinition.Serialized[] = [];
195 for (const item of list ?? []) {
196 let id = ExtensionIdentifier.toKey(extension.identifier) + '/' + item.label;
197 if (servers.some(s => s.id === id)) {
198 let i = 2;
199 while (servers.some(s => s.id === id + i)) { i++; }
200 id = id + i;
201 }
202
203 serverDataValidation.validateOrThrow(item);
204 if ((item as vscode.McpHttpServerDefinition2).authentication) {
205 checkProposedApiEnabled(extension, 'mcpToolDefinitions');
206 }
207
208 let staticMetadata: McpServerStaticMetadata | undefined;
209 const castAs2 = item as McpStdioServerDefinition | McpHttpServerDefinition;
210 if (isProposedApiEnabled(extension, 'mcpToolDefinitions') && castAs2.metadata) {
211 staticMetadata = {
212 capabilities: castAs2.metadata.capabilities as MCP.ServerCapabilities,
213 instructions: castAs2.metadata.instructions,
214 serverInfo: castAs2.metadata.serverInfo as MCP.Implementation,
215 tools: castAs2.metadata.tools?.map(t => ({
216 availability: t.availability === McpToolAvailability.Dynamic ? McpServerStaticToolAvailability.Dynamic : McpServerStaticToolAvailability.Initial,
217 definition: t.definition as MCP.Tool,
218 })),
219 };
220 }
221
222 servers.push({
223 id,
224 label: item.label,
225 cacheNonce: item.version || '$$NONE',
226 staticMetadata,
227 launch: Convert.McpServerDefinition.from(item),
228 });
229 }
230
231 this._proxy.$upsertMcpCollection(mcp, servers);
232 };
233
234 store.add(toDisposable(() => {
235 this._unresolvedMcpServers.delete(mcp.id);
236 this._proxy.$deleteMcpCollection(mcp.id);
237 }));
238
239 if (provider.onDidChangeMcpServerDefinitions) {
240 store.add(provider.onDidChangeMcpServerDefinitions(update));
241 }
242 // todo@connor4312: proposed API back-compat
243 // eslint-disable-next-line local/code-no-any-casts
244 if ((provider as any).onDidChangeServerDefinitions) {
245 // eslint-disable-next-line local/code-no-any-casts
246 store.add((provider as any).onDidChangeServerDefinitions(update));
247 }
248 // eslint-disable-next-line local/code-no-any-casts
249 if ((provider as any).onDidChange) {
250 // eslint-disable-next-line local/code-no-any-casts
251 store.add((provider as any).onDidChange(update));
252 }
253
254 const promise = new Promise<void>(resolve => {
255 setTimeout(() => update().finally(() => {
256 this._initialProviderPromises.delete(promise);
257 resolve();
258 }), 0);
259 });
260
261 this._initialProviderPromises.add(promise);
262
263 return store;
264 }
266 > /** {@link vscode.lm.startMcpGateway} */
267 > public async startMcpGateway(chatSessionResource?: URI): Promise<vscode.McpGateway | undefined> {
268 const result = await this._proxy.$startMcpGateway(chatSessionResource?.toJSON());
269 if (!result) {
270 return undefined;
271 }
272
273 const gatewayId = result.gatewayId;
274 const servers: vscode.McpGatewayServer[] = result.servers.map(s => ({
275 label: s.label,
276 address: URI.revive(s.address),
277 }));
278 const onDidChangeServers = new Emitter<readonly vscode.McpGatewayServer[]>();
279
280 this._activeGateways.set(gatewayId, { servers, onDidChangeServers });
281
282 return {
283 get servers() { return servers; },
284 onDidChangeServers: onDidChangeServers.event,
285 dispose: () => {
286 this._activeGateways.delete(gatewayId);
287 onDidChangeServers.dispose();
288 this._proxy.$disposeMcpGateway(gatewayId);
289 }
290 };
291 }
293 > /** Called by main thread to notify that a gateway's server set has changed. */
294 > $onDidChangeGatewayServers(gatewayId: string, newServers: { label: string; address: UriComponents }[]): void {
295 const gateway = this._activeGateways.get(gatewayId);
296 if (!gateway) {
297 return;
298 }
299
300 const servers: vscode.McpGatewayServer[] = newServers.map(s => ({
301 label: s.label,
302 address: URI.revive(s.address),
303 }));
304 gateway.servers.length = 0;
305 gateway.servers.push(...servers);
306 gateway.onDidChangeServers.fire(servers);
307 }
309 >
310 function stringifyError(err: unknown): string {
311 if (!(err instanceof Error)) {
312 return String(err);
313 }
314 let msg = String(err);
315 let cause: unknown = err.cause;
316 for (let depth = 0; cause !== undefined && depth < 5; depth++) {
317 msg += `: ${cause instanceof Error ? (cause.message || String(cause)) : String(cause)}`;
318 cause = cause instanceof Error ? cause.cause : undefined;
319 }
320 return msg;
321 }
323 > const enum HttpMode {
324 > Unknown,
325 > Http,
326 > SSE,
327 > }
328 >
329 > type HttpModeT =
330 > | { value: HttpMode.Unknown }
331 > | { value: HttpMode.Http; sessionId: string | undefined }
332 > | { value: HttpMode.SSE; endpoint: string };
333 >
334 > const MAX_FOLLOW_REDIRECTS = 5;
335 > const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
336 > // MCP server URLs are restricted to http(s) at configuration time; the redirect
337 > // path must enforce the same so a Location header cannot reach unix://, pipe://,
338 > // file://, etc.
339 > const ALLOWED_REDIRECT_PROTOCOLS = new Set(['http:', 'https:']);
340 > // Credential-bearing headers that must not be replayed to a different origin
341 > // after a redirect (matches browser fetch / curl behavior). Compared case-insensitively.
342 > const CROSS_ORIGIN_STRIPPED_HEADERS = new Set(['authorization', 'cookie', 'proxy-authorization', 'mcp-session-id']);
343 >
344 function setHostHeader(headers: Record<string, string>, name: string, value: string): void {
345 for (const configuredName of Object.keys(headers)) {
346 if (configuredName.toLowerCase() === name.toLowerCase()) {
347 delete headers[configuredName];
348 }
349 }
350 headers[name] = value;
351 }
353 > /**
354 > * Implementation of both MCP HTTP Streaming as well as legacy SSE.
355 > *
356 > * The first request will POST to the endpoint, assuming HTTP streaming. If the
357 > * server is legacy SSE, it should return some 4xx status in that case,
358 > * and we'll automatically fall back to SSE and res
359 > */
360 > export class McpHTTPHandle extends Disposable {
361 > private readonly _requestSequencer = new Sequencer();
362 > private readonly _postEndpoint = new DeferredPromise<{ url: string; transport: McpServerTransportHTTP }>();
363 > private _mode: HttpModeT = { value: HttpMode.Unknown };
364 > private readonly _cts = new CancellationTokenSource();
365 > private readonly _abortCtrl = new AbortController();
366 > private _authMetadata?: AuthMetadata;
367 > private _didSendClose = false;
368 >
369 > constructor(
370 private readonly _id: number,
371 private readonly _launch: McpServerTransportHTTP,
372 private readonly _proxy: MainThreadMcpShape,
373 private readonly _logService: ILogService,
374 private readonly _errorOnUserInteraction?: boolean,
375 ) {
376 super();
377
378 this._register(toDisposable(() => {
379 this._abortCtrl.abort();
380 this._cts.dispose(true);
381 }));
382 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Running });
383 }
385 > async send(message: string) {
386 try {
387 if (this._mode.value === HttpMode.Unknown) {
388 await this._requestSequencer.queue(() => this._send(message));
389 } else {
390 await this._send(message);
391 }
392 } catch (err) {
393 const msg = `Error sending message to ${this._launch.uri}: ${stringifyError(err)}`;
394 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: msg });
395 }
396 }
398 > async close() {
399 if (this._mode.value === HttpMode.Http && this._mode.sessionId && !this._didSendClose) {
400 this._didSendClose = true;
401 try {
402 await this._closeSession(this._mode.sessionId);
403 } catch {
404 // ignored -- already logged
405 }
406 }
407
408 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Stopped });
409 }
411 > private async _closeSession(sessionId: string) {
412 const headers: Record<string, string> = {
413 ...Object.fromEntries(this._launch.headers),
414 'Mcp-Session-Id': sessionId,
415 };
416
417 try {
418 await this._addAuthHeader(headers, { errorOnUserInteraction: true });
419 } catch (e) {
420 // If auth is no longer available (e.g. user signed out), skip the close request
421 this._log(LogLevel.Debug, `Skipping session close: authentication no longer available`);
422 return;
423 }
424
425 // no fetch with retry here -- don't try to auth if we get an auth failure
426 await this._fetch(
427 this._launch.uri.toString(true),
428 {
429 method: 'DELETE',
430 headers,
431 },
432 );
433 }
435 > private _send(message: string) {
436 if (this._mode.value === HttpMode.SSE) {
437 return this._sendLegacySSE(this._mode.endpoint, message);
438 } else {
439 return this._sendStreamableHttp(message, this._mode.value === HttpMode.Http ? this._mode.sessionId : undefined);
440 }
441 }
443 > /**
444 > * Sends a streamable-HTTP request.
445 > * 1. Posts to the endpoint
446 > * 2. Updates internal state as needed. Falls back to SSE if appropriate.
447 > * 3. If the response body is empty, JSON, or a JSON stream, handle it appropriately.
448 > */
449 > private async _sendStreamableHttp(message: string, sessionId: string | undefined) {
450 const asBytes = new TextEncoder().encode(message) as Uint8Array<ArrayBuffer>;
451 const headers: Record<string, string> = {
452 ...Object.fromEntries(this._launch.headers),
453 'Content-Type': 'application/json',
454 Accept: 'text/event-stream, application/json',
455 };
456 if (sessionId) {
457 headers['Mcp-Session-Id'] = sessionId;
458 }
459 await this._addAuthHeader(headers);
460
461 const res = await this._fetchWithAuthRetry(
462 this._launch.uri.toString(true),
463 {
464 method: 'POST',
465 headers,
466 body: asBytes,
467 },
468 headers
469 );
470
471 const wasUnknown = this._mode.value === HttpMode.Unknown;
472
473 // Mcp-Session-Id is the strongest signal that we're in streamable HTTP mode
474 const nextSessionId = res.headers.get('Mcp-Session-Id');
475 if (nextSessionId) {
476 this._mode = { value: HttpMode.Http, sessionId: nextSessionId };
477 }
478
479 if (this._mode.value === HttpMode.Unknown &&
480 // We care about 4xx errors...
481 res.status >= 400 && res.status < 500
482 // ...except for auth errors
483 && !isAuthStatusCode(res.status)
484 ) {
485 this._log(LogLevel.Info, `${res.status} status sending message to ${this._launch.uri}, will attempt to fall back to legacy SSE`);
486 this._sseFallbackWithMessage(message);
487 return;
488 }
489
490 if (res.status >= 300) {
491 // "When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached"
492 // Though this says only 404, some servers send 400s as well, including their example
493 // https://github.com/modelcontextprotocol/typescript-sdk/issues/389
494 const retryWithSessionId = this._mode.value === HttpMode.Http && !!this._mode.sessionId && (res.status === 400 || res.status === 404);
495
496 this._proxy.$onDidChangeState(this._id, {
497 state: McpConnectionState.Kind.Error,
498 message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + (retryWithSessionId ? `; will retry with new session ID` : ''),
499 shouldRetry: retryWithSessionId,
500 });
501 return;
502 }
503
504 if (this._mode.value === HttpMode.Unknown) {
505 this._mode = { value: HttpMode.Http, sessionId: undefined };
506 }
507 if (wasUnknown) {
508 this._attachStreamableBackchannel();
509 }
510
511 await this._handleSuccessfulStreamableHttp(res, message);
512 }
514 > private async _sseFallbackWithMessage(message: string) {
515 const endpoint = await this._attachSSE();
516 if (endpoint) {
517 this._mode = { value: HttpMode.SSE, endpoint };
518 await this._sendLegacySSE(endpoint, message);
519 }
520 }
522 > private async _handleSuccessfulStreamableHttp(res: CommonResponse, message: string) {
523 if (res.status === 202) {
524 return; // no body
525 }
526
527 const contentType = res.headers.get('Content-Type')?.toLowerCase() || '';
528 if (contentType.startsWith('text/event-stream')) {
529 const parser = new SSEParser(event => {
530 if (event.type === 'message') {
531 this._proxy.$onDidReceiveMessage(this._id, event.data);
532 } else if (event.type === 'endpoint') {
533 // An SSE server that didn't correctly return a 4xx status when we POSTed
534 this._log(LogLevel.Warning, `Received SSE endpoint from a POST to ${this._launch.uri}, will fall back to legacy SSE`);
535 this._sseFallbackWithMessage(message);
536 throw new CancellationError(); // just to end the SSE stream
537 }
538 });
539
540 try {
541 await this._doSSE(parser, res);
542 } catch (err) {
543 this._log(LogLevel.Warning, `Error reading SSE stream: ${stringifyError(err)}`);
544 }
545 } else if (contentType.startsWith('application/json')) {
546 this._proxy.$onDidReceiveMessage(this._id, await res.text());
547 } else {
548 const responseBody = await res.text();
549 if (isJSON(responseBody)) { // try to read as JSON even if the server didn't set the content type
550 this._proxy.$onDidReceiveMessage(this._id, responseBody);
551 } else {
552 this._log(LogLevel.Warning, `Unexpected ${res.status} response for request: ${responseBody}`);
553 }
554 }
555 }
557 > /**
558 > * Attaches the SSE backchannel that streamable HTTP servers can use
559 > * for async notifications. This is a "MAY" support, so if the server gives
560 > * us a 4xx code, we'll stop trying to connect..
561 > */
562 > private async _attachStreamableBackchannel() {
563 let lastEventId: string | undefined;
564 let canReconnectAt: number | undefined;
565 for (let retry = 0; !this._store.isDisposed; retry++) {
566 if (canReconnectAt !== undefined) {
567 await timeout(Math.max(0, canReconnectAt - Date.now()), this._cts.token);
568 canReconnectAt = undefined;
569 } else {
570 await timeout(Math.min(retry * 1000, 30_000), this._cts.token);
571 }
572
573 let res: CommonResponse;
574 try {
575 const headers: Record<string, string> = {
576 ...Object.fromEntries(this._launch.headers),
577 'Accept': 'text/event-stream',
578 };
579 await this._addAuthHeader(headers);
580
581 if (this._mode.value === HttpMode.Http && this._mode.sessionId !== undefined) {
582 headers['Mcp-Session-Id'] = this._mode.sessionId;
583 }
584 if (lastEventId) {
585 headers['Last-Event-ID'] = lastEventId;
586 }
587
588 res = await this._fetchWithAuthRetry(
589 this._launch.uri.toString(true),
590 {
591 method: 'GET',
592 headers,
593 },
594 headers
595 );
596 } catch (e) {
597 this._log(LogLevel.Info, `Error connecting to ${this._launch.uri} for async notifications, will retry`);
598 continue;
599 }
600
601 if (res.status >= 400) {
602 this._log(LogLevel.Debug, `${res.status} status connecting to ${this._launch.uri} for async notifications; they will be disabled: ${await this._getErrText(res)}`);
603 return;
604 }
605
606 // Only reset the retry counter if we definitely get an event stream to avoid
607 // spamming servers that (incorrectly) don't return one from this endpoint.
608 if (res.headers.get('content-type')?.toLowerCase().includes('text/event-stream')) {
609 retry = 0;
610 }
611
612 const parser = new SSEParser(event => {
613 if (event.retry) {
614 canReconnectAt = Date.now() + event.retry;
615 }
616 if (event.type === 'message' && event.data) {
617 this._proxy.$onDidReceiveMessage(this._id, event.data);
618 }
619 if (event.id) {
620 lastEventId = event.id;
621 }
622 });
623
624 try {
625 await this._doSSE(parser, res);
626 } catch (e) {
627 this._log(LogLevel.Info, `Error reading from async stream, we will reconnect: ${e}`);
628 }
629 }
630 }
632 > /**
633 > * Starts a legacy SSE attachment, where the SSE response is the session lifetime.
634 > * Unlike `_attachStreamableBackchannel`, this fails the server if it disconnects.
635 > */
636 > private async _attachSSE(): Promise<string | undefined> {
637 const postEndpoint = new DeferredPromise<string>();
638 const headers: Record<string, string> = {
639 ...Object.fromEntries(this._launch.headers),
640 'Accept': 'text/event-stream',
641 };
642 await this._addAuthHeader(headers);
643
644 let res: CommonResponse;
645 try {
646 res = await this._fetchWithAuthRetry(
647 this._launch.uri.toString(true),
648 {
649 method: 'GET',
650 headers,
651 },
652 headers
653 );
654 if (res.status >= 300) {
655 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `${res.status} status connecting to ${this._launch.uri} as SSE: ${await this._getErrText(res)}` });
656 return;
657 }
658 } catch (e) {
659 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error connecting to ${this._launch.uri} as SSE: ${e}` });
660 return;
661 }
662
663 const parser = new SSEParser(event => {
664 if (event.type === 'message') {
665 this._proxy.$onDidReceiveMessage(this._id, event.data);
666 } else if (event.type === 'endpoint') {
667 postEndpoint.complete(new URL(event.data, this._launch.uri.toString(true)).toString());
668 }
669 });
670
671 this._register(toDisposable(() => postEndpoint.cancel()));
672 this._doSSE(parser, res).catch(err => {
673 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error reading SSE stream: ${stringifyError(err)}` });
674 });
675
676 return postEndpoint.p;
677 }
679 > /**
680 > * Sends a legacy SSE message to the server. The response is always empty and
681 > * is otherwise received in {@link _attachSSE}'s loop.
682 > */
683 > private async _sendLegacySSE(url: string, message: string) {
684 const asBytes = new TextEncoder().encode(message) as Uint8Array<ArrayBuffer>;
685 const headers: Record<string, string> = {
686 ...Object.fromEntries(this._launch.headers),
687 'Content-Type': 'application/json',
688 };
689 await this._addAuthHeader(headers);
690 const res = await this._fetch(url, {
691 method: 'POST',
692 headers,
693 body: asBytes,
694 });
695
696 if (res.status >= 300) {
697 this._log(LogLevel.Warning, `${res.status} status sending message to ${this._postEndpoint}: ${await this._getErrText(res)}`);
698 }
699 }
701 > /** Generic handle to pipe a response into an SSE parser. */
702 > private async _doSSE(parser: SSEParser, res: CommonResponse) {
703 if (!res.body) {
704 return;
705 }
706
707 const reader = res.body.getReader();
708 let chunk: ReadableStreamReadResult<Uint8Array>;
709 do {
710 try {
711 chunk = await raceCancellationError(reader.read(), this._cts.token);
712 } catch (err) {
713 reader.cancel();
714 if (this._store.isDisposed) {
715 return;
716 } else {
717 throw err;
718 }
719 }
720
721 if (chunk.value) {
722 parser.feed(chunk.value);
723 }
724 } while (!chunk.done);
725 }
727 > private async _addAuthHeader(headers: Record<string, string>, options?: { forceNewRegistration?: boolean; errorOnUserInteraction?: boolean }) {
728 const errorOnUserInteraction = options?.errorOnUserInteraction ?? this._errorOnUserInteraction;
729 if (this._authMetadata) {
730 try {
731 const authDetails: IMcpAuthenticationDetails = {
732 authorizationServer: this._authMetadata.authorizationServer.toJSON(),
733 authorizationServerMetadata: this._authMetadata.serverMetadata,
734 resourceMetadata: this._authMetadata.resourceMetadata,
735 scopes: this._authMetadata.scopes,
736 clientId: this._launch.oauth?.clientId,
737 enterpriseManaged: this._launch.oauth?.enterpriseManaged,
738 };
739 const token = await this._proxy.$getTokenFromServerMetadata(
740 this._id,
741 authDetails,
742 {
743 errorOnUserInteraction,
744 forceNewRegistration: options?.forceNewRegistration
745 });
746 if (token) {
747 setHostHeader(headers, 'Authorization', `Bearer ${token}`);
748 }
749 } catch (e) {
750 if (UserInteractionRequiredError.is(e)) {
751 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Stopped, reason: 'needs-user-interaction' });
752 throw new CancellationError();
753 }
754 this._log(LogLevel.Warning, `Error getting token from server metadata: ${String(e)}`);
755 }
756 }
757 if (this._launch.authentication) {
758 try {
759 this._log(LogLevel.Debug, `Using provided authentication config: providerId=${this._launch.authentication.providerId}, scopes=${this._launch.authentication.scopes.join(', ')}`);
760 const token = await this._proxy.$getTokenForProviderId(
761 this._id,
762 this._launch.authentication.providerId,
763 this._launch.authentication.scopes,
764 {
765 errorOnUserInteraction,
766 forceNewRegistration: options?.forceNewRegistration,
767 clientId: this._launch.oauth?.clientId,
768 }
769 );
770 if (token) {
771 setHostHeader(headers, 'Authorization', `Bearer ${token}`);
772 this._log(LogLevel.Info, 'Successfully obtained token from provided authentication config');
773 }
774 } catch (e) {
775 if (UserInteractionRequiredError.is(e)) {
776 this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Stopped, reason: 'needs-user-interaction' });
777 throw new CancellationError();
778 }
779 this._log(LogLevel.Warning, `Error getting token from provided authentication config: ${String(e)}`);
780 }
781 }
782 return headers;
783 }
785 > private _log(level: LogLevel, message: string) {
786 if (!this._store.isDisposed) {
787 this._proxy.$onDidPublishLog(this._id, level, message);
788 }
789 }
791 > private async _getErrText(res: CommonResponse) {
792 try {
793 return await res.text();
794 } catch {
795 return res.statusText;
796 }
797 }
799 > /**
800 > * Helper method to perform fetch with authentication retry logic.
801 > * If the initial request returns an auth error and we don't have auth metadata,
802 > * it will populate the auth metadata and retry once.
803 > * If we already have auth metadata, check if the scopes changed and update them.
804 > */
805 > private async _fetchWithAuthRetry(mcpUrl: string, init: MinimalRequestInit, headers: Record<string, string>): Promise<CommonResponse> {
806 const doFetch = () => this._fetch(mcpUrl, init);
807
808 let res = await doFetch();
809 if (isAuthStatusCode(res.status)) {
810 if (!this._authMetadata) {
811 this._authMetadata = await createAuthMetadata(mcpUrl, res.headers, {
812 sameOriginHeaders: {
813 ...Object.fromEntries(this._launch.headers),
814 'MCP-Protocol-Version': MCP.LATEST_PROTOCOL_VERSION
815 },
816 fetch: (url, init) => this._fetch(url, init as MinimalRequestInit),
817 log: (level, message) => this._log(level, message)
818 });
819 this._proxy.$logMcpAuthSetup(this._authMetadata.telemetry);
820 await this._addAuthHeader(headers);
821 if (headers['Authorization']) {
822 // Update the headers in the init object
823 init.headers = headers;
824 res = await doFetch();
825 }
826 } else {
827 // We have auth metadata, but got an auth error. Check if the scopes changed.
828 if (this._authMetadata.update(res.headers)) {
829 await this._addAuthHeader(headers);
830 if (headers['Authorization']) {
831 // Update the headers in the init object
832 init.headers = headers;
833 res = await doFetch();
834 }
835 }
836 }
837 }
838 // If we have an Authorization header and still get an auth error, we should retry with a new auth registration
839 if (headers['Authorization'] && isAuthStatusCode(res.status)) {
840 const errorText = await this._getErrText(res);
841 this._log(LogLevel.Info, `Received ${res.status} status with Authorization header, retrying with new auth registration. Error details: ${errorText || 'no additional details'}`);
842 await this._addAuthHeader(headers, { forceNewRegistration: true });
843 res = await doFetch();
844 }
845 return res;
846 }
848 > private async _fetch(url: string, init: MinimalRequestInit): Promise<CommonResponse> {
849 setHostHeader(init.headers, 'user-agent', `${product.nameLong}/${product.version}`);
850
851 if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
852 const traceObj: any = { ...init, headers: { ...init.headers } };
853 if (traceObj.body) {
854 traceObj.body = new TextDecoder().decode(traceObj.body);
855 }
856 if (traceObj.headers?.Authorization) {
857 traceObj.headers.Authorization = '***'; // don't log the auth header
858 }
859 this._log(LogLevel.Trace, `Fetching ${url} with options: ${JSON.stringify(traceObj)}`);
860 }
861
862 let currentUrl = url;
863 let response!: CommonResponse;
864 for (let redirectCount = 0; redirectCount < MAX_FOLLOW_REDIRECTS; redirectCount++) {
865 response = await this._fetchInternal(currentUrl, {
866 ...init,
867 signal: this._abortCtrl.signal,
868 redirect: 'manual'
869 });
870
871 // Check for redirect status codes (301, 302, 303, 307, 308)
872 if (!REDIRECT_STATUS_CODES.includes(response.status)) {
873 break;
874 }
875
876 const location = response.headers.get('location');
877 if (!location) {
878 break;
879 }
880
881 const currentUrlParsed = new URL(currentUrl);
882 const nextUrlParsed = new URL(location, currentUrl);
883
884 // Only follow redirects to http(s). Blocks a malicious Location header from
885 // reaching the unix:// / pipe:// socket dispatcher or other local schemes.
886 // Fail closed so the connection errors deterministically rather than the
887 // caller treating the 3xx response as final.
888 if (!ALLOWED_REDIRECT_PROTOCOLS.has(nextUrlParsed.protocol)) {
889 throw new Error(`MCP server redirected to a non-http(s) target (${nextUrlParsed.protocol}), which is not allowed`);
890 }
891
892 // On a cross-origin redirect, strip credential-bearing headers so tokens and
893 // session ids configured for the original origin are not replayed to another host.
894 if (currentUrlParsed.origin !== nextUrlParsed.origin) {
895 for (const name of Object.keys(init.headers)) {
896 if (CROSS_ORIGIN_STRIPPED_HEADERS.has(name.toLowerCase())) {
897 delete init.headers[name];
898 }
899 }
900 }
901
902 const nextUrl = nextUrlParsed.toString();
903 this._log(LogLevel.Trace, `Redirect (${response.status}) from ${currentUrl} to ${nextUrl}`);
904 currentUrl = nextUrl;
905 // Per fetch spec, for 303 always use GET, keep method unless original was POST and 301/302, then GET.
906 if (response.status === 303 || ((response.status === 301 || response.status === 302) && init.method === 'POST')) {
907 init.method = 'GET';
908 delete init.body;
909 }
910 }
911
912 if (canLog(this._logService.getLevel(), LogLevel.Trace)) {
913 const headers: Record<string, string> = {};
914 response.headers.forEach((value, key) => { headers[key] = value; });
915 this._log(LogLevel.Trace, `Fetched ${currentUrl}: ${JSON.stringify({
916 status: response.status,
917 headers: headers,
918 })}`);
919 }
920
921 return response;
922 }
924 > protected _fetchInternal(url: string, init?: CommonRequestInit): Promise<CommonResponse> {
925 return fetch(url, init);
926 }
928 >
929 > interface MinimalRequestInit {
930 > method: string;
931 > headers: Record<string, string>;
932 > body?: Uint8Array<ArrayBuffer>;
933 > }
934 >
935 > export interface CommonRequestInit extends MinimalRequestInit {
936 > signal?: AbortSignal;
937 > redirect?: RequestRedirect;
938 > }
939 >
940 > export interface CommonResponse {
941 > status: number;
942 > statusText: string;
943 > headers: Headers;
944 > body?: ReadableStream | null;
945 > url: string;
946 > json(): Promise<any>;
947 > text(): Promise<string>;
948 > }
949 >
950 function isJSON(str: string): boolean {
951 try {
952 JSON.parse(str);
953 return true;
954 } catch (e) {
955 return false;
956 }
957 }
959 function isAuthStatusCode(status: number): boolean {
960 return status === 401 || status === 403;
961 }
963 >
964 > //#region AuthMetadata
965 >
966 > /**
967 > * Logger callback type for AuthMetadata operations.
968 > */
969 > export type AuthMetadataLogger = (level: LogLevel, message: string) => void;
970 >
971 > /**
972 > * Interface for authentication metadata that can be updated when scopes change.
973 > */
974 > export interface IAuthMetadata {
975 > readonly authorizationServer: URI;
976 > readonly serverMetadata: IAuthorizationServerMetadata;
977 > readonly resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined;
978 > readonly scopes: string[] | undefined;
979 > /** Telemetry data about how auth metadata was discovered */
980 > readonly telemetry: IAuthMetadataSource;
981 >
982 > /**
983 > * Updates the scopes based on the WWW-Authenticate header in the response.
984 > * @param response The HTTP response containing potential scope challenges
985 > * @returns true if scopes were updated, false otherwise
986 > */
987 > update(responseHeaders: Headers): boolean;
988 > }
989 >
990 > /**
991 > * Concrete implementation of IAuthMetadata that manages OAuth authentication metadata.
992 > * Consumers should use {@link createAuthMetadata} to create instances.
993 > */
994 > class AuthMetadata implements IAuthMetadata {
995 > private _scopes: string[] | undefined;
996 >
997 > constructor(
998 > public readonly authorizationServer: URI, extHostMcp.ts ×8
999 > public readonly serverMetadata: IAuthorizationServerMetadata,
1000 > public readonly resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
1001 > scopes: string[] | undefined,
1002 > public readonly telemetry: IAuthMetadataSource,
1003 > private readonly _log: AuthMetadataLogger,
1004 > ) {
1005 > this._scopes = scopes;
1006 > }
1008 > get scopes(): string[] | undefined {
1009 > return this._scopes; extHostMcp.ts ×1
1010 > }
1012 > update(responseHeaders: Headers): boolean {
1013 > const scopesChallenge = this._parseScopesFromResponse(responseHeaders); extHostMcp.ts ×4
1014 > if (!scopesMatch(scopesChallenge, this._scopes)) {
1015 > this._log(LogLevel.Info, `Scopes changed from ${JSON.stringify(this._scopes)} to ${JSON.stringify(scopesChallenge)}, updating`); extHostMcp.ts ×1
1016 > this._scopes = scopesChallenge;
1017 > return true;
1018 > }
1019 > return false; extHostMcp.ts ×1
1022 > private _parseScopesFromResponse(responseHeaders: Headers): string[] | undefined {
1023 > const authHeader = responseHeaders.get('WWW-Authenticate'); extHostMcp.ts ×4
1024 > if (!authHeader) {
1025 > return undefined; extHostMcp.ts ×1
1026 > }
1027 > const challenges = parseWWWAuthenticateHeader(authHeader); extHostMcp.ts ×2
1028 > for (const challenge of challenges) {
1029 > if (challenge.scheme === 'Bearer' && challenge.params['scope']) {
1030 > const scopes = challenge.params['scope'].split(AUTH_SCOPE_SEPARATOR).filter(s => s.trim().length); extHostMcp.ts ×1
1031 > if (scopes.length) {
1032 > this._log(LogLevel.Info, `Found scope challenge in WWW-Authenticate header: ${challenge.params['scope']}`);
1033 > return scopes;
1034 > }
1035 > }
1037 > return undefined; extHostMcp.ts ×1
1040 >
1041 > /**
1042 > * Options for creating AuthMetadata.
1043 > */
1044 > export interface ICreateAuthMetadataOptions {
1045 > /** Headers to include when fetching metadata from the same origin as the resource server */
1046 > sameOriginHeaders?: Record<string, string>;
1047 > /** Fetch function to use for HTTP requests */
1048 > fetch: (url: string, init: MinimalRequestInit) => Promise<CommonResponse>;
1049 > /** Logger function for diagnostic output */
1050 > log: AuthMetadataLogger;
1051 > }
1052 >
1053 > /**
1054 > * Creates an AuthMetadata instance by discovering OAuth metadata from the server.
1055 > *
1056 > * This function:
1057 > * 1. Parses the WWW-Authenticate header for resource_metadata and scope challenges
1058 > * 2. Fetches OAuth protected resource metadata from well-known URIs or the challenge URL
1059 > * 3. Fetches authorization server metadata
1060 > * 4. Falls back to default metadata if discovery fails
1061 > *
1062 > * @param resourceUrl The resource server URL
1063 > * @param wwwAuthenticateValue The value of the WWW-Authenticate header from the original HTTP response
1064 > * @param options Configuration options including headers, fetch function, and logger
1065 > * @returns A new AuthMetadata instance
1066 > */
1067 > export async function createAuthMetadata( extHostMcp.ts ×8
1068 > resourceUrl: string,
1069 > initialResponseHeaders: Headers,
1070 > options: ICreateAuthMetadataOptions
1071 > ): Promise<AuthMetadata> {
1072 > const { sameOriginHeaders, fetch, log } = options;
1073 >
1074 > // Track discovery sources for telemetry
1075 > let resourceMetadataSource = IAuthResourceMetadataSource.None;
1076 > let serverMetadataSource: IAuthServerMetadataSource | undefined;
1077 >
1078 > // Parse the WWW-Authenticate header for resource_metadata and scope challenges
1079 > const { resourceMetadataChallenge, scopesChallenge: scopesChallengeFromHeader } = parseWWWAuthenticateHeaderForChallenges(initialResponseHeaders.get('WWW-Authenticate') ?? undefined, log);
1080 >
1081 > // Fetch the resource metadata either from the challenge URL or from well-known URIs
1082 > let serverMetadataUrl: string | undefined;
1083 > let resource: IAuthorizationProtectedResourceMetadata | undefined;
1084 > let scopesChallenge = scopesChallengeFromHeader;
1085 >
1086 > try {
1087 > const { metadata, discoveryUrl, errors } = await fetchResourceMetadata(resourceUrl, resourceMetadataChallenge, {
1088 > sameOriginHeaders,
1089 > fetch: (url, init) => fetch(url, init as MinimalRequestInit)
1090 > });
1091 > for (const err of errors) { extHostMcp.ts ×5
1092 log(LogLevel.Warning, `Error fetching resource metadata: ${err}`);
1093 }
1094 > log(LogLevel.Info, `Discovered resource metadata at ${discoveryUrl}`); extHostMcp.ts ×5
1095 >
1096 > // Determine if resource metadata came from header or well-known
1097 > resourceMetadataSource = resourceMetadataChallenge ? IAuthResourceMetadataSource.Header : IAuthResourceMetadataSource.WellKnown; extHostMcp.ts ×8
1098 >
1099 > // TODO:@TylerLeonhardt support multiple authorization servers
1100 > // Consider using one that has an auth provider first, over the dynamic flow
1101 > serverMetadataUrl = metadata.authorization_servers?.[0];
1102 > if (!serverMetadataUrl) {
1103 log(LogLevel.Warning, `No authorization_servers found in resource metadata ${discoveryUrl} - Is this resource metadata configured correctly?`);
1104 > } else { extHostMcp.ts ×5
1105 > log(LogLevel.Info, `Using auth server metadata url: ${serverMetadataUrl}`);
1106 > serverMetadataSource = IAuthServerMetadataSource.ResourceMetadata;
1107 > }
1108 > scopesChallenge ??= metadata.scopes_supported;
1109 > resource = metadata;
1110 > } catch (e) { extHostMcp.ts ×8
1111 > log(LogLevel.Warning, `Could not fetch resource metadata: ${String(e)}`); extHostMcp.ts ×3
1112 > }
1114 > const baseUrl = new URL(resourceUrl).origin;
1115 >
1116 > // If we are not given a resource_metadata, see if the well-known server metadata is available
1117 > // on the base url.
1118 > let additionalHeaders: Record<string, string> = {};
1119 > if (!serverMetadataUrl) {
1120 > serverMetadataUrl = baseUrl; extHostMcp.ts ×3
1121 > // Maintain the same origin headers when talking to the resource origin.
1122 > if (sameOriginHeaders) {
1123 > additionalHeaders = sameOriginHeaders;
1124 > }
1125 > }
1127 > try {
1128 > log(LogLevel.Debug, `Fetching auth server metadata for: ${serverMetadataUrl} ...`);
1129 > const { metadata, discoveryUrl, errors } = await fetchAuthorizationServerMetadata(serverMetadataUrl, {
1130 > additionalHeaders,
1131 > fetch: (url, init) => fetch(url, init as MinimalRequestInit)
1132 > });
1133 > for (const err of errors) { extHostMcp.ts ×5
1134 log(LogLevel.Warning, `Error fetching authorization server metadata: ${err}`);
1135 }
1136 > log(LogLevel.Info, `Discovered authorization server metadata at ${discoveryUrl}`); extHostMcp.ts ×5
1137 >
1138 > // If serverMetadataSource is not yet defined, it means we fell back to baseUrl
1139 > // and successfully fetched from well-known
1140 > serverMetadataSource ??= IAuthServerMetadataSource.WellKnown;
1141 >
1142 > return new AuthMetadata(
1143 > URI.parse(serverMetadataUrl),
1144 > metadata,
1145 > resource,
1146 > scopesChallenge,
1147 > { resourceMetadataSource, serverMetadataSource },
1148 > log
1149 > );
1150 > } catch (e) { extHostMcp.ts ×8
1151 > log(LogLevel.Warning, `Error populating auth server metadata for ${serverMetadataUrl}: ${String(e)}`); extHostMcp.ts ×3
1152 > }
1153 >
1154 > // If there's no well-known server metadata, then use the default values based off of the url.
1155 > const defaultMetadata = getDefaultMetadataForUrl(new URL(baseUrl));
1156 > log(LogLevel.Info, 'Using default auth metadata');
1157 > return new AuthMetadata(
1158 > URI.parse(baseUrl),
1159 > defaultMetadata,
1160 > resource,
1161 > scopesChallenge,
1162 > { resourceMetadataSource, serverMetadataSource: IAuthServerMetadataSource.Default },
1163 > log
1164 > );
1165 > }
1167 > /**
1168 > * Parses the WWW-Authenticate header for resource_metadata and scope challenges.
1169 > */
1170 > function parseWWWAuthenticateHeaderForChallenges( extHostMcp.ts ×8
1171 > wwwAuthenticateValue: string | undefined,
1172 > log: AuthMetadataLogger
1173 > ): { resourceMetadataChallenge?: string; scopesChallenge?: string[] } {
1174 > if (!wwwAuthenticateValue) {
1175 > return {}; extHostMcp.ts ×1
1176 > }
1177 > let resourceMetadataChallenge: string | undefined; extHostMcp.ts ×4
1178 > let scopesChallenge: string[] | undefined;
1179 >
1180 > const challenges = parseWWWAuthenticateHeader(wwwAuthenticateValue);
1181 > for (const challenge of challenges) {
1182 > if (challenge.scheme === 'Bearer') {
1183 > if (!resourceMetadataChallenge && challenge.params['resource_metadata']) {
1184 > resourceMetadataChallenge = challenge.params['resource_metadata']; extHostMcp.ts ×1
1185 > log(LogLevel.Debug, `Found resource_metadata challenge in WWW-Authenticate header: ${resourceMetadataChallenge}`);
1186 > }
1187 > if (!scopesChallenge && challenge.params['scope']) { extHostMcp.ts ×4
1188 > const scopes = challenge.params['scope'].split(AUTH_SCOPE_SEPARATOR).filter(s => s.trim().length); extHostMcp.ts ×1
1189 > if (scopes.length) {
1190 > log(LogLevel.Debug, `Found scope challenge in WWW-Authenticate header: ${challenge.params['scope']}`);
1191 > scopesChallenge = scopes;
1192 > }
1193 > }
1194 > if (resourceMetadataChallenge && scopesChallenge) { extHostMcp.ts ×4
1195 break;
1196 }
1198 > }
1199 > return { resourceMetadataChallenge, scopesChallenge };
1200 > }
1202 > //#endregion