Просмотр исходного кода

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 10 месяцев назад
Родитель
Сommit
b2b5e1ffa6
4 измененных файлов с 92 добавлено и 8 удалено
  1. 6 4
      src/core/Cline.ts
  2. 19 2
      src/core/webview/ClineProvider.ts
  3. 7 2
      src/extension.ts
  4. 60 0
      src/services/mcp/McpServerManager.ts

+ 6 - 4
src/core/Cline.ts

@@ -832,7 +832,7 @@ export class Cline {
 		this.lastApiRequestTime = Date.now()
 
 		if (mcpEnabled ?? true) {
-			mcpHub = this.providerRef.deref()?.mcpHub
+			mcpHub = this.providerRef.deref()?.getMcpHub()
 			if (!mcpHub) {
 				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)
 					// 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)
-					// 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)
 					// - 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
@@ -2267,7 +2267,8 @@ export class Cline {
 								await this.say("mcp_server_request_started") // same as browser_action_result
 								const toolResult = await this.providerRef
 									.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
 								const toolResultPretty =
@@ -2335,7 +2336,8 @@ export class Cline {
 								await this.say("mcp_server_request_started")
 								const resourceResult = await this.providerRef
 									.deref()
-									?.mcpHub?.readResource(server_name, uri)
+									?.getMcpHub()
+									?.readResource(server_name, uri)
 								const resourceResultPretty =
 									resourceResult?.contents
 										.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 { 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
@@ -136,7 +137,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private isViewLaunched = false
 	private cline?: Cline
 	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
 	configManager: ConfigManager
 	customModesManager: CustomModesManager
@@ -148,11 +149,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		this.outputChannel.appendLine("ClineProvider instantiated")
 		ClineProvider.activeInstances.add(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
-		this.mcpHub = new McpHub(this)
 		this.configManager = new ConfigManager(this.context)
 		this.customModesManager = new CustomModesManager(this.context, async () => {
 			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.outputChannel.appendLine("Disposed all disposables")
 		ClineProvider.activeInstances.delete(this)
+
+		// Unregister from McpServerManager
+		McpServerManager.unregisterProvider(this)
 	}
 
 	public static getVisibleInstance(): ClineProvider | undefined {
@@ -2538,4 +2550,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	get messages() {
 		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 { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
 import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
+import { McpServerManager } from "./services/mcp/McpServerManager"
 
 /**
  * Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -16,10 +17,12 @@ import { handleUri, registerCommands, registerCodeActions, registerTerminalActio
  */
 
 let outputChannel: vscode.OutputChannel
+let extensionContext: vscode.ExtensionContext
 
 // This method is called when your extension is activated.
 // Your extension is activated the very first time the command is executed.
 export function activate(context: vscode.ExtensionContext) {
+	extensionContext = context
 	outputChannel = vscode.window.createOutputChannel("Roo-Code")
 	context.subscriptions.push(outputChannel)
 	outputChannel.appendLine("Roo-Code extension activated")
@@ -83,7 +86,9 @@ export function activate(context: vscode.ExtensionContext) {
 	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")
+	// 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()
+	}
+}