Skip to main content
Glama
CreatorGeetansh

YouTube MCP Server

README.md
# YouTube MCP Server

An open-source Model Context Protocol (MCP) server for using YouTube from MCP clients such as Claude Desktop, Claude Code, and Codex.

The primary workflow is:

1. Give an MCP client a list of songs.
2. Review ranked YouTube matches before anything is changed.
3. Create a private playlist from the selected videos.

The server will also provide quota-conscious tools for searching YouTube and reading videos, channels, playlists, and comments.

> [!IMPORTANT]
> The TypeScript package, stdio server, public and authenticated reads, PKCE OAuth, music preparation, confirmed new-playlist creation, and full previewed playlist mutations are implemented and tested. A prepared music draft can be committed to a new playlist or appended to an existing owned playlist.

## Design goals

- Safe playlist writes with preview-before-commit semantics.
- Official YouTube Data API v3 endpoints only.
- Bring-your-own Google OAuth client; the project never ships shared Google credentials.
- Secrets stored in the operating-system keychain whenever possible.
- Predictable quota use, pagination, caching, retries, and normalized errors.
- Local `stdio` transport for simple installation and a small attack surface.
- Structured, bounded tool outputs that treat YouTube content as untrusted data.
- Cross-platform TypeScript support on Node.js 20.17 or newer.

## Planned v1 scope

### Read tools

- Search videos, channels, and playlists.
- Read video, channel, playlist, and comment data.
- Read the authenticated user's channel, uploads, and playlists.
- Return provider page tokens for explicit, stateless pagination.

### Music playlist workflow

- Accept up to 50 structured tracks per preparation request.
- Search and rank likely YouTube music-video matches.
- Show ambiguity and alternatives instead of silently choosing weak matches.
- Commit explicitly selected matches to a new playlist or an existing owned playlist.
- Default new playlists to `private`.

### Comment management

- Post a top-level comment on a video, or reply to an existing comment.
- Delete a comment, or set its moderation status, behind a preview and a one-time handle.
- Uses the `youtube.force-ssl` scope already requested for playlist writes, so no additional consent is needed.

### Playlist management

- Create playlists and add videos.
- Update playlist metadata or privacy.
- Reorder or remove playlist items.
- Delete playlists after a short-lived, one-time confirmation handle is issued.

Playlist updates, item removal/reordering, and deletion use two tools: `youtube_prepare_playlist_mutation` returns the exact diff and a 10-minute handle without writing; `youtube_apply_playlist_mutation` rechecks ownership and the playlist snapshot before consuming that handle once.

### Video upload and lifecycle

- Upload a local video file through a resumable upload, defaulting to `private`.
- Update title, description, tags, or privacy on a video you own. Unspecified fields are carried forward rather than cleared.
- Set a custom thumbnail from a local JPEG or PNG.
- Preview and then permanently delete a video you own.

Video writes require the additional `youtube.upload` scope and refuse outright when a profile was authorized without it. `youtube_plan_video_upload` validates the file and reports the cost without contacting YouTube, because `videos.insert` costs 1600 units — about 16% of a default daily budget per upload. An upload is never retried: if the outcome is unknown, that is reported so you can check for a partial upload instead of publishing a duplicate.

Playlist, comment, and video management are the implemented write surfaces. Analytics reporting (0.8) is on the roadmap and is not callable yet. Ratings, subscriptions, and channel-settings writes remain permanently out of scope.

## Setup

The npm package has not been published yet, so the server is built and run from a clone. Work through the steps in order.

### Step 1 — check for Node.js and npm

```bash
node -v
npm -v
```

