diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 8e46cb71541..2cab7b81832 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -83,6 +83,7 @@ const CORE_TYPES = [ 'Crypto', 'SubtleCrypto', 'JsonWebKey', + 'MessageEvent', ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 235d2d68dbe..685dd59cb36 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -82,6 +82,7 @@ const CORE_TYPES = [ 'Crypto', 'SubtleCrypto', 'JsonWebKey', + 'MessageEvent', ]; // Types that are defined in a common layer but are known to be only diff --git a/build/package-lock.json b/build/package-lock.json index 66092130f14..445e842c5e3 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -8,9 +8,6 @@ "name": "code-oss-dev-build", "version": "1.0.0", "license": "MIT", - "dependencies": { - "tree-sitter-typescript": "0.23.2" - }, "devDependencies": { "@azure/core-auth": "^1.9.0", "@azure/cosmos": "^3", diff --git a/eslint.config.js b/eslint.config.js index 8e3d288967b..2b005721dee 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -997,6 +997,7 @@ export default tseslint.config( { 'target': 'src/vs/workbench/api/~', 'restrictions': [ + '@c4312/eventsource-umd', 'vscode', 'vs/base/~', 'vs/base/parts/*/~', diff --git a/package-lock.json b/package-lock.json index f0de7edaeb1..287ffc81be5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", @@ -941,6 +942,18 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@c4312/eventsource-umd": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@c4312/eventsource-umd/-/eventsource-umd-3.0.5.tgz", + "integrity": "sha512-0QhLg51eFB+SS/a4Pv5tHaRSnjJBpdFsjT3WN/Vfh6qzeFXqvaE+evVIIToYvr2lRBLg1NIB635ip8ML+/84Sg==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -6687,6 +6700,15 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", + "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", diff --git a/package.json b/package.json index 749aa92d4dd..b37a901c3ad 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json" }, "dependencies": { + "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", diff --git a/src/vs/platform/mcp/common/mcpManagementCli.ts b/src/vs/platform/mcp/common/mcpManagementCli.ts index 28ff0484bab..f25efca6999 100644 --- a/src/vs/platform/mcp/common/mcpManagementCli.ts +++ b/src/vs/platform/mcp/common/mcpManagementCli.ts @@ -5,9 +5,9 @@ import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ILogger } from '../../log/common/log.js'; -import { IMcpConfiguration, IMcpConfigurationServer } from './mcpPlatformTypes.js'; +import { IMcpConfiguration, IMcpConfigurationSSE, IMcpConfigurationStdio } from './mcpPlatformTypes.js'; -type ValidatedConfig = { name: string; config: IMcpConfigurationServer }; +type ValidatedConfig = { name: string; config: IMcpConfigurationStdio | IMcpConfigurationSSE }; export class McpManagementCli { constructor( @@ -35,7 +35,7 @@ export class McpManagementCli { } private validateConfiguration(config: string): ValidatedConfig { - let parsed: IMcpConfigurationServer & { name: string }; + let parsed: (IMcpConfigurationStdio | IMcpConfigurationSSE) & { name: string }; try { parsed = JSON.parse(config); } catch (e) { @@ -46,12 +46,12 @@ export class McpManagementCli { throw new InvalidMcpOperationError(`Missing name property in ${config}`); } - if (!parsed.command) { - throw new InvalidMcpOperationError(`Missing command property in ${config}`); + if (!('command' in parsed) && !('url' in parsed)) { + throw new InvalidMcpOperationError(`Missing command or URL property in ${config}`); } const { name, ...rest } = parsed; - return { name, config: rest }; + return { name, config: rest as IMcpConfigurationStdio | IMcpConfigurationSSE }; } } diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index bee43d9672b..815ee332b40 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -6,12 +6,19 @@ export interface IMcpConfiguration { inputs: unknown[]; /** @deprecated Only for rough cross-compat with other formats */ - mcpServers?: Record; - servers: Record; + mcpServers?: Record; + servers: Record; } -export interface IMcpConfigurationServer { +export interface IMcpConfigurationStdio { + type?: 'stdio'; command: string; args?: readonly string[]; env?: Record; } + +export interface IMcpConfigurationSSE { + type: 'sse'; + url: string; + headers?: Record; +} diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f302808a52b..2c88e4106a9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2970,7 +2970,7 @@ export interface ExtHostTestingShape { } export interface ExtHostMcpShape { - $startMcp(id: number, launch: McpServerLaunch): void; + $startMcp(id: number, launch: McpServerLaunch.Serialized): void; $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; $waitForInitialCollectionProviders(): Promise; diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 437ed6a978f..eead33f21fe 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -3,13 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as ES from '@c4312/eventsource-umd'; import * as vscode from 'vscode'; +import { importAMDNodeModule } from '../../../amdX.js'; +import { DeferredPromise, Sequencer } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; -import { DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Lazy } from '../../../base/common/lazy.js'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { StorageScope } from '../../../platform/storage/common/storage.js'; -import { extensionPrefixedIdentifier, McpCollectionDefinition, McpServerDefinition, McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportSSE, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { ExtHostMcpShape, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; import { IExtHostRpcService } from './extHostRpcService.js'; @@ -19,34 +23,52 @@ export interface IExtHostMpcService extends ExtHostMcpShape { registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable; } -export class ExtHostMcpService implements IExtHostMpcService { +export class ExtHostMcpService extends Disposable implements IExtHostMpcService { protected _proxy: MainThreadMcpShape; private readonly _initialProviderPromises = new Set>(); + private readonly _sseEventSources = this._register(new DisposableMap()); + private readonly _eventSource = new Lazy(async () => { + const es = await importAMDNodeModule('@c4312/eventsource-umd', 'dist/index.umd.js'); + return es.EventSource; + }); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, ) { + super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadMcp); } - $startMcp(id: number, launch: McpServerLaunch): void { - // todo: SSE launches can be implemented in this common layer + $startMcp(id: number, launch: McpServerLaunch.Serialized): void { + this._startMcp(id, McpServerLaunch.fromSerialized(launch)); + } + + protected _startMcp(id: number, launch: McpServerLaunch): void { + if (launch.type === McpServerTransportType.SSE) { + this._sseEventSources.set(id, new McpSSEHandle(this._eventSource.value, id, launch, this._proxy)); + return; + } + throw new Error('not implemented'); } $stopMcp(id: number): void { - // no-op + if (this._sseEventSources.has(id)) { + this._sseEventSources.deleteAndDispose(id); + this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Stopped }); + } } $sendMessage(id: number, message: string): void { - // no-op + this._sseEventSources.get(id)?.send(message); } async $waitForInitialCollectionProviders(): Promise { await Promise.all(this._initialProviderPromises); } - registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable { + /** {@link vscode.lm.registerMcpConfigurationProvider} */ + public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable { const store = new DisposableStore(); const metadata = extension.contributes?.modelContextServerCollections?.find(m => m.id === id); @@ -78,7 +100,8 @@ export class ExtHostMcpService implements IExtHostMpcService { launch: isSSEConfig(item) ? { type: McpServerTransportType.SSE, - uri: item.uri + uri: item.uri, + headers: item.headers, } : { type: McpServerTransportType.Stdio, @@ -113,3 +136,105 @@ export class ExtHostMcpService implements IExtHostMpcService { return store; } } + +class McpSSEHandle extends Disposable { + private readonly _requestSequencer = new Sequencer(); + private readonly _postEndpoint = new DeferredPromise(); + constructor( + eventSourceCtor: Promise, + private readonly _id: number, + launch: McpServerTransportSSE, + private readonly _proxy: MainThreadMcpShape + ) { + super(); + eventSourceCtor.then(EventSourceCtor => this._attach(EventSourceCtor, launch)); + } + + private _attach(EventSourceCtor: typeof ES.EventSource, launch: McpServerTransportSSE) { + if (this._store.isDisposed) { + return; + } + + const eventSource = new EventSourceCtor(launch.uri.toString(), { + // recommended way to do things https://github.com/EventSource/eventsource?tab=readme-ov-file#setting-http-request-headers + fetch: (input, init) => + fetch(input, { + ...init, + headers: { + ...Object.fromEntries(launch.headers), + ...init?.headers, + }, + }).then(async res => { + // we get more details on failure at this point, so handle it explicitly: + if (res.status >= 300) { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `${res.status} status connecting to ${launch.uri}: ${await this._getErrText(res)}` }); + eventSource.close(); + } + return res; + }, err => { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error connecting to ${launch.uri}: ${String(err)}` }); + + eventSource.close(); + return Promise.reject(err); + }) + }); + + this._register(toDisposable(() => eventSource.close())); + + // https://github.com/modelcontextprotocol/typescript-sdk/blob/0fa2397174eba309b54575294d56754c52b13a65/src/server/sse.ts#L52 + eventSource.addEventListener('endpoint', e => { + this._postEndpoint.complete(new URL(e.data, launch.uri.toString()).toString()); + }); + + // https://github.com/modelcontextprotocol/typescript-sdk/blob/0fa2397174eba309b54575294d56754c52b13a65/src/server/sse.ts#L133 + eventSource.addEventListener('message', e => { + this._proxy.$onDidReceiveMessage(this._id, e.data); + }); + + eventSource.addEventListener('open', () => { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Running }); + }); + + eventSource.addEventListener('error', (err) => { + this._postEndpoint.cancel(); + this._proxy.$onDidChangeState(this._id, { + state: McpConnectionState.Kind.Error, + message: `Error connecting to ${launch.uri}: ${err.code || 0} ${err.message || JSON.stringify(err)}`, + }); + eventSource.close(); + }); + } + + async send(message: string) { + // only the sending of the request needs to be sequenced + try { + const res = await this._requestSequencer.queue(async () => { + const endpoint = await this._postEndpoint.p; + const asBytes = new TextEncoder().encode(message); + + return fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': String(asBytes.length), + }, + body: asBytes, + }); + }); + + if (res.status >= 300) { + this._proxy.$onDidPublishLog(this._id, `${res.status} status sending message to ${this._postEndpoint}: ${await this._getErrText(res)}`); + } + } catch (err) { + // ignored + } + } + + private async _getErrText(res: Response) { + try { + return await res.text(); + } catch { + return res.statusText; + } + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index cb861fedc0f..9d28387e9cc 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -5053,6 +5053,7 @@ export class McpStdioServerDefinition implements vscode.McpStdioServerDefinition } export class McpSSEServerDefinition implements vscode.McpSSEServerDefinition { + headers: [string, string][] = []; constructor( public label: string, public uri: URI diff --git a/src/vs/workbench/api/node/extHostMpcNode.ts b/src/vs/workbench/api/node/extHostMpcNode.ts index 0ad54d3c5d1..15fb24c0a84 100644 --- a/src/vs/workbench/api/node/extHostMpcNode.ts +++ b/src/vs/workbench/api/node/extHostMpcNode.ts @@ -25,11 +25,11 @@ export class NodeExtHostMpcService extends ExtHostMcpService { child: ChildProcessWithoutNullStreams; }>(); - override $startMcp(id: number, launch: McpServerLaunch): void { + protected override _startMcp(id: number, launch: McpServerLaunch): void { if (launch.type === McpServerTransportType.Stdio) { this.startNodeMpc(id, launch); } else { - super.$startMcp(id, launch); + super._startMcp(id, launch); } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 8cd4acc8051..7e6491406cb 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -43,7 +43,7 @@ export class ListMcpServerCommand extends Action2 { f1: true, menu: { when: ContextKeyExpr.and( - McpContextKeys.hasUnknownTools, + ContextKeyExpr.or(McpContextKeys.hasUnknownTools, McpContextKeys.hasServersWithErrors), ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent) ), id: MenuId.ChatInputAttachmentToolbar, @@ -214,7 +214,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo case McpServerToolsState.RefreshingFromUnknown: thisState = DisplayedState.Refreshing; break; - case McpServerToolsState.Cached: + default: thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None; break; } diff --git a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts index 1e698b7e6b1..04931acfe3d 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts @@ -7,6 +7,7 @@ import { equals as arrayEquals } from '../../../../../base/common/arrays.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; @@ -102,7 +103,11 @@ export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery { const nextDefinitions = Object.entries(value?.servers || {}).map(([name, value]): McpServerDefinition => ({ id: `${collectionId}.${name}`, label: name, - launch: { + launch: 'type' in value && value.type === 'sse' ? { + type: McpServerTransportType.SSE, + uri: URI.parse(value.url), + headers: Object.entries(value.headers || {}), + } : { type: McpServerTransportType.Stdio, args: value.args || [], command: value.command, diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index 0c01707f9d7..ca95443116f 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -10,7 +10,7 @@ import { mcpSchemaId } from '../../../services/configuration/common/configuratio import { inputsSchema } from '../../../services/configurationResolver/common/configurationResolverSchema.js'; import { IExtensionPointDescriptor } from '../../../services/extensions/common/extensionsRegistry.js'; -export type { IMcpConfigurationServer, IMcpConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +export type { IMcpConfigurationStdio, IMcpConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; const mcpActivationEventPrefix = 'onMcpCollection:'; @@ -44,32 +44,64 @@ export const mcpServerSchema: IJSONSchema = { servers: { examples: [mcpSchemaExampleServers], additionalProperties: { - type: 'object', - additionalProperties: false, - examples: [mcpSchemaExampleServer], - properties: { - command: { - type: 'string', - description: localize('app.mcp.json.command', "The command to run the server.") - }, - args: { - type: 'array', - description: localize('app.mcp.args.command', "Arguments passed to the server."), - items: { - type: 'string' + oneOf: [{ + type: 'object', + additionalProperties: false, + examples: [mcpSchemaExampleServer], + properties: { + type: { + type: 'string', + enum: ['stdio'], + description: localize('app.mcp.json.type', "The type of the server.") }, - }, - env: { - description: localize('app.mcp.env.command', "Environment variables passed to the server."), - additionalProperties: { - anyOf: [ - { type: 'null' }, - { type: 'string' }, - { type: 'number' }, - ] - } - }, - } + command: { + type: 'string', + description: localize('app.mcp.json.command', "The command to run the server.") + }, + args: { + type: 'array', + description: localize('app.mcp.args.command', "Arguments passed to the server."), + items: { + type: 'string' + }, + }, + env: { + description: localize('app.mcp.env.command', "Environment variables passed to the server."), + additionalProperties: { + anyOf: [ + { type: 'null' }, + { type: 'string' }, + { type: 'number' }, + ] + } + }, + } + }, { + type: 'object', + additionalProperties: false, + required: ['url', 'type'], + examples: [{ + type: 'sse', + url: 'http://localhost:3001', + headers: {}, + }], + properties: { + type: { + type: 'string', + enum: ['sse'], + description: localize('app.mcp.json.type', "The type of the server.") + }, + url: { + type: 'string', + format: 'uri', + description: localize('app.mcp.json.url', "The URL of the server-sent-event (SSE) server.") + }, + env: { + description: localize('app.mcp.json.headers', "Additional headers sent to the server."), + additionalProperties: { type: 'string' }, + }, + } + }] } }, inputs: inputsSchema.definitions!.inputs diff --git a/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts index 6bade4f4f51..0d0aff9aa6d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts @@ -8,14 +8,23 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { localize } from '../../../../nls.js'; import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { LazyCollectionState, IMcpService, McpServerToolsState } from './mcpTypes.js'; +import { LazyCollectionState, IMcpService, McpServerToolsState, McpConnectionState } from './mcpTypes.js'; export namespace McpContextKeys { export const serverCount = new RawContextKey('mcp.serverCount', undefined, { type: 'number', description: localize('mcp.serverCount.description', "Context key that has the number of registered MCP servers") }); export const hasUnknownTools = new RawContextKey('mcp.hasUnknownTools', undefined, { type: 'boolean', description: localize('mcp.hasUnknownTools.description', "Indicates whether there are MCP servers with unknown tools.") }); + /** + * A context key that indicates whether there are any servers with errors. + * + * @type {boolean} + * @default undefined + * @description This key is used to track the presence of servers with errors in the MCP context. + */ + export const hasServersWithErrors = new RawContextKey('mcp.hasServersWithErrors', undefined, { type: 'boolean', description: localize('mcp.hasServersWithErrors.description', "Indicates whether there are any MCP servers with errors.") }); export const toolsCount = new RawContextKey('mcp.toolsCount', undefined, { type: 'number', description: localize('mcp.toolsCount.description', "Context key that has the number of registered MCP tools") }); } @@ -34,6 +43,8 @@ export class McpContextKeysController extends Disposable implements IWorkbenchCo const ctxToolsCount = McpContextKeys.toolsCount.bindTo(contextKeyService); const ctxHasUnknownTools = McpContextKeys.hasUnknownTools.bindTo(contextKeyService); + this._store.add(bindContextKey(McpContextKeys.hasServersWithErrors, contextKeyService, r => mcpService.servers.read(r).some(c => c.connectionState.read(r).state === McpConnectionState.Kind.Error))); + this._store.add(autorun(r => { const servers = mcpService.servers.read(r); const serverTools = servers.map(s => s.tools.read(r)); diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index d1bc0e53fe0..640f98da8ca 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -209,6 +209,12 @@ export class McpServer extends Disposable implements IMcpServer { } let connection = this._connection.get(); + if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) { + connection.dispose(); + connection = undefined; + this._connection.set(connection, undefined); + } + if (!connection) { connection = await this._mcpRegistry.resolveConnection({ collectionRef: this.collection, diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 4e9a22e7576..db12df70ab6 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -262,6 +262,7 @@ export interface McpServerTransportStdio { export interface McpServerTransportSSE { readonly type: McpServerTransportType.SSE; readonly uri: URI; + readonly headers: [string, string][]; } export type McpServerLaunch = @@ -270,7 +271,7 @@ export type McpServerLaunch = export namespace McpServerLaunch { export type Serialized = - | { type: McpServerTransportType.SSE; uri: UriComponents } + | { type: McpServerTransportType.SSE; uri: UriComponents; headers: [string, string][] } | { type: McpServerTransportType.Stdio; cwd: UriComponents | undefined; command: string; args: readonly string[]; env: Record }; export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { @@ -280,7 +281,7 @@ export namespace McpServerLaunch { export function fromSerialized(launch: McpServerLaunch.Serialized): McpServerLaunch { switch (launch.type) { case McpServerTransportType.SSE: - return { type: launch.type, uri: URI.revive(launch.uri) }; + return { type: launch.type, uri: URI.revive(launch.uri), headers: launch.headers }; case McpServerTransportType.Stdio: return { type: launch.type, diff --git a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts index ac1c7e4de6d..28a9c7b2f36 100644 --- a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts @@ -24,6 +24,8 @@ declare module 'vscode' { uri: Uri; + headers: [string, string][]; + constructor(label: string, uri: Uri); }