MCP server for fetching YouTube transcripts and shaping them as research-ready markdown.
Three well-named tools with comprehensive descriptions and proper JSON Schema inputs. All tools have clear verb-noun naming (fetch_*, format_*) and detailed parameter documentation. Tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are correctly applied. Output schemas are documented in descriptions. Main gaps: no explicit error recovery guidance in descriptions, no parameter constraints (enums, ranges), and descriptions could be more concise (some exceed 200 chars). fetch_transcript and fetch_video_metadata lack explicit rate-limit recovery steps beyond 'wait and retry'.
Fetch the transcript for a YouTube video. Self-throttled: each call sleeps a random 5-10 seconds (configurable via env) since the last call to avoid IP bans. If YouTube blocks this server's IP, the tool returns ``RATE_LIMITED`` and refuses to retry — wait, then try again later. Args: url_or_id: Either a YouTube URL (``https://youtu.be/<id>``, ``https://www.youtube.com/watch?v=<id>``, ``/shorts/<id>``, ``/embed/<id>``) or a bare 11-character video ID. language: Preferred language code (ISO-639-1, e.g. ``"en"``, ``"de"``). Empty (default) uses ``DEFAULT_LANGUAGE`` plus ``FALLBACK_LANGUAGES`` from server config. Returns: Success: ``{"data": {"video_id": str, "language": str, "language_code": str, "is_generated": bool, "transcript": str (with [MM:SS] markers), "snippet_count": int, "duration_seconds": float, "url": str}}``. Failure: ``{"error", "code", "details"}`` with code in ``{INVALID_INPUT, NOT_FOUND, RATE_LIMITED, UPSTREAM_DOWN, INTERNAL}``. Example: ``fetch_transcript("https://www.youtube.com/watch?v=dQw4w9WgXcQ")``
Fetch a YouTube video's title and channel. Uses YouTube's public oEmbed endpoint rather than the transcript scraper, so it carries a light one second interval instead of ``fetch_transcript``'s five to ten. It is the same source IP, and YouTube blocks per IP, so it is not free to hammer. Intended to run before ``format_transcript_as_research``, which needs a title and channel to produce usable frontmatter and cannot look them up itself. **Check ``metadata_available`` before giving up.** When it is false, YouTube answered but declined to describe the video, which usually means embedding is disabled. The transcript is normally still fetchable, so continue to ``fetch_transcript`` and pass an empty title through. Args: url_or_id: A YouTube URL (``https://youtu.be/<id>``, ``https://www.youtube.com/watch?v=<id>``, ``/shorts/<id>``, ``/embed/<id>``) or a bare 11-character video ID. Returns: Success: ``{"data": {"video_id": str, "title": str, "channel": str, "url": str, "thumbnail_url": str, "metadata_available": bool, "reason": str}}``. ``metadata_available`` is false, with ``title`` and ``channel`` empty, when embedding is disabled or oEmbed returned no title; the video is usually still readable via ``fetch_transcript``. Failure: ``{"error", "code", "details"}`` with code in ``{INVALID_INPUT, NOT_FOUND, RATE_LIMITED, UPSTREAM_DOWN, INTERNAL}``. ``NOT_FOUND`` means no such video. Example: ``fetch_video_metadata("https://www.youtube.com/watch?v=dQw4w9WgXcQ")``
fetch_transcript and fetch_video_metadata lack explicit error recovery guidance. Descriptions mention RATE_LIMITED and UPSTREAM_DOWN codes but do not tell the LLM what action to take (e.g., 'If RATE_LIMITED, wait 60+ seconds and retry' or 'If UPSTREAM_DOWN, check YouTube status page'). This violates pattern:recovery-guide.
language parameter in fetch_transcript accepts free-form ISO-639-1 codes with no enum constraint. Description says 'e.g. en, de' but does not enumerate valid values or link to a reference. LLMs may hallucinate invalid codes like 'english' or 'en-US'. Should either provide an enum or a strict format constraint.
fetch_video_metadata description is verbose (>300 chars) and buries the key point. The note about metadata_available=false is important but embedded in a long paragraph. Should be split: short summary, then a separate 'Important' section or bullet point.
Inferred effective spec: 2026-07-28+.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 64 | 2026-07-28+ | v2 |
Format a transcript into research-report markdown with frontmatter. Pure formatting: no network I/O, no rate limiting, no disk. Given the same arguments it returns the same markdown forever. The caller writes the file into their own workspace. Intended to run after ``fetch_transcript`` and ``fetch_video_metadata``, which supply the transcript text, title, and channel. If either is empty, this tool fills in sensible defaults (e.g. "YouTube transcript: <video_id>" for a missing title). Args: transcript: The transcript body text, already formatted (with or without timestamps). Typically the ``transcript`` field from ``fetch_transcript``. video_id: The 11-character YouTube video ID. title: Video title. Defaults to "YouTube transcript: <video_id>" if empty. channel: Channel name (free-form). Optional. url: Canonical YouTube URL. Defaults to the watch URL for ``video_id`` if empty. language: Human-readable language label (e.g. "English"). Optional. is_generated: True if this is YouTube's auto-generated caption track. Defaults to False. Returns: ``{"data": {"slug": str, "suggested_path": str, "frontmatter_markdown": str}}``. The caller writes ``frontmatter_markdown`` to disk at ``suggested_path`` (or anywhere else in their workspace). Example: ``format_transcript_as_research(transcript="[00:00] Hello world", video_id="dQw4w9WgXcQ", title="Rick Astley - Never Gonna Give You Up", channel="Rick Astley")``
format_transcript_as_research has no explicit error handling documented. What happens if video_id is not 11 characters? If transcript is empty? If title/channel/url are malformed? Descriptions should state validation rules and error codes.
No batch or multi-call variants. If an agent needs to fetch transcripts for 10 videos, it must call fetch_transcript 10 times sequentially. A batch_fetch_transcripts(urls: list[str]) variant would reduce token overhead and latency.