npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cito-mcp

v0.5.8

Published

Standalone MCP server for the Cito esports and sports API — curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).

Readme

cito-mcp

Standalone MCP server for the Cito esports API — curated outcome tools for agents building esports apps, dashboards, bots, and research flows.

Version: 0.4.9 · Node: >=20 · Install: npx cito-mcp

Primary games & sports: lol · cs2 · dota2 · cod · ufc · tennis, plus RAF wrestling, BKFC bare knuckle and the non-UFC MMA archives (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction, and mma across all of them) through the combat_* tools. Long-tail REST stays available via call_api.


Philosophy: outcomes, not endpoints

v0.1 exposed ~100+ tools auto-generated from OpenAPI. Agents had to pick among thin path wrappers, invent IDs, and stitch multi-call UI screens themselves. That catalog was hard to select against and brittle across games.

Ships 42 curated outcome tools that answer jobs instead of mirroring REST:

| Job | Tool | | --- | --- | | What’s live right now? | live_matches | | What’s coming up? | upcoming_schedule | | Build a match card / recap | match_summary | | Deep timeline / live state | match_details | | Team page | team_profile | | Player / fighter profile | player_profile | | Player form & win streak | player_form | | Table / rankings | standings | | Ranking climbers & droppers | rankings_movers | | Pre-match briefing (with surface) | match_preview | | Event / fight-night card | event_card | | Rivalry record | head_to_head | | Tennis tournament catalog | tournaments | | One day of tennis | tennis_schedule | | Tennis player match log | player_matches | | Tennis career statistics | player_stats | | Tennis ranking trajectory | player_rankings_history | | Fighter / team photos | event_card · player_profile | | Which raw REST route exists? | list_routes | | Name → ID | resolve_entity / search_entities |

Composites multi-fetch server-side, return a stable JSON envelope, and isolate partial failures in partial[] so one bad secondary section does not fail the whole page.

MCP is design-time / agent assistance. Ship production backends with Cito REST + the user’s API key. Do not put multi-tenant traffic through this process.

Honest gaps, in-band

Where the source data is partial, the tool says so instead of shipping a plausible-looking wrong value:

| Situation | What the tool returns | | --- | --- | | The two ranking lists compared by rankings_movers are not a week apart | gapDays + comparisonWindow ("12-week gap"), comparisonsAreWeekly: false, and a warning naming both dates | | A tennis tournament draw is missing matches | draw.shortRounds (["R128 45/64"]), draw.missingRounds, draw.complete | | A stats section the endpoint does not publish | player_stats.unavailable names the section and why, rather than a block of nulls | | search_entities results that do not match at all | dropped, counted under data.relevance.droppedBelowFloor, with a warning naming them |


Install

One command (recommended)

npx cito-mcp install --key cito_your_key_here

Detects the MCP clients installed on your machine — Claude Code, Claude Desktop, Cursor, Windsurf, Codex CLI — and writes the config for each. Then restart your editor.

It is safe to re-run: existing config is merged, not replaced, your other MCP servers are left alone, a .cito-bak backup is written before the first change, and running it again just rotates the key rather than adding a second entry.

npx cito-mcp install --dry-run                     # show the plan, write nothing
npx cito-mcp install --key … --client cursor       # one client only
npx cito-mcp install --help

Get a key at citoapi.com/dashboard.

Manual — Claude Code

claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" npx -y cito-mcp

Quote the --. PowerShell 5.1 strips a bare -- before the CLI sees it; because -e takes a variable number of values it then swallows npx -y cito-mcp as env vars and fails with unknown option '-y'. The quoted form works in PowerShell, cmd, bash and zsh alike. npx cito-mcp install avoids the problem entirely.

From a local clone (development):

cd mcp
npm install
npm run build
claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" node "%CD%\dist\index.js"

Cursor

Add to Cursor MCP settings (e.g. .cursor/mcp.json or Cursor Settings → MCP):

{
  "mcpServers": {
    "cito": {
      "command": "npx",
      "args": ["cito-mcp"],
      "env": {
        "CITO_API_KEY": "cito_your_key_here"
      }
    }
  }
}

Windows local clone:

{
  "mcpServers": {
    "cito": {
      "command": "node",
      "args": ["C:/Users/Owner/Projects/cito-api-scraper-windows/mcp/dist/index.js"],
      "env": {
        "CITO_API_KEY": "cito_your_key_here"
      }
    }
  }
}

Windsurf / other MCP hosts

Same shape as Cursor: command + args + env.CITO_API_KEY. Prefer npx cito-mcp so clients pick up published patches without a local install.

