YouTube transcripts, video search, chapters and extractive summaries. No YouTube API key, no LLM.
One address for any MCP client. Your key stays in your Odel vault.
Not assessed yet.
Odel charges nothing per call. The publisher may charge for its own service.
Odel could not list its tools without signing in.

Hosted version (no setup): apify.com/parallelstudio/youtube-transcripts-mcp. Connect any MCP client to
https://parallelstudio--youtube-transcripts-mcp.apify.actor/mcpwith your Apify token, pay per call. This repo is the open-source code; you can also run it locally withnpm install && npm run build && node dist/index.js(stdio).
Give your AI agent YouTube transcripts, search, chapters and summaries. This MCP server fetches the captions of any public YouTube video (as plain text, timestamped segments or SRT) together with its metadata. It also searches YouTube, returns chapters, and builds structured extractive summaries where every point links to the moment it comes from.
It works with Claude Desktop, Claude Code, Cursor, VS Code, Windsurf and any other client that supports MCP over Streamable HTTP. You don't need a YouTube API key or a Google account. No LLM runs on the server, so you don't pay for anyone else's tokens.
| Tool | What it does | Key parameters |
|---|---|---|
get_transcript | Returns the transcript plus metadata: title, channel, duration, views, publish date, category, the available caption languages and whether the returned track is auto-generated. Output can be plain text, [m:ss]-timestamped paragraphs, {start, duration, text} segments or SRT. YouTube can also machine-translate it. | video, languages (priority list, default ["en"]), format (text | segments | srt), include_timestamps, translate_to, allow_auto_generated, fallback_to_any_language, max_chars |
search_videos | Searches YouTube through the same InnerTube search API the website uses. Returns id, title, channel, duration, views, relative publish time, description snippet, thumbnail, live flag and badges. Fetches more pages automatically up to limit. | query, limit (1–50), upload_date (hour…year), duration (short <4 min, medium 4–20 min, long >20 min), sort_by, features (e.g. subtitles, live, hd, creative_commons), hl, gl |
get_video_chapters | Returns chapters with start and end times and deep links. Uses the creator's chapters from the description or YouTube's chapter markers. If there are none, it derives topic segments from the transcript and titles each one with its distinctive keywords and a representative sentence. | video, source (auto | description | transcript), languages, target_chapters |
summarize_transcript | Builds a structured extractive summary without an LLM: a TL;DR, de-duplicated key points, keywords and keyphrases, bullets per chapter, and notable quotes. Each item has a timestamp and a …&t=123s link. | video, max_points (1–30, default 8), languages, bullets_per_chapter, max_quotes, keyword_limit |
video accepts an 11-character id (dQw4w9WgXcQ) or any YouTube URL: watch?v=, youtu.be/, /shorts/, /embed/, /live/, m. and music. hosts, and youtube-nocookie.com.
All tools are read-only.
Failures come back as MCP tool errors (isError: true). The text starts with Error (<CODE>): … and structuredContent.error holds { code, message, retryable, details }, so your agent can react to the code instead of parsing the message:
| Code | Meaning |
|---|---|
INVALID_VIDEO_ID / INVALID_INPUT | The video id/URL or another argument is invalid |
VIDEO_UNAVAILABLE | Removed, never existed, or the id is wrong |
VIDEO_PRIVATE | The video is private |
AGE_RESTRICTED | YouTube requires a signed-in account (age gate) |
VIDEO_UNPLAYABLE | Members-only, region-blocked, upcoming live stream, etc. (details.reason has YouTube's text) |
TRANSCRIPTS_DISABLED | The video has no captions at all |
NO_TRANSCRIPT_FOUND | None of the requested languages exist and fallback_to_any_language is false. details.available lists the tracks that do exist |
TRANSLATION_UNAVAILABLE | YouTube can't translate this track into translate_to |
NO_CHAPTERS | source: "description" was requested, but the description has no chapter list |
REQUEST_BLOCKED | YouTube blocked the request (HTTP 429, reCAPTCHA or "confirm you're not a bot") even after the server rotated proxy sessions. retryable: true |
PO_TOKEN_REQUIRED / EMPTY_TRANSCRIPT | YouTube served a caption URL that needs a PO token, or an empty caption file |
UPSTREAM_ERROR / INTERNAL_ERROR | Timeouts, 5xx responses or unexpected response shapes |
This Actor uses pay-per-event pricing. You pay only for successful tool calls. Failed calls are not charged, whether they fail on validation, because the video is private or has no captions, or because YouTube blocks the request. Current prices are on the Actor's Pricing tab.
| Event | Price | When it's charged | Count |
|---|---|---|---|
transcript | $0.005 | Each successful get_transcript call | 1 per call, whatever the video length or format |
search | $0.003 | Each successful search_videos call | 1 per call (up to 50 results, including extra pages fetched automatically) |
chapters | $0.008 | Each successful get_video_chapters call | 1 per call, from the description or derived from the transcript |
summary | $0.015 | Each successful summarize_transcript call | 1 per call. The transcript fetched internally is not charged separately as transcript |
A search that returns zero results is still a successful call and is charged as one search event.
Endpoint (Streamable HTTP):
https://<username>--youtube-transcripts-mcp.apify.actor/mcp
Authenticate with your Apify API token by sending it as Authorization: Bearer <APIFY_TOKEN>.
{
"mcpServers": {
"youtube-transcripts": {
"type": "http",
"url": "https://<username>--youtube-transcripts-mcp.apify.actor/mcp",
"headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
}
}
}
If your client only supports stdio servers, bridge with mcp-remote:
{
"mcpServers": {
"youtube-transcripts": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://<username>--youtube-transcripts-mcp.apify.actor/mcp",
"--header", "Authorization: Bearer ${APIFY_TOKEN}"
],
"env": { "APIFY_TOKEN": "<APIFY_TOKEN>" }
}
}
}
claude mcp add --transport http youtube-transcripts \
https://<username>--youtube-transcripts-mcp.apify.actor/mcp \
--header "Authorization: Bearer $APIFY_TOKEN"
.vscode/mcp.json){
"servers": {
"youtube-transcripts": {
"type": "http",
"url": "https://<username>--youtube-transcripts-mcp.apify.actor/mcp",
"headers": { "Authorization": "Bearer ${input:apifyToken}" }
}
},
"inputs": [{ "id": "apifyToken", "type": "promptString", "description": "Apify API token", "password": true }]
}
The excerpts below come from the bundled fixture videos used in the test suite (see demo/transcript.md for every call and response).
get_transcript { "video": "kM3pLz8QwT0", "format": "segments", "include_timestamps": true } (trimmed):
{
"video": {
"id": "kM3pLz8QwT0",
"url": "https://www.youtube.com/watch?v=kM3pLz8QwT0",
"title": "How Solar Panels Actually Work (Explained Simply)",
"channel": "Bright Circuits",
"durationSeconds": 552,
"duration": "9:12",
"viewCount": 1843291,
"publishDate": "2025-03-14T07:00:12-07:00",
"category": "Education",
"isLive": false
},
"transcript": {
"language": "en",
"languageName": "English",
"autoGenerated": false,
"translatedFrom": null,
"matchedPreference": true,
"format": "segments",
"segmentCount": 128,
"wordCount": 870,
"truncated": false,
"segments": [
{ "start": 0, "duration": 4.116, "time": "0:00", "text": "Hey everyone, welcome back to Bright Circuits." },
{ "start": 4.2, "duration": 4.116, "time": "0:04", "text": "Today we're answering a question I get" }
]
},
"availableLanguages": [
{ "languageCode": "en", "languageName": "English", "autoGenerated": false, "translatable": true },
{ "languageCode": "es", "languageName": "Spanish", "autoGenerated": false, "translatable": true },
{ "languageCode": "en", "languageName": "English", "autoGenerated": true, "translatable": true }
],
"translationLanguagesCount": 18
}
get_video_chapters on a video with no creator chapters (auto-captions only). The chapters are derived from the transcript:
| # | start | title (distinctive keywords) |
|---|---|---|
| 1 | 0:00 | Starter, water, grams |
| 2 | 2:00 | Autolyse, grams, warm |
| 3 | 4:11 | Stretch, folds, bulk |
| 4 | 5:52 | Cold, sides, surface |
| 5 | 9:34 | Lid |
| 6 | 10:30 | Cool, cutting, crumb |
summarize_transcript { "video": "kM3pLz8QwT0", "max_points": 6 } (excerpt):
The server fetches transcripts the same way the popular Python library youtube-transcript-api does:
REQUEST_BLOCKED. The page also yields the InnerTube API key, the publish date, the category and any chapter markers.captionTracks. The playability status is mapped to the error codes above: private, age-restricted, unavailable, unplayable, or "confirm you're not a bot".en-GB → en). Then download the timedtext file. The server parses srv1 XML, srv3 XML and json3.All three requests for a video go through one proxy session, because YouTube signs caption URLs for the IP that requested them.
YouTube blocks most cloud IPs. In a feasibility probe from Apify, direct requests fetched 1 of 5 test videos. With Apify's datacenter proxy, 4 of 5 worked, and the fifth video simply has no transcript. On the Apify platform this server therefore routes every YouTube request through Apify Proxy (datacenter by default), using sticky sessions and an undici ProxyAgent. When YouTube blocks a session (429, captcha, bot check, empty caption file, network error), the server discards that session and retries with a new IP, up to 5 sessions per call. A healthy session is reused for later calls.
When a video has no creator chapters, the server segments the transcript in a TextTiling style:
[Music]-style tags are removed.The method is deterministic and transparent, and it is fast: milliseconds after the transcript is fetched. Because it quotes the speaker's own words, it can't hallucinate.
Do I need a YouTube Data API key or a Google account? No. You only need an Apify account and token to call this hosted server.
Which languages are supported?
Any language YouTube has captions for. Pass a priority list such as ["de", "en"]. If none of them exist, the best available track is returned along with a note, unless you set fallback_to_any_language: false. With translate_to, YouTube machine-translates the track.
How long can videos be?
There's no fixed limit. A transcript costs one transcript event however long it is. For very long videos, use max_chars or summarize_transcript to keep your agent's context small.
Why did I get REQUEST_BLOCKED?
YouTube rejected every proxy IP the server tried for that call. This is usually temporary, so retry after a short pause. Blocked calls are not charged.
Is any data stored? No. The server is stateless and keeps only a short in-memory cache. It writes nothing to datasets or key-value stores.
Can I self-host it?
Yes. See below. Residential IPs can usually reach YouTube directly. On cloud servers, set YT_PROXY_URL.
npm install
npm run build
npm start # MCP over stdio
npm run start:http # Streamable HTTP at http://127.0.0.1:3000/mcp (GET / and /health = readiness)
npm test # build + unit tests + MCP smoke test, all offline against recorded fixtures
| env var | default | purpose |
|---|---|---|
PORT / HOST | 3000 / 127.0.0.1 | HTTP bind (--http); also --port, --host |
YT_PROXY_URL | (unset) | Your own proxy or proxies: comma-separated http://user:pass@host:port URLs, one sticky URL per session, round-robin |
YT_PROXY | (unset) | off disables proxying, even on Apify |
YT_PROXY_GROUPS / YT_PROXY_COUNTRY | datacenter / any | Apify Proxy groups (e.g. RESIDENTIAL) and country code on the platform |
YT_PROXY_ATTEMPTS | 5 | Proxy sessions tried per call before returning REQUEST_BLOCKED |
YT_FIXTURES | (unset) | Serve all YouTube requests from a fixture directory (offline mode, used by tests) |
HTTP_TIMEOUT_MS | 8000 | Per-request timeout (a stalled proxy exit is rotated, not waited for) |
HTTP_MAX_RETRIES | 2 | In-place retries on 5xx/timeouts for direct (unproxied) connections only; proxied calls rotate session instead |
YT_KEEP_ALIVE_MS | 60000 | Idle keep-alive of the proxy tunnel between calls |
YT_SLOW_SESSION_MS | 6000 | A proxy session whose operation took longer is retired so the next call gets a fresh IP |
CACHE_TTL_MS | 600000 | In-memory cache TTL (0 disables) |
BILLING | (unset); apify in the Docker image | apify enables Actor.charge() pay-per-event billing; log only prints charges to stderr |
On Apify (Standby mode). When APIFY_IS_AT_HOME is set, the server calls Actor.init(), forces HTTP mode, and listens on 0.0.0.0:$ACTOR_WEB_SERVER_PORT. GET / answers the Standby readiness probe and MCP is served at /mcp. YouTube traffic goes through Apify Proxy. The HTTP server has no authentication of its own: on Apify, the platform authenticates requests with your API token. If you self-host it beyond localhost, put it behind an auth proxy.
test/unit/*.test.mjs, node --test) run against hand-made fixtures that mirror real YouTube responses: ANDROID player JSON, srv1/json3 timedtext, WEB search pages with continuations and shelves, watch pages including a reCAPTCHA page, and every error status. They cover parsing, track selection, description and derived chapters, TextRank and summaries, the search params encoding, error mapping, and proxy session rotation.test/smoke.mjs) spawns the server over stdio with YT_FIXTURES, lists the tools, calls each tool (including error paths), checks that billing charges only successful calls, checks HTTP mode and the readiness probe, and writes demo/transcript.{json,md}.test/live-remote.mjs, not part of npm test) runs against a deployed Standby URL: MCP_URL=… APIFY_TOKEN=… node test/live-remote.mjs.npm run fixtures regenerates test/fixtures/ from scripts/make-fixtures.mjs..actor/ actor.json (Standby, /mcp), input_schema.json (empty), output_schema.json (MCP endpoint)
Dockerfile apify/actor-node:20, multi-stage TypeScript build, BILLING=apify
src/
index.ts entry: stdio or stateless Streamable HTTP; Apify Standby wiring
server.ts MCP tool registrations (zod schemas, error payloads, billing)
youtube.ts watch page → InnerTube player (ANDROID) → timedtext; track selection; error mapping
transcript.ts srv1/srv3/json3 parsing; text / SRT formatting
search.ts InnerTube search, protobuf filter params, continuation paging
chapters.ts description chapter parser; TextTiling-style derived chapters
text.ts tokenizer, stemmer, sentences, TF-IDF, TextRank, keywords
summary.ts extractive summary (key points, chapter bullets, quotes, stats)
http.ts timeouts/retries, proxy sessions + rotation, fixture mode
proxy.ts Apify Proxy / YT_PROXY_URL selection
fixtures.ts offline fixture resolver
billing.ts chargeEvent() → Actor.charge() when BILLING=apify
util.ts error codes, cache, id/URL parsing, time helpers
MIT