@@ -152,6 +152,8 @@ export class McpHub {
152152 private refCount : number = 0 // Reference counter for active clients
153153 private configChangeDebounceTimers : Map < string , NodeJS . Timeout > = new Map ( )
154154 private activeToolExecutions : Map < string , Set < string > > = new Map ( ) // Track active tool executions per server
155+ private pendingRestarts : Set < string > = new Set ( ) // Track servers that need restart after tool execution
156+ private toolExecutionTimeouts : Map < string , NodeJS . Timeout > = new Map ( ) // Track timeouts for tool execution cleanup
155157
156158 constructor ( provider : ClineProvider ) {
157159 this . providerRef = new WeakRef ( provider )
@@ -1172,10 +1174,13 @@ export class McpHub {
11721174 async restartConnection ( serverName : string , source ?: "global" | "project" ) : Promise < void > {
11731175 // Check if there are active tool executions for this server
11741176 if ( this . hasActiveToolExecutions ( serverName , source ) ) {
1175- console . log ( `Skipping restart for ${ serverName } - tools are currently executing` )
1176- vscode . window . showWarningMessage (
1177- t ( "mcp:errors.cannot_restart_tools_running" , { serverName } ) ||
1178- `Cannot restart server "${ serverName } " while tools are running. Please wait for tool execution to complete.` ,
1177+ console . log ( `Deferring restart for ${ serverName } - tools are currently executing` )
1178+ // Add to pending restarts
1179+ const serverKey = `${ serverName } :${ source || "global" } `
1180+ this . pendingRestarts . add ( serverKey )
1181+ vscode . window . showInformationMessage (
1182+ t ( "mcp:info.restart_deferred" , { serverName } ) ||
1183+ `Server "${ serverName } " will be restarted after tool execution completes.` ,
11791184 )
11801185 return
11811186 }
@@ -1636,6 +1641,15 @@ export class McpHub {
16361641 const executionId = `${ toolName } :${ Date . now ( ) } `
16371642 this . activeToolExecutions . get ( serverKey ) ! . add ( executionId )
16381643
1644+ // Set up a timeout to clean up the execution tracking in case of hangs
1645+ const cleanupTimeout = setTimeout ( ( ) => {
1646+ this . cleanupToolExecution ( serverKey , executionId )
1647+ } , timeout + 5000 ) // Add 5 seconds buffer after the tool timeout
1648+
1649+ // Store the timeout so we can clear it if the tool completes normally
1650+ const timeoutKey = `${ serverKey } :${ executionId } `
1651+ this . toolExecutionTimeouts . set ( timeoutKey , cleanupTimeout )
1652+
16391653 try {
16401654 const result = await connection . client . request (
16411655 {
@@ -1651,23 +1665,60 @@ export class McpHub {
16511665 } ,
16521666 )
16531667
1668+ // Clear the cleanup timeout since the tool completed successfully
1669+ this . clearToolExecutionTimeout ( timeoutKey )
1670+
16541671 // Remove from active executions on success
1655- this . activeToolExecutions . get ( serverKey ) ?. delete ( executionId )
1656- if ( this . activeToolExecutions . get ( serverKey ) ?. size === 0 ) {
1657- this . activeToolExecutions . delete ( serverKey )
1658- }
1672+ this . cleanupToolExecution ( serverKey , executionId )
16591673
16601674 return result
16611675 } catch ( error ) {
1676+ // Clear the cleanup timeout
1677+ this . clearToolExecutionTimeout ( timeoutKey )
1678+
16621679 // Remove from active executions on error
1663- this . activeToolExecutions . get ( serverKey ) ?. delete ( executionId )
1664- if ( this . activeToolExecutions . get ( serverKey ) ?. size === 0 ) {
1665- this . activeToolExecutions . delete ( serverKey )
1666- }
1680+ this . cleanupToolExecution ( serverKey , executionId )
1681+
16671682 throw error
16681683 }
16691684 }
16701685
1686+ /**
1687+ * Clean up tool execution tracking and process pending restarts if needed
1688+ * @param serverKey The server key
1689+ * @param executionId The execution ID to clean up
1690+ */
1691+ private cleanupToolExecution ( serverKey : string , executionId : string ) : void {
1692+ // Remove from active executions
1693+ this . activeToolExecutions . get ( serverKey ) ?. delete ( executionId )
1694+ if ( this . activeToolExecutions . get ( serverKey ) ?. size === 0 ) {
1695+ this . activeToolExecutions . delete ( serverKey )
1696+
1697+ // Check if this server has pending restarts
1698+ if ( this . pendingRestarts . has ( serverKey ) ) {
1699+ this . pendingRestarts . delete ( serverKey )
1700+ // Extract server name and source from the key
1701+ const [ serverName , source ] = serverKey . split ( ":" )
1702+ // Process the pending restart
1703+ this . restartConnection ( serverName , source as "global" | "project" ) . catch ( ( error ) => {
1704+ console . error ( `Failed to process pending restart for ${ serverName } :` , error )
1705+ } )
1706+ }
1707+ }
1708+ }
1709+
1710+ /**
1711+ * Clear a tool execution timeout
1712+ * @param timeoutKey The timeout key to clear
1713+ */
1714+ private clearToolExecutionTimeout ( timeoutKey : string ) : void {
1715+ const timeout = this . toolExecutionTimeouts . get ( timeoutKey )
1716+ if ( timeout ) {
1717+ clearTimeout ( timeout )
1718+ this . toolExecutionTimeouts . delete ( timeoutKey )
1719+ }
1720+ }
1721+
16711722 /**
16721723 * Check if any tools are currently executing for a specific server
16731724 * @param serverName The name of the server to check
@@ -1708,6 +1759,27 @@ export class McpHub {
17081759 toolName : string ,
17091760 listName : "alwaysAllow" | "disabledTools" ,
17101761 addTool : boolean ,
1762+ ) : Promise < void > {
1763+ // Use the common implementation with restart enabled
1764+ await this . updateServerToolListInternal ( serverName , source , toolName , listName , addTool , true )
1765+ }
1766+
1767+ /**
1768+ * Internal implementation for updating server tool lists
1769+ * @param serverName The name of the server to update
1770+ * @param source Whether to update the global or project config
1771+ * @param toolName The name of the tool to add or remove
1772+ * @param listName The name of the list to modify ("alwaysAllow" or "disabledTools")
1773+ * @param addTool Whether to add (true) or remove (false) the tool from the list
1774+ * @param allowRestart Whether to allow file watchers to trigger restart
1775+ */
1776+ private async updateServerToolListInternal (
1777+ serverName : string ,
1778+ source : "global" | "project" ,
1779+ toolName : string ,
1780+ listName : "alwaysAllow" | "disabledTools" ,
1781+ addTool : boolean ,
1782+ allowRestart : boolean ,
17111783 ) : Promise < void > {
17121784 // Find the connection with matching name and source
17131785 const connection = this . findConnection ( serverName , source )
@@ -1731,7 +1803,6 @@ export class McpHub {
17311803 }
17321804
17331805 // Normalize path for cross-platform compatibility
1734- // Use a consistent path format for both reading and writing
17351806 const normalizedPath = process . platform === "win32" ? configPath . replace ( / \\ / g, "/" ) : configPath
17361807
17371808 // Read the appropriate config file
@@ -1763,12 +1834,57 @@ export class McpHub {
17631834 targetList . splice ( toolIndex , 1 )
17641835 }
17651836
1837+ // Handle file watcher based on allowRestart flag
1838+ let watcherToRestore : vscode . FileSystemWatcher | undefined
1839+ if ( ! allowRestart ) {
1840+ // Temporarily disable the watcher to prevent restart
1841+ if ( source === "project" && this . projectMcpWatcher ) {
1842+ watcherToRestore = this . projectMcpWatcher
1843+ this . projectMcpWatcher . dispose ( )
1844+ this . projectMcpWatcher = undefined
1845+ } else if ( source === "global" && this . settingsWatcher ) {
1846+ watcherToRestore = this . settingsWatcher
1847+ this . settingsWatcher . dispose ( )
1848+ this . settingsWatcher = undefined
1849+ }
1850+ }
1851+
17661852 await fs . writeFile ( normalizedPath , JSON . stringify ( config , null , 2 ) )
17671853
17681854 if ( connection ) {
1769- connection . server . tools = await this . fetchToolsList ( serverName , source )
1855+ if ( allowRestart ) {
1856+ // Normal flow - fetch fresh tool list
1857+ connection . server . tools = await this . fetchToolsList ( serverName , source )
1858+ } else {
1859+ // Update the in-memory tool state without restarting
1860+ const tools = connection . server . tools
1861+ if ( tools ) {
1862+ const tool = tools . find ( ( t ) => t . name === toolName )
1863+ if ( tool ) {
1864+ if ( listName === "alwaysAllow" ) {
1865+ tool . alwaysAllow = addTool
1866+ } else if ( listName === "disabledTools" ) {
1867+ tool . enabledForPrompt = ! addTool
1868+ }
1869+ }
1870+ }
1871+ }
17701872 await this . notifyWebviewOfServerChanges ( )
17711873 }
1874+
1875+ // Re-enable the watcher after a short delay if it was disabled
1876+ if ( ! allowRestart && watcherToRestore ) {
1877+ setTimeout ( async ( ) => {
1878+ // Check if not disposed during the timeout
1879+ if ( ! this . isDisposed ) {
1880+ if ( source === "project" && ! this . projectMcpWatcher ) {
1881+ await this . watchProjectMcpFile ( )
1882+ } else if ( source === "global" && ! this . settingsWatcher ) {
1883+ await this . watchMcpSettingsFile ( )
1884+ }
1885+ }
1886+ } , 1000 )
1887+ }
17721888 }
17731889
17741890 async toggleToolAlwaysAllow (
@@ -1780,7 +1896,7 @@ export class McpHub {
17801896 try {
17811897 // Check if there are active tool executions for this server
17821898 if ( this . hasActiveToolExecutions ( serverName , source ) ) {
1783- console . log ( `Skipping server restart for ${ serverName } - tools are currently executing` )
1899+ console . log ( `Deferring config update for ${ serverName } - tools are currently executing` )
17841900 // Update the config file without triggering a restart
17851901 await this . updateServerToolListWithoutRestart ( serverName , source , toolName , "alwaysAllow" , shouldAllow )
17861902 } else {
@@ -1807,101 +1923,8 @@ export class McpHub {
18071923 listName : "alwaysAllow" | "disabledTools" ,
18081924 addTool : boolean ,
18091925 ) : Promise < void > {
1810- // Find the connection with matching name and source
1811- const connection = this . findConnection ( serverName , source )
1812-
1813- if ( ! connection ) {
1814- throw new Error ( `Server ${ serverName } with source ${ source } not found` )
1815- }
1816-
1817- // Determine the correct config path based on the source
1818- let configPath : string
1819- if ( source === "project" ) {
1820- // Get project MCP config path
1821- const projectMcpPath = await this . getProjectMcpPath ( )
1822- if ( ! projectMcpPath ) {
1823- throw new Error ( "Project MCP configuration file not found" )
1824- }
1825- configPath = projectMcpPath
1826- } else {
1827- // Get global MCP settings path
1828- configPath = await this . getMcpSettingsFilePath ( )
1829- }
1830-
1831- // Normalize path for cross-platform compatibility
1832- const normalizedPath = process . platform === "win32" ? configPath . replace ( / \\ / g, "/" ) : configPath
1833-
1834- // Read the appropriate config file
1835- const content = await fs . readFile ( normalizedPath , "utf-8" )
1836- const config = JSON . parse ( content )
1837-
1838- if ( ! config . mcpServers ) {
1839- config . mcpServers = { }
1840- }
1841-
1842- if ( ! config . mcpServers [ serverName ] ) {
1843- config . mcpServers [ serverName ] = {
1844- type : "stdio" ,
1845- command : "node" ,
1846- args : [ ] , // Default to an empty array; can be set later if needed
1847- }
1848- }
1849-
1850- if ( ! config . mcpServers [ serverName ] [ listName ] ) {
1851- config . mcpServers [ serverName ] [ listName ] = [ ]
1852- }
1853-
1854- const targetList = config . mcpServers [ serverName ] [ listName ]
1855- const toolIndex = targetList . indexOf ( toolName )
1856-
1857- if ( addTool && toolIndex === - 1 ) {
1858- targetList . push ( toolName )
1859- } else if ( ! addTool && toolIndex !== - 1 ) {
1860- targetList . splice ( toolIndex , 1 )
1861- }
1862-
1863- // Write the config file directly without triggering file watcher
1864- // We'll temporarily disable the watcher to prevent restart
1865- const watcherKey = source === "project" ? this . projectMcpWatcher : this . settingsWatcher
1866- const wasWatcherActive = ! ! watcherKey
1867-
1868- if ( wasWatcherActive && source === "project" && this . projectMcpWatcher ) {
1869- this . projectMcpWatcher . dispose ( )
1870- this . projectMcpWatcher = undefined
1871- } else if ( wasWatcherActive && source === "global" && this . settingsWatcher ) {
1872- this . settingsWatcher . dispose ( )
1873- this . settingsWatcher = undefined
1874- }
1875-
1876- await fs . writeFile ( normalizedPath , JSON . stringify ( config , null , 2 ) )
1877-
1878- // Update the in-memory tool state without restarting
1879- if ( connection ) {
1880- // Update the tool's alwaysAllow or enabledForPrompt status in memory
1881- const tools = connection . server . tools
1882- if ( tools ) {
1883- const tool = tools . find ( ( t ) => t . name === toolName )
1884- if ( tool ) {
1885- if ( listName === "alwaysAllow" ) {
1886- tool . alwaysAllow = addTool
1887- } else if ( listName === "disabledTools" ) {
1888- tool . enabledForPrompt = ! addTool
1889- }
1890- }
1891- }
1892- await this . notifyWebviewOfServerChanges ( )
1893- }
1894-
1895- // Re-enable the watcher after a short delay
1896- if ( wasWatcherActive ) {
1897- setTimeout ( async ( ) => {
1898- if ( source === "project" ) {
1899- await this . watchProjectMcpFile ( )
1900- } else {
1901- await this . watchMcpSettingsFile ( )
1902- }
1903- } , 1000 )
1904- }
1926+ // Use the common implementation
1927+ await this . updateServerToolListInternal ( serverName , source , toolName , listName , addTool , false )
19051928 }
19061929
19071930 async toggleToolEnabledForPrompt (
@@ -1917,7 +1940,7 @@ export class McpHub {
19171940
19181941 // Check if there are active tool executions for this server
19191942 if ( this . hasActiveToolExecutions ( serverName , source ) ) {
1920- console . log ( `Skipping server restart for ${ serverName } - tools are currently executing` )
1943+ console . log ( `Deferring config update for ${ serverName } - tools are currently executing` )
19211944 // Update the config file without triggering a restart
19221945 await this . updateServerToolListWithoutRestart (
19231946 serverName ,
@@ -2004,6 +2027,15 @@ export class McpHub {
20042027 }
20052028 this . configChangeDebounceTimers . clear ( )
20062029
2030+ // Clear all tool execution timeouts
2031+ for ( const timeout of this . toolExecutionTimeouts . values ( ) ) {
2032+ clearTimeout ( timeout )
2033+ }
2034+ this . toolExecutionTimeouts . clear ( )
2035+
2036+ // Clear pending restarts
2037+ this . pendingRestarts . clear ( )
2038+
20072039 this . removeAllFileWatchers ( )
20082040 for ( const connection of this . connections ) {
20092041 try {
0 commit comments