Skip to content

Commit 85acffd

Browse files
authored
Merge pull request #110 from RooVetGit/toggle_mcp_servers
Allow enabling/disabling of MCP servers
2 parents 5c643af + 3d7ff32 commit 85acffd

File tree

11 files changed

+258
-9
lines changed

11 files changed

+258
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Roo Cline Changelog
22

3+
## [2.2.5]
4+
5+
- Allow MCP servers to be enabled/disabled
6+
37
## [2.2.4]
48

59
- Tweak the prompt to encourage diff edits when they're enabled

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A fork of Cline, an autonomous coding agent, optimized for speed and flexibility
1212
- Support for newer Gemini models (gemini-exp-1206 and gemini-2.0-flash-exp)
1313
- Support for dragging and dropping images into chats
1414
- Support for auto-approving MCP tools
15+
- Support for enabling/disabling MCP servers
1516

1617
## Disclaimer
1718

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Roo Cline",
44
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
55
"publisher": "RooVeterinaryInc",
6-
"version": "2.2.4",
6+
"version": "2.2.5",
77
"icon": "assets/icons/rocket.png",
88
"galleryBanner": {
99
"color": "#617A91",

src/core/prompts/system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ npm run build
633633
634634
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.
635635
636-
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.
636+
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=[].
637637
638638
\`\`\`json
639639
{

src/core/webview/ClineProvider.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
567567
}
568568
break
569569
}
570+
case "toggleMcpServer": {
571+
try {
572+
await this.mcpHub?.toggleServerDisabled(
573+
message.serverName!,
574+
message.disabled!
575+
)
576+
} catch (error) {
577+
console.error(`Failed to toggle MCP server ${message.serverName}:`, error)
578+
}
579+
break
580+
}
570581
// Add more switch case statements here as more webview message commands
571582
// are created within the webview context (i.e. inside media/main.js)
572583
case "playSound":

src/services/mcp/McpHub.ts

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ const StdioConfigSchema = z.object({
3939
command: z.string(),
4040
args: z.array(z.string()).optional(),
4141
env: z.record(z.string()).optional(),
42-
alwaysAllow: AlwaysAllowSchema.optional()
42+
alwaysAllow: AlwaysAllowSchema.optional(),
43+
disabled: z.boolean().optional()
4344
})
4445

4546
const McpSettingsSchema = z.object({
@@ -61,7 +62,10 @@ export class McpHub {
6162
}
6263

6364
getServers(): McpServer[] {
64-
return this.connections.map((conn) => conn.server)
65+
// Only return enabled servers
66+
return this.connections
67+
.filter((conn) => !conn.server.disabled)
68+
.map((conn) => conn.server)
6569
}
6670

6771
async getMcpServersPath(): Promise<string> {
@@ -117,9 +121,7 @@ export class McpHub {
117121
return
118122
}
119123
try {
120-
vscode.window.showInformationMessage("Updating MCP servers...")
121124
await this.updateServerConnections(result.data.mcpServers || {})
122-
vscode.window.showInformationMessage("MCP servers updated")
123125
} catch (error) {
124126
console.error("Failed to process MCP settings change:", error)
125127
}
@@ -202,11 +204,13 @@ export class McpHub {
202204
}
203205

204206
// valid schema
207+
const parsedConfig = StdioConfigSchema.parse(config)
205208
const connection: McpConnection = {
206209
server: {
207210
name,
208211
config: JSON.stringify(config),
209212
status: "connecting",
213+
disabled: parsedConfig.disabled,
210214
},
211215
client,
212216
transport,
@@ -466,13 +470,89 @@ export class McpHub {
466470
})
467471
}
468472

469-
// Using server
473+
// Public methods for server management
474+
475+
public async toggleServerDisabled(serverName: string, disabled: boolean): Promise<void> {
476+
let settingsPath: string
477+
try {
478+
settingsPath = await this.getMcpSettingsFilePath()
479+
480+
// Ensure the settings file exists and is accessible
481+
try {
482+
await fs.access(settingsPath)
483+
} catch (error) {
484+
console.error('Settings file not accessible:', error)
485+
throw new Error('Settings file not accessible')
486+
}
487+
const content = await fs.readFile(settingsPath, "utf-8")
488+
const config = JSON.parse(content)
489+
490+
// Validate the config structure
491+
if (!config || typeof config !== 'object') {
492+
throw new Error('Invalid config structure')
493+
}
494+
495+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
496+
config.mcpServers = {}
497+
}
498+
499+
if (config.mcpServers[serverName]) {
500+
// Create a new server config object to ensure clean structure
501+
const serverConfig = {
502+
...config.mcpServers[serverName],
503+
disabled
504+
}
505+
506+
// Ensure required fields exist
507+
if (!serverConfig.alwaysAllow) {
508+
serverConfig.alwaysAllow = []
509+
}
510+
511+
config.mcpServers[serverName] = serverConfig
512+
513+
// Write the entire config back
514+
const updatedConfig = {
515+
mcpServers: config.mcpServers
516+
}
517+
518+
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
519+
520+
const connection = this.connections.find(conn => conn.server.name === serverName)
521+
if (connection) {
522+
try {
523+
connection.server.disabled = disabled
524+
525+
// Only refresh capabilities if connected
526+
if (connection.server.status === "connected") {
527+
connection.server.tools = await this.fetchToolsList(serverName)
528+
connection.server.resources = await this.fetchResourcesList(serverName)
529+
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(serverName)
530+
}
531+
} catch (error) {
532+
console.error(`Failed to refresh capabilities for ${serverName}:`, error)
533+
}
534+
}
535+
536+
await this.notifyWebviewOfServerChanges()
537+
}
538+
} catch (error) {
539+
console.error("Failed to update server disabled state:", error)
540+
if (error instanceof Error) {
541+
console.error("Error details:", error.message, error.stack)
542+
}
543+
vscode.window.showErrorMessage(`Failed to update server state: ${error instanceof Error ? error.message : String(error)}`)
544+
throw error
545+
}
546+
}
470547

471548
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
472549
const connection = this.connections.find((conn) => conn.server.name === serverName)
473550
if (!connection) {
474551
throw new Error(`No connection found for server: ${serverName}`)
475552
}
553+
if (connection.server.disabled) {
554+
throw new Error(`Server "${serverName}" is disabled`)
555+
}
476556
return await connection.client.request(
477557
{
478558
method: "resources/read",
@@ -495,6 +575,9 @@ export class McpHub {
495575
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
496576
)
497577
}
578+
if (connection.server.disabled) {
579+
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
580+
}
498581

499582
return await connection.client.request(
500583
{

src/services/mcp/__tests__/McpHub.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,103 @@ describe('McpHub', () => {
148148
})
149149
})
150150

151+
describe('server disabled state', () => {
152+
it('should toggle server disabled state', async () => {
153+
const mockConfig = {
154+
mcpServers: {
155+
'test-server': {
156+
command: 'node',
157+
args: ['test.js'],
158+
disabled: false
159+
}
160+
}
161+
}
162+
163+
// Mock reading initial config
164+
;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
165+
166+
await mcpHub.toggleServerDisabled('test-server', true)
167+
168+
// Verify the config was updated correctly
169+
const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
170+
const writtenConfig = JSON.parse(writeCall[1])
171+
expect(writtenConfig.mcpServers['test-server'].disabled).toBe(true)
172+
})
173+
174+
it('should filter out disabled servers from getServers', () => {
175+
const mockConnections: McpConnection[] = [
176+
{
177+
server: {
178+
name: 'enabled-server',
179+
config: '{}',
180+
status: 'connected',
181+
disabled: false
182+
},
183+
client: {} as any,
184+
transport: {} as any
185+
},
186+
{
187+
server: {
188+
name: 'disabled-server',
189+
config: '{}',
190+
status: 'connected',
191+
disabled: true
192+
},
193+
client: {} as any,
194+
transport: {} as any
195+
}
196+
]
197+
198+
mcpHub.connections = mockConnections
199+
const servers = mcpHub.getServers()
200+
201+
expect(servers.length).toBe(1)
202+
expect(servers[0].name).toBe('enabled-server')
203+
})
204+
205+
it('should prevent calling tools on disabled servers', async () => {
206+
const mockConnection: McpConnection = {
207+
server: {
208+
name: 'disabled-server',
209+
config: '{}',
210+
status: 'connected',
211+
disabled: true
212+
},
213+
client: {
214+
request: jest.fn().mockResolvedValue({ result: 'success' })
215+
} as any,
216+
transport: {} as any
217+
}
218+
219+
mcpHub.connections = [mockConnection]
220+
221+
await expect(mcpHub.callTool('disabled-server', 'some-tool', {}))
222+
.rejects
223+
.toThrow('Server "disabled-server" is disabled and cannot be used')
224+
})
225+
226+
it('should prevent reading resources from disabled servers', async () => {
227+
const mockConnection: McpConnection = {
228+
server: {
229+
name: 'disabled-server',
230+
config: '{}',
231+
status: 'connected',
232+
disabled: true
233+
},
234+
client: {
235+
request: jest.fn()
236+
} as any,
237+
transport: {} as any
238+
}
239+
240+
mcpHub.connections = [mockConnection]
241+
242+
await expect(mcpHub.readResource('disabled-server', 'some/uri'))
243+
.rejects
244+
.toThrow('Server "disabled-server" is disabled')
245+
})
246+
})
247+
151248
describe('callTool', () => {
152249
it('should execute tool successfully', async () => {
153250
// Mock the connection with a minimal client implementation

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export interface WebviewMessage {
3636
| "openMcpSettings"
3737
| "restartMcpServer"
3838
| "toggleToolAlwaysAllow"
39+
| "toggleMcpServer"
3940
text?: string
41+
disabled?: boolean
4042
askResponse?: ClineAskResponse
4143
apiConfiguration?: ApiConfiguration
4244
images?: string[]

src/shared/mcp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type McpServer = {
66
tools?: McpTool[]
77
resources?: McpResource[]
88
resourceTemplates?: McpResourceTemplate[]
9+
disabled?: boolean
910
}
1011

1112
export type McpTool = {

0 commit comments

Comments
 (0)