Distributed Video QA MCP
The primary target of the server: it takes a YouTube link (including playlists and channels), probes available qualities and flags which need an audio merge, downloads the video with audio muxed, transcribes the speech, samples frames at scene cuts, and indexes both into one embedding space — so a client can ask questions and get back timestamped moments with the actual frames, find when a slide or diagram is on screen, or compare what several videos say about a topic.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Distributed Video QA MCP Index this video and list every pricing claim with timestamps"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.

Distributed Video QA MCP
Give your MCP client a video link. It downloads the video with sound, transcribes it, indexes every scene, and answers questions with timestamps and the actual frames. On a Mac, all of it runs locally, with no API keys.
v0.3.0 · 19 typed tools · 5 resources · 5 prompts · local MLX or hosted backends · Python 3.11+ · MIT
Quick start · Backends · Verification · Interface · Tool contract · How it works · Operations · Changelog
Try asking your MCP client:
Index https://www.youtube.com/watch?v=… and list every claim the speaker makes about pricing, with timestamps.
Find the moment the architecture diagram is on screen and read me its labels.
Ingest the first 20 videos of this playlist and tell me where they disagree about the roadmap.
Download this link at 1080p with audio into ~/Movies.
What it does
Link to file, with sound | Above 360p most sites serve silent video plus a separate audio stream. |
Transcript | Whisper large-v3-turbo runs on-device via MLX on Apple Silicon (47× realtime on an M4 Max) or faster-whisper elsewhere. Without a local model it uses any OpenAI-compatible endpoint, with the audio cut on silences and the chunks sent concurrently. |
Frames | Frames are sampled at a fixed interval plus every scene cut, decoding the timeline in parallel FFmpeg segments. Near-duplicates are removed with a perceptual hash and a colour check, so a talking head doesn't use up the budget. |
One space for words and pictures | Passages and frames are both embedded with |
Hybrid retrieval | Three channels: meaning-based transcript search, BM25 keyword search, and text-to-image search over frames. They are fused by weighted reciprocal rank, giving speech and picture equal say. Every hit reports which channels found it and how strongly. |
Evidence, not opinions |
|
Verified answers (optional) | With |
Related MCP server: mcptube-vision
Quick start
On Apple Silicon (fully local, no keys)
Requires FFmpeg on PATH.
git clone https://github.com/arthurcolle/distributed-video-qa-mcp.git
cd distributed-video-qa-mcp
uv venv && uv pip install -e ".[mlx]" # MLX Whisper + MLX jina-embeddings-v4
.venv/bin/distributed-video-qa-mcp fetch-models # one-time 3.9 GB download of the embedding model
.venv/bin/distributed-video-qa-mcp doctor # prints the backends; exits non-zero if not readyThe model files are pinned to exact Hugging Face revisions and cached in ~/.cache/huggingface. While loaded, the embedding model uses about 5 GB of unified memory.
Anywhere else
uv pip install -e ".[faster-whisper]" # local Whisper on Linux/CUDA/CPU, or set GROQ_API_KEY instead
export JINA_API_KEY=… # hosted jina-embeddings-v4Add .[jev] and set TYPESAFE_API_KEY to enable verification. The install includes a Deno runtime for YouTube's signature challenge; without one, YouTube offers only low qualities.
Claude Code
claude mcp add video-qa -- "$PWD/.venv/bin/distributed-video-qa-mcp" # local Mac setup: no env neededAny MCP host (stdio)
{
"mcpServers": {
"video-qa": {
"command": "/absolute/path/distributed-video-qa-mcp/.venv/bin/distributed-video-qa-mcp",
"env": { "TYPESAFE_API_KEY": "… optional, for verify" }
}
}
}Shared server (streamable HTTP)
DVQA_HTTP_TOKEN=$(openssl rand -hex 32) distributed-video-qa-mcp serve --http --host 0.0.0.0 --port 4005 \
--allowed-host video-qa.internal:4005Clients send Authorization: Bearer <token> to /mcp; /health is open. The server refuses to bind a non-loopback address without a token. See operations for TLS, process supervision and backups.
Backends
DVQA_EMBED_BACKEND=auto (the default) picks local MLX on Apple Silicon when the mlx extra is installed, and otherwise the Jina API when JINA_API_KEY is set. Transcription works the same way through DVQA_TRANSCRIBE_BACKEND.
Local MLX (Apple Silicon) | Jina API | |
Model |
|
|
Agreement with the other backend | cosine 0.999 (text), 0.988–0.995 (frames) | — |
Full index of a 6½-minute video | 27.5 s end to end (105 frames, 119 segments) | 8–12 min at a 100k tokens/min limit |
Query embedding | ~25 ms warm (2.4 s on the first query while the model loads) | ~250 ms round trip |
Cost | none; ~5 GB unified memory while loaded | per token; ~1,440 tokens per frame |
Data leaves the machine | no | yes |
Because the vectors match, both backends store them under the model name jina-embeddings-v4. An index built with one is searched with the other, and switching backends re-embeds nothing.
About the MLX port. Jina's published MLX code has three behaviours that are wrong for a long-running server. It caches results keyed on Python object ids, so a later input can silently get an earlier input's vector. It frees the vision weights after the first image. Its compiled text path ignores padding in some batches. The server loads the published weights through the port's model classes but bypasses those paths. embed_mlx.py documents the details, and tests/test_embed_mlx.py pins each failure: distinct images must get distinct vectors, a repeated image the same vector, and batches must match single inputs.
Verification with Jev
Similarity measures what is near a question, not whether it answers it. On ten test questions against one video, the best transcript similarity was 0.51–0.76 for questions the video answers and 0.42–0.55 for questions it doesn't. The ranges overlap, so no threshold works. Jev 1.14 is TypeSafe's typed decision model: instead of prose, it returns a probability for a yes/no question. Asked "does this evidence contain a direct answer?" for each retrieved moment, it separated the two groups cleanly.
Question | Best similarity | Best Jev p(answers) |
Why did he lose money on the Seoul bet? | 0.62 | 0.97 |
Which weather models does he say are free? | 0.62 | 0.97, on the moment retrieval ranked 2nd (its 1st scored 0.07) |
What is his step-by-step playbook? | 0.76 | 0.92 |
Why are the tail brackets mispriced? | 0.51 | 0.96 |
What is the capital of Australia? | 0.44 | 0.02 |
What was the price of Bitcoin that day? | 0.55 | 0.04 |
Who won the Rockets–Wizards game? | 0.42 | 0.01 |
Show me the NBA odds screen (frame) | 0.66 | 0.96 |
Show me a map of hurricane tracks (frame) | 0.59 | 0.06 |
video_ask(question, verify=true) asks Jev about every candidate moment's transcript and every frame. It ranks moments by their strongest support and returns the probabilities on each moment (transcript_answers, each frame's shows, support). It sets evidence.supported, which is true when any moment reaches 0.5; that threshold is this server's policy, not the model's. When nothing is supported, it judges the best frame matches individually as a rescue, because some answers are only on screen. A verified ask costs about 0.7–0.9 s and ~15k input tokens (~1,100 per judgement).
Frame judgements are noisier than transcript judgements on questions that aren't visual: a website screenshot scored 0.70 for "what website does he recommend". Read transcript_answers and shows separately when it matters.
Verification is off by default because it sends transcript excerpts and frames to TypeSafe. Turn it on per call, or for every call with DVQA_VERIFY=jev. It needs the jev extra and TYPESAFE_API_KEY, and it pins jev-1.14.0, failing if the provider answers with a different model.
Interface
Every tool declares a typed result. The JSON Schema is published as the tool's outputSchema, every response is validated against it before it is sent, and it arrives as structuredContent. The full per-argument and per-field reference is docs/tool-contract.md. It is generated from the code, and a test fails if it goes stale.
Group | Tool | Returns | Does |
Acquire |
|
| Quality menu for a link: sizes, and which qualities need an audio merge |
|
| Save video with audio merged, video only, or audio only (m4a/mp3/opus/flac/wav) | |
Index |
|
| Download, transcribe, sample frames, embed; resumable |
|
| Many sources at once; playlists and channels expand | |
Jobs |
|
| Wait with progress notifications, list, stop |
Retrieve |
|
| Evidence moments for a question, with match strength |
|
| Ranked hits over what was said and shown, filterable by time and video | |
|
| Frames resembling a picture you supply | |
|
| Every occurrence of exact words | |
Inspect |
|
| Timestamped, plain, SRT, WebVTT or JSON, by range |
|
| Frames at exact instants, decoded at full resolution | |
|
| Uploader chapters, or topic shifts found in the embeddings | |
|
| Export a range to MP4, stream-copied or frame-accurate | |
Library |
|
| Manage the index; readiness and capacity |
Anatomy of an answer
video_ask(question="Why did he lose money on the Seoul bet?", max_moments=1, frames_per_moment=1) against a 6½-minute video returns the structure below (transcript shortened), followed by a caption and the JPEG of each listed frame:
{
"question": "Why did he lose money on the Seoul bet?",
"moments": [{
"video_id": "cb5590f118d1",
"span": "3:44-4:48",
"matched_by": ["said_keyword", "said_semantic", "shown"],
"transcript": [
{ "at": "3:48", "start": 228.7, "text": "So let me tell you about my Seoul, South Korea bet." },
{ "at": "3:58", "start": 238.6, "text": "But I sized in heavy, $300, it was at 62 cents." },
{ "at": "4:02", "start": 242.1, "text": "The problem? Seoul's temperature crept up all day and hit exactly 13." },
{ "at": "4:09", "start": 249.4, "text": "And under 13 means under, not equal to." }
],
"frames": [{ "at": "4:01", "t": 241.9, "path": "…/frames/00066_000241900.jpg" }]
}],
"evidence": { "best_said_similarity": 0.62, "best_shown_similarity": 0.48, "keyword_matches": 9 },
"guidance": "Answer only from this evidence and cite times as m:ss. Images follow in moment order."
}evidence lets the model tell a real match from a near miss. On the same index, the off-topic question "What is the capital of Australia?" scored best_said_similarity 0.44 with 0 keyword matches.
Errors
A failed call returns isError: true and text of a fixed format, so the code can be branched on:
Error executing tool video_ingest: path_not_allowed: /private/etc/hosts is outside the allowed roots; set DVQA_ALLOWED_ROOTS to widen them — Add the folder to DVQA_ALLOWED_ROOTS.There are 32 registered codes in five categories (input, access, source, backend, state). Each one has a hint and a retryable flag, listed in the contract. A test fails if the code raises an unregistered one.
Resources and prompts
Resource | Type |
| JSON list of indexed videos |
| Timestamped transcript |
| SRT subtitles |
|
|
| JPEG of the nearest indexed frame |
Prompt | Arguments | Produces |
|
| Ingest if needed; summary, chapters, key claims, open questions |
|
| Table of verbatim claims with timestamps and on-screen support |
|
| Terms, procedures, formulas and code as shown, review questions |
|
| Per-video answers, agreements and contradictions |
|
| Located and visually verified moment, ready to clip |
How it works
┌─ audio ─ Whisper (on-device, or silence-cut chunks in parallel) ─ passages ─ text vectors ─┐
source ─ probe ──┤ ├─ SQLite index
└─ frames (parallel FFmpeg segments: interval + scene cuts) ─ dedupe ─ image vectors ────────┘Concurrent branches, resumable stages. Speech and frames run at the same time. Each branch records a signature of its inputs (backend, model, dimensions, sampling settings) when it finishes. Re-ingesting skips matching stages, and changing a setting redoes only the stage it affects. Jobs left running by a crash are marked
interruptedat startup.Retrieval. Transcript segments are grouped into ~30-second passages that overlap by one segment. A query runs dense passage search, BM25 over the passages (SQLite FTS5, porter stemming, stopwords removed), and dense frame search. Results are fused by reciprocal rank (k = 60). The two transcript channels are weighted ½ each, so a strong frame match is not outvoted two to one. Nearby hits merge into moments of at most 75 seconds.
Index. A single SQLite file in WAL mode holds segments, passages, frames, the FTS table and float32 vectors. Vector search is exact: each vector set is loaded once into a NumPy matrix and scored with one matrix product.
Rate budget. The Jina client holds a sliding one-minute token budget, seeded from measured per-image costs and corrected from every usage report. A large ingest runs at your rate limit instead of failing on it.
Measured
These numbers are from an Apple M4 Max on the 6:36 video above, plus synthetic data:
Step | Result |
Full local ingest (MLX Whisper + MLX embeddings) | 27.5 s for 6:36 of video, no keys |
Transcription, MLX Whisper large-v3-turbo | 8.5 s (47× realtime), 119 segments |
Frame sampling, 8 parallel decoders | 114 frames in 1.5 s; 105 kept after de-duplication |
Frame embedding, local MLX | ~160 ms per frame at 448 px |
Frame embedding, Jina API | ~1,440 tokens per frame; ~2 min for this video at a 90k tokens/min budget |
| ~25 ms warm |
| 0.7–0.9 s, ~15k input tokens |
Exact vector search, 300,000 × 1,024 | 35 ms per query once loaded (1 s first load, ~1.2 GB RAM) |
720p YouTube download with audio merge | ~10 s for a 10-minute video, H.264 + AAC |
Configuration
Settings are environment variables. The defaults suit a laptop, and the operations guide explains how to tune them.
Variable | Default | Meaning |
|
|
|
| — | Key for the hosted embedding API ( |
|
| Token-per-minute budget; set it to your Jina tier |
|
| Long side of frames sent for embedding; cost grows with area |
|
| Embedding dimension (Jina v4 supports 128–2048) |
|
|
|
| Groq or OpenAI if their key is set | Any OpenAI-compatible transcription endpoint |
|
| Sampling cadence and per-video frame budget |
|
| Job parallelism and decoder parallelism |
|
| Download height for indexing |
| 6 h / 8 GiB | Refuse anything larger |
|
| Index, media and downloads |
| home and tmp (stdio); none (HTTP) | Where local files may be read and written |
| — | Netscape cookies for sign-in-only videos |
| off | Permit URLs on private or loopback addresses |
| — | Bearer token for HTTP mode |
| — | Enables Jev verification |
| off / | Verify every |
Security
URLs. Only plain http(s) to public addresses is accepted. Loopback, private, link-local and cloud-metadata targets are refused unless you opt in, as are embedded credentials.
Paths. Local paths are resolved, symlinks included, and must sit under
DVQA_ALLOWED_ROOTS. In HTTP mode nothing outside the state directory is reachable by default.Deletion.
video_deleteremoves index rows and the server's own copies. A file you ingested by path is never deleted.Process. Subprocesses run without a shell, logs go to stderr, and the index directory is created
0700.
See SECURITY.md for the threat model and how to report a vulnerability. Downloading is subject to each site's terms and to copyright law; use this on media you have the right to copy.
Development
uv pip install -e ".[dev]"
ruff check src tests && ruff format --check src tests
pytest # 36 tests; the 3 MLX tests skip off Apple Silicon
distributed-video-qa-mcp contract -o docs/tool-contract.md # after changing any tool or modelThe tests build a synthetic video with FFmpeg and use an offline embedder and a scripted transcriber. They drive the whole pipeline through a real MCP client and check every result against its declared model. A fake verifier covers reranking, the frame rescue and the unsupported verdict. They also require a description on every argument and field, a registered code for every error raised, and a contract document that matches the code.
License
MIT. The Distributed Systems name and wordmark are not covered; see LICENSE.
Available Tools
19 toolsvideo_askGather evidence for a questionARead-only
The moments most relevant to a question, as evidence: for each, the transcript around it with
timestamps and the frames from it as images (in moments order). Answer from the evidence and cite
times. With verify, each moment carries calibrated support probabilities and evidence.supported
says whether the video answers the question at all.
Inputs: question (required), verify, image_px, video_ids, +2 more. Returns: moments, evidence, guidance, question
video_cancelCancel a jobA
Stop a queued or running job. Stages that already finished stay in the index and are reused.
Inputs: job_id (required). Returns: kind, next, error, stage, +8 more
video_chaptersOutline a videoARead-only
Chapters with spans, opening words and distinguishing keywords. Uses the uploader's chapters when the source has them; otherwise finds topic shifts in the transcript embeddings (frames for silent video).
Inputs: video_id (required), image_px, include_images. Returns: title, chapters, video_id
video_clipExport a clipB
Cut a time range into its own MP4 with audio. Stream copy is instant; exact re-encodes.
Inputs: end (required), start (required), video_id (required), exact, +1 more. Returns: path, exact, duration, has_audio, +1 more
video_deleteRemove a video from the indexADestructiveIdempotent
Delete a video's transcript, frames and vectors. A file you ingested by path is never deleted; only the server's own downloaded copy and extracted frames are.
Inputs: video_id (required), delete_files. Returns: removed, video_id, files_deleted
video_downloadDownload a video or its audioA
Save a link to a file. Fetches video and audio streams in parallel fragments and muxes them into one playable file, preferring H.264/AAC for MP4 so it opens everywhere. Returns a Job whose result is a DownloadReport.
Inputs: url (required), mode, quality, container, +4 more. Returns: A background download or ingest.
video_find_phraseFind every time a phrase is saidARead-only
Every transcript segment containing the exact phrase, in time order. Use it to count mentions or collect every place a name or number comes up; use video_search for meaning rather than wording. A phrase split across two segments is not matched.
Inputs: phrase (required), limit, video_ids. Returns: hits, count, phrase, truncated
video_framesLook at framesARead-only
Frames as images, at given timestamps or spread across a range. With exact=true each is decoded from the source at that instant and at higher resolution than the indexed samples, for reading small text.
Inputs: video_id (required), end, count, exact, +3 more. Returns: exact, title, frames, video_id
video_infoDescribe one videoCRead-only
Source, dimensions, language, index counts, completed stages and the uploader's description.
Inputs: video_id (required). Returns: id, fps, path, title, +18 more
video_ingestIndex a video for questionsA
Download (for a URL), transcribe, sample frames and embed, so the video can be searched and questioned. Speech and frame work run in parallel. Re-ingesting reuses finished stages, so a failed or interrupted job resumes. Returns a Job; its result holds the video_id, counts and timings.
Inputs: source (required), force, frames, quality, +5 more. Returns: A background download or ingest.
video_ingest_batchIndex several videos or a playlistA
Start one ingest job per source. Playlist and channel URLs expand to their first playlist_limit
videos. Jobs run DVQA_MAX_CONCURRENT_JOBS at a time and the rest queue; a bad source is reported in
skipped without stopping the others.
Inputs: sources (required), frames, quality, language, +3 more. Returns: jobs, skipped, expanded
video_jobCheck a jobARead-only
A job's status, stage and progress, optionally waiting for it to finish. Progress is also streamed as MCP progress notifications while waiting.
Inputs: job_id (required), wait_seconds. Returns: A background download or ingest.
video_jobsList jobsBRead-only
Recent download and ingest jobs, newest first.
Inputs: limit, active_only. Returns: jobs
video_listList indexed videosARead-only
Every video in the index with its id, title, length, status and index counts.
Inputs: no named parameters. Returns: count, videos
video_probeList download options for a URLARead-only
Show what a link offers before downloading: title, length, and each quality with its size and whether it has sound on its own or needs the audio track merged in (most qualities above 360p do).
Inputs: url (required), all_formats. Returns: id, title, formats, is_live, +8 more
video_searchSearch inside videosARead-only
Ranked hits from three channels fused by weighted reciprocal rank: meaning-based transcript search, keyword (BM25) search, and text-to-image search over frames. 'The slide with the pricing table' finds the frame even if nobody said it. Each hit lists the channels that matched it, with rank and raw score.
Inputs: query (required), end, start, top_k, +2 more. Returns: hits, count, query
video_search_by_imageFind frames that look like an imageBRead-only
Where a picture, logo, chart or slide appears: frames that look like it, and passages about it.
Inputs: image_path (required), top_k, video_ids. Returns: hits, count, query
video_statusServer statusARead-only
Readiness and capacity: FFmpeg, yt-dlp and its JavaScript runtimes, the transcription and embedding
backends, limits, and index size. ready is false, with reasons in problems, when ingest would fail.
Inputs: no named parameters. Returns: index, ready, ffmpeg, limits, +9 more
video_transcriptRead the transcriptARead-only
What was said, for the whole video or a range: timestamped lines, plain text, SRT, WebVTT, or JSON segments. Page through long videos with start and end.
Inputs: video_id (required), end, start, format, +1 more. Returns: note, text, title, format, +5 more
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 19 tools
Most tools target a distinct resource+action, and descriptions actively police the boundaries (video_find_phrase for exact wording vs video_search for meaning; video_ask for evidence vs video_search for ranked hits). The weaker spots are video_status / video_info / video_probe, which all read as 'tell me about something' and require reading descriptions to separate system readiness, indexed-video metadata and pre-download link inspection.
Every tool carries the same `video_` prefix, so the namespace is instantly recognizable and scannable. However, the suffix alternates between verbs/actions (download, ingest, ask, search, delete, cancel) and nouns (status, info, transcript, frames, chapters), and video_job vs video_jobs differs only by plurality, which is a minor readability wart rather than a real inconsistency.
19 tools is on the heavier side of the ideal band, but the domain genuinely spans an ingest pipeline, job management, retrieval (three search channels), transcript/frame/chapter inspection, and clipping, so nearly every tool earns its slot. The one arguable redundancy is video_download sitting alongside video_ingest, though they serve different ends (get a file vs build a searchable index).
The lifecycle is well covered: probe → download/ingest/batch → job status/list/cancel → list/info → search/ask/transcript/frames/chapters/clip → delete. Remaining gaps are minor: there is no corpus-level or cross-video question answering despite batch ingest, no re-index/refresh of a single video, and no way to prune job history, but agents can work around all of these.
Maintenance
Related MCP Connectors
Real-time video transcripts, RAG captions, metadata, search & comments MCP. OAuth or API key.
Multimodal video analysis MCP — transcription, vision, and OCR for any video URL.
Search your video library with natural language and retrieve relevant moments through MCP.
Video analysis AI: transcripts, summaries, visual scenes/shots, clips, answers in natural language.
Related MCP Servers
- FlicenseAqualityDmaintenanceProvides read-only MCP tools to search and retrieve evidence-grounded knowledge compiled from video content, including hybrid semantic and lexical search with citations.5-
- AlicenseNot gradedqualityCmaintenanceEnables ingesting YouTube videos into a persistent wiki knowledge base by combining transcripts with scene-change frame analysis, then answering questions over the compiled pages via full-text search and agent reasoning. Serves raw candidate pages and a wiki table of contents to MCP clients so their own model can reason without requiring an API key.MIT
- FlicenseNot gradedqualityAmaintenanceEnables users to ingest YouTube videos, extract transcripts and scene-based visual descriptions, build a compounding wiki knowledge base, and query it through agentic search from MCP-compatible clients.-
- FlicenseNot gradedqualityAmaintenanceEnables ingesting YouTube videos into a persistent, searchable wiki with transcript extraction, scene-change vision analysis, and cross-video knowledge compounding. MCP clients can then ask questions and receive reasoned answers with source citations.-