The server exits immediately if CITO_API_KEY is missing. The key is sent only as the x-api-key header and is never logged.


Environment

| Variable | Required | Default | Description | | --- | --- | --- | --- | | CITO_API_KEY | yes | — | Cito API key → x-api-key on every upstream call | | CITO_API_BASE | no | https://api.citoapi.com/api/v1 | Override base URL (staging / self-hosted) |


Preferred tool order

  1. Unsure → list_capabilities (optional api_health for tier / included games)
  2. Name without ID → resolve_entity (chain) or search_entities (browse)
  3. Live / upcoming → live_matches / upcoming_schedule
  4. Match card → match_summary (then match_details if needed)
  5. Team / player pages → team_profile / player_profile
  6. Tables / rivalry / preview / event card → standings / head_to_head / match_preview / event_card
  7. Escape hatch → list_routes to find a path, then call_api (allowlisted paths only)

Never invent IDs — resolve them or take them from live / schedule / search results.

Mnemonic: resolve → live/schedule → summary → deep.


Tool catalog (16)

All tools are read-only. Names are snake_case with no cito_ prefix (the server name already brands the surface).

| Tool | Outcome | When to use | When not to use | Key inputs | | --- | --- | --- | --- | --- | | list_capabilities | Catalog of tools, games, jobs, recipes (no network) | Session start; map UI screens to tools; filter by game / job | You already know the tool and have IDs | game?, job?, q?, includeExamples?, includeRecipes? | | api_health | Reachability, key validity, tier, rate limits, optional game probes | Once per session; after 401/403/RATE_LIMIT; entitlement checks | Fetching live scores or entity data | includeGameProbes? | | resolve_entity | Name → best typed id/slug (+ ranked candidates) | User named “T1”, “s1mple”, “IEM Cologne” without an ID; chaining into profiles | Already have a stable id/slug; large browse lists | q, game?, type?, limit? | | search_entities | Paginated entity browse/search for one game | Typeahead / pickers; “list teams matching…” | Single-name resolve for chaining; full profile pages | game, q?, type?, limit?, cursor?, activeOnly? | | live_matches | Live board (one game or all); CS2 uses /cs2/live; includes UFC | “What’s live?”; ops/dashboard strip | Upcoming fixtures; historical recaps | game? (all or omit), limitPerGame?, includeLabelsOnly? | | upcoming_schedule | Upcoming fixtures/events with filters | “What’s on this week?”; team next matches | Live-only boards; deep match recaps | game, hours?, from?, to?, team?, league?, tournamentId?, limit?, cursor? | | match_summary | Composite match card: score, performances, media stubs | Default match UI / recap after a known matchId | No matchId yet; pure pre-match → match_preview; deep timelines | game, matchId, view?, includePlayerStats?, includeMedia? | | match_details | Deep sections: timeline, live state, maps, advanced | Analyst dive; live in-game window; full demo list | First-pass live board; simple chat recaps | game, matchId, gameId?, sections?, includeTimeline?, includeLiveState?, includeAdvanced? | | player_profile | Player/fighter + form/trends/radar when available | “How is X playing?”; player page scaffold | Full roster → team_profile; unresolved name | game, playerId or slug, recentLimit?, includeTrends?, view?, window?, role? | | team_profile | Team/org + roster + recent form | Team page / “who is on this roster?” | UFC fighters → player_profile; unresolved name; pure tables | game, teamId or slug, recentLimit?, includeFormer?, view? | | head_to_head | Composed H2H (no first-class REST H2H) | Rivalry / series record; preview context | Single-side form only; live scores; standings | game, sideA, sideB, entityType?, limit?, from?, to? | | standings | League/event tables or world/division rankings | Playoff picture; UFC rankings; CDL / CS2 tables | Team form; live scores; match recaps | game, scope?, leagueId?, tournamentId?, eventId?, season?, stage?, division?, limit? | | match_preview | Pre-match briefing: sides, rosters/form, H2H stub | Upcoming deep link; pick’ems; preview cards | Completed recaps; deep live state | game, matchId or (teamA + teamB), eventId?, includeH2H?, includeRosters?, recentLimit? | | event_card | Event / fight-night card: identity + bouts in card order (main event first), each corner with photos, record, nickname, weight class; optional rankings. Tennis returns the draw bracket with per-round completeness | UFC card, CS2 event hub, tournament overview | Live-only strip; single match recap | game, eventIdOrSlug or q, includeBouts?, includeStandings?, limit?, drawMaxMatches? | | list_routes | Index of raw REST routes from the live OpenAPI spec (method, path, summary, tag) | Finding a long-tail path before call_api; checking an endpoint exists | A curated tool covers the outcome | game?, q?, limit? | | call_api | Allowlisted raw REST (data.raw) | Fortnite / long-tail paths; payload debugging | Any job covered by a curated tool | path, method?, queryJson?, bodyJson? |

