Skip to main content
Glama
modbender

youtube-analytics-mcp

by modbender
README.md
# youtube-analytics-mcp

An MCP server that gives an AI assistant the **whole** YouTube Analytics, Data v3 and
Reporting API surface for channels you own — including several channels at once.

Most YouTube MCP servers hardcode a handful of metric strings, so the first question
outside their preset list is unanswerable without forking them. This one is built the
other way round: `youtube_analytics_query` takes every parameter `reports.query` accepts,
and `youtube_data_call` / `youtube_reporting_call` do the same for the other two APIs. The
presets are conveniences on top, never the only route to something.

You bring your own Google Cloud OAuth client. Nothing is shipped with this package, no
credentials pass through any third party, and everything runs locally over stdio.

## Tools

| Tool | What it does |
| --- | --- |
| `youtube_accounts` | List authorized channels, the default, and where config lives |
| `youtube_authorize` | Start adding a channel; returns the consent URL at once |
| `youtube_authorize_status` | How the in-flight consent flow ended |
| `youtube_authorize_cancel` | Abandon an in-flight consent flow |
| `youtube_set_default_account` | Pick which channel unqualified calls use |
| `youtube_forget_account` | Drop a stored refresh token |
| `youtube_refresh_tokens` | Exercise every grant and report its age |
| `youtube_analytics_query` | **Unrestricted** `reports.query` |
| `youtube_data_call` | **Unrestricted** Data API v3 |
| `youtube_reporting_call` | **Unrestricted** Reporting API |
| `youtube_session_report` | One video or stream: summary + traffic-source split |
| `youtube_concurrent_curve` | One ended stream's concurrent viewers, minute by minute |
| `youtube_capabilities` | What these APIs can and cannot answer |

Every data tool takes an optional `account`, so one conversation can compare two channels.

### Large results go to a file, not through the model

`youtube_analytics_query`, `youtube_data_call` and `youtube_reporting_call` take
`outputPath` (and optional `format`: `csv` or `json`, otherwise inferred from the
extension). With it, the full result is written to disk and only a summary — row count,
columns, byte size, first three rows — comes back. Without it, results over 100 rows are
truncated with a pointer to the option, because a thousand-row report returned inline costs
the caller its context window and is unreadable when it arrives.

For genuinely bulk work — every day of every video, months at a time — use the Reporting
API through `youtube_reporting_call`: it produces downloadable daily CSV reports with
dimension combinations `reports.query` will not return in a single call.

## Setup

### 1. A Google Cloud OAuth client, once

- Create or pick a project.
- **APIs & Services → Library**: enable **YouTube Analytics API**, **YouTube Data API v3**
  and **YouTube Reporting API**.
