CreatorScope MCP
README.md
# CreatorScope MCP
A small MCP server that lets Claude **directly pull exact YouTube creator stats and discover new creators**, then write them into the Notion Influencers roster. Powered by the **free YouTube Data API v3** (10,000 units/day, no card).
Same infra pattern as TitanMail: Node/TypeScript, one tool per file, deployed with **pm2 + Caddy + token auth** on the always-on EC2 box.
## What it can / can't do
- ✅ Exact **subscribers, per-video views → avg/median views, likes, comments → engagement rate, upload frequency, country, channel description (often with a business email)**. This is the full input to `Avg Views × CPM` pricing.
- ❌ **Audience demographics, % US audience, fake-follower %** — private to the channel owner; no API exposes them. Use Modash's free trial only when a brand demands audience-quality proof.
## Tools
| Tool | Purpose | Quota cost |
|---|---|---|
| `ping(deep?)` | Health check; `deep=true` makes a live 1-unit call to validate the API key. | 0 or 1 |
| `resolve_channel(input)` | `@handle` / URL / video URL / channelId → `{ channelId, title, handle, url }`. | ~1–2 |
| `channel_stats(input, sample?, exclude_shorts?)` | Full computed stat object for one creator. | ~3 |
| `bulk_channel_stats(inputs[], sample?, exclude_shorts?, fresh?)` | Stats for many inputs in one call, **batched** (channels/videos 50 at a time), with per-item errors. | ~3 each (batched cheaper) |
| `search_channels(query, {min_subscribers?, country?, relevance_language?, max_results?, hydrate_stats?, exclude_channel_ids?})` | Discover candidate channels; **`max_results` is caller-chosen and paginated** (up to `MAX_SEARCH_RESULTS`, default 200); dedupes + can exclude roster channels; optionally hydrate full stats. | 100 per 50 results (+~3 each if hydrated) |
| `quota_status()` | Local daily unit counter vs the 10,000 limit. | 0 |
### Caching & freshness
Computed stats are cached per channel for `CACHE_TTL_MS` (default 6h), so repeat roster pulls don't re-spend quota. Results served from cache carry `cached: true`. Pass **`fresh: true`** to `channel_stats` / `bulk_channel_stats` to bypass the cache and force a live re-pull — do this right before quoting a brand. Concurrent identical requests are de-duplicated onto a single in-flight fetch, so a stampede can never double-spend quota.
### `channel_stats` output fields
**Identity/audience:** `channelId, title, handle, url, subscribers, subscribersHidden, totalViews, videoCount, lifetimeAvgViews, country, channelCreatedAt, channelAgeDays, description`.
**Contact/discovery:** `emails[], businessEmail, links[] ({type,url}), keywords[], topics[]`.
**Performance:** `avgViews, medianViews, minSampledViews, maxSampledViews, avgLikes, avgComments, engagementRatePct, likeRatePct, commentRatePct, viewsPerSubscriberPct, avgToMedianRatio, consistency, recentTrend, likesHidden, uploadsPerMonth, daysBetweenUploads, lastUploadDate, sampledCount, excludeShorts`, plus `recentVideos[]` (included by default on `channel_stats`; opt in on `bulk_channel_stats` via `include_videos`).
> **Emails:** `emails[]` is parsed from the public channel description. YouTube's About-page "business email" button is CAPTCHA-gated and exposed by **no** API — use `links[]`/website plus a dedicated email-finder to go further.
**Computed:**
- `avgViews` = mean views of the last *N* non-Short videos (default N=15).
- `medianViews` = median (robust to a single viral outlier).
- `engagementRatePct` = (mean likes + mean comments) ÷ avgViews × 100. Divide-by-zero guarded; hidden `likeCount` is excluded from the mean and flags `likesHidden`.
- `uploadsPerMonth` = sample size ÷ (span of sampled days ÷ 30).
- **Shorts filter** (`exclude_shorts`, default true): parses ISO-8601 `duration` and drops videos < 60s so avg views isn't skewed.
- `businessEmail` = first email found in the channel description (handles `at`/`dot` obfuscation).
## Quota model
Per the API: `channels.list` = 1, `playlistItems.list` = 1/page, `videos.list` = 1/batch of 50, `search.list` = 100. One full `channel_stats` ≈ 3 units → ~3,000 creators/day on the free tier. Discovery (`search_channels`) costs 100 each → ~100 searches/day. A local `.quota-counter.json` tracks usage per UTC day as a soft-guard and warns near the limit.
---
## Local development
```bash
npm install
cp .env.example .env # fill in YOUTUBE_API_KEY and CREATORSCOPE_TOKEN
npm run build
npm start
npm test # run the unit-test suite (no network needed)
npm run smoke # end-to-end: exercises the live tools (server must be running)
```
The server listens on `http://127.0.0.1:8790/mcp` (Streamable HTTP). `GET /healthz` is an unauthenticated liveness probe. Every `/mcp` request requires the token via `Authorization: Bearer <CREATORSCOPE_TOKEN>` (or an `X-CreatorScope-Token` header).
Quick check once running:
```bash
curl -s http://127.0.0.1:8790/healthz
```
## Configuration (`.env`)
| Var | Default | Notes |
|---|---|---|
| `YOUTUBE_API_KEY` | — | **Required.** Restrict to *YouTube Data API v3* only (+ optional IP-restrict to the EC2 IP). |
| `CREATORSCOPE_TOKEN` | — | **Required.** Bearer token clients must present. Generate with `openssl rand -hex 32`. |
| `PORT` | `8790` | Local port Caddy proxies to. |
| `HOST` | `127.0.0.1` | Keep loopback so only Caddy can reach it. |
| `QUOTA_DAILY_LIMIT` | `10000` | Free-tier daily unit budget. |
| `QUOTA_WARN_THRESHOLD` | `0.85` | Warn once usage crosses this fraction. |
| `DEFAULT_SAMPLE` | `15` | Recent non-Short videos to sample. |
---
## Deploy on EC2 (pm2 + Caddy + token)
1. **Clone + build** on the box:
```bash
git clone <private-repo> creatorscope-mcp && cd creatorscope-mcp
npm ci && npm run build
cp .env.example .env # set YOUTUBE_API_KEY + a fresh CREATORSCOPE_TOKEN
```
2. **Start with pm2:**
```bash
pm2 start ecosystem.config.cjs
pm2 save && pm2 startup
```
3. **Add the Caddy route** — copy the block from `Caddyfile.example` into your Caddyfile (adjust the subdomain/port), then `caddy reload`. Caddy handles TLS; the app enforces the token.
4. **Connect to Claude:** claude.ai → Settings → Connectors → Add custom connector →
- URL: `https://creatorscope.perplexionmedia.com/mcp`
- Auth: Bearer token = your `CREATORSCOPE_TOKEN`.
5. **Verify:** tell Claude *"CreatorScope connected"* → it runs `ping deep` + one test channel, then enriches the roster.
## Founder one-time setup (Google Cloud)
console.cloud.google.com → New Project ("Perplexion CreatorScope") → APIs & Services → Library → enable **YouTube Data API v3** → Credentials → **Create API key** → restrict it to that API. ~5 min, free, no card.
---
## How Claude uses it (workflow)
1. **Enrich existing roster:** `bulk_channel_stats(["@Techlore", "@AllThingsSecured", …])` → write real subs/avgViews/engagement into Notion Influencers (replacing manual estimates); Est. Price auto-recomputes.
2. **Discover new creators:** `search_channels("VPN review", { min_subscribers: 20000, country: "US" })` → `bulk_channel_stats` the candidates → filter by engagement + avg views + niche fit → add the good ones to Notion (status *Collected*) with stats + auto price.
3. **On-demand vetting:** brand replies "send creators" → pull fresh stats for the shortlist so pricing is current.
CPM stays Claude-set by sub-niche (see *Pricing & Economics*); the MCP supplies views/engagement only.
## Project layout
```
src/
config.ts env loading + validation
quota.ts per-day unit counter (soft-guard, atomic writes)
cache.ts TTL cache + in-flight de-duplication (stampede-safe)
log.ts structured JSONL logging (stdout + optional file)
util.ts input parsing, ISO-8601 duration, median, email extraction
youtube.ts typed YouTube Data API v3 client (timeout, retry/backoff, batching)
stats.ts resolve + compute stat objects (batched) + discovery
server.ts Streamable HTTP MCP server + bearer-token auth
tools/ one file per tool (ping, resolve_channel, channel_stats, …)
test/ unit tests (node:test) — util, pricing math, cache/dedupe
scripts/
smoke-client.mjs end-to-end MCP client test against a running server
ecosystem.config.cjs pm2 process definition (fork mode, single instance)
Caddyfile.example sample reverse-proxy route
.env.example config template
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues