A remote MCP server at mcp.getroster.com exposing eleven curated, read-only
tools over the existing v2 public API, fronted by a new OAuth 2.1 authorization
service that reuses Brand Portal login for consent. Launched as a documented
custom-connector URL brands add to Claude themselves — no tokens, no Anthropic
approval gate.
Brands increasingly work inside Claude (claude.ai, Claude Cowork) and want their Roster data there — today their only options are manual CSV exports or hand-rolled scripts against the v2 API with pasted tokens. Claude's hosted surfaces don't support static API keys at all, so without an OAuth-capable MCP server, Roster simply cannot be connected to Claude.
A decoupled MCP + OAuth stack that acts as a normal client of the existing v2 API. Brands click Add custom connector, log in with their existing Brand Portal credentials, approve read-only access, and ask Claude things like "what's my ambassador program ROI this quarter?" — the #1 churn-cited gap.
| Goal | Metric | Target |
|---|---|---|
| Adoption | Brands with an active Claude connection | 20 brands within 60 days of launch |
| Engagement | Brands with ≥1 MCP tool call in trailing 7 days | 50% of connected brands |
| Retention signal | Connected brands citing ROI-visibility in cancellation requests | Directional decrease (qualitative; baseline 27%) |
| Trust | Tool results disagreeing with portal report numbers | 0 known discrepancies — numbers come from the same API the portal uses |
The MCP + OAuth stack is fully decoupled from Roster core: it's a normal API client with no database or shard access. The dashed green path runs once per connection (the OAuth grant and credential bridge); the solid path runs on every tool call.
Solid grey = per-tool-call request path. Dashed green = one-time OAuth grant: Claude
discovers the authorization service via RFC 9728/8414 metadata, registers via DCR,
the user logs in with portal credentials and approves; the service mints its own
short-lived JWT + rotating refresh token for Claude and bridges the grant to a Roster
ApiSession held server-side only.
All tools are scoped to the connected brand — no brand parameter; identity rides
on the token, making cross-tenant leaks structurally impossible at the tool layer. Date filters
are explicit ISO dates — Claude resolves relative ranges in the user's timezone
before calling (Decision 5); omitted dates default to UTC last 30 days to match
the portal. Data must come from the same v2 API paths the portal reports use — never recomputed in
the MCP layer. Every tool declares readOnlyHint: true. Full per-tool definitions (input
schemas, response shapes, upstream mappings): tool-specs.html.
Portal tie-back (required on every report/dashboard tool): the response
includes a portal_source object — the human name of the portal surface it mirrors
("Program Dashboard", "Sales Attribution report"), the resolved date range, and a deep link to
the corresponding portal page (e.g. app.getroster.com/programs/{id}/dashboard) —
so a brand can verify any number in one click and always knows exactly where it came from. The
catalog deliberately favors report/dashboard-shaped tools over raw-row endpoints: each
performance tool maps 1:1 to a named portal surface.
| # | Tool | What it returns / key params | Use case |
|---|---|---|---|
| 1 | list_programs | Programs with member counts, applicant counts, join requirements. Cheap — and Claude needs it to resolve "my VIP program" → program_id for other tools' filters. |
Id resolution for filtered queries |
| 2 | get_program_performance NEW API | The portal's Program Dashboard for one program (/programs/{id}/dashboard), mirroring the brand's saved dashboard configuration (Decision 6): the same cards the brand sees when they log in (typically applicants, members, first-time logins, post mentions/engagements/impressions, EMV, points, referred revenue), each with a time series, period total, and delta vs the preceding period. Params: program_id, start_date, end_date (default last 30 days, matching the dashboard), optional metrics filter. Runs the same per-card queries as the dashboard — no new SQL. |
Program health / ROI narrative with exact portal tie-back — primary use case |
| 3 | list_campaigns | Campaigns with status, dates, participant counts. | Navigation / id resolution |
| 4 | get_campaign_performance NEW API | Single campaign, mirroring the portal's campaign overview (/campaigns/{id}/analytics/overview — Decision 7; the all-campaigns "Campaign Performance Dashboard" is explicitly not used): invite funnel (added, emails sent/opened, joined, completed, participation/completion rates), content generated (posts/stories/uploads with likes/comments/views), reach and engagement by network, EMV, and reward fulfillment counts. No attributed revenue (Decision 8 — not a Roster concept at campaign level; revenue questions route to get_sales_attribution_report). |
Campaign post-mortems & comparisons |
| 5 | list_ambassadors NEW API | Paginated contact search (wraps the portal's contact search): query (name/email), program_id, tag, status, joined_after/before, sort (referral_revenue | posts | joined_date | followers | engagement — no EMV sort, Decision 9; EMV leaderboards come from get_social_posts_report). Full fields: email, phone, social handles with follower counts, tags, custom properties, program memberships, lifetime referral revenue. The existing /v2/contacts has no search/sort/performance fields. |
Leaderboards; list export to SMS/Klaviyo |
| 6 | get_ambassador NEW API | Single contact by id or email, mirroring the portal's contact detail page: profile, socials, tags, custom properties, program memberships plus performance (attributed orders/revenue, total commission, posts + engagement, rewards earned/redeemed, referral links and discount codes, last activity date). The existing /contact/{contactId} endpoint carries no performance data and isn't useful on its own. |
Ambassador deep-dive, "who went quiet?" |
| 7 | get_sales_attribution_report NEW API | The portal's Sales Attribution report: one row per ambassador with activity in range — link clicks, new customers, referred orders/revenue, commissions and points, personal orders/revenue — plus grand totals. Params: start/end_date, program_id, contact_id, tag, attribution_methods (emailAddress | rewardCode | referralLink | discountCode | recurringOrder — rows always carry both referred and personal columns, matching the portal), sort. Aggregate report rows, not raw orders. |
Attribution drill-down, revenue questions, exec reporting |
| 8 | get_social_posts_report NEW API | The portal's Social Posts report: one row per ambassador who posted in the range (Decision 9 — portal parity; per-post rows are get_social_feed_posts), with post/story counts, reach, impressions, likes, comments, shares, saves, engagement rate, and EMV (confirmed available — stored per-post in UserSocialListeningPostEMV), plus grand totals. Params: start/end_date, platform, program_id, campaign_id, contact_id, tag, sort (any metric incl. emv), pagination. |
Content analysis, EMV leaderboards, campaign comparisons |
| 9 | get_social_feed_posts NEW API | Recent posts from the portal's Social Feed: post URL/media, caption, platform, post type (post/story/reel), ambassador, program, posted date, engagement metrics and per-post EMV. Params: start/end_date, platform, program_id, contact_id, sort, pagination. |
"What are my ambassadors posting right now?" — live-feed complement to the report view |
| 10 | search_help_docs | Keyword search over the published help-docs corpus (docs.getroster.com), returning excerpts + canonical URLs. | Setup/troubleshooting copilot; deflects "how do I…" mid-analysis |
| 11 | get_connection_info | No params. Returns the connected brand (name, domain), the authorizing user, the access scope (read-only), and grant date. | Brand-context visibility for multi-brand users — "which brand is this connection?" — and a cheap connectivity check |
NEW API = requires a new v2 Open API endpoint (confirmed scope — Decision 2; the full audited list E1–E9 with sizes is in tool-specs §13). Dropped from Phase 1 (revisit on demand): list_orders and list_commissions — raw-row tools superseded by get_sales_attribution_report for the aggregate need; the commission-override audit ask (Intercom conv 61134) is the one mined use case this leaves uncovered, and list_commissions is the Phase 2 answer if it recurs.
POST /mcp per current MCP spec (2025-11-25 revision). No SSE-only legacy transport.401 with WWW-Authenticate pointing at protected-resource metadata (RFC 9728), also served at /.well-known/oauth-protected-resource.truncated: true marker plus guidance text ("narrow the date range or filters") when limits hit.title, an LLM-audience description (when to use it, what it returns), and readOnlyHint: true. No tool in Phase 1 may perform a write.https://api.getroster.com/ v2 with the brand credential from the OAuth bridge. It is a normal API client — no direct DB access, no shard awareness.api-brand-portal (App.WebApi.Open), not by side-channels. The audit is complete: 5 new endpoints + 2 small modifications (E1–E9, specified in tool-specs §13) are part of this PRD's REST API scope.Concrete implementation blueprint (stack, libraries, consent sequence, storage, revocation, environments): the MCP & OAuth writeup. The blueprint recommends serving the AS and the MCP server from the single host mcp.getroster.com.
/.well-known/oauth-authorization-server, advertising code_challenge_methods_supported: ["S256"].https://claude.ai/api/mcp/auth_callback (hosted surfaces) and loopback http://localhost:* (Claude Code), and store whatever DCR registers./authorize stores the pending request and redirects to a new app.getroster.com/connect/claude route in the Brand Portal SPA. Login uses the existing portal login (password, social SSO, lockout) — the OAuth service never handles credentials, SSO-required brands can connect, and already-logged-in users skip straight to consent.A Roster login is user-level; API tokens are brand-level. The OAuth grant is the bridge between the two:
AccessToUserId model). The grant — and the bridged brand-specific ApiSession — are permanently bound to that brand.get_connection_info returns the active brand, and every tool response includes a top-level brand field, so multi-brand users always see which context they're in.brand parameter on tools, which would forfeit the isolation guarantee.ApiSession token created for the selected brand (via the same path as UserAccessTokenService / TokenApiController, consistent with existing private-token lifetimes — Decision 1), tagged as MCP-issued, stored encrypted, and never returned to the MCP client.ApiSession row immediately. claude.ai sends no signal on disconnect (verified 2026-06-12, D-11) — orphaned grants are reaped by a daily idle sweep after 35 days without token activity.ApiSession tokens distinguishable from customer-created tokens (recommended: nullable SourceTypeId column on Global ApiSession — trade-off vs a new AccessTypeId in MCP & OAuth writeup §3.3) so they can be listed, rate-limited, and revoked independently.https://mcp.getroster.com/mcp) on claude.ai / Desktop / Cowork; Team/Enterprise note that an org Owner must add it, then each member connects; Free-plan note (one custom-connector slot); what data Claude can and cannot access (read-only, single brand); how to disconnect from either side; 5 example prompts mapped to the top use cases — starting with the program-performance prompt.AccessId on ApiSession/ApiAccessRight; new OAuth grant store (clients from DCR, auth codes, refresh-token families, grant↔ApiSession mapping) — lives in the OAuth service's own storage if it's a separate stack, not necessarily Global DB.Four decisions on 2026-06-09; five more on 2026-06-10 from the tool-specs deep dive (details in tool-specs §14). Two questions remain open before sprint start; two were resolved by the deep dive's verification work.
CLIENT_API_TOKEN_EXPIRE_YEARS = 30 is the real behavior; same brand token in active use for four years). The credential bridge mints a standard long-lived private ApiSession per grant. The customer's 1-hour-JWT report (Intercom conv 215473520540411) does not reflect private-token behavior — worth a support follow-up, but not a design input.workers-oauth-provider is the fastest proven route (Linear, Sentry, and Intercom launched on it), and Roster already runs parts of its stack on Cloudflare — adopting it here is not a stretch. Eng makes the final call.get_program_performance mirrors the brand's saved Program Dashboard RESOLVEDUserPage/UserPageComponent); the new endpoint loads the brand's saved cards (default-template fallback) and runs the existing dashboard dispatcher SP per card — output matches exactly what the brand sees in the portal. No new SQL./dashboards/campaign-performance) must not be used or referenced anywhere in this project. get_campaign_performance is built from /campaigns and /campaigns/{id}/analytics/overview.get_campaign_performance; the tool description routes revenue questions to get_sales_attribution_report.get_social_posts_report returns per-ambassador rollups (portal parity; per-post rows live in get_social_feed_posts). list_ambassadors cannot sort by EMV (not in the search index) — the EMV leaderboard is get_social_posts_report with sort: emv.USER_SETTING_SSO_REQUIREMENT reject password auth outright (isSSOError), so an OAuth-service-rendered login form can't serve them — and it would put portal passwords on a second surface. Instead, /authorize redirects to a new app.getroster.com/connect/claude portal route: existing login (password + SSO), consent + brand picker in the portal, one-time connect ticket back to the OAuth service. Supersedes the Worker-rendered consent screen in this PRD's earlier draft and the writeup's original §4.2.brand parameter on tools (see Brand context model).RateLimitService); the limit value (default 100/interval, subscription item 149) is per brand. A bridged MCP ApiSession token automatically gets its own pool and cannot starve a brand's existing integration token.UserSocialListeningPostEMV stores per-post EMV (Ayzenberg components + computed total); brand-customizable rates in EmvPlatformDefault/UserEmvConfig. Exposed via the new report endpoints (E1, E7, E8).Gated server-side by a brand allowlist in the OAuth service — consent fails closed with a friendly "not enabled for your account yet" for non-allowlisted brands. No portal feature flag needed in Phase 1.
Connect Claude (claude.ai + Cowork + Claude Code) to a demo brand; run the 10-use-case prompt suite from research.md; verify report-number parity against the portal for the same date ranges.
Hand-picked from active API/export askers in Intercom evidence, plus at least one agency. CSM-guided setup; weekly check-ins; watch usage logs.
Publish the help article, announce in-product/newsletter, arm CSMs. Begin Phase 2 (directory submission) prep in parallel.
Disable the OAuth /authorize endpoint (no new grants) and/or revoke all bridged
ApiSession tokens to cut access instantly. The MCP service is fully decoupled —
turning it off cannot affect the portal or existing API customers.
initialize → tools/list → each of the 11 tools returns schema-valid results.On claude.ai (Pro), Settings → Connectors → Add custom connector → enter URL → Roster login → consent shows read-only copy → Allow → back in Claude, run "How is my ambassador program performing over the last 30 days?" → numbers match the Program Dashboard for the same range, and the response cites its portal_source deep link.
User with 3 brands connects → brand picker shows exactly the brands they can access → pick brand B → all tool results are brand B only → disconnect in Claude → reconnect choosing brand C → results switch accordingly; brand B's bridged token is revoked.
With an active Claude session mid-conversation, revoke the connection in Brand Portal → next tool call fails with re-auth prompt, no stale data served → "Connected apps" row disappears.
Team org Owner adds the connector org-wide → a non-owner member connects with their own credentials → same member uses it in Cowork → tools work identically; a member without Brand Portal access for that brand cannot complete consent.
Request 5 years of orders → tool returns truncation/narrow-the-range guidance, not a timeout. Hammer tools past the rate limit → clean retryable error in Claude, portal/API usage for the brand unaffected.