Independent, unofficial project for YouTube metadata, transcripts, comments, and search.
YouTube MCP lets an MCP client read YouTube video metadata, caption transcripts, comments, and search results. It needs no YouTube API key or account.
Supports both stdio (local) and Streamable HTTP (VPS/remote) transports.
| 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 |
No YouTube API key required. This server uses
youtubei.js(YouTube's InnerTube API) for video info, comments, and search, andyoutube-transcript-plusfor transcripts. Neither needs an API key or authentication.
Your MCP client can run the published package without a clone:
npx -y @granitebps/youtube-mcp
The server uses stdio. Keep stdout reserved for MCP traffic.
To develop the server or use a clone directly:
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:
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.
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 can be added as environment variables.
Add the npm package to Claude Code:
claude mcp add youtube -- npx -y @granitebps/youtube-mcp
For a local build:
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.
{
"mcpServers": {
"youtube": {
"command": "npx",
"args": ["-y", "@granitebps/youtube-mcp"]
}
}
}
For a local build, replace command and args with:
{
"command": "node",
"args": ["/absolute/path/to/youtube-mcp/dist/index.js"]
}
Add the npm package to ~/.codex/config.toml, or to .codex/config.toml in a trusted project:
[mcp_servers.youtube]
command = "npx"
args = ["-y", "@granitebps/youtube-mcp"]
For a local build:
[mcp_servers.youtube]
command = "node"
args = ["/absolute/path/to/youtube-mcp/dist/index.js"]
Restart Codex after editing the file.
Add the npm package to opencode.json or opencode.jsonc:
{
"$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:
{
"command": ["node", "/absolute/path/to/youtube-mcp/dist/index.js"]
}
Add the npm package to .cursor/mcp.json in a project, or to ~/.cursor/mcp.json for global use:
{
"mcpServers": {
"youtube": {
"command": "npx",
"args": ["-y", "@granitebps/youtube-mcp"]
}
}
}
For a local build, replace command and args with:
{
"command": "node",
"args": ["/absolute/path/to/youtube-mcp/dist/index.js"]
}
Add to your VS Code settings.json:
{
"mcp": {
"servers": {
"youtube": {
"command": "npx",
"args": ["-y", "@granitebps/youtube-mcp"]
}
}
}
}
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).
# 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
# 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.
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:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d mcp.yourdomain.com
Create /etc/systemd/system/youtube-mcp.service:
[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
sudo systemctl daemon-reload
sudo systemctl enable youtube-mcp
sudo systemctl start youtube-mcp
sudo systemctl status youtube-mcp
{
"mcp": {
"servers": {
"youtube": {
"type": "http",
"url": "https://mcp.yourdomain.com/mcp"
}
}
}
}
{
"mcpServers": {
"youtube": {
"type": "streamable-http",
"url": "https://mcp.yourdomain.com/mcp"
}
}
}
Once connected, you can ask your AI agent things like:
All tools return:
content: short human-readable text for chat-style MCP clientsstructuredContent: JSON-shaped data for agents that need reliable fieldsThe 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:
{
"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".
Failures return isError: true. The text starts with a code (❌ [ACCESS_BLOCKED] ...) and _meta["youtube-mcp/error"] carries the details:
{
"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 or consent_required. 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.
All settings are optional environment variables. Invalid values stop the server at startup with a message on stderr.
| Variable | Default | Effect |
|---|---|---|
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.
search_youtubequery (search text), maxResults (1-50, default 10), sortBy (relevance | date | viewCount | rating), uploadDate (any | hour | today | week | month | year), videoDuration (any | short | medium | long)content plus structured JSON results in structuredContenttoday 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)get_video_infovideo (YouTube URL or video ID)content plus structured JSON metadata in structuredContent, including durationSeconds. Metadata is returned even when YouTube refuses playback, as long as it provides the video detailsget_video_commentsvideo (URL or ID), maxResults (1-20, default 20), sortBy (relevance or time), page (default 1)content plus structured JSON comment threads, pagination fields, and hasMore in structuredContentreplies contains only replies YouTube includes with the thread; replyCount is the full count. Fetching the rest would cost one request per threadget_video_transcriptvideo (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)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 captionsavailable: false with reason: "NO_CAPTIONS", or reason: "LANGUAGE_UNAVAILABLE" plus availableLanguages. Access problems are errors, never available: falseget_transcript_languagesvideo (YouTube URL or video ID)content plus structured JSON language metadata in structuredContent. available: false means the video is playable and has no caption tracks; access problems are errorsnpm 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.
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 for the approved-tag npm, MCP Registry, and GitHub release process.
This server uses YouTube's InnerTube API (the same API used by youtube.com). There are no official API quotas, but:
ISC. This project is unofficial and is not affiliated with YouTube or Google.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @granitebps/youtube-mcpMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-granitebps-youtube-mcp": {
"command": "npx",
"args": [
"-y",
"@granitebps/youtube-mcp"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup reference@granitebps/youtube-mcpnpmYouTube MCP works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.