Skip to content

Commit c2cc964

Browse files
committed
feat: add programmatic MCP server refresh capability
- Add refreshMcpServers command to VS Code command palette - Expose MCP refresh functionality through API for extensions - Add McpApi interface to @roo-code/types package - Initialize global MCP API instance on extension activation - Add tests for new functionality Fixes #8356
1 parent a57528d commit c2cc964

File tree

9 files changed

+324
-140
lines changed

9 files changed

+324
-140
lines changed

.tmp/review/Roo-Code

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 8dbd8c4b1b72fb48be3990a8e78285a787a1828c

.work/reviews/Roo-Code

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit ea8420be8c5386d867fe6aa7b1f9756a44a3b5b1

packages/types/src/api.ts

Lines changed: 29 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,35 @@
1-
import type { EventEmitter } from "events"
2-
import type { Socket } from "net"
1+
/**
2+
* API for programmatic MCP server operations
3+
*/
34

4-
import type { RooCodeEvents } from "./events.js"
5-
import type { RooCodeSettings } from "./global-settings.js"
6-
import type { ProviderSettingsEntry, ProviderSettings } from "./provider-settings.js"
7-
import type { IpcMessage, IpcServerEvents } from "./ipc.js"
8-
9-
export type RooCodeAPIEvents = RooCodeEvents
10-
11-
export interface RooCodeAPI extends EventEmitter<RooCodeAPIEvents> {
12-
/**
13-
* Starts a new task with an optional initial message and images.
14-
* @param task Optional initial task message.
15-
* @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
16-
* @returns The ID of the new task.
17-
*/
18-
startNewTask({
19-
configuration,
20-
text,
21-
images,
22-
newTab,
23-
}: {
24-
configuration?: RooCodeSettings
25-
text?: string
26-
images?: string[]
27-
newTab?: boolean
28-
}): Promise<string>
29-
/**
30-
* Resumes a task with the given ID.
31-
* @param taskId The ID of the task to resume.
32-
* @throws Error if the task is not found in the task history.
33-
*/
34-
resumeTask(taskId: string): Promise<void>
35-
/**
36-
* Checks if a task with the given ID is in the task history.
37-
* @param taskId The ID of the task to check.
38-
* @returns True if the task is in the task history, false otherwise.
39-
*/
40-
isTaskInHistory(taskId: string): Promise<boolean>
41-
/**
42-
* Returns the current task stack.
43-
* @returns An array of task IDs.
44-
*/
45-
getCurrentTaskStack(): string[]
46-
/**
47-
* Clears the current task.
48-
*/
49-
clearCurrentTask(lastMessage?: string): Promise<void>
50-
/**
51-
* Cancels the current task.
52-
*/
53-
cancelCurrentTask(): Promise<void>
54-
/**
55-
* Sends a message to the current task.
56-
* @param message Optional message to send.
57-
* @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
58-
*/
59-
sendMessage(message?: string, images?: string[]): Promise<void>
60-
/**
61-
* Simulates pressing the primary button in the chat interface.
62-
*/
63-
pressPrimaryButton(): Promise<void>
64-
/**
65-
* Simulates pressing the secondary button in the chat interface.
66-
*/
67-
pressSecondaryButton(): Promise<void>
68-
/**
69-
* Returns true if the API is ready to use.
70-
*/
71-
isReady(): boolean
5+
/**
6+
* Interface for MCP operations that can be accessed programmatically
7+
*/
8+
export interface McpApi {
729
/**
73-
* Returns the current configuration.
74-
* @returns The current configuration.
10+
* Refresh all MCP server connections
11+
* @returns Promise that resolves when refresh is complete
7512
*/
76-
getConfiguration(): RooCodeSettings
77-
/**
78-
* Sets the configuration for the current task.
79-
* @param values An object containing key-value pairs to set.
80-
*/
81-
setConfiguration(values: RooCodeSettings): Promise<void>
82-
/**
83-
* Returns a list of all configured profile names
84-
* @returns Array of profile names
85-
*/
86-
getProfiles(): string[]
87-
/**
88-
* Returns the profile entry for a given name
89-
* @param name The name of the profile
90-
* @returns The profile entry, or undefined if the profile does not exist
91-
*/
92-
getProfileEntry(name: string): ProviderSettingsEntry | undefined
93-
/**
94-
* Creates a new API configuration profile
95-
* @param name The name of the profile
96-
* @param profile The profile to create; defaults to an empty object
97-
* @param activate Whether to activate the profile after creation; defaults to true
98-
* @returns The ID of the created profile
99-
* @throws Error if the profile already exists
100-
*/
101-
createProfile(name: string, profile?: ProviderSettings, activate?: boolean): Promise<string>
102-
/**
103-
* Updates an existing API configuration profile
104-
* @param name The name of the profile
105-
* @param profile The profile to update
106-
* @param activate Whether to activate the profile after update; defaults to true
107-
* @returns The ID of the updated profile
108-
* @throws Error if the profile does not exist
109-
*/
110-
updateProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
111-
/**
112-
* Creates a new API configuration profile or updates an existing one
113-
* @param name The name of the profile
114-
* @param profile The profile to create or update; defaults to an empty object
115-
* @param activate Whether to activate the profile after upsert; defaults to true
116-
* @returns The ID of the upserted profile
117-
*/
118-
upsertProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
119-
/**
120-
* Deletes a profile by name
121-
* @param name The name of the profile to delete
122-
* @throws Error if the profile does not exist
123-
*/
124-
deleteProfile(name: string): Promise<void>
125-
/**
126-
* Returns the name of the currently active profile
127-
* @returns The profile name, or undefined if no profile is active
128-
*/
129-
getActiveProfile(): string | undefined
130-
/**
131-
* Changes the active API configuration profile
132-
* @param name The name of the profile to activate
133-
* @throws Error if the profile does not exist
134-
*/
135-
setActiveProfile(name: string): Promise<string | undefined>
13+
refreshMcpServers(): Promise<void>
14+
}
15+
16+
/**
17+
* Global MCP API instance that will be set by the extension
18+
*/
19+
export let mcpApi: McpApi | undefined
20+
21+
/**
22+
* Set the global MCP API instance
23+
* @param api The MCP API implementation
24+
*/
25+
export function setMcpApi(api: McpApi): void {
26+
mcpApi = api
13627
}
13728

138-
export interface RooCodeIpcServer extends EventEmitter<IpcServerEvents> {
139-
listen(): void
140-
broadcast(message: IpcMessage): void
141-
send(client: string | Socket, message: IpcMessage): void
142-
get socketPath(): string
143-
get isListening(): boolean
29+
/**
30+
* Get the global MCP API instance
31+
* @returns The MCP API instance or undefined if not set
32+
*/
33+
export function getMcpApi(): McpApi | undefined {
34+
return mcpApi
14435
}

packages/types/src/vscode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const commandIds = [
5454
"acceptInput",
5555
"focusPanel",
5656
"toggleAutoApprove",
57+
"refreshMcpServers",
5758
] as const
5859

5960
export type CommandId = (typeof commandIds)[number]
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import { registerCommands } from "../registerCommands"
4+
import { ClineProvider } from "../../core/webview/ClineProvider"
5+
6+
// Mock vscode module
7+
vi.mock("vscode", () => ({
8+
window: {
9+
showInformationMessage: vi.fn(),
10+
showWarningMessage: vi.fn(),
11+
},
12+
commands: {
13+
registerCommand: vi.fn(),
14+
},
15+
}))
16+
17+
describe("registerCommands", () => {
18+
let mockContext: vscode.ExtensionContext
19+
let mockOutputChannel: vscode.OutputChannel
20+
let mockProvider: ClineProvider
21+
22+
beforeEach(() => {
23+
// Reset all mocks
24+
vi.clearAllMocks()
25+
26+
// Create mock objects
27+
mockContext = {
28+
subscriptions: {
29+
push: vi.fn(),
30+
},
31+
} as any
32+
33+
mockOutputChannel = {
34+
appendLine: vi.fn(),
35+
} as any
36+
37+
mockProvider = {
38+
getMcpHub: vi.fn(),
39+
} as any
40+
})
41+
42+
describe("refreshMcpServers command", () => {
43+
it("should refresh MCP servers when hub is available", async () => {
44+
// Arrange
45+
const mockMcpHub = {
46+
refreshAllConnections: vi.fn().mockResolvedValue(undefined),
47+
}
48+
mockProvider.getMcpHub = vi.fn().mockReturnValue(mockMcpHub)
49+
50+
// Mock getVisibleProviderOrLog to return our mock provider
51+
const getVisibleProviderOrLog = vi.fn().mockReturnValue(mockProvider)
52+
53+
// Register commands
54+
const commands = registerCommands({
55+
context: mockContext,
56+
outputChannel: mockOutputChannel,
57+
provider: mockProvider,
58+
})
59+
60+
// Find and execute the refreshMcpServers command
61+
const registerCommandCalls = vi.mocked(vscode.commands.registerCommand).mock.calls
62+
const refreshCommand = registerCommandCalls.find(([cmd]) => cmd === "roo-cline.refreshMcpServers")
63+
64+
expect(refreshCommand).toBeDefined()
65+
66+
// Execute the command callback
67+
const commandCallback = refreshCommand![1] as () => Promise<void>
68+
await commandCallback()
69+
70+
// Assert
71+
expect(mockProvider.getMcpHub).toHaveBeenCalled()
72+
expect(mockMcpHub.refreshAllConnections).toHaveBeenCalled()
73+
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("MCP servers refreshed successfully")
74+
})
75+
76+
it("should show warning when MCP hub is not available", async () => {
77+
// Arrange
78+
mockProvider.getMcpHub = vi.fn().mockReturnValue(undefined)
79+
80+
// Mock getVisibleProviderOrLog to return our mock provider
81+
const getVisibleProviderOrLog = vi.fn().mockReturnValue(mockProvider)
82+
83+
// Register commands
84+
const commands = registerCommands({
85+
context: mockContext,
86+
outputChannel: mockOutputChannel,
87+
provider: mockProvider,
88+
})
89+
90+
// Find and execute the refreshMcpServers command
91+
const registerCommandCalls = vi.mocked(vscode.commands.registerCommand).mock.calls
92+
const refreshCommand = registerCommandCalls.find(([cmd]) => cmd === "roo-cline.refreshMcpServers")
93+
94+
expect(refreshCommand).toBeDefined()
95+
96+
// Execute the command callback
97+
const commandCallback = refreshCommand![1] as () => Promise<void>
98+
await commandCallback()
99+
100+
// Assert
101+
expect(mockProvider.getMcpHub).toHaveBeenCalled()
102+
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("MCP hub is not available")
103+
})
104+
105+
it("should not execute when no visible provider is found", async () => {
106+
// Arrange
107+
// Mock getVisibleProviderOrLog to return undefined
108+
const getVisibleProviderOrLog = vi.fn().mockReturnValue(undefined)
109+
110+
// Register commands
111+
const commands = registerCommands({
112+
context: mockContext,
113+
outputChannel: mockOutputChannel,
114+
provider: mockProvider,
115+
})
116+
117+
// Find and execute the refreshMcpServers command
118+
const registerCommandCalls = vi.mocked(vscode.commands.registerCommand).mock.calls
119+
const refreshCommand = registerCommandCalls.find(([cmd]) => cmd === "roo-cline.refreshMcpServers")
120+
121+
expect(refreshCommand).toBeDefined()
122+
123+
// Execute the command callback
124+
const commandCallback = refreshCommand![1] as () => Promise<void>
125+
await commandCallback()
126+
127+
// Assert - should return early without calling getMcpHub
128+
expect(mockProvider.getMcpHub).not.toHaveBeenCalled()
129+
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
130+
expect(vscode.window.showWarningMessage).not.toHaveBeenCalled()
131+
})
132+
})
133+
})

src/activate/registerCommands.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,21 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
233233
action: "toggleAutoApprove",
234234
})
235235
},
236+
refreshMcpServers: async () => {
237+
const visibleProvider = getVisibleProviderOrLog(outputChannel)
238+
239+
if (!visibleProvider) {
240+
return
241+
}
242+
243+
const mcpHub = visibleProvider.getMcpHub()
244+
if (mcpHub) {
245+
await mcpHub.refreshAllConnections()
246+
vscode.window.showInformationMessage("MCP servers refreshed successfully")
247+
} else {
248+
vscode.window.showWarningMessage("MCP hub is not available")
249+
}
250+
},
236251
})
237252

238253
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {

0 commit comments

Comments
 (0)