MCP Integration
Plug Sharp Leagues into your own LLM. No platform API costs.
Overview
Sharp Leagues exposes a read/write Model Context Protocol server at /api/mcp/http using the HTTP transport only. Bring your own model — Claude Desktop, Claude Code, or any MCP-aware client that can connect over HTTP. Tools are scoped to the authenticated user via a per-user bearer token; reads require league membership and writes go through a two-step previewPick → submitPick flow with a single-use confirmation token. For pick-status questions, start with getMyPickStatus. For league discovery, ask the assistant to run listMyLeagues first.
Operational limits: each user may hold at most 10 active MCP tokens. The MCP route rate-limits failed or missing auth to 20 requests per minute per IP and authenticated traffic to 120 requests per minute per token.
Setup
- Generate a token on /ai-assistant. Copy it immediately — it is only shown once.
- Add Sharp Leagues to your MCP client using one of the snippets below. Replace
YOUR-DOMAINandYOUR_TOKEN_HEREwith real values. - Use the HTTP endpoint only:
/api/mcp/http. - Restart your client. The Sharp Leagues tools should appear in the tool list.
Claude Desktop
Append to claude_desktop_config.json:
{
"mcpServers": {
"survivor": {
"type": "http",
"url": "https://YOUR-DOMAIN/api/mcp/http",
"headers": {
"Authorization": "Bearer mcp_YOUR_TOKEN_HERE"
}
}
}
}Claude Code CLI
claude mcp add --transport http survivor \
https://YOUR-DOMAIN/api/mcp/http \
--header "Authorization: Bearer mcp_YOUR_TOKEN_HERE"Tools
All tools require a valid bearer token. Tools that take a leagueId verify the caller is a member of that league — otherwise they return a permission error. Use getMyPickStatus first for pick-status questions, previewPick with optional entryId when a league has multiple active entries, and getMatchupSchedule with exactly one of leagueId or contestSlug.
listMyLeagues
List every league available to the authenticated user, including contest, sport, role, entry counts, and paused or settled state.
Input
{}Output
{
leagues: Array<{
leagueId: number;
leagueName: string;
contestSlug: string;
contestName: string;
sportCode: string;
sportName: string | null;
memberRole: "commissioner" | "member";
entryCount: number;
activeEntryCount: number;
isPaused: boolean;
isSettled: boolean;
}>;
}getRemainingTeams
List teams the authenticated user has not yet used in their active entry(s) in the given league.
Input
{ leagueId: number }Output
{
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
entries: Array<{
entryId: number;
entryNumber: number;
remainingTeams: Array<{
id: number;
externalTeamId: string;
abbreviation: string;
name: string;
city: string;
}>;
}>;
}getMatchupSchedule
Upcoming games over the next N weeks (default 4), using either the given leagueId or contestSlug for sport and contest context. Each returned game includes the numeric gameId accepted by previewPick and previewSlatePicks. If the initial window is empty but future games still exist, the window is re-anchored to the next scheduled game so the result does not return empty until the schedule is exhausted.
Input
// Provide exactly one of leagueId or contestSlug:
{ leagueId: number; weeksAhead?: number }
{ contestSlug: string; weeksAhead?: number } // weeksAhead 1..22, default 4Output
{
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
games: Array<{
gameId: number;
externalGameId: string;
gameDate: string;
startsAt: string;
status: "scheduled" | "live" | "final" | "postponed" | "canceled" | "suspended";
periodKey: string;
weekKey: string;
week: number | null;
home: { teamId: number; externalTeamId: string; abbr: string } | null;
away: { teamId: number; externalTeamId: string; abbr: string } | null;
spread: number | null;
moneylineHome: number | null;
moneylineAway: number | null;
}>;
}getUsedTeams
Teams already used across the authenticated user's entries in the given league.
Input
{ leagueId: number }Output
{
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
used: Array<{
entryId: number;
teamId: number;
externalTeamId: string;
abbreviation: string;
usedAt: string;
periodKey: string;
weekKey: string;
}>;
}getLeagueContext
League, contest, sport, module, config, alive count, lives distribution, and current-period pick distribution (respects pickReveal).
Input
{ leagueId: number }Output
{
league: { id: number; name: string };
contest: { id: number; slug: string; name: string };
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
config: LeagueRuntimeConfig;
aliveCount: number;
livesDistribution: Record<string, number>;
pickDistribution: Record<string, number>;
currentPeriodKey: string;
currentWeekKey: string;
}getMyPickStatus
Recommended first call for pick-status questions. For each active entry in a league or standalone contest, returns whether the caller has already picked this period and the server-computed deadline enforced by submitPick or submitSlatePicks.
Input
// Provide exactly one of leagueId or contestSlug:
{ leagueId: number }
{ contestSlug: string }Output
{
league: { id: number; name: string } | null;
contest: { id: number; slug: string; name: string; seasonId: number };
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
entries: Array<{
entryId: number;
entryNumber: number;
currentPeriodKey: string;
deadline: string | null;
deadlinePassed: boolean;
deadlineReason: "no_remaining_schedulable_games" | null;
hasPicked: boolean;
pick: {
teamId: number;
teamAbbr: string;
lockedAt: string | null;
} | null;
}>;
}getMyPickHistory
All picks for the authenticated user's entries in the given league, with results and snapshots.
Input
{ leagueId: number }Output
{
picks: Array<{
pickId: number;
entryId: number;
pickDate: string;
teamId: number;
teamAbbr: string;
teamExternalId: string;
result: "pending" | "win" | "loss" | "void" | "draw" | "forfeit";
spreadSnapshot: string | null;
moneylineSnapshot: number | null;
autoAssigned: boolean;
lockedAt: string | null;
gradedAt: string | null;
gameStartsAt: string;
gameDate: string;
periodKey: string;
weekKey: string;
}>;
}previewPick
Validate a proposed survivor pick and return a short-lived HMAC-signed confirmation token plus a human summary. Does not submit the pick. Errors on weekly pick'em/slate contests and routes you to previewSlatePicks/submitSlatePicks instead.
Input
// Exactly one of leagueId or contestSlug:
{ leagueId: number; teamId: number; gameId: number; entryId?: number; tiebreakerPrediction?: number }
{ contestSlug: string; teamId: number; gameId: number; tiebreakerPrediction?: number }Output
{
confirmationToken: string;
expiresAt: string;
summary: string;
periodKey: string;
weekKey: string;
contest: { id: number; slug: string; name: string; seasonId: number };
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
}previewSlatePicks
Validate 1-16 weekly slate entries for a pick'em contest, dedupe duplicate gameId entries last-write-wins, and return one short-lived confirmation token for the whole slate. Errors on survivor contests and routes you back to previewPick.
Input
// Exactly one of leagueId or contestSlug:
{ leagueId: number; entries: Array<{ gameId: number; teamId: number }>; entryId?: number }
{ contestSlug: string; entries: Array<{ gameId: number; teamId: number }>; entryId?: number }Output
{
confirmationToken: string;
expiresAt: string;
summary: Array<{
gameId: number;
teamId: number;
summary: string;
replacesExistingPendingPick: boolean;
}>;
contest: { id: number; slug: string; name: string; seasonId: number };
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
}submitPick
Consume a previewPick confirmation token and submit the survivor pick atomically. Token is single-use. Errors on weekly pick'em/slate contests and routes you to previewSlatePicks/submitSlatePicks instead.
Input
{ confirmationToken: string }Output
{
pickId: number;
periodKey: string;
weekKey: string;
lockedAt: string | null;
contest: { id: number; slug: string; name: string; seasonId: number };
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
}submitSlatePicks
Consume a previewSlatePicks confirmation token and submit the full weekly slate once. Returns per-game saved and rejected results.
Input
{ confirmationToken: string }Output
{
saved: number[];
rejected: Array<{ gameId: number; reason: string }>;
}getFutureValueHeatmap
Team by period grid of point spreads and ESPN FPI win probabilities (0-100) for upcoming games, excluding teams the user has already used. Each cell includes the numeric gameId accepted by previewPick and previewSlatePicks.
Input
{ leagueId: number }Output
{
sport: { id: number; code: string; name: string | null };
module: { slug: string; displayName: string; sportCode: string };
heatmap: Record<
string,
Record<
string,
{
gameId: number;
spread: number | null;
fpiWinPct: number | null;
opponentAbbr: string;
isHome: boolean;
}
>
>;
}startEntry
Start a new entry for the authenticated user in the given contest. Omit leagueId for the default standalone flow, or pass leagueId to create an entry in a specific league.
Input
{ contestSlug: string; leagueId?: number }Output
{
entryId: number;
leagueId: number;
status: "active" | "won" | "lost" | "cancelled";
contestId: number;
contestSlug: string;
created: boolean;
}joinLeagueByInviteCode
Join a private or public league via invite code and create a new entry for the authenticated user. The code alone is enough — contestSlug is optional (derived from the league) and no password is needed.
Input
{ inviteCode: string; contestSlug?: string; password?: string }Output
{
entryId: number;
leagueId: number;
leagueName: string;
entryNumber: number;
contestSlug: string;
}addEntryToLeague
Add one of the authenticated user's existing entries to a compatible league by id.
Input
{ entryId: number; leagueId: number }Output
{
entryId: number;
leagueId: number;
leagueName: string | null;
entryNumber: number;
promotedPrimaryLeague: boolean;
status: string;
}listCompatibleLeaguesForEntry
List leagues that the given entry can join without recomputing compatibility rules client-side.
Input
{ entryId: number }Output
{
entryId: number;
leagues: Array<{
leagueId: number;
leagueName: string;
isPrivate: boolean;
maxMembers: number;
}>;
status: string;
}listMyEntries
List all entries for the authenticated user across contests, optionally filtered by status.
Input
{ status?: "active" | "won" | "lost" | "cancelled" }Output
{
entries: Array<{
entryId: number;
status: "active" | "won" | "lost" | "cancelled";
leagueId: number | null;
leagueName: string | null;
entryNumber: number;
livesRemaining: number;
livesMax: number;
currentStreak: number;
maxStreak: number;
startedAt: string;
endedAt: string | null;
contestId: number;
contestSlug: string;
contestName: string;
sportCode: string;
sportName: string | null;
}>;
}getEntryStatus
Get detailed status for a single entry. Errors if the entry does not belong to the caller.
Input
{ entryId: number }Output
{
entryId: number;
status: "active" | "won" | "lost" | "cancelled";
leagueId: number | null;
entryNumber: number;
livesRemaining: number;
livesMax: number;
currentStreak: number;
maxStreak: number;
eliminatedAtWeek: string | null;
startedAt: string;
endedAt: string | null;
contestSlug: string;
contestName: string;
sportCode: string;
}cancelEntry
Cancel an active entry owned by the authenticated user.
Input
{ entryId: number }Output
{ entryId: number; status: "cancelled"; endedAt: string }listContests
List contests available on the platform, optionally filtered by sport code or active-only.
Input
{ sportCode?: string; activeOnly?: boolean }Output
{
contests: Array<{
contestId: number;
slug: string;
name: string;
active: boolean;
startsAt: string | null;
endsAt: string | null;
sportCode: string;
sportName: string | null;
}>;
}listPublicLeagues
Discover public (non-private), unsettled leagues the caller has not yet joined.
Input
{ contestSlug?: string; limit?: number } // limit 1..100, default 25Output
{
leagues: Array<{
leagueId: number;
leagueName: string;
maxMembers: number;
pausedAt: string | null;
createdAt: string;
contestSlug: string;
contestName: string;
sportCode: string;
}>;
}getLeagueStandings
Ranked standings for every entry in a league. Requires membership.
Input
{ leagueId: number }Output
{
standings: Array<{
rank: number;
entryId: number;
userPublicId: string;
displayName: string | null;
username: string;
status: "active" | "won" | "lost" | "cancelled";
livesRemaining: number;
livesMax: number;
currentStreak: number;
maxStreak: number;
entryNumber: number;
eliminatedAtWeek: string | null;
isAlive: boolean;
}>;
}getGlobalLeaderboard
Top active entries across all leagues for a contest, ranked by current streak then max streak.
Input
{ contestSlug: string; limit?: number } // limit 1..200, default 50Output
{
contest: { id: number; slug: string; name: string };
leaderboard: Array<{
rank: number;
entryId: number;
userPublicId: string;
displayName: string | null;
username: string;
status: "active";
livesRemaining: number;
currentStreak: number;
maxStreak: number;
leagueId: number | null;
startedAt: string;
}>;
}createLeague
Create a new league for the given contest. Caller becomes commissioner and first member.
Input
{
contestSlug: string;
name: string;
maxMembers?: number;
isPrivate?: boolean;
password?: string;
config?: Partial<LeagueRuntimeConfig>;
}Output
{
leagueId: number;
inviteCode: string;
inviteUrl: string;
contestSlug: string;
name: string;
}getLeagueInviteCode
Return the invite code for a league. Commissioner-only.
Input
{ leagueId: number }Output
{ inviteCode: string; name: string; isPrivate: boolean; inviteUrl: string }pauseLeague
Pause the given league (commissioner-only). Blocks new picks until resumed.
Input
{ leagueId: number }Output
{ leagueId: number; paused: true; at: string }unpauseLeague
Resume a paused league (commissioner-only).
Input
{ leagueId: number }Output
{ leagueId: number; paused: false; at: string }getGameResults
Recent final game results for a sport with optional date range. Public data, no league scoping.
Input
{ sportCode: string; startDate?: string; endDate?: string; limit?: number }Output
{
sport: { id: number; code: string; name: string | null };
games: Array<{
gameId: number;
externalGameId: string;
gameDate: string;
startsAt: string;
home: { id: number; externalTeamId: string; abbreviation: string } | null;
away: { id: number; externalTeamId: string; abbreviation: string } | null;
homeScore: number | null;
awayScore: number | null;
winnerTeamId: number | null;
isDraw: boolean;
}>;
}getTeamUsageStats
Per-team count of how many league members have already burned each team. Requires membership.
Input
{ leagueId: number }Output
{
sport: { id: number; code: string; name: string | null };
aliveCount: number;
stats: Array<{
teamId: number;
teamAbbr: string;
externalTeamId: string;
timesUsed: number;
}>;
}getEliminationFeed
List of eliminated or cancelled entries in a league ordered by most recently ended. Requires membership.
Input
{ leagueId: number; limit?: number } // limit 1..200, default 50Output
{
eliminations: Array<{
entryId: number;
userPublicId: string;
displayName: string | null;
username: string;
status: "lost" | "cancelled";
entryNumber: number;
eliminatedAtWeek: string | null;
endedAt: string;
endedGameDate: string | null;
maxStreak: number;
}>;
}getMyProfile
Account profile for the authenticated user.
Input
{}Output
{
publicId: string;
username: string;
displayName: string | null;
email: string;
timezone: string;
role: "user" | "admin";
createdAt: string;
}getMyNotifications
Recent notifications for the authenticated user. Pass unreadOnly to filter to pending only.
Input
{ limit?: number; unreadOnly?: boolean }Output
{
notifications: Array<{
id: number;
type: string;
channel: string;
status: "pending" | "sent" | "failed";
sentAt: string | null;
createdAt: string;
payload: unknown;
}>;
}getMyMcpTokens
List the authenticated user's MCP tokens (label, createdAt, lastUsedAt, revokedAt, no raw token).
Input
{}Output
{
tokens: Array<{
id: number;
label: string;
createdAt: string;
lastUsedAt: string | null;
revokedAt: string | null;
}>;
}Examples
Multi-tool prompts you can paste into your client:
List leagues
"List all my leagues on Sharp Leagues, including league IDs, contests, sports, my role, and active entry counts."
Compare NFL leagues
"Compare my NFL survivor leagues. Show active entries, league settings, remaining teams, and visible pick trends."
Pick history
"Show my pick history for league 42, including results, picked teams, game dates, and pending picks."
Pick status first
"Call getMyPickStatus for league 42 and tell me which entries still need a pick this period, what the enforced deadline is, and whether any pick is already locked in."
Preview pick
"Preview a pick for league 42 using entryId 314, team 12, and game 345. Explain the matchup and any risk before I decide."