Images

Fighter and team sides carry an images object wherever upstream supplies one — on event_card bout corners and on player_profile. No extra call, no N+1.

"team1": {
  "name": "Islam Makhachev",
  "slug": "islam-makhachev",
  "record": { "wins": 28, "losses": 1, "draws": 0, "text": "28-1-0 (W-L-D)" },
  "championStatus": "champion",
  "images": {
    "headshotUrl":     "https://…/MAKHACHEV_ISLAM_BELT_01-18.png",
    "bodyImageUrl":    "https://…/athlete_bio_full_body/…",
    "imageUrl":        "https://…/event_fight_card_upper_body/…",
    "proxiedImageUrl": "https://api.citoapi.com/api/v1/public/images/ufc/aHR0cHM6…"
  }
}

Use proxiedImageUrl in a browser. the image host sends no CORS header and can hotlink-block, so raw URLs render as broken images in a web UI. The proxied URL is served by the API and is safe to put in an <img src>.

When a key is null the image does not exist for that entity; when the whole images object is absent, upstream sent nothing for that side. The object is never partially shaped — if any image exists, all four keys are present.

Tennis is the exception on purpose: the feed supplies a single portrait_url (Wikimedia Commons) and no square crop, so that one URL fills both headshotUrl and imageUrl, and proxiedImageUrl stays null because only fighter image URLs have a proxy.

Tennis images carry their licence and credit — and a UI is expected to render them:

"images": {
  "headshotUrl": "https://commons.wikimedia.org/wiki/Special:FilePath/Hoang_WMQ18_%2833%29_%2842647295495%29.jpg?width=400",
  "license":     "CC BY-SA 2.0",
  "attribution": "si.robi",
  "sourceUrl":   "https://commons.wikimedia.org/wiki/File:Hoang_WMQ18_(33)_(42647295495).jpg"
}

Commons portraits are mostly CC BY / CC BY-SA, which for commercial display generally requires naming the author and stating the licence. attribution is null for public-domain and CC0 images, which need no credit — that is a fact, not missing data. Anything without a defensible licence has its URL removed upstream, so a non-null headshotUrl always has a license beside it.

Games

| Game | Depth | Notes | | --- | --- | --- | | lol | deep | Team slugs; lol-match-*; no native global search | | cs2 | deep | Prefixed ids; live path GET /cs2/live; trends / rankings | | dota2 | thin | Numeric ids; radar; weak/no standings & roster endpoints | | cod | medium | Org slugs; CDL standings; UUID match ids | | ufc | medium | Fighter slugs; boutId; live + rankings. Includes Dana White's Contender Series and Road to UFC (2022-2026, all 19 cards with official round stats; event slugs like road-to-ufc-2-7, titles "Road to UFC 2.7") |

Combat leagues (combat_*, league argument)

RAF, BKFC and the MMA archives are not on the game enum. combat_events, combat_card, combat_bout, combat_athlete, combat_h2h and combat_rankings take league: "raf" | "bkfc" | "bellator" | "pride" | "strikeforce" | "wec" | "dream" | "elitexc" | "affliction" | "mma", and call_api reaches each of those prefixes. None of them is ever mixed into UFC answers.

| League | Scoring | Notes | | --- | --- | --- | | raf | per PERIOD (wrestling points, takedowns) | Athletes; champions per division | | bkfc | per ROUND (punches landed/thrown, knockdowns) | Fighters; official rankings + champions | | MMA archives: bellator, pride, strikeforce, wec, dream, elitexc, affliction, mma | per ROUND, official stats in UFC's shape ("13 of 18", controlTime "1:42", round 0 = whole fight; knockdowns, submission attempts, reversals) | Closed archives on one API router with identical routes: Bellator 131-172 (2014-2017), PRIDE 1997-2007 (PRIDE rules: 10-minute first round, then 5-minute rounds), Strikeforce 2006-2013, WEC 2001-2010, DREAM 2008-2012, EliteXC 2007-2008, Affliction 2008-2009. mma is every promotion at once: a fighter's whole non-UFC career, with recordByPromotion on combat_athlete, and recent / last across promotions; every bout and event carries its real promotion. Event slugs like bellator-142, pride-34, wec-48, strikeforce-barnett-vs-cormier; bout ids <promotion>-<fightId> (a bare fight id resolves in any archive). A fighter we also hold a UFC record for keeps the same slug as /ufc/fighters/{slug} (ufcFighterSlug). No live data, rankings or upcoming cards: combat_events when:"upcoming" returns [] with a note (default is recent), combat_card event:"last" is the most recent card (the default) and "next" is a non-retryable NOT_IMPLEMENTED, combat_rankings is a non-retryable NOT_IMPLEMENTED. All of it is older than the Free (30-day) / Starter (90-day) history window, so its stats are Pro: on those plans round stats come back as a HISTORY_WINDOW_EXCEEDED partial note while results still return. |

