Add MCP server (v1 read-only): toggle, per-pane gate, panel UI
This commit is contained in:
parent
6068522ee3
commit
83d8932c98
15 changed files with 1235 additions and 7 deletions
191
src/components/McpPanel.tsx
Normal file
191
src/components/McpPanel.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { useEffect, useState, useCallback } from "react";
|
||||
import {
|
||||
writeText as clipboardWriteText,
|
||||
} from "@tauri-apps/plugin-clipboard-manager";
|
||||
import type { McpStatus } from "../ipc";
|
||||
import "./McpPanel.css";
|
||||
|
||||
interface McpPanelProps {
|
||||
status: McpStatus;
|
||||
onStart: () => Promise<void>;
|
||||
onStop: () => Promise<void>;
|
||||
onClose: () => void;
|
||||
/** Count of leaves with mcpAllow=true — shown so the user knows whether
|
||||
* enabling the server will actually expose anything. */
|
||||
allowedPaneCount: number;
|
||||
/** Total pane count for context. */
|
||||
totalPaneCount: number;
|
||||
}
|
||||
|
||||
export default function McpPanel({
|
||||
status,
|
||||
onStart,
|
||||
onStop,
|
||||
onClose,
|
||||
allowedPaneCount,
|
||||
totalPaneCount,
|
||||
}: McpPanelProps) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [revealToken, setRevealToken] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [onClose]);
|
||||
|
||||
const toggle = useCallback(async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
if (status.running) await onStop();
|
||||
else await onStart();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [busy, status.running, onStart, onStop]);
|
||||
|
||||
const copy = useCallback((s: string) => {
|
||||
void clipboardWriteText(s).catch((e) =>
|
||||
console.warn("clipboard write failed:", e),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="backdrop" onClick={onClose} aria-label="Close" />
|
||||
<div className="mcp-panel" role="dialog" aria-label="MCP server">
|
||||
<header className="mcp-header">
|
||||
<span className="mcp-title">MCP server</span>
|
||||
<button className="mcp-close" onClick={onClose} aria-label="Close">×</button>
|
||||
</header>
|
||||
|
||||
<div className="mcp-body">
|
||||
<p className="mcp-blurb">
|
||||
Lets a Claude session on the same machine inspect this workspace
|
||||
via Model Context Protocol — see which panes are running, read
|
||||
their scrollback, wait for commands to settle. Read-only in v1;
|
||||
Claude can't send keystrokes or reshape the layout yet.
|
||||
</p>
|
||||
|
||||
<div className="mcp-toggle-row">
|
||||
<button
|
||||
className={`mcp-toggle${status.running ? " on" : ""}`}
|
||||
onClick={toggle}
|
||||
disabled={busy}
|
||||
>
|
||||
<span className="mcp-dot" />
|
||||
{status.running ? "Server: ON" : "Server: OFF"}
|
||||
</button>
|
||||
<span className="mcp-allow-count">
|
||||
{allowedPaneCount} of {totalPaneCount} pane
|
||||
{totalPaneCount === 1 ? "" : "s"} allow-listed
|
||||
{allowedPaneCount === 0 && status.running && (
|
||||
<span className="mcp-allow-warn">
|
||||
{" "}
|
||||
— Claude will see nothing until you toggle 🤖 on at least
|
||||
one pane.
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{status.running && status.url && status.token && (
|
||||
<>
|
||||
<div className="mcp-field">
|
||||
<label>URL</label>
|
||||
<div className="mcp-field-row">
|
||||
<input readOnly value={status.url} onFocus={(e) => e.currentTarget.select()} />
|
||||
<button onClick={() => copy(status.url!)}>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mcp-field">
|
||||
<label>Bearer token</label>
|
||||
<div className="mcp-field-row">
|
||||
<input
|
||||
readOnly
|
||||
type={revealToken ? "text" : "password"}
|
||||
value={status.token}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
/>
|
||||
<button onClick={() => setRevealToken((r) => !r)}>
|
||||
{revealToken ? "Hide" : "Show"}
|
||||
</button>
|
||||
<button onClick={() => copy(status.token!)}>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mcp-field">
|
||||
<label>Claude config snippet</label>
|
||||
<pre className="mcp-snippet">
|
||||
{`{
|
||||
"mcpServers": {
|
||||
"tiletopia": {
|
||||
"url": "${status.url}",
|
||||
"headers": { "Authorization": "Bearer ${status.token}" }
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() =>
|
||||
copy(
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
tiletopia: {
|
||||
url: status.url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${status.token}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
Copy config snippet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mcp-tips">
|
||||
<strong>WSL connectivity:</strong> for Claude running inside
|
||||
WSL to reach this server, enable mirrored networking in your
|
||||
<code> %UserProfile%\.wslconfig</code> (Win11 22H2+):
|
||||
<pre>{`[wsl2]
|
||||
networkingMode=mirrored`}</pre>
|
||||
Then <code>127.0.0.1</code> in WSL routes to this Windows
|
||||
host. Without mirrored mode you'll need to use the WSL
|
||||
gateway IP.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!status.running && (
|
||||
<p className="mcp-off-hint">
|
||||
Server is off — no port is open. Token is generated when you
|
||||
start. Each pane needs the 🤖 chip toggled on for Claude to
|
||||
see it.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="mcp-security">
|
||||
<strong>Security:</strong> bound to 127.0.0.1 only. Anyone on
|
||||
this machine running as you can read the bearer token if they
|
||||
see it (e.g. via this UI or by guessing the localhost port).
|
||||
Treat MCP access as equivalent to terminal access. Saved SSH
|
||||
passwords are <em>never</em> exposed through MCP.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue