broll-mcp
<div align="center">
# ποΈ broll-mcp
**Find, preview and cut public-domain b-roll from the Internet Archive, from inside any MCP agent.**
Every clip lands on disk with a provenance record next to it: source, license, exact segment and attribution text.
[](https://github.com/edigeh/broll-mcp/actions/workflows/ci.yml)
[](LICENSE)



</div>
---
```text
You: "I need 8 seconds of children eating bananas, ideally black and white."
Agent β search_footage("children eating bananas") 5 ranked items, license + tier on each
Agent β inspect_item("AboutBan1935") scenes with a keyframe image per scene
Agent β extract_clip("AboutBan1935", start_s=300, 8s) verified H.264 MP4 + provenance JSON
Agent β get_provenance(clip_id) "About Bananas" (1935), Prelinger Archives
via Internet Archive Β· Public Domain
```
The agent sees the footage before it cuts: `inspect_item` returns real keyframe images, so a vision model can pick the shot instead of guessing from titles.
## Why
archive.org holds decades of public-domain film (Prelinger, NASA, newsreels, stock footage), but using it from an agent is awkward:
- Raw item metadata can exceed **1 MB** for one item. That's a huge token cost before you've seen a single frame.
- Licenses differ **per item**, even inside one collection.
- Clips cut with `-c copy` snap to keyframes. An 8 s request came back as **11.35 s**.
broll-mcp handles all of this behind four small tools. Responses are compact and cached, and the server applies polite rate limits. Each cut is re-encoded and checked against the requested length.
## Features
| | |
|---|---|
| π **Brief-driven search** | Plain-language briefs become archive.org queries. A two-stage search first requires every term, then matches any term. Local ranking adds collection trust tiers and a black-and-white preference. Results come back 5 per page. |
| πΌοΈ **Visual inspection** | Scene detection runs over HTTPS (PySceneDetect, with ffmpeg as fallback) and returns one keyframe per scene as an MCP image. Items that hold hundreds of clips return a paged clip list with thumbnails. |
| βοΈ **Frame-accurate cuts** | Cuts are re-encoded to H.264 MP4 with faststart. ffprobe confirms the duration is within Β±0.1 s and the decode shows zero errors, and you get 3 check frames back. The same request twice returns the same clip. |
| π **Provenance by design** | A JSON record sits next to every clip. It holds the source file MD5, the license copied verbatim, `commercial_use`, the data node, a timestamp and ready-to-paste attribution. Provenance is written before the clip, so a clip never exists without its record. |
| π€ **Polite to archive.org** | Follows [archive.org's bot guidance](https://archive.org/developers/bots.html): a descriptive User-Agent, a 1 req/s token bucket, at most 4 media connections, `Retry-After` support and exponential backoff. |
| πΎ **Cache-first** | Uses a SQLite WAL cache. Repeating a search, inspection or cut sends **0** upstream requests, and every tool result reports `upstream_requests`. |
## Quick start
**Requirements:** Python 3.12+, [uv](https://docs.astral.sh/uv/), and `ffmpeg` / `ffprobe` built with HTTPS support (`brew install ffmpeg`, `apt install ffmpeg`).
### Claude Code
```bash
claude mcp add broll -- uvx --from git+https://github.com/edigeh/broll-mcp broll-mcp
```
### Claude Desktop, Cursor and other MCP hosts
```json
{
"mcpServers": {
"broll": {
"command": "uvx",
"args": ["--from", "git+https://github.com/edigeh/broll-mcp", "broll-mcp"],
"env": { "BROLL_AGENT_MODEL": "claude-opus-5-5" }
}
}
}
```
### From a clone
```bash
git clone https://github.com/edigeh/broll-mcp && cd broll-mcp
uv sync
uv run broll-mcp # stdio server; point your host at this command
```
Try it without an agent using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
```bash
npx @modelcontextprotocol/inspector --cli uv run broll-mcp \
--method tools/call --tool-name search_footage --tool-arg 'brief=aerial highway 1950s'
```
## Tools
| Tool | What it does | Upstream cost |
|---|---|---|
| `search_footage(brief, filters?, cursor?)` | Ranked items with title, year, collections, trust tier, license, runtime, tile image URL and a short *why it matched*. Filters: `max_tier`, `license` (`stated` Β· `commercial_ok` Β· `any`), `collections`, `year_from/to`, `color`. | β€ 2 requests, 0 when cached |
| `inspect_item(identifier, file?, window?, page_size?, cursor?)` | **Scene mode:** scenes with timings plus a keyframe image each, a shotlist excerpt and license. **Clip-list mode:** for items with many separate clips, a paged list with thumbnails. | a few requests, 0 when cached |
| `extract_clip(identifier, file, start_s, duration_s, label?, audio?, brief?)` | Cuts 1β60 s into `BROLL_HOME/clips/`, verifies it and writes the provenance JSON. Returns 3 check frames. | ~3 requests, 0 on repeat |
| `get_provenance(clip_id \| identifier, cursor?)` | Reads the record for a clip, or for an item and the clips already cut from it. Local only. | always 0 |
**Resource:** `broll://stats` reports request counts by endpoint, cache contents, the active scene detector and the ffmpeg version.
Every tool declares an `outputSchema` and returns `structuredContent`. Errors come back as tool results with a stable code, such as `NO_RESULTS`, `ITEM_NOT_FOUND`, `FILE_NOT_IN_ITEM`, `RANGE_OUT_OF_BOUNDS`, `THROTTLED` or `CUT_VERIFY_FAILED`. Each message tells the agent how to recover.
## Provenance record
```jsonc
// clips/AboutBan1935__0300.00-0308.00__bananas.provenance.json
{
"schema": "broll-mcp/provenance/1",
"clip_id": "clp_20261023_4ee00c",
"clip_sha256": "78e33da7β¦",
"source": {
"identifier": "AboutBan1935",
"title": "About Bananas",
"file": "AboutBan1935.mp4",
"file_md5": "de484b1b573d28be4860f00a593f4416",
"derived_from": "AboutBan1935.mpeg",
"collections": ["prelinger"],
"tier": 1,
"date": "1935",
"creator": "Castle Films"
},
"license": {
"licenseurl": "http://creativecommons.org/licenses/publicdomain/",
"license_label": "Public Domain (CC PD dedication)",
"commercial_use": "allowed",
"attribution_required": "no",
"determined_from": "licenseurl"
},
"segment": { "start_s": 300.0, "duration_s": 8.0, "verified_duration_s": 7.975 },
"retrieval": { "retrieved_at": "2026-10-23T12:28:30Z", "data_node": "dn600301.us.archive.org", "user_agent": "broll-mcp/0.1.0 (β¦)" },
"processing": { "ffmpeg": "8.1.1", "audio": false, "reencoded": true },
"attribution_text": "\"About Bananas\" (1935), Prelinger Archives via Internet Archive, https://archive.org/details/AboutBan1935. License: Public Domain (CC PD dedication)."
}
```
License labels come only from an exact URL table covering PD, CC0, the PD Mark, and CC BY / BY-SA / BY-ND / BY-NC variants for versions 2.0β4.0. Any value the table doesn't recognize is reported as `unknown`, never guessed.
## Configuration
| Variable | Default | Purpose |
|---|---|---|
| `BROLL_HOME` | platform data dir (`~/Library/Application Support/broll-mcp`, `~/.local/share/broll-mcp`) | Cache, keyframes, clips. The server writes nowhere else. |
| `BROLL_AGENT_MODEL` | `unspecified` | Model name sent in the User-Agent, as archive.org asks of AI agents. |
| `BROLL_CACHE_MAX_MB` | `2048` | Keyframe cache budget; older keyframes are evicted first. |
Collection trust tiers live in `BROLL_HOME/tiers.toml`. Tier 1 is `prelinger` and `nasa`, tier 2 is `stock_footage` and `feature_films`, and everything else is tier 3. Edit the file to tune ranking and the `max_tier` filter.
## How it works
```text
MCP host ββstdioβββΊ server.py ββ jsonschema validation, result shaping, images
β
βββββββββββββββββΌβββββββββββββββββββ¬βββββββββββββββββββββ
SearchService SceneService ClipService SQLite cache (WAL)
two-stage query PySceneDetect / cut β ffprobe verify searches Β· items Β· scenes
+ local ranking ffmpeg, keyframes β provenance β clip clips Β· provenance Β· request_log
βββββββββββββββββ΄βββββββ ArchiveClient βββββββββββββββββ
UA Β· 1 req/s bucket Β· 4 media slots Β· backoff
β
archive.org (advancedsearch Β· metadata Β· download)
```
[`ARCHITECTURE.md`](ARCHITECTURE.md) has the full design, built on evidence (E1βE16) from live archive.org responses. Raw captures are in [`research/`](research/).
## Development
```bash
uv sync
uv run pytest # 76 offline tests (mock transport, captured metadata)
uv run ruff check . && uv run ruff format --check .
uv run python scripts/e2e_smoke.py /tmp/broll-e2e # live end-to-end over stdio against archive.org
```
See [CONTRIBUTING.md](CONTRIBUTING.md).
## A note on rights
broll-mcp copies license information exactly as archive.org publishes it, and it makes no legal judgement. `commercial_use: "unknown"` means archive.org states no machine-readable license for that item. Check the source item before you publish footage, especially material that carries only a rights statement.
## License
[MIT](LICENSE) for the code. Footage belongs to its respective rights holders under the terms recorded in each clip's provenance file.
TDQS
Scored across 4 tools
Each tool maps to a distinct stage: search remote footage, inspect a remote item, extract a clip locally, and read local provenance. There is minor overlap because both inspect_item and get_provenance can return license/source information for an item, but the remote-vs-local distinction is clearly stated.
All four names follow a consistent snake_case verb_noun pattern: search_footage, inspect_item, extract_clip, get_provenance. The convention is predictable and immediately readable.
Four tools are well-scoped for a focused footage search, inspection, extraction, and provenance workflow. Each tool has a clear role and none appears redundant.
The surface covers the core lifecycle from discovery through extraction and provenance, with paging and repeat-cut handling described. Minor gaps exist around managing or listing already extracted clips, but agents can work around this through provenance lookups.