diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 2c36dd5495..0f2e796132 100644
--- a/src/core/Cline.ts
+++ b/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 (with optional line break after) and (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) => {
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 9326c83350..7b4d4b2047 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/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
+ }
}
diff --git a/src/extension.ts b/src/extension.ts
index 8ca7f3312c..a05afa4651 100644
--- a/src/extension.ts
+++ b/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)
}
diff --git a/src/services/mcp/McpServerManager.ts b/src/services/mcp/McpServerManager.ts
new file mode 100644
index 0000000000..20ed2b8322
--- /dev/null
+++ b/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 = 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 {
+ // 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 {
+ if (this.instance) {
+ await this.instance.dispose()
+ this.instance = null
+ await context.globalState.update(this.GLOBAL_STATE_KEY, undefined)
+ }
+ this.providers.clear()
+ }
+}