cito-mcp 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +620 -609
- package/dist/client.js +107 -31
- package/dist/envelope.js +79 -1
- package/dist/instructions.js +82 -81
- package/dist/pacing.js +195 -0
- package/dist/tools/meta.js +41 -10
- package/dist/tools/types.js +13 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,609 +1,620 @@
|
|
|
1
|
-
# cito-mcp
|
|
2
|
-
|
|
3
|
-
Standalone [MCP](https://modelcontextprotocol.io) server for the [Cito esports API](https://api.citoapi.com) — **curated outcome tools** for agents building esports apps, dashboards, bots, and research flows.
|
|
4
|
-
|
|
5
|
-
**Version:** `0.4.9` · **Node:** `>=20` · **Install:** `npx cito-mcp`
|
|
6
|
-
|
|
7
|
-
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`.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Philosophy: outcomes, not endpoints
|
|
12
|
-
|
|
13
|
-
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.
|
|
14
|
-
|
|
15
|
-
**Ships 42 curated outcome tools** that answer *jobs* instead of mirroring REST:
|
|
16
|
-
|
|
17
|
-
| Job | Tool |
|
|
18
|
-
| --- | --- |
|
|
19
|
-
| What’s live right now? | `live_matches` |
|
|
20
|
-
| What’s coming up? | `upcoming_schedule` |
|
|
21
|
-
| Build a match card / recap | `match_summary` |
|
|
22
|
-
| Deep timeline / live state | `match_details` |
|
|
23
|
-
| Team page | `team_profile` |
|
|
24
|
-
| Player / fighter profile | `player_profile` |
|
|
25
|
-
| Player form & win streak | `player_form` |
|
|
26
|
-
| Table / rankings | `standings` |
|
|
27
|
-
| Ranking climbers & droppers | `rankings_movers` |
|
|
28
|
-
| Pre-match briefing (with surface) | `match_preview` |
|
|
29
|
-
| Event / fight-night card | `event_card` |
|
|
30
|
-
| Rivalry record | `head_to_head` |
|
|
31
|
-
| Tennis tournament catalog | `tournaments` |
|
|
32
|
-
| One day of tennis | `tennis_schedule` |
|
|
33
|
-
| Tennis player match log | `player_matches` |
|
|
34
|
-
| Tennis career statistics | `player_stats` |
|
|
35
|
-
| Tennis ranking trajectory | `player_rankings_history` |
|
|
36
|
-
| Fighter / team photos | `event_card` · `player_profile` |
|
|
37
|
-
| Which raw REST route exists? | `list_routes` |
|
|
38
|
-
| Name → ID | `resolve_entity` / `search_entities` |
|
|
39
|
-
|
|
40
|
-
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.
|
|
41
|
-
|
|
42
|
-
**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.
|
|
43
|
-
|
|
44
|
-
### Honest gaps, in-band
|
|
45
|
-
|
|
46
|
-
Where the source data is partial, the tool says so instead of shipping a plausible-looking wrong value:
|
|
47
|
-
|
|
48
|
-
| Situation | What the tool returns |
|
|
49
|
-
| --- | --- |
|
|
50
|
-
| 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 |
|
|
51
|
-
| A tennis tournament draw is missing matches | `draw.shortRounds` (`["R128 45/64"]`), `draw.missingRounds`, `draw.complete` |
|
|
52
|
-
| A stats section the endpoint does not publish | `player_stats.unavailable` names the section and why, rather than a block of nulls |
|
|
53
|
-
| `search_entities` results that do not match at all | dropped, counted under `data.relevance.droppedBelowFloor`, with a warning naming them |
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## Install
|
|
58
|
-
|
|
59
|
-
### One command (recommended)
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
npx cito-mcp install --key cito_your_key_here
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
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.
|
|
66
|
-
|
|
67
|
-
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.
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npx cito-mcp install --dry-run # show the plan, write nothing
|
|
71
|
-
npx cito-mcp install --key … --client cursor # one client only
|
|
72
|
-
npx cito-mcp install --help
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Get a key at [citoapi.com/dashboard](https://citoapi.com/dashboard).
|
|
76
|
-
|
|
77
|
-
### Manual — Claude Code
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" npx -y cito-mcp
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
**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.
|
|
84
|
-
|
|
85
|
-
From a local clone (development):
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
cd mcp
|
|
89
|
-
npm install
|
|
90
|
-
npm run build
|
|
91
|
-
claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" node "%CD%\dist\index.js"
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Cursor
|
|
95
|
-
|
|
96
|
-
Add to Cursor MCP settings (e.g. `.cursor/mcp.json` or Cursor Settings → MCP):
|
|
97
|
-
|
|
98
|
-
```json
|
|
99
|
-
{
|
|
100
|
-
"mcpServers": {
|
|
101
|
-
"cito": {
|
|
102
|
-
"command": "npx",
|
|
103
|
-
"args": ["cito-mcp"],
|
|
104
|
-
"env": {
|
|
105
|
-
"CITO_API_KEY": "cito_your_key_here"
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Windows local clone:
|
|
113
|
-
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"mcpServers": {
|
|
117
|
-
"cito": {
|
|
118
|
-
"command": "node",
|
|
119
|
-
"args": ["C:/Users/Owner/Projects/cito-api-scraper-windows/mcp/dist/index.js"],
|
|
120
|
-
"env": {
|
|
121
|
-
"CITO_API_KEY": "cito_your_key_here"
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Windsurf / other MCP hosts
|
|
129
|
-
|
|
130
|
-
Same shape as Cursor: `command` + `args` + `env.CITO_API_KEY`. Prefer `npx cito-mcp` so clients pick up published patches without a local install.
|
|
131
|
-
|
|
132
|
-
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.
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## Environment
|
|
137
|
-
|
|
138
|
-
| Variable | Required | Default | Description |
|
|
139
|
-
| --- | --- | --- | --- |
|
|
140
|
-
| `CITO_API_KEY` | **yes** | — | Cito API key → `x-api-key` on every upstream call |
|
|
141
|
-
| `CITO_API_BASE` | no | `https://api.citoapi.com/api/v1` | Override base URL (staging / self-hosted) |
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## Preferred tool order
|
|
146
|
-
|
|
147
|
-
1. Unsure → `list_capabilities` (optional `api_health` for tier / included games)
|
|
148
|
-
2. Name without ID → `resolve_entity` (chain) or `search_entities` (browse)
|
|
149
|
-
3. Live / upcoming → `live_matches` / `upcoming_schedule`
|
|
150
|
-
4. Match card → `match_summary` (then `match_details` if needed)
|
|
151
|
-
5. Team / player pages → `team_profile` / `player_profile`
|
|
152
|
-
6. Tables / rivalry / preview / event card → `standings` / `head_to_head` / `match_preview` / `event_card`
|
|
153
|
-
7. Escape hatch → `list_routes` to find a path, then `call_api` (allowlisted paths only)
|
|
154
|
-
|
|
155
|
-
**Never invent IDs** — resolve them or take them from live / schedule / search results.
|
|
156
|
-
|
|
157
|
-
Mnemonic: **resolve → live/schedule → summary → deep**.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Tool catalog (16)
|
|
162
|
-
|
|
163
|
-
All tools are **read-only**. Names are `snake_case` with **no** `cito_` prefix (the server name already brands the surface).
|
|
164
|
-
|
|
165
|
-
| Tool | Outcome | When to use | When not to use | Key inputs |
|
|
166
|
-
| --- | --- | --- | --- | --- |
|
|
167
|
-
| `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?` |
|
|
168
|
-
| `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?` |
|
|
169
|
-
| `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?` |
|
|
170
|
-
| `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?` |
|
|
171
|
-
| `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?` |
|
|
172
|
-
| `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?` |
|
|
173
|
-
| `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?` |
|
|
174
|
-
| `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?` |
|
|
175
|
-
| `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?` |
|
|
176
|
-
| `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?` |
|
|
177
|
-
| `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?` |
|
|
178
|
-
| `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?` |
|
|
179
|
-
| `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?` |
|
|
180
|
-
| `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?` |
|
|
181
|
-
| `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?` |
|
|
182
|
-
| `call_api` | Allowlisted raw REST (`data.raw`) | Fortnite / long-tail paths; payload debugging | Any job covered by a curated tool | `path`, `method?`, `queryJson?`, `bodyJson?` |
|
|
183
|
-
|
|
184
|
-
### Images
|
|
185
|
-
|
|
186
|
-
Fighter and team sides carry an `images` object wherever upstream supplies one —
|
|
187
|
-
on `event_card` bout corners and on `player_profile`. No extra call, no N+1.
|
|
188
|
-
|
|
189
|
-
```jsonc
|
|
190
|
-
"team1": {
|
|
191
|
-
"name": "Islam Makhachev",
|
|
192
|
-
"slug": "islam-makhachev",
|
|
193
|
-
"record": { "wins": 28, "losses": 1, "draws": 0, "text": "28-1-0 (W-L-D)" },
|
|
194
|
-
"championStatus": "champion",
|
|
195
|
-
"images": {
|
|
196
|
-
"headshotUrl": "https://…/MAKHACHEV_ISLAM_BELT_01-18.png",
|
|
197
|
-
"bodyImageUrl": "https://…/athlete_bio_full_body/…",
|
|
198
|
-
"imageUrl": "https://…/event_fight_card_upper_body/…",
|
|
199
|
-
"proxiedImageUrl": "https://api.citoapi.com/api/v1/public/images/ufc/aHR0cHM6…"
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
**Use `proxiedImageUrl` in a browser.** the image host sends no CORS header and can
|
|
205
|
-
hotlink-block, so raw URLs render as broken images in a web UI. The proxied URL
|
|
206
|
-
is served by the API and is safe to put in an `<img src>`.
|
|
207
|
-
|
|
208
|
-
When a key is `null` the image does not exist for that entity; when the whole
|
|
209
|
-
`images` object is absent, upstream sent nothing for that side. The object is
|
|
210
|
-
never partially shaped — if any image exists, all four keys are present.
|
|
211
|
-
|
|
212
|
-
Tennis is the exception on purpose: the feed supplies a single `portrait_url`
|
|
213
|
-
(Wikimedia Commons) and no square crop, so that one URL fills both `headshotUrl`
|
|
214
|
-
and `imageUrl`, and `proxiedImageUrl` stays `null` because only fighter image URLs have
|
|
215
|
-
a proxy.
|
|
216
|
-
|
|
217
|
-
**Tennis images carry their licence and credit** — and a UI is expected to render
|
|
218
|
-
them:
|
|
219
|
-
|
|
220
|
-
```jsonc
|
|
221
|
-
"images": {
|
|
222
|
-
"headshotUrl": "https://commons.wikimedia.org/wiki/Special:FilePath/Hoang_WMQ18_%2833%29_%2842647295495%29.jpg?width=400",
|
|
223
|
-
"license": "CC BY-SA 2.0",
|
|
224
|
-
"attribution": "si.robi",
|
|
225
|
-
"sourceUrl": "https://commons.wikimedia.org/wiki/File:Hoang_WMQ18_(33)_(42647295495).jpg"
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Commons portraits are mostly CC BY / CC BY-SA, which for commercial display
|
|
230
|
-
generally requires naming the author and stating the licence. `attribution` is
|
|
231
|
-
`null` for public-domain and CC0 images, which need no credit — that is a fact,
|
|
232
|
-
not missing data. Anything without a defensible licence has its URL removed
|
|
233
|
-
upstream, so a non-null `headshotUrl` always has a `license` beside it.
|
|
234
|
-
|
|
235
|
-
### Games
|
|
236
|
-
|
|
237
|
-
| Game | Depth | Notes |
|
|
238
|
-
| --- | --- | --- |
|
|
239
|
-
| `lol` | deep | Team slugs; `lol-match-*`; no native global search |
|
|
240
|
-
| `cs2` | deep | Prefixed ids; live path **`GET /cs2/live`**; trends / rankings |
|
|
241
|
-
| `dota2` | thin | Numeric ids; radar; weak/no standings & roster endpoints |
|
|
242
|
-
| `cod` | medium | Org slugs; CDL standings; UUID match ids |
|
|
243
|
-
| `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") |
|
|
244
|
-
|
|
245
|
-
### Combat leagues (`combat_*`, `league` argument)
|
|
246
|
-
|
|
247
|
-
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.
|
|
248
|
-
|
|
249
|
-
| League | Scoring | Notes |
|
|
250
|
-
| --- | --- | --- |
|
|
251
|
-
| `raf` | per PERIOD (wrestling points, takedowns) | Athletes; champions per division |
|
|
252
|
-
| `bkfc` | per ROUND (punches landed/thrown, knockdowns) | Fighters; official rankings + champions |
|
|
253
|
-
| 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. |
|
|
254
|
-
|
|
255
|
-
### UFC projection notes (0.2.4)
|
|
256
|
-
|
|
257
|
-
Hardened agent-facing UFC shapes (offline-tested):
|
|
258
|
-
|
|
259
|
-
- **`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).
|
|
260
|
-
- **`live_matches`** uses `extractLiveRows`: prefer `liveBouts` (empty array = honest empty board); never promote supervisor `events[]` shells into match rows.
|
|
261
|
-
- **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.
|
|
262
|
-
- **`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 ?`.
|
|
263
|
-
- **`event_card`** bouts share the same `normalizeMatch` path.
|
|
264
|
-
- **`standings`** maps division champions to rank **`C`** (`rankText: "C"`, interim **`IC`**); contender `#1` stays numeric `1` — no dual numeric `#1`.
|
|
265
|
-
|
|
266
|
-
Fortnite: **`call_api` only** until promoted into the primary enum.
|
|
267
|
-
|
|
268
|
-
### `call_api` allowlist
|
|
269
|
-
|
|
270
|
-
Path must start with `/` and match one of:
|
|
271
|
-
|
|
272
|
-
`/health` · `/lol` · `/cs2` · `/dota2` · `/cod` · `/ufc` · `/fortnite` · `/tennis` · `/raf` · `/bkfc` · `/bellator` · `/pride` · `/strikeforce` · `/wec` · `/dream` · `/elitexc` · `/affliction` · `/mma` · `/openapi.json`
|
|
273
|
-
|
|
274
|
-
Absolute URLs and `..` traversal → `PATH_NOT_ALLOWED`. Prefer curated tools for all standard jobs.
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## Response envelope
|
|
279
|
-
|
|
280
|
-
Every tool returns one JSON object:
|
|
281
|
-
|
|
282
|
-
```json
|
|
283
|
-
{
|
|
284
|
-
"ok": true,
|
|
285
|
-
"data": {},
|
|
286
|
-
"meta": {
|
|
287
|
-
"game": "cs2",
|
|
288
|
-
"source": "match_summary",
|
|
289
|
-
"fetchedAt": "2026-07-27T12:00:00.000Z",
|
|
290
|
-
"requestId": "mcp_…",
|
|
291
|
-
"tookMs": 120,
|
|
292
|
-
"upstreamCalls": 3,
|
|
293
|
-
"rateLimit": { "tier": "pro", "remaining": 54 }
|
|
294
|
-
},
|
|
295
|
-
"pagination": { "limit": 20, "hasMore": false, "nextCursor": null },
|
|
296
|
-
"partial": [],
|
|
297
|
-
"error": null
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
| Condition | How to handle |
|
|
302
|
-
| --- | --- |
|
|
303
|
-
| `ok: false` | Tool-level failure; MCP `isError: true`; read `error.code` + `error.recover[]` |
|
|
304
|
-
| `ok: true` with `partial[]` | Use successful sections; **do not** treat as total failure |
|
|
305
|
-
| List tools | `data.items` + `pagination` (`limit`, `hasMore`, `nextCursor`, …) |
|
|
306
|
-
|
|
307
|
-
**Error codes:** `VALIDATION` · `NOT_FOUND` · `UNSUPPORTED_GAME` · `UNAUTHORIZED` · `RATE_LIMIT` · `UPSTREAM` · `PATH_NOT_ALLOWED` · `NOT_IMPLEMENTED`
|
|
308
|
-
|
|
309
|
-
**Ambiguity:** `resolve_entity` returns `ok: true` with `data.needsDisambiguation` + `data.candidates` — pick a candidate; do not wait for a hard error.
|
|
310
|
-
|
|
311
|
-
Retry only when `error.retryable` is true (typically `RATE_LIMIT`, `UPSTREAM`).
|
|
312
|
-
|
|
313
|
-
**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.
|
|
314
|
-
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
## Builder recipes
|
|
318
|
-
|
|
319
|
-
Use these as agent playbooks when scaffolding apps. Prefer one composite per screen over N+1 thin GETs.
|
|
320
|
-
|
|
321
|
-
### 1. Live scoreboard
|
|
322
|
-
|
|
323
|
-
**Goal:** multi-game (or single-game) live strip with optional match drill-down.
|
|
324
|
-
|
|
325
|
-
```
|
|
326
|
-
1. api_health # optional — once per session
|
|
327
|
-
2. live_matches { game: "all" } # or "cs2" / "lol" / …
|
|
328
|
-
3. For a selected row:
|
|
329
|
-
match_summary { game, matchId } # default card
|
|
330
|
-
match_details { …, includeLiveState: true } # only if needed
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
**UI map**
|
|
334
|
-
|
|
335
|
-
| Screen region | Source |
|
|
336
|
-
| --- | --- |
|
|
337
|
-
| Live rows (label, score, status) | `live_matches` → `data.sections[].items` |
|
|
338
|
-
| Expanded card | `match_summary` → `data.scoreline`, `playerPerformances`, `media` |
|
|
339
|
-
| In-game window | `match_details` sections `liveState` / `timeline` |
|
|
340
|
-
|
|
341
|
-
**Do not:** N+1 `match_details` for every live row; invent matchIds; use `upcoming_schedule` for live-only boards.
|
|
342
|
-
|
|
343
|
-
### 2. Player profile page
|
|
344
|
-
|
|
345
|
-
**Goal:** identity, team, recent form for a named player or fighter.
|
|
346
|
-
|
|
347
|
-
```
|
|
348
|
-
1. resolve_entity { q: "s1mple", game: "cs2", type: "player" }
|
|
349
|
-
# or type: "fighter" for UFC
|
|
350
|
-
2. player_profile {
|
|
351
|
-
game,
|
|
352
|
-
playerId | slug, # from resolve best/candidates
|
|
353
|
-
recentLimit: 10,
|
|
354
|
-
includeTrends: true,
|
|
355
|
-
view: "summary"
|
|
356
|
-
}
|
|
357
|
-
3. Optional: head_to_head for rivalry context (fighters/teams)
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
**UI map**
|
|
361
|
-
|
|
362
|
-
| Screen region | Source |
|
|
363
|
-
| --- | --- |
|
|
364
|
-
| Header / identity | `data.player` |
|
|
365
|
-
| Current team | `data.currentTeam` |
|
|
366
|
-
| Recent form list | `data.recentMatches` + `data.form` |
|
|
367
|
-
| Career / radar | `data.career`, `data.radar`, `data.seasonStats` (when present) |
|
|
368
|
-
|
|
369
|
-
**Do not:** invent playerIds; use `team_profile` for full rosters; skip resolve when the user only gave a display name.
|
|
370
|
-
|
|
371
|
-
### 3. Event hub
|
|
372
|
-
|
|
373
|
-
**Goal:** schedule strip + standings/rankings + match previews for a league, tournament, or event.
|
|
374
|
-
|
|
375
|
-
```
|
|
376
|
-
1. list_capabilities { job: "schedule" } # optional orientation
|
|
377
|
-
2. upcoming_schedule {
|
|
378
|
-
game: "cs2", # or lol / cod / ufc / dota2
|
|
379
|
-
hours: 168, # or from/to ISO window
|
|
380
|
-
league | tournamentId | team # scope filters as available
|
|
381
|
-
}
|
|
382
|
-
3. standings {
|
|
383
|
-
game,
|
|
384
|
-
scope: "event" | "league" | "tournament" | "world" | "division",
|
|
385
|
-
eventId | leagueId | tournamentId | division | season
|
|
386
|
-
}
|
|
387
|
-
4. For a featured upcoming match:
|
|
388
|
-
match_preview { game, matchId } # or teamA + teamB
|
|
389
|
-
5. When a match goes live / completes:
|
|
390
|
-
match_summary { game, matchId }
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
**UI map**
|
|
394
|
-
|
|
395
|
-
| Screen region | Source |
|
|
396
|
-
| --- | --- |
|
|
397
|
-
| Upcoming list | `upcoming_schedule` → `data.items` |
|
|
398
|
-
| Table / ranks | `standings` → `data.rows` |
|
|
399
|
-
| Featured match briefing | `match_preview` → `sideA` / `sideB` / `h2h` / `talkingPoints` |
|
|
400
|
-
| Live / completed card | `match_summary` |
|
|
401
|
-
|
|
402
|
-
**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"`.
|
|
403
|
-
|
|
404
|
-
### Other quick recipes (from `list_capabilities`)
|
|
405
|
-
|
|
406
|
-
| Recipe id | Steps |
|
|
407
|
-
| --- | --- |
|
|
408
|
-
| `live_ops_board` | `api_health` → `live_matches` → `match_summary` / `match_details` |
|
|
409
|
-
| `team_page` | `resolve_entity { type: "team" }` → `team_profile` |
|
|
410
|
-
| `match_center_completed` | schedule/live for `matchId` → `match_summary` → optional `match_details` |
|
|
411
|
-
| `app_scaffold` | `list_capabilities` ∥ `api_health` → sample composites → **ship REST in production** |
|
|
412
|
-
|
|
413
|
-
---
|
|
414
|
-
|
|
415
|
-
## Resources
|
|
416
|
-
|
|
417
|
-
| URI | Purpose |
|
|
418
|
-
| --- | --- |
|
|
419
|
-
| `cito://capabilities` | Static tool / game / workflow summary |
|
|
420
|
-
| `cito://llms.txt` | Live product agent context (may 404 upstream) |
|
|
421
|
-
| `cito://openapi.json` | Live public OpenAPI for **typed clients** — not for tool generation |
|
|
422
|
-
|
|
423
|
-
---
|
|
424
|
-
|
|
425
|
-
## Transports
|
|
426
|
-
|
|
427
|
-
**stdio** is the default (Claude Code, Cursor, Windsurf).
|
|
428
|
-
|
|
429
|
-
Optional **stateless Streamable HTTP**:
|
|
430
|
-
|
|
431
|
-
```bash
|
|
432
|
-
CITO_API_KEY=cito_your_key_here node dist/index.js --http 8787
|
|
433
|
-
# → http://127.0.0.1:8787/mcp
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
Hosted (production): `https://api.citoapi.com/mcp`
|
|
437
|
-
Send the user's Cito key as `x-api-key`. Initialize and `list_capabilities` work without a key so Smithery can scan.
|
|
438
|
-
|
|
439
|
-
---
|
|
440
|
-
|
|
441
|
-
## Migration from 0.1 (auto-generated tools)
|
|
442
|
-
|
|
443
|
-
**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.
|
|
444
|
-
|
|
445
|
-
| 0.1 pattern | 0.2 replacement |
|
|
446
|
-
| --- | --- |
|
|
447
|
-
| Generated per-path tools (`/cs2/matches/...`, etc.) | Outcome tools in the table above |
|
|
448
|
-
| `cito_live_overview` / ad-hoc live probes | `live_matches` (fixed CS2 path `/cs2/live`; UFC included) |
|
|
449
|
-
| `cito_api_health` | `api_health` |
|
|
450
|
-
| OpenAPI-named tools with `cito_` noise | **15** `snake_case` names, no `cito_` prefix |
|
|
451
|
-
| Raw HTTP-shaped responses | Stable envelope: `ok`, `data`, `meta`, `pagination`, `partial`, `error` |
|
|
452
|
-
| Agent stitches roster + matches + H2H | `team_profile`, `player_profile`, `head_to_head`, `match_preview` |
|
|
453
|
-
| Fortnite / niche paths as first-class tools | `call_api` allowlist |
|
|
454
|
-
| Spec cache / boot network for tool list | Static catalog; `list_capabilities` is offline |
|
|
455
|
-
|
|
456
|
-
**Action items for existing clients**
|
|
457
|
-
|
|
458
|
-
1. Drop any hardcoded 0.1 tool names; rebind prompts to the 15-tool catalog.
|
|
459
|
-
2. Always parse the envelope (`ok` / `partial` / `error.recover`) — do not assume bare REST JSON.
|
|
460
|
-
3. Use `resolve_entity` before profile tools when you only have a display name.
|
|
461
|
-
4. Prefer composites over reintroducing N+1 `call_api` loops.
|
|
462
|
-
5. Bump config to `npx cito-mcp` (or rebuild from this package) and re-test live board + one profile + one match flow.
|
|
463
|
-
|
|
464
|
-
There is **no compatibility shim** for 0.1 tool names in 0.2.
|
|
465
|
-
|
|
466
|
-
---
|
|
467
|
-
|
|
468
|
-
## Development
|
|
469
|
-
|
|
470
|
-
```bash
|
|
471
|
-
npm install
|
|
472
|
-
npm run build # tsc → dist/
|
|
473
|
-
npm test # node:test via tsx, offline (no network)
|
|
474
|
-
npm start # node dist/index.js (requires CITO_API_KEY)
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
Source layout (authoritative tool registry: `src/tools/index.ts`):
|
|
478
|
-
|
|
479
|
-
```
|
|
480
|
-
src/
|
|
481
|
-
index.ts # MCP wiring, resources, stdio / --http
|
|
482
|
-
client.ts # fetchJson, auth, present, cursors
|
|
483
|
-
envelope.ts # success / error envelopes, MCP isError
|
|
484
|
-
instructions.ts # server instructions for agents
|
|
485
|
-
tools/
|
|
486
|
-
index.ts # allTools (15)
|
|
487
|
-
meta.ts # list_capabilities, api_health, call_api
|
|
488
|
-
resolve.ts # resolve_entity, search_entities
|
|
489
|
-
live.ts # live_matches, upcoming_schedule
|
|
490
|
-
match.ts # match_summary, match_details
|
|
491
|
-
player.ts # player_profile
|
|
492
|
-
team.ts # team_profile, head_to_head
|
|
493
|
-
standings.ts # standings
|
|
494
|
-
insight.ts # match_preview, event_card
|
|
495
|
-
normalize.ts # cross-game normalization
|
|
496
|
-
types.ts # ToolDef, schemas
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
---
|
|
500
|
-
|
|
501
|
-
## Publish notes
|
|
502
|
-
|
|
503
|
-
Package: **`cito-mcp@0.2.4`**
|
|
504
|
-
|
|
505
|
-
| Item | Value |
|
|
506
|
-
| --- | --- |
|
|
507
|
-
| npm name | `cito-mcp` |
|
|
508
|
-
| bin | `cito-mcp` → `dist/index.js` |
|
|
509
|
-
| published files | `dist/`, `README.md` (`package.json` `"files"`) |
|
|
510
|
-
| engines | `node >= 20` |
|
|
511
|
-
| runtime dependency | `@modelcontextprotocol/sdk` only |
|
|
512
|
-
| prepublish | `npm run prepublishOnly` → `npm run build` |
|
|
513
|
-
|
|
514
|
-
### Pre-publish checklist
|
|
515
|
-
|
|
516
|
-
1. `npm test` — offline suite green.
|
|
517
|
-
2. `npm run build` — clean `dist/`.
|
|
518
|
-
3. Smoke against a real key (not in CI by default):
|
|
519
|
-
- `api_health`
|
|
520
|
-
- `live_matches`
|
|
521
|
-
- `resolve_entity` (e.g. T1 / s1mple)
|
|
522
|
-
- `match_summary` with a real `matchId` from live/schedule
|
|
523
|
-
4. Confirm `package.json` version and README match the shipped tool list.
|
|
524
|
-
5. `npm publish` from `mcp/` (or your release pipeline) with appropriate npm auth / access.
|
|
525
|
-
`prepublishOnly` runs build + tests + smoke, so a broken build cannot ship.
|
|
526
|
-
|
|
527
|
-
### Install line for docs & marketing
|
|
528
|
-
|
|
529
|
-
```bash
|
|
530
|
-
npx cito-mcp install --key cito_…
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
Manual fallback (note the quoted `--`, required for PowerShell):
|
|
534
|
-
|
|
535
|
-
```bash
|
|
536
|
-
claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
```json
|
|
540
|
-
{ "command": "npx", "args": ["cito-mcp"], "env": { "CITO_API_KEY": "cito_…" } }
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Semver expectations
|
|
544
|
-
|
|
545
|
-
- **0.2.4** is a **major surface break** vs 0.1 (tool rename + removal of OpenAPI mass-generation).
|
|
546
|
-
- 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.)
|
|
547
|
-
- Promoting Fortnite (or other titles) into the primary `game` enum would be a minor feature bump with catalog/docs updates.
|
|
548
|
-
|
|
549
|
-
---
|
|
550
|
-
|
|
551
|
-
## Changelog (summary)
|
|
552
|
-
|
|
553
|
-
### 0.4
|
|
554
|
-
|
|
555
|
-
- **
|
|
556
|
-
-
|
|
557
|
-
|
|
558
|
-
### 0.
|
|
559
|
-
|
|
560
|
-
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
- **`
|
|
583
|
-
- **`
|
|
584
|
-
- **`
|
|
585
|
-
-
|
|
586
|
-
- **`
|
|
587
|
-
- **`
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
-
|
|
606
|
-
-
|
|
607
|
-
-
|
|
608
|
-
|
|
609
|
-
|
|
1
|
+
# cito-mcp
|
|
2
|
+
|
|
3
|
+
Standalone [MCP](https://modelcontextprotocol.io) server for the [Cito esports API](https://api.citoapi.com) — **curated outcome tools** for agents building esports apps, dashboards, bots, and research flows.
|
|
4
|
+
|
|
5
|
+
**Version:** `0.4.9` · **Node:** `>=20` · **Install:** `npx cito-mcp`
|
|
6
|
+
|
|
7
|
+
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`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Philosophy: outcomes, not endpoints
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
**Ships 42 curated outcome tools** that answer *jobs* instead of mirroring REST:
|
|
16
|
+
|
|
17
|
+
| Job | Tool |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| What’s live right now? | `live_matches` |
|
|
20
|
+
| What’s coming up? | `upcoming_schedule` |
|
|
21
|
+
| Build a match card / recap | `match_summary` |
|
|
22
|
+
| Deep timeline / live state | `match_details` |
|
|
23
|
+
| Team page | `team_profile` |
|
|
24
|
+
| Player / fighter profile | `player_profile` |
|
|
25
|
+
| Player form & win streak | `player_form` |
|
|
26
|
+
| Table / rankings | `standings` |
|
|
27
|
+
| Ranking climbers & droppers | `rankings_movers` |
|
|
28
|
+
| Pre-match briefing (with surface) | `match_preview` |
|
|
29
|
+
| Event / fight-night card | `event_card` |
|
|
30
|
+
| Rivalry record | `head_to_head` |
|
|
31
|
+
| Tennis tournament catalog | `tournaments` |
|
|
32
|
+
| One day of tennis | `tennis_schedule` |
|
|
33
|
+
| Tennis player match log | `player_matches` |
|
|
34
|
+
| Tennis career statistics | `player_stats` |
|
|
35
|
+
| Tennis ranking trajectory | `player_rankings_history` |
|
|
36
|
+
| Fighter / team photos | `event_card` · `player_profile` |
|
|
37
|
+
| Which raw REST route exists? | `list_routes` |
|
|
38
|
+
| Name → ID | `resolve_entity` / `search_entities` |
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
**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.
|
|
43
|
+
|
|
44
|
+
### Honest gaps, in-band
|
|
45
|
+
|
|
46
|
+
Where the source data is partial, the tool says so instead of shipping a plausible-looking wrong value:
|
|
47
|
+
|
|
48
|
+
| Situation | What the tool returns |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| 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 |
|
|
51
|
+
| A tennis tournament draw is missing matches | `draw.shortRounds` (`["R128 45/64"]`), `draw.missingRounds`, `draw.complete` |
|
|
52
|
+
| A stats section the endpoint does not publish | `player_stats.unavailable` names the section and why, rather than a block of nulls |
|
|
53
|
+
| `search_entities` results that do not match at all | dropped, counted under `data.relevance.droppedBelowFloor`, with a warning naming them |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
### One command (recommended)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx cito-mcp install --key cito_your_key_here
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx cito-mcp install --dry-run # show the plan, write nothing
|
|
71
|
+
npx cito-mcp install --key … --client cursor # one client only
|
|
72
|
+
npx cito-mcp install --help
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Get a key at [citoapi.com/dashboard](https://citoapi.com/dashboard).
|
|
76
|
+
|
|
77
|
+
### Manual — Claude Code
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" npx -y cito-mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**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.
|
|
84
|
+
|
|
85
|
+
From a local clone (development):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cd mcp
|
|
89
|
+
npm install
|
|
90
|
+
npm run build
|
|
91
|
+
claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" node "%CD%\dist\index.js"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Cursor
|
|
95
|
+
|
|
96
|
+
Add to Cursor MCP settings (e.g. `.cursor/mcp.json` or Cursor Settings → MCP):
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"cito": {
|
|
102
|
+
"command": "npx",
|
|
103
|
+
"args": ["cito-mcp"],
|
|
104
|
+
"env": {
|
|
105
|
+
"CITO_API_KEY": "cito_your_key_here"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Windows local clone:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"cito": {
|
|
118
|
+
"command": "node",
|
|
119
|
+
"args": ["C:/Users/Owner/Projects/cito-api-scraper-windows/mcp/dist/index.js"],
|
|
120
|
+
"env": {
|
|
121
|
+
"CITO_API_KEY": "cito_your_key_here"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Windsurf / other MCP hosts
|
|
129
|
+
|
|
130
|
+
Same shape as Cursor: `command` + `args` + `env.CITO_API_KEY`. Prefer `npx cito-mcp` so clients pick up published patches without a local install.
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Environment
|
|
137
|
+
|
|
138
|
+
| Variable | Required | Default | Description |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| `CITO_API_KEY` | **yes** | — | Cito API key → `x-api-key` on every upstream call |
|
|
141
|
+
| `CITO_API_BASE` | no | `https://api.citoapi.com/api/v1` | Override base URL (staging / self-hosted) |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Preferred tool order
|
|
146
|
+
|
|
147
|
+
1. Unsure → `list_capabilities` (optional `api_health` for tier / included games)
|
|
148
|
+
2. Name without ID → `resolve_entity` (chain) or `search_entities` (browse)
|
|
149
|
+
3. Live / upcoming → `live_matches` / `upcoming_schedule`
|
|
150
|
+
4. Match card → `match_summary` (then `match_details` if needed)
|
|
151
|
+
5. Team / player pages → `team_profile` / `player_profile`
|
|
152
|
+
6. Tables / rivalry / preview / event card → `standings` / `head_to_head` / `match_preview` / `event_card`
|
|
153
|
+
7. Escape hatch → `list_routes` to find a path, then `call_api` (allowlisted paths only)
|
|
154
|
+
|
|
155
|
+
**Never invent IDs** — resolve them or take them from live / schedule / search results.
|
|
156
|
+
|
|
157
|
+
Mnemonic: **resolve → live/schedule → summary → deep**.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Tool catalog (16)
|
|
162
|
+
|
|
163
|
+
All tools are **read-only**. Names are `snake_case` with **no** `cito_` prefix (the server name already brands the surface).
|
|
164
|
+
|
|
165
|
+
| Tool | Outcome | When to use | When not to use | Key inputs |
|
|
166
|
+
| --- | --- | --- | --- | --- |
|
|
167
|
+
| `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?` |
|
|
168
|
+
| `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?` |
|
|
169
|
+
| `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?` |
|
|
170
|
+
| `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?` |
|
|
171
|
+
| `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?` |
|
|
172
|
+
| `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?` |
|
|
173
|
+
| `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?` |
|
|
174
|
+
| `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?` |
|
|
175
|
+
| `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?` |
|
|
176
|
+
| `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?` |
|
|
177
|
+
| `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?` |
|
|
178
|
+
| `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?` |
|
|
179
|
+
| `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?` |
|
|
180
|
+
| `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?` |
|
|
181
|
+
| `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?` |
|
|
182
|
+
| `call_api` | Allowlisted raw REST (`data.raw`) | Fortnite / long-tail paths; payload debugging | Any job covered by a curated tool | `path`, `method?`, `queryJson?`, `bodyJson?` |
|
|
183
|
+
|
|
184
|
+
### Images
|
|
185
|
+
|
|
186
|
+
Fighter and team sides carry an `images` object wherever upstream supplies one —
|
|
187
|
+
on `event_card` bout corners and on `player_profile`. No extra call, no N+1.
|
|
188
|
+
|
|
189
|
+
```jsonc
|
|
190
|
+
"team1": {
|
|
191
|
+
"name": "Islam Makhachev",
|
|
192
|
+
"slug": "islam-makhachev",
|
|
193
|
+
"record": { "wins": 28, "losses": 1, "draws": 0, "text": "28-1-0 (W-L-D)" },
|
|
194
|
+
"championStatus": "champion",
|
|
195
|
+
"images": {
|
|
196
|
+
"headshotUrl": "https://…/MAKHACHEV_ISLAM_BELT_01-18.png",
|
|
197
|
+
"bodyImageUrl": "https://…/athlete_bio_full_body/…",
|
|
198
|
+
"imageUrl": "https://…/event_fight_card_upper_body/…",
|
|
199
|
+
"proxiedImageUrl": "https://api.citoapi.com/api/v1/public/images/ufc/aHR0cHM6…"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Use `proxiedImageUrl` in a browser.** the image host sends no CORS header and can
|
|
205
|
+
hotlink-block, so raw URLs render as broken images in a web UI. The proxied URL
|
|
206
|
+
is served by the API and is safe to put in an `<img src>`.
|
|
207
|
+
|
|
208
|
+
When a key is `null` the image does not exist for that entity; when the whole
|
|
209
|
+
`images` object is absent, upstream sent nothing for that side. The object is
|
|
210
|
+
never partially shaped — if any image exists, all four keys are present.
|
|
211
|
+
|
|
212
|
+
Tennis is the exception on purpose: the feed supplies a single `portrait_url`
|
|
213
|
+
(Wikimedia Commons) and no square crop, so that one URL fills both `headshotUrl`
|
|
214
|
+
and `imageUrl`, and `proxiedImageUrl` stays `null` because only fighter image URLs have
|
|
215
|
+
a proxy.
|
|
216
|
+
|
|
217
|
+
**Tennis images carry their licence and credit** — and a UI is expected to render
|
|
218
|
+
them:
|
|
219
|
+
|
|
220
|
+
```jsonc
|
|
221
|
+
"images": {
|
|
222
|
+
"headshotUrl": "https://commons.wikimedia.org/wiki/Special:FilePath/Hoang_WMQ18_%2833%29_%2842647295495%29.jpg?width=400",
|
|
223
|
+
"license": "CC BY-SA 2.0",
|
|
224
|
+
"attribution": "si.robi",
|
|
225
|
+
"sourceUrl": "https://commons.wikimedia.org/wiki/File:Hoang_WMQ18_(33)_(42647295495).jpg"
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Commons portraits are mostly CC BY / CC BY-SA, which for commercial display
|
|
230
|
+
generally requires naming the author and stating the licence. `attribution` is
|
|
231
|
+
`null` for public-domain and CC0 images, which need no credit — that is a fact,
|
|
232
|
+
not missing data. Anything without a defensible licence has its URL removed
|
|
233
|
+
upstream, so a non-null `headshotUrl` always has a `license` beside it.
|
|
234
|
+
|
|
235
|
+
### Games
|
|
236
|
+
|
|
237
|
+
| Game | Depth | Notes |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| `lol` | deep | Team slugs; `lol-match-*`; no native global search |
|
|
240
|
+
| `cs2` | deep | Prefixed ids; live path **`GET /cs2/live`**; trends / rankings |
|
|
241
|
+
| `dota2` | thin | Numeric ids; radar; weak/no standings & roster endpoints |
|
|
242
|
+
| `cod` | medium | Org slugs; CDL standings; UUID match ids |
|
|
243
|
+
| `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") |
|
|
244
|
+
|
|
245
|
+
### Combat leagues (`combat_*`, `league` argument)
|
|
246
|
+
|
|
247
|
+
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.
|
|
248
|
+
|
|
249
|
+
| League | Scoring | Notes |
|
|
250
|
+
| --- | --- | --- |
|
|
251
|
+
| `raf` | per PERIOD (wrestling points, takedowns) | Athletes; champions per division |
|
|
252
|
+
| `bkfc` | per ROUND (punches landed/thrown, knockdowns) | Fighters; official rankings + champions |
|
|
253
|
+
| 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. |
|
|
254
|
+
|
|
255
|
+
### UFC projection notes (0.2.4)
|
|
256
|
+
|
|
257
|
+
Hardened agent-facing UFC shapes (offline-tested):
|
|
258
|
+
|
|
259
|
+
- **`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).
|
|
260
|
+
- **`live_matches`** uses `extractLiveRows`: prefer `liveBouts` (empty array = honest empty board); never promote supervisor `events[]` shells into match rows.
|
|
261
|
+
- **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.
|
|
262
|
+
- **`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 ?`.
|
|
263
|
+
- **`event_card`** bouts share the same `normalizeMatch` path.
|
|
264
|
+
- **`standings`** maps division champions to rank **`C`** (`rankText: "C"`, interim **`IC`**); contender `#1` stays numeric `1` — no dual numeric `#1`.
|
|
265
|
+
|
|
266
|
+
Fortnite: **`call_api` only** until promoted into the primary enum.
|
|
267
|
+
|
|
268
|
+
### `call_api` allowlist
|
|
269
|
+
|
|
270
|
+
Path must start with `/` and match one of:
|
|
271
|
+
|
|
272
|
+
`/health` · `/lol` · `/cs2` · `/dota2` · `/cod` · `/ufc` · `/fortnite` · `/tennis` · `/raf` · `/bkfc` · `/bellator` · `/pride` · `/strikeforce` · `/wec` · `/dream` · `/elitexc` · `/affliction` · `/mma` · `/openapi.json`
|
|
273
|
+
|
|
274
|
+
Absolute URLs and `..` traversal → `PATH_NOT_ALLOWED`. Prefer curated tools for all standard jobs.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Response envelope
|
|
279
|
+
|
|
280
|
+
Every tool returns one JSON object:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"ok": true,
|
|
285
|
+
"data": {},
|
|
286
|
+
"meta": {
|
|
287
|
+
"game": "cs2",
|
|
288
|
+
"source": "match_summary",
|
|
289
|
+
"fetchedAt": "2026-07-27T12:00:00.000Z",
|
|
290
|
+
"requestId": "mcp_…",
|
|
291
|
+
"tookMs": 120,
|
|
292
|
+
"upstreamCalls": 3,
|
|
293
|
+
"rateLimit": { "tier": "pro", "remaining": 54 }
|
|
294
|
+
},
|
|
295
|
+
"pagination": { "limit": 20, "hasMore": false, "nextCursor": null },
|
|
296
|
+
"partial": [],
|
|
297
|
+
"error": null
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
| Condition | How to handle |
|
|
302
|
+
| --- | --- |
|
|
303
|
+
| `ok: false` | Tool-level failure; MCP `isError: true`; read `error.code` + `error.recover[]` |
|
|
304
|
+
| `ok: true` with `partial[]` | Use successful sections; **do not** treat as total failure |
|
|
305
|
+
| List tools | `data.items` + `pagination` (`limit`, `hasMore`, `nextCursor`, …) |
|
|
306
|
+
|
|
307
|
+
**Error codes:** `VALIDATION` · `NOT_FOUND` · `UNSUPPORTED_GAME` · `UNAUTHORIZED` · `RATE_LIMIT` · `UPSTREAM` · `PATH_NOT_ALLOWED` · `NOT_IMPLEMENTED`
|
|
308
|
+
|
|
309
|
+
**Ambiguity:** `resolve_entity` returns `ok: true` with `data.needsDisambiguation` + `data.candidates` — pick a candidate; do not wait for a hard error.
|
|
310
|
+
|
|
311
|
+
Retry only when `error.retryable` is true (typically `RATE_LIMIT`, `UPSTREAM`).
|
|
312
|
+
|
|
313
|
+
**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.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Builder recipes
|
|
318
|
+
|
|
319
|
+
Use these as agent playbooks when scaffolding apps. Prefer one composite per screen over N+1 thin GETs.
|
|
320
|
+
|
|
321
|
+
### 1. Live scoreboard
|
|
322
|
+
|
|
323
|
+
**Goal:** multi-game (or single-game) live strip with optional match drill-down.
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
1. api_health # optional — once per session
|
|
327
|
+
2. live_matches { game: "all" } # or "cs2" / "lol" / …
|
|
328
|
+
3. For a selected row:
|
|
329
|
+
match_summary { game, matchId } # default card
|
|
330
|
+
match_details { …, includeLiveState: true } # only if needed
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**UI map**
|
|
334
|
+
|
|
335
|
+
| Screen region | Source |
|
|
336
|
+
| --- | --- |
|
|
337
|
+
| Live rows (label, score, status) | `live_matches` → `data.sections[].items` |
|
|
338
|
+
| Expanded card | `match_summary` → `data.scoreline`, `playerPerformances`, `media` |
|
|
339
|
+
| In-game window | `match_details` sections `liveState` / `timeline` |
|
|
340
|
+
|
|
341
|
+
**Do not:** N+1 `match_details` for every live row; invent matchIds; use `upcoming_schedule` for live-only boards.
|
|
342
|
+
|
|
343
|
+
### 2. Player profile page
|
|
344
|
+
|
|
345
|
+
**Goal:** identity, team, recent form for a named player or fighter.
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
1. resolve_entity { q: "s1mple", game: "cs2", type: "player" }
|
|
349
|
+
# or type: "fighter" for UFC
|
|
350
|
+
2. player_profile {
|
|
351
|
+
game,
|
|
352
|
+
playerId | slug, # from resolve best/candidates
|
|
353
|
+
recentLimit: 10,
|
|
354
|
+
includeTrends: true,
|
|
355
|
+
view: "summary"
|
|
356
|
+
}
|
|
357
|
+
3. Optional: head_to_head for rivalry context (fighters/teams)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**UI map**
|
|
361
|
+
|
|
362
|
+
| Screen region | Source |
|
|
363
|
+
| --- | --- |
|
|
364
|
+
| Header / identity | `data.player` |
|
|
365
|
+
| Current team | `data.currentTeam` |
|
|
366
|
+
| Recent form list | `data.recentMatches` + `data.form` |
|
|
367
|
+
| Career / radar | `data.career`, `data.radar`, `data.seasonStats` (when present) |
|
|
368
|
+
|
|
369
|
+
**Do not:** invent playerIds; use `team_profile` for full rosters; skip resolve when the user only gave a display name.
|
|
370
|
+
|
|
371
|
+
### 3. Event hub
|
|
372
|
+
|
|
373
|
+
**Goal:** schedule strip + standings/rankings + match previews for a league, tournament, or event.
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
1. list_capabilities { job: "schedule" } # optional orientation
|
|
377
|
+
2. upcoming_schedule {
|
|
378
|
+
game: "cs2", # or lol / cod / ufc / dota2
|
|
379
|
+
hours: 168, # or from/to ISO window
|
|
380
|
+
league | tournamentId | team # scope filters as available
|
|
381
|
+
}
|
|
382
|
+
3. standings {
|
|
383
|
+
game,
|
|
384
|
+
scope: "event" | "league" | "tournament" | "world" | "division",
|
|
385
|
+
eventId | leagueId | tournamentId | division | season
|
|
386
|
+
}
|
|
387
|
+
4. For a featured upcoming match:
|
|
388
|
+
match_preview { game, matchId } # or teamA + teamB
|
|
389
|
+
5. When a match goes live / completes:
|
|
390
|
+
match_summary { game, matchId }
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**UI map**
|
|
394
|
+
|
|
395
|
+
| Screen region | Source |
|
|
396
|
+
| --- | --- |
|
|
397
|
+
| Upcoming list | `upcoming_schedule` → `data.items` |
|
|
398
|
+
| Table / ranks | `standings` → `data.rows` |
|
|
399
|
+
| Featured match briefing | `match_preview` → `sideA` / `sideB` / `h2h` / `talkingPoints` |
|
|
400
|
+
| Live / completed card | `match_summary` |
|
|
401
|
+
|
|
402
|
+
**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"`.
|
|
403
|
+
|
|
404
|
+
### Other quick recipes (from `list_capabilities`)
|
|
405
|
+
|
|
406
|
+
| Recipe id | Steps |
|
|
407
|
+
| --- | --- |
|
|
408
|
+
| `live_ops_board` | `api_health` → `live_matches` → `match_summary` / `match_details` |
|
|
409
|
+
| `team_page` | `resolve_entity { type: "team" }` → `team_profile` |
|
|
410
|
+
| `match_center_completed` | schedule/live for `matchId` → `match_summary` → optional `match_details` |
|
|
411
|
+
| `app_scaffold` | `list_capabilities` ∥ `api_health` → sample composites → **ship REST in production** |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Resources
|
|
416
|
+
|
|
417
|
+
| URI | Purpose |
|
|
418
|
+
| --- | --- |
|
|
419
|
+
| `cito://capabilities` | Static tool / game / workflow summary |
|
|
420
|
+
| `cito://llms.txt` | Live product agent context (may 404 upstream) |
|
|
421
|
+
| `cito://openapi.json` | Live public OpenAPI for **typed clients** — not for tool generation |
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Transports
|
|
426
|
+
|
|
427
|
+
**stdio** is the default (Claude Code, Cursor, Windsurf).
|
|
428
|
+
|
|
429
|
+
Optional **stateless Streamable HTTP**:
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
CITO_API_KEY=cito_your_key_here node dist/index.js --http 8787
|
|
433
|
+
# → http://127.0.0.1:8787/mcp
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Hosted (production): `https://api.citoapi.com/mcp`
|
|
437
|
+
Send the user's Cito key as `x-api-key`. Initialize and `list_capabilities` work without a key so Smithery can scan.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Migration from 0.1 (auto-generated tools)
|
|
442
|
+
|
|
443
|
+
**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.
|
|
444
|
+
|
|
445
|
+
| 0.1 pattern | 0.2 replacement |
|
|
446
|
+
| --- | --- |
|
|
447
|
+
| Generated per-path tools (`/cs2/matches/...`, etc.) | Outcome tools in the table above |
|
|
448
|
+
| `cito_live_overview` / ad-hoc live probes | `live_matches` (fixed CS2 path `/cs2/live`; UFC included) |
|
|
449
|
+
| `cito_api_health` | `api_health` |
|
|
450
|
+
| OpenAPI-named tools with `cito_` noise | **15** `snake_case` names, no `cito_` prefix |
|
|
451
|
+
| Raw HTTP-shaped responses | Stable envelope: `ok`, `data`, `meta`, `pagination`, `partial`, `error` |
|
|
452
|
+
| Agent stitches roster + matches + H2H | `team_profile`, `player_profile`, `head_to_head`, `match_preview` |
|
|
453
|
+
| Fortnite / niche paths as first-class tools | `call_api` allowlist |
|
|
454
|
+
| Spec cache / boot network for tool list | Static catalog; `list_capabilities` is offline |
|
|
455
|
+
|
|
456
|
+
**Action items for existing clients**
|
|
457
|
+
|
|
458
|
+
1. Drop any hardcoded 0.1 tool names; rebind prompts to the 15-tool catalog.
|
|
459
|
+
2. Always parse the envelope (`ok` / `partial` / `error.recover`) — do not assume bare REST JSON.
|
|
460
|
+
3. Use `resolve_entity` before profile tools when you only have a display name.
|
|
461
|
+
4. Prefer composites over reintroducing N+1 `call_api` loops.
|
|
462
|
+
5. Bump config to `npx cito-mcp` (or rebuild from this package) and re-test live board + one profile + one match flow.
|
|
463
|
+
|
|
464
|
+
There is **no compatibility shim** for 0.1 tool names in 0.2.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Development
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
npm install
|
|
472
|
+
npm run build # tsc → dist/
|
|
473
|
+
npm test # node:test via tsx, offline (no network)
|
|
474
|
+
npm start # node dist/index.js (requires CITO_API_KEY)
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Source layout (authoritative tool registry: `src/tools/index.ts`):
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
src/
|
|
481
|
+
index.ts # MCP wiring, resources, stdio / --http
|
|
482
|
+
client.ts # fetchJson, auth, present, cursors
|
|
483
|
+
envelope.ts # success / error envelopes, MCP isError
|
|
484
|
+
instructions.ts # server instructions for agents
|
|
485
|
+
tools/
|
|
486
|
+
index.ts # allTools (15)
|
|
487
|
+
meta.ts # list_capabilities, api_health, call_api
|
|
488
|
+
resolve.ts # resolve_entity, search_entities
|
|
489
|
+
live.ts # live_matches, upcoming_schedule
|
|
490
|
+
match.ts # match_summary, match_details
|
|
491
|
+
player.ts # player_profile
|
|
492
|
+
team.ts # team_profile, head_to_head
|
|
493
|
+
standings.ts # standings
|
|
494
|
+
insight.ts # match_preview, event_card
|
|
495
|
+
normalize.ts # cross-game normalization
|
|
496
|
+
types.ts # ToolDef, schemas
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Publish notes
|
|
502
|
+
|
|
503
|
+
Package: **`cito-mcp@0.2.4`**
|
|
504
|
+
|
|
505
|
+
| Item | Value |
|
|
506
|
+
| --- | --- |
|
|
507
|
+
| npm name | `cito-mcp` |
|
|
508
|
+
| bin | `cito-mcp` → `dist/index.js` |
|
|
509
|
+
| published files | `dist/`, `README.md` (`package.json` `"files"`) |
|
|
510
|
+
| engines | `node >= 20` |
|
|
511
|
+
| runtime dependency | `@modelcontextprotocol/sdk` only |
|
|
512
|
+
| prepublish | `npm run prepublishOnly` → `npm run build` |
|
|
513
|
+
|
|
514
|
+
### Pre-publish checklist
|
|
515
|
+
|
|
516
|
+
1. `npm test` — offline suite green.
|
|
517
|
+
2. `npm run build` — clean `dist/`.
|
|
518
|
+
3. Smoke against a real key (not in CI by default):
|
|
519
|
+
- `api_health`
|
|
520
|
+
- `live_matches`
|
|
521
|
+
- `resolve_entity` (e.g. T1 / s1mple)
|
|
522
|
+
- `match_summary` with a real `matchId` from live/schedule
|
|
523
|
+
4. Confirm `package.json` version and README match the shipped tool list.
|
|
524
|
+
5. `npm publish` from `mcp/` (or your release pipeline) with appropriate npm auth / access.
|
|
525
|
+
`prepublishOnly` runs build + tests + smoke, so a broken build cannot ship.
|
|
526
|
+
|
|
527
|
+
### Install line for docs & marketing
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
npx cito-mcp install --key cito_…
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Manual fallback (note the quoted `--`, required for PowerShell):
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
{ "command": "npx", "args": ["cito-mcp"], "env": { "CITO_API_KEY": "cito_…" } }
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Semver expectations
|
|
544
|
+
|
|
545
|
+
- **0.2.4** is a **major surface break** vs 0.1 (tool rename + removal of OpenAPI mass-generation).
|
|
546
|
+
- 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.)
|
|
547
|
+
- Promoting Fortnite (or other titles) into the primary `game` enum would be a minor feature bump with catalog/docs updates.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Changelog (summary)
|
|
552
|
+
|
|
553
|
+
### 0.5.4
|
|
554
|
+
|
|
555
|
+
- **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`.
|
|
556
|
+
- **`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`.
|
|
557
|
+
|
|
558
|
+
### 0.5.3
|
|
559
|
+
|
|
560
|
+
- **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 `429`s 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.
|
|
561
|
+
- `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.
|
|
562
|
+
- 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.
|
|
563
|
+
|
|
564
|
+
### 0.4.9
|
|
565
|
+
|
|
566
|
+
- **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.
|
|
567
|
+
- 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`).
|
|
568
|
+
|
|
569
|
+
### 0.4.8
|
|
570
|
+
|
|
571
|
+
- RAF wrestling and BKFC bare knuckle: six league-parameterised `combat_*` tools, and `/raf` + `/bkfc` on the `call_api` allowlist.
|
|
572
|
+
|
|
573
|
+
### 0.4.7
|
|
574
|
+
|
|
575
|
+
- Removed the unregistered tennis odds module and its README entries; `match_details` `sections:["odds"]` stays UFC-only and answers `NOT_IMPLEMENTED` for other games.
|
|
576
|
+
|
|
577
|
+
### 0.4.2
|
|
578
|
+
|
|
579
|
+
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.
|
|
580
|
+
|
|
581
|
+
- **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.
|
|
582
|
+
- **`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.
|
|
583
|
+
- **`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`.
|
|
584
|
+
- **`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.
|
|
585
|
+
- **`/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).
|
|
586
|
+
- **`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**.
|
|
587
|
+
- **`bestOf`** now reads `best_of`, the only spelling the tennis route emits.
|
|
588
|
+
|
|
589
|
+
### 0.4.1
|
|
590
|
+
|
|
591
|
+
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.
|
|
592
|
+
|
|
593
|
+
- **`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`.
|
|
594
|
+
- **`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.
|
|
595
|
+
- **`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.
|
|
596
|
+
- **`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`.
|
|
597
|
+
- **`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`.
|
|
598
|
+
- **`tennis_schedule`** now counts the day's list/completed overlap, summarises statuses, and flags date-only `startsAt` placeholders.
|
|
599
|
+
- **`live_matches` labelled tennis rows by tournament**, not players, because the live route's `name` is the event.
|
|
600
|
+
- **Tennis players had no images** although `/tennis/players/{id}` carries `portrait_url`. Now surfaced (plus `ioc` as the side country on boards).
|
|
601
|
+
- **`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.
|
|
602
|
+
|
|
603
|
+
### 0.2.4
|
|
604
|
+
|
|
605
|
+
- **UFC projection hardening** (offline-tested):
|
|
606
|
+
- `normalizeMatch`: `fighters[]` corners + profile, live red/blue, `boutId`/`dataId`; never keep `? vs ?` when names exist
|
|
607
|
+
- `extractRows` / `extractLiveRows`: prefer `liveBouts` (including empty); never fake match rows from supervisor `events[]`
|
|
608
|
+
- `upcoming_schedule`: client-side `hours`/`from`/`to`; bout expansion with fighter labels; event shells labeled by event name
|
|
609
|
+
- `event_card` bouts share the same normalize path
|
|
610
|
+
- `standings`: champion rank `C` / interim `IC`; no dual numeric `#1` for champ + contender
|
|
611
|
+
|
|
612
|
+
### 0.2.4
|
|
613
|
+
|
|
614
|
+
- **Breaking:** replaced OpenAPI mass-generated tools with **15 curated outcome tools**
|
|
615
|
+
- Stable JSON envelope + MCP server instructions
|
|
616
|
+
- Fixed CS2 live path (`/cs2/live`); UFC in live fan-out
|
|
617
|
+
- Composites with `partial[]` recovery; `call_api` path allowlist
|
|
618
|
+
- Resources: `cito://capabilities`, `cito://llms.txt`, `cito://openapi.json`
|
|
619
|
+
|
|
620
|
+
See [`_workflow/CHANGELOG-0.2.md`](_workflow/CHANGELOG-0.2.md) for the full 0.2 write-up.
|