MCP: persistent port/token + mcp-remote shim recipe for Claude Code
Port (default 47821) and bearer token now persist to mcp.json with OS-picked fallback if the port is taken; new Regenerate button in the panel rotates the token and restarts the running server. rmcp's DNS-rebinding host allowlist is disabled so WSL gateway IPs can connect (bearer-auth handles the gatekeeping); the auth middleware only enforces on /mcp paths so OAuth-discovery clients don't see a Bearer challenge on /.well-known/* probes. Claude Code's HTTP-MCP client currently tries OAuth and ignores static `headers` auth (anthropics/claude-code#17152, #46879), so the panel + README config snippet now uses `npx mcp-remote` as a stdio shim that proxies the HTTP endpoint with the bearer baked in.
This commit is contained in:
parent
352aa8c281
commit
799f507c3c
8 changed files with 290 additions and 46 deletions
|
|
@ -9,6 +9,7 @@ interface McpPanelProps {
|
|||
status: McpStatus;
|
||||
onStart: () => Promise<void>;
|
||||
onStop: () => Promise<void>;
|
||||
onRegenerateToken: () => Promise<void>;
|
||||
onClose: () => void;
|
||||
/** Count of leaves with mcpAllow=true — shown so the user knows whether
|
||||
* enabling the server will actually expose anything. */
|
||||
|
|
@ -21,12 +22,14 @@ export default function McpPanel({
|
|||
status,
|
||||
onStart,
|
||||
onStop,
|
||||
onRegenerateToken,
|
||||
onClose,
|
||||
allowedPaneCount,
|
||||
totalPaneCount,
|
||||
}: McpPanelProps) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [revealToken, setRevealToken] = useState(false);
|
||||
const [regenBusy, setRegenBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
|
|
@ -56,6 +59,20 @@ export default function McpPanel({
|
|||
);
|
||||
}, []);
|
||||
|
||||
const regenerate = useCallback(async () => {
|
||||
if (regenBusy) return;
|
||||
const warn = status.running
|
||||
? "Regenerate token? Existing MCP clients will be disconnected and need the new token to reconnect."
|
||||
: "Regenerate token? Any saved client config with the old token will stop working.";
|
||||
if (!window.confirm(warn)) return;
|
||||
setRegenBusy(true);
|
||||
try {
|
||||
await onRegenerateToken();
|
||||
} finally {
|
||||
setRegenBusy(false);
|
||||
}
|
||||
}, [regenBusy, status.running, onRegenerateToken]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="backdrop" onClick={onClose} aria-label="Close" />
|
||||
|
|
@ -117,17 +134,30 @@ export default function McpPanel({
|
|||
{revealToken ? "Hide" : "Show"}
|
||||
</button>
|
||||
<button onClick={() => copy(status.token!)}>Copy</button>
|
||||
<button onClick={regenerate} disabled={regenBusy}>
|
||||
{regenBusy ? "…" : "Regenerate"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mcp-hint">
|
||||
URL + token persist across restarts — paste the snippet
|
||||
into your Claude config once. Regenerate if the token
|
||||
leaks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mcp-field">
|
||||
<label>Claude config snippet</label>
|
||||
<label>Claude Code config snippet (.mcp.json)</label>
|
||||
<pre className="mcp-snippet">
|
||||
{`{
|
||||
"mcpServers": {
|
||||
"tiletopia": {
|
||||
"url": "${status.url}",
|
||||
"headers": { "Authorization": "Bearer ${status.token}" }
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y", "mcp-remote",
|
||||
"${status.url}",
|
||||
"--allow-http",
|
||||
"--header", "Authorization: Bearer ${status.token}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}`}
|
||||
|
|
@ -139,10 +169,15 @@ export default function McpPanel({
|
|||
{
|
||||
mcpServers: {
|
||||
tiletopia: {
|
||||
url: status.url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${status.token}`,
|
||||
},
|
||||
command: "npx",
|
||||
args: [
|
||||
"-y",
|
||||
"mcp-remote",
|
||||
status.url,
|
||||
"--allow-http",
|
||||
"--header",
|
||||
`Authorization: Bearer ${status.token}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -157,14 +192,34 @@ export default function McpPanel({
|
|||
</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.
|
||||
<strong>Why the shim?</strong> Claude Code's HTTP-MCP
|
||||
client tries OAuth discovery and ignores static{" "}
|
||||
<code>headers</code> auth (Anthropic issues #17152, #46879).
|
||||
The <code>mcp-remote</code> stdio shim transparently
|
||||
proxies the HTTP endpoint with the bearer header attached,
|
||||
which sidesteps the OAuth flow entirely. Other MCP
|
||||
clients that handle bearer auth correctly can connect
|
||||
directly to the URL above with the token in an{" "}
|
||||
<code>Authorization</code> header.
|
||||
<br />
|
||||
<br />
|
||||
<strong>WSL connectivity:</strong> the URL uses{" "}
|
||||
<code>127.0.0.1</code>; a Claude session running inside
|
||||
WSL needs to either swap that for the WSL gateway IP
|
||||
(<code>ip route show default | awk '{`{print $3}`}'</code>{" "}
|
||||
inside WSL — changes after each WSL restart), or enable
|
||||
mirrored networking (<code>networkingMode=mirrored</code>{" "}
|
||||
in <code>%UserProfile%\.wslconfig</code>, Win11 22H2+)
|
||||
so <code>127.0.0.1</code> in WSL routes to this host.
|
||||
You'll likely also need to allow the port through Windows
|
||||
Defender Firewall:{" "}
|
||||
<code>
|
||||
New-NetFirewallRule -DisplayName 'tiletopia MCP'
|
||||
-Direction Inbound -Action Allow -Protocol TCP
|
||||
-LocalPort {status.url.match(/:(\d+)\//)?.[1] ?? "47821"}{" "}
|
||||
-Profile Any
|
||||
</code>{" "}
|
||||
(elevated PowerShell).
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -178,11 +233,12 @@ networkingMode=mirrored`}</pre>
|
|||
)}
|
||||
|
||||
<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.
|
||||
<strong>Security:</strong> bound to <code>0.0.0.0</code> so WSL
|
||||
distros and other machines on your LAN can reach it; bearer
|
||||
token is the only thing keeping them out. Treat MCP access as
|
||||
equivalent to terminal access — don't share the token, don't
|
||||
run the server on an untrusted network. Saved SSH passwords are{" "}
|
||||
<em>never</em> exposed through MCP.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue