Skip to content

Commit d99ae58

Browse files
Rexarriorellipsis-dev[bot]daniel-lxsmrubens
authored
feature: add toggle for disable mcp server tool from prompt (#3551)
* add toggle for disable mcp server tool from prompt * languages * fix error message * Update src/core/prompts/instructions/create-mcp-server.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix build * typo * refactor: make the switch style consistent * respect review * refactor: improve layout and styling in McpToolRow component * fix: update new mcp tests * tailwind css for disabledTool swipper * id locale (from main) * fix: improve error message for updating tool settings * fix: specify type for serverConfigData as Record<string, any> * fix: enhance UI layout and improve accessibility for tool controls * fix: migrate jest.Mock to vitest Mock type * Update .changeset/slimy-years-smell.md * fix: replace enabled switch with button --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Daniel <[email protected]> Co-authored-by: Daniel Riccio <[email protected]> Co-authored-by: Matt Rubens <[email protected]>
1 parent 805fe70 commit d99ae58

File tree

29 files changed

+379
-140
lines changed

29 files changed

+379
-140
lines changed

.changeset/slimy-years-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
new feature allowing users to toggle whether an individual MCP (Model Context Protocol) tool is included in the context provided to the AI model

src/__mocks__/fs/promises.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ const mockFs = {
168168
args: ["test.js"],
169169
disabled: false,
170170
alwaysAllow: ["existing-tool"],
171+
disabledTools: [],
171172
},
172173
},
173174
}),

src/core/prompts/instructions/create-mcp-server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Common configuration options for both types:
5050
- \`disabled\`: (optional) Set to true to temporarily disable the server
5151
- \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60)
5252
- \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation
53+
- \`disabledTools\`: (optional) Array of tool names that are not included in the system prompt and won't be used
5354
5455
### Example Local MCP Server
5556
@@ -276,7 +277,7 @@ npm run build
276277
277278
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.
278279
279-
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=[].
280+
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false, alwaysAllow=[] and disabledTools=[].
280281
281282
\`\`\`json
282283
{

src/core/prompts/sections/mcp-servers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export async function getMcpServersSection(
1717
.filter((server) => server.status === "connected")
1818
.map((server) => {
1919
const tools = server.tools
20+
?.filter((tool) => tool.enabledForPrompt !== false)
2021
?.map((tool) => {
2122
const schemaStr = tool.inputSchema
2223
? ` Input Schema:

