Building an MCP server for YouTube subtitles
There is no official way to download subtitles from someone else's YouTube video. The Data API has a captions.download endpoint, but it needs OAuth from the channel owner — useless if you just want to read a lecture.
So everyone uses the same private endpoint the YouTube apps use: youtubei/v1/player. POST a video ID and a client context, get back a list of caption tracks, each with a signed URL. No API key needed.
Two things will eat your afternoon.
Ask as a phone, not a browser. Send the WEB client context and you get a perfectly good track list whose caption URL returns HTTP 200 with an empty body. YouTube wants a proof-of-origin token there now. The ANDROID and IOS contexts still serve real text. Same request, one field different, and the failure looks like success.
Auto-generated captions parse differently. Tracks come back as srv3 XML. Human-written ones put text straight in <p>. Auto-generated ones split every word into <s> segments you have to stitch back together, and throw in text-free <p> elements used purely for timing. Miss that and your lecture transcript is mostly blank lines.
The MCP part is the easy bit. The official Go SDK handles the protocol; you write one tool that takes a URL and returns text. One warning: don't use the SDK's stateless mode. It answers GET /mcp with 405, and Claude's connector opens exactly that stream while setting up, decides your server has no tools, and shows an empty list. My curl tests only ever POSTed, so I stared at "this connector has no tools available" for a while.
Then the real wall: YouTube blocks datacenter IPs. Not everything — specific videos, from your specific server, for days. youtube-transcript-api documents the same behaviour and tells you to go buy residential proxies.
I tried being clever first. A cookie jar warmed on the homepage, a visitor ID, a genuinely signed-in cookie, even the SAPISIDHASH header YouTube's own web client sends. All correct. All useless. The same binary from a different IP walked straight through.
The fix was boring. ssh -N -D 1080 to another box I already had, then YT_PROXY_URL=socks5://tunnel:1080. Go's net/http speaks SOCKS5 natively, so it took zero lines of application code. Seven videos that had refused all day worked immediately.
A day of fingerprint archaeology, beaten by an SSH tunnel.