UFC projection notes (0.2.4)

Hardened agent-facing UFC shapes (offline-tested):

  • normalizeMatch reads fighters[] (corner + fighterName / profile.name), live red/blue, and boutId / dataId / bare id. Never keeps label ? vs ? when fighter names exist (including blue-first arrays).
  • live_matches uses extractLiveRows: prefer liveBouts (empty array = honest empty board); never promote supervisor events[] shells into match rows.
  • UFC empty board honesty: when count === 0, section includes note, emptyReason, slim health (workerAlive, lag), optional non-live supervisor strip and nextCard (armed only). Also data.note + meta.warnings for single-game UFC. Do not treat empty as HTTP failure; do not invent ? vs ? from event shells.
  • upcoming_schedule expands event → bout rows with fighter labels; applies client-side hours / from / to (API has no hours); event shells without bouts use the event name, not ? vs ?.
  • event_card bouts share the same normalizeMatch path.
  • standings maps division champions to rank C (rankText: "C", interim IC); contender #1 stays numeric 1 — no dual numeric #1.

Fortnite: call_api only until promoted into the primary enum.

call_api allowlist

Path must start with / and match one of:

/health · /lol · /cs2 · /dota2 · /cod · /ufc · /fortnite · /tennis · /raf · /bkfc · /bellator · /pride · /strikeforce · /wec · /dream · /elitexc · /affliction · /mma · /openapi.json

Absolute URLs and .. traversal → PATH_NOT_ALLOWED. Prefer curated tools for all standard jobs.


Response envelope

Every tool returns one JSON object:

{
  "ok": true,
  "data": {},
  "meta": {
    "game": "cs2",
    "source": "match_summary",
    "fetchedAt": "2026-07-27T12:00:00.000Z",
    "requestId": "mcp_…",
    "tookMs": 120,
    "upstreamCalls": 3,
    "rateLimit": { "tier": "pro", "remaining": 54 }
  },
  "pagination": { "limit": 20, "hasMore": false, "nextCursor": null },
  "partial": [],
  "error": null
}

| Condition | How to handle | | --- | --- | | ok: false | Tool-level failure; MCP isError: true; read error.code + error.recover[] | | ok: true with partial[] | Use successful sections; do not treat as total failure | | List tools | data.items + pagination (limit, hasMore, nextCursor, …) |

Error codes: VALIDATION · NOT_FOUND · UNSUPPORTED_GAME · UNAUTHORIZED · RATE_LIMIT · UPSTREAM · PATH_NOT_ALLOWED · NOT_IMPLEMENTED

Ambiguity: resolve_entity returns ok: true with data.needsDisambiguation + data.candidates — pick a candidate; do not wait for a hard error.

Retry only when error.retryable is true (typically RATE_LIMIT, UPSTREAM).

Pagination: default limit 20, max 50 (some tools clamp differently). Honor pagination.hasMore and pass pagination.nextCursor only — never invent cursors or reuse them after changing filters.


Builder recipes

Use these as agent playbooks when scaffolding apps. Prefer one composite per screen over N+1 thin GETs.

1. Live scoreboard

Goal: multi-game (or single-game) live strip with optional match drill-down.

1. api_health                          # optional — once per session
2. live_matches { game: "all" }        # or "cs2" / "lol" / …
3. For a selected row:
   match_summary { game, matchId }     # default card
   match_details { …, includeLiveState: true }  # only if needed

UI map

| Screen region | Source | | --- | --- | | Live rows (label, score, status) | live_matches → data.sections[].items | | Expanded card | match_summary → data.scoreline, playerPerformances, media | | In-game window | match_details sections liveState / timeline |

Do not: N+1 match_details for every live row; invent matchIds; use upcoming_schedule for live-only boards.

2. Player profile page

Goal: identity, team, recent form for a named player or fighter.

1. resolve_entity { q: "s1mple", game: "cs2", type: "player" }
   # or type: "fighter" for UFC
