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:
parent
464c576b79
commit
26ffe8859a
6 changed files with 397 additions and 49 deletions
81
src/components/McpConfirm.tsx
Normal file
81
src/components/McpConfirm.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
export interface McpConfirmSpec {
|
||||
tool: string;
|
||||
args: unknown;
|
||||
reason: string | null;
|
||||
/** Human-readable summary of what's about to happen, computed by the
|
||||
* per-tool handler (e.g. "rename pane 'shell' to 'build'"). */
|
||||
summary: string;
|
||||
}
|
||||
|
||||
interface McpConfirmProps {
|
||||
spec: McpConfirmSpec;
|
||||
onAccept: () => void;
|
||||
onReject: () => void;
|
||||
/** Approve this call AND add the bare tool name to the policy allow list
|
||||
* so future calls of this tool skip the prompt. */
|
||||
onAlwaysAllow: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export default function McpConfirm({ spec, onAccept, onReject, onAlwaysAllow }: McpConfirmProps) {
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onReject();
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onAccept();
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [onAccept, onReject]);
|
||||
|
||||
const argsJson = JSON.stringify(spec.args, null, 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="backdrop"
|
||||
onClick={onReject}
|
||||
aria-label="Reject MCP action"
|
||||
/>
|
||||
<div className="mcp-confirm" role="dialog" aria-label="MCP action confirm">
|
||||
<header className="mcp-confirm-header">
|
||||
<span className="mcp-confirm-title">
|
||||
MCP wants to run <code>{spec.tool}</code>
|
||||
</span>
|
||||
</header>
|
||||
<div className="mcp-confirm-body">
|
||||
<p className="mcp-confirm-summary">{spec.summary}</p>
|
||||
{spec.reason && (
|
||||
<p className="mcp-confirm-reason">
|
||||
Policy decision: <em>{spec.reason}</em>
|
||||
</p>
|
||||
)}
|
||||
<details className="mcp-confirm-args">
|
||||
<summary>Raw arguments</summary>
|
||||
<pre>{argsJson}</pre>
|
||||
</details>
|
||||
</div>
|
||||
<footer className="mcp-confirm-actions">
|
||||
<button className="mcp-confirm-reject" onClick={onReject}>
|
||||
Reject (Esc)
|
||||
</button>
|
||||
<button
|
||||
className="mcp-confirm-always"
|
||||
onClick={() => { void onAlwaysAllow(); }}
|
||||
title={`Add "${spec.tool}" to the policy allow list — future calls of this tool won't prompt`}
|
||||
>
|
||||
Always allow {spec.tool}
|
||||
</button>
|
||||
<button className="mcp-confirm-accept" onClick={onAccept} autoFocus>
|
||||
Approve (Enter)
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue