- Python 93.3%
- Shell 4.8%
- Dockerfile 1.9%
Lidarr applies addOptions monitor:'none' server-side AFTER our flags: the new artist row lands monitored=false (hiding all its albums from wanted/missing and Soularr), and the initial RefreshArtist rewrites album rows monitored=false racing the immediate monitor PUT. Now: verify-retry loop (4x6s) re-monitors the album until the refresh settles + repairs the artist flag; already_in_library early-return also repairs an unmonitored artist instead of skipping it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRSnHJdvjZWGfqd7v4mf7c |
||
|---|---|---|
| arr | ||
| crowdsec | ||
| docker | ||
| forgejo | ||
| headscale | ||
| homeassistant | ||
| host | ||
| jellyfin | ||
| jellyseerr | ||
| matrix | ||
| npm | ||
| pihole | ||
| qbit | ||
| searxng | ||
| system | ||
| .gitignore | ||
| .leak-allowlist | ||
| _safety.py | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| INSTALL.md | ||
| install.sh | ||
| README.md | ||
mcp-stack
Docker-compose stack of Model Context Protocol
servers. Each subdirectory is one server; each server exposes a small set of
tools that an MCP client (Claude Code, the Python SDK, etc.) can call over
HTTP/SSE on 127.0.0.1.
Setup: git clone … && cd mcp-stack && ./install.sh. See INSTALL.md.
Services
8 servers, 68 tools, all bound to 127.0.0.1.
| Service | Port | Tools | Backend |
|---|---|---|---|
mcp-jellyfin |
8765 | 10 | Jellyfin :8096 |
mcp-arr |
8766 | 14 | Sonarr :8989 + Radarr :7878 |
mcp-qbit |
8767 | 7 | qBittorrent :8080 |
mcp-jellyseerr |
8768 | 3 | Jellyseerr :5055 |
mcp-system |
8769 | 6 | host metrics via psutil |
mcp-matrix |
8770 | 8 | Synapse C2S + admin API |
mcp-npm |
8771 | 11 | Nginx Proxy Manager :81 |
mcp-pihole |
8772 | 9 | Pi-hole v6 :5080 |
Port range 8770–8779 reserved for future servers.
Tools
* mutates state. No extra confirmation at the MCP layer (see Security).
mcp-jellyfin — search_library, get_recently_added, get_active_sessions, get_item_details, browse_by_genre, get_libraries, get_continue_watching, get_next_up, mark_watched, mark_unwatched
mcp-arr — get_series, get_series_episodes, get_movie, get_queue, get_health, get_history, get_calendar, get_blocklisted, get_quality_profiles, lookup_movie, lookup_series, trigger_search, add_movie, add_series*
mcp-qbit — list_downloads, get_transfer_stats, get_torrent_details, get_categories, force_recheck, pause_torrent, resume_torrent*
mcp-jellyseerr — list_requests, get_request, search_jellyseerr
mcp-system — get_disk_usage, get_disk_usage_all, get_memory, get_cpu, get_temps, get_uptime
mcp-matrix — send_message*, get_recent_messages, list_rooms, get_room_members, get_room_info, get_admin_stats, list_admin_users, whoami
mcp-npm — list_proxy_hosts, get_proxy_host, delete_proxy_host, enable_proxy_host, disable_proxy_host, add_proxy_host, list_certificates, get_certificate, renew_certificate*, list_redirection_hosts, get_server_info
mcp-pihole — get_query_stats, get_top_clients, get_top_blocked_domains, get_top_permitted_domains, get_query_history, list_blocklists, get_blocking_status, disable_blocking, enable_blocking
Architecture
- One subdirectory per server:
Dockerfile,server.py,requirements.txt. network_mode: host— each server binds127.0.0.1:<port>and reaches its backend vialocalhost.- Secrets read from
.envat the repo root (symlink/opt/yams/.envif you already have one). mcp-systemalso needspid: hostand/:/host:roso psutil sees host processes and filesystems. Tool args take host paths (/srv/media); the server maps them to/host/srv/mediainternally.- Tool return shapes are slimmed via
_slim_*()helpers — LLM-friendly, smaller payloads.
Connecting a client
Claude Code — add to .mcp.json:
{
"mcpServers": {
"mcp-jellyfin": { "type": "sse", "url": "http://localhost:8765/sse" },
"mcp-arr": { "type": "sse", "url": "http://localhost:8766/sse" },
"mcp-qbit": { "type": "sse", "url": "http://localhost:8767/sse" },
"mcp-jellyseerr": { "type": "sse", "url": "http://localhost:8768/sse" },
"mcp-system": { "type": "sse", "url": "http://localhost:8769/sse" },
"mcp-matrix": { "type": "sse", "url": "http://localhost:8770/sse" },
"mcp-npm": { "type": "sse", "url": "http://localhost:8771/sse" },
"mcp-pihole": { "type": "sse", "url": "http://localhost:8772/sse" }
}
}
Python SDK:
from mcp import ClientSession
from mcp.client.sse import sse_client
async with sse_client("http://localhost:8765/sse") as (r, w):
async with ClientSession(r, w) as session:
await session.initialize()
result = await session.call_tool("search_library", {"query": "Mad Max", "limit": 5})
Gotcha for custom clients: FastMCP returns list-typed results as N separate text content blocks (one per element), not one JSON list. Concat them:
blocks = [json.loads(b.text) for b in res.content if hasattr(b, "text")]
return blocks[0] if len(blocks) == 1 else blocks
Security
Loopback bind only. The backend still checks its own API keys. The calling
LLM is the write-gate — there's no extra "are you sure?" at the MCP layer
for add_movie, delete_proxy_host, disable_blocking, etc. If you need
write gating, fork and split write tools behind a separate port.
Operating it
docker compose up -d # bring up
docker compose ps
docker logs -f mcp-matrix
docker compose up -d --build mcp-pihole # rebuild one after edit
docker compose down
Adding a new MCP server
mkdir <service>/withDockerfile,server.py,requirements.txt.pihole/is small and easy to copy.- Pick a free port in 8770–8779. Reserve it in the table above.
- Add a
services:entry todocker-compose.yml—network_mode: host, only the env vars the server needs,:?on the required ones. - Tool docstrings describe inputs + output + when to call. Slim returns to fields useful to an LLM.
docker compose up -d --build mcp-<service>and smoke-test.- Add the SSE URL to clients'
.mcp.json.
License
Source-available, no formal license. Fork freely.