Custom YouTube Transcribe MCP Server
Click on "Install 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., "@Custom YouTube Transcribe MCP ServerTranscribe YouTube video dQw4w9WgXcQ"
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.
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
Related MCP server: YouTube Subtitle MCP Server
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| ClientClass 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 --> TranscriptFormatServices 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 --> ManifestRepositoryDetailed 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.jsonRetention 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 serverThe MCP HTTP endpoint listens at:
http://localhost:8080/mcpUse 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 testOptional 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:localTesting
Install dev dependencies and run tests (pytest.ini sets PYTHONPATH=src):
pip install -r requirements-dev.txt
pytestCoverage (line-level):
python -m coverage run -m pytest
python -m coverage report -mDocker 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:localThis 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 }
endError 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.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables fetching, searching, and analyzing YouTube video transcripts in multiple languages using yt-dlp. Supports timestamp filtering, language detection, and transcript summaries with robust error handling for production use.4MIT
- AlicenseAqualityCmaintenanceFetches YouTube video subtitles and transcripts with support for multiple languages and output formats (SRT, VTT, TXT, JSON).110Apache 2.0
- FlicenseNot gradedqualityFmaintenanceEnables retrieval of YouTube video transcripts/subtitles and titles using yt-dlp, with automatic subtitle conversion to reduce tokens. Supports Streamable HTTP transport for easy integration with frontends like OpenWebUI and Odysseus.
- AlicenseNot gradedqualityDmaintenanceExtracts clean text transcripts from YouTube videos using their subtitles and returns them as plain text.13MIT
Related MCP Connectors
Fetch transcripts, subtitles, chapters, metadata and frames from YouTube and 10+ video platforms
Extract YouTube transcripts, search what was said, and read on-screen frames with cited timestamps.
YouTube transcripts, search, channels, playlists and bulk transcript jobs for AI agents. 14 tools.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/rroos64/youtube_transcribe_mcp_server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server