diff --git a/CLAUDE.md b/CLAUDE.md index 783e9b6..a095f16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,8 +78,15 @@ the MCP server requires Google login before any tool call. 2. Authorized redirect URIs: `/auth/callback` 3. Copy Client ID + Secret to `~/.unraid-mcp/.env` +**Generate a stable JWT signing key:** +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + **Omit `GOOGLE_CLIENT_ID` to run without auth** (default — preserves existing behaviour). +**Full guide:** [`docs/GOOGLE_OAUTH.md`](docs/GOOGLE_OAUTH.md) + ## Architecture ### Core Components diff --git a/README.md b/README.md index 0bdaf18..eff41a5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ - 🔄 **Real-time Data**: WebSocket subscriptions for live metrics, logs, array state, and more - 📊 **Health Monitoring**: Comprehensive system diagnostics and status - 🐳 **Docker Ready**: Full containerization support with Docker Compose -- 🔒 **Secure**: Proper SSL/TLS configuration and API key management +- 🔒 **Secure**: Optional Google OAuth 2.0 authentication + SSL/TLS + API key management - 📝 **Rich Logging**: Structured logging with rotation and multiple levels --- @@ -25,6 +25,7 @@ - [Quick Start](#-quick-start) - [Installation](#-installation) - [Configuration](#-configuration) +- [Google OAuth](#-google-oauth-optional) - [Available Tools & Resources](#-available-tools--resources) - [Development](#-development) - [Architecture](#-architecture) @@ -229,7 +230,7 @@ UNRAID_VERIFY_SSL=true # true, false, or path to CA bundle # Subscription Configuration UNRAID_AUTO_START_SUBSCRIPTIONS=true # Auto-start WebSocket subscriptions on startup (default: true) -UNRAID_MAX_RECONNECT_ATTEMPTS=5 # Max WebSocket reconnection attempts (default: 5) +UNRAID_MAX_RECONNECT_ATTEMPTS=10 # Max WebSocket reconnection attempts (default: 10) # Optional: Log Stream Configuration # UNRAID_AUTOSTART_LOG_PATH=/var/log/syslog # Path for log streaming resource (unraid://logs/stream) @@ -245,6 +246,32 @@ UNRAID_MAX_RECONNECT_ATTEMPTS=5 # Max WebSocket reconnection attempts (def --- +## 🔐 Google OAuth (Optional) + +Protect the HTTP server with Google OAuth 2.0 — clients must complete a Google login before any tool call is executed. + +Add these to `~/.unraid-mcp/.env`: + +```bash +GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=GOCSPX-your-secret +UNRAID_MCP_BASE_URL=http://10.1.0.2:6970 # public URL of this server +UNRAID_MCP_JWT_SIGNING_KEY=<64-char-hex> # prevents token invalidation on restart +``` + +**Quick setup:** +1. [Google Cloud Console](https://console.cloud.google.com/) → Credentials → OAuth 2.0 Client ID (Web application) +2. Authorized redirect URI: `/auth/callback` +3. Copy Client ID + Secret into `~/.unraid-mcp/.env` +4. Generate a signing key: `python3 -c "import secrets; print(secrets.token_hex(32))"` +5. Restart the server + +Omit `GOOGLE_CLIENT_ID` to run without authentication (default behavior). + +**Full guide:** [`docs/GOOGLE_OAUTH.md`](docs/GOOGLE_OAUTH.md) + +--- + ## 🛠️ Available Tools & Resources The single `unraid` tool uses `action` (domain) + `subaction` (operation) routing to expose all operations via one MCP tool, minimizing context window usage. Destructive actions require `confirm=True`. diff --git a/docs/GOOGLE_OAUTH.md b/docs/GOOGLE_OAUTH.md new file mode 100644 index 0000000..2df3db0 --- /dev/null +++ b/docs/GOOGLE_OAUTH.md @@ -0,0 +1,165 @@ +# Google OAuth Setup Guide + +This document explains how to protect the Unraid MCP HTTP server with Google OAuth 2.0 authentication using FastMCP's built-in `GoogleProvider`. + +--- + +## Overview + +By default the MCP server is **open** — any client on the network can call tools. Setting three environment variables enables Google OAuth 2.1 authentication: clients must complete a Google login flow before the server will execute any tool. + +OAuth state (issued tokens, refresh tokens) is persisted to an encrypted file store at `~/.local/share/fastmcp/oauth-proxy/`, so sessions survive server restarts when `UNRAID_MCP_JWT_SIGNING_KEY` is set. + +> **Transport requirement**: OAuth only works with HTTP transports (`streamable-http` or `sse`). It has no effect on `stdio` — the server logs a warning if you configure both. + +--- + +## Prerequisites + +- Google account with access to [Google Cloud Console](https://console.cloud.google.com/) +- MCP server reachable at a known URL from your browser (LAN IP, Tailscale IP, or public domain) +- `UNRAID_MCP_TRANSPORT=streamable-http` (the default) + +--- + +## Step 1: Create a Google OAuth Client + +1. Open [Google Cloud Console](https://console.cloud.google.com/) → **APIs & Services** → **Credentials** +2. Click **Create Credentials** → **OAuth 2.0 Client ID** +3. Application type: **Web application** +4. Name: anything (e.g. `Unraid MCP`) +5. **Authorized redirect URIs** — add exactly: + ``` + http://:6970/auth/callback + ``` + Replace `` with the IP/hostname your browser uses to reach the MCP server (e.g. `10.1.0.2`, `100.x.x.x` for Tailscale, or a domain name). +6. Click **Create** — copy the **Client ID** and **Client Secret** + +--- + +## Step 2: Configure Environment Variables + +Add these to `~/.unraid-mcp/.env` (the canonical credential file for all runtimes): + +```bash +# Google OAuth (optional — enables authentication) +GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=GOCSPX-your-client-secret + +# Public base URL of this MCP server (must match the redirect URI above) +UNRAID_MCP_BASE_URL=http://10.1.0.2:6970 + +# Stable JWT signing key — prevents token invalidation on server restart +# Generate one: python3 -c "import secrets; print(secrets.token_hex(32))" +UNRAID_MCP_JWT_SIGNING_KEY=your-64-char-hex-string +``` + +**All four variables at once** (copy-paste template): + +```bash +cat >> ~/.unraid-mcp/.env <<'EOF' + +# Google OAuth +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +UNRAID_MCP_BASE_URL=http://10.1.0.2:6970 +UNRAID_MCP_JWT_SIGNING_KEY= +EOF +``` + +Then fill in the blanks. + +--- + +## Step 3: Generate a Stable JWT Signing Key + +Without `UNRAID_MCP_JWT_SIGNING_KEY`, FastMCP derives a key on startup. Any server restart invalidates all existing tokens and forces every client to re-authenticate. + +Generate a stable key once: + +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +Paste the output into `UNRAID_MCP_JWT_SIGNING_KEY`. This value never needs to change unless you intentionally want to invalidate all sessions. + +--- + +## Step 4: Restart the Server + +```bash +# Docker Compose +docker compose restart unraid-mcp + +# Direct / uv +uv run unraid-mcp-server +``` + +On startup you should see: + +``` +INFO [SERVER] Google OAuth enabled — base_url=http://10.1.0.2:6970, redirect_uri=http://10.1.0.2:6970/auth/callback +``` + +--- + +## How Authentication Works + +1. An MCP client connects to `http://:6970/mcp` +2. The server responds with a `401 Unauthorized` and an OAuth authorization URL +3. The client opens the URL in a browser; the user logs in with Google +4. Google redirects to `/auth/callback` with an authorization code +5. FastMCP exchanges the code for tokens, issues a signed JWT, and returns it to the client +6. The client includes the JWT in subsequent requests — the server validates it without hitting Google again +7. Tokens persist to `~/.local/share/fastmcp/oauth-proxy/` — sessions survive server restarts + +--- + +## Environment Variable Reference + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `GOOGLE_CLIENT_ID` | For OAuth | `""` | OAuth 2.0 Client ID from Google Cloud Console | +| `GOOGLE_CLIENT_SECRET` | For OAuth | `""` | OAuth 2.0 Client Secret from Google Cloud Console | +| `UNRAID_MCP_BASE_URL` | For OAuth | `""` | Public base URL of this server — must match the authorized redirect URI | +| `UNRAID_MCP_JWT_SIGNING_KEY` | Recommended | auto-derived | Stable 32+ char secret for JWT signing — prevents token invalidation on restart | + +OAuth is activated only when **all three** of `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and `UNRAID_MCP_BASE_URL` are non-empty. Omit any one to run without authentication. + +--- + +## Disabling OAuth + +Remove (or empty) `GOOGLE_CLIENT_ID` from `~/.unraid-mcp/.env` and restart. The server reverts to unauthenticated mode and logs: + +``` +WARNING [SERVER] No authentication configured — MCP server is open to all clients on the network. +``` + +--- + +## Troubleshooting + +**`redirect_uri_mismatch` from Google** +The redirect URI in Google Cloud Console must exactly match `/auth/callback` — same scheme, host, port, and path. Trailing slashes matter. + +**Tokens invalidated after restart** +Set `UNRAID_MCP_JWT_SIGNING_KEY` to a stable secret (see Step 3). Without it, FastMCP generates a new key on every start. + +**`stdio` transport warning** +OAuth requires an HTTP transport. Set `UNRAID_MCP_TRANSPORT=streamable-http` (the default) or `sse`. + +**Client cannot reach the callback URL** +`UNRAID_MCP_BASE_URL` must be the address your browser uses to reach the server — not `localhost` or `0.0.0.0`. Use the LAN IP, Tailscale IP, or a domain name. + +**OAuth configured but server not starting** +Check `logs/unraid-mcp.log` or `docker compose logs unraid-mcp` for startup errors. + +--- + +## Security Notes + +- OAuth protects the MCP HTTP interface — the Unraid GraphQL API itself still uses `UNRAID_API_KEY` +- OAuth state files at `~/.local/share/fastmcp/oauth-proxy/` should be on a private filesystem; do not expose them +- Restrict Google OAuth to specific accounts via the Google Cloud Console **OAuth consent screen** → **Test users** if you don't want to publish the app +- `UNRAID_MCP_JWT_SIGNING_KEY` is a credential — store it in `~/.unraid-mcp/.env` (mode 600), never in source control