mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Allow enabling/disabling of MCP servers
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Roo Cline Changelog
|
# Roo Cline Changelog
|
||||||
|
|
||||||
|
## [2.2.5]
|
||||||
|
|
||||||
|
- Allow MCP servers to be enabled/disabled
|
||||||
|
|
||||||
## [2.2.4]
|
## [2.2.4]
|
||||||
|
|
||||||
- Tweak the prompt to encourage diff edits when they're enabled
|
- Tweak the prompt to encourage diff edits when they're enabled
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ A fork of Cline, an autonomous coding agent, optimized for speed and flexibility
|
|||||||
- Support for newer Gemini models (gemini-exp-1206 and gemini-2.0-flash-exp)
|
- Support for newer Gemini models (gemini-exp-1206 and gemini-2.0-flash-exp)
|
||||||
- Support for dragging and dropping images into chats
|
- Support for dragging and dropping images into chats
|
||||||
- Support for auto-approving MCP tools
|
- Support for auto-approving MCP tools
|
||||||
|
- Support for enabling/disabling MCP servers
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "roo-cline",
|
"name": "roo-cline",
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "roo-cline",
|
"name": "roo-cline",
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||||
"@anthropic-ai/sdk": "^0.26.0",
|
"@anthropic-ai/sdk": "^0.26.0",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"displayName": "Roo Cline",
|
"displayName": "Roo Cline",
|
||||||
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
||||||
"publisher": "RooVeterinaryInc",
|
"publisher": "RooVeterinaryInc",
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"icon": "assets/icons/rocket.png",
|
"icon": "assets/icons/rocket.png",
|
||||||
"galleryBanner": {
|
"galleryBanner": {
|
||||||
"color": "#617A91",
|
"color": "#617A91",
|
||||||
|
|||||||
@@ -633,7 +633,7 @@ npm run build
|
|||||||
|
|
||||||
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
|
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
|
||||||
|
|
||||||
IMPORTANT: Regardless of what else you see in the settings file, you must not set any defaults for the \`alwaysAllow\` array in the newly added MCP server.
|
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and alwaysAllow=[].
|
||||||
|
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -567,6 +567,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "toggleMcpServer": {
|
||||||
|
try {
|
||||||
|
await this.mcpHub?.toggleServerDisabled(
|
||||||
|
message.serverName!,
|
||||||
|
message.disabled!
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to toggle MCP server ${message.serverName}:`, error)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
// Add more switch case statements here as more webview message commands
|
// Add more switch case statements here as more webview message commands
|
||||||
// are created within the webview context (i.e. inside media/main.js)
|
// are created within the webview context (i.e. inside media/main.js)
|
||||||
case "playSound":
|
case "playSound":
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ const StdioConfigSchema = z.object({
|
|||||||
command: z.string(),
|
command: z.string(),
|
||||||
args: z.array(z.string()).optional(),
|
args: z.array(z.string()).optional(),
|
||||||
env: z.record(z.string()).optional(),
|
env: z.record(z.string()).optional(),
|
||||||
alwaysAllow: AlwaysAllowSchema.optional()
|
alwaysAllow: AlwaysAllowSchema.optional(),
|
||||||
|
disabled: z.boolean().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
const McpSettingsSchema = z.object({
|
const McpSettingsSchema = z.object({
|
||||||
@@ -61,7 +62,10 @@ export class McpHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getServers(): McpServer[] {
|
getServers(): McpServer[] {
|
||||||
return this.connections.map((conn) => conn.server)
|
// Only return enabled servers
|
||||||
|
return this.connections
|
||||||
|
.filter((conn) => !conn.server.disabled)
|
||||||
|
.map((conn) => conn.server)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMcpServersPath(): Promise<string> {
|
async getMcpServersPath(): Promise<string> {
|
||||||
@@ -117,9 +121,7 @@ export class McpHub {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
vscode.window.showInformationMessage("Updating MCP servers...")
|
|
||||||
await this.updateServerConnections(result.data.mcpServers || {})
|
await this.updateServerConnections(result.data.mcpServers || {})
|
||||||
vscode.window.showInformationMessage("MCP servers updated")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to process MCP settings change:", error)
|
console.error("Failed to process MCP settings change:", error)
|
||||||
}
|
}
|
||||||
@@ -202,11 +204,13 @@ export class McpHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// valid schema
|
// valid schema
|
||||||
|
const parsedConfig = StdioConfigSchema.parse(config)
|
||||||
const connection: McpConnection = {
|
const connection: McpConnection = {
|
||||||
server: {
|
server: {
|
||||||
name,
|
name,
|
||||||
config: JSON.stringify(config),
|
config: JSON.stringify(config),
|
||||||
status: "connecting",
|
status: "connecting",
|
||||||
|
disabled: parsedConfig.disabled,
|
||||||
},
|
},
|
||||||
client,
|
client,
|
||||||
transport,
|
transport,
|
||||||
@@ -466,13 +470,89 @@ export class McpHub {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using server
|
// Public methods for server management
|
||||||
|
|
||||||
|
public async toggleServerDisabled(serverName: string, disabled: boolean): Promise<void> {
|
||||||
|
let settingsPath: string
|
||||||
|
try {
|
||||||
|
settingsPath = await this.getMcpSettingsFilePath()
|
||||||
|
|
||||||
|
// Ensure the settings file exists and is accessible
|
||||||
|
try {
|
||||||
|
await fs.access(settingsPath)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Settings file not accessible:', error)
|
||||||
|
throw new Error('Settings file not accessible')
|
||||||
|
}
|
||||||
|
const content = await fs.readFile(settingsPath, "utf-8")
|
||||||
|
const config = JSON.parse(content)
|
||||||
|
|
||||||
|
// Validate the config structure
|
||||||
|
if (!config || typeof config !== 'object') {
|
||||||
|
throw new Error('Invalid config structure')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
||||||
|
config.mcpServers = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.mcpServers[serverName]) {
|
||||||
|
// Create a new server config object to ensure clean structure
|
||||||
|
const serverConfig = {
|
||||||
|
...config.mcpServers[serverName],
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure required fields exist
|
||||||
|
if (!serverConfig.alwaysAllow) {
|
||||||
|
serverConfig.alwaysAllow = []
|
||||||
|
}
|
||||||
|
|
||||||
|
config.mcpServers[serverName] = serverConfig
|
||||||
|
|
||||||
|
// Write the entire config back
|
||||||
|
const updatedConfig = {
|
||||||
|
mcpServers: config.mcpServers
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
|
||||||
|
|
||||||
|
const connection = this.connections.find(conn => conn.server.name === serverName)
|
||||||
|
if (connection) {
|
||||||
|
try {
|
||||||
|
connection.server.disabled = disabled
|
||||||
|
|
||||||
|
// Only refresh capabilities if connected
|
||||||
|
if (connection.server.status === "connected") {
|
||||||
|
connection.server.tools = await this.fetchToolsList(serverName)
|
||||||
|
connection.server.resources = await this.fetchResourcesList(serverName)
|
||||||
|
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(serverName)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to refresh capabilities for ${serverName}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.notifyWebviewOfServerChanges()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update server disabled state:", error)
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error("Error details:", error.message, error.stack)
|
||||||
|
}
|
||||||
|
vscode.window.showErrorMessage(`Failed to update server state: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
|
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
|
||||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
throw new Error(`No connection found for server: ${serverName}`)
|
throw new Error(`No connection found for server: ${serverName}`)
|
||||||
}
|
}
|
||||||
|
if (connection.server.disabled) {
|
||||||
|
throw new Error(`Server "${serverName}" is disabled`)
|
||||||
|
}
|
||||||
return await connection.client.request(
|
return await connection.client.request(
|
||||||
{
|
{
|
||||||
method: "resources/read",
|
method: "resources/read",
|
||||||
@@ -495,6 +575,9 @@ export class McpHub {
|
|||||||
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
|
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (connection.server.disabled) {
|
||||||
|
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
|
||||||
|
}
|
||||||
|
|
||||||
return await connection.client.request(
|
return await connection.client.request(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -148,6 +148,103 @@ describe('McpHub', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('server disabled state', () => {
|
||||||
|
it('should toggle server disabled state', async () => {
|
||||||
|
const mockConfig = {
|
||||||
|
mcpServers: {
|
||||||
|
'test-server': {
|
||||||
|
command: 'node',
|
||||||
|
args: ['test.js'],
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock reading initial config
|
||||||
|
;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
|
||||||
|
|
||||||
|
await mcpHub.toggleServerDisabled('test-server', true)
|
||||||
|
|
||||||
|
// Verify the config was updated correctly
|
||||||
|
const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
|
||||||
|
const writtenConfig = JSON.parse(writeCall[1])
|
||||||
|
expect(writtenConfig.mcpServers['test-server'].disabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter out disabled servers from getServers', () => {
|
||||||
|
const mockConnections: McpConnection[] = [
|
||||||
|
{
|
||||||
|
server: {
|
||||||
|
name: 'enabled-server',
|
||||||
|
config: '{}',
|
||||||
|
status: 'connected',
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
|
client: {} as any,
|
||||||
|
transport: {} as any
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: {
|
||||||
|
name: 'disabled-server',
|
||||||
|
config: '{}',
|
||||||
|
status: 'connected',
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
client: {} as any,
|
||||||
|
transport: {} as any
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
mcpHub.connections = mockConnections
|
||||||
|
const servers = mcpHub.getServers()
|
||||||
|
|
||||||
|
expect(servers.length).toBe(1)
|
||||||
|
expect(servers[0].name).toBe('enabled-server')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent calling tools on disabled servers', async () => {
|
||||||
|
const mockConnection: McpConnection = {
|
||||||
|
server: {
|
||||||
|
name: 'disabled-server',
|
||||||
|
config: '{}',
|
||||||
|
status: 'connected',
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
request: jest.fn().mockResolvedValue({ result: 'success' })
|
||||||
|
} as any,
|
||||||
|
transport: {} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpHub.connections = [mockConnection]
|
||||||
|
|
||||||
|
await expect(mcpHub.callTool('disabled-server', 'some-tool', {}))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Server "disabled-server" is disabled and cannot be used')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent reading resources from disabled servers', async () => {
|
||||||
|
const mockConnection: McpConnection = {
|
||||||
|
server: {
|
||||||
|
name: 'disabled-server',
|
||||||
|
config: '{}',
|
||||||
|
status: 'connected',
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
request: jest.fn()
|
||||||
|
} as any,
|
||||||
|
transport: {} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpHub.connections = [mockConnection]
|
||||||
|
|
||||||
|
await expect(mcpHub.readResource('disabled-server', 'some/uri'))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Server "disabled-server" is disabled')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('callTool', () => {
|
describe('callTool', () => {
|
||||||
it('should execute tool successfully', async () => {
|
it('should execute tool successfully', async () => {
|
||||||
// Mock the connection with a minimal client implementation
|
// Mock the connection with a minimal client implementation
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export interface WebviewMessage {
|
|||||||
| "openMcpSettings"
|
| "openMcpSettings"
|
||||||
| "restartMcpServer"
|
| "restartMcpServer"
|
||||||
| "toggleToolAlwaysAllow"
|
| "toggleToolAlwaysAllow"
|
||||||
|
| "toggleMcpServer"
|
||||||
text?: string
|
text?: string
|
||||||
|
disabled?: boolean
|
||||||
askResponse?: ClineAskResponse
|
askResponse?: ClineAskResponse
|
||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
images?: string[]
|
images?: string[]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type McpServer = {
|
|||||||
tools?: McpTool[]
|
tools?: McpTool[]
|
||||||
resources?: McpResource[]
|
resources?: McpResource[]
|
||||||
resourceTemplates?: McpResourceTemplate[]
|
resourceTemplates?: McpResourceTemplate[]
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type McpTool = {
|
export type McpTool = {
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer, alwaysAllowM
|
|||||||
background: "var(--vscode-textCodeBlock-background)",
|
background: "var(--vscode-textCodeBlock-background)",
|
||||||
cursor: server.error ? "default" : "pointer",
|
cursor: server.error ? "default" : "pointer",
|
||||||
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
|
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
|
||||||
|
opacity: server.disabled ? 0.6 : 1,
|
||||||
}}
|
}}
|
||||||
onClick={handleRowClick}>
|
onClick={handleRowClick}>
|
||||||
{!server.error && (
|
{!server.error && (
|
||||||
@@ -198,6 +199,55 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer, alwaysAllowM
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span style={{ flex: 1 }}>{server.name}</span>
|
<span style={{ flex: 1 }}>{server.name}</span>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", alignItems: "center", marginRight: "8px" }}
|
||||||
|
onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div
|
||||||
|
role="switch"
|
||||||
|
aria-checked={!server.disabled}
|
||||||
|
tabIndex={0}
|
||||||
|
style={{
|
||||||
|
width: "20px",
|
||||||
|
height: "10px",
|
||||||
|
backgroundColor: server.disabled ?
|
||||||
|
"var(--vscode-titleBar-inactiveForeground)" :
|
||||||
|
"var(--vscode-button-background)",
|
||||||
|
borderRadius: "5px",
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "background-color 0.2s",
|
||||||
|
opacity: server.disabled ? 0.4 : 0.8,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "toggleMcpServer",
|
||||||
|
serverName: server.name,
|
||||||
|
disabled: !server.disabled
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "toggleMcpServer",
|
||||||
|
serverName: server.name,
|
||||||
|
disabled: !server.disabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
width: "6px",
|
||||||
|
height: "6px",
|
||||||
|
backgroundColor: "var(--vscode-titleBar-activeForeground)",
|
||||||
|
borderRadius: "50%",
|
||||||
|
position: "absolute",
|
||||||
|
top: "2px",
|
||||||
|
left: server.disabled ? "2px" : "12px",
|
||||||
|
transition: "left 0.2s",
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "8px",
|
width: "8px",
|
||||||
|
|||||||
Reference in New Issue
Block a user