add toggle for disable mcp server tool from prompt

This commit is contained in:
Александр Родионов 2025-05-13 15:08:47 +03:00 committed by Daniel Riccio
parent 44f3a8418b
commit 04b2887ab4
11 changed files with 254 additions and 14 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": minor
---
new feature allowing users to toggle whether an individual MCP (Model Context Protocol) tool is included in the context provided to the AI model

View file

@ -168,6 +168,7 @@ const mockFs = {
args: ["test.js"],
disabled: false,
alwaysAllow: ["existing-tool"],
disabledForPromptTools: [],
},
},
}),

View file

@ -50,6 +50,7 @@ Common configuration options for both types:
- \`disabled\`: (optional) Set to true to temporarily disable the server
- \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60)
- \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation
- \`disabledForPromptTools\`: (optional) Array of tool names that don't includes to system prompt and won't be used
### Example Local MCP Server
@ -276,7 +277,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.
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=[].
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 disabledForPromptTools=[].
\`\`\`json
{

View file

@ -17,6 +17,7 @@ export async function getMcpServersSection(
.filter((server) => server.status === "connected")
.map((server) => {
const tools = server.tools
?.filter((tool) => tool.enabledForPrompt !== false)
?.map((tool) => {
const schemaStr = tool.inputSchema
? ` Input Schema:

View file

