MCP v2 PR-1b: action dispatcher, confirm modal, set_label end-to-end

App.tsx now listens on "mcp://request" and resolves each call:
needsConfirm=true queues a confirm modal (Accept/Reject, or
"Always allow <tool>" which appends the bare tool name to the
policy's allow bucket on the fly); needsConfirm=false runs straight
through. Replies via mcp_action_reply with externally-tagged
Result. The only wired-up tool for now is set_label, which delegates
to the existing ops.setLabel path.

McpConfirm.tsx (new) — themed amber-bordered modal sibling to the
existing overlays. Enter = accept, Esc = reject. Shows tool, the
policy reason that triggered the prompt, a human-readable summary
("Rename pane X → Y"), and an expandable raw-args section.

Audit log: subscription lifted from AuditTab up to App.tsx so events
fired while the panel is closed (or on Config/Policy tab) still land
in the ring. AuditTab becomes presentational; McpPanel forwards
entries + clearAudit + computes the unread badge from a baseline
seen-count.

StrictMode race fix: both new App-level listeners (mcp://audit and
mcp://request) use the cancelled-flag pattern so a late-resolving
listen() Promise after a strict-mode pretend-unmount tears itself
down instead of leaking a second subscription. Previously this
manifested as duplicate audit rows and a need-to-click-twice on
modal buttons.
This commit is contained in:
megaproxy 2026-05-26 12:26:33 +01:00
parent 464c576b79
commit 26ffe8859a
6 changed files with 397 additions and 49 deletions

View file

@ -173,5 +173,18 @@ export const mcpPolicyLoad = (): Promise<McpPolicy> =>
export const mcpPolicySave = (policy: McpPolicy): Promise<void> =>
invoke("mcp_policy_save", { policy });
// (No JS wrapper for mcp_action_reply or events — App.tsx wires those
// directly in the integration step.)
/** Subscribe to MCP action requests from the backend. Each request is a
* tool call the frontend must handle (mutate state) and reply to via
* {@link mcpActionReply}. */
export const onMcpRequest = (
cb: (req: McpActionRequest) => void,
): Promise<UnlistenFn> =>
listen<McpActionRequest>("mcp://request", (e) => cb(e.payload));
/** Reply to an MCP action request. The Rust side expects an externally-
* tagged Result `{ Ok: <value> }` on success, `{ Err: <msg> }` on
* failure or user rejection. */
export const mcpActionReply = (
requestId: string,
result: { Ok: unknown } | { Err: string },
): Promise<void> => invoke("mcp_action_reply", { requestId, result });