Skip to content

Commit 324c5c8

Browse files
authored
Merge pull request #852 from MuriloFP/mcp-server-fixes
MCP Server Bug Fixes Issues #833 and #834
2 parents da2fe80 + c9be470 commit 324c5c8

File tree

4 files changed

+125
-8
lines changed

4 files changed

+125
-8
lines changed

src/core/Cline.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,7 @@ export class Cline {
832832
this.lastApiRequestTime = Date.now()
833833

834834
if (mcpEnabled ?? true) {
835-
mcpHub = this.providerRef.deref()?.mcpHub
835+
mcpHub = this.providerRef.deref()?.getMcpHub()
836836
if (!mcpHub) {
837837
throw new Error("MCP hub not available")
838838
}
@@ -1013,7 +1013,7 @@ export class Cline {
10131013
// (have to do this for partial and complete since sending content in thinking tags to markdown renderer will automatically be removed)
10141014
// Remove end substrings of <thinking or </thinking (below xml parsing is only for opening tags)
10151015
// (this is done with the xml parsing below now, but keeping here for reference)
1016-
// content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?$/, "")
1016+
// content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
10171017
// Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before)
10181018
// - Needs to be separate since we dont want to remove the line break before the first tag
10191019
// - Needs to happen before the xml parsing below
@@ -2267,7 +2267,8 @@ export class Cline {
22672267
await this.say("mcp_server_request_started") // same as browser_action_result
22682268
const toolResult = await this.providerRef
22692269
.deref()
2270-
?.mcpHub?.callTool(server_name, tool_name, parsedArguments)
2270+
?.getMcpHub()
2271+
?.callTool(server_name, tool_name, parsedArguments)
22712272

22722273
// TODO: add progress indicator and ability to parse images and non-text responses
22732274
const toolResultPretty =
@@ -2335,7 +2336,8 @@ export class Cline {
23352336
await this.say("mcp_server_request_started")
23362337
const resourceResult = await this.providerRef
23372338
.deref()
2338-
?.mcpHub?.readResource(server_name, uri)
2339+
?.getMcpHub()
2340+
?.readResource(server_name, uri)
23392341
const resourceResultPretty =
23402342
resourceResult?.contents
23412343
.map((item) => {

src/core/webview/ClineProvider.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, Experime
3636
import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
3737

3838
import { ACTION_NAMES } from "../CodeActionProvider"
39+
import { McpServerManager } from "../../services/mcp/McpServerManager"
3940

4041
/*
4142
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 {
136137
private isViewLaunched = false
137138
private cline?: Cline
138139
private workspaceTracker?: WorkspaceTracker
139-
mcpHub?: McpHub
140+
protected mcpHub?: McpHub // Change from private to protected
140141
private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
141142
configManager: ConfigManager
142143
customModesManager: CustomModesManager
@@ -148,11 +149,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
148149
this.outputChannel.appendLine("ClineProvider instantiated")
149150
ClineProvider.activeInstances.add(this)
150151
this.workspaceTracker = new WorkspaceTracker(this)
151-
this.mcpHub = new McpHub(this)
152152
this.configManager = new ConfigManager(this.context)
153153
this.customModesManager = new CustomModesManager(this.context, async () => {
154154
await this.postStateToWebview()
155155
})
156+
157+
// Initialize MCP Hub through the singleton manager
158+
McpServerManager.getInstance(this.context, this)
159+
.then((hub) => {
160+
this.mcpHub = hub
161+
})
162+
.catch((error) => {
163+
this.outputChannel.appendLine(`Failed to initialize MCP Hub: ${error}`)
164+
})
156165
}
157166

158167
/*
@@ -181,6 +190,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
181190
this.customModesManager?.dispose()
182191
this.outputChannel.appendLine("Disposed all disposables")
183192
ClineProvider.activeInstances.delete(this)
193+
194+
// Unregister from McpServerManager
195+
McpServerManager.unregisterProvider(this)
184196
}
185197

186198
public static getVisibleInstance(): ClineProvider | undefined {
@@ -601,6 +613,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
601613
this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
602614
}
603615
})
616+
617+
// If MCP Hub is already initialized, update the webview with current server list
618+
if (this.mcpHub) {
619+
this.postMessageToWebview({
620+
type: "mcpServers",
621+
mcpServers: this.mcpHub.getServers(),
622+
})
623+
}
624+
604625
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
605626
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
606627
// (see normalizeApiConfiguration > openrouter)
@@ -2103,6 +2124,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21032124
autoApprovalEnabled: autoApprovalEnabled ?? false,
21042125
customModes: await this.customModesManager.getCustomModes(),
21052126
experiments: experiments ?? experimentDefault,
2127+
mcpServers: this.mcpHub?.getServers() ?? [],
21062128
}
21072129
}
21082130

@@ -2538,4 +2560,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
25382560
get messages() {
25392561
return this.cline?.clineMessages || []
25402562
}
2563+
2564+
// Add public getter
2565+
public getMcpHub(): McpHub | undefined {
2566+
return this.mcpHub
2567+
}
25412568
}

src/extension.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "./utils/path" // Necessary to have access to String.prototype.toPosix.
66
import { CodeActionProvider } from "./core/CodeActionProvider"
77
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
88
import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
9+
import { McpServerManager } from "./services/mcp/McpServerManager"
910

1011
/**
1112
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -16,10 +17,12 @@ import { handleUri, registerCommands, registerCodeActions, registerTerminalActio
1617
*/
1718

1819
let outputChannel: vscode.OutputChannel
20+
let extensionContext: vscode.ExtensionContext
1921

2022
// This method is called when your extension is activated.
2123
// Your extension is activated the very first time the command is executed.
2224
export function activate(context: vscode.ExtensionContext) {
25+
extensionContext = context
2326
outputChannel = vscode.window.createOutputChannel("Roo-Code")
2427
context.subscriptions.push(outputChannel)
2528
outputChannel.appendLine("Roo-Code extension activated")
@@ -83,7 +86,9 @@ export function activate(context: vscode.ExtensionContext) {
8386
return createClineAPI(outputChannel, sidebarProvider)
8487
}
8588

86-
// This method is called when your extension is deactivated.
87-
export function deactivate() {
89+
// This method is called when your extension is deactivated
90+
export async function deactivate() {
8891
outputChannel.appendLine("Roo-Code extension deactivated")
92+
// Clean up MCP server manager
93+
await McpServerManager.cleanup(extensionContext)
8994
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as vscode from "vscode"
2+
import { McpHub } from "./McpHub"
3+
import { ClineProvider } from "../../core/webview/ClineProvider"
4+
5+
/**
6+
* Singleton manager for MCP server instances.
7+
* Ensures only one set of MCP servers runs across all webviews.
8+
*/
9+
export class McpServerManager {
10+
private static instance: McpHub | null = null
11+
private static readonly GLOBAL_STATE_KEY = "mcpHubInstanceId"
12+
private static providers: Set<ClineProvider> = new Set()
13+
private static initializationPromise: Promise<McpHub> | null = null
14+
15+
/**
16+
* Get the singleton McpHub instance.
17+
* Creates a new instance if one doesn't exist.
18+
* Thread-safe implementation using a promise-based lock.
19+
*/
20+
static async getInstance(context: vscode.ExtensionContext, provider: ClineProvider): Promise<McpHub> {
21+
// Register the provider
22+
this.providers.add(provider)
23+
24+
// If we already have an instance, return it
25+
if (this.instance) {
26+
return this.instance
27+
}
28+
29+
// If initialization is in progress, wait for it
30+
if (this.initializationPromise) {
31+
return this.initializationPromise
32+
}
33+
34+
// Create a new initialization promise
35+
this.initializationPromise = (async () => {
36+
try {
37+
// Double-check instance in case it was created while we were waiting
38+
if (!this.instance) {
39+
this.instance = new McpHub(provider)
40+
// Store a unique identifier in global state to track the primary instance
41+
await context.globalState.update(this.GLOBAL_STATE_KEY, Date.now().toString())
42+
}
43+
return this.instance
44+
} finally {
45+
// Clear the initialization promise after completion or error
46+
this.initializationPromise = null
47+
}
48+
})()
49+
50+
return this.initializationPromise
51+
}
52+
53+
/**
54+
* Remove a provider from the tracked set.
55+
* This is called when a webview is disposed.
56+
*/
57+
static unregisterProvider(provider: ClineProvider): void {
58+
this.providers.delete(provider)
59+
}
60+
61+
/**
62+
* Notify all registered providers of server state changes.
63+
*/
64+
static notifyProviders(message: any): void {
65+
this.providers.forEach((provider) => {
66+
provider.postMessageToWebview(message).catch((error) => {
67+
console.error("Failed to notify provider:", error)
68+
})
69+
})
70+
}
71+
72+
/**
73+
* Clean up the singleton instance and all its resources.
74+
*/
75+
static async cleanup(context: vscode.ExtensionContext): Promise<void> {
76+
if (this.instance) {
77+
await this.instance.dispose()
78+
this.instance = null
79+
await context.globalState.update(this.GLOBAL_STATE_KEY, undefined)
80+
}
81+
this.providers.clear()
82+
}
83+
}

0 commit comments

Comments
 (0)