Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 58 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,34 +182,79 @@ tunnel/port-forward — see [Security](#security-model) for why the LAN address
discouraged) and `<token>` 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@<device>
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@<device>
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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@<device> # "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@<device> \
'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"
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
```
Comment on lines +246 to +251

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant README section with line numbers
sed -n '220,255p' README.md

# Find other Inspector references in the repo
rg -n "modelcontextprotocol/inspector|MCP Inspector|NANOKVM_MCP_TOKEN|Authorization: Bearer" -S .

Repository: tylervick/nanokvm-mcp

Length of output: 5632


🌐 Web query:

@modelcontextprotocol/inspector CLI --header file stdin environment variable bearer token documentation

💡 Result:

The @modelcontextprotocol/inspector CLI supports custom HTTP headers, including bearer tokens, through the --header flag [1][2]. When using the CLI mode, you can pass authentication credentials by providing the header directly in the command [3][4]. The format requires the header name and value, typically structured as an Authorization header for bearer tokens [2][3]. Usage example: npx @modelcontextprotocol/inspector --cli https://example.com/mcp --transport http --method tools/list --header "Authorization: Bearer YOUR_TOKEN" Key details regarding this functionality: - Multiple --header flags: You can pass multiple --header flags in a single command if needed [2]. - Format: The argument must follow the "HeaderName: Value" format [1][2]. - Transport Compatibility: The headers are passed to both HTTP and SSE transports [2]. - Environment Variables: While the CLI supports the --header flag for ad-hoc requests, environment variables are not a native, direct configuration mechanism for header injection in the Inspector CLI itself; you should inject the variable into the command string (e.g., --header "Authorization: Bearer ${MY_TOKEN}") [2][4]. - Limitations: There have been reports that in some scenarios, specifically involving the Inspector's own proxy mechanisms, custom headers might be misinterpreted or overwritten if they conflict with internal authentication headers [5][6]. Ensure your header names do not conflict with reserved proxy headers if you are experiencing connectivity issues [6]. For standard testing, using the --header flag as shown above is the official and recommended method for providing bearer tokens to remote MCP servers via the Inspector CLI [2][3].

Citations:


🌐 Web query:

@modelcontextprotocol/inspector README --header stdin file environment variable inspector cli auth bearer token

💡 Result:

The @modelcontextprotocol/inspector is a developer tool used for testing and debugging MCP servers [1][2]. It provides a CLI mode, a web-based UI, and a Terminal UI (TUI), all executable via npx [3][4]. Regarding your specific areas of interest: Environment Variables and Auth For authentication, particularly when running in containerized environments, you can manage tokens using environment variables [5]: - MCP_INSPECTOR_API_TOKEN: Use this to set a known bearer token for the inspector [5]. If not set, one is automatically generated and displayed in the logs [5]. - DANGEROUSLY_OMIT_AUTH: Set this to true to disable authentication entirely [5][6]. - DANGEROUSLY_BIND_ALL_INTERFACES: Set this to true if you need to bind the inspector to 0.0.0.0, which is otherwise refused by default for security reasons [5]. CLI and Bearer Tokens - CLI Mode: The CLI allows for programmatic interaction with MCP servers (e.g., npx @modelcontextprotocol/inspector --cli ) [3][7]. - Bearer Tokens in UI: When using the web UI to connect to remote servers (e.g., SSE transport), you can provide a bearer token in the sidebar [7]. The inspector includes this in the Authorization header [7]. Note that there have been reports of the proxy session token potentially overwriting the Authorization header if they share the same name [6]; users are advised to use a different header name or set DANGEROUSLY_OMIT_AUTH if conflicts arise [6]. Stdin/File Configuration - The inspector supports configuring servers via command-line arguments or configuration files [1][3]. You can use --config to point to a configuration file that defines server launch commands, environment variables, and other parameters [1][7]. - When inspecting locally developed servers, the tool can be invoked with the server's command directly in the CLI, or by using a config file to manage multiple server definitions [1][7]. Detailed documentation on server configuration (including catalog vs. config files and per-server fields) is available in the project's docs/ directory [3].

Citations:


🌐 Web query:

@modelcontextprotocol/inspector config headers authorization bearer token --config

💡 Result:

To configure an authorization bearer token in the Model Context Protocol (MCP) Inspector, you can use the web interface or command-line arguments depending on your setup. Web Interface Configuration The Inspector UI allows you to add custom headers to your connection settings to support bearer token authentication [1][2][3]. 1. Open the MCP Inspector interface [4][5]. 2. Click the Configuration button (or "Add Header" in the connection setup area) [6][7][2]. 3. Add a new header entry [2]. 4. Set the Header field to Authorization and the Value field to Bearer YOUR_TOKEN [2][3]. Recent updates have enhanced header management, allowing for multiple custom headers and flexible configurations [1]. The UI now supports both form-based and JSON editor modes for these headers [1]. These settings are stored in your browser's local storage for persistence between sessions [8][3]. Command-Line Configuration When starting the Inspector from the command line, you can pass custom headers directly using the --header argument [6][9]. Example command: npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --header "Authorization: Bearer your-api-key" Proxy Authentication If you are running the MCP Inspector proxy, note that it requires its own authentication by default [6][10]. A session token is generated when the proxy starts and is printed to the console [6][4]. This token must be included as a Bearer token in the Authorization header for all requests to the proxy server [6][10]. You can also specify this token using the MCP_PROXY_AUTH_TOKEN environment variable when starting the server [6][4][10]. Top Results: [6], [1], [2], [3], [5]

Citations:


Avoid interpolating the token in the Inspector CLI example. Shell expansion puts ${NANOKVM_MCP_TOKEN} into the spawned process’s argv, so the bearer token is exposed to ps/local users. Use a file/config-based header path if this Inspector release supports one, or document a non-CLI check instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 237 - 242, Update the MCP Inspector example near the
tools/list invocation to avoid interpolating NANOKVM_MCP_TOKEN into command-line
arguments. Use a supported file/config-based header mechanism, or replace the
command with a documented non-CLI validation method that does not expose the
bearer token through process arguments.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caveat added in 5f6613c rather than restructuring: per your own research above, Inspector has no file/config-based header mechanism — --header interpolation is its official method — and the primary flow (claude mcp add) already keeps the secret out of argv via the unexpanded ${VAR}. The note flags the ps exposure and points shared-host users at curl's @file header form.


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):
Expand Down