Polyhydra Skills / Uncategorized
Control the arch-server OBS Studio instance (scenes, sources, browser sources, stream/record) via the obs-websocket-mcp MCP server, or re-register/repair that server if it's missing. Use when the user wants to add a browser/media source to OBS, switch scenes, start/stop streaming or recording, or asks to "wire up OBS" / "use the OBS skill".
Drop this in — save the block below as ~/.claude/skills/obs-websocket/SKILL.md, or run:
mkdir -p ~/.claude/skills/obs-websocket cat > ~/.claude/skills/obs-websocket/SKILL.md <<'EOF' # (paste the full source block below into this file) EOF
# obs-websocket
Agentic control of the OBS Studio instance on arch-server (`192.168.0.173`,
same box `obs-websocket-mcp` runs on) via the `obs-websocket-mcp` MCP server
— repo: `github.com/lancer1977/obs-websocket-mcp` (private), cloned at
`~/code/obs-websocket-mcp` (canonical, tracks `main`) with active development
in a worktree at `~/worktrees/obs-websocket-mcp-v1` until the V1 PR merges.
## First: check the MCP tools are actually live
The `obs.*` tools (`obs.get_status`, `obs.list_scenes`, `obs.list_sources`,
`obs.set_current_scene`, `obs.set_source_enabled`, `obs.remove_source`,
`obs.create_browser_source`, `obs.start_stream`/`stop_stream`,
`obs.start_record`/`stop_record`, `obs.get_input_settings`,
`obs.get_scene_item_transform`, `obs.get_source_filters`, `obs.export_snapshot`)
come from the registered MCP server, not from this skill directly. If they're
not showing up as available tools, the server isn't registered in this session
— see "Registering the MCP server" below.
## Credentials
Live in `~/.config/secrets/obs.env` (`OBS_HOST`, `OBS_PORT`, `OBS_PASSWORD`,
`OBS_TIMEOUT_SECONDS`, `OBS_ALLOW_WRITES`) — `OBS_HOST=127.0.0.1` since OBS
and the MCP server run on the same box (arch-server), which sidesteps any
firewall question entirely. **Never** hardcode these values or put
`OBS_PASSWORD` in a command string you're about to show/echo — always
`source` the file and reference `$OBS_PASSWORD` as a variable, same pattern
as the `seq`/`cloudflare-dns` skills.
## Hard rule: writes are off by default
`OBS_ALLOW_WRITES=false` is the default. Every mutating tool
(`set_current_scene`, `set_source_enabled`, `create_browser_source`,
`start_stream`, `stop_stream`, `start_record`, `stop_record`) raises
`UnauthorizedError` until it's flipped to `true` — this is a live A/V
control surface (it can put something on someone's actual stream), so
treat turning writes on as a real decision, not a default to reach for.
To enable: edit `OBS_ALLOW_WRITES=true` in `~/.config/secrets/obs.env`,
then re-run the registration command below (env vars are baked in at
registration time, not read live per-call).
## Registering the MCP server
Background/sandboxed Claude Code sessions cannot write their own MCP config
(`~/.claude/.config.json`) — the command silently no-ops even when it
reports success. Registration has to be run by the user directly:
```
source ~/.config/secrets/obs.env && claude mcp add obs-websocket-mcp -s user \
-e PYTHONPATH=<path to obs-websocket-mcp checkout, e.g. ~/code/obs-websocket-mcp once merged> \
-e OBS_HOST="$OBS_HOST" -e OBS_PORT="$OBS_PORT" -e OBS_PASSWORD="$OBS_PASSWORD" \
-e OBS_TIMEOUT_SECONDS="$OBS_TIMEOUT_SECONDS" -e OBS_ALLOW_WRITES="$OBS_ALLOW_WRITES" \
-- <path>/.venv/bin/python -m app.main
```
`claude mcp get obs-websocket-mcp` confirms it's actually registered — don't
trust the "Added stdio MCP server..." success message alone in a background
session, verify with `get`.
## Verified-live vs. mock-only (2026-08-19)
`obsws_python.ReqClient` has **no consistent kwarg-naming convention across
methods** — some take snake_case, some camelCase, some a bare positional
`name`. The connector's unit tests only ever ran against a mock I wrote
myself, which used the wrong convention for several methods; this wasn't
caught until testing against the real OBS instance. Confirmed correct via
`inspect.signature`/`getsource` on the installed package, not guessing:
- `get_current_program_scene()`, `set_current_program_scene(name)`,
`get_scene_item_list(name)` — bare positional `name`.
- `set_scene_item_enabled(scene_name, item_id, enabled)` — snake_case.
- `create_input(sceneName, inputName, inputKind, inputSettings, sceneItemEnabled)`
— camelCase.
**Live-verified end-to-end** (against real OBS 32.2.1, websocket 5.7.4):
`get_status`, `list_scenes`, `list_sources`, `create_browser_source`,
`get_input_settings`, `get_scene_item_transform`, `get_source_filters`,
`export_snapshot` — the snapshot tools returned a clean, error-free dump of
both live scenes (11 sources total) on first real run, no kwarg-naming
surprises.
**Still mock-only, not yet exercised against real OBS**: `set_current_scene`,
`set_source_enabled`, `start_stream`/`stop_stream`, `start_record`/`stop_record`.
If any of these misbehave, check the actual installed `obsws_python`
signature first (`python -c "import obsws_python as o, inspect;
print(inspect.signature(o.ReqClient.<method>))"`) before assuming the bug is
elsewhere — this library's inconsistency is the most likely culprit.
## Adding chop-it (or any URL) as a test browser source
Once writes are enabled and the server is registered:
`obs.create_browser_source` with `scene_name`, `source_name`, `url` (plus
optional `width`/`height`, default 1920x1080). See
[[project_chop_it_status_202608]] for the live chop-it HLS test URL and a
known caveat about its manifest-replacement behavior in OBS Media/Browser
sources.
## Scene/source inventory (snapshot)
Four read-only tools introspect OBS layout and configuration — they work
regardless of `OBS_ALLOW_WRITES` since they only query, never modify:
- `obs.get_input_settings` (args: `source_name`) — returns `{kind, settings}`
for a single input/source (e.g. a browser source's URL, width, height).
- `obs.get_scene_item_transform` (args: `scene_name`, `item_id`) — returns
a scene item's transform dict (position, scale, rotation, crop, bounds).
- `obs.get_source_filters` (args: `source_name`) — returns a list of filters
applied to a source (name, kind, enabled, settings).
- `obs.export_snapshot` (no args) — full nested dump: every scene and every
source's kind, settings, transform, and filters in a single call.
**Snapshot convention:** `obs.export_snapshot` results get written to
`snapshots/<timestamp>.json` in the `obs-websocket-mcp` repo, git-tracked,
so scene layouts are diffable over time and act as a durable journal.
Built and live-verified as [obs-websocket-mcp#5](https://github.com/lancer1977/obs-websocket-mcp/issues/5).
**Automatic on stream start/stop**: `obs.start_stream` and `obs.stop_stream`
each call `_capture_snapshot()` internally before performing the action —
`snapshots/<timestamp>_stream-start.json` / `..._stream-stop.json`, no
manual `export_snapshot` call needed for real stream sessions. Best-effort:
a snapshot failure never blocks the actual stream action (caught and
swallowed in `app/tool_handlers.py`). See
[obs-websocket-mcp#6](https://github.com/lancer1977/obs-websocket-mcp/issues/6).
Snapshots are **capture-only for now** — no restore/apply-back-into-OBS
capability exists yet (a deliberately separate, higher-risk mutating
follow-up, not yet filed).
## DreadTV's parallel adapter
`~/code/dreadtv`'s `ObsSourceAdapter` (`src/dreadtv/adapters.py`) is a
separate, older obs-websocket client this package was extracted from.
DreadTV issue #353 tracks refactoring it to depend on this package instead
— blocked on `obs-websocket-mcp`'s V1 PR merging.