mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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
This commit is contained in:
parent
a57528dc8e
commit
c2cc9643e1
9 changed files with 324 additions and 140 deletions
1
.tmp/review/Roo-Code
Submodule
1
.tmp/review/Roo-Code
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 8dbd8c4b1b72fb48be3990a8e78285a787a1828c
|
||||
1
.work/reviews/Roo-Code
Submodule
1
.work/reviews/Roo-Code
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit ea8420be8c5386d867fe6aa7b1f9756a44a3b5b1
|
||||
|
|
@ -1,144 +1,35 @@
|
|||
import type { EventEmitter } from "events"
|
||||
import type { Socket } from "net"
|
||||
/**
|
||||
* API for programmatic MCP server operations
|
||||
*/
|
||||
|
||||
import type { RooCodeEvents } from "./events.js"
|
||||
import type { RooCodeSettings } from "./global-settings.js"
|
||||
import type { ProviderSettingsEntry, ProviderSettings } from "./provider-settings.js"
|
||||
import type { IpcMessage, IpcServerEvents } from "./ipc.js"
|
||||
|
||||
export type RooCodeAPIEvents = RooCodeEvents
|
||||
|
||||
export interface RooCodeAPI extends EventEmitter<RooCodeAPIEvents> {
|
||||
/**
|
||||
* Interface for MCP operations that can be accessed programmatically
|
||||
*/
|
||||
export interface McpApi {
|
||||
/**
|
||||
* Starts a new task with an optional initial message and images.
|
||||
* @param task Optional initial task message.
|
||||
* @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
|
||||
* @returns The ID of the new task.
|
||||
* Refresh all MCP server connections
|
||||
* @returns Promise that resolves when refresh is complete
|
||||
*/
|
||||
startNewTask({
|
||||
configuration,
|
||||
text,
|
||||
images,
|
||||
newTab,
|
||||
}: {
|
||||
configuration?: RooCodeSettings
|
||||
text?: string
|
||||
images?: string[]
|
||||
newTab?: boolean
|
||||
}): Promise<string>
|
||||
/**
|
||||
* Resumes a task with the given ID.
|
||||
* @param taskId The ID of the task to resume.
|
||||
* @throws Error if the task is not found in the task history.
|
||||
*/
|
||||
resumeTask(taskId: string): Promise<void>
|
||||
/**
|
||||
* Checks if a task with the given ID is in the task history.
|
||||
* @param taskId The ID of the task to check.
|
||||
* @returns True if the task is in the task history, false otherwise.
|
||||
*/
|
||||
isTaskInHistory(taskId: string): Promise<boolean>
|
||||
/**
|
||||
* Returns the current task stack.
|
||||
* @returns An array of task IDs.
|
||||
*/
|
||||
getCurrentTaskStack(): string[]
|
||||
/**
|
||||
* Clears the current task.
|
||||
*/
|
||||
clearCurrentTask(lastMessage?: string): Promise<void>
|
||||
/**
|
||||
* Cancels the current task.
|
||||
*/
|
||||
cancelCurrentTask(): Promise<void>
|
||||
/**
|
||||
* Sends a message to the current task.
|
||||
* @param message Optional message to send.
|
||||
* @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
|
||||
*/
|
||||
sendMessage(message?: string, images?: string[]): Promise<void>
|
||||
/**
|
||||
* Simulates pressing the primary button in the chat interface.
|
||||
*/
|
||||
pressPrimaryButton(): Promise<void>
|
||||
/**
|
||||
* Simulates pressing the secondary button in the chat interface.
|
||||
*/
|
||||
pressSecondaryButton(): Promise<void>
|
||||
/**
|
||||
* Returns true if the API is ready to use.
|
||||
*/
|
||||
isReady(): boolean
|
||||
/**
|
||||
* Returns the current configuration.
|
||||
* @returns The current configuration.
|
||||
*/
|
||||
getConfiguration(): RooCodeSettings
|
||||
/**
|
||||
* Sets the configuration for the current task.
|
||||
* @param values An object containing key-value pairs to set.
|
||||
*/
|
||||
setConfiguration(values: RooCodeSettings): Promise<void>
|
||||
/**
|
||||
* Returns a list of all configured profile names
|
||||
* @returns Array of profile names
|
||||
*/
|
||||
getProfiles(): string[]
|
||||
/**
|
||||
* Returns the profile entry for a given name
|
||||
* @param name The name of the profile
|
||||
* @returns The profile entry, or undefined if the profile does not exist
|
||||
*/
|
||||
getProfileEntry(name: string): ProviderSettingsEntry | undefined
|
||||
/**
|
||||
* Creates a new API configuration profile
|
||||
* @param name The name of the profile
|
||||
* @param profile The profile to create; defaults to an empty object
|
||||
* @param activate Whether to activate the profile after creation; defaults to true
|
||||
* @returns The ID of the created profile
|
||||
* @throws Error if the profile already exists
|
||||
*/
|
||||
createProfile(name: string, profile?: ProviderSettings, activate?: boolean): Promise<string>
|
||||
/**
|
||||
* Updates an existing API configuration profile
|
||||
* @param name The name of the profile
|
||||
* @param profile The profile to update
|
||||
* @param activate Whether to activate the profile after update; defaults to true
|
||||
* @returns The ID of the updated profile
|
||||
* @throws Error if the profile does not exist
|
||||
*/
|
||||
updateProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
|
||||
/**
|
||||
* Creates a new API configuration profile or updates an existing one
|
||||
* @param name The name of the profile
|
||||
* @param profile The profile to create or update; defaults to an empty object
|
||||
* @param activate Whether to activate the profile after upsert; defaults to true
|
||||
* @returns The ID of the upserted profile
|
||||
*/
|
||||
upsertProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
|
||||
/**
|
||||
* Deletes a profile by name
|
||||
* @param name The name of the profile to delete
|
||||
* @throws Error if the profile does not exist
|
||||
*/
|
||||
deleteProfile(name: string): Promise<void>
|
||||
/**
|
||||
* Returns the name of the currently active profile
|
||||
* @returns The profile name, or undefined if no profile is active
|
||||
*/
|
||||
getActiveProfile(): string | undefined
|
||||
/**
|
||||
* Changes the active API configuration profile
|
||||
* @param name The name of the profile to activate
|
||||
* @throws Error if the profile does not exist
|
||||
*/
|
||||
setActiveProfile(name: string): Promise<string | undefined>
|
||||
refreshMcpServers(): Promise<void>
|
||||
}
|
||||
|
||||
export interface RooCodeIpcServer extends EventEmitter<IpcServerEvents> {
|
||||
listen(): void
|
||||
broadcast(message: IpcMessage): void
|
||||
send(client: string | Socket, message: IpcMessage): void
|
||||
get socketPath(): string
|
||||
get isListening(): boolean
|
||||
/**
|
||||
* Global MCP API instance that will be set by the extension
|
||||
*/
|
||||
export let mcpApi: McpApi | undefined
|
||||
|
||||
/**
|
||||
* Set the global MCP API instance
|
||||
* @param api The MCP API implementation
|
||||
*/
|
||||
export function setMcpApi(api: McpApi): void {
|
||||
mcpApi = api
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global MCP API instance
|
||||
* @returns The MCP API instance or undefined if not set
|
||||
*/
|
||||
export function getMcpApi(): McpApi | undefined {
|
||||
return mcpApi
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export const commandIds = [
|
|||
"acceptInput",
|
||||
"focusPanel",
|
||||
"toggleAutoApprove",
|
||||
"refreshMcpServers",
|
||||
] as const
|
||||
|
||||
export type CommandId = (typeof commandIds)[number]
|
||||
|
|
|
|||
133
src/activate/__tests__/registerCommands.test.ts
Normal file
133
src/activate/__tests__/registerCommands.test.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import { registerCommands } from "../registerCommands"
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("vscode", () => ({
|
||||
window: {
|
||||
showInformationMessage: vi.fn(),
|
||||
showWarningMessage: vi.fn(),
|
||||
},
|
||||
commands: {
|
||||
registerCommand: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("registerCommands", () => {
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let mockOutputChannel: vscode.OutputChannel
|
||||
let mockProvider: ClineProvider
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create mock objects
|
||||
mockContext = {
|
||||
subscriptions: {
|
||||
push: vi.fn(),
|
||||
},
|
||||
} as any
|
||||
|
||||
mockOutputChannel = {
|
||||
appendLine: vi.fn(),
|
||||
} as any
|
||||
|
||||
mockProvider = {
|
||||
getMcpHub: vi.fn(),
|
||||
} as any
|
||||
})
|
||||
|
||||
describe("refreshMcpServers command", () => {
|
||||
it("should refresh MCP servers when hub is available", async () => {
|
||||
// Arrange
|
||||
const mockMcpHub = {
|
||||
refreshAllConnections: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
mockProvider.getMcpHub = vi.fn().mockReturnValue(mockMcpHub)
|
||||
|
||||
// Mock getVisibleProviderOrLog to return our mock provider
|
||||
const getVisibleProviderOrLog = vi.fn().mockReturnValue(mockProvider)
|
||||
|
||||
// Register commands
|
||||
const commands = registerCommands({
|
||||
context: mockContext,
|
||||
outputChannel: mockOutputChannel,
|
||||
provider: mockProvider,
|
||||
})
|
||||
|
||||
// Find and execute the refreshMcpServers command
|
||||
const registerCommandCalls = vi.mocked(vscode.commands.registerCommand).mock.calls
|
||||
const refreshCommand = registerCommandCalls.find(([cmd]) => cmd === "roo-cline.refreshMcpServers")
|
||||
|
||||
expect(refreshCommand).toBeDefined()
|
||||
|
||||
// Execute the command callback
|
||||
const commandCallback = refreshCommand![1] as () => Promise<void>
|
||||
await commandCallback()
|
||||
|
||||
// Assert
|
||||
expect(mockProvider.getMcpHub).toHaveBeenCalled()
|
||||
expect(mockMcpHub.refreshAllConnections).toHaveBeenCalled()
|
||||
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("MCP servers refreshed successfully")
|
||||
})
|
||||
|
||||
it("should show warning when MCP hub is not available", async () => {
|
||||
// Arrange
|
||||
mockProvider.getMcpHub = vi.fn().mockReturnValue(undefined)
|
||||
|
||||
// Mock getVisibleProviderOrLog to return our mock provider
|
||||
const getVisibleProviderOrLog = vi.fn().mockReturnValue(mockProvider)
|
||||
|
||||
// Register commands
|
||||
const commands = registerCommands({
|
||||
context: mockContext,
|
||||
outputChannel: mockOutputChannel,
|
||||
provider: mockProvider,
|
||||
})
|
||||
|
||||
// Find and execute the refreshMcpServers command
|
||||
const registerCommandCalls = vi.mocked(vscode.commands.registerCommand).mock.calls
|
||||
const refreshCommand = registerCommandCalls.find(([cmd]) => cmd === "roo-cline.refreshMcpServers")
|
||||
|
||||
expect(refreshCommand).toBeDefined()
|
||||
|
||||
// Execute the command callback
|
||||
const commandCallback = refreshCommand![1] as () => Promise<void>
|
||||
await commandCallback()
|
||||
|
||||
// Assert
|
||||
expect(mockProvider.getMcpHub).toHaveBeenCalled()
|
||||
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("MCP hub is not available")
|
||||
})
|
||||
|
||||
it("should not execute when no visible provider is found", async () => {
|
||||
// Arrange
|
||||
// Mock getVisibleProviderOrLog to return undefined
|
||||
const getVisibleProviderOrLog = vi.fn().mockReturnValue(undefined)
|
||||
|
||||
// Register commands
|
||||
const commands = registerCommands({
|
||||
context: mockContext,
|
||||
outputChannel: mockOutputChannel,
|
||||
provider: mockProvider,
|
||||
})
|
||||
|
||||
// Find and execute the refreshMcpServers command
|
||||
const registerCommandCalls = vi.mocked(vscode.commands.registerCommand).mock.calls
|
||||
const refreshCommand = registerCommandCalls.find(([cmd]) => cmd === "roo-cline.refreshMcpServers")
|
||||
|
||||
expect(refreshCommand).toBeDefined()
|
||||
|
||||
// Execute the command callback
|
||||
const commandCallback = refreshCommand![1] as () => Promise<void>
|
||||
await commandCallback()
|
||||
|
||||
// Assert - should return early without calling getMcpHub
|
||||
expect(mockProvider.getMcpHub).not.toHaveBeenCalled()
|
||||
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
|
||||
expect(vscode.window.showWarningMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -233,6 +233,21 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
|
|||
action: "toggleAutoApprove",
|
||||
})
|
||||
},
|
||||
refreshMcpServers: async () => {
|
||||
const visibleProvider = getVisibleProviderOrLog(outputChannel)
|
||||
|
||||
if (!visibleProvider) {
|
||||
return
|
||||
}
|
||||
|
||||
const mcpHub = visibleProvider.getMcpHub()
|
||||
if (mcpHub) {
|
||||
await mcpHub.refreshAllConnections()
|
||||
vscode.window.showInformationMessage("MCP servers refreshed successfully")
|
||||
} else {
|
||||
vscode.window.showWarningMessage("MCP hub is not available")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
|
||||
|
|
|
|||
123
src/extension/__tests__/api.test.ts
Normal file
123
src/extension/__tests__/api.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import { API } from "../api"
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { setMcpApi, getMcpApi } from "@roo-code/types"
|
||||
|
||||
// Mock the types module with partial mocking
|
||||
vi.mock("@roo-code/types", async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any
|
||||
return {
|
||||
...actual,
|
||||
setMcpApi: vi.fn(),
|
||||
getMcpApi: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("vscode", () => ({
|
||||
OutputChannel: vi.fn(),
|
||||
ExtensionContext: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("API", () => {
|
||||
let mockOutputChannel: vscode.OutputChannel
|
||||
let mockProvider: ClineProvider
|
||||
let api: API
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create mock objects
|
||||
mockOutputChannel = {
|
||||
appendLine: vi.fn(),
|
||||
} as any
|
||||
|
||||
mockProvider = {
|
||||
context: {
|
||||
subscriptions: [],
|
||||
} as any,
|
||||
getMcpHub: vi.fn(),
|
||||
on: vi.fn(),
|
||||
viewLaunched: true,
|
||||
getCurrentTaskStack: vi.fn(),
|
||||
finishSubTask: vi.fn(),
|
||||
postStateToWebview: vi.fn(),
|
||||
cancelTask: vi.fn(),
|
||||
postMessageToWebview: vi.fn(),
|
||||
removeClineFromStack: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
getTaskWithId: vi.fn(),
|
||||
createTaskWithHistoryItem: vi.fn(),
|
||||
getValues: vi.fn().mockReturnValue({}),
|
||||
contextProxy: {
|
||||
setValues: vi.fn(),
|
||||
},
|
||||
providerSettingsManager: {
|
||||
saveConfig: vi.fn(),
|
||||
},
|
||||
getProviderProfileEntries: vi.fn().mockReturnValue([]),
|
||||
getProviderProfileEntry: vi.fn(),
|
||||
upsertProviderProfile: vi.fn(),
|
||||
deleteProviderProfile: vi.fn(),
|
||||
activateProviderProfile: vi.fn(),
|
||||
} as any
|
||||
|
||||
// Create API instance
|
||||
api = new API(mockOutputChannel, mockProvider)
|
||||
})
|
||||
|
||||
describe("refreshMcpServers", () => {
|
||||
it("should call refreshAllConnections on the MCP hub", async () => {
|
||||
// Arrange
|
||||
const mockMcpHub = {
|
||||
refreshAllConnections: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
mockProvider.getMcpHub = vi.fn().mockReturnValue(mockMcpHub)
|
||||
|
||||
// Act
|
||||
await api.refreshMcpServers()
|
||||
|
||||
// Assert
|
||||
expect(mockProvider.getMcpHub).toHaveBeenCalled()
|
||||
expect(mockMcpHub.refreshAllConnections).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should throw an error when MCP hub is not available", async () => {
|
||||
// Arrange
|
||||
mockProvider.getMcpHub = vi.fn().mockReturnValue(undefined)
|
||||
|
||||
// Act & Assert
|
||||
await expect(api.refreshMcpServers()).rejects.toThrow("MCP hub is not available")
|
||||
expect(mockProvider.getMcpHub).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should propagate errors from refreshAllConnections", async () => {
|
||||
// Arrange
|
||||
const testError = new Error("Connection failed")
|
||||
const mockMcpHub = {
|
||||
refreshAllConnections: vi.fn().mockRejectedValue(testError),
|
||||
}
|
||||
mockProvider.getMcpHub = vi.fn().mockReturnValue(mockMcpHub)
|
||||
|
||||
// Act & Assert
|
||||
await expect(api.refreshMcpServers()).rejects.toThrow("Connection failed")
|
||||
expect(mockProvider.getMcpHub).toHaveBeenCalled()
|
||||
expect(mockMcpHub.refreshAllConnections).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("API initialization", () => {
|
||||
it("should register the API with setMcpApi", () => {
|
||||
// Assert
|
||||
expect(setMcpApi).toHaveBeenCalledWith(api)
|
||||
})
|
||||
|
||||
it("should implement the McpApi interface", () => {
|
||||
// Assert
|
||||
expect(api.refreshMcpServers).toBeDefined()
|
||||
expect(typeof api.refreshMcpServers).toBe("function")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -6,18 +6,19 @@ import * as os from "os"
|
|||
import * as vscode from "vscode"
|
||||
|
||||
import {
|
||||
type RooCodeAPI,
|
||||
type RooCodeSettings,
|
||||
type RooCodeEvents,
|
||||
type ProviderSettings,
|
||||
type ProviderSettingsEntry,
|
||||
type TaskEvent,
|
||||
type CreateTaskOptions,
|
||||
type McpApi,
|
||||
RooCodeEventName,
|
||||
TaskCommandName,
|
||||
isSecretStateKey,
|
||||
IpcOrigin,
|
||||
IpcMessageType,
|
||||
setMcpApi,
|
||||
} from "@roo-code/types"
|
||||
import { IpcServer } from "@roo-code/ipc"
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ import { Package } from "../shared/package"
|
|||
import { ClineProvider } from "../core/webview/ClineProvider"
|
||||
import { openClineInNewTab } from "../activate/registerCommands"
|
||||
|
||||
export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
|
||||
export class API extends EventEmitter<RooCodeEvents> implements McpApi {
|
||||
private readonly outputChannel: vscode.OutputChannel
|
||||
private readonly sidebarProvider: ClineProvider
|
||||
private readonly context: vscode.ExtensionContext
|
||||
|
|
@ -94,6 +95,19 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize the global MCP API
|
||||
setMcpApi(this)
|
||||
}
|
||||
|
||||
// McpApi implementation
|
||||
public async refreshMcpServers(): Promise<void> {
|
||||
const mcpHub = this.sidebarProvider.getMcpHub()
|
||||
if (mcpHub) {
|
||||
await mcpHub.refreshAllConnections()
|
||||
} else {
|
||||
throw new Error("MCP hub is not available")
|
||||
}
|
||||
}
|
||||
|
||||
public override emit<K extends keyof RooCodeEvents>(
|
||||
|
|
|
|||
|
|
@ -179,6 +179,11 @@
|
|||
"command": "roo-cline.toggleAutoApprove",
|
||||
"title": "%command.toggleAutoApprove.title%",
|
||||
"category": "%configuration.title%"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.refreshMcpServers",
|
||||
"title": "Refresh MCP Servers",
|
||||
"category": "%configuration.title%"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue