Skip to main content
Glama

Distributed Systems — Any video. Every frame. Answered.

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.

Fetch, listen, see, ask: the pipeline from a link to answers with timestamps and frames.

What it does

Link to file, with sound

Above 360p most sites serve silent video plus a separate audio stream. video_download fetches both in parallel fragments and muxes them, preferring H.264/AAC so the file plays everywhere. video_probe shows the quality menu first and flags which qualities need the merge. Any site yt-dlp supports will work, including playlists and channels.

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 jina-embeddings-v4, so a text question can land directly on a frame with no captioning step. On Apple Silicon it runs locally through Jina's official 8-bit MLX port; elsewhere it uses the hosted API. Both produce the same vectors, so indexes are interchangeable.

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

video_ask returns moments, meaning transcript windows with the frames from them as images, plus raw match strength. Your client's model reads the screen itself and answers, and no second model is paid to summarise along the way.

Verified answers (optional)

With verify=true, Jev 1.14 judges whether each moment actually answers the question and reranks by that. When nothing answers it, evidence.supported is false, which similarity scores alone can't tell you.

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 ready

The 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-v4

Add .[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 needed

Any 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:4005

Clients 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

jinaai/jina-embeddings-v4-mlx-8bit, pinned

jina-embeddings-v4

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

video_probe

ProbeResult

Quality menu for a link: sizes, and which qualities need an audio merge

video_download

Job

Save video with audio merged, video only, or audio only (m4a/mp3/opus/flac/wav)

Index

video_ingest

Job

Download, transcribe, sample frames, embed; resumable

video_ingest_batch

BatchResult

Many sources at once; playlists and channels expand

Jobs

video_job · video_jobs · video_cancel

Job · JobList · CancelResult

Wait with progress notifications, list, stop

Retrieve

video_ask

AskResult + images

Evidence moments for a question, with match strength

video_search

SearchResult

Ranked hits over what was said and shown, filterable by time and video

video_search_by_image

SearchResult

Frames resembling a picture you supply

video_find_phrase

PhraseResult

Every occurrence of exact words

Inspect

video_transcript

TranscriptResult

Timestamped, plain, SRT, WebVTT or JSON, by range

video_frames

FramesResult + images

Frames at exact instants, decoded at full resolution

video_chapters

ChaptersResult (+ images)

Uploader chapters, or topic shifts found in the embeddings

video_clip

ClipResult

Export a range to MP4, stream-copied or frame-accurate

Library

video_list · video_info · video_delete · video_status

VideoList · VideoDetail · DeleteResult · StatusReport

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

video://library

JSON list of indexed videos

video://{video_id}/transcript

Timestamped transcript

video://{video_id}/subtitles.srt

SRT subtitles

video://{video_id}/info

VideoDetail JSON

video://{video_id}/frame/{seconds}

JPEG of the nearest indexed frame

Prompt

Arguments

Produces

video_brief

source

Ingest if needed; summary, chapters, key claims, open questions

video_fact_check

video_id, topic?

Table of verbatim claims with timestamps and on-screen support

video_study_notes

video_id

Terms, procedures, formulas and code as shown, review questions

video_compare

video_ids, question

Per-video answers, agreements and contradictions

video_find_moment

description

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 interrupted at 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

video_search, local embeddings

~25 ms warm

video_ask with Jev verification

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

DVQA_EMBED_BACKEND

auto

mlx (local, Apple Silicon), jina (hosted API), or auto

JINA_API_KEY

—

Key for the hosted embedding API (DVQA_EMBED_API_KEY overrides)

DVQA_EMBED_TPM

90000

Token-per-minute budget; set it to your Jina tier

DVQA_EMBED_IMAGE_PX

448

Long side of frames sent for embedding; cost grows with area

DVQA_EMBED_DIM

1024

Embedding dimension (Jina v4 supports 128–2048)

DVQA_TRANSCRIBE_BACKEND

auto

mlx, faster-whisper, api, or none (frames only)

DVQA_TRANSCRIBE_BASE_URL / _API_KEY

Groq or OpenAI if their key is set

Any OpenAI-compatible transcription endpoint

DVQA_FRAME_INTERVAL / DVQA_MAX_FRAMES

5 / 600

Sampling cadence and per-video frame budget

DVQA_MAX_CONCURRENT_JOBS / DVQA_CPU_WORKERS

2 / half the cores

Job parallelism and decoder parallelism

DVQA_INGEST_HEIGHT

720

Download height for indexing

DVQA_MAX_DURATION / DVQA_MAX_FILESIZE

6 h / 8 GiB

Refuse anything larger

DVQA_STATE_DIR

~/.local/state/distributed-video-qa-mcp

Index, media and downloads

DVQA_ALLOWED_ROOTS

home and tmp (stdio); none (HTTP)

Where local files may be read and written

DVQA_COOKIES_FILE

—

Netscape cookies for sign-in-only videos

DVQA_ALLOW_PRIVATE_HOSTS

off

Permit URLs on private or loopback addresses

DVQA_HTTP_TOKEN

—

Bearer token for HTTP mode

TYPESAFE_API_KEY

—

Enables Jev verification

DVQA_VERIFY / DVQA_VERIFY_MODEL

off / jev-1.14.0

Verify every video_ask by default; pinned judge model

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_delete removes 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 model

The 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 tools
video_askGather evidence for a questionA
Read-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 videoA
Read-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 indexA
DestructiveIdempotent

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 saidA
Read-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 framesA
Read-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 videoC
Read-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 jobA
Read-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 jobsB
Read-only

Recent download and ingest jobs, newest first.

Inputs: limit, active_only. Returns: jobs

video_listList indexed videosA
Read-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 URLA
Read-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_search_by_imageFind frames that look like an imageB
Read-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 statusA
Read-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 transcriptA
Read-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

    B3.4/5.0

    Scored across 19 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count4/5

    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).

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      A
      quality
      D
      maintenance
      Provides read-only MCP tools to search and retrieve evidence-grounded knowledge compiled from video content, including hybrid semantic and lexical search with citations.
      5
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables 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
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables 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.
      -