YouTube MCP Connector
by deepujain
README.md
# 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](https://modelcontextprotocol.io/)
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:
1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create
(or pick) a project.
2. **APIs & Services → Library** → search **"YouTube Data API v3"** → **Enable**.
3. **APIs & Services → Credentials → Create Credentials → API key** — for
public reads. (Optional: restrict the key to the YouTube Data API v3.)
4. **OAuth consent screen** → user type **External** → fill app name, support
email, developer contact → add scopes:
- `https://www.googleapis.com/auth/youtube.readonly`
- `https://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.
5. **Create Credentials → OAuth client ID** → type **Desktop app** (personal
use) → run Google's OAuth flow once and capture an **access token**.
6. Copy `.env.example` to `.env` and fill in the values:
- `YOUTUBE_API_KEY` — public reads
- `YOUTUBE_OAUTH_TOKEN` — private reads + writes (refresh when it expires)
- `YOUTUBE_HOST` / `YOUTUBE_PORT` — bind address (default `127.0.0.1:8000`)
- `YOUTUBE_APPROVAL_TTL_SECONDS` — approval window (default `600`)
- `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
Local Python:
```bash
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):
```bash
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](https://modelcontextprotocol.io/) for your client's exact
config shape.
## Run the tests
```bash
.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:
- **`channelNotFound` on `my_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_PROXY` on purpose (they break inside containers). Set
`YOUTUBE_HTTP_PROXY` / `YOUTUBE_HTTPS_PROXY` explicitly; if the proxy
re-signs TLS with a private CA, point `YOUTUBE_CA_BUNDLE` at 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
1. "Catch me up on my subscriptions from this week — what did I miss?"
2. "Find me a video under 20 minutes that explains how sourdough starter works."
3. "Save this video for later: https://www.youtube.com/watch?v=…"
4. "What are the three most-viewed uploads from Marques Brownlee this month, and how long is each?"
5. "Make a private playlist called 'Weekend cooking' and add the pasta video you found yesterday."
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues