src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts
331 LOC · 242 covered · 89 uncovered · 55 ranges · 17 concepts · 14 introducers · 13 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.
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.
/*---------------------------------------------------------------------------------------------
mcpGatewayToolBrokerChannel.ts ×13
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { autorun } from '../../../../base/common/observable.js';
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IMcpGatewayServerDescriptor } from '../../../../platform/mcp/common/mcpGateway.js';
import { MCP } from '../../../../platform/mcp/common/modelContextProtocol.js';
import { URI } from '../../../../base/common/uri.js';
import { McpServer } from './mcpServer.js';
import { IMcpServer, IMcpService, McpCapability, McpServerCacheState, McpToolVisibility } from './mcpTypes.js';
import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js';
interface ICallToolForServerArgs {
serverId: string;
name: string;
args: Record<string, unknown>;
chatSessionResource?: string;
}
interface IReadResourceForServerArgs {
serverId: string;
uri: string;
}
interface IServerIdArg {
serverId: string;
}
export class McpGatewayToolBrokerChannel extends Disposable implements IServerChannel<unknown> {
private readonly _onDidChangeTools = this._register(new Emitter<void>());
private readonly _onDidChangeResources = this._register(new Emitter<void>());
private readonly _onDidChangeServers = this._register(new Emitter<readonly IMcpGatewayServerDescriptor[]>());
/**
* Per-server promise that races server startup against the grace period timeout.
* Once set for a server, subsequent list calls await the already-resolved promise
* and return immediately instead of waiting again.
*
* The `resolved` flag tracks whether the promise has settled. If a server's
* cacheState regresses to Unknown/Outdated after the promise resolved (e.g.
* after a cache reset), `_waitForStartup` discards the stale entry and creates
* a fresh race so the server gets another chance to start.
*/
private readonly _startupGrace = new Map<string, { promise: Promise<boolean>; resolved: boolean }>();
constructor(
private readonly _mcpService: IMcpService,
private readonly _logService: ILogService,
private readonly _startupGracePeriodMs = 5000,
) {
super();
this._logService.debug('[McpGateway][ToolBroker] Initialized');
let toolsInitialized = false;
this._register(autorun(reader => {
for (const server of this._mcpService.servers.read(reader)) {
server.tools.read(reader);
}
if (toolsInitialized) {
this._logService.debug('[McpGateway][ToolBroker] Tools changed, firing onDidChangeTools');
this._onDidChangeTools.fire();
} else {
toolsInitialized = true;
}
}));
let resourcesInitialized = false;
this._register(autorun(reader => {
for (const server of this._mcpService.servers.read(reader)) {
server.capabilities.read(reader);
}
if (resourcesInitialized) {
this._logService.debug('[McpGateway][ToolBroker] Resources changed, firing onDidChangeResources');
this._onDidChangeResources.fire();
} else {
resourcesInitialized = true;
}
}));
let serversInitialized = false;
this._register(autorun(reader => {
const servers = this._mcpService.servers.read(reader);
if (serversInitialized) {
this._logService.debug('[McpGateway][ToolBroker] Servers changed, firing onDidChangeServers');
this._onDidChangeServers.fire(servers.map(s => ({ id: s.definition.id, label: s.definition.label })));
} else {
serversInitialized = true;
}
}));
}
private _getServerById(serverId: string): IMcpServer | undefined {
if (server.definition.id === serverId) {
return server;
}
}
return undefined;
private _waitForStartup(server: IMcpServer): Promise<boolean> {
const existing = this._startupGrace.get(id);
// If the previous grace promise already resolved but the server is still
// Unknown/Outdated, the entry is stale (e.g. caches were reset). Discard
// it so we create a fresh race below.
if (existing?.resolved) {
if (state === McpServerCacheState.Unknown || state === McpServerCacheState.Outdated) {
this._startupGrace.delete(id);
}
}
const entry: { promise: Promise<boolean>; resolved: boolean } = {
promise: Promise.race([
this._ensureServerReady(server),
new Promise<boolean>(resolve => setTimeout(() => resolve(false), this._startupGracePeriodMs)),
]),
resolved: false,
};
entry.promise.then(() => { entry.resolved = true; });
this._startupGrace.set(id, entry);
}
return this._startupGrace.get(id)!.promise;
}
private async _shouldUseCachedData(server: IMcpServer): Promise<boolean> {
if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) {
const newState = server.cacheState.get();
return newState === McpServerCacheState.Live
|| newState === McpServerCacheState.RefreshingFromCached;
|| cacheState === McpServerCacheState.Cached
|| cacheState === McpServerCacheState.RefreshingFromCached;
listen<T>(_ctx: unknown, event: string): Event<T> {
case 'onDidChangeTools':
return this._onDidChangeResources.event as Event<T>;
throw new Error(`Invalid listen: ${event}`);
async call<T>(_ctx: unknown, command: string, arg?: unknown, cancellationToken?: CancellationToken): Promise<T> {
this._logService.debug(`[McpGateway][ToolBroker] IPC call: ${command}`);
mcpGatewayToolBrokerChannel.ts ×8
switch (command) {
case 'listServers': {
return servers as T;
}
const tools = await this._listToolsForServer(serverId);
return tools as T;
}
const { serverId, name, args, chatSessionResource } = arg as ICallToolForServerArgs;
mcpGatewayToolBrokerChannel.ts ×4
const result = await this._callToolForServer(serverId, name, args || {}, chatSessionResource, cancellationToken);
return result as T;
}
const { serverId } = arg as IServerIdArg;
const resources = await this._listResourcesForServer(serverId);
return resources as T;
}
const { serverId, uri } = arg as IReadResourceForServerArgs;
const result = await this._readResourceForServer(serverId, uri, cancellationToken);
return result as T;
}
const { serverId } = arg as IServerIdArg;
const templates = await this._listResourceTemplatesForServer(serverId);
return templates as T;
}
throw new Error(`Invalid call: ${command}`);
private _listServers(): readonly IMcpGatewayServerDescriptor[] {
const result: IMcpGatewayServerDescriptor[] = [];
for (const server of servers) {
result.push({ id: server.definition.id, label: server.definition.label });
}
this._logService.debug(`[McpGateway][ToolBroker] listServers result: ${result.length} server(s): [${result.map(s => s.label).join(', ')}]`);
return result;
}
private async _listToolsForServer(serverId: string): Promise<readonly MCP.Tool[]> {
if (!server) {
this._logService.warn(`[McpGateway][ToolBroker] listToolsForServer: unknown server '${serverId}'`);
return [];
}
this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' not ready, skipping tool listing`);
mcpGatewayToolBrokerChannel.ts ×3
return [];
}
.filter(t => t.visibility & McpToolVisibility.Model)
.map(t => t.definition);
this._logService.debug(`[McpGateway][ToolBroker] listToolsForServer '${serverId}': ${tools.length} tool(s)`);
return tools;
private async _callToolForServer(serverId: string, name: string, args: Record<string, unknown>, chatSessionResource?: string, token: CancellationToken = CancellationToken.None): Promise<MCP.CallToolResult> {
this._logService.debug(`[McpGateway][ToolBroker] callToolForServer '${serverId}' tool '${name}' with args: ${JSON.stringify(args)}`);
mcpGatewayToolBrokerChannel.ts ×4
const server = this._getServerById(serverId);
if (!server) {
throw new Error(`Unknown server: ${serverId}`);
}
const tool = server.tools.get().find(t =>
t.definition.name === name && (t.visibility & McpToolVisibility.Model)
);
if (!tool) {
throw new Error(`Unknown tool '${name}' on server '${serverId}'`);
}
const context = chatSessionResource ? { chatSessionResource: URI.parse(chatSessionResource) } : undefined;
const result = await tool.call(args, context, token);
this._logService.debug(`[McpGateway][ToolBroker] Tool '${name}' on '${serverId}' completed (isError=${result.isError ?? false}, content blocks=${result.content.length})`);
return result;
}
private async _listResourcesForServer(serverId: string): Promise<readonly MCP.Resource[]> {
const server = this._getServerById(serverId);
if (!server) {
this._logService.warn(`[McpGateway][ToolBroker] listResourcesForServer: unknown server '${serverId}'`);
return [];
}
if (!await this._shouldUseCachedData(server)) {
return [];
}
const capabilities = server.capabilities.get();
if (!capabilities || !(capabilities & McpCapability.Resources)) {
this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' has no resource capability`);
return [];
}
try {
const resources = await McpServer.callOn(server, h => h.listResources());
this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' listed ${resources.length} resource(s)`);
return resources;
} catch (error) {
this._logService.warn(`[McpGateway][ToolBroker] Server '${serverId}' failed to list resources`, error);
return [];
}
}
private async _readResourceForServer(serverId: string, uri: string, token: CancellationToken = CancellationToken.None): Promise<MCP.ReadResourceResult> {
const server = this._getServerById(serverId);
if (!server) {
throw new Error(`Unknown server: ${serverId}`);
}
this._logService.debug(`[McpGateway][ToolBroker] readResourceForServer '${uri}' from server '${serverId}'`);
const result = await McpServer.callOn(server, h => h.readResource({ uri }, token), token);
this._logService.debug(`[McpGateway][ToolBroker] readResourceForServer returned ${result.contents.length} content(s)`);
return result;
}
private async _listResourceTemplatesForServer(serverId: string): Promise<readonly MCP.ResourceTemplate[]> {
const server = this._getServerById(serverId);
if (!server) {
this._logService.warn(`[McpGateway][ToolBroker] listResourceTemplatesForServer: unknown server '${serverId}'`);
return [];
}
if (!await this._shouldUseCachedData(server)) {
return [];
}
const capabilities = server.capabilities.get();
if (!capabilities || !(capabilities & McpCapability.Resources)) {
return [];
}
try {
const resourceTemplates = await McpServer.callOn(server, h => h.listResourceTemplates());
this._logService.debug(`[McpGateway][ToolBroker] Server '${serverId}' listed ${resourceTemplates.length} resource template(s)`);
return resourceTemplates;
} catch (error) {
this._logService.warn(`[McpGateway][ToolBroker] Server '${serverId}' failed to list resource templates`, error);
return [];
}
}
private async _ensureServerReady(server: IMcpServer): Promise<boolean> {
if (cacheState !== McpServerCacheState.Unknown && cacheState !== McpServerCacheState.Outdated) {
return true;
}
this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' not ready (cacheState=${cacheState}), starting...`);
try {
const ready = await startServerAndWaitForLiveTools(server, {
promptType: 'all-untrusted',
errorOnUserInteraction: true,
});
this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' ready=${ready}`);
mcpTypesUtils.ts ×4
return ready;
} catch (error) {
this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to start`, error);
return false;
}