Local-first MCP-oriented media search project.
This repository is currently an MVP scaffold. The safe default path is local filesystem media, local sidecar subtitles, SQLite/FTS search, deterministic mock embeddings, and a stdio MCP server. Embedded subtitle extraction, Plex, OpenSubtitles, Bazarr provider APIs, OpenAI embeddings, REST, Discord, and hosted mode remain disabled unless explicitly enabled and gated in config.
- Python package layout under
src/media_memory - CLI entrypoint:
media-memory - SQLite metadata DB with FTS5 lexical search
- LanceDB vector-store abstraction (local fallback implementation)
- Filesystem media scanner, sidecar subtitle discovery (
.srt/.vtt/.ass/.ssa), and opt-in embedded/Bazarr filesystem subtitle adapters - Subtitle parsing, normalization, and chunking
- Embedding abstraction with deterministic mock provider
- Hybrid lexical + vector search pipeline
- MCP tool surface:
search_mediafind_episodefind_scenesearch_dialogueget_scene_context
- Optional REST ASGI app under
media_memory.api, disabled by default and not used as the Docker command - Optional Discord bot command facade under
media_memory.discord_bot, disabled by default and backed only by REST API calls
PYTHONPATH=src python -m media_memory.cli.main scan /path/to/media
PYTHONPATH=src python -m media_memory.cli.main --db .media_memory/media_memory.db ingest /path/to/media
PYTHONPATH=src python -m media_memory.cli.main --db .media_memory/media_memory.db search "i am your father"
PYTHONPATH=src python -m media_memory.cli.main --db .media_memory/media_memory.db mcp-call search_dialogue --params '{"query":"winter is coming"}'Start from config.example.yaml and .env.example. The example config follows the spec-shaped sections future tasks consume: list-based media_sources, local/embedded/OpenSubtitles/Bazarr subtitle sections, metadata.prefer provider order, /data/media-memory.sqlite, /data/vectors, mock embeddings, and the default local corpus:
uv run python -c "from media_memory.config import load_config; print(load_config('config.example.yaml').mcp.allow_ingest_tools)"Environment placeholders such as ${PLEX_TOKEN}, ${OPENAI_API_KEY}, ${OPENSUBTITLES_API_KEY}, ${BAZARR_API_KEY}, and ${DISCORD_BOT_TOKEN} are resolved at load time when set, but the examples intentionally contain no credentials and all external providers remain disabled by default. subtitle_sources.embedded only invokes ffprobe/ffmpeg when both enabled and extract_with_ffmpeg are true, and extracted subtitles are written under extract_to rather than beside media files. subtitle_sources.bazarr can read subtitles that Bazarr has already placed beside media or under configured read-only roots; Bazarr API calls remain off unless api_enabled is explicitly true. The Discord bot stays disabled unless discord.enabled: true, a token is configured, and a local REST API URL is provided; its handlers call /search rather than core search or database services.
media_memory.discord_bot provides command handlers for /episode show query, /scene query, /quote query, and /movie query. The handlers use the REST /search endpoint only, format concise evidence snippets with timestamps when available, and return safe no-result/error messages. Runtime Discord wiring is optional and requires installing discord.py yourself; normal tests and local MCP usage do not require a Discord token or package.
The REST app is available as media_memory.api:create_app / media_memory.api:app for local ASGI runners, but api.enabled defaults to false and the Docker/home-lab default remains the stdio MCP server. Endpoints are intentionally thin wrappers over the same MCP/core services: GET /health, GET /status, POST /search, POST /ingest, GET /media/{id}, and GET /media/{id}/scene?start=123. POST /ingest uses the same safety gate as MCP and returns forbidden unless mcp.allow_ingest_tools: true is explicitly configured.
The image installs the package and Debian ffmpeg, which also provides ffprobe for optional embedded subtitle extraction when enabled:
docker build -t media-memory-mcp:test .For compose, create local host directories and place a safe config at ./config/config.yaml (for example, copy config.example.yaml and keep secrets out of the file unless you intentionally enable a provider):
mkdir -p config data media bazarr
chown -R 10001:10001 data
cp config.example.yaml config/config.yaml
docker compose config
docker compose up media-memorydocker-compose.yml runs media-memory mcp --config /config/config.yaml over stdio by default as UID/GID 10001:10001 through user: "${PUID:-10001}:${PGID:-10001}". It mounts /config read-only, /media read-only, optional /bazarr read-only, and /data read-write for SQLite, vectors, caches, and derived subtitle files. Override MEDIA_LIBRARY_PATH or BAZARR_SUBTITLE_PATH to point at existing host directories; do not mount media read-write.
The hardened image runs by default as the non-root media-memory user with UID/GID 10001:10001. Host mounts must allow /data writes by 10001:10001 so SQLite, vector data, and derived subtitle artifacts can be created; for the default local compose layout, run chown -R 10001:10001 data after creating the directories.
/config, /media, and /bazarr are intentionally designed as read-only mounts. The container should not need to write to those paths, and only /data is treated as the writable application path.
Compose includes user: "${PUID:-10001}:${PGID:-10001}" so operators can align container writes with host ownership by exporting PUID and PGID when a different writable /data owner is required.
Run the container-level end-to-end check with:
bash scripts/e2e-container.shThe command runs a default E2E path that builds the image, creates temporary /config, /media, and /data mounts, and copies synthetic fixtures from tests/fixtures/media, so no user media is required. It validates both CLI search and MCP search_dialogue against the container-mounted config and data, and it verifies a real database file at /data/media-memory.sqlite is created in the container.
Optional environment overrides:
IMAGE_TAG: custom image tag used for build and test runs.SKIP_BUILD=1: reuse an existing image without rebuilding.KEEP_E2E_TMP=1: keep temporary E2E directories for inspection.
Operational status is available from the CLI and is safe to emit as JSON because it reports provider enablement flags and model/provider names, not tokens or service URLs:
media-memory status --config config.example.yaml --jsondocs/home-lab-spec.md: local deployment assumptions and safe mount defaults.docs/mcp-tools.md: current/planned MCP tool surface and ingest safety.docs/data-model.md: current scaffold concepts and planned corpus-aware model boundaries.docs/hosted-architecture.md: deferred hosted architecture direction.