2025-08-19 21:03:19 +02:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-10-30 11:05:49 -07:00
import type {
Config ,
GeminiCLIExtension ,
MCPServerConfig ,
} from '../config/config.js' ;
2025-08-26 00:04:53 +02:00
import type { ToolRegistry } from './tool-registry.js' ;
2025-08-19 21:03:19 +02:00
import {
McpClient ,
MCPDiscoveryState ,
populateMcpServerCommand ,
} from './mcp-client.js' ;
2025-12-02 20:01:33 -05:00
import { getErrorMessage , isAuthenticationError } from '../utils/errors.js' ;
2025-08-28 21:53:56 +02:00
import type { EventEmitter } from 'node:events' ;
2025-10-27 16:46:35 -07:00
import { coreEvents } from '../utils/events.js' ;
2025-10-30 11:05:49 -07:00
import { debugLogger } from '../utils/debugLogger.js' ;
2025-08-19 21:03:19 +02:00
/**
* Manages the lifecycle of multiple MCP clients, including local child processes.
* This class is responsible for starting, stopping, and discovering tools from
* a collection of MCP servers defined in the configuration.
*/
export class McpClientManager {
private clients : Map < string , McpClient > = new Map ( ) ;
2026-01-22 15:38:06 -08:00
// Track all configured servers (including disabled ones) for UI display
private allServerConfigs : Map < string , MCPServerConfig > = new Map ( ) ;
2026-01-20 22:01:18 +00:00
private readonly clientVersion : string ;
2025-08-19 21:03:19 +02:00
private readonly toolRegistry : ToolRegistry ;
2025-10-30 11:05:49 -07:00
private readonly cliConfig : Config ;
// If we have ongoing MCP client discovery, this completes once that is done.
private discoveryPromise : Promise < void > | undefined ;
2025-08-19 21:03:19 +02:00
private discoveryState : MCPDiscoveryState = MCPDiscoveryState . NOT_STARTED ;
2025-08-28 21:53:56 +02:00
private readonly eventEmitter? : EventEmitter ;
2026-01-20 11:10:21 -05:00
private pendingRefreshPromise : Promise < void > | null = null ;
2025-11-04 07:51:18 -08:00
private readonly blockedMcpServers : Array < {
name : string ;
extensionName : string ;
} > = [ ] ;
2025-08-19 21:03:19 +02:00
2026-02-27 15:04:36 -05:00
/**
* Track whether the user has explicitly interacted with MCP in this session
* (e.g. by running an /mcp command).
*/
private userInteractedWithMcp : boolean = false ;
/**
* Track which MCP diagnostics have already been shown to the user this session
* and at what verbosity level.
*/
private shownDiagnostics : Map < string , 'silent' | 'verbose' > = new Map ( ) ;
/**
* Track whether the MCP "hint" has been shown.
*/
private hintShown : boolean = false ;
/**
* Track the last error message for each server.
*/
private lastErrors : Map < string , string > = new Map ( ) ;
2025-10-30 11:05:49 -07:00
constructor (
2026-01-20 22:01:18 +00:00
clientVersion : string ,
2025-10-30 11:05:49 -07:00
toolRegistry : ToolRegistry ,
cliConfig : Config ,
eventEmitter? : EventEmitter ,
) {
2026-01-20 22:01:18 +00:00
this . clientVersion = clientVersion ;
2025-08-19 21:03:19 +02:00
this . toolRegistry = toolRegistry ;
2025-10-30 11:05:49 -07:00
this . cliConfig = cliConfig ;
2025-08-28 21:53:56 +02:00
this . eventEmitter = eventEmitter ;
2025-11-04 07:51:18 -08:00
}
2026-02-27 15:04:36 -05:00
setUserInteractedWithMcp() {
this . userInteractedWithMcp = true ;
}
getLastError ( serverName : string ) : string | undefined {
return this . lastErrors . get ( serverName ) ;
}
/**
* Emit an MCP diagnostic message, adhering to the user's intent and
* deduplication rules.
*/
emitDiagnostic (
severity : 'info' | 'warning' | 'error' ,
message : string ,
error? : unknown ,
serverName? : string ,
) {
// Capture error for later display if it's an error/warning
if ( severity === 'error' || severity === 'warning' ) {
if ( serverName ) {
this . lastErrors . set ( serverName , message ) ;
}
}
// Deduplicate
const diagnosticKey = ` ${ severity } : ${ message } ` ;
const previousStatus = this . shownDiagnostics . get ( diagnosticKey ) ;
// If user has interacted, show verbosely unless already shown verbosely
if ( this . userInteractedWithMcp ) {
if ( previousStatus === 'verbose' ) {
debugLogger . debug (
` Deduplicated verbose MCP diagnostic: ${ diagnosticKey } ` ,
) ;
return ;
}
this . shownDiagnostics . set ( diagnosticKey , 'verbose' ) ;
coreEvents . emitFeedback ( severity , message , error ) ;
return ;
}
// In silent mode, if it has been shown at all, skip
if ( previousStatus ) {
debugLogger . debug ( ` Deduplicated silent MCP diagnostic: ${ diagnosticKey } ` ) ;
return ;
}
this . shownDiagnostics . set ( diagnosticKey , 'silent' ) ;
// Otherwise, be less annoying
debugLogger . log ( ` [MCP ${ severity } ] ${ message } ` , error ) ;
if ( severity === 'error' || severity === 'warning' ) {
if ( ! this . hintShown ) {
this . hintShown = true ;
coreEvents . emitFeedback (
'info' ,
'MCP issues detected. Run /mcp list for status.' ,
) ;
}
}
}
2025-11-04 07:51:18 -08:00
getBlockedMcpServers() {
return this . blockedMcpServers ;
2025-10-30 11:05:49 -07:00
}
2025-12-09 03:43:12 +01:00
getClient ( serverName : string ) : McpClient | undefined {
return this . clients . get ( serverName ) ;
}
2025-10-30 11:05:49 -07:00
/**
* For all the MCP servers associated with this extension:
*
* - Removes all its MCP servers from the global configuration object.
* - Disconnects all MCP clients from their servers.
* - Updates the Gemini chat configuration to load the new tools.
*/
2025-11-04 07:51:18 -08:00
async stopExtension ( extension : GeminiCLIExtension ) {
2025-10-30 11:05:49 -07:00
debugLogger . log ( ` Unloading extension: ${ extension . name } ` ) ;
await Promise . all (
2026-02-03 11:18:09 -05:00
Object . keys ( extension . mcpServers ? ? { } ) . map ( ( name ) = > {
const config = this . allServerConfigs . get ( name ) ;
2026-02-03 14:29:15 -05:00
if ( config ? . extension ? . id === extension . id ) {
2026-02-03 11:18:09 -05:00
this . allServerConfigs . delete ( name ) ;
// Also remove from blocked servers if present
const index = this . blockedMcpServers . findIndex (
( s ) = > s . name === name && s . extensionName === extension . name ,
) ;
if ( index !== - 1 ) {
this . blockedMcpServers . splice ( index , 1 ) ;
}
return this . disconnectClient ( name , true ) ;
}
return Promise . resolve ( ) ;
} ) ,
2025-10-30 11:05:49 -07:00
) ;
2026-03-04 06:46:17 -08:00
await this . scheduleMcpContextRefresh ( ) ;
2025-10-30 11:05:49 -07:00
}
/**
* For all the MCP servers associated with this extension:
*
* - Adds all its MCP servers to the global configuration object.
* - Connects MCP clients to each server and discovers their tools.
* - Updates the Gemini chat configuration to load the new tools.
*/
2025-11-04 07:51:18 -08:00
async startExtension ( extension : GeminiCLIExtension ) {
2025-10-30 11:05:49 -07:00
debugLogger . log ( ` Loading extension: ${ extension . name } ` ) ;
await Promise . all (
2025-11-04 07:51:18 -08:00
Object . entries ( extension . mcpServers ? ? { } ) . map ( ( [ name , config ] ) = >
this . maybeDiscoverMcpServer ( name , {
. . . config ,
extension ,
} ) ,
) ,
2025-10-30 11:05:49 -07:00
) ;
2026-03-04 06:46:17 -08:00
await this . scheduleMcpContextRefresh ( ) ;
2025-11-04 07:51:18 -08:00
}
2026-01-22 15:38:06 -08:00
/**
* Check if server is blocked by admin settings (allowlist/excludelist).
* Returns true if blocked, false if allowed.
*/
private isBlockedBySettings ( name : string ) : boolean {
2025-11-04 07:51:18 -08:00
const allowedNames = this . cliConfig . getAllowedMcpServers ( ) ;
if (
allowedNames &&
allowedNames . length > 0 &&
2026-01-22 15:38:06 -08:00
! allowedNames . includes ( name )
2025-11-04 07:51:18 -08:00
) {
2026-01-22 15:38:06 -08:00
return true ;
2025-11-04 07:51:18 -08:00
}
const blockedNames = this . cliConfig . getBlockedMcpServers ( ) ;
if (
blockedNames &&
blockedNames . length > 0 &&
2026-01-22 15:38:06 -08:00
blockedNames . includes ( name )
2025-11-04 07:51:18 -08:00
) {
2026-01-22 15:38:06 -08:00
return true ;
2025-11-04 07:51:18 -08:00
}
2026-01-22 15:38:06 -08:00
return false ;
}
/**
* Check if server is disabled by user (session or file-based).
*/
private async isDisabledByUser ( name : string ) : Promise < boolean > {
const callbacks = this . cliConfig . getMcpEnablementCallbacks ( ) ;
if ( callbacks ) {
if ( callbacks . isSessionDisabled ( name ) ) {
return true ;
}
if ( ! ( await callbacks . isFileEnabled ( name ) ) ) {
return true ;
}
}
return false ;
2025-10-30 11:05:49 -07:00
}
2026-01-20 11:10:21 -05:00
private async disconnectClient ( name : string , skipRefresh = false ) {
2025-10-30 11:05:49 -07:00
const existing = this . clients . get ( name ) ;
if ( existing ) {
try {
this . clients . delete ( name ) ;
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
await existing . disconnect ( ) ;
} catch ( error ) {
debugLogger . warn (
` Error stopping client ' ${ name } ': ${ getErrorMessage ( error ) } ` ,
) ;
2025-11-04 07:51:18 -08:00
} finally {
2026-01-20 11:10:21 -05:00
if ( ! skipRefresh ) {
// This is required to update the content generator configuration with the
// new tool configuration and system instructions.
2026-03-04 06:46:17 -08:00
await this . scheduleMcpContextRefresh ( ) ;
2025-11-04 07:51:18 -08:00
}
2025-10-30 11:05:49 -07:00
}
}
}
2026-03-14 22:46:06 -04:00
/**
* Merges two MCP configurations. The second configuration (override)
* takes precedence for scalar properties, but array properties are
* merged securely (exclude = union, include = intersection) and
* environment objects are merged.
*/
private mergeMcpConfigs (
base : MCPServerConfig ,
override : MCPServerConfig ,
) : MCPServerConfig {
// For allowlists (includeTools), use intersection to ensure the most
// restrictive policy wins. A tool must be allowed by BOTH parties.
let includeTools : string [ ] | undefined ;
if ( base . includeTools && override . includeTools ) {
includeTools = base . includeTools . filter ( ( t ) = >
override . includeTools ! . includes ( t ) ,
) ;
// If the intersection is empty, we must keep an empty array to indicate
// that NO tools are allowed (undefined would allow everything).
} else {
// If only one provides an allowlist, use that.
includeTools = override . includeTools ? ? base . includeTools ;
}
// For blocklists (excludeTools), use union so if ANY party blocks it,
// it stays blocked.
const excludeTools = [
. . . new Set ( [
. . . ( base . excludeTools ? ? [ ] ) ,
. . . ( override . excludeTools ? ? [ ] ) ,
] ) ,
] ;
const env = { . . . ( base . env ? ? { } ) , . . . ( override . env ? ? { } ) } ;
return {
. . . base ,
. . . override ,
includeTools ,
excludeTools : excludeTools.length > 0 ? excludeTools : undefined ,
env : Object.keys ( env ) . length > 0 ? env : undefined ,
extension : override.extension ? ? base . extension ,
} ;
}
2026-01-22 15:38:06 -08:00
async maybeDiscoverMcpServer (
2025-10-30 11:05:49 -07:00
name : string ,
config : MCPServerConfig ,
2026-01-22 15:38:06 -08:00
) : Promise < void > {
2026-03-15 14:28:26 -04:00
const existingConfig = this . allServerConfigs . get ( name ) ;
2026-02-24 00:05:31 +05:30
if (
2026-03-14 22:46:06 -04:00
existingConfig ? . extension ? . id &&
config . extension ? . id &&
existingConfig . extension . id !== config . extension . id
2026-02-24 00:05:31 +05:30
) {
const extensionText = config . extension
? ` from extension " ${ config . extension . name } " `
: '' ;
debugLogger . warn (
` Skipping MCP config for server with name " ${ name } " ${ extensionText } as it already exists. ` ,
) ;
return ;
}
2026-03-14 22:46:06 -04:00
let finalConfig = config ;
2026-03-15 14:28:26 -04:00
if ( existingConfig ) {
2026-03-14 22:46:06 -04:00
// If we're merging an extension config into a user config,
// the user config should be the override.
if ( config . extension && ! existingConfig . extension ) {
finalConfig = this . mergeMcpConfigs ( config , existingConfig ) ;
} else {
// Otherwise (User over Extension, or User over User),
// the incoming config is the override.
finalConfig = this . mergeMcpConfigs ( existingConfig , config ) ;
}
}
2026-01-22 15:38:06 -08:00
// Always track server config for UI display
2026-03-14 22:46:06 -04:00
this . allServerConfigs . set ( name , finalConfig ) ;
2026-01-22 15:38:06 -08:00
2026-03-15 14:28:26 -04:00
// Capture the existing client synchronously here before any asynchronous
// operations. This ensures that if multiple discovery turns happen
// concurrently, this turn only replaces/disconnects the client that was
// present when this specific configuration update request began.
const existing = this . clients . get ( name ) ;
// If no connection details are provided, we can't discover this server.
// This often happens when a user provides only overrides (like excludeTools)
// for a server that is actually provided by an extension.
if ( ! finalConfig . command && ! finalConfig . url && ! finalConfig . httpUrl ) {
return ;
}
2026-01-22 15:38:06 -08:00
// Check if blocked by admin settings (allowlist/excludelist)
if ( this . isBlockedBySettings ( name ) ) {
2025-11-04 07:51:18 -08:00
if ( ! this . blockedMcpServers . find ( ( s ) = > s . name === name ) ) {
this . blockedMcpServers ? . push ( {
name ,
2026-03-14 22:46:06 -04:00
extensionName : finalConfig.extension?.name ? ? '' ,
2025-11-04 07:51:18 -08:00
} ) ;
}
return ;
}
2026-01-22 15:38:06 -08:00
// User-disabled servers: disconnect if running, don't start
if ( await this . isDisabledByUser ( name ) ) {
if ( existing ) {
await this . disconnectClient ( name ) ;
}
return ;
}
2025-10-30 11:05:49 -07:00
if ( ! this . cliConfig . isTrustedFolder ( ) ) {
return ;
}
2026-03-14 22:46:06 -04:00
if ( finalConfig . extension && ! finalConfig . extension . isActive ) {
2025-10-30 11:05:49 -07:00
return ;
}
2026-01-03 01:06:56 +09:00
const currentDiscoveryPromise = new Promise < void > ( ( resolve , reject ) = > {
2025-10-30 11:05:49 -07:00
( async ( ) = > {
try {
2025-11-04 07:51:18 -08:00
if ( existing ) {
2026-02-24 23:33:33 +05:30
this . clients . delete ( name ) ;
2025-11-04 07:51:18 -08:00
await existing . disconnect ( ) ;
}
2026-02-24 23:33:33 +05:30
const client = new McpClient (
name ,
2026-03-14 22:46:06 -04:00
finalConfig ,
2026-02-24 23:33:33 +05:30
this . toolRegistry ,
this . cliConfig . getPromptRegistry ( ) ,
this . cliConfig . getResourceRegistry ( ) ,
this . cliConfig . getWorkspaceContext ( ) ,
this . cliConfig ,
this . cliConfig . getDebugMode ( ) ,
this . clientVersion ,
async ( ) = > {
2026-03-04 06:46:17 -08:00
debugLogger . log ( ` 🔔 Refreshing context for server ' ${ name } '... ` ) ;
2026-02-24 23:33:33 +05:30
await this . scheduleMcpContextRefresh ( ) ;
} ,
) ;
this . clients . set ( name , client ) ;
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
2025-10-30 11:05:49 -07:00
try {
await client . connect ( ) ;
await client . discover ( this . cliConfig ) ;
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
} catch ( error ) {
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
2025-12-02 20:01:33 -05:00
// Check if this is a 401/auth error - if so, don't show as red error
// (the info message was already shown in mcp-client.ts)
if ( ! isAuthenticationError ( error ) ) {
// Log the error but don't let a single failed server stop the others
const errorMessage = getErrorMessage ( error ) ;
2026-02-27 15:04:36 -05:00
this . emitDiagnostic (
2025-12-02 20:01:33 -05:00
'error' ,
` Error during discovery for MCP server ' ${ name } ': ${ errorMessage } ` ,
2025-10-30 11:05:49 -07:00
error ,
2025-12-02 20:01:33 -05:00
) ;
}
2025-10-30 11:05:49 -07:00
}
2026-01-03 01:06:56 +09:00
} catch ( error ) {
const errorMessage = getErrorMessage ( error ) ;
2026-02-27 15:04:36 -05:00
this . emitDiagnostic (
2026-01-03 01:06:56 +09:00
'error' ,
` Error initializing MCP server ' ${ name } ': ${ errorMessage } ` ,
error ,
) ;
2025-10-30 11:05:49 -07:00
} finally {
resolve ( ) ;
}
2026-01-03 01:06:56 +09:00
} ) ( ) . catch ( reject ) ;
2025-10-30 11:05:49 -07:00
} ) ;
if ( this . discoveryPromise ) {
2026-01-03 01:06:56 +09:00
// Ensure the next discovery starts regardless of the previous one's success/failure
this . discoveryPromise = this . discoveryPromise
. catch ( ( ) = > { } )
. then ( ( ) = > currentDiscoveryPromise ) ;
2025-10-30 11:05:49 -07:00
} else {
this . discoveryState = MCPDiscoveryState . IN_PROGRESS ;
this . discoveryPromise = currentDiscoveryPromise ;
}
2025-11-04 07:51:18 -08:00
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
2025-10-30 11:05:49 -07:00
const currentPromise = this . discoveryPromise ;
2026-01-03 01:06:56 +09:00
void currentPromise
. finally ( ( ) = > {
// If we are the last recorded discoveryPromise, then we are done, reset
// the world.
if ( currentPromise === this . discoveryPromise ) {
this . discoveryPromise = undefined ;
this . discoveryState = MCPDiscoveryState . COMPLETED ;
2026-01-23 18:32:06 -05:00
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
2026-01-03 01:06:56 +09:00
}
} )
. catch ( ( ) = > { } ) ; // Prevents unhandled rejection from the .finally branch
2025-10-30 11:05:49 -07:00
return currentPromise ;
2025-08-19 21:03:19 +02:00
}
/**
2025-11-04 07:51:18 -08:00
* Initiates the tool discovery process for all configured MCP servers (via
* gemini settings or command line arguments).
*
2025-08-19 21:03:19 +02:00
* It connects to each server, discovers its available tools, and registers
* them with the `ToolRegistry`.
2025-11-04 07:51:18 -08:00
*
* For any server which is already connected, it will first be disconnected.
*
* This does NOT load extension MCP servers - this happens when the
* ExtensionLoader explicitly calls `loadExtension`.
2025-08-19 21:03:19 +02:00
*/
2025-11-04 07:51:18 -08:00
async startConfiguredMcpServers ( ) : Promise < void > {
2025-10-30 11:05:49 -07:00
if ( ! this . cliConfig . isTrustedFolder ( ) ) {
2025-08-28 15:46:27 -07:00
return ;
}
2025-08-28 21:53:56 +02:00
2025-08-19 21:03:19 +02:00
const servers = populateMcpServerCommand (
2025-10-30 11:05:49 -07:00
this . cliConfig . getMcpServers ( ) || { } ,
this . cliConfig . getMcpServerCommand ( ) ,
2025-08-19 21:03:19 +02:00
) ;
2026-01-23 18:32:06 -05:00
if ( Object . keys ( servers ) . length === 0 ) {
this . discoveryState = MCPDiscoveryState . COMPLETED ;
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
return ;
}
2026-01-22 15:38:06 -08:00
// Set state synchronously before any await yields control
if ( ! this . discoveryPromise ) {
this . discoveryState = MCPDiscoveryState . IN_PROGRESS ;
}
2025-09-08 16:37:36 -07:00
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
2025-10-30 11:05:49 -07:00
await Promise . all (
2025-11-04 07:51:18 -08:00
Object . entries ( servers ) . map ( ( [ name , config ] ) = >
this . maybeDiscoverMcpServer ( name , config ) ,
2025-10-30 11:05:49 -07:00
) ,
) ;
2026-02-11 11:30:46 -05:00
// If every configured server was skipped (for example because all are
// disabled by user settings), no discovery promise is created. In that
// case we must still mark discovery complete or the UI will wait forever.
if ( this . discoveryState === MCPDiscoveryState . IN_PROGRESS ) {
this . discoveryState = MCPDiscoveryState . COMPLETED ;
this . eventEmitter ? . emit ( 'mcp-client-update' , this . clients ) ;
}
2026-03-04 06:46:17 -08:00
await this . scheduleMcpContextRefresh ( ) ;
2025-08-19 21:03:19 +02:00
}
2025-11-04 07:51:18 -08:00
/**
2026-01-22 15:38:06 -08:00
* Restarts all MCP servers (including newly enabled ones).
2025-11-04 07:51:18 -08:00
*/
async restart ( ) : Promise < void > {
await Promise . all (
2026-01-22 15:38:06 -08:00
Array . from ( this . allServerConfigs . entries ( ) ) . map (
async ( [ name , config ] ) = > {
try {
await this . maybeDiscoverMcpServer ( name , config ) ;
} catch ( error ) {
debugLogger . error (
` Error restarting client ' ${ name } ': ${ getErrorMessage ( error ) } ` ,
) ;
}
} ,
) ,
2025-11-04 07:51:18 -08:00
) ;
2026-03-04 06:46:17 -08:00
await this . scheduleMcpContextRefresh ( ) ;
2025-11-04 07:51:18 -08:00
}
/**
* Restart a single MCP server by name.
*/
async restartServer ( name : string ) {
2026-01-22 15:38:06 -08:00
const config = this . allServerConfigs . get ( name ) ;
if ( ! config ) {
2025-11-04 07:51:18 -08:00
throw new Error ( ` No MCP server registered with the name " ${ name } " ` ) ;
}
2026-01-22 15:38:06 -08:00
await this . maybeDiscoverMcpServer ( name , config ) ;
2026-03-04 06:46:17 -08:00
await this . scheduleMcpContextRefresh ( ) ;
2025-11-04 07:51:18 -08:00
}
2025-08-19 21:03:19 +02:00
/**
* Stops all running local MCP servers and closes all client connections.
* This is the cleanup method to be called on application exit.
*/
async stop ( ) : Promise < void > {
const disconnectionPromises = Array . from ( this . clients . entries ( ) ) . map (
async ( [ name , client ] ) = > {
try {
await client . disconnect ( ) ;
} catch ( error ) {
2026-02-27 15:04:36 -05:00
this . emitDiagnostic (
2025-10-31 14:52:56 -04:00
'error' ,
` Error stopping client ' ${ name } ': ` ,
error ,
2025-08-19 21:03:19 +02:00
) ;
}
} ,
) ;
await Promise . all ( disconnectionPromises ) ;
this . clients . clear ( ) ;
}
getDiscoveryState ( ) : MCPDiscoveryState {
return this . discoveryState ;
}
2025-11-04 07:51:18 -08:00
/**
2026-01-22 15:38:06 -08:00
* All of the MCP server configurations (including disabled ones).
2025-11-04 07:51:18 -08:00
*/
getMcpServers ( ) : Record < string , MCPServerConfig > {
const mcpServers : Record < string , MCPServerConfig > = { } ;
2026-01-22 15:38:06 -08:00
for ( const [ name , config ] of this . allServerConfigs . entries ( ) ) {
mcpServers [ name ] = config ;
2025-11-04 07:51:18 -08:00
}
return mcpServers ;
}
2025-11-26 13:08:47 -05:00
getMcpInstructions ( ) : string {
const instructions : string [ ] = [ ] ;
for ( const [ name , client ] of this . clients ) {
2025-12-01 11:17:54 -06:00
const clientInstructions = client . getInstructions ( ) ;
if ( clientInstructions ) {
instructions . push (
2026-01-20 11:10:21 -05:00
` The following are instructions provided by the tool server ' ${ name } ': \ n---[start of server instructions]--- \ n ${ clientInstructions } \ n---[end of server instructions]--- ` ,
2025-12-01 11:17:54 -06:00
) ;
2025-11-26 13:08:47 -05:00
}
}
return instructions . join ( '\n\n' ) ;
}
2026-01-15 15:33:16 -05:00
2026-03-04 06:46:17 -08:00
private isRefreshingMcpContext : boolean = false ;
private pendingMcpContextRefresh : boolean = false ;
2026-01-20 11:10:21 -05:00
private async scheduleMcpContextRefresh ( ) : Promise < void > {
2026-03-04 06:46:17 -08:00
this . pendingMcpContextRefresh = true ;
if ( this . isRefreshingMcpContext ) {
debugLogger . log (
'MCP context refresh already in progress, queuing trailing execution.' ,
) ;
return this . pendingRefreshPromise ? ? Promise . resolve ( ) ;
}
2026-01-20 11:10:21 -05:00
if ( this . pendingRefreshPromise ) {
2026-03-04 06:46:17 -08:00
debugLogger . log (
'MCP context refresh already scheduled, coalescing with existing request.' ,
) ;
2026-01-20 11:10:21 -05:00
return this . pendingRefreshPromise ;
}
2026-03-04 06:46:17 -08:00
debugLogger . log ( 'Scheduling MCP context refresh...' ) ;
2026-01-20 11:10:21 -05:00
this . pendingRefreshPromise = ( async ( ) = > {
2026-03-04 06:46:17 -08:00
this . isRefreshingMcpContext = true ;
2026-01-20 11:10:21 -05:00
try {
2026-03-04 06:46:17 -08:00
do {
this . pendingMcpContextRefresh = false ;
debugLogger . log ( 'Executing MCP context refresh...' ) ;
await this . cliConfig . refreshMcpContext ( ) ;
debugLogger . log ( 'MCP context refresh complete.' ) ;
// If more refresh requests came in during the execution, wait a bit
// to coalesce them before the next iteration.
if ( this . pendingMcpContextRefresh ) {
debugLogger . log (
'Coalescing burst refresh requests (300ms delay)...' ,
) ;
await new Promise ( ( resolve ) = > setTimeout ( resolve , 300 ) ) ;
}
} while ( this . pendingMcpContextRefresh ) ;
2026-01-20 11:10:21 -05:00
} catch ( error ) {
debugLogger . error (
` Error refreshing MCP context: ${ getErrorMessage ( error ) } ` ,
) ;
} finally {
2026-03-04 06:46:17 -08:00
this . isRefreshingMcpContext = false ;
2026-01-20 11:10:21 -05:00
this . pendingRefreshPromise = null ;
}
} ) ( ) ;
return this . pendingRefreshPromise ;
}
2026-01-15 15:33:16 -05:00
getMcpServerCount ( ) : number {
return this . clients . size ;
}
2025-08-19 21:03:19 +02:00
}