Skip to main content
Glama
KashifMalik777

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
```