2. player_profile {
     game,
     playerId | slug,          # from resolve best/candidates
     recentLimit: 10,
     includeTrends: true,
     view: "summary"
   }
3. Optional: head_to_head for rivalry context (fighters/teams)

UI map

| Screen region | Source | | --- | --- | | Header / identity | data.player | | Current team | data.currentTeam | | Recent form list | data.recentMatches + data.form | | Career / radar | data.career, data.radar, data.seasonStats (when present) |

Do not: invent playerIds; use team_profile for full rosters; skip resolve when the user only gave a display name.

3. Event hub

Goal: schedule strip + standings/rankings + match previews for a league, tournament, or event.

1. list_capabilities { job: "schedule" }   # optional orientation
2. upcoming_schedule {
     game: "cs2",                          # or lol / cod / ufc / dota2
     hours: 168,                           # or from/to ISO window
     league | tournamentId | team          # scope filters as available
   }
3. standings {
     game,
     scope: "event" | "league" | "tournament" | "world" | "division",
     eventId | leagueId | tournamentId | division | season
   }
4. For a featured upcoming match:
   match_preview { game, matchId }         # or teamA + teamB
5. When a match goes live / completes:
   match_summary { game, matchId }

UI map

| Screen region | Source | | --- | --- | | Upcoming list | upcoming_schedule → data.items | | Table / ranks | standings → data.rows | | Featured match briefing | match_preview → sideA / sideB / h2h / talkingPoints | | Live / completed card | match_summary |

Notes: Dota standings may return NOT_IMPLEMENTED or sparse data — fall back to schedule + profiles. UFC event hubs lean on fighter slugs, boutId, and standings with scope: "division".

Other quick recipes (from list_capabilities)

| Recipe id | Steps | | --- | --- | | live_ops_board | api_health → live_matches → match_summary / match_details | | team_page | resolve_entity { type: "team" } → team_profile | | match_center_completed | schedule/live for matchId → match_summary → optional match_details | | app_scaffold | list_capabilities ∥ api_health → sample composites → ship REST in production |


Resources

| URI | Purpose | | --- | --- | | cito://capabilities | Static tool / game / workflow summary | | cito://llms.txt | Live product agent context (may 404 upstream) | | cito://openapi.json | Live public OpenAPI for typed clients — not for tool generation |


Transports

stdio is the default (Claude Code, Cursor, Windsurf).

Optional stateless Streamable HTTP:

CITO_API_KEY=cito_your_key_here node dist/index.js --http 8787
# → http://127.0.0.1:8787/mcp

Hosted (production): https://api.citoapi.com/mcp
Send the user's Cito key as x-api-key. Initialize and list_capabilities work without a key so Smithery can scan.


Migration from 0.1 (auto-generated tools)

Breaking change. 0.1.x loaded OpenAPI at boot and mass-generated ~100+ tools (generateAllTools), plus a couple of hard-coded helpers (e.g. cito_live_overview, cito_api_health). Agents selected among thin REST wrappers and often reimplemented composition client-side.

| 0.1 pattern | 0.2 replacement | | --- | --- | | Generated per-path tools (/cs2/matches/..., etc.) | Outcome tools in the table above | | cito_live_overview / ad-hoc live probes | live_matches (fixed CS2 path /cs2/live; UFC included) | | cito_api_health | api_health | | OpenAPI-named tools with cito_ noise | 15 snake_case names, no cito_ prefix | | Raw HTTP-shaped responses | Stable envelope: ok, data, meta, pagination, partial, error | | Agent stitches roster + matches + H2H | team_profile, player_profile, head_to_head, match_preview | | Fortnite / niche paths as first-class tools | call_api allowlist | | Spec cache / boot network for tool list | Static catalog; list_capabilities is offline |

Action items for existing clients

  1. Drop any hardcoded 0.1 tool names; rebind prompts to the 15-tool catalog.
  2. Always parse the envelope (ok / partial / error.recover) — do not assume bare REST JSON.
  3. Use resolve_entity before profile tools when you only have a display name.
  4. Prefer composites over reintroducing N+1 call_api loops.
  5. Bump config to npx cito-mcp (or rebuild from this package) and re-test live board + one profile + one match flow.

There is no compatibility shim for 0.1 tool names in 0.2.


Development

npm install
npm run build     # tsc → dist/
npm test          # node:test via tsx, offline (no network)
npm start         # node dist/index.js (requires CITO_API_KEY)

Source layout (authoritative tool registry: src/tools/index.ts):

src/
  index.ts              # MCP wiring, resources, stdio / --http
  client.ts             # fetchJson, auth, present, cursors
  envelope.ts           # success / error envelopes, MCP isError
  instructions.ts       # server instructions for agents
  tools/
    index.ts            # allTools (15)
    meta.ts             # list_capabilities, api_health, call_api
    resolve.ts          # resolve_entity, search_entities
    live.ts             # live_matches, upcoming_schedule
    match.ts            # match_summary, match_details
    player.ts           # player_profile
    team.ts             # team_profile, head_to_head
    standings.ts        # standings
    insight.ts          # match_preview, event_card
    normalize.ts        # cross-game normalization
    types.ts            # ToolDef, schemas

Publish notes

Package: [email protected]

| Item | Value | | --- | --- | | npm name | cito-mcp | | bin | cito-mcp → dist/index.js | | published files | dist/, README.md (package.json "files") | | engines | node >= 20 | | runtime dependency | @modelcontextprotocol/sdk only | | prepublish | npm run prepublishOnly → npm run build |

Pre-publish checklist

  1. npm test — offline suite green.
  2. npm run build — clean dist/.
  3. Smoke against a real key (not in CI by default):
    • api_health
    • live_matches
    • resolve_entity (e.g. T1 / s1mple)
    • match_summary with a real matchId from live/schedule
  4. Confirm package.json version and README match the shipped tool list.
  5. npm publish from mcp/ (or your release pipeline) with appropriate npm auth / access. prepublishOnly runs build + tests + smoke, so a broken build cannot ship.

Install line for docs & marketing

npx cito-mcp install --key cito_…

Manual fallback (note the quoted --, required for PowerShell):

claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
{ "command": "npx", "args": ["cito-mcp"], "env": { "CITO_API_KEY": "cito_…" } }

Semver expectations

  • 0.2.4 is a major surface break vs 0.1 (tool rename + removal of OpenAPI mass-generation).
  • Further 0.2.x patches may refine envelopes and composite quality without renaming the shipped tools. (This line previously said "15 tools" while the catalog shipped 42 — the count here is intentionally not restated, because a number duplicated in prose drifts the moment the catalog changes.)
  • Promoting Fortnite (or other titles) into the primary game enum would be a minor feature bump with catalog/docs updates.

Changelog (summary)

0.5.8

  • Tennis player_rankings_history asks /tennis/rankings/history/{id} for one page (limit, new page, since sent as from) instead of downloading the whole career and slicing it; page 1 is the newest limit rows, returned oldest-first as before. omittedOlder counts rows at the old end, new omittedNewer the rows on earlier (newer) pages, hasMore is the route's own has_next.

0.5.7

  • Tennis event_card: event.drawSize and draw.drawSize are the API's stored draw size, the same value /tennis/tournaments/{id} serves (it is now corrected to the proven size: Paris 56, Shanghai 96). drawSizeVerified says a proof stands behind it; draw.drawSizeDerived is what the edition's own bracket shows.

0.5.6

  • Tennis event_card draw is a real bracket (customer audit 2026-11-03). draw.complete is true only when every main round to the Final is played (it said true beside missingRounds F/SF/QF); every main round is listed with status (completed / in_progress / not_started / incomplete_archive), expected (byes excluded: a 56 draw's R64 holds 24), byes, walkovers, and pending (scheduled / live matches, never presented as results). drawSize is the real field (Paris 56, drawSizeSource, bracketSize, byes); matchesExpected counts the main draw and is compared with mainDrawMatchesReturned (qualifying is reported apart). Each bout carries winnerId / loserId / outcome (a "6-3 6-7 RET" retirement says who won), startTime, nextMatchId (the bracket edge) and bracketPosition; rounds report included / omitted when drawMaxMatches trims them.
  • Tennis head_to_head honours limit: it is sent to /tennis/h2h, which pages the meetings while every tally still covers all of them (meetingsReturned / meetingsTotal; the H2H level_breakdown now always sums to total_matches).
  • Other tennis audit fixes in the same release: live stats as structured numerator / denominator / pct, isSimulated / liveFeed on live rows, setNumber from set_number, and the schedule / rankings / player readers updated for the API changes of 2026-11-03.

0.5.5

  • cs2_veto_sequence returns the veto. It called the match head-to-head route and answered with the two teams' head-to-head record (cs2_team_h2h) instead of the veto (0.5.1 to 0.5.4). It now calls /cs2/matches/{matchId}/veto: vetoOrder (every ban, pick and the decider, in order), vetoStatus (complete, pending, forfeit_no_veto, unavailable), bestOf and the maps.
  • cs2_opening_duels with playerId works. It called a route the API does not have (every call was a 404). A player's opening duels now come from the career totals: openingKills / openingDeaths (null when fewer than 90% of the player's maps record them), the recorded sums, coverage and success rate.
  • cs2_team_map_stats takes opponentId for a map-by-map comparison of two teams (/cs2/teams/{team}/map-pool/vs/{opponent}: win rates, round difference, pick/ban/decider rates, their meetings per map, edge). Map rows carry inPool / poolStatus.
  • cs2_round_economy points to /cs2/maps/{mapId}/economy for per-player money each round.

