Skip to main content
Glama
README.md
# youtube-mcp

A small MCP server that lets a model read public YouTube data. Three tools, no
API key, nothing clever:

| tool | what you get |
| --- | --- |
| `youtube_transcript` | what is said in a video, as plain text, optionally trimmed to a time window, plus which language it came back in |
| `youtube_playlist` | the videos in a playlist, in order |
| `youtube_video_facts` | title, channel, duration, publish date, views, likes, description, caption languages |

That is the entire surface. The server fetches, checks the link, trims by
timestamp and returns. Summarising, searching and ranking are the calling
model's job, not this server's.

## Requirements

- Python 3.12 or newer
- [uv](https://docs.astral.sh/uv/) for the short path, though plain pip is fine

## Install

```bash
git clone https://github.com/chiekh-a/youtube-mcp.git
cd youtube-mcp

uv venv
uv pip install -e ".[dev]"
```

With pip instead:

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

## Configuration

Copy the example file and fill in whatever you need:

```bash
cp .env.example .env
```

All of it is optional. With an empty `.env` the server still runs, it just talks
to YouTube directly from wherever it happens to be.

| variable | default | what it does |
| --- | --- | --- |
| `YOUTUBE_USE_PROXY` | `false` | the switch that turns the proxy on |
| `YOUTUBE_PROXY_URL` | unset | proxy endpoint, credentials included |
| `MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
| `MCP_HOST` | `127.0.0.1` | bind address, http only |
| `PORT` | `8000` | bind port, http only |
| `PUBLIC_BASE_URL` | unset | where the server is reachable, used for the icon URL |

`PUBLIC_BASE_URL` is only worth setting if you deploy somewhere that is not
Railway. Railway publishes the domain itself and the server picks it up. Leave
it unset over stdio, where the icon travels inline instead.

### Why the proxy exists

YouTube is aggressive about blocking datacenter IP ranges. On your own machine
you will usually be fine without a proxy. On a server (Railway, a VPS, CI) the
transcript requests start coming back as bot checks fairly quickly, and pointing
them at a residential proxy is the fix.

Both variables have to be set before anything is routed. A URL sitting in the
environment with `YOUTUBE_USE_PROXY=false` is ignored, which makes it easy to
flip on and off without editing config.

The proxy URL contains a password. It is never logged, and `.env` is gitignored.
Please keep it that way.

## Run it

Over stdio, which is what a desktop MCP client expects:

```bash
uv run python -m youtube_mcp
```

Over HTTP, for when the server lives somewhere else:

```bash
uv run python -m youtube_mcp --transport http --host 0.0.0.0 --port 8000
```

The endpoint is `/mcp`, so a local server answers at
`http://127.0.0.1:8000/mcp`. Leave the trailing slash off. A URL ending in
`/mcp/` gets redirected, and the redirect loses the session header, so the
client fails with a confusing 400.

Flags: `--transport`, `--host`, `--port`, `--log-level`. Logs always go to
stderr, because stdout belongs to the MCP protocol.

## Connect a client

### Local, over stdio

Drop this into `claude_desktop_config.json`, or `.mcp.json` for Claude Code, and
point the path at your checkout:

```json
{
  "mcpServers": {
    "youtube": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/youtube-mcp",
        "run",
        "python",
        "-m",
        "youtube_mcp"
      ],
      "env": {
        "YOUTUBE_USE_PROXY": "false"
      }
    }
  }
}
```

If you would rather not go through `uv`, call the venv's Python directly:

```json
{
  "mcpServers": {
    "youtube": {
      "command": "/absolute/path/to/youtube-mcp/.venv/bin/python",
      "args": ["-m", "youtube_mcp"]
    }
  }
}
```

Claude Code can skip the file editing entirely:

```bash
claude mcp add youtube -- uv --directory /absolute/path/to/youtube-mcp run python -m youtube_mcp
```

### Remote, over HTTP

Once it is deployed somewhere:

```json
{
  "mcpServers": {
    "youtube": {
      "type": "http",
      "url": "https://your-app.up.railway.app/mcp"
    }
  }
}
```

No trailing slash on that URL, for the reason above.

## Deploy to Railway

There is a `Dockerfile` and a `railway.json` in the repo, so Railway needs very
little from you.

1. Push the repo to GitHub.
2. In Railway, create a project from the repo. It finds the Dockerfile by itself.
3. Under Settings, Variables, add:
   - `MCP_TRANSPORT` = `http`
   - `MCP_HOST` = `0.0.0.0`
   - `YOUTUBE_USE_PROXY` = `true`
   - `YOUTUBE_PROXY_URL` = your proxy endpoint
4. Generate a domain under Settings, Networking.

`PORT` comes from Railway, so leave it alone.

From the CLI instead:

```bash
railway login
railway init
railway up
railway domain
```

Two things to keep in mind about a public deployment. The HTTP endpoint has no
auth on it, so anyone holding the URL can spend your proxy quota. And a hosted
IP will run into YouTube's bot checks without a residential proxy, which is the
whole reason `YOUTUBE_USE_PROXY` is there.

## How it behaves

Small decisions that occasionally surprise people, all of them on purpose:

- Only `https://` links, and only on `youtube.com`, `m.youtube.com` or `youtu.be`.
- Shorts are turned away. Pass a regular video link.
- Captions are tried in this order and the first hit wins: en, ar, fr, es, de,
  it, pt, ru, zh. If a video has none of those, you get whatever it does have
  rather than an error, since the model reading it can translate. The response
  says which language turned up, and whether a human wrote the captions or
  speech recognition did.
- Trimming is an overlap test, not a containment test. A caption line that
  begins before `start_seconds` but is still being spoken at that moment is kept
  whole, because half a sentence is worth less than a slightly wider clip.
- `start_seconds` has to be strictly smaller than `end_seconds`. Equal values are
  an error, and it is caught before any request goes out.
- A video with no captions in a supported language is an error. A time window
  that lands past the end of the video is simply an empty result.
- Only text comes back, never timestamps. They are dead weight in a context window.
- `youtube_playlist` also accepts a watch link that happens to be playing inside
  a playlist, since it carries the same `list=` id.

## When something breaks

**"Sign in to confirm you're not a bot", or "YouTube is blocking requests from
your IP".** The host is on an IP range YouTube does not trust, which is normal
for any cloud provider. Set `YOUTUBE_USE_PROXY=true` with a working residential
proxy. Playlist listing often keeps working while transcripts and video facts
fail, so a partial outage like that is usually this.

**`Tunnel connection failed: 407 Proxy Authentication Required`.** The proxy is
reachable and it turned your credentials down. Check the username and password,
and check whether the provider expects the client IP to be allowlisted first.
The scheme on the proxy URL is not the problem, `http://` and `https://` behave
the same here.

**A 400 from the HTTP endpoint straight after connecting.** The client URL ends
in `/mcp/`. Drop the trailing slash.

**Nothing at all over stdio.** Something wrote to stdout. Only this server's own
logging is careful about that, so a stray `print` in your own changes is the
usual culprit.

## Tests

```bash
uv run pytest
```

Nothing in the suite touches the network. An autouse fixture in
`tests/conftest.py` swaps both clients for tripwires that raise on contact, so a
forgotten stub fails loudly instead of quietly making live requests.

## Layout

```
youtube_mcp/
  __main__.py   flags, transport choice, start
  server.py     the FastMCP instance and the three tools
  links.py      parsing YouTube URLs into ids
  captions.py   caption fetching and the time window
  catalog.py    yt-dlp: playlists and video facts
  schemas.py    what the tools return
  settings.py   the only module that reads the environment
  errors.py     the two ways a call can fail
  branding.py   the icon clients show, and the routes serving it
  assets/       the icon itself, as PNG and SVG
```

The icon is a hand drawn play button rather than YouTube's official artwork, so
there is nothing here that belongs to anyone else.

## License

MIT

TDQS

A4.7/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: transcript extracts spoken content, playlist lists videos in a playlist, and video_facts provides metadata. There is no overlap or ambiguity between them.

Naming Consistency5/5

All tool names follow the same pattern: 'youtube_' prefix followed by a descriptive noun or noun phrase. The naming is consistent and predictable, using lowercase with underscores throughout.

Tool Count5/5

Three tools is a reasonable and well-scoped count for a YouTube-focused MCP server. Each tool covers a core, distinct capability without unnecessary bloat, falling within the typical 3-15 range.

Completeness4/5

The tool set covers the main YouTube interaction needs: reading transcripts, fetching video metadata, and enumerating playlists. Minor gaps exist (e.g., no search or comment retrieval), but the core workflows are complete and work well together.

Maintenance

ActivityMaintained
ResponsivenessNo issues