src/core/webview/webviewMessageHandler.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,23 @@ export const webviewMessageHandler = async (
596596
}
597597
break
598598
}
599+
case "toggleToolEnabledForPrompt": {
600+
try {
601+
await provider
602+
.getMcpHub()
603+
?.toggleToolEnabledForPrompt(
604+
message.serverName!,
605+
message.source as "global" | "project",
606+
message.toolName!,
607+
Boolean(message.isEnabled),
608+
)
609+
} catch (error) {
610+
provider.log(
611+
`Failed to toggle enabled for prompt for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
612+
)
613+
}
614+
break
615+
}
599616
case "toggleMcpServer": {
600617
try {
601618
await provider

src/services/mcp/McpHub.ts

Lines changed: 103 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const BaseConfigSchema = z.object({
4545
timeout: z.number().min(1).max(3600).optional().default(60),
4646
alwaysAllow: z.array(z.string()).default([]),
4747
watchPaths: z.array(z.string()).optional(), // paths to watch for changes and restart server
48+
disabledTools: z.array(z.string()).default([]),
4849
})
4950

5051
// Custom error messages for better user feedback
@@ -835,34 +836,39 @@ export class McpHub {
835836
const actualSource = connection.server.source || "global"
836837
let configPath: string
837838
let alwaysAllowConfig: string[] = []
839+
let disabledToolsList: string[] = []
838840

839841
// Read from the appropriate config file based on the actual source
840842
try {
843+
let serverConfigData: Record<string, any> = {}
841844
if (actualSource === "project") {
842845
// Get project MCP config path
843846
const projectMcpPath = await this.getProjectMcpPath()
844847
if (projectMcpPath) {
845848
configPath = projectMcpPath
846849
const content = await fs.readFile(configPath, "utf-8")
847-
const config = JSON.parse(content)
848-
alwaysAllowConfig = config.mcpServers?.[serverName]?.alwaysAllow || []
850+
serverConfigData = JSON.parse(content)
849851
}
850852
} else {
851853
// Get global MCP settings path
852854
configPath = await this.getMcpSettingsFilePath()
853855
const content = await fs.readFile(configPath, "utf-8")
854-
const config = JSON.parse(content)
855-
alwaysAllowConfig = config.mcpServers?.[serverName]?.alwaysAllow || []
856+
serverConfigData = JSON.parse(content)
857+
}
858+
if (serverConfigData) {
859+
alwaysAllowConfig = serverConfigData.mcpServers?.[serverName]?.alwaysAllow || []
860+
disabledToolsList = serverConfigData.mcpServers?.[serverName]?.disabledTools || []
856861
}
857862
} catch (error) {
858-
console.error(`Failed to read alwaysAllow config for ${serverName}:`, error)
859-
// Continue with empty alwaysAllowConfig
863+
console.error(`Failed to read tool configuration for ${serverName}:`, error)
864+
// Continue with empty configs
860865
}
861866

862-
// Mark tools as always allowed based on settings
867+
// Mark tools as always allowed and enabled for prompt based on settings
863868
const tools = (response?.tools || []).map((tool) => ({
864869
...tool,
865870
alwaysAllow: alwaysAllowConfig.includes(tool.name),
871+
enabledForPrompt: !disabledToolsList.includes(tool.name),
866872
}))
867873

868874
return tools
@@ -1491,83 +1497,114 @@ export class McpHub {
14911497
)
14921498
}
14931499

1494-
async toggleToolAlwaysAllow(
1500+
/**
1501+
* Helper method to update a specific tool list (alwaysAllow or disabledTools)
1502+
* in the appropriate settings file.
1503+
* @param serverName The name of the server to update
1504+
* @param source Whether to update the global or project config
1505+
* @param toolName The name of the tool to add or remove
1506+
* @param listName The name of the list to modify ("alwaysAllow" or "disabledTools")
1507+
* @param addTool Whether to add (true) or remove (false) the tool from the list
1508+
*/
1509+
private async updateServerToolList(
14951510
serverName: string,
14961511
source: "global" | "project",
14971512
toolName: string,
1498-
shouldAllow: boolean,
1513+
listName: "alwaysAllow" | "disabledTools",
1514+
addTool: boolean,
14991515
): Promise<void> {
1500-
try {
1501-
// Find the connection with matching name and source
1502-
const connection = this.findConnection(serverName, source)
1516+
// Find the connection with matching name and source
1517+
const connection = this.findConnection(serverName, source)
15031518

1504-
if (!connection) {
1505-
throw new Error(`Server ${serverName} with source ${source} not found`)
1506-
}
1519+
if (!connection) {
1520+
throw new Error(`Server ${serverName} with source ${source} not found`)
1521+
}
15071522

1508-
// Determine the correct config path based on the source
1509-
let configPath: string
1510-
if (source === "project") {
1511-
// Get project MCP config path
1512-
const projectMcpPath = await this.getProjectMcpPath()
1513-
if (!projectMcpPath) {
1514-
throw new Error("Project MCP configuration file not found")
1515-
}
1516-
configPath = projectMcpPath
1517-
} else {
1518-
// Get global MCP settings path
1519-
configPath = await this.getMcpSettingsFilePath()
1523+
// Determine the correct config path based on the source
1524+
let configPath: string
1525+
if (source === "project") {
1526+
// Get project MCP config path
1527+
const projectMcpPath = await this.getProjectMcpPath()
1528+
if (!projectMcpPath) {
1529+
throw new Error("Project MCP configuration file not found")
15201530
}
1531+
configPath = projectMcpPath
1532+
} else {
1533+
// Get global MCP settings path
1534+
configPath = await this.getMcpSettingsFilePath()
1535+
}
15211536

1522-
// Normalize path for cross-platform compatibility
1523-
// Use a consistent path format for both reading and writing
1524-
const normalizedPath = process.platform === "win32" ? configPath.replace(/\\/g, "/") : configPath
1537+
// Normalize path for cross-platform compatibility
1538+
// Use a consistent path format for both reading and writing
1539+
const normalizedPath = process.platform === "win32" ? configPath.replace(/\\/g, "/") : configPath
15251540

1526-
// Read the appropriate config file
1527-
const content = await fs.readFile(normalizedPath, "utf-8")
1528-
const config = JSON.parse(content)
1541+
// Read the appropriate config file
1542+
const content = await fs.readFile(normalizedPath, "utf-8")
1543+
const config = JSON.parse(content)
15291544

1530-
// Initialize mcpServers if it doesn't exist
1531-
if (!config.mcpServers) {
1532-
config.mcpServers = {}
1533-
}
1545+
if (!config.mcpServers) {
1546+
config.mcpServers = {}
1547+
}
15341548

1535-
// Initialize server config if it doesn't exist
1536-
if (!config.mcpServers[serverName]) {
1537-
config.mcpServers[serverName] = {
1538-
type: "stdio",
1539-
command: "node",
1540-
args: [], // Default to an empty array; can be set later if needed
1541-
}
1549+
if (!config.mcpServers[serverName]) {
1550+
config.mcpServers[serverName] = {
1551+
type: "stdio",
1552+
command: "node",
1553+
args: [], // Default to an empty array; can be set later if needed
15421554
}
1555+
}
15431556

1544-
// Initialize alwaysAllow if it doesn't exist
1545-
if (!config.mcpServers[serverName].alwaysAllow) {
1546-
config.mcpServers[serverName].alwaysAllow = []
1547-
}
1557+
if (!config.mcpServers[serverName][listName]) {
1558+
config.mcpServers[serverName][listName] = []
1559+
}
15481560

1549-
const alwaysAllow = config.mcpServers[serverName].alwaysAllow
1550-
const toolIndex = alwaysAllow.indexOf(toolName)
1561+
const targetList = config.mcpServers[serverName][listName]
1562+
const toolIndex = targetList.indexOf(toolName)
15511563

1552-
if (shouldAllow && toolIndex === -1) {
1553-
// Add tool to always allow list
1554-
alwaysAllow.push(toolName)
1555-
} else if (!shouldAllow && toolIndex !== -1) {
1556-
// Remove tool from always allow list
1557-
alwaysAllow.splice(toolIndex, 1)
1558-
}
1564+
if (addTool && toolIndex === -1) {
1565+
targetList.push(toolName)
1566+
} else if (!addTool && toolIndex !== -1) {
1567+
targetList.splice(toolIndex, 1)
1568+
}
15591569

1560-
// Write updated config back to file
1561-
await fs.writeFile(normalizedPath, JSON.stringify(config, null, 2))
1570+
await fs.writeFile(normalizedPath, JSON.stringify(config, null, 2))
15621571

1563-
// Update the tools list to reflect the change
1564-
if (connection) {
1565-
// Explicitly pass the source to ensure we're updating the correct server's tools
1566-
connection.server.tools = await this.fetchToolsList(serverName, source)
1567-
await this.notifyWebviewOfServerChanges()
1568-
}
1572+
if (connection) {
1573+
connection.server.tools = await this.fetchToolsList(serverName, source)
1574+
await this.notifyWebviewOfServerChanges()
1575+
}
1576+
}
1577+
1578+
async toggleToolAlwaysAllow(
1579+
serverName: string,
1580+
source: "global" | "project",
1581+
toolName: string,
1582+
shouldAllow: boolean,
1583+
): Promise<void> {
1584+
try {
1585+
await this.updateServerToolList(serverName, source, toolName, "alwaysAllow", shouldAllow)
1586+
} catch (error) {
1587+
this.showErrorMessage(
1588+
`Failed to toggle always allow for tool "${toolName}" on server "${serverName}" with source "${source}"`,
1589+
error,
1590+
)
1591+
throw error
1592+
}
1593+
}
1594+
1595+
async toggleToolEnabledForPrompt(
1596+
serverName: string,
1597+
source: "global" | "project",
1598+
toolName: string,
1599+
isEnabled: boolean,
1600+
): Promise<void> {
1601+
try {
1602+
// When isEnabled is true, we want to remove the tool from the disabledTools list.
1603+
// When isEnabled is false, we want to add the tool to the disabledTools list.
1604+
const addToolToDisabledList = !isEnabled
1605+
await this.updateServerToolList(serverName, source, toolName, "disabledTools", addToolToDisabledList)
15691606
} catch (error) {
1570-
this.showErrorMessage(`Failed to update always allow settings for tool ${toolName}`, error)
1607+
this.showErrorMessage(`Failed to update settings for tool ${toolName}`, error)
15711608
throw error // Re-throw to ensure the error is properly handled
15721609
}
15731610
}

0 commit comments

Comments
 (0)