YouTube MCP Connector
An MCP server (streamable HTTP) connecting any MCP-compatible client to the YouTube Data API v3: search videos, read subscriptions and playlists, build a "catch me up" digest of recent uploads, and manage playlists — with every write gated behind an explicit approval step.
Works with
This is a standard Model Context Protocol
server — nothing in it is tied to any single assistant. Run it yourself
(below) and point any MCP-compatible client at http://127.0.0.1:8000/mcp:
- Meta Muse, Claude / Claude Code (Anthropic), ChatGPT (OpenAI)
- Cursor, Windsurf, Cline, and other MCP-capable coding assistants
- Any custom agent built on an MCP SDK (Python, TypeScript, …)
How it works
Any MCP client connects to this server's streamable-HTTP endpoint (/mcp).
Credentials are never hard-coded: the server reads
YOUTUBE_API_KEY / YOUTUBE_OAUTH_TOKEN from the environment (your local
.env file).
Auth split (mirrors the YouTube API itself):
| Tool kind | Credential | Why |
|---|---|---|
Public reads (search_videos, get_video_details) |
API key | Public data needs no user identity |
Private reads (my_subscriptions, my_playlists, catch_me_up) |
OAuth 2.0 user token | mine=true calls are per-user |
| All writes | OAuth 2.0 user token | Every mutation requires authorization |
Approval gating: write tools (create_playlist, add_to_playlist,
save_for_later, remove_from_playlist) never execute directly. They
return a pending_confirmation payload with a single-use, expiring token
(default 600 s). The agent surfaces the action description to the user; on
approval it calls confirm_action with the token, which executes exactly
once. cancel_action discards a pending action. This maps 1:1 onto the
approval-card UX in MCP clients.
Known API limitation: YouTube's Data API cannot read or modify the
native Watch Later playlist (support removed August 2016; the API returns
watchLaterNotAccessible). save_for_later therefore uses a user-owned
playlist named "Watch Later (via 1xAI)" as the supported replacement,
creating it on first use.
Setup
You need a Google Cloud project. Do not create anything from this repo — these are manual steps in your own Google account:
-
Go to Google Cloud Console and create (or pick) a project.
-
APIs & Services → Library → search "YouTube Data API v3" → Enable.
-
APIs & Services → Credentials → Create Credentials → API key — for public reads. (Optional: restrict the key to the YouTube Data API v3.)
-
OAuth consent screen → user type External → fill app name, support email, developer contact → add scopes:
https://www.googleapis.com/auth/youtube.readonlyhttps://www.googleapis.com/auth/youtube.force-ssl
Tip: publish the consent screen to Production. Apps left in Testing issue refresh tokens that expire after 7 days.
-
Create Credentials → OAuth client ID → type Desktop app (personal use) → run Google's OAuth flow once and capture an access token.
-
Copy
.env.exampleto.envand fill in the values:YOUTUBE_API_KEY— public readsYOUTUBE_OAUTH_TOKEN— private reads + writes (refresh when it expires)YOUTUBE_HOST/YOUTUBE_PORT— bind address (default127.0.0.1:8000)YOUTUBE_APPROVAL_TTL_SECONDS— approval window (default600)YOUTUBE_HTTP_PROXY/YOUTUBE_HTTPS_PROXY— only if your host needs an explicit egress proxy (ambient proxy env vars are intentionally ignored)YOUTUBE_CA_BUNDLE— path to a PEM CA bundle, only if your egress proxy re-signs TLS with a private CA (it extends the default trust store)
No billing account is needed: the API is free within the daily quota below.
Quota budget
Default project quota: 10,000 units/day, resetting at midnight Pacific. Costs are per call (each result page costs the full amount again).
| Tool | API call(s) | Units |
|---|---|---|
search_videos |
search.list |
100 |
my_subscriptions |
subscriptions.list |
1 / page |
my_playlists |
playlists.list |
1 / page |
get_video_details |
videos.list (≤50 IDs batched) |
1 |
catch_me_up |
subscriptions.list + channels.list batch + playlistItems.list per channel |
≈ 2 + #channels |
create_playlist (on confirm) |
playlists.insert |
50 |
add_to_playlist (on confirm) |
playlistItems.insert |
50 |
remove_from_playlist (on confirm) |
playlistItems.delete |
50 |
save_for_later (on confirm) |
playlistItems.insert (+ playlists.insert first time) |
50 (100 first time) |
Example daily budget: 5 searches (500) + 3 digests over 40 subscriptions
(~126) + 20 detail lookups (20) + 10 playlist writes (500) ≈ 1,150 units —
about 11% of the free quota. Design rule used throughout: never call
search.list when videos.list/playlistItems.list can answer the question.
On quotaExceeded (HTTP 403) every tool returns a structured
quota_exceeded error telling the user when the quota resets.
Run the server
From PyPI:
pip install youtube-mcp-dj
youtube-mcp # console entry point; configure via env vars (see .env.example)
Local Python:
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
cp .env.example .env # then fill in your keys
.venv/bin/python -m youtube_mcp.server
# Point your MCP client at http://127.0.0.1:8000/mcp
Or Docker (the image bakes in a /healthz liveness probe):
cp .env.example .env # then fill in your keys
docker build -t youtube-mcp .
docker run --env-file .env -p 8000:8000 youtube-mcp
Connect a client
Point any MCP-compatible client at http://127.0.0.1:8000/mcp
(streamable HTTP). Claude Code, Cursor, Windsurf, Cline, and ChatGPT's
developer mode all accept a remote MCP server URL in their MCP/integration
settings — paste the URL there. See the
MCP documentation for your client's exact
config shape.
Run the tests
.venv/bin/python -m pytest -q # unit tests (mocked HTTP): 71 tests
# Integration tests hit the real API (reads only, ~110 units).
# Skipped automatically when credentials are absent.
YOUTUBE_API_KEY=... YOUTUBE_OAUTH_TOKEN=... .venv/bin/python -m pytest -q -m integration
Integration tests are skipped automatically when credentials are absent.
Write-path tests run against mocks only — real writes are exercised manually
through the propose → approve → confirm_action flow.
Troubleshooting
Learned the hard way while building this:
channelNotFoundonmy_subscriptions/my_playlists/catch_me_up: the Google account that granted OAuth has no YouTube channel. Create one at youtube.com (any name works), then re-run the OAuth flow.quotaExceeded(HTTP 403) immediately: run OAuth against your own Cloud project. Shared/demo projects (e.g. Google's OAuth 2.0 Playground project) can already have their quota exhausted, and there is nothing you can do about it. Your own project gets a fresh 10,000 units/day.- Auth errors right after pasting a token: make sure you pasted the
full access token — a truncated paste fails every call. You can
sanity-check a token's scopes at
https://oauth2.googleapis.com/tokeninfo?access_token=TOKEN. - Refresh token stops working after ~7 days: your OAuth consent screen is still in Testing mode. Publish it to Production (step 4 of Setup).
- Behind a corporate egress proxy: the server ignores ambient
HTTP_PROXY/HTTPS_PROXYon purpose (they break inside containers). SetYOUTUBE_HTTP_PROXY/YOUTUBE_HTTPS_PROXYexplicitly; if the proxy re-signs TLS with a private CA, pointYOUTUBE_CA_BUNDLEat your PEM bundle.
Project layout
src/youtube_mcp/
config.py # env-var settings (no secrets in code)
errors.py # typed errors: auth, quota, not-found, API
client.py # httpx client: key/OAuth injection, error mapping, pagination
quota.py # quota cost table + daily budget
approvals.py # single-use expiring tokens for write approvals
tools.py # the 11 tool implementations (pure, fully tested)
server.py # FastMCP wiring → streamable HTTP
tests/
test_client.py # auth headers, error mapping, pagination
test_approvals.py # TTL, single-use, cancel
test_tools.py # every tool: happy path, errors, auth/quota, empty, paging
test_integration.py # real API, reads only, skipped without credentials
Example prompts
- "Catch me up on my subscriptions from this week — what did I miss?"
- "Find me a video under 20 minutes that explains how sourdough starter works."
- "Save this video for later: https://www.youtube.com/watch?v=…"
- "What are the three most-viewed uploads from Marques Brownlee this month, and how long is each?"
- "Make a private playlist called 'Weekend cooking' and add the pasta video you found yesterday."
License
MIT — see LICENSE.
Release files for youtube-mcp-dj 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| youtube_mcp_dj-0.1.1.tar.gz | 26.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| youtube_mcp_dj-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.5 kB
Release files / youtube_mcp_dj-0.1.1.tar.gz
| Download URL | youtube_mcp_dj-0.1.1.tar.gz |
|---|---|
| Size | 26.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
09616498fad2467fb0a726667f223b7f07151be313913094f28dfba0ecb80772
|
|
BLAKE2b-256 checksum How to use checksums |
6d28a7de0916d7f69650062ca4e02cd1e74c504b19a1a10b67b2ad2bedf65060
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / youtube_mcp_dj-0.1.1-py3-none-any.whl
| Download URL | youtube_mcp_dj-0.1.1-py3-none-any.whl |
|---|---|
| Size | 20.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
66d4285d2c8bd321070cf131f2672262f94d0de8535d61cbc252420736669597
|
|
BLAKE2b-256 checksum How to use checksums |
41228ed4f37a6e2e6c70de85251c9b2f5581ef202d31fb9a74c210f845caf3c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log