@uscreentv/mcp
v0.3.0
Published
Uscreen MCP server — exposes Uscreen primitives (catalog, entitlement, sandbox, checkout) as tools for Claude Desktop, Cursor, Windsurf, and other MCP clients
Readme
@uscreentv/mcp
MCP server for Uscreen — exposes the Uscreen analytics surface as tools that AI assistants like Claude Desktop, Cursor, and Windsurf can call directly.
Status: alpha. v1 ships read-only analytics + user-detail primitives backed by the public
developer/v1/*API. Catalog, entitlement, sandbox, and checkout primitives are planned and will land as their backing endpoints ship.
What it does
Once installed in your MCP-compatible client, this server lets an AI assistant answer questions about your Uscreen business in plain English:
- "What's my MRR?" —
get_store_metricsreturns the canonical top-line number, same as your Bullet dashboard. - "What's working in my catalog this month?" —
list_top_contentranks videos by watch time. - "How is video 1234 trending?" —
get_content_performancereturns a daily series for one piece of content. - "How many new signups in the last 7 days?" —
get_subscriber_growthwith a trial-vs-paid split. - "Which of my plans is driving revenue?" —
get_revenue_breakdownper-plan, sorted by MRR. - "What's going on with [email protected]?" —
find_user_by_emailthenget_user,list_user_emails,list_user_views,list_user_comments,list_user_paymentsfor a full activity drill-down. - "Did Sarah's churn coincide with a failed charge?" —
list_user_paymentswithstatus=overdueorstatus=unpaid. - "Is Sarah about to churn?" —
get_userand readcurrent_plan.cancel_at_period_end(true means they clicked cancel; the sub stays active untilnext_billing_at). - "When does Sarah's trial end?" —
get_userand readcurrent_plan.trial_ends_at. - "Show me users whose trials end this week." —
list_usersand filter bycurrent_plan.trial_ends_atwithin the window. Cohort prompts work without the per-user round-trip becausecurrent_planrides on the list rows. - "Who's about to churn?" —
list_usersand filter bycurrent_plan.cancel_at_period_end: true. - "Did Sarah ever pay, or only trial?" —
get_userand checkplan_historyrows: anyis_trial: falserow means they reached the paid phase. Orlist_user_paymentsfor the definitive answer (anystatus: "paid"row). - "Show me everyone who signed up this week." —
list_userswith adays=7window. - "What can this key do?" —
whoamireturns the workspace identity and the granted scope list.
In Uscreen vocabulary, a user is anyone in your CRM — leads, paid subscribers, and one-time buyers. The shipped get_subscriber_growth tool keeps its existing name; the user_* tools are the broader CRM-record surface.
All tools are primitives, not workflows. Agents chain them freely; Uscreen does not prescribe flows.
Install
Prerequisites
- Node 20+ (22+ recommended).
- A Uscreen developer API key. Generate one from Bullet → Settings → Developers → Apps. Tokens look like
usc_sk_live_<32 chars>and are shown exactly once at creation — copy them then.
Install as an npm package
npm install -g @uscreentv/mcpOr run directly without install:
npx -y @uscreentv/mcpClaude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the equivalent on Windows / Linux:
{
"mcpServers": {
"uscreen": {
"command": "npx",
"args": ["-y", "@uscreentv/mcp"],
"env": {
"USCREEN_API_KEY": "usc_sk_live_..."
}
}
}
}Restart Claude Desktop. The Uscreen tools appear in the tools menu.
Cursor
Open Cursor Settings → MCP → Add new MCP server:
- Name: Uscreen
- Type: stdio
- Command:
npx -y @uscreentv/mcp - Environment:
USCREEN_API_KEY=usc_sk_live_...
Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"uscreen": {
"command": "npx",
"args": ["-y", "@uscreentv/mcp"],
"env": { "USCREEN_API_KEY": "usc_sk_live_..." }
}
}
}Tools
Each tool requires a specific scope on the API key — see Scopes for the full breakdown. Missing-scope requests return 403. For the complete tool reference — every tool's title, description, full input schema (types, defaults, descriptions) — see TOOLS.md, which is auto-generated from defineTools() and kept in sync by CI. Drop it on an LLM as a one-shot context for what this MCP can do.
Identity
| Name | Backing endpoint | Scope | Returns |
|---|---|---|---|
| whoami | GET /developer/v1/whoami | none | Workspace identity (store_id, store_name, store_url, store_timezone), the calling key's granted scopes, and the API version. Safe to call as a connection sanity check or for an agent that wants to know what's accessible before chaining other tools. |
Analytics
| Name | Backing endpoint | Scope | Returns |
|---|---|---|---|
| get_store_metrics | GET /developer/v1/analytics/store_metrics | finance_analytics.read | Top-line MRR, active subscribers, trial counts (current + prior month, with confidence envelope) |
| list_top_content | GET /developer/v1/analytics/top_content | content_analytics.read | Content ranked by watch time over a trailing window (days, limit, content_type) |
| get_content_performance | GET /developer/v1/analytics/content_performance | content_analytics.read | Per-content daily view + watch-time series. Requires content_id |
| get_subscriber_growth | GET /developer/v1/analytics/subscriber_growth | finance_analytics.read | Daily new-signup counts with trial/paid split |
| get_revenue_breakdown | GET /developer/v1/analytics/revenue_breakdown | finance_analytics.read | Per-plan MRR + active subscribers, sorted by MRR desc |
Numbers come from the same canonical analytics pipeline the Bullet admin dashboard uses, so MCP MRR matches Bullet MRR by construction.
User detail
All user-detail tools require the crm.read scope.
| Name | Backing endpoint | Returns |
|---|---|---|
| list_users | GET /developer/v1/users | Cursor-paginated list of users in the store — lightweight {id, name, email, signup_at, last_signed_in_at, current_plan} per row. Optional signup-window filter via days; chain into get_user for full detail. Rate-limited per key and per store; default page size is 25 |
| find_user_by_email | POST /developer/v1/users/lookup | Minimal {id, name, email} projection — start here when you only have an email |
| get_user | GET /developer/v1/users/{id} | One user's identity, subscription state, plan history, tags, typed custom fields |
| list_user_emails | GET /developer/v1/users/{id}/emails | Email history — manual broadcasts vs transactional (automation drips and system messages) — cursor-paginated, trailing window via days, optional kind filter |
| list_user_views | GET /developer/v1/users/{id}/views | Content watch history — cursor-paginated, trailing window via days |
| list_user_comments | GET /developer/v1/users/{id}/comments | Comments on content + community posts — cursor-paginated, trailing window via days (DMs not included) |
| list_user_payments | GET /developer/v1/users/{id}/payments | Payment history (Invoices) — cursor-paginated, trailing window via days. kind discriminator labels each row as subscription / purchase / rental / freebie / gift / donation / booking. Optional status filter (paid / refunded / unpaid / etc.). Drafts and integration ownerships are excluded |
find_user_by_email sends the email in the request body, never in the URL or logs, and is rate-limited per key and per store. The id returned is the canonical internal id — never use email or other PII as a lookup key on subsequent calls.
The projection is intentionally narrow — {id, name, email} only — not a stub. Lookup is rate-limited specifically to discourage enumeration, so the response is kept minimal to avoid amplifying that surface. Token cost matters too: full UserDetail carries plan history, tags, and custom fields that bloat the LLM context on every email probe. When you want full detail, chain into get_user with the returned id.
list_users returns a strictly larger PII surface than the lookup tool (name + email + signup_at + current_plan) and walks the entire CRM when no days window is supplied. The same per-key / per-store throttle applies, but holders of a crm.read key should treat the tool as a bulk-export surface and grant it only to integrations that actually need cohort walks. For one-off lookups, prefer find_user_by_email or get_user.
Reading subscription state
current_plan is Stripe-shaped — it carries the timestamps an LLM needs to compose specific prompts directly, without composing them from indirect signals:
| Question | How to read it |
|---|---|
| Is the sub active? | current_plan != null && status in ['active', 'trialing'] |
| When's the next charge? | current_plan.next_billing_at |
| Are they about to churn? | current_plan.cancel_at_period_end == true (they clicked cancel; the sub stays active until next_billing_at) |
| When does the trial end? | current_plan.trial_ends_at (null when not trialing) |
| Are they paused? | current_plan.paused_at != null; resumes_at is the scheduled resume |
| Deep-link to Stripe? | current_plan.provider_subscription_id (Stripe sub_*, PayPal billing agreement id, etc.) |
current_plan: null means no active subscription — combine with plan_history to disambiguate the four customer states:
| State | Signal |
|---|---|
| Lead — never had a sub | current_plan: null AND plan_history: [] |
| Trial-only, never paid | current_plan: null AND every history row has is_trial: true |
| Trial → paid → quit | current_plan: null AND any history row has is_trial: false, status: "canceled" |
| Active subscriber | current_plan != null |
Trial conversion flips the same Subscription record's trial: true → false, so the is_trial field on a history row reflects what they were when the sub ended (i.e., a converted-then-quit user shows is_trial: false, not the original trial state).
For the absolute "did they pay?" answer including one-time purchases, use list_user_payments and check for any row with status: "paid".
Configuration
| Environment variable | Required? | Default | Purpose |
|---|---|---|---|
| USCREEN_API_KEY | yes | — | Secret key (usc_sk_live_*). The key gates which store the assistant sees. |
| USCREEN_API_BASE_URL | no | https://uscreen.io | Override for local development or staging |
Security
- API keys are sent as
Authorization: Bearer <token>over HTTPS. - v1 keys are read-only; rejecting any key with non-read scopes is enforced server-side.
- Keys are stored on Uscreen as SHA-256 digests, never plaintext — same model Stripe and GitHub use. Lost keys cannot be recovered; revoke and re-create.
Scopes
A key carries one or more scopes; missing-scope requests return 403. The MCP server registers every tool unconditionally — scope enforcement is per-request on the server side.
content_analytics.read— gates the content analytics endpoints (top videos, view + watch-time aggregates, content performance).finance_analytics.read— gates the finance analytics endpoints (MRR, churn, ARPU, plan revenue, signup growth). A content recommender has no business reading MRR; a CFO dashboard has no business reading view counts. Issue them independently.crm.read— gates the user-detail endpoints (/developer/v1/users/*). Reads individual user profiles (leads, paid subscribers, one-time buyers), including: contact info, subscription and billing history, all custom field values, email communication history (broadcasts and transactional — system messages, automation drips, password resets), content watch history, and comments. Grant only to trusted integrations.
Multiple scopes may be present on a single key.
Development
pnpm install
pnpm --filter @uscreentv/mcp build
pnpm --filter @uscreentv/mcp test
pnpm --filter @uscreentv/mcp dev # watch modeSmoke-test against a live server:
USCREEN_API_KEY=usc_sk_live_... \
USCREEN_API_BASE_URL=http://localhost:3000 \
SMOKE_CONTENT_ID=42 \
[email protected] \
pnpm dlx tsx packages/mcp/test/smoke.tsUser-detail tools require crm.read on the key. Omit SMOKE_USER_EMAIL (and SMOKE_USER_ID) to skip them and exercise only analytics.
The smoke runner prints a truncated JSON slice of each response to stderr — by design, useful when iterating against a local server. Do not run smoke against a usc_sk_live_* key in a shared or recorded terminal (CI logs, pair sessions, screen-share). The slices include user names, primary emails, store name, and plan names. For local-only localhost:3000 runs it's fine.
What's not in v1
- Catalog reads (videos, collections) — the Rails endpoints are not yet built.
- Entitlement checks for end-users.
- Checkout session creation.
- Sandbox tenants for end-to-end testing.
These are tracked in the Uscreen Headless plan and will land as separate releases.
