refactor: remove Docker and HTTP transport support, fix hypothesis cache directory

This commit is contained in:
Jacob Magar
2026-03-24 19:22:27 -04:00
parent e68d4a80e4
commit e548f6e6c9
39 changed files with 369 additions and 1757 deletions

View File

@@ -1,188 +0,0 @@
# Authentication Setup Guide
This document covers both Google OAuth 2.0 and API key bearer token authentication for the Unraid MCP HTTP server. It explains how to protect the server using FastMCP's built-in `GoogleProvider` for OAuth, or a static bearer token for headless/machine access.
---
## 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://<your-server-ip>:6970/auth/callback
```
Replace `<your-server-ip>` 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://<server>: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 `<UNRAID_MCP_BASE_URL>/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 `<UNRAID_MCP_BASE_URL>/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.
---
## API Key Authentication (Alternative / Combined)
For machine-to-machine access (scripts, CI, other agents) without a browser-based OAuth flow, set `UNRAID_MCP_API_KEY`:
```bash
# In ~/.unraid-mcp/.env
UNRAID_MCP_API_KEY=your-secret-token
```
Clients present it as a standard bearer token:
```
Authorization: Bearer your-secret-token
```
**Combining with Google OAuth**: set both `GOOGLE_CLIENT_ID` and `UNRAID_MCP_API_KEY`. The server activates `MultiAuth` and accepts either method — Google OAuth for interactive clients, API key for headless clients.
**Reusing the Unraid API key**: you can set `UNRAID_MCP_API_KEY` to the same value as `UNRAID_API_KEY` for simplicity. The two vars are kept separate so each concern has its own name.
**Standalone API key** (no Google OAuth): set only `UNRAID_MCP_API_KEY`. The server validates bearer tokens directly with no OAuth redirect flow.
---
## 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

View File

@@ -1,14 +1,14 @@
# Destructive Actions
**Last Updated:** 2026-03-16
**Last Updated:** 2026-03-24
**Total destructive actions:** 12 across 8 domains (single `unraid` tool)
All destructive actions require `confirm=True` at the call site. There is no additional environment variable gate — `confirm` is the sole guard.
> **mcporter commands below** use `$MCP_URL` (default: `http://localhost:6970/mcp`). Run `test-actions.sh` for automated non-destructive coverage; destructive actions are always skipped there and tested manually per the strategies below.
> **mcporter commands below** use stdio transport. Run `test-tools.sh` for automated non-destructive coverage; destructive actions are always skipped there and tested manually per the strategies below.
>
> **Calling convention (v1.0.0+):** All operations use the single `unraid` tool with `action` (domain) + `subaction` (operation). For example:
> `mcporter call --http-url "$MCP_URL" --tool unraid --args '{"action":"docker","subaction":"list"}'`
> `mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid --args '{"action":"docker","subaction":"list"}'`
---
@@ -26,7 +26,7 @@ Stopping the array unmounts all shares and can interrupt running containers and
```bash
# Prerequisite: array must already be stopped; use a disk you intend to remove
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"array","subaction":"remove_disk","disk_id":"<DISK_ID>","confirm":true}' --output json
```
@@ -36,11 +36,11 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
```bash
# Discover disk IDs
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"disk","subaction":"disks"}' --output json
# Clear stats for a specific disk
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"array","subaction":"clear_disk_stats","disk_id":"<DISK_ID>","confirm":true}' --output json
```
@@ -54,15 +54,15 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
# Prerequisite: create a minimal Alpine test VM in Unraid VM manager
# (Alpine ISO, 512MB RAM, no persistent disk, name contains "mcp-test")
VID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
VID=$(mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"vm","subaction":"list"}' --output json \
| python3 -c "import json,sys; vms=json.load(sys.stdin).get('vms',[]); print(next(v.get('uuid',v.get('id','')) for v in vms if 'mcp-test' in v.get('name','')))")
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args "{\"action\":\"vm\",\"subaction\":\"force_stop\",\"vm_id\":\"$VID\",\"confirm\":true}" --output json
# Verify: VM state should return to stopped
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args "{\"action\":\"vm\",\"subaction\":\"details\",\"vm_id\":\"$VID\"}" --output json
```
@@ -72,11 +72,11 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
```bash
# Same minimal Alpine test VM as above
VID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
VID=$(mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"vm","subaction":"list"}' --output json \
| python3 -c "import json,sys; vms=json.load(sys.stdin).get('vms',[]); print(next(v.get('uuid',v.get('id','')) for v in vms if 'mcp-test' in v.get('name','')))")
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args "{\"action\":\"vm\",\"subaction\":\"reset\",\"vm_id\":\"$VID\",\"confirm\":true}" --output json
```
@@ -89,9 +89,9 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
```bash
# 1. Create a test notification, then list to get the real stored ID (create response
# ID is ULID-based; stored filename uses a unix timestamp, so IDs differ)
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"notification","subaction":"create","title":"mcp-test-delete","subject":"safe to delete","description":"MCP destructive action test","importance":"INFO"}' --output json
NID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
NID=$(mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"notification","subaction":"list","notification_type":"UNREAD"}' --output json \
| python3 -c "
import json,sys
@@ -100,11 +100,11 @@ matches=[n['id'] for n in reversed(notifs) if n.get('title')=='mcp-test-delete']
print(matches[0] if matches else '')")
# 2. Delete it (notification_type required)
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args "{\"action\":\"notification\",\"subaction\":\"delete\",\"notification_id\":\"$NID\",\"notification_type\":\"UNREAD\",\"confirm\":true}" --output json
# 3. Verify
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"notification","subaction":"list"}' --output json | python3 -c \
"import json,sys; ns=[n for n in json.load(sys.stdin).get('notifications',[]) if 'mcp-test' in n.get('title','')]; print('clean' if not ns else ns)"
```
@@ -115,21 +115,21 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
```bash
# 1. Create and archive a test notification
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"notification","subaction":"create","title":"mcp-test-archive-wipe","subject":"archive me","description":"safe to delete","importance":"INFO"}' --output json
AID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
AID=$(mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"notification","subaction":"list","notification_type":"UNREAD"}' --output json \
| python3 -c "
import json,sys
notifs=json.load(sys.stdin).get('notifications',[])
matches=[n['id'] for n in reversed(notifs) if n.get('title')=='mcp-test-archive-wipe']
print(matches[0] if matches else '')")
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args "{\"action\":\"notification\",\"subaction\":\"archive\",\"notification_id\":\"$AID\"}" --output json
# 2. Wipe all archived
# NOTE: this deletes ALL archived notifications, not just the test one
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"notification","subaction":"delete_archived","confirm":true}' --output json
```
@@ -144,15 +144,15 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
```bash
# 1. Create a throwaway local remote (points to /tmp — no real data)
# Parameters: name (str), provider_type (str), config_data (dict)
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"rclone","subaction":"create_remote","name":"mcp-test-remote","provider_type":"local","config_data":{"root":"/tmp"}}' --output json
# 2. Delete it
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"rclone","subaction":"delete_remote","name":"mcp-test-remote","confirm":true}' --output json
# 3. Verify
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"rclone","subaction":"list_remotes"}' --output json | python3 -c \
"import json,sys; remotes=json.load(sys.stdin).get('remotes',[]); print('clean' if 'mcp-test-remote' not in remotes else 'FOUND — cleanup failed')"
```
@@ -167,16 +167,16 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
```bash
# 1. Create a test key (names cannot contain hyphens; ID is at key.id)
KID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
KID=$(mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"key","subaction":"create","name":"mcp test key","roles":["VIEWER"]}' --output json \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('key',{}).get('id',''))")
# 2. Delete it
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args "{\"action\":\"key\",\"subaction\":\"delete\",\"key_id\":\"$KID\",\"confirm\":true}" --output json
# 3. Verify
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"key","subaction":"list"}' --output json | python3 -c \
"import json,sys; ks=json.load(sys.stdin).get('keys',[]); print('clean' if not any('mcp test key' in k.get('name','') for k in ks) else 'FOUND — cleanup failed')"
```
@@ -191,7 +191,7 @@ mcporter call --http-url "$MCP_URL" --tool unraid \
# Prerequisite: create a dedicated test remote pointing away from real backup destination
# (use rclone create_remote first, or configure mcp-test-remote manually)
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"disk","subaction":"flash_backup","remote_name":"mcp-test-remote","source_path":"/boot","destination_path":"/flash-backup-test","confirm":true}' --output json
```
@@ -217,7 +217,7 @@ Removing a plugin cannot be undone without a full re-install. Test via `tests/sa
```bash
# If live testing is necessary (intentional removal only):
mcporter call --http-url "$MCP_URL" --tool unraid \
mcporter call --stdio-cmd "uv run unraid-mcp-server" --tool unraid \
--args '{"action":"plugin","subaction":"remove","names":["<plugin-name>"],"confirm":true}' --output json
```

View File

@@ -11,37 +11,77 @@ The marketplace catalog that lists all available plugins in this repository.
**Contents:**
- Marketplace metadata (name, version, owner, repository)
- Plugin catalog with the "unraid" skill
- Plugin catalog with the "unraid" plugin
- Categories and tags for discoverability
### 2. Plugin Manifest (`.claude-plugin/plugin.json`)
The individual plugin configuration for the Unraid skill.
The individual plugin configuration for the Unraid MCP server.
**Location:** `.claude-plugin/plugin.json`
**Contents:**
- Plugin name, version, author
- Plugin name (`unraid`), version (`1.1.2`), author
- Repository and homepage links
- Plugin-specific metadata
- `mcpServers` block that configures the server to run via `uv run unraid-mcp-server` in stdio mode
### 3. Documentation
- `.claude-plugin/README.md` - Marketplace installation guide
- Updated root `README.md` with plugin installation section
### 3. Validation Script
- `scripts/validate-marketplace.sh` — Automated validation of marketplace structure
### 4. Validation Script
- `scripts/validate-marketplace.sh` - Automated validation of marketplace structure
## MCP Tools Exposed
The plugin registers **3 MCP tools**:
| Tool | Purpose |
|------|---------|
| `unraid` | Primary tool — `action` (domain) + `subaction` (operation) routing, ~107 subactions across 15 domains |
| `diagnose_subscriptions` | Inspect WebSocket subscription connection states and errors |
| `test_subscription_query` | Test a specific GraphQL subscription query (allowlisted fields only) |
### Calling Convention
All Unraid operations go through the single `unraid` tool:
```
unraid(action="docker", subaction="list")
unraid(action="system", subaction="overview")
unraid(action="array", subaction="parity_status")
unraid(action="vm", subaction="list")
unraid(action="live", subaction="cpu")
```
### Domains (action=)
| action | example subactions |
|--------|--------------------|
| `system` | overview, array, network, metrics, services, ups_devices |
| `health` | check, test_connection, diagnose, setup |
| `array` | parity_status, parity_start, start_array, add_disk |
| `disk` | shares, disks, disk_details, logs |
| `docker` | list, details, start, stop, restart |
| `vm` | list, details, start, stop, pause, resume |
| `notification` | overview, list, create, archive, archive_all |
| `key` | list, get, create, update, delete |
| `plugin` | list, add, remove |
| `rclone` | list_remotes, config_form, create_remote |
| `setting` | update, configure_ups |
| `customization` | theme, set_theme, sso_enabled |
| `oidc` | providers, configuration, validate_session |
| `user` | me |
| `live` | cpu, memory, array_state, log_tail, notification_feed |
Destructive subactions (e.g. `stop_array`, `force_stop`, `delete`) require `confirm=True`.
## Installation Methods
### Method 1: GitHub Distribution (Recommended for Users)
Once you push this to GitHub, users can install via:
Once pushed to GitHub, users install via:
```bash
# Add your marketplace
# Add the marketplace
/plugin marketplace add jmagar/unraid-mcp
# Install the Unraid skill
# Install the Unraid plugin
/plugin install unraid @unraid-mcp
```
@@ -59,7 +99,7 @@ For testing locally before publishing:
### Method 3: Direct URL
Users can also install from a specific commit or branch:
Install from a specific branch or commit:
```bash
# From specific branch
@@ -75,14 +115,14 @@ Users can also install from a specific commit or branch:
unraid-mcp/
├── .claude-plugin/ # Plugin manifest + marketplace manifest
│ ├── plugin.json # Plugin configuration (name, version, mcpServers)
── marketplace.json # Marketplace catalog
│ └── README.md # Marketplace installation guide
├── skills/unraid/ # Skill documentation and helpers
│ ├── SKILL.md # Skill documentation
│ ├── README.md # Plugin documentation
│ ├── examples/ # Example scripts
│ ├── scripts/ # Helper scripts
│ └── references/ # API reference docs
── marketplace.json # Marketplace catalog
├── unraid_mcp/ # Python package (the actual MCP server)
│ ├── main.py # Entry point
│ ├── server.py # FastMCP server registration
│ ├── tools/unraid.py # Consolidated tool (all 3 tools registered here)
│ ├── config/ # Settings management
│ ├── core/ # GraphQL client, exceptions, shared types
│ └── subscriptions/ # Real-time WebSocket subscription manager
└── scripts/
└── validate-marketplace.sh # Validation tool
```
@@ -90,15 +130,15 @@ unraid-mcp/
## Marketplace Metadata
### Categories
- `infrastructure` - Server management and monitoring tools
- `infrastructure` Server management and monitoring tools
### Tags
- `unraid` - Unraid-specific functionality
- `monitoring` - System monitoring capabilities
- `homelab` - Homelab automation
- `graphql` - GraphQL API integration
- `docker` - Docker container management
- `virtualization` - VM management
- `unraid` Unraid-specific functionality
- `monitoring` System monitoring capabilities
- `homelab` Homelab automation
- `graphql` GraphQL API integration
- `docker` Docker container management
- `virtualization` VM management
## Publishing Checklist
@@ -109,10 +149,10 @@ Before publishing to GitHub:
./scripts/validate-marketplace.sh
```
2. **Update Version Numbers**
- Bump version in `.claude-plugin/marketplace.json`
- Bump version in `.claude-plugin/plugin.json`
- Update version in `README.md` if needed
2. **Update Version Numbers** (must be in sync)
- `pyproject.toml` → `version = "X.Y.Z"` under `[project]`
- `.claude-plugin/plugin.json` → `"version": "X.Y.Z"`
- `.claude-plugin/marketplace.json` → `"version"` in both `metadata` and `plugins[]`
3. **Test Locally**
```bash
@@ -123,33 +163,38 @@ Before publishing to GitHub:
4. **Commit and Push**
```bash
git add .claude-plugin/
git commit -m "feat: add Claude Code marketplace configuration"
git commit -m "chore: bump marketplace to vX.Y.Z"
git push origin main
```
5. **Create Release Tag** (Optional)
5. **Create Release Tag**
```bash
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push origin vX.Y.Z
```
## User Experience
After installation, users will:
After installation, users can:
1. **See the skill in their skill list**
```bash
/skill list
1. **Invoke Unraid operations directly in Claude Code**
```
unraid(action="system", subaction="overview")
unraid(action="docker", subaction="list")
unraid(action="health", subaction="check")
```
2. **Access Unraid functionality directly**
- Claude Code will automatically detect when to invoke the skill
- Users can explicitly invoke with `/unraid`
2. **Use the credential setup tool on first run**
```
unraid(action="health", subaction="setup")
```
This triggers elicitation to collect and persist credentials to `~/.unraid-mcp/.env`.
3. **Have access to all helper scripts**
- Example scripts in `examples/`
- Utility scripts in `scripts/`
- API reference in `references/`
3. **Monitor live data via subscriptions**
```
unraid(action="live", subaction="cpu")
unraid(action="live", subaction="log_tail")
```
## Maintenance
@@ -157,31 +202,21 @@ After installation, users will:
To release a new version:
1. Make changes to the plugin
2. Update version in `.claude-plugin/plugin.json`
3. Update marketplace catalog in `.claude-plugin/marketplace.json`
4. Run validation: `./scripts/validate-marketplace.sh`
5. Commit and push
1. Make changes to the plugin code
2. Update version in `pyproject.toml`, `.claude-plugin/plugin.json`, and `.claude-plugin/marketplace.json`
3. Run validation: `./scripts/validate-marketplace.sh`
4. Commit and push
Users with the plugin installed will see the update available and can upgrade with:
Users with the plugin installed will see the update available and can upgrade:
```bash
/plugin update unraid
```
### Adding More Plugins
To add additional plugins to this marketplace:
1. Create new plugin directory: `skills/new-plugin/`
2. Add plugin manifest: `skills/new-plugin/.claude-plugin/plugin.json`
3. Update marketplace catalog: add entry to `.plugins[]` array in `.claude-plugin/marketplace.json`
4. Validate: `./scripts/validate-marketplace.sh`
## Support
- **Repository:** https://github.com/jmagar/unraid-mcp
- **Issues:** https://github.com/jmagar/unraid-mcp/issues
- **Documentation:** See `.claude-plugin/README.md` and `skills/unraid/README.md`
- **Destructive Actions:** `docs/DESTRUCTIVE_ACTIONS.md`
## Validation
@@ -198,5 +233,3 @@ This checks:
- Plugin structure
- Source path accuracy
- Documentation completeness
All 17 checks must pass before publishing.