0.5.4

  • Apex Legends through call_api. /apex/... paths (ALGS events, standings, scores) were refused with PATH_NOT_ALLOWED although they are a live REST product. They are allowed now and listed in list_capabilities.
  • api_health reports your real plan and limits: the tier plus the per-minute and monthly limit, used and remaining, read from the API's /health, instead of null.

0.5.3

  • Paces itself and retries rate limits. One question fans out into several API calls, and on the Free plan (10 requests/minute on average, bursts to 30) those bursts came back as 429s the MCP never waited out. Requests are now capped at 4 in flight per API key (2 for a minute after a 429), and once the API sends X-RateLimit-Remaining-Minute: 0 the next request waits for the minute window instead of being refused. Paid keys never receive those headers, so they never wait.
  • 429 RATE_LIMIT_EXCEEDED (and TOO_MANY_CONCURRENT_REQUESTS) waits Retry-After (at most 20 s per wait) and retries up to twice; 502/503 get one short retry. Each tool call has a 45 s wait budget shared by all its requests. MONTHLY_LIMIT_REACHED and 403 are never retried.
  • When retries run out, the RATE_LIMIT envelope says what happened (plan limit, retries already made, retryAfterMs) and carries the API's upgrade link. MONTHLY_LIMIT_REACHED is now retryable: false with the API's own message (reset date + upgrade), instead of a retryable error an agent kept hammering.

0.4.9

  • Non-UFC MMA archives: league bellator (2014-2017), pride (1997-2007), strikeforce (2006-2013), wec (2001-2010), dream (2008-2012), elitexc (2007-2008), affliction (2008-2009) and mma (all of them at once) on every combat_* tool, and /bellator /pride /strikeforce /wec /dream /elitexc /affliction /mma on the call_api / list_routes allowlist. Official per-round stats in /ufc's shape; fighters with a UFC record share the UFC slug (combat_athlete returns ufcFighterSlug, and under mma the whole non-UFC career split by recordByPromotion); mma events recent and combat_card last work across promotions. Archive gaps are answered honestly: upcoming events are an empty list with a note, combat_card defaults to the most recent card and refuses "next", and combat_rankings returns a non-retryable NOT_IMPLEMENTED. HISTORY_WINDOW_EXCEEDED sections are reported as a plan gap (Pro), not as a key problem.
  • Agent guidance (server instructions, list_capabilities): the MMA archives are never mixed into UFC answers; UFC already includes Dana White's Contender Series and Road to UFC (2022-2026, all 19 cards with official round stats, slugs like road-to-ufc-2-7).

0.4.8

  • RAF wrestling and BKFC bare knuckle: six league-parameterised combat_* tools, and /raf + /bkfc on the call_api allowlist.

0.4.7

  • Removed the unregistered tennis odds module and its README entries; match_details sections:["odds"] stays UFC-only and answers NOT_IMPLEMENTED for other games.

0.4.2

