cito-mcp
v0.2.7
Published
Standalone MCP server for the Cito esports API — 15 curated outcome tools for agents (live, schedule, profiles, standings, previews, event cards).
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.2.4 · Node: >=20 · Install: npx cito-mcp
Primary games: lol · cs2 · dota2 · cod · ufc. Fortnite and long-tail REST stay 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.
v0.2 ships 16 hand-authored 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 form | player_profile |
| Table / rankings | standings |
| Pre-match briefing | match_preview |
| Event / fight-night card | event_card |
| Rivalry record | head_to_head |
| 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.
Install
One command (recommended)
npx cito-mcp install --key cito_your_key_hereDetects 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 --helpGet a key at citoapi.com/dashboard.
Manual — Claude Code
claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" npx -y cito-mcpQuote 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
- Unsure →
list_capabilities(optionalapi_healthfor tier / included games) - Name without ID →
resolve_entity(chain) orsearch_entities(browse) - Live / upcoming →
live_matches/upcoming_schedule - Match card →
match_summary(thenmatch_detailsif needed) - Team / player pages →
team_profile/player_profile - Tables / rivalry / preview / event card →
standings/head_to_head/match_preview/event_card - Escape hatch →
list_routesto find a path, thencall_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 | UFC card, CS2 event hub, tournament overview | Live-only strip; single match recap | game, eventIdOrSlug or q, includeBouts?, includeStandings?, limit? |
| 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://ufc.com/…/MAKHACHEV_ISLAM_BELT_01-18.png",
"bodyImageUrl": "https://ufc.com/…/athlete_bio_full_body/…",
"imageUrl": "https://ufc.com/…/event_fight_card_upper_body/…",
"proxiedImageUrl": "https://api.citoapi.com/api/v1/public/images/ufc/aHR0cHM6…"
}
}Use proxiedImageUrl in a browser. ufc.com 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.
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 |
UFC projection notes (0.2.4)
Hardened agent-facing UFC shapes (offline-tested):
normalizeMatchreadsfighters[](corner+fighterName/profile.name), livered/blue, andboutId/dataId/ bareid. Never keeps label? vs ?when fighter names exist (including blue-first arrays).live_matchesusesextractLiveRows: preferliveBouts(empty array = honest empty board); never promote supervisorevents[]shells into match rows.- UFC empty board honesty: when
count === 0, section includesnote,emptyReason, slimhealth(workerAlive, lag), optional non-livesupervisorstrip andnextCard(armed only). Alsodata.note+meta.warningsfor single-game UFC. Do not treat empty as HTTP failure; do not invent? vs ?from event shells. upcoming_scheduleexpands event → bout rows with fighter labels; applies client-sidehours/from/to(API has nohours); event shells without bouts use the event name, not? vs ?.event_cardbouts share the samenormalizeMatchpath.standingsmaps division champions to rankC(rankText: "C", interimIC); contender#1stays numeric1— 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
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 neededUI 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/mcpMigration 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
- Drop any hardcoded 0.1 tool names; rebind prompts to the 15-tool catalog.
- Always parse the envelope (
ok/partial/error.recover) — do not assume bare REST JSON. - Use
resolve_entitybefore profile tools when you only have a display name. - Prefer composites over reintroducing N+1
call_apiloops. - 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, schemasPublish 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
npm test— offline suite green.npm run build— cleandist/.- Smoke against a real key (not in CI by default):
api_healthlive_matchesresolve_entity(e.g. T1 / s1mple)match_summarywith a realmatchIdfrom live/schedule
- Confirm
package.jsonversion and README match the shipped tool list. npm publishfrommcp/(or your release pipeline) with appropriate npm auth / access.prepublishOnlyruns 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 15 tools.
- Promoting Fortnite (or other titles) into the primary
gameenum would be a minor feature bump with catalog/docs updates.
Changelog (summary)
0.2.4
- UFC projection hardening (offline-tested):
normalizeMatch:fighters[]corners + profile, live red/blue,boutId/dataId; never keep? vs ?when names existextractRows/extractLiveRows: preferliveBouts(including empty); never fake match rows from supervisorevents[]upcoming_schedule: client-sidehours/from/to; bout expansion with fighter labels; event shells labeled by event nameevent_cardbouts share the same normalize pathstandings: champion rankC/ interimIC; no dual numeric#1for 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_apipath allowlist - Resources:
cito://capabilities,cito://llms.txt,cito://openapi.json
See _workflow/CHANGELOG-0.2.md for the full 0.2 write-up.
