scenecap is a local, chat-controlled recording orchestrator for macOS. OBS
remains the capture engine: it owns sources, permissions, encoding, and output
files. A singleton TypeScript MCP sidecar provides the narrow, safe control
surface that Claude Code and Codex use to inspect and operate that OBS setup.
The intended workflow is to ask an agent to discover and preview capture targets, configure the requested OBS sources, and explicitly start or stop a recording. The result can keep independent sources for editing rather than baking a composited layout at record time.
Issue #3 supersedes the direct Go + FFmpeg capture path as the active
architecture. That implementation is retained in this repository for
reference and regression evidence; it is not deleted as part of the
migration. FFmpeg is no longer in the capture path. ffprobe may remain an
optional, best-effort post-recording validator where it adds useful evidence.
The TypeScript server exposes persistent capture configuration in addition to
read-only discovery and sensitive preview. get_status reports OBS/version,
screen-capture capability, and Source Record filter capability.
list_capture_targets reports configured capture inputs and the currently
selectable windows without changing OBS. preview_capture_target accepts only
one opaque reference returned by that discovery: it screenshots a matching
configured source without mutation, or creates a disabled temporary
screen_capture input in an isolated temporary scene for an unconfigured
window. The temporary path momentarily uses OBS Studio Mode's Preview scene
(never Program), enables the item only there to render it, restores the prior
Preview and Studio Mode state, then removes both resources. Preview images can
contain sensitive on-screen content. Discovery also reports explicit
limitations for display, application, and camera lists that this OBS build
cannot safely expose over WebSocket; already-configured selections remain
visible when OBS reports their explicit target identifiers. Recording state
and output reporting remain later status evolution, after recording controls
exist.
configure_capture_target accepts one opaque target reference from discovery
and either an opaque existing source reference or a validated name for a new
allowlisted capture input. It persistently updates or creates that source in
OBS and ensures it belongs to the selected existing scene, or the current
Program scene by default. get_session reports the shared sidecar-owned
configuration session: a stable session ID, revision, configured source and
target references, scene identities, and a non-secret recovery summary. This
is the cross-client contract for later recording and restoration tools. When
an intended output size is supplied, configuration rounds it up to an even
encoder-safe size and applies it only to that source's Source Record encoder.
Each configured source receives a uniquely owned source_record_filter set to
record when OBS recording starts. By default it inherits the current OBS
profile's encoder, container, and output path; scenecap does not guess
hardware-specific encoder tuning. A caller may safely override an absolute
output directory, filename template, crash-resilient container (mkv,
fragmented MP4, or fragmented MOV), and intended output dimensions. It does
not alter camera presets or scene transforms, which could crop, distort, or
select an unsupported device mode.
start_recording explicitly starts OBS's global recording only when the
sidecar has at least one complete configured source and a fresh discovery
still matches every stored source/target pair. It will not adopt an OBS
recording started elsewhere. stop_recording only stops that sidecar-owned
recording, then reports OBS's global completed output path and whether the
file is currently visible on disk. A timeout, cancellation, lost OBS response,
or contradictory status becomes an explicit ambiguous session state and blocks
further recording mutations until an operator inspects OBS. These tools do not
configure, infer, or report Source Record plugin outputs; that remains a
separate per-source settings extension.
On OBS 32.2.1 for macOS, GetInputPropertiesListPropertyItems can crash OBS
when called with only an input UUID, or when obs-websocket serializes some
dynamic string-valued capture lists. Scenecap therefore sends the current
input name, permits only the verified integer-valued window property, and
reports other dynamic lists as unavailable rather than probing them.
Claude Code / Codex
│ MCP
▼
scenecap TypeScript sidecar (singleton, localhost only)
│ OBS WebSocket
▼
OBS: sources, permissions, capture, encoding, files
The sidecar is the single local owner of recording-session state. The fixed port prevents two HTTP listeners from starting, while an atomic per-user inter-process lock prevents a second sidecar on any port from becoming another owner. Future mutation tools will build on that boundary so separate agents cannot race to start or stop OBS.
If the sidecar crashes, its lock is intentionally not removed
automatically: automatic stale-lock recovery can race with a newly starting
owner. First confirm that no scenecap sidecar process remains; then inspect
and remove ~/.scenecap/mcp.lock manually before restarting. An unreadable
lock is handled the same way. The startup error distinguishes an active PID,
a stale PID, and an unreadable lock without exposing OBS credentials.
A shared Claude Code/Codex plugin package provides the connection metadata for this MCP server. We will create recording-driving skills only after using the real tools enough to identify the stable operational knowledge worth packaging.
Prerequisites: Node.js 22 or later, pnpm 10, and a running local OBS instance
with WebSocket authentication enabled. Configure OBS WebSocket to listen only
on loopback; scenecap always connects to 127.0.0.1, but it cannot constrain
the address on which OBS itself listens. Install and configure OBS Source
Record when you need isolated source files; the capability check reports
whether its filter is available.
pnpm install --frozen-lockfile
pnpm run build
pnpm startThe built sidecar listens only at http://127.0.0.1:3233/mcp. Keep it running,
then connect a local client directly:
claude mcp add --transport http scenecap http://127.0.0.1:3233/mcp
codex mcp add scenecap --url http://127.0.0.1:3233/mcpThese are local MCP connection commands, not marketplace installation
commands. The shared development bundle lives in agent-plugin/
and points both hosts at the same endpoint. It connects to the sidecar; it does
not launch it. The sidecar retains at most 16 MCP sessions and expires an idle
session after one hour, which accommodates pauses during a recording workflow
without allowing abandoned clients to accumulate indefinitely.
The sidecar always binds to 127.0.0.1. It reads configuration in this order:
- Set
SCENECAP_OBS_PASSWORDto use that password directly (the legacyOBS_WEBSOCKET_PASSWORDis also accepted). In this mode OBS is assumed to be127.0.0.1onSCENECAP_OBS_PORT, defaulting to4455. - Otherwise it reads OBS's WebSocket JSON configuration from
SCENECAP_OBS_CONFIG, or by default from~/Library/Application Support/obs-studio/plugin_config/obs-websocket/config.json. That file must provide a non-empty password; its host, if present, must be exactly127.0.0.1.SCENECAP_OBS_PORToverrides its configured port. - Set
SCENECAP_PORTto change the MCP HTTP port from its default of3233. Becauseagent-plugin/.mcp.jsoncontains the literal default endpoint, a custom port also requires updating that file (or configuring the client directly with the matching URL).
All configured ports must be numeric values from 1 through 65535.
For development validation, run:
pnpm run check
pnpm run buildpnpm run check lints and type-checks the TypeScript/config surface, runs the
server tests, and verifies that both plugin manifests and .mcp.json remain in
sync. The retained Go/FFmpeg tree is frozen, unverified legacy and is
intentionally excluded from this default check.
For a live smoke test, leave OBS and pnpm start running in one terminal, then
use the repository's installed MCP SDK client from another:
pnpm run smokeThe command connects over Streamable HTTP, lists tools, calls get_status and
list_capture_targets, and prints the responses. It does not preview or
mutate OBS.
Skills remain deliberately deferred until real sessions using these tools establish a stable workflow worth packaging.
The server will continue adding narrow tools rather than a generic OBS passthrough:
preview_capture_targetconfigure_capture_targetandget_sessionstart_recordingandstop_recordingrestore_obs_state
The tools must cover the practical capture cases: OBS Source Record for isolated source files, a phone camera source, and multi-display selection. They will discover and preview targets instead of relying on guessed window or display IDs. Configuration will account for encoder-safe aligned dimensions when an OBS source has an odd or otherwise unsupported size.
- The MCP sidecar binds to loopback only and connects only to OBS at
127.0.0.1. OBS WebSocket must be configured separately to avoid exposing its remote-control listener on the network. get_statusandlist_capture_targetsare read-only. Preview is explicit, single-target, and may expose sensitive screen content. For an unconfigured window it creates uniquely named temporary resources in an isolated scene, temporarily renders only through Studio Mode Preview, restores the prior Preview/Studio Mode state, and never sends a Program mutation. It fails closed if Studio Mode is already in use or streaming, recording, replay, or Virtual Camera output is active. Cleanup compares current Preview, Studio, and Program state before writing: external changes are left untouched and reported with conflict-specific manual recovery guidance. Temporary probes are serialized in this sidecar, but that does not coordinate other OBS clients.configure_capture_targetaccepts only discovered opaque references and allowlisted capture kinds; it never passes raw OBS settings through MCP. The sidecar retains recovery data privately andget_sessionexposes only a non-secret summary. Its Source Record filter name is derived from the OBS input UUID and is never caller-controlled; a colliding non-Source-Record filter is left untouched. A missing Source Record capability is detected before changing the source. If a later scene attachment or filter mutation fails after OBS accepted a target update, the tool records a partial state and requires inspection before retrying rather than assuming OBS reverted it.- Recording controls serialize across every MCP client in the singleton
sidecar. They revalidate configured inputs before starting, do not adopt an
externally active OBS recording, and leave uncertain transitions blocked for
manual OBS inspection rather than retrying a possibly delivered mutation.
stop_recordingreports only OBS's global output path; Source Record output paths are not inferred by recording controls even when a configured source has a Source Record filter. ffproberemains an optional future validator for an existing completed file—not a capture or encoding dependency.
See PLAN.md for the migration milestones.