Custom YouTube Transcribe MCP Server
Fetches YouTube subtitles via yt-dlp, cleans them into plain text, and provides tools for transcript retrieval, file management, and session-based storage with paging.
README
Custom YouTube Transcribe MCP Server
A lightweight FastMCP server that uses yt-dlp to fetch YouTube subtitles, cleans them into plain text, and exposes tools for direct transcript retrieval or paged file reads. It is designed for MCP clients that need to reliably pull large transcripts without hitting response size limits.
What it does
- Validates YouTube URLs (
youtube.com/watch?v=...oryoutu.be/...). - Uses
yt-dlpto download auto-generated subtitles in VTT format (English by default). - Cleans and de-duplicates subtitle lines into a readable transcript.
- Exposes session-scoped tools for transcription, metadata, retention, and paging:
youtube_transcribe,youtube_transcribe_to_file,youtube_transcribe_autoyoutube_get_durationlist_session_items,pin_item,unpin_item,set_item_ttl,delete_itemread_file_info,read_file_chunkwrite_text_filefor storing derived outputs
High-level architecture
flowchart LR
Client["MCP Client"]
Server["FastMCP Server"]
YTDLP["yt-dlp"]
YT["YouTube"]
Data["Session storage"]
Client -->|MCP HTTP| Server
Server -->|spawn process| YTDLP
YTDLP -->|HTTP requests| YT
YTDLP -->|VTT file| Server
Server -->|write file + manifest| Data
Server -->|text or item id| Client
Class diagrams
Domain model
classDiagram
class SessionId {
+value: str
}
class ItemId {
+value: str
}
class Manifest {
+session_id: SessionId
+created_at: str
+items: list~ManifestItem~
}
class ManifestItem {
+id: ItemId
+kind: ItemKind
+format: str
+relpath: str
+size: int
+created_at: str
+expires_at: str?
+pinned: bool
}
class ItemKind
class TranscriptFormat
Manifest "1" o-- "*" ManifestItem
Manifest --> SessionId
ManifestItem --> ItemId
ManifestItem --> ItemKind
ManifestItem --> TranscriptFormat
Services and adapters
classDiagram
class AppConfig
class SessionStore
class ManifestRepository
class YtDlpClient
class TranscriptParser
class TranscriptionService
class SessionService
class TranscriptWriter
YtDlpClient --> AppConfig
ManifestRepository --> SessionStore
TranscriptionService --> YtDlpClient
TranscriptionService --> TranscriptParser
TranscriptionService --> SessionStore
TranscriptionService --> ManifestRepository
TranscriptionService --> TranscriptWriter : uses
SessionService --> SessionStore
SessionService --> ManifestRepository
Detailed data flow
flowchart TD
A["Client calls transcribe tool"] --> B["Validate URL format"]
B -->|valid| C[Run yt-dlp with subtitle args]
C --> D[Pick .en.vtt if present, else any .vtt]
D --> E["Parse VTT to lines"]
E --> F["De-duplicate lines"]
F --> G["Join lines to transcript"]
G -->|return text| H["youtube_transcribe"]
G -->|write file| I["youtube_transcribe_to_file"]
I --> J["Write transcript file"]
J --> K["Update manifest"]
K --> L["Return item id"]
B -->|invalid| X["Raise ValueError"]
C -->|non-zero exit| Y["Raise RuntimeError"]
D -->|no files| Z["Raise RuntimeError"]
Session storage layout
/data/
<session_id>/
manifest.json
transcripts/
youtube_<hash>_<timestamp>.txt
youtube_<hash>_<timestamp>.vtt
youtube_<hash>_<timestamp>.jsonl
derived/
Each manifest item tracks:
{ id, kind, format, relpath, size, created_at, expires_at, pinned }
VTT cleaning and de-duplication logic
The server normalizes WebVTT into a clean transcript by:
- Dropping headers and metadata lines such as
WEBVTT,NOTE,STYLE,REGION,Kind:,Language:. - Dropping cue timing lines like
00:00:01.000 --> 00:00:03.000. - Removing inline timestamps like
<00:00:00.400>. - Removing
<c>tags and other HTML-like tags. - Normalizing whitespace.
- De-duplicating lines with two rules:
- Remove consecutive duplicates.
- Remove recent duplicates within a rolling window (default window size 6) to reduce YouTube caption rollover and double tracks.
MCP tools
Session-scoped tools accept session_id explicitly or infer it from the MCP HTTP header mcp-session-id.
youtube_transcribe(url: str) -> str
- Returns plain transcript text.
- Intended for small to medium transcripts that fit in a single response.
- Raises errors on invalid URLs, failed
yt-dlp, missing subtitles, or empty output after parsing.
youtube_transcribe_to_file(url: str, fmt: str = "txt", session_id: str | None = None) -> dict
- Saves transcript under
/data/<session_id>/transcriptsand returns a manifest item object. fmtoptions:txt(default): cleaned transcript textvtt: raw VTT output fromyt-dlpjsonl: one JSON object per line:{ "text": "..." }
- Item fields:
{ id, kind, format, relpath, size, created_at, expires_at, pinned }.
youtube_get_duration(url: str) -> dict
- Returns
{ duration, duration_string, title, is_live }. - Useful for choosing a strategy before downloading subtitles.
durationcan benullfor live streams.
youtube_transcribe_auto(url: str, fmt: str = "txt", max_text_bytes: int | None = None, session_id: str | None = None) -> dict
- Returns text when the transcript size in UTF-8 bytes is below the threshold.
- Otherwise writes a file under
/data/<session_id>/transcriptsand returns a manifest item object. - Includes
{ duration, duration_string, title, is_live }from metadata. max_text_bytesdefaults toAUTO_TEXT_MAX_BYTESwhen not provided.
list_session_items(kind: str | None = None, format: str | None = None, pinned: bool | None = None, session_id: str | None = None) -> dict
- Returns manifest items for the session, optionally filtered by
kind,format, orpinned.
pin_item(item_id: str, session_id: str | None = None) -> dict
- Marks an item as pinned (no TTL cleanup).
unpin_item(item_id: str, session_id: str | None = None) -> dict
- Removes pin and applies default TTL.
set_item_ttl(item_id: str, ttl_seconds: int, session_id: str | None = None) -> dict
- Sets a custom TTL for an item (unpinned).
delete_item(item_id: str, session_id: str | None = None) -> dict
- Deletes the file and removes it from the manifest.
write_text_file(relpath: str, content: str, overwrite: bool = False, session_id: str | None = None) -> dict
- Writes a derived file under
/data/<session_id>/derivedand registers it in the manifest.
read_file_info(item_id: str | None = None, relpath: str | None = None, session_id: str | None = None) -> dict
- Provide either
item_id(preferred) orrelpath(relative to session root). - Returns
{ id, path, relpath, size, pinned, expires_at?, format, kind }.
read_file_chunk(item_id: str | None = None, relpath: str | None = None, offset: int = 0, max_bytes: int = 200000, session_id: str | None = None) -> dict
- Provide either
item_id(preferred) orrelpath. - Returns
data(decoded text),next_offset,eof,size,path,id. max_bytesis clamped to1..200000.
MCP request/response examples
These examples show raw JSON-RPC payloads over HTTP (POST /mcp). Client SDKs often hide this, but the structure is the same.
youtube_transcribe
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "youtube_transcribe",
"arguments": {
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Line 1\nLine 2\nLine 3"
}
]
}
}
youtube_transcribe_to_file
Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "youtube_transcribe_to_file",
"arguments": {
"url": "https://youtu.be/dQw4w9WgXcQ",
"session_id": "sess_123",
"fmt": "jsonl"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "json",
"json": {
"id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1",
"session_id": "sess_123",
"relpath": "transcripts/youtube_a1b2c3d4e5_20240101T120000Z.jsonl",
"expires_at": "2025-01-01T12:00:00Z",
"pinned": false,
"format": "jsonl",
"size": 123456,
"kind": "transcript",
"created_at": "2025-01-01T11:00:00Z"
}
}
]
}
}
youtube_get_duration
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "youtube_get_duration",
"arguments": {
"url": "https://youtu.be/dQw4w9WgXcQ"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "json",
"json": {
"duration": 213,
"duration_string": "00:03:33",
"title": "Example Title",
"is_live": false
}
}
]
}
}
Note: metadata responses may be served from the in-memory cache for up to
YTDLP_INFO_CACHE_TTL_SEC seconds to reduce repeated yt-dlp calls.
youtube_transcribe_auto
Request:
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "youtube_transcribe_auto",
"arguments": {
"url": "https://youtu.be/dQw4w9WgXcQ",
"fmt": "txt",
"max_text_bytes": 150000,
"session_id": "sess_123"
}
}
}
Response (text):
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "json",
"json": {
"kind": "text",
"text": "Line 1\nLine 2\nLine 3",
"bytes": 12345,
"duration": 213,
"duration_string": "00:03:33",
"title": "Example Title",
"is_live": false
}
}
]
}
}
Response (file):
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "json",
"json": {
"kind": "file",
"id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1",
"session_id": "sess_123",
"relpath": "transcripts/youtube_a1b2c3d4e5_20240101T120000Z.txt",
"expires_at": "2025-01-01T12:00:00Z",
"pinned": false,
"format": "txt",
"size": 987654,
"created_at": "2025-01-01T11:00:00Z",
"bytes": 987654,
"duration": 213,
"duration_string": "00:03:33",
"title": "Example Title",
"is_live": false
}
}
]
}
}
read_file_info
Request:
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "read_file_info",
"arguments": {
"item_id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1",
"session_id": "sess_123"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "json",
"json": {
"id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1",
"session_id": "sess_123",
"path": "/data/sess_123/transcripts/youtube_a1b2c3d4e5_20240101T120000Z.txt",
"relpath": "transcripts/youtube_a1b2c3d4e5_20240101T120000Z.txt",
"size": 120345,
"expires_at": "2025-01-01T12:00:00Z",
"pinned": false,
"format": "txt",
"kind": "transcript"
}
}
]
}
}
read_file_chunk
Request:
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "read_file_chunk",
"arguments": {
"item_id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1",
"session_id": "sess_123",
"offset": 0,
"max_bytes": 200000
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"content": [
{
"type": "json",
"json": {
"data": "First chunk of text...",
"next_offset": 200000,
"eof": false,
"size": 120345,
"path": "/data/sess_123/transcripts/youtube_a1b2c3d4e5_20240101T120000Z.txt",
"id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1"
}
}
]
}
}
Resources
Resources are session-scoped. Use your session id in the URI (it must match mcp-session-id if the header is present).
transcripts://session/{session_id}/indexreturns the session manifest.transcripts://session/{session_id}/latestreturns the most recent transcript item.transcripts://session/{session_id}/item/{id}returns item metadata and inline content if small.
Example response for transcripts://session/sess_123/item/tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1:
{
"session_id": "sess_123",
"item": {
"id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1",
"kind": "transcript",
"format": "txt",
"relpath": "transcripts/youtube_a1b2c3d4e5_20240101T120000Z.txt",
"size": 120345,
"created_at": "2025-01-01T11:00:00Z",
"expires_at": "2025-01-01T12:00:00Z",
"pinned": false
},
"content": "Line 1\nLine 2\nLine 3",
"truncated": false,
"inline_max_bytes": 20000
}
Prompts
Prompts emit instructions for the calling agent to run. They do not perform AI work on the server.
paragraphssummarytranslateoutlinequotesfaqglossaryaction_items
Clients that only support tools can still access prompts via the PromptToolMiddleware-exposed
list_prompts and get_prompt tools.
Resource templates (legacy prompt-only)
These templates emit structured prompt payloads for clients that expect the template:// resources.
template://transcript/paragraphs/{id}template://transcript/summary/{id}template://transcript/translate/{id}/{target_lang}template://transcript/outline/{id}template://transcript/quotes/{id}template://transcript/faq/{id}template://transcript/glossary/{id}template://transcript/action-items/{id}
Example response for template://transcript/summary/tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1:
{
"name": "summary",
"inputs": {
"item_id": "tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1",
"session_id": "sess_123"
},
"prompt": "Summarize the transcript with: 1) A one-paragraph executive summary. 2) 5-8 bullet key points. Keep it concise and faithful to the source.",
"recommended_steps": [
"Call transcripts://session/sess_123/item/tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1 to get metadata and inline content.",
"If content is missing or truncated, call read_file_chunk(item_id=\"tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1\", session_id=\"sess_123\", offset=0, max_bytes=200000) until eof.",
"Complete the task and output only the result.",
"If you need this transcript later, call pin_item(item_id=\"tr_58aafd83e6f14c6e8c2f1c5f21d9a2a1\")."
]
}
File naming scheme
Files written by youtube_transcribe_to_file use:
youtube_{sha1(url)[:10]}_{utc_timestamp}.{ext}
Example: /data/sess_123/transcripts/youtube_a1b2c3d4e5_20240101T120000Z.txt
Each session maintains a manifest:
/data/<session_id>/manifest.json
Retention and expiry
- By default, file outputs expire after
TRANSCRIPT_TTL_SECONDS(1 hour). - Expired, unpinned files are cleaned up when the session is accessed (transcribe or read).
- Pinning an item (
pin_item) clearsexpires_atand prevents TTL cleanup. - Limits can be enforced via
MAX_SESSION_ITEMSandMAX_SESSION_BYTES.
Configuration
Environment variables:
See .env.example for a copy-paste starting point.
PORT(default8080): HTTP port.DATA_DIR(default/data): output directory for transcripts.YTDLP_BIN(defaultyt-dlp): path to the yt-dlp binary.YTDLP_PLAYER_CLIENT(defaultweb_safari): YouTube player client used by yt-dlp.YTDLP_REMOTE_EJS(defaultejs:github): yt-dlp remote components selector.YTDLP_SUB_LANG(defaulten.*): subtitle language pattern.YTDLP_TIMEOUT_SEC(default180): yt-dlp subprocess timeout.YTDLP_INFO_CACHE_TTL_SEC(default300): cache TTL for yt-dlp metadata lookups.AUTO_TEXT_MAX_BYTES(default200000): threshold foryoutube_transcribe_autotext responses.TRANSCRIPT_TTL_SECONDS(default3600): file expiry for session items (falls back toDEFAULT_TTL_SECif set).INLINE_TEXT_MAX_BYTES(default20000): inline content threshold fortranscripts://session/{session_id}/item/{id}.MAX_SESSION_ITEMS(default0unlimited): max items per session.MAX_SESSION_BYTES(default0unlimited): max bytes per session.DEFAULT_SESSION_ID(default empty): fallback session id when nosession_idormcp-session-idis provided.
Session identification
The server scopes data by session. Tools and resources use the MCP HTTP header mcp-session-id when available. If your client cannot send it, pass session_id explicitly to tools that require it. When both are present, they must match. If neither is provided, the server can fall back to DEFAULT_SESSION_ID.
Agent configuration examples
These snippets show how to register the running HTTP MCP server at http://localhost:8080/mcp. File paths and keys vary by client, so treat them as templates.
Codex CLI (~/.codex/config.toml template)
[features]
# Required in some Codex versions for HTTP MCP servers.
experimental_use_rmcp_client = true
[mcp_servers.yt_dlp_transcriber]
transport = "http"
url = "http://localhost:8080/mcp"
Claude Desktop (claude_desktop_config.json template)
{
"mcpServers": {
"yt-dlp-transcriber": {
"transport": "http",
"url": "http://localhost:8080/mcp"
}
}
}
If your Claude build only supports stdio-based MCP servers, run a local MCP bridge or adapt the server to stdio (not included here).
Other dev agents (Cursor, Cline, Continue, etc.)
{
"mcpServers": {
"yt-dlp-transcriber": {
"transport": "http",
"url": "http://localhost:8080/mcp"
}
}
}
Running locally (no Docker)
Prerequisites:
- Python 3.12+
yt-dlpavailable on PATH
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
export PORT=8080
export DATA_DIR=/tmp/yt-transcripts
mkdir -p "$DATA_DIR"
PYTHONPATH=src python -m server
The MCP HTTP endpoint listens at:
http://localhost:8080/mcp
Use an MCP-capable client to invoke the tools.
Makefile shortcuts
If you prefer a repeatable local workflow, use the included Makefile:
make install
make run
make test
Optional overrides:
make run PORT=9090 DATA_DIR=./data
make docker-build IMAGE=yt-dlp-transcriber:local
make docker-run PORT=8080 DATA_DIR=./data IMAGE=yt-dlp-transcriber:local
Testing
Install dev dependencies and run tests (pytest.ini sets PYTHONPATH=src):
pip install -r requirements-dev.txt
pytest
Coverage (line-level):
python -m coverage run -m pytest
python -m coverage report -m
Docker usage
Build
docker build -t yt-dlp-transcriber:local .
Run
Bind-mount a host directory to persist transcripts and expose the MCP port:
docker run --rm -p 8080:8080 \
-v "$PWD/data:/data" \
-e PORT=8080 \
yt-dlp-transcriber:local
This container installs yt-dlp and starts the FastMCP server at http://localhost:8080/mcp.
Sequence diagram (paged read)
sequenceDiagram
participant C as MCP Client
participant S as FastMCP Server
participant Y as yt-dlp
participant FS as DATA_DIR
C->>S: youtube_transcribe_to_file
S->>S: Validate URL
S->>Y: Run yt-dlp (auto-subs)
Y-->>S: VTT file
S->>S: Clean and de-duplicate
S->>FS: Write transcript file under data session_id
S-->>C: item id and relpath
loop Read chunks
C->>S: read_file_chunk
S->>FS: Read bytes
S-->>C: { data, next_offset, eof }
end
Error handling and limits
- Invalid URL format:
ValueErrorwith a message describing expected URL formats. yt-dlpfailure or timeout:RuntimeErrorwith captured stdout.- Missing subtitles:
RuntimeErrorwithyt-dlpoutput. read_file_chunkvalidatesmax_bytesand file existence.- MCP errors include explicit codes in the error message:
ERR_INVALID_SESSION,ERR_INVALID_ITEM,ERR_NOT_FOUND,ERR_EXPIRED_ITEM,ERR_EXTERNAL_COMMAND.
Notes and behavior details
- The server is stateless over HTTP, and file outputs are scoped to
/data/<session_id>with TTL-based expiry and a session manifest. - Subtitle language defaults to English (
en.*). Adjust withYTDLP_SUB_LANG. - The server prefers
.en.vttoutputs when multiple subtitle files exist. youtube_transcribe_autochooses text vs file output based on UTF-8 byte size, returningkind: "text"orkind: "file".youtube_transcribe_autoperforms a metadata call (youtube_get_duration) before downloading subtitles.session_idmust be 1-64 characters of letters, numbers,-, or_.- Pinned items are exempt from TTL cleanup.
read_file_chunkdecodes bytes using UTF-8 with replacement for invalid sequences.
Logging
The server emits structured logs via the yt_dlp_transcriber logger at debug,
info, warning, and error levels. Entries include a per-request request_id and
session_id where available. Configure logging in the hosting process to see
the details:
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("yt_dlp_transcriber").setLevel(logging.DEBUG)
Logs are also written to logs.txt inside DATA_DIR. On each server restart,
the previous log file is archived with a UTC timestamp and a new logs.txt is
created.
Contributing
Thanks for the interest. To keep the project focused, please open an issue before starting a PR so we can agree on scope. I make the final call on what gets merged, and I may decline changes that don't fit the project's goals.
If you submit a PR, keep it small and focused, include tests where relevant, and update documentation when behavior changes. By contributing, you agree that your work can be licensed under the MIT License.
Repository layout
src/server.py: thin composition root that starts FastMCP.src/mcp_server/: FastMCP wiring (tools/resources/templates/deps).src/services/: application services for transcription and sessions.src/adapters/: filesystem + yt-dlp adapters.src/ports/: protocols for repositories and transcribers.src/domain/: domain models, enums, and value objects.src/config.py: AppConfig and env parsing.src/logging_utils.py: structured logging helper.tests: unit tests for domain, storage, services, and MCP resources/templates.requirements.txt: runtime dependencies.requirements-dev.txt: test dependencies.Dockerfile: container build for running the server.Makefile: local dev, test, and Docker shortcuts.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.