Skip to main content
Glama
README.md
# PiKVM MCP

Control one PiKVM from an MCP client such as ChatGPT Codex or Google Antigravity. It can view and crop the screen, read on-screen text, navigate BIOS, type, mount an ISO already stored on PiKVM, and operate power controls — but it is not a remote shell.

## Choose one Compose file

| File | Use it when | What is exposed |
| --- | --- | --- |
| `compose.yaml` | **Recommended.** You have a DNS name and Cloudflare DNS. | Only Traefik on HTTPS (port 443) and its HTTP redirect (port 80). MCP itself is private inside Docker. |
| `compose-http.yaml` | You explicitly trust your LAN and do not want HTTPS. | MCP directly on port 8000, protected by bearer token and a CIDR client-network allow-list. |

Do not run both files together.

## The three keys

| Key | Where it lives | What it allows |
| --- | --- | --- |
| Bearer token | The Compose file | Connecting to MCP and reading PiKVM status. |
| View token | Generated on container start; shown in Docker logs | Capturing a screen image. |
| Control token | Generated on container start; shown in Docker logs | Keyboard, mouse, ISO and power actions. |

View and control tokens are replaced whenever the PiKVM MCP container restarts. An old chat therefore loses screen/control authority after a restart. Docker logs are sensitive while those tokens are valid.

If the PiKVM URL, user, or password are incomplete, MCP exposes only `pikvm_info` and explains what needs configuring.

## Recommended: HTTPS with Cloudflare DNS-01

You need a DNS name such as `mcp.example.com`, a Cloudflare API token scoped to that zone with **Zone / Zone / Read** and **Zone / DNS / Edit**, and Docker Compose on the server/LXC.

On the Docker host:

```sh
mkdir mcp-pikvm && cd mcp-pikvm
curl -fsSLO https://raw.githubusercontent.com/rovingclimber/mcp-pikvm/v0.9.2/compose.yaml
```

Open `compose.yaml` and replace every `CHANGE ME` value. In particular:

1. Add the PiKVM URL, username and password.
2. Replace all `mcp.example.com` occurrences with your DNS name, including `MCP_PUBLIC_ENDPOINT`.
3. Change the Traefik `ipallowlist` CIDR to your own LAN, for example `192.168.1.0/24`.
4. Add the Cloudflare token and certificate-notification email.

Generate and insert a bearer token without printing it:

```sh
TOKEN="$(openssl rand -base64 32 | tr -d '\n')" && sed -i "s|MCP_HTTP_BEARER_TOKEN: \"CHANGE-ME-USE-A-LONG-RANDOM-TOKEN\"|MCP_HTTP_BEARER_TOKEN: \"$TOKEN\"|" compose.yaml && unset TOKEN
```

Then start it:

```sh
docker compose up -d
docker compose logs pikvm-mcp
```

The second command displays the exact MCP endpoint and the temporary view/control tokens. The HTTPS endpoint normally looks like:

```text
https://mcp.example.com/mcp
```

The MCP service has **no published port** in this file. `127.0.0.1` is not involved: Traefik reaches MCP through Docker's private service network, and only Traefik publishes ports 80/443.

Traefik obtains and renews the certificate using Cloudflare DNS-01. It has a read-only Docker socket only to discover the labelled MCP service; the MCP container itself has no Docker socket access. [Traefik’s ACME documentation](https://doc.traefik.io/traefik/reference/install-configuration/tls/certificate-resolvers/acme/)

## Fallback: trusted-LAN HTTP

Use this only when every network between your client and the Docker host is trusted. It is normal HTTP, so the bearer token travels unencrypted.

```sh
mkdir mcp-pikvm && cd mcp-pikvm
curl -fsSLO https://raw.githubusercontent.com/rovingclimber/mcp-pikvm/v0.9.2/compose-http.yaml
```

Edit the marked values, then generate the bearer token:

```sh
TOKEN="$(openssl rand -base64 32 | tr -d '\n')" && sed -i "s|MCP_HTTP_BEARER_TOKEN: \"CHANGE-ME-USE-A-LONG-RANDOM-TOKEN\"|MCP_HTTP_BEARER_TOKEN: \"$TOKEN\"|" compose-http.yaml && unset TOKEN
docker compose -f compose-http.yaml up -d
```

Set `MCP_HTTP_ALLOWED_HOSTS` to the Docker host’s actual LAN address, for example `192.168.1.139:8000`. It is an HTTP **Host** allow-list, so it must not contain CIDR values.

Set `MCP_HTTP_ALLOWED_CLIENT_NETWORKS` to who may connect. It accepts CIDR ranges, for example:

```text
192.168.0.0/24
192.168.0.0/24,10.0.0.0/8
192.168.1.0/24,fd00::/8
```

The HTTP file publishes `8000:8000`, meaning Docker listens on the host’s network interfaces. The CIDR gate is enforced by MCP as a second check; use your host firewall as well if it has more than one network.

The endpoint is:

```text
http://your-docker-host:8000/mcp
```

## Connect ChatGPT Codex on Windows

First set the bearer token as a Windows user environment variable. Copy the `MCP_HTTP_BEARER_TOKEN` value from your Compose file, then run this in PowerShell:

```powershell
[Environment]::SetEnvironmentVariable('PIKVM_MCP_BEARER_TOKEN', 'paste-the-bearer-token-here', 'User')
```

Completely close and reopen ChatGPT Desktop after setting it.

### Use the desktop UI

In ChatGPT Desktop, go to **File → Settings → Plugins → MCPs → Add server**. Enter:

- **URL:** `https://mcp.example.com/mcp` — or the trusted-LAN HTTP endpoint.
- **Bearer token env var:** `PIKVM_MCP_BEARER_TOKEN`

The bearer token stays in your Windows user environment rather than in the UI configuration.

### Or edit Codex configuration directly

Add this to `%USERPROFILE%\.codex\config.toml` (for example `C:\Users\Justin\.codex\config.toml`):

```toml
[mcp_servers.mcp-pikvm]
enabled = true
url = "https://mcp.example.com/mcp"
bearer_token_env_var = "PIKVM_MCP_BEARER_TOKEN"
```

For an HTTP-only LAN deployment, change only the URL. Codex supports streamable HTTP servers with `url` and `bearer_token_env_var`; personal settings belong in `~/.codex/config.toml`, while trusted projects may use `.codex/config.toml`. [Codex configuration reference](https://learn.chatgpt.com/docs/config-file/config-basic), [MCP configuration](https://learn.chatgpt.com/docs/extend/mcp)

Start a new task and type `/mcp` to check the connection. Codex receives the tool names and safety guidance automatically. Begin with `pikvm_status`; it needs only the bearer token. Give the view/control token only when you explicitly want screen access or control.

## Connect Google Antigravity

Antigravity’s remote-MCP configuration uses static HTTP headers. Its documented `env` object is for a local **stdio** server process, not for substituting values into headers of a remote `serverUrl`; there is no documented environment-variable reference for this bearer header. That means the bearer token is stored in the JSON file below, so keep that file private. [Antigravity MCP documentation](https://antigravity.google/docs/mcp)

Put this in your Antigravity MCP configuration file (commonly `%USERPROFILE%\.gemini\config\mcp_config.json`; use the location shown by your installed Antigravity build):

```json
{
  "mcpServers": {
    "mcp-pikvm": {
      "serverUrl": "https://mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer paste-the-bearer-token-here"
      },
      "requiresOAuth": false
    }
  }
}
```

Use your HTTP endpoint instead only for a trusted LAN. Restart Antigravity after saving the file.

## Operating safely

- `pikvm_status` is bearer-only and read-only.
- `pikvm_screenshot`, `pikvm_screenshot_region`, and on-demand `pikvm_read_screen_text` require the current view token. OCR is local, runs only when requested, caps its working image at roughly 1.2 megapixels, and never needs a GPU.
- Keyboard, mouse, ISO mounting/ejection, HID changes and ATX power controls require the current control token. Destructive operations also require clear confirmation text.
- For desktop work use **absolute** mouse mode and a fresh screenshot before coordinate actions. `pikvm_move_pointer_to`, `pikvm_click_at`, `pikvm_double_click_at`, and `pikvm_click_drag` use that screenshot ID; dragging requires `CONFIRM DRAG` and always releases the left button if movement fails. For BIOS/UEFI use **relative** mouse mode plus `pikvm_move_mouse_relative` and `pikvm_click_mouse`.

To revoke view/control authority immediately, restart the MCP service:

```sh
docker compose restart pikvm-mcp
```

The server deliberately does not offer arbitrary shell execution, PiKVM configuration changes, arbitrary PiKVM API calls, or media upload/download.

## Moving from v0.5 or older

v0.7 removes the admin page, runtime configuration, `secrets/` directory, setup script, and `.env` file. Make a fresh deployment directory using **one** Compose file above, update the Windows bearer-token environment variable, then stop the old service before starting the new one.

## Reusable PiKVM API core

This repository also contains `pikvm_core`: a reusable authenticated PiKVM API
client with no MCP or LLM dependency. The public MCP uses it, while a future
trusted local runtime can import it directly. Its full-API transport and
integration boundary are documented in [PiKVM Core](docs/pikvm-core.md).

## Development

```sh
uv sync --group dev
uv run pytest
```