mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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.
This commit is contained in:
parent
c641c59160
commit
b2b5e1ffa6
4 changed files with 92 additions and 8 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
src/services/mcp/McpServerManager.ts
Normal file
60
src/services/mcp/McpServerManager.ts
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue