Skip to content

get_transcript returns empty text: YouTube now IP-binds signed caption URLs #3

Description

@SimonGeiger

Hey, thank you for putting this together and sharing it publicly — digging into it and trying to get transcripts working was a genuinely interesting MCP server learning experience for me. Unfortunately I wasn't able to get get_transcript working in my own testing, so I wanted to share what I found in case it's useful to you or anyone else running into the same thing.

Claude's Assessment: This report was investigated and together with Claude

get_transcript silently returns empty text for every video

What I found

get_transcript currently returns an empty string for every video I tried, with no error surfaced anywhere — the server responds 200 OK, so nothing looks broken from the outside, but the transcript field is blank.

What seems to be causing it

src/lib/youtube.ts uses the youtube-transcript npm package (v1.2.1), which scrapes the video's watch page for a captionTracks list, then fetches the transcript text from a signed URL embedded there.

Digging into that signed URL, it now has an IP binding baked into its signature:

https://www.youtube.com/api/timedtext?v=...&ip=0.0.0.0&ipbits=0&...&sparams=ip,ipbits,expire,...&signature=...

My read is: when the request isn't coming from what YouTube considers a genuine browser session, it issues the URL pre-signed against a dummy 0.0.0.0 address. The follow-up fetch to that URL still returns 200 OK, but with an empty body — no error, just silently no captions. I confirmed this by fetching the signed URL directly across multiple videos and multiple fmt= variants (json3, vtt, srv1, srv3); none returned content, and adding Referer/Origin/cookie headers to mimic a browser didn't change the outcome either.

My best guess is this is YouTube hardening against unofficial scraping generally (cloud/datacenter IP ranges — AWS, GCP, Azure, and by extension Cloudflare Workers — are commonly blocked/degraded this way), rather than anything specific to this repo's request shape — but I can't say that with certainty.

What I tried next

In my own fork, I swapped youtube-transcript for youtubei.js (calls YouTube's internal InnerTube API instead of scraping the page). That resolved the specific issue above — video metadata and caption-track listing worked fine — but info.getTranscript() then hit a hard 400 from YouTube's get_transcript endpoint specifically, across all client contexts I tested (WEB/IOS/TV/ANDROID). My guess is that endpoint now needs a PoToken (proof-of-origin token), which looks like a substantially bigger fix (BotGuard attestation) than a library swap, and seems to be an open problem across the wider YouTube-scraping ecosystem right now rather than something unique to this repo.

I'm happy to share the diff for the youtubei.js swap if it's useful as a starting point, even though it doesn't fully resolve things on its own — it at least turns "silently returns wrong data" into a clear thrown error.

To reproduce

curl -X POST https://<your-worker>/mcp -H "Content-Type: application/json" -d '{
  "jsonrpc":"2.0","id":1,"method":"tools/call",
  "params":{"name":"get_transcript","arguments":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","language":"en"}}
}'

Returns 200 OK with result.content[0].text empty.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions