Skip to content

Commit 3e3fae2

Browse files
committed
feat: add fine-grained auto-approval for servers and tools
- Add autoApprove config field (boolean or string array) - Support server-level and tool-specific auto-approval - Add 'a' keymap to toggle auto-approval in UI - Show visual indicators for auto-approval status - Resources are always auto-approved by default
1 parent f3f6fd5 commit 3e3fae2

File tree

6 files changed

+282
-27
lines changed

6 files changed

+282
-27
lines changed

lua/mcphub/extensions/avante.lua

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,10 @@ function M.mcp_tool()
7171
if #params.errors > 0 then
7272
return on_complete(nil, table.concat(params.errors, "\n"))
7373
end
74-
local auto_approve = vim.g.mcphub_auto_approve == true
74+
-- Check both global and server-specific auto-approval
75+
local auto_approve = vim.g.mcphub_auto_approve == true or params.should_auto_approve
7576
if not auto_approve then
76-
local confirmed = shared.show_mcp_tool_prompt({
77-
action = params.action,
78-
server_name = params.server_name,
79-
tool_name = params.tool_name,
80-
uri = params.uri,
81-
arguments = params.arguments,
82-
})
77+
local confirmed = shared.show_mcp_tool_prompt(params)
8378
if not confirmed then
8479
return on_complete(nil, "User cancelled the operation")
8580
end

lua/mcphub/extensions/codecompanion/utils.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ function M.create_handler(action_name, has_function_calling, opts)
1818
})
1919
end
2020

21-
local auto_approve = (vim.g.mcphub_auto_approve == true) or (vim.g.codecompanion_auto_tool_mode == true)
21+
local auto_approve = (vim.g.mcphub_auto_approve == true)
22+
or (vim.g.codecompanion_auto_tool_mode == true)
23+
or params.should_auto_approve
2224
if not auto_approve then
2325
local confirmed = shared.show_mcp_tool_prompt(params)
2426
if not confirmed then

lua/mcphub/extensions/shared.lua

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,52 @@ local NuiLine = require("mcphub.utils.nuiline")
33
local Text = require("mcphub.utils.text")
44
local ui_utils = require("mcphub.utils.ui")
55

6-
---@alias MCPCallParams {errors: string[], action: MCPHubToolType, server_name: string, tool_name: string, uri: string, arguments: table}
6+
---@alias MCPCallParams {errors: string[], action: MCPHubToolType, server_name: string, tool_name: string, uri: string, arguments: table, should_auto_approve: boolean}
7+
8+
---Check if auto-approval is enabled for a specific tool/resource
9+
---@param server_name string
10+
---@param tool_name? string Tool name for tool calls (nil for resources)
11+
---@return boolean
12+
local function should_auto_approve(server_name, tool_name)
13+
-- Global auto-approve check
14+
if vim.g.mcphub_auto_approve == true then
15+
return true
16+
end
17+
18+
-- Get server config
19+
local State = require("mcphub.state")
20+
local native = require("mcphub.native")
21+
22+
local is_native = native.is_native_server(server_name)
23+
local server_config = is_native and State.native_servers_config[server_name] or State.servers_config[server_name]
24+
25+
if not server_config then
26+
return false
27+
end
28+
29+
local auto_approve = server_config.autoApprove
30+
if not auto_approve then
31+
return false
32+
end
33+
34+
-- If autoApprove is true, approve everything for this server
35+
if auto_approve == true then
36+
return true
37+
end
38+
39+
-- If autoApprove is an array, check if tool is in the list
40+
if type(auto_approve) == "table" and vim.islist(auto_approve) then
41+
-- For resources, always auto-approve (no explicit config needed)
42+
if not tool_name then
43+
return true
44+
end
45+
46+
-- For tools, check if the specific tool is in the list
47+
return vim.tbl_contains(auto_approve, tool_name)
48+
end
49+
50+
return false
51+
end
752

853
---@param params {server_name: string, tool_name: string, uri: string, tool_input: table | string}
954
---@param action_name MCPHubToolType
@@ -40,13 +85,19 @@ function M.parse_params(params, action_name)
4085
if action_name == "access_mcp_resource" and not uri then
4186
table.insert(errors, "uri is required")
4287
end
88+
89+
-- Check auto-approval based on server config and tool/resource
90+
local should_auto_approve_result =
91+
should_auto_approve(server_name, action_name == "use_mcp_tool" and tool_name or nil)
92+
4393
return {
4494
errors = errors,
4595
action = action_name or "nil",
4696
server_name = server_name or "nil",
4797
tool_name = tool_name or "nil",
4898
uri = uri or "nil",
4999
arguments = arguments or {},
100+
should_auto_approve = should_auto_approve_result,
50101
}
51102
end
52103

@@ -251,6 +302,11 @@ end
251302
---@param params MCPCallParams
252303
---@return boolean, boolean
253304
function M.show_mcp_tool_prompt(params)
305+
-- Check if we should auto-approve based on server/tool config
306+
if params.should_auto_approve then
307+
return true, false -- approved, not cancelled
308+
end
309+
254310
local action_name = params.action
255311
local server_name = params.server_name
256312
local tool_name = params.tool_name

lua/mcphub/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
---@field disabled_resources? string[]
2929
---@field disabled_resourceTemplates? string[]
3030
---@field custom_instructions? CustomMCPServerConfig.CustomInstructions
31+
---@field autoApprove? boolean|string[] -- true for all tools, array of tool names for specific tools
3132

3233
---@class MCPServerConfig: CustomMCPServerConfig
3334
---@field command? string

lua/mcphub/ui/views/main.lua

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,12 @@ function MainView:setup_active_mode()
453453
end,
454454
desc = "Toggle",
455455
},
456+
["a"] = {
457+
action = function()
458+
self:handle_auto_approve_toggle()
459+
end,
460+
desc = "Auto-approve",
461+
},
456462
["h"] = {
457463
action = function()
458464
self:handle_collapse()
@@ -483,6 +489,137 @@ function MainView:setup_active_mode()
483489
self:apply_keymaps()
484490
end
485491

492+
-- Helper function to get all tool names for a server
493+
local function get_server_tool_names(server_name)
494+
local tools = {}
495+
496+
-- Check if it's a native server
497+
local is_native = native.is_native_server(server_name)
498+
if is_native then
499+
local native_server = is_native
500+
for _, tool in ipairs(native_server.capabilities.tools or {}) do
501+
table.insert(tools, tool.name)
502+
end
503+
else
504+
-- Regular MCP server
505+
for _, server in ipairs(State.server_state.servers) do
506+
if server.name == server_name and server.capabilities then
507+
for _, tool in ipairs(server.capabilities.tools or {}) do
508+
table.insert(tools, tool.name)
509+
end
510+
break
511+
end
512+
end
513+
end
514+
515+
return tools
516+
end
517+
518+
-- Helper function to determine actual auto-approval status
519+
local function get_auto_approval_status(server_config, all_tools)
520+
local auto_approve = server_config.autoApprove
521+
522+
if auto_approve == true then
523+
return "all", #all_tools -- All tools auto-approved
524+
elseif type(auto_approve) == "table" and vim.islist(auto_approve) then
525+
if #auto_approve == 0 then
526+
return "none", 0
527+
elseif #auto_approve == #all_tools then
528+
return "all", #all_tools -- All tools are in the list
529+
else
530+
return "partial", #auto_approve -- Some tools auto-approved
531+
end
532+
else
533+
return "none", 0 -- No auto-approval
534+
end
535+
end
536+
537+
function MainView:handle_auto_approve_toggle()
538+
-- Get current line
539+
local cursor = vim.api.nvim_win_get_cursor(0)
540+
local line = cursor[1]
541+
542+
-- Get line info
543+
local type, context = self:get_line_info(line)
544+
if not type or not context or not State.hub_instance then
545+
return
546+
end
547+
548+
if type == "server" then
549+
-- Check if server is enabled
550+
if context.status == "disabled" or context.status == "disconnected" then
551+
return
552+
end
553+
554+
-- Toggle auto-approval for entire server
555+
local server_name = context.name
556+
local is_native = native.is_native_server(server_name)
557+
local server_config = (
558+
is_native and State.native_servers_config[server_name] or State.servers_config[server_name]
559+
) or {}
560+
561+
local all_tools = get_server_tool_names(server_name)
562+
local status, _ = get_auto_approval_status(server_config, all_tools)
563+
local new_auto_approve
564+
565+
if status == "all" then
566+
-- Currently auto-approving all tools, turn off
567+
new_auto_approve = {}
568+
else
569+
-- Currently partial or no auto-approval, enable for all
570+
new_auto_approve = vim.deepcopy(all_tools)
571+
end
572+
573+
State.hub_instance:update_server_config(server_name, {
574+
autoApprove = new_auto_approve,
575+
})
576+
elseif type == "tool" and context then
577+
-- Check if tool is enabled
578+
if context.disabled then
579+
return
580+
end
581+
582+
-- Toggle auto-approval for specific tool
583+
local server_name = context.server_name
584+
local tool_name = context.def.name
585+
local is_native = native.is_native_server(server_name)
586+
local server_config = (
587+
is_native and State.native_servers_config[server_name] or State.servers_config[server_name]
588+
) or {}
589+
590+
local current_auto_approve = server_config.autoApprove or {}
591+
local new_auto_approve
592+
593+
-- Handle boolean case (convert to array first)
594+
if current_auto_approve == true then
595+
local all_tools = get_server_tool_names(server_name)
596+
current_auto_approve = vim.deepcopy(all_tools)
597+
end
598+
599+
-- Ensure it's an array
600+
if not vim.islist(current_auto_approve) then
601+
current_auto_approve = {}
602+
end
603+
604+
-- Toggle the tool in the list using filter instead of table.remove
605+
local tool_approved = vim.tbl_contains(current_auto_approve, tool_name)
606+
607+
if tool_approved then
608+
-- Remove tool from auto-approve list
609+
new_auto_approve = vim.tbl_filter(function(tool)
610+
return tool ~= tool_name
611+
end, current_auto_approve)
612+
else
613+
-- Add tool to auto-approve list
614+
new_auto_approve = vim.deepcopy(current_auto_approve)
615+
table.insert(new_auto_approve, tool_name)
616+
end
617+
618+
State.hub_instance:update_server_config(server_name, {
619+
autoApprove = new_auto_approve,
620+
})
621+
end
622+
end
486623
function MainView:handle_server_toggle()
487624
-- Get current line
488625
local cursor = vim.api.nvim_win_get_cursor(0)

0 commit comments

Comments
 (0)