If `node -v` prints `v20.17` or newer and `npm -v` prints a version, skip to [Step 3](#step-3--install-dependencies-and-build). If either command reports "command not found", continue with Step 2.

### Step 2 — install Node.js and npm (only if Step 1 failed)

npm ships with Node.js; installing Node installs both. Pick one row for your platform, then re-run Step 1 to confirm.

| Platform                                     | Command                                                                                                |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| macOS (Homebrew)                             | `brew install node@22`                                                                                 |
| macOS / Windows / Linux (no package manager) | Download the LTS installer from [nodejs.org/en/download](https://nodejs.org/en/download) and run it    |
| Windows (winget)                             | `winget install OpenJS.NodeJS.LTS`                                                                     |
| Debian / Ubuntu                              | `curl -fsSL https://deb.nodesource.com/setup_22.x \| sudo -E bash - && sudo apt-get install -y nodejs` |
| Fedora / RHEL                                | `sudo dnf install nodejs npm`                                                                          |

If you would rather not install Node system-wide, or you need several Node versions side by side, use a version manager:

```bash
# macOS and Linux
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
nvm install 22
nvm use 22
```

On Windows the equivalent is [nvm-windows](https://github.com/coreybutler/nvm-windows): `nvm install 22` then `nvm use 22`.

Close and reopen the terminal after installing, then re-run `node -v` and `npm -v`.

### Step 3 — install dependencies and build

```bash
git clone <repository-url>
cd "Youtube MCP"
npm ci
npm run build
```

`npm ci` installs the exact versions in `package-lock.json`; use `npm install` only when you intend to change dependencies. The build writes the executable to `dist/cli/index.js`, which every command below invokes.

Verify the build and the local data directory:

```bash
node dist/cli/index.js doctor
```

### Step 4 — create the Google credentials

Everything below comes from **your own** Google Cloud project. This project never ships shared Google credentials.

1. Create or select a project in the [Google Cloud console](https://console.cloud.google.com/).
2. Enable **YouTube Data API v3** for that project.
3. Create an **API key** (Credentials → Create credentials → API key). This covers public reads.
4. Configure the **OAuth consent screen**. While the project is in Testing status, add your own Google account under **Test users**, or `login` will be refused.
5. Create an **OAuth client** of type **Desktop app**, then copy both its **client ID** and its **client secret**.

Google requires `client_secret` in the authorization-code exchange even for installed applications, so PKCE supplements the secret here rather than replacing it.

### Step 5 — the credentials the server needs

Four credentials exist in total. You supply the first three; the fourth is obtained for you by `login`.

| Credential                    | Needed for                                                          | Where it comes from | How you supply it                                                        | Where it is kept                                                                                             |
| ----------------------------- | ------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `YOUTUBE_API_KEY`             | Public reads (search, videos, channels, public playlists, comments) | Step 4.3            | Process environment only                                                 | Not persisted. It is read from the environment on every start, so an MCP client must pass it in each launch. |
| `YOUTUBE_OAUTH_CLIENT_ID`     | Any account action: reading your own playlists, creating playlists  | Step 4.5            | `YOUTUBE_OAUTH_CLIENT_ID` env var, or the interactive `setup` prompt     | Profile JSON in the data directory. It is not a secret.                                                      |
| `YOUTUBE_OAUTH_CLIENT_SECRET` | The authorization-code exchange during `login`                      | Step 4.5            | `YOUTUBE_OAUTH_CLIENT_SECRET` env var, or the interactive `setup` prompt | Operating-system keychain, per profile. Never written to the profile JSON.                                   |
| OAuth refresh token           | Staying signed in across restarts                                   | Produced by `login` | —                                                                        | Operating-system keychain, per profile. Access tokens stay in memory only.                                   |

Optional environment variables: `YOUTUBE_MCP_PROFILE` (default `default`), `YOUTUBE_MCP_DATA_DIR`, and `YOUTUBE_MCP_LOG_LEVEL` (`error`, `warn`, `info`, `debug`). See `.env.example`.

Never paste any of these into a chat message, a shared MCP configuration file, or a command that will be committed. Prefer the interactive prompts or your client's environment/secret-injection field.

### Step 6 — run setup, then log in

Run these in order. `setup` rewrites the profile's stored scopes and channel identity, so running it _after_ `login` discards that state and requires logging in again.

macOS and Linux:

```bash
YOUTUBE_OAUTH_CLIENT_ID="YOUR_DESKTOP_CLIENT_ID" \
  YOUTUBE_OAUTH_CLIENT_SECRET="YOUR_DESKTOP_CLIENT_SECRET" \
  node dist/cli/index.js setup
node dist/cli/index.js login
node dist/cli/index.js status
```

Windows PowerShell:

```powershell
$env:YOUTUBE_OAUTH_CLIENT_ID  = "YOUR_DESKTOP_CLIENT_ID"
$env:YOUTUBE_OAUTH_CLIENT_SECRET = "YOUR_DESKTOP_CLIENT_SECRET"
node dist\cli\index.js setup
node dist\cli\index.js login
node dist\cli\index.js status
Remove-Item Env:\YOUTUBE_OAUTH_CLIENT_SECRET
```

To avoid putting the secret in shell history or the process table at all, omit both variables and let `setup` prompt for them:

```bash
node dist/cli/index.js setup
```

`setup` prompts for each missing value when the terminal is interactive.

`login` opens Google's authorization page and returns through a random loopback port on `127.0.0.1`, using PKCE S256 and a random state value. It fails immediately, before opening a browser, when no client secret is stored for the profile.

To revoke and remove the stored credential:

```bash
node dist/cli/index.js logout
```

### Step 7 — start the server

```bash
YOUTUBE_API_KEY="your-api-key" node dist/cli/index.js serve
```

The server speaks MCP over stdio, so it is normally launched by a client rather than by hand. Available commands are `serve`, `doctor`, `status`, `setup`, `login`, and `logout`.

### Local data location

Profiles, the quota ledger, drafts, and operation journals live in a `0700` directory:

| Platform | Default path                                                         |
| -------- | -------------------------------------------------------------------- |
| macOS    | `~/Library/Application Support/youtube-mcp`                          |
| Linux    | `$XDG_DATA_HOME/youtube-mcp`, otherwise `~/.local/share/youtube-mcp` |
| Windows  | `%LOCALAPPDATA%\youtube-mcp`                                         |

Override with `YOUTUBE_MCP_DATA_DIR`. To remove all local state, run `logout` and then delete that directory. Keychain entries are removed by `logout`.

## Connecting a client to the local build

Until the package is published, point clients at the absolute path of your built `dist/cli/index.js`.

### Claude Code

```bash
claude mcp add youtube --scope user \
  --env YOUTUBE_MCP_PROFILE=default \
  --env YOUTUBE_API_KEY=your-api-key -- \
  node /absolute/path/to/Youtube\ MCP/dist/cli/index.js serve
```

### Claude Desktop

```json
{
  "mcpServers": {
    "youtube": {
      "command": "node",
      "args": ["/absolute/path/to/Youtube MCP/dist/cli/index.js", "serve"],
      "env": {
        "YOUTUBE_MCP_PROFILE": "default",
        "YOUTUBE_API_KEY": "your-api-key"
      }
    }
  }
}
```

### Codex

```toml
[mcp_servers.youtube]
command = "node"
args = ["/absolute/path/to/Youtube MCP/dist/cli/index.js", "serve"]

[mcp_servers.youtube.env]
YOUTUBE_MCP_PROFILE = "default"
YOUTUBE_API_KEY = "your-api-key"
```

Re-run `npm run build` after pulling changes; clients execute the compiled `dist` output, not `src`.

Google authorization for this local server is performed by its own `setup` and `login` commands. Client-level MCP login commands do not replace the downstream Google OAuth flow.

## Post-publication client configuration

Once the package is released, pin a released version instead of using `latest` so an MCP client cannot change behavior unexpectedly.

### Claude Desktop

```json
{
  "mcpServers": {
    "youtube": {
      "command": "npx",
      "args": ["-y", "@youtube-mcp/server@0.7.0", "serve"],
      "env": {
        "YOUTUBE_MCP_PROFILE": "default"
      }
    }
  }
}
```

On native Windows, use `"command": "cmd"` and prefix the arguments with `"/c", "npx"`.

### Claude Code

```bash
claude mcp add youtube --scope user \
  --env YOUTUBE_MCP_PROFILE=default -- \
  npx -y @youtube-mcp/server@0.7.0 serve
```

### Codex

```bash
codex mcp add youtube \
  --env YOUTUBE_MCP_PROFILE=default -- \
  npx -y @youtube-mcp/server@0.7.0 serve
```

Equivalent Codex configuration:

```toml
[mcp_servers.youtube]
command = "npx"
args = ["-y", "@youtube-mcp/server@0.7.0", "serve"]

[mcp_servers.youtube.env]
YOUTUBE_MCP_PROFILE = "default"
```

## How much can be added in one go

Hard schema limits per tool call:

| Operation                                      | Maximum per call |
| ---------------------------------------------- | ---------------- |
| Tracks per `youtube_prepare_music_playlist`    | 50               |
| Selections per `youtube_commit_music_playlist` | 50               |
| Video IDs per `youtube_get_videos`             | 50               |
| Item removals per playlist mutation            | 50               |
| Reorder moves per playlist mutation            | 50               |
| Items per read page                            | 50               |

So **50 songs is the ceiling for a single commit**. A longer list is handled by committing in batches of 50: create the playlist with the first batch, then pass its `target_playlist_id` on each subsequent commit to append. Duplicates already present are skipped and reported rather than inserted twice.

The daily quota is the tighter constraint in practice. Against Google's default 10,000 units per project per day, one 50-song run costs roughly:

| Step                                   | Calls | Published unit cost | Subtotal    |
| -------------------------------------- | ----- | ------------------- | ----------- |
| `search.list`, one per track           | 50    | 100                 | 5,000       |
| `videos.list` hydration, batched by 50 | 1–5   | 1                   | 1–5         |
| `playlists.insert`                     | 1     | 50                  | 50          |
| `playlistItems.insert`                 | 50    | 50                  | 2,500       |
| **Total**                              |       |                     | **≈ 7,550** |

That means roughly **one 50-song playlist per project per day**. A second full run the same day will exhaust the quota and fail partway through insertion. Preparing the same list twice is especially expensive: the searches are charged again even though the answers are unchanged.

Quota resets at midnight US Pacific time, which is the day boundary the local ledger uses.

## Quota expectations

`youtube_quota_status` reports **locally observed** usage, not an authoritative Google balance. General units and `search.list` calls are tracked separately because Google applies a separate default daily search-call limit.

`search.list` is billed at its published 100 units, so `general_units` reflects real consumption. Releases before 0.5.0 recorded it as 1 unit and therefore under-reported usage by 99 units per search; a write could be rejected for quota while the reported figure still looked low. Upgrade if you are on an earlier build.

Quota values can change. Implementation and release work must verify the [current official cost table](https://developers.google.com/youtube/v3/determine_quota_cost) instead of treating values in this README as permanent constants.

## Troubleshooting

**A commit reports `status: "partial"` with empty `completed` and everything in `pending`.** The playlist was created but the first insert was rejected — most often the daily quota. Nothing is retried blindly, so no duplicate items are written. Check `youtube_quota_status`, delete the empty playlist, and re-run after the Pacific-time reset. Because a draft is single-use, re-running requires a fresh `youtube_prepare_music_playlist`.

**`login` fails before a browser opens.** No client secret is stored for the profile. Run `setup` first, and confirm you are on the intended `YOUTUBE_MCP_PROFILE`.

**Authorization succeeds, then stops working about a week later.** Google OAuth projects left in Testing status issue refresh tokens that expire after seven days. Publish the consent screen or re-run `login`.

**`403` on a public read.** `YOUTUBE_API_KEY` is missing from the server's environment. It is never persisted, so it must be present in every launch — including the `env` block of the MCP client configuration.

## Authentication model

- Public reads require `YOUTUBE_API_KEY` in the process environment.
- Account reads require OAuth with the `youtube.readonly` scope.
- Playlist creation and comment writes require `youtube.force-ssl` because Google does not provide a playlist-only scope.
- Video upload and lifecycle require `youtube.upload`. It is requested separately, and video tools refuse when a profile was authorized without it. Add it to your consent screen and re-run `login` before uploading.
- The server counteracts that broad Google scope with a strict endpoint allowlist: only playlist and playlist-item write endpoints are callable.
- Installed applications use Authorization Code + PKCE, a random `state`, and a loopback redirect on `127.0.0.1` with a random port.
- Service accounts are not supported for ordinary YouTube accounts.

Never commit API keys, OAuth client data, access tokens, refresh tokens, local databases, debug logs, or `.env` files.

## Captions and analytics

General public transcript retrieval is not part of v1. The official captions download endpoint is permission-gated and expensive, so unofficial scraping will not be used. Owner-authorized caption management may be considered later.

YouTube Analytics and Reporting APIs are also deferred. They require separate OAuth, data models, and operational behavior and should not complicate the initial playlist-focused server.

## Development

The implemented stack is TypeScript, Node.js 20.17+, ESM, the official MCP TypeScript SDK, Zod validation, direct typed REST calls to approved Google endpoints, SQLite for local quota/draft/journal state, and an OS-keychain adapter for OAuth refresh tokens.

Current checks:

```bash
npm run format:check
npm run lint
npm run typecheck
npm test
npm run build
```

Implementation should follow the phases and acceptance gates in [PLAN.md](PLAN.md). Agent-specific constraints and definitions of done are in [AGENTS.md](AGENTS.md). Claude Code should begin with [CLAUDE.md](CLAUDE.md).

## Project status

- [x] Product and security architecture
- [x] Repository development instructions
- [x] TypeScript package scaffold
- [x] Public read tools
- [x] OAuth and profiles
- [x] Music matching and preview
- [x] Confirmed new-playlist creation
- [x] Previewed playlist update, reorder, removal, and deletion
- [x] Existing-playlist target for music draft commits (0.5)
- [x] Correct `search.list` general-unit accounting in the quota ledger (0.5)
- [x] Comment insert, reply, delete, and moderation (0.6)
- [x] Video upload, metadata update, delete, and thumbnails (0.7)
- [ ] YouTube Analytics reporting (0.8)
- [ ] Cross-client integration tests
- [ ] First npm release

## License

Licensed under the Apache License 2.0. The complete license text is in [LICENSE](LICENSE).

## References

- [Model Context Protocol](https://modelcontextprotocol.io/)
- [YouTube Data API v3](https://developers.google.com/youtube/v3)
- [YouTube Data API quota costs](https://developers.google.com/youtube/v3/determine_quota_cost)
- [Google OAuth for installed apps](https://developers.google.com/identity/protocols/oauth2/native-app)
- [Claude MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp)
- [Codex MCP documentation](https://developers.openai.com/codex/mcp/)