@ -574,6 +574,23 @@ export const webviewMessageHandler = async (
}
break
}
case "toggleToolEnabledForPrompt": {
try {
await provider
.getMcpHub()
?.toggleToolEnabledForPrompt(
message.serverName!,
message.source as "global" | "project",
message.toolName!,
Boolean(message.isEnabled),
)
} catch (error) {
provider.log(
`Failed to toggle enabled for prompt for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
}
break
}
case "toggleMcpServer": {
try {
await provider

View file

@ -45,6 +45,7 @@ const BaseConfigSchema = z.object({
timeout: z.number().min(1).max(3600).optional().default(60),
alwaysAllow: z.array(z.string()).default([]),
watchPaths: z.array(z.string()).optional(), // paths to watch for changes and restart server
disabledForPromptTools: z.array(z.string()).default([]),
})
// Custom error messages for better user feedback
@ -819,34 +820,39 @@ export class McpHub {
const actualSource = connection.server.source || "global"
let configPath: string
let alwaysAllowConfig: string[] = []
let disabledForPromptToolsList: string[] = []
// Read from the appropriate config file based on the actual source
try {
let serverConfigData: any
if (actualSource === "project") {
// Get project MCP config path
const projectMcpPath = await this.getProjectMcpPath()
if (projectMcpPath) {
configPath = projectMcpPath
const content = await fs.readFile(configPath, "utf-8")
const config = JSON.parse(content)
alwaysAllowConfig = config.mcpServers?.[serverName]?.alwaysAllow || []
serverConfigData = JSON.parse(content)
}
} else {
// Get global MCP settings path
configPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(configPath, "utf-8")
const config = JSON.parse(content)
alwaysAllowConfig = config.mcpServers?.[serverName]?.alwaysAllow || []
serverConfigData = JSON.parse(content)
}
if (serverConfigData) {
alwaysAllowConfig = serverConfigData.mcpServers?.[serverName]?.alwaysAllow || []
disabledForPromptToolsList = serverConfigData.mcpServers?.[serverName]?.disabledForPromptTools || []
}
} catch (error) {
console.error(`Failed to read alwaysAllow config for ${serverName}:`, error)
// Continue with empty alwaysAllowConfig
console.error(`Failed to read tool configuration for ${serverName}:`, error)
// Continue with empty configs
}
// Mark tools as always allowed based on settings
// Mark tools as always allowed and enabled for prompt based on settings
const tools = (response?.tools || []).map((tool) => ({
...tool,
alwaysAllow: alwaysAllowConfig.includes(tool.name),
enabledForPrompt: !disabledForPromptToolsList.includes(tool.name),
}))
return tools
@ -1550,6 +1556,74 @@ export class McpHub {
connection.server.tools = await this.fetchToolsList(serverName, source)
await this.notifyWebviewOfServerChanges()
}
} catch (error) {
this.showErrorMessage(
`Failed to toggle always allow for tool "${toolName}" on server "${serverName}" with source "${source}"`,
error,
)
throw error
}
}
async toggleToolEnabledForPrompt(
serverName: string,
source: "global" | "project",
toolName: string,
isEnabled: boolean,
): Promise<void> {
try {
const connection = this.findConnection(serverName, source)
if (!connection) {
throw new Error(`Server ${serverName} with source ${source} not found`)
}
let configPath: string
if (source === "project") {
const projectMcpPath = await this.getProjectMcpPath()
if (!projectMcpPath) {
throw new Error("Project MCP configuration file not found")
}
configPath = projectMcpPath
} else {
configPath = await this.getMcpSettingsFilePath()
}
const normalizedPath = process.platform === "win32" ? configPath.replace(/\\/g, "/") : configPath
const content = await fs.readFile(normalizedPath, "utf-8")
const config = JSON.parse(content)
if (!config.mcpServers) {
config.mcpServers = {}
}
if (!config.mcpServers[serverName]) {
// Initialize with minimal valid config if server entry doesn't exist
config.mcpServers[serverName] = {
type: "stdio",
command: "echo",
args: ["MCP server not fully configured"],
}
}
if (!config.mcpServers[serverName].disabledForPromptTools) {
config.mcpServers[serverName].disabledForPromptTools = []
}
const disabledList = config.mcpServers[serverName].disabledForPromptTools
const toolIndex = disabledList.indexOf(toolName)
if (!isEnabled && toolIndex === -1) {
// If tool should be disabled (not included in prompt) and is not in disabled list
disabledList.push(toolName)
} else if (isEnabled && toolIndex !== -1) {
// If tool should be enabled (included in prompt) and is in disabled list
disabledList.splice(toolIndex, 1)
}
await fs.writeFile(normalizedPath, JSON.stringify(config, null, 2))
if (connection) {
connection.server.tools = await this.fetchToolsList(serverName, source)
await this.notifyWebviewOfServerChanges()
}
} catch (error) {
this.showErrorMessage(`Failed to update always allow settings for tool ${toolName}`, error)
throw error // Re-throw to ensure the error is properly handled

View file

@ -100,6 +100,7 @@ describe("McpHub", () => {
command: "node",
args: ["test.js"],
alwaysAllow: ["allowed-tool"],
disabledForPromptTools: ["disabled-tool"],
},
},
}),
@ -257,6 +258,107 @@ describe("McpHub", () => {
})
})
describe("toggleToolEnabledForPrompt", () => {
it("should add tool to disabledForPromptTools list when enabling", async () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
disabledForPromptTools: [],
},
},
}
// Mock reading initial config
;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
await mcpHub.toggleToolEnabledForPrompt("test-server", "global", "new-tool", false)
// Verify the config was updated correctly
const writeCalls = (fs.writeFile as jest.Mock).mock.calls
expect(writeCalls.length).toBeGreaterThan(0)
// Find the write call
const callToUse = writeCalls[writeCalls.length - 1]
expect(callToUse).toBeTruthy()
// The path might be normalized differently on different platforms,
// so we'll just check that we have a call with valid content
const writtenConfig = JSON.parse(callToUse[1])
expect(writtenConfig.mcpServers).toBeDefined()
expect(writtenConfig.mcpServers["test-server"]).toBeDefined()
expect(Array.isArray(writtenConfig.mcpServers["test-server"].enabledForPrompt)).toBe(false)
expect(writtenConfig.mcpServers["test-server"].disabledForPromptTools).toContain("new-tool")
})
it("should remove tool from disabledForPromptTools list when disabling", async () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
disabledForPromptTools: ["existing-tool"],
},
},
}
// Mock reading initial config
;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
await mcpHub.toggleToolEnabledForPrompt("test-server", "global", "existing-tool", true)
// Verify the config was updated correctly
const writeCalls = (fs.writeFile as jest.Mock).mock.calls
expect(writeCalls.length).toBeGreaterThan(0)
// Find the write call
const callToUse = writeCalls[writeCalls.length - 1]
expect(callToUse).toBeTruthy()
// The path might be normalized differently on different platforms,
// so we'll just check that we have a call with valid content
const writtenConfig = JSON.parse(callToUse[1])
expect(writtenConfig.mcpServers).toBeDefined()
expect(writtenConfig.mcpServers["test-server"]).toBeDefined()
expect(Array.isArray(writtenConfig.mcpServers["test-server"].enabledForPrompt)).toBe(false)
expect(writtenConfig.mcpServers["test-server"].disabledForPromptTools).not.toContain("existing-tool")
})
it("should initialize disabledForPromptTools if it does not exist", async () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
},
},
}
// Mock reading initial config
;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig))
// Call with false because of "true" is default value
await mcpHub.toggleToolEnabledForPrompt("test-server", "global", "new-tool", false)
// Verify the config was updated with initialized disabledForPromptTools
// Find the write call with the normalized path
const normalizedSettingsPath = "/mock/settings/path/cline_mcp_settings.json"
const writeCalls = (fs.writeFile as jest.Mock).mock.calls
// Find the write call with the normalized path
const writeCall = writeCalls.find((call) => call[0] === normalizedSettingsPath)
const callToUse = writeCall || writeCalls[0]
const writtenConfig = JSON.parse(callToUse[1])
expect(writtenConfig.mcpServers["test-server"].disabledForPromptTools).toBeDefined()
expect(writtenConfig.mcpServers["test-server"].disabledForPromptTools).toContain("new-tool")
})
})
describe("server disabled state", () => {
it("should toggle server disabled state", async () => {
const mockConfig = {

View file

@ -91,6 +91,7 @@ export interface WebviewMessage {
| "restartMcpServer"
| "refreshAllMcpServers"
| "toggleToolAlwaysAllow"
| "toggleToolEnabledForPrompt"
| "toggleMcpServer"
| "updateMcpTimeout"
| "fuzzyMatchThreshold"
@ -182,6 +183,7 @@ export interface WebviewMessage {
serverName?: string
toolName?: string
alwaysAllow?: boolean
isEnabled?: boolean
mode?: Mode
promptMode?: PromptMode
customPrompt?: PromptComponent

View file

@ -25,6 +25,7 @@ export type McpTool = {
description?: string
inputSchema?: object
alwaysAllow?: boolean
enabledForPrompt?: boolean
}
export type McpResource = {

View file

@ -25,6 +25,17 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolR
})
}
const handleEnabledForPromptChange = () => {
if (!serverName) return
vscode.postMessage({
type: "toggleToolEnabledForPrompt",
serverName,
source: serverSource || "global",
toolName: tool.name,
isEnabled: !tool.enabledForPrompt,
})
}
return (
<div
key={tool.name}
@ -39,11 +50,35 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolR
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
<span style={{ fontWeight: 500 }}>{tool.name}</span>
</div>
{serverName && alwaysAllowMcp && (
<VSCodeCheckbox checked={tool.alwaysAllow} onChange={handleAlwaysAllowChange} data-tool={tool.name}>
{t("mcp:tool.alwaysAllow")}
</VSCodeCheckbox>
)}
<div className="flex items-center space-x-4">
{" "}
{/* Wrapper for checkboxes */}
{serverName && (
<div
role="switch"
aria-checked={tool.enabledForPrompt}
className={`flex items-center cursor-pointer rounded-full w-8 h-4 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
tool.enabledForPrompt ? "bg-blue-600" : "bg-gray-200 dark:bg-gray-700"
}`}
onClick={handleEnabledForPromptChange}
data-tool-prompt-toggle={tool.name}
title={t("mcp:tool.togglePromptInclusion")}>
<span
className={`pointer-events-none inline-block h-3 w-3 transform rounded-full bg-white shadow-lg ring-0 transition-transform duration-200 ease-in-out ${
tool.enabledForPrompt ? "translate-x-4" : "translate-x-1"
}`}
/>
</div>
)}
{serverName && alwaysAllowMcp && (
<VSCodeCheckbox
checked={tool.alwaysAllow}
onChange={handleAlwaysAllowChange}
data-tool={tool.name}>
{t("mcp:tool.alwaysAllow")}
</VSCodeCheckbox>
)}
</div>
</div>
{tool.description && (
<div

View file

@ -19,7 +19,8 @@
"tool": {
"alwaysAllow": "Always allow",
"parameters": "Parameters",
"noDescription": "No description"
"noDescription": "No description",
"togglePromptInclusion": "Toggle inclusion in prompt"
},
"tabs": {
"tools": "Tools",