youtube-music-mcp
by nazaryanenko
README.md
# YouTube Playlist Generator MCP Server
A Model Context Protocol (MCP) server that enables AI applications to search for YouTube music videos and manage playlists using the official YouTube Data API v3.
## Features
- **Search Music Videos**: Find music videos on YouTube with customizable search parameters
- **Get Video Details**: Retrieve comprehensive information about specific videos
- **Get Playlist Items**: List videos from public playlists
- **Playlist Management**: Complete playlist management including creating, editing, and managing playlists (requires OAuth setup)
## Setup
### Prerequisites
- Node.js 18 or higher
- A Google Cloud Project with YouTube Data API v3 enabled
- YouTube Data API key
- OAuth 2.0 Client credentials (optional, required for playlist management features)
### Installation
1. Clone this repository:
```bash
git clone https://github.com/nazaryanenko/youtube-music-mcp
```
2. Install dependencies:
```bash
npm install
```
3. Get a YouTube Data API key:
- Go to the [Google Cloud Console](https://console.cloud.google.com/)
- Create a new project or select an existing one
- Enable the YouTube Data API v3
- Create credentials (API key)
- Copy the API key
4. Set up environment variables:
```bash
# Create a .env file
echo "YOUTUBE_API_KEY=your_api_key_here" > .env
echo "YOUTUBE_CLIENT_ID=your_client_id_here" >> .env
echo "YOUTUBE_CLIENT_SECRET=your_client_secret_here" >> .env
```
5. Build the TypeScript code:
```bash
npm run build
```
### Running the Server
For development with auto-rebuild:
```bash
npm run dev
```
For production:
```bash
npm start
```
### Using with MCP Clients
This server uses the stdio transport, so it can be used with any MCP client that supports stdio.
#### Claude Desktop Configuration
Add to your Claude Desktop configuration file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"youtube-playlist-generator": {
"command": "node",
"args": ["/absolute/path/to/yt-playlist-generator/build/index.js"],
"env": {
"YOUTUBE_API_KEY": "your_youtube_api_key_here",
"YOUTUBE_CLIENT_ID": "your_client_id_here",
"YOUTUBE_CLIENT_SECRET": "your_client_secret_here"
}
}
}
}
```
#### Visual Studio Code with GitHub Copilot Configuration
To use this MCP server with GitHub Copilot in Visual Studio Code, you need to configure it in your VS Code settings:
1. **Open VS Code Settings**: Press `Ctrl+,` (Windows/Linux) or `Cmd+,` (macOS)
2. **Search for MCP**: Type "mcp" in the search bar
3. **Add MCP Server Configuration**: Add the following to your VS Code settings JSON:
```json
{
"github.copilot.chat.mcp.servers": {
"youtube-playlist-generator": {
"command": "node",
"args": ["/absolute/path/to/yt-playlist-generator/build/index.js"],
"env": {
"YOUTUBE_API_KEY": "your_youtube_api_key_here",
"YOUTUBE_CLIENT_ID": "your_client_id_here",
"YOUTUBE_CLIENT_SECRET": "your_client_secret_here"
}
}
}
}
```
**Alternative: Use Settings UI**
1. Go to **File > Preferences > Settings** (or use `Ctrl+,`)
2. Search for "GitHub Copilot MCP"
3. Click "Edit in settings.json" next to "Github › Copilot › Chat: Mcp Servers"
4. Add the server configuration as shown above
**Environment Variables Setup**
For security, you can also set environment variables system-wide instead of in the config:
**Windows (PowerShell)**:
```powershell
[Environment]::SetEnvironmentVariable("YOUTUBE_API_KEY", "your_api_key_here", "User")
[Environment]::SetEnvironmentVariable("YOUTUBE_CLIENT_ID", "your_client_id_here", "User")
[Environment]::SetEnvironmentVariable("YOUTUBE_CLIENT_SECRET", "your_client_secret_here", "User")
```
**Windows (Command Prompt)**:
```cmd
setx YOUTUBE_API_KEY "your_api_key_here"
setx YOUTUBE_CLIENT_ID "your_client_id_here"
setx YOUTUBE_CLIENT_SECRET "your_client_secret_here"
```
**macOS/Linux**:
```bash
export YOUTUBE_API_KEY="your_api_key_here"
export YOUTUBE_CLIENT_ID="your_client_id_here"
export YOUTUBE_CLIENT_SECRET="your_client_secret_here"
# Add to ~/.bashrc, ~/.zshrc, or ~/.profile to persist
echo 'export YOUTUBE_API_KEY="your_api_key_here"' >> ~/.bashrc
echo 'export YOUTUBE_CLIENT_ID="your_client_id_here"' >> ~/.bashrc
echo 'export YOUTUBE_CLIENT_SECRET="your_client_secret_here"' >> ~/.bashrc
```
Then remove the `env` section from the VS Code configuration:
```json
{
"github.copilot.chat.mcp.servers": {
"youtube-playlist-generator": {
"command": "node",
"args": ["/absolute/path/to/yt-playlist-generator/build/index.js"]
}
}
}
```
**Usage in VS Code**
Once configured, you can use the MCP server through GitHub Copilot Chat:
1. Open Copilot Chat (`Ctrl+Alt+I` or click the chat icon)
2. Use natural language to interact with YouTube:
- "Search for music videos about jazz"
- "Create a new playlist called 'My Favorites'"
- "Add this video to my playlist"
- "Show me details about this YouTube video"
## Available Tools
### Read-Only Tools (API Key Authentication)
#### search_music_videos
Search YouTube for music videos with specified criteria.
**Parameters:**
- `query` (string): Search query for music videos
- `maxResults` (number, 1-50, default: 10): Maximum number of results
- `order` (enum): Sort order - "relevance", "date", "rating", "viewCount", "title"
#### get_video_details
Get detailed information about a specific YouTube video.
**Parameters:**
- `videoId` (string): YouTube video ID
#### get_playlist_items
Retrieve videos from a public YouTube playlist.
**Parameters:**
- `playlistId` (string): YouTube playlist ID
- `maxResults` (number, 1-50, default: 25): Maximum number of items
### OAuth-Required Tools (Playlist Management)
#### authenticate_youtube
Authenticate with YouTube OAuth to enable playlist management features.
**Parameters:**
- `getAuthUrl` (boolean, default: false): Set to true to get the authorization URL
- `authCode` (string, optional): Authorization code from OAuth flow
#### get_auth_status
Check current OAuth authentication status.
**Parameters:** None
#### create_playlist
Create a new YouTube playlist (requires OAuth authentication).
**Parameters:**
- `title` (string): Playlist title
- `description` (string, optional): Playlist description
- `privacy` (enum): "private", "public", or "unlisted"
#### edit_playlist
Edit existing playlist information (requires OAuth authentication).
**Parameters:**
- `playlistId` (string): ID of the playlist to edit
- `title` (string, optional): New title for the playlist
- `description` (string, optional): New description for the playlist
- `privacy` (enum, optional): New privacy setting - "private", "public", or "unlisted"
**Note:** At least one field (title, description, or privacy) must be provided to update.
#### add_to_playlist
Add a video to an existing playlist (requires OAuth authentication).
**Parameters:**
- `playlistId` (string): Target playlist ID
- `videoId` (string): Video ID to add
#### list_playlists
List user's playlists (requires OAuth authentication).
**Parameters:**
- `maxResults` (number, 1-50, default: 25): Maximum number of playlists
- `mine` (boolean, default: true): List authenticated user's playlists
#### remove_from_playlist
Remove a video from a playlist (requires OAuth authentication).
**Parameters:**
- `playlistItemId` (string): ID of the playlist item to remove (get from get_playlist_items)
### Channel Subscription Tools (OAuth)
All subscription tools work under the existing `https://www.googleapis.com/auth/youtube` scope — no re-consent required. Every response includes a `_quotaCost` field; the running session total is also logged to stderr.
#### subscribe_to_channel
Subscribe the authenticated user to a channel. Idempotent: an already-subscribed channel returns `{ alreadySubscribed: true, subscriptionId, channelTitle }` instead of erroring.
**Parameters:**
- `channel` (string): channelId (UC...), `@handle`, channel URL, or channel name.
**Quota:** ~51 units on a new subscribe (50 insert + 1 duplicate lookup), 1–2 units when already subscribed.
#### unsubscribe_from_channel
Unsubscribe the authenticated user from a channel. If not subscribed, returns `{ success: false, reason: "not_subscribed" }` rather than throwing.
**Parameters:**
- `subscriptionId` (string, optional): preferred — skips the lookup.
- `channelId` (string, optional): used when subscriptionId is unknown.
At least one of the two must be supplied.
**Quota:** 50 units when subscriptionId is passed, ~51 units when only channelId is passed.
#### list_subscriptions
List the authenticated user's channel subscriptions. When called without `pageToken`, transparently auto-paginates up to 10 pages (500 subscriptions) and returns the merged list; pass an explicit `pageToken` to fetch a single page and drive pagination yourself.
**Parameters:**
- `maxResults` (number, 1–50, default: 50): page size.
- `pageToken` (string, optional): if supplied, only one page is returned.
- `order` (enum): `"alphabetical"` (default) | `"relevance"` | `"unread"`.
**Quota:** 1 unit per page.
#### get_channel_latest_videos
Get the latest uploads from a channel. Uses the uploads playlist instead of `search.list` — 1–2 units versus 100.
**Parameters:**
- `channel` (string): channelId (UC...), `@handle`, or channel name.
- `maxResults` (number, 1–50, default: 10).
- `publishedAfter` (ISO 8601 string, optional): client-side filter on publish date.
- `excludeShorts` (boolean, default: false): filter out videos shorter than 60 seconds. Adds 1 quota unit per 50 fetched.
**Quota:** ~2 units baseline (channels.list + playlistItems.list), +1 per additional 50 items when `excludeShorts` is on.
### Watch History Tools (Local Takeout Import)
The YouTube Data API stopped exposing watch history in August 2016 — the `relatedPlaylists.watchHistory` field was removed and the legacy `HL` playlist ID returns no items. Instead of pretending otherwise, this server imports data from a **Google Takeout** export into a local SQLite database and answers queries against that.
#### Getting a Takeout export
1. Go to [takeout.google.com](https://takeout.google.com/).
2. **Deselect all**, then select **YouTube and YouTube Music**.
3. Click **All YouTube data included** → uncheck everything except **history**.
4. Click **Multiple formats** → set **history** to **JSON** (default is HTML — the server refuses HTML with a clear error, since HTML parsing would be fragile).
5. Export. When ready, download the archive and locate `Takeout/YouTube and YouTube Music/history/watch-history.json`.
The DB file lives at `~/.yt-playlist-mcp-history.db` by default (override with `YOUTUBE_HISTORY_DB_PATH`).
#### check_history_api_availability
Runtime probe to prove the Data API still lacks watch-history access. Runs three checks and returns their raw evidence — never hard-codes the verdict. If Google ever restores access, `apiAvailable: true` will bubble up automatically.
**Parameters:** None.
Returns `{ apiAvailable, checkedAt, evidence[], conclusion, alternative }`. Evidence entries include `step`, `request`, `statusCode`, `result`.
#### import_watch_history
Streams the Takeout JSON file (safe for exports >100 MB) into SQLite. Deduplicates by `(videoId, watchedAt)`, so re-importing is idempotent — running it again after a fresh Takeout adds new events without touching existing rows.
**Parameters:**
- `filePath` (string): absolute or `~`-expandable path to `watch-history.json`.
Skips ad-driven watches (`details[].name` starting with `"From Google Ads"`) and marks deleted/removed videos separately. Returns:
```json
{
"success": true,
"imported": 12345,
"adsExcluded": 87,
"deletedVideos": 42,
"skippedDuplicates": 0,
"dateRange": { "from": "…", "to": "…" },
"dbPath": "…",
"filePath": "…"
}
```
On a repeat import of the same or an overlapping file, `skippedDuplicates` reports how many events were already in the DB (deduplicated by `(videoId, watchedAt)`).
**Errors** — distinct codes so the caller can give the right advice:
| `error` | Meaning |
| --- | --- |
| `file_not_found` | Path echoed back as `filePath` so you can see how `~` expanded. |
| `html_not_json` | Takeout gave HTML instead of JSON — re-export with format = JSON. |
| `unexpected_extension` | Not `.html`, not `.json` — probably wrong file. |
| `parse_error` | Broken JSON. Includes `bytePosition` when stream-json can locate the failure. |
| `db_locked` | SQLite reports `SQLITE_BUSY`/`SQLITE_LOCKED` — another process holds the DB. |
#### query_watch_history
Aggregate queries over the imported history.
**Parameters:**
- `groupBy` (enum): `"channel"` (default) | `"month"` | `"hour_of_day"` | `"weekday"`.
- `channel` (string, optional): exact-match channel filter.
- `from`, `to` (ISO 8601 strings, optional): inclusive/exclusive date bounds.
- `limit` (number, 1–1000, default 30).
- `timezone` (string, IANA, default `"Europe/Kyiv"`): governs `hour_of_day` / `weekday` bucketing; date arithmetic honours DST.
Rows marked `deleted` are excluded from aggregates. Returns `{ groupBy, timezone, totalEventsMatched, filters, groups[] }`.
#### watch_history_status
Fast probe: does the DB exist, how many events, source split (`youtube` vs `youtube_music`), date range, per-file import log. Use this before deciding whether to re-run `import_watch_history`.
**Parameters:** None.
### Watch-history analytics (pre-aggregated for LLM context)
All five analytics tools return a compact envelope — **≤ 50 KB per response** — with heavy trimming when needed. Raw events are only returned by `sample_watch_events`. Every tool accepts the shared filter set:
- `from` / `to` (ISO 8601): date-range bounds.
- `source`: `"youtube"` | `"youtube_music"` | `"all"` (default).
- `timezone` (IANA, default `"Europe/Kyiv"`): governs hour-of-day, weekday, month buckets.
Every response is wrapped as:
```json
{
"meta": {
"dbEvents": 12345,
"dateRange": { "from": "2020-01-...", "to": "2026-07-..." },
"filtered": 8901,
"timezone": "Europe/Kyiv",
"truncated": false
},
"data": { /* tool-specific */ }
}
```
#### get_watch_overview
One-call snapshot. Returns totals, source split, deleted / ad-excluded counts, month-by-month volume, hour-of-day distribution split into weekday/weekend, and the peak day. Start any exploration here.
#### get_channel_stats
Per-channel breakdown, grouped by **stable channelId** — a channel that renamed still lands in one row.
Fields per row: `channelId` (UC…, `null` for old Takeouts without a `/channel/` URL), `keyType` (`"id"` when grouped by UC, `"name"` fallback for URL-less rows), `channel` (freshest display name — pulled from the row with `MAX(watchedAt)`), `aliases[]` (older display names for the same channelId, if any), `channelUrl`, `watches`, `uniqueVideos`, `firstWatch`, `lastWatch`, `last90d`, `share`.
`sortBy: "watches"` (default) surfaces long-term favorites; `sortBy: "recent"` surfaces channels with the newest activity (fresh obsessions). The client derives "dead interests" (`watches ≥ 20` with old `lastWatch`) locally rather than asking for a separate endpoint.
**Migration note:** the underlying `watch_events.channelId` column is added and backfilled from `channelUrl` the first time the server opens an older DB (log line `[history migration] backfilled channelId for N/M rows`). Rows whose Takeout URL was an `@handle` (no UC) stay `NULL` and fall back to name-based grouping.
#### get_sessions_analysis
Sessions are contiguous runs of watches separated by gaps under `gapMinutes` (default 30). Returns:
- session medians (length in minutes, videos per session)
- `rabbitHoles`: top same-channel streaks of `≥ minStreak` consecutive videos within one session
- `channelCooccurrence`: top 20 channel pairs that show up in the same session, with `sessionsTogether ≥ 3` — raw material for topic clustering, which the client performs itself
All aggregation runs inside SQLite via `LAG()` window functions; the events table is never materialized in JS memory.
#### get_taste_divergence
Compares watching against a live pull of `videos.list?myRating=like` (up to 500 videos, cached in-process for 1 hour). Matching is **by channelId** for both directions — a rename doesn't break the join. Rows without a channelId (old Takeouts) fall back to name matching and are flagged with `keyType: "name-fallback"` (weaker match — draw softer conclusions from those).
Returns:
- `quietDominants`: heavily watched channels you have never liked (habit-consumption signal) — with `channelId`, `keyType`, `channel`, `watches`, `likes: 0`.
- `likedButAbandoned`: channels with likes but no watch activity in the last ~6 months (drifting-away signal) — with `channelId`, `keyType`, `channel`, `likes`, `lastWatch`.
- `likesCachedAt`: ISO timestamp of the current in-process cache — subtract from wall time for age.
Requires OAuth. Costs 1 quota unit per 50 liked videos fetched, once per hour.
#### sample_watch_events
The only tool that returns raw event rows (`title`, `channel`, `watchedAt`, `videoId`), newest-first. Optional exact-match `channel` filter. Hard limit: 100 rows — asking for more returns `error: "limit_too_high"` rather than a silent truncation.
## Quota Tracking
Every subscription tool returns `_quotaCost` — the estimated cost of that single call — and the server logs a running session total to stderr after each API call, e.g.:
```
[quota] +50 for subscriptions.insert (session total: 61 units)
```
The default YouTube Data API daily quota is **10 000 units**. Structured errors are returned for the following conditions:
| Condition | `error` value | Notes |
| --- | --- | --- |
| Daily quota exhausted | `quota_exceeded` | Resets at midnight Pacific Time. |
| Channel disabled subscriptions | `subscription_forbidden` | |
| Already subscribed (on insert) | Handled transparently as `alreadySubscribed: true`. | |
| OAuth token expired/invalid | `auth_expired` | Re-run `authenticate_youtube`. |
| Channel deleted / not found | `not_found` | |
| Not subscribed (on delete) | `success: false, reason: "not_subscribed"` | Not an MCP error. |
## OAuth Authentication Setup
For full playlist management capabilities, you need to set up OAuth 2.0:
### 1. Google Cloud Console Setup
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing one
3. Enable the YouTube Data API v3
4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client ID"
5. Choose "Desktop Application" as application type
6. Download the credentials or copy the Client ID and Client Secret
### 2. Environment Configuration
Add your OAuth credentials to the `.env` file:
```bash
YOUTUBE_API_KEY=your_api_key_here
YOUTUBE_CLIENT_ID=your_client_id_here
YOUTUBE_CLIENT_SECRET=your_client_secret_here
```
### 3. Authentication Flow
1. Use the `authenticate_youtube` tool with `getAuthUrl: true`
2. Open the provided URL in your browser
3. Sign in to your Google account and grant permissions
4. Copy the authorization code
5. Use `authenticate_youtube` tool again with the `authCode` parameter
### 4. Using Authenticated Features
Once authenticated, you can:
- Create new playlists
- Edit existing playlist information (title, description, privacy)
- Add videos to your playlists
- List your private playlists
- Remove videos from playlists
## API Limitations
- **Quota Limits**: YouTube API has daily quota limits (10,000 units/day by default)
- **Rate Limiting**: API calls are subject to rate limiting
- **OAuth Tokens**: Access tokens expire and need refresh (handled automatically)
- **Permissions**: OAuth scope determines available operations
## Development
### Building
```bash
npm run build
```
### stdio transport: never write to stdout
The server uses the MCP stdio transport — **stdout is reserved for JSON-RPC frames**. A single `console.log`, `process.stdout.write`, or a logger that defaults to stdout (pino/winston/etc. without explicit stderr config) will corrupt the message framing and the client will silently drop responses, making every tool appear to hang.
When adding new tools:
- Log to stderr only: `console.error(...)`.
- If you must use a logger, pin its destination stream to `process.stderr`.
- Verify with: `node build/index.js 2>/dev/null | head -c 200` — at idle this MUST print nothing.
### Development Mode
```bash
npm run dev
```
### Project Structure
```
src/
└── index.ts # Main server implementation
build/ # Compiled JavaScript output
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
MIT License - see LICENSE file for details
## Troubleshooting
### Common Issues
1. **"YOUTUBE_API_KEY environment variable is required"**
- Make sure you've set the YOUTUBE_API_KEY environment variable
- Verify the API key is valid and has YouTube Data API access
2. **"Quota exceeded" errors**
- You've hit the daily API quota limit
- Wait for quota reset or request quota increase
3. **"Playlist not found" errors**
- Ensure the playlist ID is correct
- Verify the playlist is public (private playlists require OAuth)
4. **Build errors**
- Make sure you have Node.js 18+ installed
- Run `npm install` to ensure all dependencies are installed
- Check TypeScript compilation with `npm run build`
## Security Considerations
- Never commit API keys to version control
- Use environment variables for sensitive configuration
- Validate all user inputs
- Implement proper error handling and logging
---
*This project was created using GitHub Copilot*
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues