Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slimy-years-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

new feature allowing users to toggle whether an individual MCP (Model Context Protocol) tool is included in the context provided to the AI model
1 change: 1 addition & 0 deletions src/__mocks__/fs/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const mockFs = {
args: ["test.js"],
disabled: false,
alwaysAllow: ["existing-tool"],
disabledTools: [],
},
},
}),
Expand Down
3 changes: 2 additions & 1 deletion src/core/prompts/instructions/create-mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
- \`disabledTools\`: (optional) Array of tool names that are not included in the system prompt and won't be used

### Example Local MCP Server

Expand Down Expand Up @@ -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 disabledTools=[].

\`\`\`json
{
Expand Down
1 change: 1 addition & 0 deletions src/core/prompts/sections/mcp-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,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
Expand Down
169 changes: 103 additions & 66 deletions src/services/mcp/McpHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
disabledTools: z.array(z.string()).default([]),
})

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

// Read from the appropriate config file based on the actual source
try {
let serverConfigData: Record<string, 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 || []
disabledToolsList = serverConfigData.mcpServers?.[serverName]?.disabledTools || []
}
} 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: !disabledToolsList.includes(tool.name),
}))

return tools
Expand Down Expand Up @@ -1491,83 +1497,114 @@ export class McpHub {
)
}

async toggleToolAlwaysAllow(
/**
* Helper method to update a specific tool list (alwaysAllow or disabledTools)
* in the appropriate settings file.
* @param serverName The name of the server to update
* @param source Whether to update the global or project config
* @param toolName The name of the tool to add or remove
* @param listName The name of the list to modify ("alwaysAllow" or "disabledTools")
* @param addTool Whether to add (true) or remove (false) the tool from the list
*/
private async updateServerToolList(
serverName: string,
source: "global" | "project",
toolName: string,
shouldAllow: boolean,
listName: "alwaysAllow" | "disabledTools",
addTool: boolean,
): Promise<void> {
try {
// Find the connection with matching name and source
const connection = this.findConnection(serverName, source)
// Find the connection with matching name and source
const connection = this.findConnection(serverName, source)

if (!connection) {
throw new Error(`Server ${serverName} with source ${source} not found`)
}
if (!connection) {
throw new Error(`Server ${serverName} with source ${source} not found`)
}

// Determine the correct config path based on the source
let configPath: string
if (source === "project") {
// Get project MCP config path
const projectMcpPath = await this.getProjectMcpPath()
if (!projectMcpPath) {
throw new Error("Project MCP configuration file not found")
}
configPath = projectMcpPath
} else {
// Get global MCP settings path
configPath = await this.getMcpSettingsFilePath()
// Determine the correct config path based on the source
let configPath: string
if (source === "project") {
// Get project MCP config path
const projectMcpPath = await this.getProjectMcpPath()
if (!projectMcpPath) {
throw new Error("Project MCP configuration file not found")
}
configPath = projectMcpPath
} else {
// Get global MCP settings path
configPath = await this.getMcpSettingsFilePath()
}

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

// Read the appropriate config file
const content = await fs.readFile(normalizedPath, "utf-8")
const config = JSON.parse(content)
// Read the appropriate config file
const content = await fs.readFile(normalizedPath, "utf-8")
const config = JSON.parse(content)

// Initialize mcpServers if it doesn't exist
if (!config.mcpServers) {
config.mcpServers = {}
}
if (!config.mcpServers) {
config.mcpServers = {}
}

// Initialize server config if it doesn't exist
if (!config.mcpServers[serverName]) {
config.mcpServers[serverName] = {
type: "stdio",
command: "node",
args: [], // Default to an empty array; can be set later if needed
}
if (!config.mcpServers[serverName]) {
config.mcpServers[serverName] = {
type: "stdio",
command: "node",
args: [], // Default to an empty array; can be set later if needed
}
}

// Initialize alwaysAllow if it doesn't exist
if (!config.mcpServers[serverName].alwaysAllow) {
config.mcpServers[serverName].alwaysAllow = []
}
if (!config.mcpServers[serverName][listName]) {
config.mcpServers[serverName][listName] = []
}

const alwaysAllow = config.mcpServers[serverName].alwaysAllow
const toolIndex = alwaysAllow.indexOf(toolName)
const targetList = config.mcpServers[serverName][listName]
const toolIndex = targetList.indexOf(toolName)

if (shouldAllow && toolIndex === -1) {
// Add tool to always allow list
alwaysAllow.push(toolName)
} else if (!shouldAllow && toolIndex !== -1) {
// Remove tool from always allow list
alwaysAllow.splice(toolIndex, 1)
}
if (addTool && toolIndex === -1) {
targetList.push(toolName)
} else if (!addTool && toolIndex !== -1) {
targetList.splice(toolIndex, 1)
}

// Write updated config back to file
await fs.writeFile(normalizedPath, JSON.stringify(config, null, 2))
await fs.writeFile(normalizedPath, JSON.stringify(config, null, 2))

// Update the tools list to reflect the change
if (connection) {
// Explicitly pass the source to ensure we're updating the correct server's tools
connection.server.tools = await this.fetchToolsList(serverName, source)
await this.notifyWebviewOfServerChanges()
}
if (connection) {
connection.server.tools = await this.fetchToolsList(serverName, source)
await this.notifyWebviewOfServerChanges()
}
}

async toggleToolAlwaysAllow(
serverName: string,
source: "global" | "project",
toolName: string,
shouldAllow: boolean,
): Promise<void> {
try {
await this.updateServerToolList(serverName, source, toolName, "alwaysAllow", shouldAllow)
} 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 {
// When isEnabled is true, we want to remove the tool from the disabledTools list.
// When isEnabled is false, we want to add the tool to the disabledTools list.
const addToolToDisabledList = !isEnabled
await this.updateServerToolList(serverName, source, toolName, "disabledTools", addToolToDisabledList)
} catch (error) {
this.showErrorMessage(`Failed to update always allow settings for tool ${toolName}`, error)
this.showErrorMessage(`Failed to update settings for tool ${toolName}`, error)
throw error // Re-throw to ensure the error is properly handled
}
}
Expand Down
Loading
Loading