| .. | ||
| src | ||
| .gitignore | ||
| bun.lock | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
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
- VLC → Preferences → bottom-left Show All.
- Interface → Main interfaces → check Web.
- Interface → Main interfaces → Lua → set Lua HTTP password to anything strong.
- Quit + relaunch VLC. (The Web interface only loads at startup.)
- Confirm:
curl -u :"$VLC_HTTP_PASSWORD" http://127.0.0.1:8080/requests/status.jsonshould 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_recentsreadsorg.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
SxxEyyin the filename — files without that pattern are skipped.