Metadata-Version: 2.4
Name: youtube-mcp-dj
Version: 0.1.1
Summary: YouTube MCP server (streamable HTTP) over YouTube Data API v3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp<2,>=1.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Dynamic: license-file

# 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

From PyPI:

```bash
pip install youtube-mcp-dj
youtube-mcp   # console entry point; configure via env vars (see .env.example)
```

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).
