mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
mcp: support sse (#243621)
* mcp: support sse
Didn't seem like Claude Desktop configs have SSE support yet, but I did
the obvious of having an object with a `url`. I also added a `type`
(optional for stdio) so we can better disambiguate types of configs.
Example .vscode/mcp.json:
```
{
"servers": {
"everything": {
"type": "sse",
"url": "http://localhost:3001/sse"
}
}
}
```
Closes #243242
* update layer check
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
-3
@@ -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",
|
||||
|
||||
@@ -997,6 +997,7 @@ export default tseslint.config(
|
||||
{
|
||||
'target': 'src/vs/workbench/api/~',
|
||||
'restrictions': [
|
||||
'@c4312/eventsource-umd',
|
||||
'vscode',
|
||||
'vs/base/~',
|
||||
'vs/base/parts/*/~',
|
||||
|
||||
Generated
+22
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,19 @@
|
||||
export interface IMcpConfiguration {
|
||||
inputs: unknown[];
|
||||
/** @deprecated Only for rough cross-compat with other formats */
|
||||
mcpServers?: Record<string, IMcpConfigurationServer>;
|
||||
servers: Record<string, IMcpConfigurationServer>;
|
||||
mcpServers?: Record<string, IMcpConfigurationStdio>;
|
||||
servers: Record<string, IMcpConfigurationStdio | IMcpConfigurationSSE>;
|
||||
}
|
||||
|
||||
export interface IMcpConfigurationServer {
|
||||
export interface IMcpConfigurationStdio {
|
||||
type?: 'stdio';
|
||||
command: string;
|
||||
args?: readonly string[];
|
||||
env?: Record<string, string | number | null>;
|
||||
}
|
||||
|
||||
export interface IMcpConfigurationSSE {
|
||||
type: 'sse';
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
|
||||
@@ -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<Promise<void>>();
|
||||
private readonly _sseEventSources = this._register(new DisposableMap<number, McpSSEHandle>());
|
||||
private readonly _eventSource = new Lazy(async () => {
|
||||
const es = await importAMDNodeModule<typeof ES>('@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<void> {
|
||||
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<string>();
|
||||
constructor(
|
||||
eventSourceCtor: Promise<typeof ES.EventSource>,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<number>('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<boolean>('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<boolean>('mcp.hasServersWithErrors', undefined, { type: 'boolean', description: localize('mcp.hasServersWithErrors.description', "Indicates whether there are any MCP servers with errors.") });
|
||||
export const toolsCount = new RawContextKey<number>('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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string | number | null> };
|
||||
|
||||
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,
|
||||
|
||||
@@ -24,6 +24,8 @@ declare module 'vscode' {
|
||||
|
||||
uri: Uri;
|
||||
|
||||
headers: [string, string][];
|
||||
|
||||
constructor(label: string, uri: Uri);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user