MCP Server for YouTube APIs
AI-Ready YouTube Tools • Built with Dart • Hybrid Tool Architecture • 26 API Services
A standalone MCP (Model Context Protocol) server for the YouTube Data API v3 and Live Streaming API. Built on top of the yt Dart library, it exposes YouTube operations as MCP tools over stdio transport for AI assistants and LLM integration.
- What's New
- Quick Start
- MCP Host Configuration
- Available Tools
- Usage Examples
- Code Mode Usage
- Configuration
- Development Setup
- Testing with MCP Inspector
- License
This release introduces Hybrid Tool Architecture for optimal AI assistant interaction:
- Complete API coverage — All 90+ tools across 26 YouTube API services fully exposed as MCP tools
- Core tools directly callable — Key read/query tools (
search_list,channels_list,videos_list,subscriptions_list, etc.) are exposed withcodeModeVisible: truefor natural language requests like "what channels do I subscribe to?" - Code mode for complex workflows — All tools accessible via
yt_executefor JavaScript orchestration with parallel execution - Enhanced tool descriptions — Rich, actionable descriptions with usage examples for better LLM understanding
- Tool annotations —
readOnlyHint,destructiveHint,idempotentHint, andopenWorldHintfor precise LLM guidance
How it works:
- Natural language queries: LLM directly calls visible tools (e.g.,
yt_subscriptions_list) - Complex workflows: LLM writes JavaScript code using
external_yt_<tool_name>()functions - Hybrid approach: Mix direct tool calls with code execution in the same session
Example - Direct tool call:
User: "What channels do I subscribe to?"
LLM calls: yt_subscriptions_list(mine: true, maxResults: 10)
Example - Code mode execution:
// Search for channels and get their recent videos in one execution
const channels = await external_yt_channels_list({ part: 'snippet', forUsername: 'example' });
const items = channels.items || [];
if (items.length === 0) throw new Error('Channel not found');
const channelId = items[0].id;
const videos = await external_yt_videos_list({ part: 'snippet', channelId: channelId, maxResults: 5 });
return { channels, videos };Introduced the code mode architecture that v2.3.1 builds upon:
- Progressive tool discovery — LLMs search for needed tools by name/description before execution
- JavaScript code orchestration — LLMs write JavaScript code to call multiple YouTube API tools in a single execution
- Parallel execution support — Use
Promise.all()to run multiple API calls concurrently - Complex logic enabled — Math operations, data transformations, and conditional logic within a single tool call
- 90+ MCP tools across all 26 YouTube API services (full parity achieved in v2.3.2)
- Complete CRUD operations: Create, update, and delete videos, playlists, comments, subscriptions
- Live Streaming: Full broadcast/stream lifecycle management, live chat, cuepoints
- Analytics: Reports, groups, and group items management
- Media Uploads: Video upload, caption management, thumbnail/watermark uploads
- Content Management: Video ratings, abuse reporting, moderation controls
For end-users: Choose between global Dart activation (Option 1) or Homebrew installation (Option 2). Both make the yt-mcp command available on your PATH for use with AI agents like Qoder, Claude Desktop, or VS Code.
For developers: If you need to modify the source code or add new tools, see the Development Setup section below.
# Activate the package globally (makes the 'yt-mcp' command available)
dart pub global activate yt_mcp
# For read-only access (API key):
export YT_API_KEY="your-api-key-here"
# OR for full access (OAuth - file-based credentials):
export YT_CLIENT_SECRETS_FILE="path/to/client_secrets.json"
export YT_ACCESS_TOKENS_FILE="path/to/access_tokens.json"# Tap the Homebrew repository
brew tap cdavis-code/yt
# Install yt_mcp (use hyphenated name: yt-mcp)
brew install yt-mcp
# Verify installation (binary is installed as 'yt-mcp' on your PATH)
which yt-mcp
# Output: /opt/homebrew/bin/yt-mcp (Apple Silicon) or /usr/local/bin/yt-mcp (Intel)
# Configure credentials
export YT_API_KEY="your-api-key-here"
# OR for OAuth:
export YT_CLIENT_SECRETS_FILE="path/to/client_secrets.json"
export YT_ACCESS_TOKENS_FILE="path/to/access_tokens.json"Note: Install and run with yt-mcp (hyphen), which matches the Homebrew formula name.
That's it! Configure your AI agent using one of the MCP Host Configuration examples below, and the agent will launch the server automatically.
These core tools are visible in both standard tool listing and code mode for natural language interactions:
| Tool | Description | Auth |
|---|---|---|
yt_search_list |
Search YouTube for videos, channels, and playlists | API Key or OAuth |
yt_channels_list |
List channels by ID, username, or mine=true | API Key or OAuth |
yt_videos_list |
List videos by ID or get trending videos | API Key or OAuth |
yt_subscriptions_list |
List your YouTube subscriptions (mine=true) | OAuth required |
yt_playlist_items_list |
List items in a playlist | API Key or OAuth |
yt_comment_threads_list |
List comment threads for a video | API Key or OAuth |
yt_videos_insert |
Upload a video to YouTube | OAuth required |
yt_subscriptions_insert |
Subscribe to a channel | OAuth required |
All 90+ YouTube API operations are accessible through the execute tool as external_yt_<tool_name>() async JavaScript functions:
| Group | Key Tools | Description |
|---|---|---|
| Search | search_list |
Search videos, channels, playlists by query |
| Videos | videos_list, videos_insert, videos_update, videos_rate, videos_delete |
Video CRUD, ratings, uploads |
| Channels | channels_list, channels_listMine, channels_update |
Channel details and updates |
| Playlists | playlists_list, playlists_insert, playlists_update, playlists_delete |
Playlist lifecycle management |
| Playlist Items | playlist_items_list, playlist_items_insert, playlist_items_update, playlist_items_delete |
Add/remove videos from playlists |
| Comments | comments_list, comments_insert, comments_update, comments_setModerationStatus, comments_delete |
Full comment management and moderation |
| Comment Threads | comment_threads_list, comment_threads_insert |
Thread-based comment operations |
| Subscriptions | subscriptions_list, subscriptions_insert, subscriptions_delete |
Channel subscription management |
| Live Broadcasts | broadcasts_list, broadcasts_insert, broadcasts_update, broadcasts_transition, broadcasts_bind, broadcasts_delete |
Broadcast lifecycle and transitions |
| Live Streams | live_streams_list, live_streams_insert, live_streams_update, live_streams_delete |
Stream configuration management |
| Live Chat | live_chat_list, live_chat_insert, live_chat_delete |
Live chat message operations |
| Analytics | analytics_query, analytics_groupsList, analytics_groupsInsert, analytics_groupsUpdate, analytics_groupsDelete |
YouTube Analytics reports and groups |
| Captions | captions_list, captions_insert, captions_update, captions_download, captions_delete |
Caption track management |
| Thumbnails | thumbnails_set |
Upload video thumbnails |
| Watermarks | watermarks_set, watermarks_unset |
Channel watermark management |
| Channel Banners | channel_banners_insert |
Upload channel banner images |
| Channel Sections | channel_sections_list, channel_sections_insert, channel_sections_update, channel_sections_delete |
Channel page sections |
| Activities | activities_list |
Channel activity feed |
| Video Categories | video_categories_list |
YouTube video category listings |
| I18n | i18n_languages_list, i18n_regions_list |
Internationalization data |
| Playlist Images | playlist_images_list, playlist_images_insert, playlist_images_update, playlist_images_delete |
Playlist image management |
| Third-Party Links | third_party_links_list, third_party_links_insert, third_party_links_update, third_party_links_delete |
Channel third-party link management |
This section provides practical examples for common YouTube API workflows using yt_mcp in code mode.
Use case: Search for videos and display basic information.
// Search for videos about "Dart programming"
const result = await external_yt_search_list({
q: 'Dart programming',
type: 'video',
maxResults: 5
});
// Extract video information
const videos = result.items || [];
return {
count: videos.length,
videos: videos.map(video => ({
id: video.id?.videoId || 'N/A',
title: video.snippet?.title || 'N/A',
channel: video.snippet?.channelTitle || 'N/A',
publishedAt: video.snippet?.publishedAt || 'N/A'
}))
};Use case: Search for videos, channels, and playlists in parallel.
// Search for different types of content simultaneously
const [videos, channels, playlists] = await Promise.all([
external_yt_search_list({ q: 'JavaScript tutorial', type: 'video', maxResults: 3 }),
external_yt_search_list({ q: 'Google', type: 'channel', maxResults: 3 }),
external_yt_search_list({ q: 'music', type: 'playlist', maxResults: 3 })
]);
return {
videoCount: videos.items?.length || 0,
channelCount: channels.items?.length || 0,
playlistCount: playlists.items?.length || 0,
sampleVideo: videos.items?.[0]?.snippet?.title || 'N/A',
sampleChannel: channels.items?.[0]?.snippet?.title || 'N/A',
samplePlaylist: playlists.items?.[0]?.snippet?.title || 'N/A'
};Use case: Find the most popular videos on a topic.
// Search for popular programming videos
const result = await external_yt_search_list({
q: 'programming',
type: 'video',
order: 'viewCount',
maxResults: 5
});
const videos = result.items || [];
return {
query: 'programming (sorted by view count)',
topVideos: videos.map(v => ({
title: v.snippet?.title || 'N/A',
channel: v.snippet?.channelTitle || 'N/A',
publishedAt: v.snippet?.publishedAt?.split('T')[0] || 'N/A'
}))
};Use case: Find recently uploaded videos on a topic.
// Search for videos uploaded this month
const result = await external_yt_search_list({
q: 'Dart programming',
type: 'video',
order: 'date',
maxResults: 5
});
const videos = result.items || [];
return {
query: 'Dart programming (most recent)',
recentVideos: videos.map(v => ({
title: v.snippet?.title || 'N/A',
channel: v.snippet?.channelTitle || 'N/A',
uploadedAt: v.snippet?.publishedAt?.split('T')[0] || 'N/A'
}))
};Use case: Find channels related to a topic.
// Search for programming channels
const result = await external_yt_search_list({
q: 'programming tutorials',
type: 'channel',
maxResults: 5
});
const channels = result.items || [];
return {
channelCount: channels.length,
channels: channels.map(ch => ({
title: ch.snippet?.title || 'N/A',
description: ch.snippet?.description?.substring(0, 100) || 'N/A',
publishedAt: ch.snippet?.publishedAt?.split('T')[0] || 'N/A'
}))
};Use case: Find playlists for learning a topic.
// Search for learning playlists
const result = await external_yt_search_list({
q: 'learn programming',
type: 'playlist',
maxResults: 5
});
const playlists = result.items || [];
return {
playlistCount: playlists.length,
playlists: playlists.map(pl => ({
title: pl.snippet?.title || 'N/A',
channel: pl.snippet?.channelTitle || 'N/A',
videoCount: pl.snippet?.channelId ? 'available' : 'N/A'
}))
};Use case: Compare different search queries to find the best results.
// Test different search queries
const queries = ['Dart programming', 'Flutter development', 'Mobile app development'];
const results = await Promise.all(
queries.map(q =>
external_yt_search_list({ q, type: 'video', maxResults: 1 })
)
);
return {
comparison: queries.map((q, i) => ({
query: q,
resultCount: results[i].items?.length || 0,
topResult: results[i].items?.[0]?.snippet?.title || 'N/A'
}))
};Use case: Find content specific to a region.
// Search for trending content in a specific region
const result = await external_yt_search_list({
q: 'cooking',
type: 'video',
relevanceLanguage: 'en',
maxResults: 5
});
const videos = result.items || [];
return {
query: 'cooking (English relevance)',
videos: videos.map(v => ({
title: v.snippet?.title || 'N/A',
channel: v.snippet?.channelTitle || 'N/A',
thumbnail: v.snippet?.thumbnails?.default?.url || 'N/A'
}))
};Use case: Access your personal YouTube subscriptions (requires OAuth authentication).
// Get your subscriptions (requires YT_CLIENT_SECRETS_FILE and YT_ACCESS_TOKENS_FILE)
const subscriptions = await external_yt_subscriptions_list({
part: 'snippet',
mine: true,
maxResults: 10
});
const subs = subscriptions.items || [];
return {
totalSubscriptions: subs.length,
subscriptions: subs.map(sub => ({
channelName: sub.snippet?.title || 'N/A',
channelId: sub.snippet?.resourceId?.channelId || 'N/A',
description: sub.snippet?.description?.substring(0, 100) || 'N/A',
thumbnail: sub.snippet?.thumbnails?.default?.url || 'N/A'
}))
};Note: This example requires OAuth credentials. See Authentication for setup instructions.
// Search for videos about "Dart programming"
const result = await external_yt_search_list({
q: 'Dart programming',
type: 'video',
maxResults: 5
});
// Safely access items with null checks
const items = result.items || [];
return items.map(item => ({
id: item.id?.videoId || 'unknown',
title: item.snippet?.title || 'unknown'
}));// Fetch multiple resources in parallel
const [channels, videos, playlists] = await Promise.all([
external_yt_channels_list({ part: 'snippet', forUsername: 'GoogleDevelopers' }),
external_yt_videos_list({ part: 'snippet', chart: 'mostPopular', maxResults: 5 }),
external_yt_playlists_list({ part: 'snippet', channelId: 'UCN9HPn2fq-NL8M5_kp4RWZQ' })
]);
// Safely access array lengths
return {
channels: channels.items?.length || 0,
videos: videos.items?.length || 0,
playlists: playlists.items?.length || 0
};// Find a channel, then get their playlists
const channels = await external_yt_channels_list({
part: 'snippet',
forUsername: 'example'
});
// Safely check if channel was found
if (!channels.items || channels.items.length === 0) {
throw new Error('Channel not found');
}
const channelId = channels.items[0].id;
const playlists = await external_yt_playlists_list({
part: 'snippet,contentDetails',
channelId: channelId,
maxResults: 10
});
// Safely map playlists with null checks
const items = playlists.items || [];
return items.map(p => ({
id: p.id,
title: p.snippet?.title || 'unknown',
videoCount: p.contentDetails?.itemCount || 0
}));// Alternative: use call_tool for dynamic tool invocation
const result = await call_tool('yt_videos_list', {
part: 'snippet',
id: 'dQw4w9WgXcQ'
});
const items = result.items || [];
return items[0]?.snippet?.title || 'Unknown';dependencies:
yt: ^3.2.0
easy_api_annotations: ^1.2.2
dart_mcp: ^0.5.1Environment variables can be set via the shell or a .env file. The server searches for .env in the following locations (in order): .env, bin/.env, or adjacent to the running script.
| Variable | Description | Required For |
|---|---|---|
YT_API_KEY |
YouTube Data API key | Read-only public data (search, video details, etc.) |
YT_CLIENT_SECRETS_FILE |
Path to OAuth client secrets JSON file | OAuth authentication (uploads, updates, deletes, live streaming) |
YT_ACCESS_TOKENS_FILE |
Path to OAuth access/refresh tokens JSON file | OAuth authentication (auto-generated after first authorization) |
Authentication Notes:
- Use
YT_API_KEYfor read-only operations on public data - Use
YT_CLIENT_SECRETS_FILE+YT_ACCESS_TOKENS_FILEfor authenticated operations (uploads, modifications, private data) - OAuth credentials are file-based: the client secrets file contains your app credentials, and the access tokens file stores your authenticated session
- Never commit API keys or credential files to version control
- Add credential files to
.gitignore - API keys should be restricted to YouTube Data API only in Google Cloud Console
For OAuth credential file generation, use the yt_cli package:
# Install yt_cli
dart pub global activate yt_cli
# Run OAuth flow to generate credentials
yt authorize
# This creates two files:
# - client_secrets.json (download from Google Cloud Console)
# - access_tokens.json (auto-generated after authorization)
#
# Set the environment variables to point to these files:
export YT_CLIENT_SECRETS_FILE="path/to/client_secrets.json"
export YT_ACCESS_TOKENS_FILE="~/.yt/access_tokens.json"Note: The access_tokens.json file is automatically created and managed by the yt package after successful authorization. It contains your access and refresh tokens and is automatically updated when tokens expire.
By default, error details are logged to stderr without stack traces to prevent information leakage.
To enable verbose error logging (for development only):
# When running from source
dart run -D YT_MCP_DEBUG=true bin/yt_mcp_server.dart
# When compiled
dart compile exe -D YT_MCP_DEBUG=true -o yt-mcp bin/yt_mcp_server.dartWarning: Do not enable debug mode in production environments.
Important: The YouTube Data API has strict quota limits. Each API call consumes quota units:
| Operation | Quota Cost |
|---|---|
search.list |
100 units |
videos.list |
1 unit |
channels.list |
1 unit |
subscriptions.list |
1 unit |
videos.insert (upload) |
1600 units |
Default quota: 10,000 units per day
Recommendations:
- Implement rate limiting at the MCP host level (e.g., Qoder, Claude Desktop)
- Use
maxResultsparameter to minimize API calls - Cache results when possible
- Monitor your quota in Google Cloud Console
Example rate limiting configuration (for MCP hosts that support it):
{
"mcpServers": {
"yt-mcp": {
"command": "yt-mcp",
"rateLimit": {
"requestsPerMinute": 30,
"requestsPerDay": 100
}
}
}
}Note: The yt_mcp server itself does not enforce rate limiting. Protect your API quota by configuring limits in your MCP host or API gateway.
This section is for developers who want to modify the yt_mcp source code or add new tools.
- Dart SDK >= 3.8.0
- YouTube Data API key or OAuth credentials
If you're working on the package locally, you can run the server directly from source instead of using the globally activated version:
cd packages/yt_mcp
dart pub get
dart run bin/yt_mcp_server.dartThe MCP dispatcher (lib/src/yt_mcp_server.mcp.dart) is pre-generated and committed to the repository. You only need to run build_runner if you're modifying the YtMcpServer class or adding new tools.
If you modify lib/src/yt_mcp_server.dart, regenerate the dispatcher:
dart run build_runner buildThis produces lib/src/yt_mcp_server.mcp.dart, which wires up all tool handlers.
The MCP Inspector (@modelcontextprotocol/inspector) is the preferred tool for interactively debugging and testing the server.
- Node.js installed (required for code mode execution sandbox)
- YouTube API credentials (API key or OAuth)
-
Install the MCP Inspector globally:
npm install -g @modelcontextprotocol/inspector
-
Launch the Inspector connected to yt-mcp:
From source:
cd packages/yt_mcp npx @modelcontextprotocol/inspector dart run bin/yt_mcp_server.dartOr with globally activated package:
npx @modelcontextprotocol/inspector yt-mcp
-
Provide authentication credentials:
Important: MCP Inspector launches the server as a subprocess and does NOT inherit shell environment variables. You must provide credentials via a
.envfile:Create a
.envfile in thepackages/yt_mcp/directory:# For read-only access YT_API_KEY=your-api-key-here # OR for full OAuth access YT_CLIENT_SECRETS_FILE=/path/to/client_secrets.json YT_ACCESS_TOKENS_FILE=/path/to/access_tokens.json
-
Test the search tool:
In the Inspector UI, call
yt_searchwith:{ "query": "video search", "detail_level": "brief" }This returns all video-related tools with their names and descriptions.
-
Test basic execute tool (API key - read-only):
Call
yt_executewith JavaScript code:// Simple video search with error handling const result = await external_yt_search_list({ q: 'Dart programming tutorial', type: 'video', maxResults: 3 }); // Safely access items (API may return undefined on error) const items = result.items || []; return { count: items.length, videos: items.map(v => ({ id: v.id?.videoId || 'unknown', title: v.snippet?.title || 'unknown' })) };
-
Test OAuth operations (requires OAuth credentials):
If you configured OAuth credentials in step 3, test authenticated operations:
// Get your subscriptions (requires OAuth) const subscriptions = await external_yt_subscriptions_list({ part: 'snippet', mine: true, maxResults: 5 }); const subs = subscriptions.items || []; return { subscriptionCount: subs.length, channels: subs.map(s => s.snippet?.title || 'N/A') };
Expected result: If OAuth is configured correctly, you'll see your actual subscription count and channel names. If OAuth is not configured, the call will fail with an error.
-
Test parallel execution:
// Fetch multiple resources concurrently const [videos, channels] = await Promise.all([ external_yt_search_list({ q: 'JavaScript', type: 'video', maxResults: 3 }), external_yt_search_list({ q: 'Google', type: 'channel', maxResults: 3 }) ]); return { videoCount: videos.items?.length || 0, channelCount: channels.items?.length || 0 };
-
Inspect results:
Browse the request/response payloads in the web UI to verify:
- Tool discovery returns expected results
- JavaScript execution completes successfully
- API responses are properly formatted
- Error handling works as expected
If OAuth operations fail:
- Check
.envfile location: Ensure the.envfile is in the same directory where you run thenpx @modelcontextprotocol/inspectorcommand - Verify file paths: Make sure
YT_CLIENT_SECRETS_FILEandYT_ACCESS_TOKENS_FILEpoint to actual files that exist - Check token expiration: OAuth access tokens expire after 1 hour (but should auto-refresh using the refresh token)
- Validate scopes: Ensure your OAuth app has the
youtube.force-sslscope in Google Cloud Console - Test from command line: Verify OAuth works outside MCP Inspector:
cd packages/yt_mcp export YT_CLIENT_SECRETS_FILE=/path/to/client_secrets.json export YT_ACCESS_TOKENS_FILE=/path/to/access_tokens.json dart run bin/yt_mcp_server.dart
- View stderr output: The Inspector shows "Error output from MCP server" pane for debugging
- Enable verbose errors: Run with
dart run -D YT_MCP_DEBUG=true bin/yt_mcp_server.dart - Check Node.js: Ensure
node --versionworks (code mode requires Node.js) - Timeout issues: Code execution has a 30-second timeout by default
- Tool discovery: Use
detail_level: 'full'to see complete parameter schemas
Contributions are welcome! This package is part of the yt workspace.
Before submitting a PR:
- Make changes to
lib/src/yt_mcp_server.dart(add@Toolannotated methods) - Regenerate the dispatcher:
dart run build_runner build - Run analysis:
dart analyze - Format code:
dart format .
Hybrid Architecture Notes:
- Tools with
codeModeVisible: trueare directly callable by LLMs for natural language queries - All
@Toolmethods are automatically available in code mode asexternal_yt_<tool_name>()functions - To exclude a tool from code mode (e.g., destructive operations), add
codeMode: falseto the@Toolannotation - Tools remain visible in standard
tools/listunless you also setcodeModeVisible: false - Tool annotations (
readOnlyHint,destructiveHint, etc.) help LLMs understand tool behavior - Test new tools using both direct invocation and code mode execution
See the workspace README for development setup.
This package supports Homebrew installation via the cdavis-code/yt tap. To release a new version:
-
Update the version in
packages/yt_mcp/pubspec.yaml -
Push a version tag matching
yt_mcp-v*:git tag yt_mcp-v2.3.1 git push origin yt_mcp-v2.3.1
-
GitHub Actions workflow automatically:
- Compiles native binaries for macOS (ARM64 + x64) and Linux (x64)
- Creates a GitHub Release with all binaries
- Updates
homebrew/yt_mcp.rbwith the new version and SHA256 - Syncs the formula to the
cdavis-code/homebrew-yttap repository
-
Users install/update via:
brew install yt-mcp # First-time installation brew upgrade yt-mcp # Update to latest version
Required secrets: Set HOMEBREW_TAP_TOKEN in the GitHub repo under Settings > Secrets and variables > Actions (GitHub PAT with write access to cdavis-code/homebrew-yt).
Note: The Homebrew release workflow (.github/workflows/homebrew-release.yml) supports both yt_cli and yt_mcp packages. Tags must follow the format yt_cli-v* or yt_mcp-v* to trigger the automated build and release process.
MIT — see the LICENSE file for details.