View File

@@ -2,6 +2,26 @@
This guide covers how to publish `unraid-mcp` to PyPI so it can be installed via `uvx` or `pip` from anywhere.
## Package Overview
**PyPI package name:** `unraid-mcp`
**Entry point binary:** `unraid-mcp-server` (also aliased as `unraid-mcp`)
**Current version:** `1.1.2`
The package ships a FastMCP server exposing **3 MCP tools**:
- `unraid` — primary tool with `action` + `subaction` routing (~107 subactions, 15 domains)
- `diagnose_subscriptions` — WebSocket subscription diagnostics
- `test_subscription_query` — test individual GraphQL subscription queries
Tool call convention: `unraid(action="docker", subaction="list")`
### Version Sync Requirement
When bumping the version, **all three files must be updated together**:
- `pyproject.toml``version = "X.Y.Z"` under `[project]`
- `.claude-plugin/plugin.json``"version": "X.Y.Z"`
- `.claude-plugin/marketplace.json``"version"` in both `metadata` and `plugins[]`
## Prerequisites
1. **PyPI Account**: Create accounts on both:
@@ -40,7 +60,7 @@ Before publishing, update the version in `pyproject.toml`:
```toml
[project]
version = "1.0.0" # Follow semantic versioning: MAJOR.MINOR.PATCH
version = "1.1.2" # Follow semantic versioning: MAJOR.MINOR.PATCH
```
**Semantic Versioning Guide:**
@@ -82,8 +102,8 @@ uv run python -m build
```
This creates:
- `dist/unraid_mcp-VERSION-py3-none-any.whl` (wheel)
- `dist/unraid_mcp-VERSION.tar.gz` (source distribution)
- `dist/unraid_mcp-1.1.2-py3-none-any.whl` (wheel)
- `dist/unraid_mcp-1.1.2.tar.gz` (source distribution)
### 4. Validate the Package
@@ -156,7 +176,7 @@ UNRAID_API_URL=https://your-server uvx unraid-mcp-server
**Benefits of uvx:**
- No installation required
- Automatic virtual environment management
- Always uses the latest version (or specify version: `uvx unraid-mcp-server@1.0.0`)
- Always uses the latest version (or specify version: `uvx unraid-mcp-server@1.1.2`)
- Clean execution environment
## Automation with GitHub Actions (Future)