Skip to main content
Glama
jaimebg

youtube-studio-mcp

by jaimebg

YouTube Studio MCP

An MCP server for auditing and improving a YouTube channel's discoverability.

It connects any MCP-capable AI agent to your own channel's data: catalog, Analytics API metrics, retention curves, inbound search terms, and the impressions and click-through rate that only a Studio CSV export exposes. It then ranks what is worth fixing by recoverable views rather than by click-through rate — see How underperformers are ranked.

Eight tools: auth_status, list_videos, get_video, query_analytics, get_search_terms, get_retention_curve, import_studio_data, and find_underperformers.

Everything is read-only and local: the SQLite cache, your OAuth tokens, and your Studio exports never leave your machine.

Requirements

  • Node.js ≥ 22

  • A Google account that owns the YouTube channel

Related MCP server: MCP YouTube Intelligence

Setup

1. Create a Google Cloud project and enable the APIs

  1. Go to https://console.cloud.google.com/ and create a project.

  2. Enable YouTube Data API v3 and YouTube Analytics API. (Both are in active use: the Data API backs catalog sync and list_videos/ get_video, and the Analytics API backs query_analytics, get_search_terms, and get_retention_curve. Google Cloud Console only lets you add a consent-screen scope for an API you've enabled, so enable both before the next step.)

  1. Go to APIs & Services → OAuth consent screen.

  2. Choose External and fill in the required fields.

  3. Add these scopes:

    • https://www.googleapis.com/auth/yt-analytics.readonly

    • https://www.googleapis.com/auth/youtube.readonly

    • https://www.googleapis.com/auth/youtube.force-ssl

Important — publish the app to Production. While the app is in Testing, Google expires refresh tokens after 7 days, so you would have to re-authenticate every week. Click Publish app. The app stays unverified, which is fine: you are the only user and you are accessing your own data. You will see an "unverified app" warning once — choose Advanced → Go to (app name).

3. Create the OAuth client

  1. APIs & Services → Credentials → Create credentials → OAuth client ID.

  2. Application type: Desktop app.

  3. Download the JSON.

4. Install and authenticate

npm install
npm run build

Save the downloaded OAuth client JSON as credentials.json in the server's config directory (create the directory first if it doesn't exist):

# Linux/macOS — adjust the source filename to match what Google actually
# named your download (it starts with "client_secret_")
mkdir -p ~/.config/youtube-studio-mcp
mv ~/Downloads/client_secret_*.json ~/.config/youtube-studio-mcp/credentials.json
# Windows (PowerShell) — same caveat about the source filename
New-Item -ItemType Directory -Force "$env:USERPROFILE\.config\youtube-studio-mcp" | Out-Null
Move-Item "$env:USERPROFILE\Downloads\client_secret_*.json" "$env:USERPROFILE\.config\youtube-studio-mcp\credentials.json"

Then run:

node dist/index.js auth

This opens your browser automatically. Authorize there, and the tokens are saved to <config dir>/tokens.json. On Linux/macOS the file is written with owner-only permissions (chmod 600); Windows has no equivalent file-permission bits, so that step is a no-op there — rely on your user account's normal file protections.

If no browser opens, the command also writes the authorization link to <config dir>/authorize-url.txt — open that file and click the link. Do not hand-copy the URL out of your terminal: it is ~520 characters, wraps across several lines, and a truncated copy fails at Google with the misleading error Required parameter is missing: response_type (the missing parameter is in the part that got cut off, not in the request we build).

Set YTMCP_HOME to override the config directory (e.g. for a second channel or a test setup). It replaces the whole ~/.config/youtube-studio-mcp path, so credentials.json, tokens.json, and the SQLite cache all move with it.

5. Register the server with your AI agent

The server speaks standard MCP over stdio, so any MCP-capable client can run it. You need one thing in every case: the absolute path to dist/index.js in this repo.

Most clients share the same JSON shape. Substitute your own path:

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

Agent

Where that JSON goes

Claude Code

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

Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json (macOS) · %APPDATA%\Claude\claude_desktop_config.json (Windows)

Cursor

~/.cursor/mcp.json for all projects, or .cursor/mcp.json inside one

Windsurf

~/.codeium/windsurf/mcp_config.json

Cline

the extension's cline_mcp_settings.json, via MCP Servers → Configure

Continue

~/.continue/config.yaml (or config.json)

Gemini CLI

~/.gemini/settings.json

Zed

settings.json, under context_servers

Two clients use a different shape.

VS Code / GitHub Copilot.vscode/mcp.json, keyed servers, not mcpServers:

{
  "servers": {
    "youtube-studio": {
      "type": "stdio",
      "command": "node",
      "args": ["/absolute/path/to/youtube-studio-mcp/dist/index.js"]
    }
  }
}

OpenAI Codex CLI~/.codex/config.toml, TOML rather than JSON:

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

Restart the agent after editing its config. Ask it to run auth_status: it should name your channel and report remaining quota. If it reports Authenticated: NO, re-run node dist/index.js auth.

If your client is not listed, look for "MCP" in its settings — the command and args above are all any of them need. Config paths do move between releases, so check your client's own docs if a path here does not exist.

A note on agent behaviour

Every tool is annotated readOnlyHint: true, so agents that surface that hint will not prompt for write confirmation. Nothing in this server modifies your channel — writing metadata back is a later stage.

list_videos serves from the local cache unless asked to sync, and find_underperformers and import_studio_data never touch the network at all. Only explicit syncs and Analytics queries spend quota, which matters because agents explore: an agent that calls list_videos twenty times costs nothing, while twenty catalog syncs would exhaust a day's budget. auth_status reports what is left.

Tools

Tool

Purpose

auth_status

Connection state, channel identity, remaining quota, local cache size

list_videos

List and filter the catalog; sync: true refreshes from the API

get_video

Full cached metadata and statistics for one video

query_analytics

Escape hatch onto the Analytics API — arbitrary metrics, dimensions, filters

get_search_terms

The search queries that brought viewers in, cross-referenced against the video's metadata

get_retention_curve

Where viewers stop watching, as annotated drop-offs rather than raw points

import_studio_data

Import impressions and CTR from a Studio CSV export — the one metric the Analytics API does not expose. Reads a local file; no authentication or quota needed

find_underperformers

Rank the catalog by recoverable views — impressions times the gap to the channel's impression-weighted CTR baseline. Needs a Studio export imported first; reads local data only

list_videos serves entirely from the local SQLite cache unless you pass sync: true — plain reads (filtering by Shorts/long-form, view count, publish date, title, or sorting) cost no quota. If nothing has been synced yet, it tells you to call it again with sync: true instead of returning an empty list.

get_search_terms and get_retention_curve cache their results per date window (see Quota below); query_analytics does not cache and always makes a live call. get_search_terms returns at most 25 rows — Google caps the underlying report there, so a wider date range changes which terms rank in the top 25 rather than how many rows come back.

Quota

YouTube grants 10,000 units/day plus a separate 100 search.list calls/day. The server tracks both and holds back a reserve (500 units, 10 search calls) so a bulk operation cannot leave interactive tools unusable. Quota resets at midnight Pacific, which is what auth_status reports.

A full catalog sync (list_videos with sync: true) makes one channels.list call, then pages the uploads playlist (playlistItems.list, 50 videos per page) and fetches video details in batches (videos.list, 50 IDs per call), each call costing 1 unit. That works out to 1 + ceil(videos/50) + ceil(videos/50) units — about 5 units for a 100-video channel.

The YouTube Analytics API has its own per-project quota in Cloud Console, separate from the Data API's 10,000 units. Analytics calls are recorded in the local ledger at zero unit cost, so auth_status will not show them draining your Data API budget.

Search-term results and retention curves are cached per date window, because the underlying reports return a ranked top-N over a range rather than per-day rows. A repeat call with the same dates serves from the cache; pass refresh: true to re-query.

Importing impressions and CTR

impressions and impressionClickThroughRate do not exist in the YouTube Analytics API — they are Studio-only. To get them:

  1. YouTube Studio → AnalyticsAdvanced mode (top right)

  2. Make sure the Impressions and Impressions click-through rate columns are visible — the export contains only the columns currently on screen

  3. ExportComma-separated values (.csv) — you get a zip containing three files

  4. Unzip it, then run import_studio_data with the folder path

import_studio_data only reads a file from disk — it never calls the YouTube Data API or the Analytics API, so it needs no authentication and costs no API quota.

The date window is read from the folder name (Studio names it like Contenido 2010-01-26_2026-08-09 Channel). To override it, pass both rangeStart and rangeEnd (YYYY-MM-DD) — supplying only one is rejected with a validation error rather than silently falling back to the folder-name window, since that could land data under the wrong dates with no warning. Both dates must be real calendar dates (2026-13-45 is rejected, not rolled over) and rangeStart must not be after rangeEnd.

Impressions and CTR are a whole-range aggregate. Of the three files in the export, only the per-video table (Datos de la tabla.csv / Table data.csv) carries impressions and CTR, and it reports one row per video summed over the entire date range — there is no daily CTR anywhere in the export. The per-day file (Datos del gráfico.csv / Chart data.csv) and the channel-totals file (Totales.csv / Totals.csv) carry views only. So a comparison across time means importing several exports with different ranges, not slicing one.

import_studio_data accepts either the export folder or a specific CSV path. Point it at the folder and it finds the table file automatically. Point it at one of the other two files directly and the import is rejected outright: a CSV's header tells you unambiguously which of the three reports it is (reportType is table, chart, or totals — see src/studio/csvSchemas.ts), and only table carries anything this importer can store. The chart file does have a video id, so a naive import would silently succeed while overwriting impressions/CTR with NULL and views with the last day's figure instead of the range total; the totals file has no video id at all. Both are rejected before anything is written, with a message naming Datos de la tabla.csv / Table data.csv as the file to point at instead.

Rows for videos that are no longer public are stored and reported as unmatched; that is expected, not an error.

Shorts

A video counts as a Short only if it is 180 seconds or shorter and was published on or after 2020-09-14, the day Shorts launched.

Duration alone is not enough. On a catalog of naturally short long-form video — music videos, edits, trailers — a duration-only rule misclassifies wholesale. Live verification against a dormant pre-2020 channel flagged roughly 70% of its catalog as Shorts, every one a false positive: the channel's newest upload predated the Shorts launch by months, so not one of them could have been genuine. find_underperformers reads this flag through its cohort parameter. Passing cohort: 'short' or cohort: 'long' restricts the baseline to that half of the catalog, so Shorts and long-form are compared only against their own kind. The default, cohort: 'all', does not do that segmenting — it pools both into a single blended baseline. On a catalog that is one cohort already (this user's real case, entirely long-form) pooling is a no-op, but on a mixed catalog the default blends two populations with different typical CTRs; pass cohort explicitly to segment them. A wrong flag on a video would produce confident nonsense rather than an obvious error, which is why the publish-date guard matters.

How underperformers are ranked

find_underperformers ranks by recoverable views, not by click-through rate:

recoverable views = impressions x (baseline CTR - video CTR) / 100

That is an estimate of the views a video would have gained at the channel's own baseline — the quantity worth acting on. Ranking on CTR alone is misleading, in three specific ways:

  • Impressions concentrate. Most of a channel's impressions sit in a small fraction of its videos, so "worst CTR" and "biggest opportunity" are close to disjoint sets. The video with the ugliest ratio is often one almost nobody was shown.

  • Zero impressions produce a 0% CTR by division, not by performance. Sorting ascending puts every never-surfaced video at the top of the list of things to fix, which is exactly backwards.

  • The highest CTR on a channel is usually a tiny denominator — a handful of impressions that happened to convert. It is noise presented as a triumph.

So two rules follow. Videos below an impressions floor are reported as insufficient data and never ranked as poor performers. And the baseline is impression-weighted, because an unweighted mean is dominated by low-traffic videos and describes almost none of the traffic the channel actually gets.

Each opportunity is typed. weak_metadata means the metadata score is low enough that it is the thing to fix first; low_ctr means the metadata is already sound and the thumbnail or title framing is the lever.

Development

npm test          # unit tests, no network
npm run typecheck
npm run build

npm run typecheck runs two projects: tsconfig.json (src/**, the build) and tsconfig.test.json (src/** + test/** + vitest.config.ts, noEmit only). Run just the test project with npm run typecheck:test. Vitest itself only strips types via esbuild and does not type-check, so npm run typecheck is what actually catches a type error in a test file.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jaimebg/youtube-studio-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server