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

A small, self-hosted [MCP](https://modelcontextprotocol.io) server that gives
any MCP-compatible AI assistant (Claude, Claude Desktop / Cowork, or any
other MCP client) read access to **your own YouTube channel's** stats and
upload schedule.

It runs entirely on your own machine via Docker. Your Google OAuth refresh
token stays in a local file that's never committed to git and never leaves
your container. Anyone can clone this repo, connect it to their **own**
Google account, and start querying their **own** channel — no shared
credentials, no central server, no data collected by anyone but you.

## Features

Five tools, all backed by the [YouTube Data API v3](https://developers.google.com/youtube/v3):

| Tool | Description |
|---|---|
| `list_my_channels` | Profile + stats (subs/views/video count) for the authenticated channel |
| `get_channel_stats(channelId?)` | Public stats for any channel, or the authenticated one if omitted |
| `get_recent_uploads(maxResults=10)` | Most recent published videos, each with view/like/comment counts and duration |
| `get_upload_schedule(lookahead=50)` | Upcoming scheduled/private uploads and premieres/live streams not yet public |
| `get_video_stats(videoId)` | Views/likes/comments and metadata for a specific video |

The server uses the `youtube.readonly` scope only — it can read your
channel's data but cannot upload, edit, delete, or change anything.

## Prerequisites

- [Docker](https://docs.docker.com/get-docker/) and Docker Compose (to run the server)
- [Node.js 18+](https://nodejs.org/) (only needed for the one-time OAuth setup script — the server itself runs in Docker)
- A Google account that owns or manages the YouTube channel you want to track

## Quick start

```bash
git clone https://github.com/YOUR_GITHUB_USERNAME/youtube-stats-mcp.git
cd youtube-stats-mcp
npm install
```

1. **Create a Google Cloud OAuth client** — one-time setup, see the
   walkthrough below. You'll end up with a Client ID and Client Secret.

2. **Configure your credentials:**

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

   Open `.env` and fill in `YOUTUBE_CLIENT_ID` / `YOUTUBE_CLIENT_SECRET` from
   step 1.

3. **Authorize your Google account:**

   ```bash
   npm run setup
   ```

   This prints a Google consent URL. Open it in the browser signed into the
   YouTube account you want to track, approve access, and the script saves
   `secrets/youtube_token.json` for you. That folder is gitignored — it
   never gets committed.

4. **Build and run the server:**

   ```bash
   docker compose up -d --build
   ```

5. **Confirm it's up:**

   ```bash
   curl http://localhost:8787/
   # {"ok":true,"name":"youtube-stats-mcp"}
   ```

6. **Connect it to your MCP client** — see "Connecting it to Claude / Cowork /
   any MCP client" below.

## Create a Google Cloud OAuth client

You need your own OAuth client because this scope (`youtube.readonly`) is
tied to a specific Google Cloud project and can't be shared across users.
This takes about five minutes and is free.

1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and
   create a new project (or pick an existing one).
2. **APIs & Services > Library** — search for **YouTube Data API v3** and
   click **Enable**.
3. **APIs & Services > OAuth consent screen**:
   - User type: **External** (unless you have a Google Workspace org and want Internal)
   - Fill in an app name, your email as support contact, and your email again as developer contact
   - Scopes: add `.../auth/youtube.readonly`
   - Test users: add the Google account whose channel you're tracking
   - Save — you can leave this in "Testing" status, but read
     ["Why this keeps happening"](#why-this-keeps-happening-invalid_grant) below
     before you do
4. **APIs & Services > Credentials > Create Credentials > OAuth client ID**:
   - Application type: **Desktop app** (important — this is what lets the
     setup script use an arbitrary local port without pre-registering an
     exact redirect URI)
   - Name it whatever you like
   - Click **Create**, then copy the **Client ID** and **Client Secret**
     into your `.env` file

## Connecting it to Claude / Cowork / any MCP client

The server speaks MCP over Streamable HTTP at:

```
http://localhost:8787/mcp
```

Add it as a custom/local MCP server in your client's settings — the exact
menu depends on the app (look for "Add custom connector" / "Add MCP server").
Once connected, tool names will appear prefixed by however your client names
the connection, e.g. `mcp__youtube-stats-mcp__list_my_channels`.

## Multi-channel / brand accounts

The token only grants access to the channel tied to the Google account you
authorized in step 3 above. If you (or someone using this repo) manage
separate brand-account channels, each one needs its own:

- OAuth authorization run (`npm run setup`, signed into that brand account)
- Token file (change `TOKEN_PATH` to a different file)
- Container/port (copy `docker-compose.yml`, give it a different service
  name, host port, and `TOKEN_PATH`)

## Re-authorizing (`invalid_grant` errors)

If a tool call fails with `invalid_grant`, your refresh token has expired or
been revoked. Fix it:

```bash
npm run reauth
```

This reuses the client ID/secret already saved in
`secrets/youtube_token.json`, opens a fresh consent URL, and overwrites the
token file. Then restart the server:

```bash
docker compose up -d --build
```

### Why this keeps happening (`invalid_grant`)

If your Google Cloud project's OAuth consent screen is still in **Testing**
publishing status, Google expires refresh tokens for it after **7 days** —
no exceptions. This is by far the most common cause of recurring
`invalid_grant` errors on a self-hosted setup like this one. Two ways to stop
the cycle:

- **Move the consent screen to "In production"** (OAuth consent screen >
  Publish App). For a personal, narrow-scope (`youtube.readonly`) app this
  does not require Google's formal verification review in practice — tokens
  then last until you explicitly revoke them or leave them unused for 6
  months.
- Otherwise, budget for running `npm run reauth` weekly while in Testing mode.

Other things that produce auth errors, and what they mean:

| Error | Likely cause |
|---|---|
| `redirect_uri_mismatch` | Your OAuth client isn't type "Desktop app" — recreate it as that type, or add the exact printed redirect URI to "Authorized redirect URIs" on a Web app client |
| `access_denied` / "Access blocked: this app's request is invalid" | Your Google account isn't listed as a Test user on the OAuth consent screen (while it's in Testing status) |
| `invalid_grant` | Refresh token expired (see above) or was revoked at [myaccount.google.com/permissions](https://myaccount.google.com/permissions) |
| No `refresh_token` returned after consent | Google only issues a new one on first consent per account+app; remove the app's access at [myaccount.google.com/permissions](https://myaccount.google.com/permissions) and re-run `npm run setup` |

## Security

- **Local-only by default.** The server binds to all interfaces on the port
  you publish, but `docker-compose.yml` only publishes it to your host's
  `localhost` unless you change that mapping yourself. Don't expose port
  8787 to the public internet without also setting `MCP_AUTH_TOKEN`.
- **Optional bearer-token auth.** Set `MCP_AUTH_TOKEN` in `.env` to require a
  matching `Authorization: Bearer <token>` header on every `/mcp` request —
  useful if you're reaching the server over Tailscale, ngrok, etc.
- **Read-only scope.** `youtube.readonly` cannot upload, edit, or delete
  anything on your channel.
- **Credentials never committed.** `secrets/` and `.env` are gitignored and
  excluded from the Docker build context (`.dockerignore`). Only
  `.env.example` (no real values) is tracked.
- **Revoke access anytime** at [myaccount.google.com/permissions](https://myaccount.google.com/permissions).
- Runs as a non-root user inside the container (see `Dockerfile`).

## Development (without Docker)

```bash
npm install
npm start
```

Reads the same `.env` / `secrets/youtube_token.json` as the Docker setup.

## Project structure

```
youtube-stats-mcp/
├── src/
│   └── index.js        # MCP server + YouTube API calls
├── reauth.js            # OAuth setup / re-auth script (npm run setup | reauth)
├── secrets/              # gitignored — holds youtube_token.json after setup
├── .env.example
├── docker-compose.yml
├── Dockerfile
└── package.json
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

[MIT](LICENSE)