Content
# Tvheadend DVR MCP server (read + create only)
A minimal Model Context Protocol server for using Tvheadend purely as a DVR
from a voice/speech assistant. It exposes **four** tools and nothing else:
| Tool | What it does | Tvheadend endpoint |
|------|--------------|--------------------|
| `search_epg` | Find upcoming programmes ("when is the F1 race?") | `GET /api/epg/events/grid` |
| `schedule_recording` | Create a one-off timer from an EPG event | `POST /api/dvr/entry/create_by_event` |
| `list_upcoming_recordings` | Show scheduled / in-progress timers | `GET /api/dvr/entry/grid_upcoming` |
| `list_finished_recordings` | Show successfully completed recordings | `GET /api/dvr/entry/grid_finished` |
There are **no** tools to cancel, delete, edit or re-record. That is the whole
point of this server.
## Why the access design matters
Tvheadend's permission model does **not** offer a "create but never delete your
own recording" privilege — a user that can schedule recordings can generally
also delete its own entries through the raw API. So "no remove/change" is
enforced two ways here:
1. **This server never issues a delete or modify request.** The code paths
simply do not exist.
2. **You give it a dedicated, low-privilege Tvheadend user.**
## Step 1 — create a locked-down Tvheadend user
In the Tvheadend web UI: **Configuration → Users → Access Entries → Add**.
Create an entry (e.g. username `voice`) and enable **only**:
- **Web interface** — required for any `/api/...` call.
- **Video recorder** — required to create DVR timers.
Leave **disabled**: *Administrator*, *All DVR*, *All DVR (read/write)*,
*HTSP*, *Streaming*, *Change settings*, etc. Set a strong password.
Optional extra hardening:
- Restrict the entry by source **IP / prefix** to the machine running this
server.
- Set **Min/Max channel number** if you only want certain channels visible.
- Assign a specific **DVR config profile** and point the server at it with
`TVH_DVR_CONFIG`.
> Note: this user can still technically delete *its own* timers via the raw
> API. The guarantee that it never happens comes from this server not exposing
> those operations — keep the credentials private to the server.
## Step 2 — install
Requires Python 3.9+.
```bash
pip install -r requirements.txt
```
## Step 3 — configure (environment variables)
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `TVH_URL` | yes | – | e.g. `http://tvheadend.lan:9981` (no trailing `/`) |
| `TVH_USER` | yes | – | the `voice` user above |
| `TVH_PASS` | yes | – | its password |
| `TVH_AUTH` | no | `digest` | `digest` or `basic` (server auto-falls back once) |
| `TVH_TZ` | no | system local | e.g. `Europe/Helsinki` |
| `TVH_DVR_CONFIG` | no | first enabled profile | DVR profile name or uuid |
| `TVH_VERIFY_SSL` | no | `true` | set `false` for self-signed HTTPS |
| `TVH_EPG_LIMIT` | no | `8` | default max EPG results |
## Step 4 — register with an MCP client (e.g. Claude Desktop)
In `claude_desktop_config.json`:
```json
{
"mcpServers": {
"tvheadend-dvr": {
"command": "python3",
"args": ["/absolute/path/to/tvheadend_dvr_mcp.py"],
"env": {
"TVH_URL": "http://tvheadend.lan:9981",
"TVH_USER": "voice",
"TVH_PASS": "your-strong-password",
"TVH_TZ": "Europe/Helsinki"
}
}
}
}
```
## Typical spoken flow
1. *"When is the next Formula 1 race?"* → `search_epg("Formula 1")` returns the
programme plus an `event_id`.
2. *"Record it."* → `schedule_recording(event_id=...)`.
3. *"What have I got scheduled?"* → `list_upcoming_recordings()`.
4. *"What did I record this week?"* → `list_finished_recordings()`.
## Quick manual check (no MCP client needed)
You can verify the server starts and connects by setting the env vars and
running it; it speaks MCP over stdio, so it will wait for a client. To sanity
check connectivity to Tvheadend independently:
```bash
curl --digest -u voice:PASS \
'http://tvheadend.lan:9981/api/dvr/entry/grid_upcoming?limit=5'
```