Skip to main content
Glama
granitebps

YouTube MCP Server

by granitebps
README.md
# YouTube MCP

[![CI](https://github.com/granitebps/youtube-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/granitebps/youtube-mcp/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/%40granitebps%2Fyoutube-mcp.svg)](https://www.npmjs.com/package/@granitebps/youtube-mcp)
[![npm downloads](https://img.shields.io/npm/dm/%40granitebps%2Fyoutube-mcp.svg)](https://www.npmjs.com/package/@granitebps/youtube-mcp)
[![MCP Registry](https://img.shields.io/badge/MCP-Registry-5A67D8.svg)](https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.granitebps%2Fyoutube-mcp)
[![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](LICENSE)

YouTube MCP lets an MCP client read YouTube video metadata, caption transcripts, comments, and search results. By default it needs no YouTube API key or account; an optional [official YouTube Data API provider](#providers) serves metadata, comments, and search with an API key.

Supports both **stdio** (local) and **Streamable HTTP** (VPS/remote) transports.

## Access model

This is an independent, unofficial open-source project. It is not affiliated with,
endorsed by, or sponsored by YouTube or Google.

- The default `innertube` provider serves metadata, comments, and search through
  `youtubei.js`, using an unofficial/internal YouTube interface without a YouTube
  Data API key. Unofficial access may change, break, be restricted, or be blocked.
- Select `YT_PROVIDER=api` and supply your own `YOUTUBE_API_KEY` to use the
  official YouTube Data API v3 for those three tools.
- Transcript tools always use `youtube-transcript-plus`, regardless of
  `YT_PROVIDER`. Transcript retrieval remains unofficial and does not use the
  official Data API path or your API key.

Users are responsible for deciding whether an access method suits their use and
for complying with applicable service terms and law. See
[access methods and service terms](https://github.com/granitebps/youtube-mcp/blob/main/docs/access-and-compliance.md)
for the tool/backend mapping, BYOK setup, limitations, and software licenses.

## Features

| Tool | Description |
|------|-------------|
| `search_youtube` | Search videos with filters for upload date and popularity |
| `get_video_info` | Video metadata: title, channel, description, duration, views, likes, upload date, tags |
| `get_video_comments` | Comment threads with author info, likes, and the replies YouTube includes |
| `get_video_transcript` | Transcripts (manual + auto-generated captions) with timestamps |
| `get_transcript_languages` | Lists available manual and auto-generated caption languages |
| `get_server_info` | Server version, active provider, and the backend serving each tool |

## Requirements

- Node.js 22 or newer. CI checks Node 22.21.0 and Node 24.

> **No YouTube API key required by default.** The default `innertube` provider uses `youtubei.js` (an unofficial/internal YouTube interface) for video info, comments, and search. Transcripts always use `youtube-transcript-plus`. Neither needs an API key or authentication. See [Providers](#providers) for the optional official API.

## Quick start

Your MCP client can run the published package without a clone:

```bash
npx -y @granitebps/youtube-mcp
```

The server uses stdio. Keep stdout reserved for MCP traffic.

## Run from a cloned repository

To develop the server or use a clone directly:

```bash
git clone https://github.com/granitebps/youtube-mcp.git
cd youtube-mcp
npm ci
npm run build
```

Point your MCP client at the compiled entry point:

```text
node /absolute/path/to/youtube-mcp/dist/index.js
```

Run `npm run build` after each source change. Do not use `src` or `npm run dev` as the client's stdio command; build output on stdout can corrupt MCP messages.

## Client configuration

Each example starts with the npm package, followed by the local equivalent. Replace `/absolute/path/to/youtube-mcp` with your clone's path. Settings from [Configuration](#configuration) can be added as environment variables.

### Claude

Add the npm package to Claude Code:

```bash
claude mcp add youtube -- npx -y @granitebps/youtube-mcp
```

For a local build:

```bash
claude mcp add youtube -- node /absolute/path/to/youtube-mcp/dist/index.js
```

Claude Code uses local scope by default. Add `--scope user` before `youtube` to make the server available across projects.

Claude Desktop reads the same server from `claude_desktop_config.json` (on macOS, `~/Library/Application Support/Claude/claude_desktop_config.json`). Restart the app after editing the file.

```json
{
  "mcpServers": {
    "youtube": {
      "command": "npx",
      "args": ["-y", "@granitebps/youtube-mcp"]
    }
  }
}
```

For a local build, replace `command` and `args` with:

```json
{
  "command": "node",
  "args": ["/absolute/path/to/youtube-mcp/dist/index.js"]
}
```

### Codex

Add the npm package to `~/.codex/config.toml`, or to `.codex/config.toml` in a trusted project:

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

For a local build:

```toml
[mcp_servers.youtube]
command = "node"
args = ["/absolute/path/to/youtube-mcp/dist/index.js"]
```

Restart Codex after editing the file.

### OpenCode

Add the npm package to `opencode.json` or `opencode.jsonc`:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "youtube": {
      "type": "local",
      "command": ["npx", "-y", "@granitebps/youtube-mcp"],
      "enabled": true
    }
  }
}
```

For a local build, replace the `command` array with:

```json
{
  "command": ["node", "/absolute/path/to/youtube-mcp/dist/index.js"]
}
```

### Cursor

Add the npm package to `.cursor/mcp.json` in a project, or to `~/.cursor/mcp.json` for global use:

```json
{
  "mcpServers": {
    "youtube": {
      "command": "npx",
      "args": ["-y", "@granitebps/youtube-mcp"]
    }
  }
}
```

For a local build, replace `command` and `args` with:

```json
{
  "command": "node",
  "args": ["/absolute/path/to/youtube-mcp/dist/index.js"]
}
```

### GitHub Copilot (VS Code)

Add to your VS Code `settings.json`:

```json
{
  "mcp": {
    "servers": {
      "youtube": {
        "command": "npx",
        "args": ["-y", "@granitebps/youtube-mcp"]
      }
    }
  }
}
```

---

## VPS deployment (Streamable HTTP)

For remote deployment, the server runs as a persistent HTTP service using the **Streamable HTTP** transport (the current MCP standard, replacing the deprecated SSE transport).

### 1. Deploy to your VPS

```bash
# On your VPS
git clone https://github.com/granitebps/youtube-mcp.git
cd youtube-mcp
npm ci
npm run build

# Create .env (optional, for HTTP mode)
cp .env.example .env
# Uncomment TRANSPORT=http, PORT, HOST as needed
```

YouTube often blocks datacenter IPs. If `get_video_info` returns `ACCESS_BLOCKED` with "Sign in to confirm you're not a bot", set `YT_PROVIDER=api` and `YOUTUBE_API_KEY` to serve metadata, comments, and search through the official API. See [Providers](#providers).

### 2. Run with HTTP transport

```bash
# Using --http flag
node dist/index.js --http

# Or using environment variable
TRANSPORT=http PORT=3000 node dist/index.js

# Or using npm script
npm run start:http
```

The server will listen on `http://0.0.0.0:3000/mcp`.

### 3. Set up Nginx reverse proxy with TLS

```nginx
server {
    listen 443 ssl;
    server_name mcp.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;

    location /mcp {
        proxy_pass http://127.0.0.1:3000/mcp;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Required for SSE streaming
        proxy_set_header Connection '';
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;
    }
}
```

Get a free TLS certificate:
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d mcp.yourdomain.com
```

### 4. Keep it running with systemd

Create `/etc/systemd/system/youtube-mcp.service`:

```ini
[Unit]
Description=YouTube MCP Server
After=network.target

[Service]
Type=simple
User=your_user
WorkingDirectory=/path/to/youtube-mcp
ExecStart=/usr/bin/node dist/index.js --http
Restart=always
RestartSec=5
Environment=TRANSPORT=http
Environment=PORT=3000
Environment=HOST=127.0.0.1

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable youtube-mcp
sudo systemctl start youtube-mcp
sudo systemctl status youtube-mcp
```

### 5. Connect MCP clients to your VPS

#### GitHub Copilot (VS Code) — Remote

```json
{
  "mcp": {
    "servers": {
      "youtube": {
        "type": "http",
        "url": "https://mcp.yourdomain.com/mcp"
      }
    }
  }
}
```

#### Claude Desktop — Remote

```json
{
  "mcpServers": {
    "youtube": {
      "type": "streamable-http",
      "url": "https://mcp.yourdomain.com/mcp"
    }
  }
}
```

---

## Usage Examples

Once connected, you can ask your AI agent things like:

- *"Get info about this YouTube video: https://www.youtube.com/watch?v=dQw4w9WgXcQ"*
- *"Show me the top comments on video ID abc123"*
- *"Get the transcript of this video in English"*
- *"Summarize the transcript of https://youtu.be/xyz789"*
- *"Search for Node.js tutorials uploaded this week, sorted by views"*
- *"Find the most popular React videos from the last month"*

## Tool Response Format

All tools return:

- `content`: short human-readable text for chat-style MCP clients
- `structuredContent`: JSON-shaped data for agents that need reliable fields

The server is intentionally **JSON-first, text-second**. Agents should prefer `structuredContent` when they need to filter, transform, or chain tool results.

Example shape from `get_video_info`:

```json
{
  "content": [
    {
      "type": "text",
      "text": "📹 Example title\n\nChannel: Example channel\n..."
    }
  ],
  "structuredContent": {
    "videoId": "dQw4w9WgXcQ",
    "title": "Example title",
    "description": "Example description",
    "channelName": "Example channel",
    "channelId": "UC123",
    "uploadedAt": "Sep 14, 2017",
    "duration": "3m 33s",
    "durationSeconds": 213,
    "viewCount": "123456",
    "likeCount": "7890",
    "commentCount": null,
    "tags": ["music", "pop"],
    "thumbnailUrl": "https://..."
  }
}
```

Values YouTube did not provide are `null`, never placeholders such as `"0"`, `"N/A"` or `"Unknown"`.

### Errors

Failures return `isError: true`. The text starts with a code (`❌ [ACCESS_BLOCKED] ...`) and `_meta["youtube-mcp/error"]` carries the details:

```json
{
  "isError": true,
  "content": [{ "type": "text", "text": "❌ [ACCESS_BLOCKED] YouTube requires sign-in to access video C7OQHIpDlvA: Sign in to confirm you’re not a bot" }],
  "_meta": {
    "youtube-mcp/error": {
      "code": "ACCESS_BLOCKED",
      "message": "...",
      "retryable": false,
      "upstream": { "playabilityStatus": "LOGIN_REQUIRED", "reason": "Sign in to confirm you’re not a bot", "accessKind": "login_required" }
    }
  }
}
```

The error is in `_meta`, not `structuredContent`, because MCP SDK clients validate any `structuredContent` against the tool's output schema, even on error results.

| Code | Meaning |
|------|---------|
| `INVALID_INPUT` | Not a YouTube URL or 11-character video ID, or an invalid language code |
| `ACCESS_BLOCKED` | YouTube refused this client: HTTP 429/403, CAPTCHA, consent page, sign-in or bot check. `upstream.accessKind` is `rate_limited`, `forbidden`, `login_required`, `consent_required`, or `quota_exceeded` (official API quota used up). Says nothing about whether the video exists or has captions |
| `VIDEO_UNAVAILABLE` | YouTube reports the video as `ERROR`, `UNPLAYABLE`, private or offline. `upstream.reason` is YouTube's own text |
| `CAPTIONS_UNAVAILABLE` / `LANGUAGE_UNAVAILABLE` | Used internally; transcript tools return these as `available: false` results, not errors |
| `COMMENTS_UNAVAILABLE` | Comments are disabled or YouTube returned no comments section |
| `PARSE_ERROR` | YouTube answered but the response could not be interpreted (layout change, wrong video ID returned) |
| `LIMIT_EXCEEDED` | A configured size limit was hit |
| `TIMEOUT` | A configured deadline expired; the underlying requests were aborted |
| `CANCELLED` | The MCP client cancelled the request, or the server is shutting down |
| `UPSTREAM_ERROR` | Other HTTP or network failure (`retryable: true`) |

The server never retries on its own.

## Providers

`YT_PROVIDER` selects the backend for `get_video_info`, `get_video_comments`, and `search_youtube`. `get_video_transcript` and `get_transcript_languages` always use `youtube-transcript-plus`, whichever provider is active. `get_server_info` reports the active provider.

| Provider | Select with | Needs | Notes |
|----------|-------------|-------|-------|
| InnerTube | Default, or `YT_PROVIDER=innertube` | Nothing | Uses an unofficial/internal interface through `youtubei.js`, with no Data API key. YouTube may restrict or block access, including from datacenter IPs (VPS, cloud) |
| Official API | `YT_PROVIDER=api` | `YOUTUBE_API_KEY` | Uses the [YouTube Data API v3](https://developers.google.com/youtube/v3). Subject to your Google Cloud project's quota, key restrictions, and applicable service terms |

This server does not use the Data API for captions. Google's [caption-download endpoint](https://developers.google.com/youtube/v3/docs/captions/download) requires OAuth authorization and permission to edit the video; an API key alone is insufficient. Transcript tools use the unofficial `youtube-transcript-plus` path and can return `ACCESS_BLOCKED` with either provider.

### Using the official API

Bring your own key (BYOK). The key belongs to you; the project does not distribute or host a shared YouTube API key. Supply these settings through the process or MCP client environment; the server does not automatically load `.env` files.

1. In the [Google Cloud console](https://console.cloud.google.com/), create a project, enable **YouTube Data API v3**, and create an API key. Restricting the key to that API is recommended.
2. Start the server with:

```bash
YT_PROVIDER=api YOUTUBE_API_KEY=your_api_key npx -y @granitebps/youtube-mcp
```

Quota ([costs](https://developers.google.com/youtube/v3/determine_quota_cost)): the default allocation is 10,000 units per day, plus a separate allowance of 100 `search.list` calls per day.

| Tool | API calls |
|------|-----------|
| `get_video_info` | `videos.list`: 1 unit |
| `get_video_comments` | `commentThreads.list`: 1 unit per page. `page: N` walks the N-1 pages before it, so it costs N units |
| `search_youtube` | `search.list` (search allowance) plus one `videos.list` for views and durations: 1 unit |

Results are cached as with the default provider (video info 30 min, comments 10 min, search 15 min), so repeated calls cost nothing. A used-up quota returns `ACCESS_BLOCKED` with `upstream.accessKind: "quota_exceeded"`; an invalid key or disabled API returns `ACCESS_BLOCKED` with `accessKind: "forbidden"`.

Differences from the default provider:

- `uploadedAt` (`get_video_info`) and `publishedAt` (comments, search) are ISO 8601 timestamps instead of display text such as "Sep 14, 2017" or "2 days ago"
- Search `viewCount` is a plain number such as `"1234"` instead of "1.2K views", and `duration` uses the same format as `get_video_info` (`"3m 5s"`)
- `search_youtube` applies all `sortBy` and `uploadDate` values exactly and never returns `warnings`. `videoDuration: "short"` means under 4 minutes and `medium` 4 to 20 minutes (YouTube's API definitions)
- `get_video_comments` returns only the replies the API includes with each thread (a subset); `replyCount` is the full count

## Configuration

All settings are optional environment variables, except `YOUTUBE_API_KEY` when `YT_PROVIDER=api`. Invalid values stop the server at startup with a message on stderr.

| Variable | Default | Effect |
|----------|---------|--------|
| `YT_PROVIDER` | `innertube` | Provider for video info, comments, and search: `innertube` or `api`. See [Providers](#providers) |
| `YOUTUBE_API_KEY` | none | Your own YouTube Data API v3 key. Required when `YT_PROVIDER=api`, ignored otherwise |
| `YT_INIT_TIMEOUT_MS` | `15000` | Deadline for creating the shared YouTube client |
| `YT_REQUEST_TIMEOUT_MS` | `20000` | Deadline for one tool call's upstream work, including waiting for client init |
| `YT_TRANSCRIPT_MAX_BYTES` | `5000000` | Maximum bytes read from each response fetched for captions |
| `YT_TRANSCRIPT_MAX_SEGMENTS` | `50000` | Maximum caption segments processed from one track |
| `YT_TRANSCRIPT_PAGE_MAX_SEGMENTS` | `500` | Maximum segments returned in one `get_video_transcript` response |

When a deadline expires or the client cancels, the in-flight HTTP requests are aborted, not just abandoned.

## Tool Details

### `search_youtube`
- **Input**: `query` (search text), `maxResults` (1-50, default 10), `sortBy` (`relevance` | `date` | `viewCount` | `rating`), `uploadDate` (`any` | `hour` | `today` | `week` | `month` | `year`), `videoDuration` (`any` | `short` | `medium` | `long`)
- **Returns**: Human-readable summary in `content` plus structured JSON results in `structuredContent`
- **Note**: With the default `innertube` provider, YouTube search only orders by relevance or popularity and only filters uploads from `today` up. `sortBy: "date"` keeps relevance order, `sortBy: "rating"` sorts by popularity, and `uploadDate: "hour"` is applied as `today`. Each case adds an entry to the optional `warnings` array (`code`, `message`). The `api` provider applies every option exactly

### `get_video_info`
- **Input**: `video` (YouTube URL or video ID)
- **Returns**: Human-readable summary in `content` plus structured JSON metadata in `structuredContent`, including `durationSeconds`. Metadata is returned even when YouTube refuses playback, as long as it provides the video details

### `get_video_comments`
- **Input**: `video` (URL or ID), `maxResults` (1-20, default 20), `sortBy` (`relevance` or `time`), `page` (default 1)
- **Returns**: Human-readable summary in `content` plus structured JSON comment threads, pagination fields, and `hasMore` in `structuredContent`
- **Note**: `replies` contains only replies YouTube includes with the thread; `replyCount` is the full count. Fetching the rest would cost one request per thread

### `get_video_transcript`
- **Input**: `video` (URL or ID), `lang` (optional language code; omitted means the first manual track, or the first auto-generated one if there is none), `maxSegments` (default `0`: as many as `YT_TRANSCRIPT_PAGE_MAX_SEGMENTS` allows), `startSegment` (default `0`)
- **Returns**: Human-readable transcript in `content` plus structured JSON segments, plain text, and pagination metadata in `structuredContent`. Continue with `startSegment = nextStartSegment` and the returned `languageCode` as `lang` while `hasMore` is true. `isAutoGenerated` tells whether the track is YouTube's automatic captions
- **No captions**: `available: false` with `reason: "NO_CAPTIONS"`, or `reason: "LANGUAGE_UNAVAILABLE"` plus `availableLanguages`. Access problems are errors, never `available: false`
- **Note**: When a language has both manual and automatic tracks, the manual track is returned
- **Note**: Does NOT require an API key — works via YouTube's internal caption system. A transcript is caption text only; it is not an analysis of the video's picture

### `get_transcript_languages`
- **Input**: `video` (YouTube URL or video ID)
- **Returns**: Human-readable language list in `content` plus structured JSON language metadata in `structuredContent`. `available: false` means the video is playable and has no caption tracks; access problems are errors

### `get_server_info`
- **Input**: none
- **Returns**: `name`, `version`, `provider` (`innertube` or `api`), `authenticated`, `exactSearchFilters`, and `tools`, a map from each tool to the backend serving it

## Development

```bash
npm test        # builds, then runs the offline test suite (no network access)
npm run check   # tests plus release metadata, package contents, and tarball install checks
```

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

## Release verification

The automated suite covers input parsing, configuration, error classification, cancellation, the compiled stdio entry point, release metadata, and installation from an npm tarball. It never calls YouTube. Live YouTube checks are manual and separate from CI.

Maintainers follow the [release guide](RELEASE.md) for the approved-tag npm, MCP Registry, and GitHub release process.

## Rate Limits

The default `innertube` provider and transcript tools use unofficial YouTube interfaces. They do not use the Data API quota system, but this does not imply unrestricted access:
- Heavy automated usage may trigger CAPTCHAs or temporary blocks
- Requests from datacenter IPs (VPS, cloud) are often blocked outright
- Request pacing does not guarantee access or establish permission under service terms

The `api` provider is limited by your Google Cloud project's YouTube Data API quota instead. See [Providers](#providers).

## License

[ISC](LICENSE). The software license is separate from YouTube/Google service terms and rights in retrieved content. See the [access guide](https://github.com/granitebps/youtube-mcp/blob/main/docs/access-and-compliance.md#software-licenses-and-service-terms).

TDQS

A4.1/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a clearly distinct resource and action: video metadata, comments, transcript content, language discovery, search, and server info. There is no overlap in purpose, and the transcript/language split is explicitly signposted (call get_transcript_languages first).

Naming Consistency4/5

Most tools follow a get_<resource> pattern (get_video_info, get_video_comments, get_video_transcript, get_server_info), which is highly readable. Minor deviations exist: search_youtube uses a different verb form and get_transcript_languages breaks the get_video_ prefix used by its sibling transcript tool.

Tool Count5/5

Six tools is well-scoped for a read-oriented YouTube data server, with each tool earning its place and no redundant operations. It sits comfortably in the ideal 3-15 range.

Completeness4/5

The surface covers the core read workflows well: video metadata, comments, transcripts, language discovery, and search. Gaps exist around channel-level and playlist operations, but these are workable omissions for a video-centric read-only server.

Maintenance

ActivityMaintained
ResponsivenessNo issues