tv-anarchy/mcp/README.md

6.7 KiB

plum-control-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, 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 (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:

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

cd ~/Code/@applications/plum-control-mcp
bun install
bun run typecheck

Register with Claude Code

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

claude mcp add plum-control \
  --command bun \
  --args "run,$HOME/Code/@applications/plum-control-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.