Parcourir la source

fix: Implement singleton pattern for MCP server management to prevent multiple instances

Problem:
- Multiple instances of the Roo Code application were launching separate MCP server instances
- This led to unnecessary resource consumption and potential conflicts between instances

Solution:
1. Created new McpServerManager singleton class to manage MCP server instances:
   - Static getInstance() method ensures only one McpHub instance exists
   - Tracks registered ClineProvider instances
   - Handles cleanup on extension deactivation

2. Modified ClineProvider class:
   - Changed mcpHub from private to protected
   - Added getMcpHub() public getter method
   - Updated initialization to use McpServerManager
   - Added unregister logic in dispose()

3. Updated extension.ts to handle cleanup:
   - Added McpServerManager cleanup in deactivate()

Technical Implementation:
- Uses WeakRef for provider tracking to allow proper garbage collection
- Maintains global state to track instance IDs
- Implements proper cleanup of resources on disposal
- Ensures backward compatibility with existing code

This change significantly improves resource usage and prevents potential conflicts
between multiple instances of the application while maintaining all existing
functionality.
MuriloFP il y a 10 mois
Parent
commit
b2b5e1ffa6

+ 6 - 4
src/core/Cline.ts

@@ -832,7 +832,7 @@ export class Cline {
 		this.lastApiRequestTime = Date.now()
 		this.lastApiRequestTime = Date.now()
 
 
 		if (mcpEnabled ?? true) {
 		if (mcpEnabled ?? true) {
-			mcpHub = this.providerRef.deref()?.mcpHub
+			mcpHub = this.providerRef.deref()?.getMcpHub()
 			if (!mcpHub) {
 			if (!mcpHub) {
 				throw new Error("MCP hub not available")
 				throw new Error("MCP hub not available")
 			}
 			}
@@ -1013,7 +1013,7 @@ export class Cline {
 					// (have to do this for partial and complete since sending content in thinking tags to markdown renderer will automatically be removed)
 					// (have to do this for partial and complete since sending content in thinking tags to markdown renderer will automatically be removed)
 					// Remove end substrings of <thinking or </thinking (below xml parsing is only for opening tags)
 					// Remove end substrings of <thinking or </thinking (below xml parsing is only for opening tags)
 					// (this is done with the xml parsing below now, but keeping here for reference)
 					// (this is done with the xml parsing below now, but keeping here for reference)
-					// content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?$/, "")
+					// content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
 					// Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before)
 					// Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before)
 					// - Needs to be separate since we dont want to remove the line break before the first tag
 					// - Needs to be separate since we dont want to remove the line break before the first tag
 					// - Needs to happen before the xml parsing below
 					// - Needs to happen before the xml parsing below
@@ -2267,7 +2267,8 @@ export class Cline {
 								await this.say("mcp_server_request_started") // same as browser_action_result
 								await this.say("mcp_server_request_started") // same as browser_action_result
 								const toolResult = await this.providerRef
 								const toolResult = await this.providerRef
 									.deref()
 									.deref()
-									?.mcpHub?.callTool(server_name, tool_name, parsedArguments)
+									?.getMcpHub()
+									?.callTool(server_name, tool_name, parsedArguments)
 
 
 								// TODO: add progress indicator and ability to parse images and non-text responses
 								// TODO: add progress indicator and ability to parse images and non-text responses
 								const toolResultPretty =
 								const toolResultPretty =
@@ -2335,7 +2336,8 @@ export class Cline {
 								await this.say("mcp_server_request_started")
 								await this.say("mcp_server_request_started")
 								const resourceResult = await this.providerRef
 								const resourceResult = await this.providerRef
 									.deref()
 									.deref()
-									?.mcpHub?.readResource(server_name, uri)
+									?.getMcpHub()
+									?.readResource(server_name, uri)
 								const resourceResultPretty =
 								const resourceResultPretty =
 									resourceResult?.contents
 									resourceResult?.contents
 										.map((item) => {
 										.map((item) => {

+ 19 - 2
src/core/webview/ClineProvider.ts

@@ -36,6 +36,7 @@ import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, Experime
 import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
 import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
 
 
 import { ACTION_NAMES } from "../CodeActionProvider"
 import { ACTION_NAMES } from "../CodeActionProvider"
+import { McpServerManager } from "../../services/mcp/McpServerManager"
 
 
 /*
 /*
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -136,7 +137,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private isViewLaunched = false
 	private isViewLaunched = false
 	private cline?: Cline
 	private cline?: Cline
 	private workspaceTracker?: WorkspaceTracker
 	private workspaceTracker?: WorkspaceTracker
-	mcpHub?: McpHub
+	protected mcpHub?: McpHub // Change from private to protected
 	private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
 	private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
 	configManager: ConfigManager
 	configManager: ConfigManager
 	customModesManager: CustomModesManager
 	customModesManager: CustomModesManager
@@ -148,11 +149,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		this.outputChannel.appendLine("ClineProvider instantiated")
 		this.outputChannel.appendLine("ClineProvider instantiated")
 		ClineProvider.activeInstances.add(this)
 		ClineProvider.activeInstances.add(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
-		this.mcpHub = new McpHub(this)
 		this.configManager = new ConfigManager(this.context)
 		this.configManager = new ConfigManager(this.context)
 		this.customModesManager = new CustomModesManager(this.context, async () => {
 		this.customModesManager = new CustomModesManager(this.context, async () => {
 			await this.postStateToWebview()
 			await this.postStateToWebview()
 		})
 		})
+
+		// Initialize MCP Hub through the singleton manager
+		McpServerManager.getInstance(this.context, this)
+			.then((hub) => {
+				this.mcpHub = hub
+			})
+			.catch((error) => {
+				this.outputChannel.appendLine(`Failed to initialize MCP Hub: ${error}`)
+			})
 	}
 	}
 
 
 	/*
 	/*
@@ -181,6 +190,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		this.customModesManager?.dispose()
 		this.customModesManager?.dispose()
 		this.outputChannel.appendLine("Disposed all disposables")
 		this.outputChannel.appendLine("Disposed all disposables")
 		ClineProvider.activeInstances.delete(this)
 		ClineProvider.activeInstances.delete(this)
+
+		// Unregister from McpServerManager
+		McpServerManager.unregisterProvider(this)
 	}
 	}
 
 
 	public static getVisibleInstance(): ClineProvider | undefined {
 	public static getVisibleInstance(): ClineProvider | undefined {
@@ -2538,4 +2550,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	get messages() {
 	get messages() {
 		return this.cline?.clineMessages || []
 		return this.cline?.clineMessages || []
 	}
 	}
+
+	// Add public getter
+	public getMcpHub(): McpHub | undefined {
+		return this.mcpHub
+	}
 }
 }

+ 7 - 2
src/extension.ts

@@ -6,6 +6,7 @@ import "./utils/path" // Necessary to have access to String.prototype.toPosix.
 import { CodeActionProvider } from "./core/CodeActionProvider"
 import { CodeActionProvider } from "./core/CodeActionProvider"
 import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
 import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
 import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
 import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
+import { McpServerManager } from "./services/mcp/McpServerManager"
 
 
 /**
 /**
  * Built using https://github.com/microsoft/vscode-webview-ui-toolkit
  * Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -16,10 +17,12 @@ import { handleUri, registerCommands, registerCodeActions, registerTerminalActio
  */
  */
 
 
 let outputChannel: vscode.OutputChannel
 let outputChannel: vscode.OutputChannel
+let extensionContext: vscode.ExtensionContext
 
 
 // This method is called when your extension is activated.
 // This method is called when your extension is activated.
 // Your extension is activated the very first time the command is executed.
 // Your extension is activated the very first time the command is executed.
 export function activate(context: vscode.ExtensionContext) {
 export function activate(context: vscode.ExtensionContext) {
+	extensionContext = context
 	outputChannel = vscode.window.createOutputChannel("Roo-Code")
 	outputChannel = vscode.window.createOutputChannel("Roo-Code")
 	context.subscriptions.push(outputChannel)
 	context.subscriptions.push(outputChannel)
 	outputChannel.appendLine("Roo-Code extension activated")
 	outputChannel.appendLine("Roo-Code extension activated")
@@ -83,7 +86,9 @@ export function activate(context: vscode.ExtensionContext) {
 	return createClineAPI(outputChannel, sidebarProvider)
 	return createClineAPI(outputChannel, sidebarProvider)
 }
 }
 
 
-// This method is called when your extension is deactivated.
-export function deactivate() {
+// This method is called when your extension is deactivated
+export async function deactivate() {
 	outputChannel.appendLine("Roo-Code extension deactivated")
 	outputChannel.appendLine("Roo-Code extension deactivated")
+	// Clean up MCP server manager
+	await McpServerManager.cleanup(extensionContext)
 }
 }

+ 60 - 0
src/services/mcp/McpServerManager.ts

@@ -0,0 +1,60 @@
+import * as vscode from "vscode"
+import { McpHub } from "./McpHub"
+import { ClineProvider } from "../../core/webview/ClineProvider"
+
+/**
+ * Singleton manager for MCP server instances.
+ * Ensures only one set of MCP servers runs across all webviews.
+ */
+export class McpServerManager {
+	private static instance: McpHub | null = null
+	private static readonly GLOBAL_STATE_KEY = "mcpHubInstanceId"
+	private static providers: Set<ClineProvider> = new Set()
+
+	/**
+	 * Get the singleton McpHub instance.
+	 * Creates a new instance if one doesn't exist.
+	 */
+	static async getInstance(context: vscode.ExtensionContext, provider: ClineProvider): Promise<McpHub> {
+		// Register the provider
+		this.providers.add(provider)
+
+		if (!this.instance) {
+			this.instance = new McpHub(provider)
+			// Store a unique identifier in global state to track the primary instance
+			await context.globalState.update(this.GLOBAL_STATE_KEY, Date.now().toString())
+		}
+		return this.instance
+	}
+
+	/**
+	 * Remove a provider from the tracked set.
+	 * This is called when a webview is disposed.
+	 */
+	static unregisterProvider(provider: ClineProvider): void {
+		this.providers.delete(provider)
+	}
+
+	/**
+	 * Notify all registered providers of server state changes.
+	 */
+	static notifyProviders(message: any): void {
+		this.providers.forEach((provider) => {
+			provider.postMessageToWebview(message).catch((error) => {
+				console.error("Failed to notify provider:", error)
+			})
+		})
+	}
+
+	/**
+	 * Clean up the singleton instance and all its resources.
+	 */
+	static async cleanup(context: vscode.ExtensionContext): Promise<void> {
+		if (this.instance) {
+			await this.instance.dispose()
+			this.instance = null
+			await context.globalState.update(this.GLOBAL_STATE_KEY, undefined)
+		}
+		this.providers.clear()
+	}
+}