Second tennis correctness pass, from the 2026-09-12 re-sweep. Five defects, each reproduced from a raw payload and re-verified after the fix. Two were upstream (fixed on the API and verified with the 40/40 invariant gate); three were MCP-side.

  • Tennis standings published a fake total and could not be paged. /tennis/standings?top_n=5 set rank_max=top_n, so the count (which carried the rank bounds) equalled the page size: total:5, hasMore:false for a list of 150 ranked players. The API now counts the population without the rank window and returns ranked_players/total_pages/has_next, /standings takes a page argument, and the tool reports total/rankedPlayers/totalPages/hasMore — page 2 of the ATP list now starts at rank 6. The same allRows.length mistake in the pagination block was fixed too, so the two counts cannot disagree.
  • event_card published the requested id as the event name. With a bad id it answered ok:true and event.name = "atp_2026_999999" over a 404 that partial[] reported correctly. Names now come from the payload or are null, with event.resolved recording whether the detail fetch succeeded.
  • leaderboard_* reported the page size as the population. Every board returned "total": len(items) after applying .limit(limit), so limit=2 answered total:2 — for boards whose own docs record thousands of qualifiers. Upstream now carries count(*) OVER () (Postgres evaluates it after GROUP BY/HAVING, so it is exactly the qualifying population, at no extra round trip): aces 2488, break-conversion 1089 (matching the figure in its own docstring), 1st-serve-won 1141, bp-saved 1186, return-games 987, tiebreak 364, deciders 334, comebacks 611, finals 66. Each tool also reports returned and the board's minimumAttempts.
  • resolve_entity { q: "Nole" } answered "M Canoles". The upstream name search puts Novak Djokovic first for Nole — it knows a nickname nothing in the payload carries, so rankScore scores it 0 — and the tool floored every unmatched row to 1 and then sorted, letting a substring collision win. pushCandidates now lets the upstream's own first row stand as the alias answer (labelled meta.matchedBy: "upstream-first") when our scorer is silent, and search_entities exempts exactly that one row from its relevance floor. Padding is still dropped: Sinner still returns only Sinner.
  • /tennis/tournaments/calendar silently ignored ?month=. FastAPI drops unknown query parameters, so ?month=9 and ?month=3 returned the identical 621-event season list and a caller asking "what is on in September" got the whole year. The route now takes month and filters on the event's start month (2026-09 → 12 events).
  • standings and leaderboards were never cached, so every call re-ran its grouped scan and paid db1's ~90ms RTT per query: 474–922ms on every request while every cached route answered in <20ms. That held eleven tools permanently over the 400ms latency budget. Both prefixes now have TTLs (1800s / 600s); repeat calls measure 4–5ms.
  • bestOf now reads best_of, the only spelling the tennis route emits.

0.4.1

Tennis correctness pass, driven by the 2026-09-12 sweep (reports/tennis-mcp-sweep-2026-09-12.md). Every item was reproduced against live ids and re-verified after the fix.

  • match_summary tennis scoreline was always - : -. /tennis/matches/{id} puts no sets_won on its player objects, so the sets-won score was null for every completed match while the same payload held the real score in score and sets[]. Sets won are now derived (explicit → winner_sets_won → sets[] → score string), score.detail carries the game score, and a set's completed is inferred when the archive omits is_completed. The winner-oriented winner_games/loser_games pair is deliberately not used as a side score. bestOf now reads best_of.
  • player_stats.bySurface was always four nulls. The endpoint publishes one object per surface and the projector coerced each to a number. Real per-surface blocks, servingStats, and an explicit unavailable map for sections the endpoint does not publish.
  • limit was ignored by player_rankings_history (asked 4, got 342) and by /tennis/matches/completed. Both are bounded locally now, with *Returned versus *Count so the day total is never confused with the page.
  • tournaments level="WTA 1000" returned ATP Masters events. The API's level reverse-map is not tour-aware (_TIER_WTA maps both PM and M to "WTA 1000"), so the tool now sends and enforces the tour the level name states, and drops + counts contradicting rows. Fixed at the source too — the same widening also leaked WTA 125, Challenger, Davis Cup and Billie Jean King Cup.
  • rankings_movers called an 84-day gap "week-over-week". gapDays/comparisonWindow/comparisonsAreWeekly now state the real interval, with a warning. standings reports the tennis ranking_date as updatedAt instead of null.
  • tennis_schedule now counts the day's list/completed overlap, summarises statuses, and flags date-only startsAt placeholders.
  • live_matches labelled tennis rows by tournament, not players, because the live route's name is the event.
  • Tennis players had no images although /tennis/players/{id} carries portrait_url. Now surfaced (plus ioc as the side country on boards).
  • search_entities had no relevance floor: q=Sinner returned eight unrelated names. Rows that match nothing are dropped and reported, and a one- or two-character fragment no longer counts as a match.

0.2.4

  • UFC projection hardening (offline-tested):
    • normalizeMatch: fighters[] corners + profile, live red/blue, boutId/dataId; never keep ? vs ? when names exist
    • extractRows / extractLiveRows: prefer liveBouts (including empty); never fake match rows from supervisor events[]
    • upcoming_schedule: client-side hours/from/to; bout expansion with fighter labels; event shells labeled by event name
    • event_card bouts share the same normalize path
    • standings: champion rank C / interim IC; no dual numeric #1 for champ + contender

0.2.4

  • Breaking: replaced OpenAPI mass-generated tools with 15 curated outcome tools
  • Stable JSON envelope + MCP server instructions
  • Fixed CS2 live path (/cs2/live); UFC in live fan-out
  • Composites with partial[] recovery; call_api path allowlist
  • Resources: cito://capabilities, cito://llms.txt, cito://openapi.json

See _workflow/CHANGELOG-0.2.md for the full 0.2 write-up.