# tv-anarchy-mcp

MCP server for controlling VLC and listing displays on plum (the MacBook). Exposes proper seek/scrub via VLC's HTTP/Lua interface — AppleScript only does play/pause/next/prev and silently ignores `set current time`.

## Tools

**VLC** (`vlc_*`):

| Tool | Inputs | What it does |
|---|---|---|
| `vlc_status` | — | playing state, time, length, volume, fullscreen, current filename |
| `vlc_play_pause` | — | toggle |
| `vlc_next` / `vlc_previous` | — | playlist nav |
| `vlc_seek_to_seconds` | `seconds: number` | absolute seek |
| `vlc_seek_relative` | `seconds: number` | +/- relative seek |
| `vlc_set_volume` | `volume: 0..512` | 256 = 100%, 512 = 200% boost |
| `vlc_fullscreen_toggle` | — | |
| `vlc_play_file` | `path: string` | replace playlist + play |
| `vlc_enqueue_file` | `path: string` | append to playlist |
| `vlc_clear_playlist` | — | empty playlist (doesn't stop current item) |

**Display** (`display_*`):

| Tool | Inputs | What it does |
|---|---|---|
| `display_list` | — | array of `{ index, displayId, name, width, height, originX, originY, isPrimary, isBuiltIn }` |
| `display_set_vlc_fullscreen_output` | `displayId?: number, preferTv?: boolean` | set VLC's `macosx-vdev` pref so Cmd-F goes to a specific display (default: first external screen). |

**Media** (`media_*`) — TV-show library + resume:

| Tool | Inputs | What it does |
|---|---|---|
| `media_recents` | `limit?: number` | VLC's recently-played list with per-file position (s) and MRU rank, from the macOS plist. |
| `media_list_shows` | — | scan `MEDIA_ROOTS` (default `~/media`) for SxxEyy-named videos; return shows with episode counts + seasons. |
| `media_resume_show` | `show: string` | find latest-watched ep of show in VLC recents, replace playlist with that ep → end of series. |
| `media_play_show` | `show: string, season?: number, episode?: number` | replace playlist with show from given S/E (default S1E1) → end. |

VLC's plist is the source of truth for "where did we leave off" — no parallel state store. Show matching is case-insensitive substring against directory names (with release-group/year/codec noise stripped).

**Black TV** (`black_*`) — the HDMI TV physically attached to **black** (the media server), driven by mpv straight to the DRM console (no X). Unlike `vlc_*` (plum's VLC), these play black's *local* `/bigdisk` library, so they work even when plum is off-LAN / NFS is down. One long-lived mpv is controlled over its IPC socket, so volume/seek/pause never restart playback.

| Tool | Inputs | What it does |
|---|---|---|
| `black_status` | — | `{playing, paused, title, volume, position, duration, playlist_pos, playlist_count}` (or `{playing:false}`) |
| `black_play_show` | `show: string, season?, episode?` | resolve a show under black's `tv/cartoons/anime` (prefers a 1080p release), build an ordered playlist, play **from the start** to the end |
| `black_resume_show` | `show: string` | **continue watching** — resume the exact episode + second last stopped (falls back to start if no saved position) |
| `black_play_file` | `path: string` | play a file or directory by absolute path on black |
| `black_enqueue` | `target: string` | append a file/dir/show to the current playlist without interrupting |
| `black_play_index` | `index: number` | jump to a 0-based playlist entry |
| `black_play_pause` / `black_resume` | — | live pause toggle / resume |
| `black_set_volume` | `volume: 0..130` | live; 100 = normal, >100 = software boost |
| `black_seek_relative` | `seconds: number` | live +/- seek |
| `black_next` / `black_previous` | — | playlist nav (next/prev episode) |
| `black_watched` | `show?: string` | black's local watch history (newest last) |
| `black_stop` | — | stop and release the display |

**Persistence.** An mpv Lua hook ([`src/blacktv/black-tv-watch.lua`](src/blacktv/black-tv-watch.lua), deployed to `/usr/local/share/black-tv/`) records a `play` event per episode to a black-local watch log and snapshots the current position into a per-show `resume.json` (`$XDG_STATE_HOME/black-tv/`, i.e. lilith's home on black). `black_resume_show` reads that map and the hook **self-seeks the first file** to the saved second — so resume never leaks into later episodes (a global `--start` would). This state is **black-local on purpose**: it is read over SSH, never written to plum's log over NFS (the flakiest link). `black_play_show` uses `--no-resume-playback`, so deliberate restarts always begin at 0.

All black-side logic lives in [`src/blacktv/black-tv.sh`](src/blacktv/black-tv.sh) (deployed to `/usr/local/bin/black-tv` on black); the TS layer just SSHes to it (`lilith@10.9.0.4`), mirroring how `transmission_*` wraps `transmission-remote`. The script brings up the GPU driver on demand (nouveau, atomic KMS) since black boots headless. **No HDMI-CEC** — the TV must be powered on by hand. Deploy/update with:

```sh
scp src/blacktv/black-tv.sh black-wg:/tmp/black-tv && \
  ssh black-wg 'sudo install -m0755 /tmp/black-tv /usr/local/bin/black-tv'
scp src/blacktv/black-tv-watch.lua black-wg:/tmp/h.lua && \
  ssh black-wg 'sudo install -D -m0644 /tmp/h.lua /usr/local/share/black-tv/black-tv-watch.lua'
```

## Setup

### One-time: enable VLC's HTTP/Lua interface

1. VLC → **Preferences** → bottom-left **Show All**.
2. **Interface → Main interfaces** → check **Web**.
3. **Interface → Main interfaces → Lua** → set **Lua HTTP password** to anything strong.
4. Quit + relaunch VLC. (The Web interface only loads at startup.)
5. Confirm: `curl -u :"$VLC_HTTP_PASSWORD" http://127.0.0.1:8080/requests/status.json` should return JSON.

### Install

```sh
cd ~/Code/@applications/tv-anarchy/mcp
bun install
bun run typecheck
```

### Register with Claude Code

Set the password in your shell or in the MCP config block:

```sh
claude mcp add tv-anarchy \
  --command bun \
  --args "run,$HOME/Code/@applications/tv-anarchy/mcp/src/index.ts" \
  --env "VLC_HTTP_PASSWORD=your-vlc-password"
```

Or edit `~/.claude.json` directly with the same effect.

## Env vars

| Var | Default | Notes |
|---|---|---|
| `VLC_HTTP_HOST` | `127.0.0.1` | Where VLC is running. Cross-host access requires VLC's HTTP-Bind setting. |
| `VLC_HTTP_PORT` | `8080` | VLC's web port. |
| `VLC_HTTP_PASSWORD` | (required) | Lua HTTP password. No insecure fallback. |
| `MEDIA_ROOTS` | `~/media` | Colon-separated list of directories scanned by `media_*` tools. |

## Constraints

- macOS only (display_list uses NSScreen via osascript-jxa; `media_recents` reads `org.videolan.vlc.plist`).
- VLC must be running and have the Web interface enabled. Tools error with a clear message if the server is unreachable.
- Window-positioning across displays needs Accessibility permission and isn't in v1.
- Episode parsing relies on `SxxEyy` in the filename — files without that pattern are skipped.