- **OAuth consent screen** → **Audience**: set user type to **External** (Internal is only
  offered when a Workspace organisation is attached). On that same
  [Audience page](https://console.cloud.google.com/auth/audience), under **Test users**, click
  **+ Add users** and add the Google account of every channel owner — *including your own*.

  Miss this and consent fails with **"… has not completed the Google verification process. The
  app is currently being tested and can only be accessed by developer-approved testers."**
  Being the project owner does not make you a test user; you have to add yourself explicitly.
- **Set publishing status to *In production*.** This matters more than it looks. Google:

  > A Google Cloud Platform project with an OAuth consent screen configured for an external
  > user type and a publishing status of "Testing" is issued a refresh token expiring in 7
  > days, unless the only OAuth scopes requested are a subset of name, email address, and
  > user profile.

  Every YouTube scope is sensitive, so a *Testing* app makes you re-authorize **every week**.

  Be warned that publishing is not simply a switch for these scopes: the console is likely to
  require a demo video and put the app through YouTube API verification review before it will
  let you leave Testing. That is real work for a personal tool, and weekly re-consent is often
  the better trade. See [The 7-day grant limit](#the-7-day-grant-limit-and-why-no-code-can-work-around-it)
  below for the alternatives.
- **Credentials → Create credentials → OAuth client ID → Desktop app.** Not *Web
  application*: this server listens on a random free loopback port each run, and a Web
  client requires every redirect URI, port included, to be registered in advance.
- Download the JSON.

### 2. Tell the server where the client is

Put it in the config file (see `config.example.json`):

```jsonc
// %APPDATA%\youtube-analytics-mcp\config.json          (Windows)
// ~/Library/Application Support/youtube-analytics-mcp/  (macOS)
// ~/.config/youtube-analytics-mcp/config.json           (Linux)
{
  "client": { "client_id": "...", "client_secret": "..." }
}
```

Run `youtube-analytics-mcp --where` to print that directory. Environment variables work
too and take precedence — `YTMCP_CLIENT_ID` + `YTMCP_CLIENT_SECRET`, or
`YTMCP_CLIENT_FILE` pointing at Google's download verbatim (the `{"installed": …}` wrapper
is unwrapped for you). `YTMCP_CONFIG_DIR` relocates the whole directory.

### 3. Authorize each channel

```bash
bun run auth                      # or: youtube-analytics-mcp --authorize
bun run auth -- --alias second    # name it yourself
```

Your browser opens on the consent page automatically; the URL is printed too, for the
cases where it cannot (SSH, containers, CI). Pick the Google account that owns the channel and
approve. Repeat for each channel — **choose a different account in the browser each time**.
Accounts are named after their `@handle` unless you pass `--alias`.

Set `YTMCP_NO_BROWSER=1` to never launch a browser, or pass `openBrowser: false` to the
`youtube_authorize` tool for a single call.

Refresh tokens are written to `accounts.json` in the same directory, separate from the
`config.json` you hand-edit, so the file you might paste into a bug report is never the
file holding tokens. Both are written `0600` where the platform honours it.

Your assistant can also drive this. `youtube_authorize` returns the consent URL immediately
and keeps listening in the background; `youtube_authorize_status` reports how it ended. It
does not block, because consent takes as long as a human takes and MCP clients give up on a
tool call long before that. The URL is also written to `pending-auth.txt` in the config
directory, since most clients discard a server's stderr and a URL nobody can read is no use.

### 4. Register with your MCP client

Claude Code:

```bash
claude mcp add youtube-analytics --scope user -- bunx youtube-analytics-mcp
```

Or by hand, in any client's `mcpServers` map:

```json
{
  "mcpServers": {
    "youtube-analytics": { "command": "bunx", "args": ["youtube-analytics-mcp"] }
  }
}
```

## Read-only by default

Updating a video, posting or moderating comments, and uploading thumbnails are not
reversible on a live channel, so the write scope is **not requested** and non-GET calls are
refused. To enable them set `YTMCP_ALLOW_WRITE=1` **and re-authorize** — the flag alone
does nothing, because the stored token does not carry the scope.

## Concurrent viewers, and the query shape nobody guesses

`averageConcurrentViewers` and `peakConcurrentViewers` **do** work on ended streams, and
they match Studio's own numbers exactly. They are widely believed not to exist because the
API refuses them in every shape but one: the filter must pin a single video **and**
`dimensions` must be `livestreamPosition`.

| query | result |
| --- | --- |
| `metrics=peakConcurrentViewers` alone | 400 `The query is not supported` |
| `+ filters=video==ID` | **500 internal error** |
| `+ filters=video==ID;liveOrOnDemand==LIVE` | 400 — the extra filter is rejected |
| `+ filters=video==ID` `+ dimensions=livestreamPosition` | one row per minute of the stream |

No error names the missing dimension, and the 500 in particular reads as the metric being
broken rather than the request being wrong. `youtube_concurrent_curve` assembles it for you
and returns the peak, the mean, and the whole minute-by-minute curve.

## What it genuinely cannot give you

`youtube_capabilities` returns the current list. Both were checked by asking for the metric
and getting `Unknown identifier` back, which is how the API distinguishes a name it has
never heard of from one it knows but cannot serve here:

- **Live chat message and reaction totals.** Studio-only. `liveChatMessages` reads a chat
  in real time and cannot recover an ended one.
- **Impressions and impression click-through rate.** Studio-only, in the Reach tab.

## Two things worth knowing

**There is no "since published" window.** The Analytics API is purely date-range, so a
window covering a stream day returns that stream's live audience by construction. Studio's
default per-video window excludes the entire live period, which is an easy and expensive
trap when analysing live streams. This API cannot fall into it.

**Analytics quota is separate.** The Analytics and Reporting APIs meter independently of
the Data API v3 daily unit budget, so querying here does not consume the quota that live
chat polling competes for. *Strong inference from them being distinct APIs with their own
console quota pages — not measured.*

## Development

```bash
bun install
bun run dev          # start on stdio
bunx tsc --noEmit    # typecheck
bun run inspector    # MCP Inspector
```

MIT.

## The API lags a few days

Finalized Analytics data is not available immediately. Measured on 2026-08-25, day-dimension
rows ran through 08-22 and stopped: sessions from the previous three days returned no rows at
all, not zero rows. A query for a stream that ended hours ago will look like a channel with no
traffic.

Studio's web UI has a realtime path that the API does not expose, so **same-day reporting still
has to come from Studio**. Use this server for everything older than roughly three days, where
it is far better than clicking through Studio one video at a time.

## The 7-day grant limit, and why no code can work around it

While the Cloud project's publishing status is **Testing** with an External user type, Google
revokes refresh tokens after 7 days unless the only scopes requested are name, email and
profile. Every YouTube scope is sensitive, so the exception never applies here.

**This cannot be automated away.** The 7 days is on the *refresh* token. Minting a new one
requires a human approving a consent screen in a browser — that is what consent means, not a
gap to engineer around. Refreshing access tokens more often does not touch it.

What this server does instead:

- `youtube_accounts` reports each grant's `ageDays` and warns from day 5.
- An expired grant fails with a message naming the cause and the fix, not a bare
  `invalid_grant`.
- `youtube_refresh_tokens` (or `--refresh` from the CLI) exercises every grant as a health
  check. It is also a hedge: it is **not established** whether the 7-day clock is absolute from
  issuance or slides on use. If it slides, running this daily on a scheduler keeps grants alive
  indefinitely; if it does not, the call costs almost nothing. Worth running either way.
- Re-consenting is one call to `youtube_authorize`, which opens the browser itself — about
  fifteen seconds.

**The real fixes**, in order of cost:

1. **Publishing status → In production.** Free, and grants stop expiring. For sensitive YouTube
   scopes Google may require a demo video and verification review before it will let you
   publish, which is a real amount of work for a personal tool.
2. **Internal user type.** No 7-day limit and no verification, but the option only exists when
   the project belongs to a Google Workspace organisation — a paid subscription.
3. **Live with weekly re-consent.** For a single-user tool this is often the right answer.

TDQS

A3.7/5.0

Scored across 13 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: authorization flow (authorize, status, cancel), account management (accounts, set default, forget, refresh), raw API access (analytics_query, data_call, reporting_call), convenience wrappers (session_report, concurrent_curve), and capability explanation. The only slight overlap among the three unrestricted calls is mitigated by descriptions pointing to different YouTube APIs.

Naming Consistency4/5

All tools share the consistent 'youtube_' prefix and use snake_case. However, the naming pattern mixes nouns (youtube_accounts, youtube_capabilities) with verb phrases (youtube_set_default_account, youtube_refresh_tokens) and compound nouns (youtube_analytics_query, youtube_concurrent_curve). Still, the names are readable and predictable after a moment.

Tool Count5/5

At 13 tools, the server has a well-scoped surface covering authentication, account management, and multiple API query methods without redundancy. Each tool contributes a distinct capability, and none feel superfluous.

Completeness5/5

The surface covers the full lifecycle: authorization, account state, raw access to all three YouTube APIs, convenience queries for typical needs, and a capabilities tool to explain limitations. No obvious missing operations; even token revocation is addressed with a pointer to Google's page.

Maintenance

ActivityMaintained
ResponsivenessNo issues