SHARPLEAGUES

MCP Integration

Plug Sharp Leagues into your own LLM. No platform API costs.

Looking for setup instructions? AI Assistant is the primary setup hub for token creation, Claude, ChatGPT, safety notes, and example prompts. This page remains as the technical MCP reference.

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

  1. Generate a token on /ai-assistant. Copy it immediately — it is only shown once.
  2. Add Sharp Leagues to your MCP client using one of the snippets below. Replace YOUR-DOMAIN and YOUR_TOKEN_HERE with real values.
  3. Use the HTTP endpoint only: /api/mcp/http.
  4. 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 4

Output

{
  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 25

Output

{
  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 50

Output

{
  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 50

Output

{
  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."