diff --git a/README.md b/README.md index 8472aa0..99fe09d 100644 --- a/README.md +++ b/README.md @@ -182,34 +182,79 @@ tunnel/port-forward — see [Security](#security-model) for why the LAN address discouraged) and `` with the value of `NANOKVM_MCP_TOKEN`, or the token printed to `/data/nanokvm-mcp/daemon.log` if you didn't set one. -### Over an SSH tunnel +### Claude Code over an SSH tunnel (tested end-to-end) -With the default loopback bind, forward the port from your machine (the keepalive matters -— an idle MCP session will otherwise drop the tunnel): +This is the setup validated against real hardware. It uses an SSH master +connection that carries the port-forward and authenticates once; every later +`ssh`/`scp` rides the same socket without re-prompting. (The master exits after +an hour *idle*, so this is a per-session flow, not a permanent one — a durable +path is tracked in [#16](https://github.com/tylervick/nanokvm-mcp/issues/16).) + +**1. Open the master connection + tunnel** (one password prompt): ```sh -ssh -N -L 8080:127.0.0.1:8080 -o ServerAliveInterval=30 root@ +ssh -f -N -M -S /tmp/nkvm.sock -o ControlPersist=3600 -o ServerAliveInterval=30 \ + -o ExitOnForwardFailure=yes \ + -o PubkeyAuthentication=no -o PreferredAuthentications=password -o IdentitiesOnly=yes \ + -L 8080:127.0.0.1:8080 root@ ``` -Then the endpoint is `http://127.0.0.1:8080/` locally. Keep the token out of your -shell history: export it once (`export NANOKVM_MCP_TOKEN=...` with a leading space, -or `export NANOKVM_MCP_TOKEN=$(cat token-file)`) and reference the variable so the -literal value never appears on a command line. Verify with MCP Inspector: +`ExitOnForwardFailure=yes` makes a failed port-forward (e.g. 8080 already taken +by a stale tunnel) fail loudly here instead of backgrounding a broken master. + +The `PubkeyAuthentication=no` trio matters: the stock firmware has no authorized +keys, and an ssh-agent holding several keys will exhaust dropbear's auth attempts +before password auth is ever offered ("Too many authentication failures"). + +Check whether the tunnel is still alive later (`ControlPersist=3600` means the +master exits after 3600 s with no client connections — active use keeps it open): ```sh -npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8080/ \ - --transport http --header "Authorization: Bearer ${NANOKVM_MCP_TOKEN}" --method tools/list +ssh -S /tmp/nkvm.sock -O check root@ # "Master running" = alive +``` + +**2. Pull the bearer token to a local file** — by reference from here on, so the +secret never enters shell history or configs: + +```sh +(umask 077; ssh -S /tmp/nkvm.sock root@ \ + 'sed -n "s/^NANOKVM_MCP_TOKEN=//p" /root/nanokvm-mcp/nanokvm-mcp.env' \ + > ~/.nanokvm-token) && test -s ~/.nanokvm-token || echo "no token in env file" ``` -Or add it to Claude Code — single quotes matter: they pass the `${VAR}` reference -through unexpanded, and Claude Code's `.mcp.json` expands it at connection time, -so neither your shell history nor the saved config ever contains the secret: +The `test -s` guard catches the case where no explicit `NANOKVM_MCP_TOKEN` is +set on the device (the generated-token path prints to `daemon.log` instead — +but set an explicit one; see the [first-run checklist](#first-run-checklist-learned-on-real-hardware)). + +**3. Register with Claude Code** — the single quotes are load-bearing: they pass +the `${VAR}` reference through unexpanded, and Claude Code's `.mcp.json` expands +it at connection time, so the saved config holds the reference, not the secret: ```sh +export NANOKVM_MCP_TOKEN=$(cat ~/.nanokvm-token) # add to your shell profile claude mcp add --transport http nanokvm http://127.0.0.1:8080/ \ --header 'Authorization: Bearer ${NANOKVM_MCP_TOKEN}' ``` +**4. Use it.** Start a *fresh* Claude Code session (new MCP servers are picked up +at session start) from a shell where the variable is set, and ask for a +screenshot or device info. In the default read-only mode the 7 read-only tools +appear; mutating tools require `NANOKVM_MCP_READONLY=false` in the device env +plus an init-script restart, after which they show up annotated as destructive — +Claude asks before using them, and every call lands in the audit log. + +To sanity-check the endpoint without Claude, use MCP Inspector: + +```sh +npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8080/ \ + --transport http --header "Authorization: Bearer ${NANOKVM_MCP_TOKEN}" --method tools/list +``` + +Note the shell expands the token into the process's arguments here (Inspector +has no file-based header option), so it's briefly visible to `ps` — fine on a +single-user machine, but on a shared host prefer checking with curl's `@file` +header form instead. + ## Tools 14 tools are registered by default (7 in read-only mode):