Skip to content

Commit a4f6554

Browse files
committed
fix: update approval
1 parent eeb67e8 commit a4f6554

3 files changed

Lines changed: 319 additions & 70 deletions

File tree

lua/mcphub/extensions/proxy/init.lua

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,6 @@ local M = {}
22

33
local shared = require("mcphub.extensions.shared")
44

5-
--- Synchronous approval check for RPC context (can't use async UI)
6-
--- Uses vim.schedule + vim.wait to show dialog in main loop while blocking RPC
7-
---@param parsed_params table
8-
---@return {error?: string, approve: boolean}
9-
local function handle_approval_sync(parsed_params)
10-
local State = require("mcphub.state")
11-
12-
local auto_approve = State.config.auto_approve or false
13-
local status = { approve = false, error = nil }
14-
15-
-- Check global auto_approve config
16-
if type(auto_approve) == "function" then
17-
local ok, res = pcall(auto_approve, parsed_params)
18-
if not ok or type(res) == "string" then
19-
status = { approve = false, error = res }
20-
elseif type(res) == "boolean" then
21-
status = { approve = res, error = nil }
22-
end
23-
elseif type(auto_approve) == "boolean" then
24-
status = { approve = auto_approve, error = nil }
25-
end
26-
27-
-- Check server-level autoApprove
28-
if parsed_params.is_auto_approved_in_server then
29-
status = { approve = true, error = nil }
30-
end
31-
32-
if status.error then
33-
return { error = status.error, approve = false }
34-
end
35-
36-
-- If not auto-approved and needs confirmation, show dialog via vim.schedule
37-
if status.approve == false and parsed_params.needs_confirmation_window then
38-
local is_tool = parsed_params.action == "use_mcp_tool"
39-
local msg = is_tool
40-
and string.format("Allow tool '%s' on server '%s'?", parsed_params.tool_name, parsed_params.server_name)
41-
or string.format("Allow access to '%s' on server '%s'?", parsed_params.uri, parsed_params.server_name)
42-
43-
-- Use vim.schedule to run confirm in main loop, then wait for result
44-
local result = nil
45-
local done = false
46-
47-
vim.schedule(function()
48-
local choice = vim.fn.confirm(msg, "&Yes\n&No", 2)
49-
result = choice == 1
50-
done = true
51-
end)
52-
53-
-- Wait for the scheduled function to complete (timeout after 60 seconds)
54-
vim.wait(60000, function()
55-
return done
56-
end, 50)
57-
58-
if not done then
59-
return { error = "Approval timeout", approve = false }
60-
end
61-
62-
return { error = not result and "User cancelled the operation" or nil, approve = result }
63-
end
64-
65-
return status
66-
end
67-
685
--- Get proxy command and args for external MCP clients
696
---@return { command: string, args: string[] }
707
function M.get()
@@ -179,7 +116,7 @@ function M.call_tool(server_name, tool_name, params)
179116
return { error = table.concat(parsed_params.errors, "\n") }
180117
end
181118

182-
local approval = handle_approval_sync(parsed_params)
119+
local approval = shared.handle_auto_approval_decision_sync(parsed_params)
183120
if approval.error then
184121
return { error = approval.error }
185122
end
@@ -226,7 +163,7 @@ function M.access_resource(server_name, uri, params)
226163
return { error = table.concat(parsed_params.errors, "\n") }
227164
end
228165

229-
local approval = handle_approval_sync(parsed_params)
166+
local approval = shared.handle_auto_approval_decision_sync(parsed_params)
230167
if approval.error then
231168
return { error = approval.error }
232169
end

lua/mcphub/extensions/shared.lua

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,20 +292,120 @@ function M.show_mcp_tool_prompt(params)
292292
return confirmed, cancelled
293293
end
294294

295+
--- Synchronous version for RPC context (can't use async UI)
296+
---@param params MCPHub.ParsedParams
297+
---@return boolean confirmed
298+
---@return boolean cancelled
299+
function M.show_mcp_tool_prompt_sync(params)
300+
local action_name = params.action
301+
local server_name = params.server_name
302+
local tool_name = params.tool_name
303+
local uri = params.uri
304+
local arguments = params.arguments or {}
305+
306+
local lines = {}
307+
local is_tool = action_name == "use_mcp_tool"
308+
309+
-- Header as a question
310+
local header_line = NuiLine()
311+
header_line:append(Text.icons.event, Text.highlights.warn)
312+
header_line:append(" Do you want to ", Text.highlights.text)
313+
if is_tool then
314+
header_line:append("call ", Text.highlights.text)
315+
header_line:append(tool_name, Text.highlights.warn_italic)
316+
else
317+
header_line:append("access ", Text.highlights.text)
318+
header_line:append(uri, Text.highlights.link)
319+
end
320+
header_line:append(" on ", Text.highlights.text)
321+
header_line:append(server_name, Text.highlights.success_italic)
322+
header_line:append("?", Text.highlights.text)
323+
table.insert(lines, header_line)
324+
325+
-- Parameters section
326+
if is_tool and next(arguments) then
327+
table.insert(lines, NuiLine():append(""))
328+
329+
for key, value in pairs(arguments) do
330+
-- Parameter name
331+
local param_name_line = NuiLine()
332+
param_name_line:append(Text.icons.param, Text.highlights.info)
333+
param_name_line:append(" " .. key .. ":", Text.highlights.json_property)
334+
table.insert(lines, param_name_line)
335+
336+
-- Parameter value
337+
local function add_value_lines(val)
338+
if type(val) == "string" then
339+
local value_lines = val:find("\n") and vim.split(val, "\n", { plain = true })
340+
or { '"' .. val .. '"' }
341+
for _, line in ipairs(value_lines) do
342+
local value_line = NuiLine()
343+
value_line:append(" " .. line, Text.highlights.json_string)
344+
table.insert(lines, value_line)
345+
end
346+
elseif type(val) == "boolean" then
347+
local value_line = NuiLine()
348+
value_line:append(" " .. tostring(val), Text.highlights.json_boolean)
349+
table.insert(lines, value_line)
350+
elseif type(val) == "number" then
351+
local value_line = NuiLine()
352+
value_line:append(" " .. tostring(val), Text.highlights.json_number)
353+
table.insert(lines, value_line)
354+
else
355+
for _, line in ipairs(vim.split(vim.inspect(val), "\n", { plain = true })) do
356+
local value_line = NuiLine()
357+
value_line:append(" " .. line, Text.highlights.muted)
358+
table.insert(lines, value_line)
359+
end
360+
end
361+
end
362+
363+
add_value_lines(value)
364+
table.insert(lines, NuiLine():append(""))
365+
end
366+
end
367+
368+
-- Fire event before showing confirmation window
369+
utils.fire("MCPHubApprovalWindowOpened", {
370+
action = action_name,
371+
server_name = server_name,
372+
tool_name = tool_name,
373+
uri = uri,
374+
arguments = arguments,
375+
})
376+
377+
local confirmed, cancelled = ui_utils.confirm_sync(lines, {
378+
min_width = 70,
379+
max_width = 100,
380+
})
381+
382+
-- Fire event after user makes decision
383+
utils.fire("MCPHubApprovalWindowClosed", {
384+
action = action_name,
385+
server_name = server_name,
386+
tool_name = tool_name,
387+
uri = uri,
388+
arguments = arguments,
389+
confirmed = confirmed,
390+
cancelled = cancelled,
391+
})
392+
393+
return confirmed, cancelled
394+
end
395+
396+
--- Check auto-approval status without showing any dialog
295397
---@param parsed_params MCPHub.ParsedParams
296398
---@return {error?:string, approve:boolean}
297-
function M.handle_auto_approval_decision(parsed_params)
399+
function M.check_auto_approval(parsed_params)
298400
local auto_approve = State.config.auto_approve or false
299401
local status = { approve = false, error = nil }
300-
--- If user has a custom function that decides whether to auto-approve
301-
--- call that with params + saved autoApprove state as is_auto_approved_in_server field
402+
403+
-- Check global auto_approve config (can be boolean or function)
302404
if type(auto_approve) == "function" then
303405
local ok, res = pcall(auto_approve, parsed_params)
304406
if not ok or type(res) == "string" then
305-
--- If auto_approve function throws an error, or returns a string, treat it as an error
306407
status = { approve = false, error = res }
307408
elseif type(res) == "boolean" then
308-
--- If auto_approve function returns a boolean, use that as the decision
309409
status = { approve = res, error = nil }
310410
end
311411
elseif type(auto_approve) == "boolean" then
@@ -317,6 +417,14 @@ function M.handle_auto_approval_decision(parsed_params)
317417
status = { approve = true, error = nil }
318418
end
319419

420+
return status
421+
end
422+
423+
---@param parsed_params MCPHub.ParsedParams
424+
---@return {error?:string, approve:boolean}
425+
function M.handle_auto_approval_decision(parsed_params)
426+
local status = M.check_auto_approval(parsed_params)
427+
320428
if status.error then
321429
return { error = status.error or "Something went wrong with auto-approval", approve = false }
322430
end
@@ -325,6 +433,25 @@ function M.handle_auto_approval_decision(parsed_params)
325433
local confirmed, _ = M.show_mcp_tool_prompt(parsed_params)
326434
return { error = not confirmed and "User cancelled the operation", approve = confirmed }
327435
end
436+
437+
return status
438+
end
439+
440+
--- Synchronous version for RPC context (can't use async UI)
441+
---@param parsed_params MCPHub.ParsedParams
442+
---@return {error?:string, approve:boolean}
443+
function M.handle_auto_approval_decision_sync(parsed_params)
444+
local status = M.check_auto_approval(parsed_params)
445+
446+
if status.error then
447+
return { error = status.error or "Something went wrong with auto-approval", approve = false }
448+
end
449+
450+
if status.approve == false and parsed_params.needs_confirmation_window then
451+
local confirmed, _ = M.show_mcp_tool_prompt_sync(parsed_params)
452+
return { error = not confirmed and "User cancelled the operation", approve = confirmed }
453+
end
454+
328455
return status
329456
end
330457

0 commit comments

Comments
 (0)