YouTube Transcript MCP
# YouTube Transcript MCP
**Search YouTube, then get transcripts, without paying for ones you could have had free.**
Most YouTube videos already have captions. This gets those first, checks they're any good, and only pays a model to transcribe the video when they're missing or broken. A typical call costs nothing.
You need nothing to start. A key is only for the fallback.
```json
{
"mcpServers": {
"yt-transcript": {
"command": "npx",
"args": ["-y", "yt-transcript-gemini-mcp"],
"env": { "AI_GATEWAY_API_KEY": "paste-your-key-here" }
}
}
}
```
## What you get back
```
**Me at the zoo** by jawed, 19s.
Source: human-written captions (en). Free, no model call.
All right, so here we are in front of the elephants...
```
The source line is the point. A human-written caption track and a machine transcription are different kinds of evidence, and a transcript that doesn't say which it is can't be judged.
## Search
Search and filter in one call, free, no API key. The filters combine, which is the point:
> a video about `rust async` with over 50,000 views, from a channel with over 80,000 subscribers, longer than a minute
```
**3** result(s) for **rust async**.
Read 19 result(s), 3 channel lookup(s). Free, no API key.
- **Async Rust explained in 20 minutes**
`wXtngLBkK4Q` · 62,742 views · 2 months ago · 19:15
Let's Get Rusty (172,000 subscribers)
```
| Filter | Cost | Notes |
|---|---|---|
| `minViews` / `maxViews` | free | Comes back with the search results |
| `publishedWithinDays` | free | Coarse; see below |
| `minDurationSeconds` / `maxDurationSeconds` | free | `60` excludes most Shorts |
| `minSubscribers` | one request per channel | Runs last, after the free filters have cut the set. Each channel is looked up once however many of its videos survive |
**The date filter is approximate, and the tool says so every time you use it.** YouTube reports "3 weeks ago", not a date. Anything posted inside the current month reads as weeks at best, so "the past month" is honest and "since the 14th" is not.
The result tells you how many raw results were read and how many channel lookups it cost, so a filter that had to work hard is visible rather than silent. If it returns fewer than you asked for, raise `maxPages`.
## Several videos at once
`youtube_transcripts` takes up to 20 URLs or ids and runs them in parallel. One failure doesn't stop the rest.
**The paid fallback is OFF by default here**, which is the opposite of the single-video tool. Twenty videos is twenty bills, and an agent handed a search result will pass the whole list. `allowPaid: true` when you mean it.
## Three sources, in order
**1. The video's own captions.** Free. Fetched through YouTube's InnerTube endpoint, parsed, and checked before you get them.
**2. Gemini reads the URL.** Billed. The watch URL goes straight to the model; there's no download and no upload step. Needs a key.
**3. yt-dlp.** Free, if you have it installed. Last because it's the only step with a binary to install and keep current.
That order is deliberate and it isn't what the research recommended. Every write-up on this puts yt-dlp second, because it's the traditional answer. But Gemini needs no binary, no PATH, and no maintenance against a target that actively changes to break you, so it earns the middle slot.
## Run it locally
YouTube refuses most datacentre addresses outright. The caption path works from a home connection and fails from a cloud host, which quietly pushes every request onto the paid fallback and turns a free tool into a billed one.
`npx` in your own MCP config is exactly right. A hosted deployment is the case to think twice about.
## What it checks before handing captions over
All local string work, so the checks cost nothing. That's the design: the expensive decision is whether to pay, and deciding shouldn't itself cost money.
| Check | Rejects |
|---|---|
| Coverage | A track that stops a third of the way in. The text that IS there reads fine, which is what makes this the failure people miss |
| Language | A German track when you asked for English. Returning the wrong language silently is worse than returning nothing |
| Repetition | A stuck recogniser looping one phrase. Only applied to auto-generated tracks: a human track that repeats is a chorus |
| Punctuation | Auto-captions with no full stops or capitals. Sent for repair rather than rejected |
**Note:** the repetition check ignoring human tracks is not a nicety. Measured on a pop video, 56% of the phrasing repeated and the captions were perfect. Applying that check to a manual track rejects every chorus ever written.
## Repairing beats replacing
When a video has auto-captions and nothing better, the words are usually right. What's missing is punctuation, casing and paragraphs. Sending that text back as **text** costs a fraction of sending the **video**, because video is billed by sampled frames and audio seconds while text is billed by the word.
Measured on the same 19-second video:
| | cost |
|---|---|
| Transcribe the video | $0.0028 |
| Repair the captions | $0.0006 |
On by default. `YT_TRANSCRIPT_REPAIR=false` turns it off and you get the raw unpunctuated track instead.
## Two things that cost money for no reason
Both found by measuring, and both the opposite of what they look like.
**Thinking is off, and that's most of the saving.** Transcription has nothing to reason about, but the model reasons anyway: 438 of 503 output tokens on one call, 1,097 of 1,146 on another. Output is billed at five times input, so that was 96% of the bill spent deliberating over where to put full stops. Setting the thinking budget to zero gives identical text for a twentieth of the tokens.
**Flex tier is a false economy here, so it's off.** Flex halves both rates, which sounds like free money. It also ignores the thinking budget. Measured twice on the same prompt: standard with thinking off returned 33 output tokens; flex with identical options returned 604 and 650, nearly all reasoning. Half the rate on nineteen times the tokens is about nine times the bill. `YT_TRANSCRIPT_FLEX=true` if a future gateway release honours the setting.
## Getting a key
Only needed for the fallback. Captions work without one.
**Vercel AI Gateway** ([vercel.com/docs/ai-gateway](https://vercel.com/docs/ai-gateway)) is the one to get. `gemini-3.6-flash` resolves there.
**Google AI Studio** ([aistudio.google.com/apikey](https://aistudio.google.com/apikey)) also works, with a caveat worth knowing: on the key tested here, `gemini-3.6-flash` returned 404 while every other model resolved. If yours does the same, set `YT_TRANSCRIPT_MODEL=gemini-3.5-flash`, which works and transcribes fine.
Treat either like a password.
## About money
**Captions are free and most videos have them.** The bill only starts when they don't.
**Set a budget anyway.** Vercel and Google Cloud both let you set a spending limit and an alert. Two minutes each, and it's the difference between a surprise and a number you chose.
This server caps transcription at 30 per hour and 180 minutes per video, both on by default, because an agent looping over a playlist is the shape that runs up a bill nobody agreed to. Captions are never capped.
> **No liability for spend.** Free software, as-is, MIT. You are responsible for your own API usage and any charges Google or Vercel bill you, including charges caused by bugs, misconfiguration, runaway agents, or anything else. Neither Luke Rhodes nor Fledgeling is liable for your spending. Set a budget.
## Settings
All optional. The server runs with none of them, captions only.
| Setting | Default | What it does |
|---|---|---|
| `AI_GATEWAY_API_KEY` | | Vercel AI Gateway. Preferred, because `gemini-3.6-flash` resolves there |
| `GEMINI_API_KEY` | | AI Studio key. `GOOGLE_API_KEY` works too |
| `YT_TRANSCRIPT_MODEL` | `gemini-3.6-flash` | Which model transcribes. The gateway prefix is added for you |
| `YT_TRANSCRIPT_REPAIR` | `true` | Repair auto-captions with a text call instead of re-transcribing |
| `YT_TRANSCRIPT_FLEX` | `false` | Vercel's flex tier. Off because it ignores the thinking budget; see above |
| `YT_TRANSCRIPT_MAX_PER_HOUR` | `30` | Transcriptions per rolling hour. `0` removes the cap |
| `YT_TRANSCRIPT_MAX_MINUTES` | `180` | Longest video the paid path will accept |
| `YT_TRANSCRIPT_TIMEOUT_MS` | `600000` | How long to wait on a model call |
| `YT_DLP_PATH` | `yt-dlp` | Where yt-dlp lives, if it isn't on PATH |
## Tools
**`youtube_search`** searches and filters. Free, no key.
**`youtube_transcript`** takes a URL or a bare video id, plus optional `languages`, `timestamps`, and `allowPaid`. Set `allowPaid: false` to get captions or nothing, which is right when cost matters more than coverage.
**`youtube_transcripts`** does up to 20 at once, in parallel, with the paid fallback off unless you ask for it.
**`youtube_transcript_doctor`** says what actually works right now: whether captions are reachable from this address, whether a key is configured, whether yt-dlp is installed. Free.
## What it doesn't do
Timestamps only survive the caption path. A model transcription has no timings, so `timestamps: true` is quietly ignored there.
It doesn't work on private or unlisted videos, through any of the three paths.
It doesn't evade blocks. No proxy rotation, no token harvesting. On a refusal it says so and moves to the next source. Working around a bot check is against YouTube's terms and a treadmill nobody wins.
## Things that surprised me
Written down because they contradict what's widely published, and they'll drift again.
**Watch-page caption URLs are dead.** They return HTTP 200 with zero bytes, in every format, with every client parameter. That looks exactly like a video with no captions unless you check the length.
**Search and captions need different InnerTube clients.** `IOS` serves captions; its search response carries only `videoId`, with no view counts, dates or channel. `WEB` serves a full search result but its caption URLs return nothing. So the two paths use different clients, and neither is a typo.
**The subscriber count on a channel page is not `subscriberCountText`.** A channel response carries several of those and none of them is the channel's own: they're the recommended-channel sidebar. Measured on a channel with 4.24M subscribers, the two values present were 466 thousand and 49 thousand. The real figure is in the page header's metadata rows. An early version of this server reported the sidebar number, which is wrong and entirely plausible.
**`IOS` is the InnerTube client that works for captions.** yt-dlp's PO Token Guide lists `tv`, `android_vr` and `web_embedded` as the ones needing no token. Measured on 2 August 2026, those return `LOGIN_REQUIRED`, `LOGIN_REQUIRED` and `ERROR`. `IOS` answers with a usable caption list. The guide may describe a different request context, or it may have drifted.
The client list is ordered rather than fixed, because this is an undocumented surface and a second client is a cheaper recovery than a release.
## For developers
TypeScript, ESM, Node 20.11 or newer. FastMCP over stdio, Vercel AI SDK v7, Zod at every boundary, `exactOptionalPropertyTypes` on, no `any`.
```bash
npm install
npm run gate # typecheck, lint, test, build
```
## Who made this
I'm **Luke Rhodes**, a founder and engineer building [Fledgeling](https://www.fledgeling.app). I spent years shipping developer tools and got tired of proprietary formats and slow editors, so Fledgeling is the opposite of that: fast software built from scratch, your work in plain text you own, and AI as a collaborator you can always overrule. The human is always the editor of record.
I'm also co-founder of [Diolog](https://diolog.app) with Amy Benson, building investor-relations software for listed companies and the retail investors following them.
- GitHub: [github.com/lprhodes](https://github.com/lprhodes)
- LinkedIn: [linkedin.com/in/lukerhodes](https://www.linkedin.com/in/lukerhodes/)
- X: [x.com/lp_rhodes](https://x.com/lp_rhodes)
- Email: [hello@fledgeling.app](mailto:hello@fledgeling.app)
## Three related things
**[Media Gen Pro](https://www.npmjs.com/package/media-gen-pro-mcp)** generates images and video. Eight image models and four video ones behind one call, with the server reading your prompt to pick which. Returns file paths, not megabytes of base64.
```
npx -y media-gen-pro-mcp
```
**[Google Search MCP](https://www.npmjs.com/package/google-search-api-mcp)** does the looking: Google search through the Gemini API, returning the answer, the real source URLs, and the queries the model actually ran.
**[Dossier](https://www.npmjs.com/package/dossier-research-mcp)** runs proper deep research: several backends on the same question at once, then checks the citations resolve and flags where the backends disagree. The architecture of this server came out of a Dossier panel, and two of its recommendations turned out to be wrong when measured, which is exactly why the merge tells you which claims only one backend made.
## Licence
MIT. Use it, fork it, ship it.
TDQS
Scored across 4 tools
youtube_transcript and youtube_transcripts are potentially confusing due to their similar names, but their descriptions clearly distinguish single from batch operations. youtube_search and youtube_transcript_doctor are unambiguous.
All tool names use snake_case and share the youtube_ prefix, creating a consistent and predictable pattern. The use of singular vs plural is a clear convention for single vs batch operations.
With only 4 tools, the server is well-scoped and each tool serves a distinct purpose: single transcript, batch transcript, search, and diagnostics. The count feels appropriate for a focused transcript service.
The tool set covers the core workflow completely: search for videos, retrieve a single transcript, retrieve multiple transcripts, and check server capabilities. No obvious gaps exist for a transcript-focused server.