cito-mcp 0.5.8 → 0.6.0
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 +21 -21
- package/dist/envelope.js +9 -9
- package/dist/index.js +2 -2
- package/dist/instructions.js +10 -4
- package/dist/scrub.js +42 -4
- package/dist/tools/combat.js +6 -6
- package/dist/tools/index.js +2 -0
- package/dist/tools/insight.js +12 -10
- package/dist/tools/leaderboard.js +2 -2
- package/dist/tools/live.js +8 -7
- package/dist/tools/match.js +65 -22
- package/dist/tools/meta.js +46 -11
- package/dist/tools/normalize.js +3 -2
- package/dist/tools/player.js +44 -5
- package/dist/tools/rankings.js +35 -2
- package/dist/tools/resolve.js +11 -11
- package/dist/tools/schedule.js +7 -6
- package/dist/tools/standings.js +43 -23
- package/dist/tools/team.js +8 -6
- package/dist/tools/tournaments.js +5 -7
- package/dist/tools/types.js +1 -1
- package/dist/tools/webhooks.js +377 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Composites multi-fetch server-side, return a **stable JSON envelope**, and isola
|
|
|
43
43
|
|
|
44
44
|
### Honest gaps, in-band
|
|
45
45
|
|
|
46
|
-
Where
|
|
46
|
+
Where data is partial, the tool says so instead of shipping a plausible-looking wrong value:
|
|
47
47
|
|
|
48
48
|
| Situation | What the tool returns |
|
|
49
49
|
| --- | --- |
|
|
@@ -116,7 +116,7 @@ Windows local clone:
|
|
|
116
116
|
"mcpServers": {
|
|
117
117
|
"cito": {
|
|
118
118
|
"command": "node",
|
|
119
|
-
"args": ["
|
|
119
|
+
"args": ["/absolute/path/to/cito-mcp/dist/index.js"],
|
|
120
120
|
"env": {
|
|
121
121
|
"CITO_API_KEY": "cito_your_key_here"
|
|
122
122
|
}
|
|
@@ -137,7 +137,7 @@ The server **exits immediately** if `CITO_API_KEY` is missing. The key is sent o
|
|
|
137
137
|
|
|
138
138
|
| Variable | Required | Default | Description |
|
|
139
139
|
| --- | --- | --- | --- |
|
|
140
|
-
| `CITO_API_KEY` | **yes** | — | Cito API key → `x-api-key` on every
|
|
140
|
+
| `CITO_API_KEY` | **yes** | — | Cito API key → `x-api-key` on every API call |
|
|
141
141
|
| `CITO_API_BASE` | no | `https://api.citoapi.com/api/v1` | Override base URL (staging / self-hosted) |
|
|
142
142
|
|
|
143
143
|
---
|
|
@@ -160,7 +160,7 @@ Mnemonic: **resolve → live/schedule → summary → deep**.
|
|
|
160
160
|
|
|
161
161
|
## Tool catalog (16)
|
|
162
162
|
|
|
163
|
-
All tools are **read-only
|
|
163
|
+
All tools are **read-only** except the webhook tools (`create_webhook`, `update_webhook`, `delete_webhook`, `regenerate_webhook_secret`, `test_webhook`, `replay_webhook`, `catch_up_webhook`, and the inbox tools), which only touch your own webhooks and test inboxes. Names are `snake_case` with **no** `cito_` prefix (the server name already brands the surface).
|
|
164
164
|
|
|
165
165
|
| Tool | Outcome | When to use | When not to use | Key inputs |
|
|
166
166
|
| --- | --- | --- | --- | --- |
|
|
@@ -183,7 +183,7 @@ All tools are **read-only**. Names are `snake_case` with **no** `cito_` prefix (
|
|
|
183
183
|
|
|
184
184
|
### Images
|
|
185
185
|
|
|
186
|
-
Fighter and team sides carry an `images` object wherever
|
|
186
|
+
Fighter and team sides carry an `images` object wherever one exists —
|
|
187
187
|
on `event_card` bout corners and on `player_profile`. No extra call, no N+1.
|
|
188
188
|
|
|
189
189
|
```jsonc
|
|
@@ -206,11 +206,11 @@ hotlink-block, so raw URLs render as broken images in a web UI. The proxied URL
|
|
|
206
206
|
is served by the API and is safe to put in an `<img src>`.
|
|
207
207
|
|
|
208
208
|
When a key is `null` the image does not exist for that entity; when the whole
|
|
209
|
-
`images` object is absent,
|
|
209
|
+
`images` object is absent, there is no image for that side. The object is
|
|
210
210
|
never partially shaped — if any image exists, all four keys are present.
|
|
211
211
|
|
|
212
|
-
Tennis is the exception on purpose:
|
|
213
|
-
|
|
212
|
+
Tennis is the exception on purpose: there is a single `portrait_url`
|
|
213
|
+
and no square crop, so that one URL fills both `headshotUrl`
|
|
214
214
|
and `imageUrl`, and `proxiedImageUrl` stays `null` because only fighter image URLs have
|
|
215
215
|
a proxy.
|
|
216
216
|
|
|
@@ -229,8 +229,8 @@ them:
|
|
|
229
229
|
Commons portraits are mostly CC BY / CC BY-SA, which for commercial display
|
|
230
230
|
generally requires naming the author and stating the licence. `attribution` is
|
|
231
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
|
-
|
|
232
|
+
not missing data. Anything without a defensible licence has its URL removed,
|
|
233
|
+
so a non-null `headshotUrl` always has a `license` beside it.
|
|
234
234
|
|
|
235
235
|
### Games
|
|
236
236
|
|
|
@@ -285,11 +285,11 @@ Every tool returns one JSON object:
|
|
|
285
285
|
"data": {},
|
|
286
286
|
"meta": {
|
|
287
287
|
"game": "cs2",
|
|
288
|
-
"
|
|
288
|
+
"tool": "match_summary",
|
|
289
289
|
"fetchedAt": "2026-07-27T12:00:00.000Z",
|
|
290
290
|
"requestId": "mcp_…",
|
|
291
291
|
"tookMs": 120,
|
|
292
|
-
"
|
|
292
|
+
"apiCalls": 3,
|
|
293
293
|
"rateLimit": { "tier": "pro", "remaining": 54 }
|
|
294
294
|
},
|
|
295
295
|
"pagination": { "limit": 20, "hasMore": false, "nextCursor": null },
|
|
@@ -304,11 +304,11 @@ Every tool returns one JSON object:
|
|
|
304
304
|
| `ok: true` with `partial[]` | Use successful sections; **do not** treat as total failure |
|
|
305
305
|
| List tools | `data.items` + `pagination` (`limit`, `hasMore`, `nextCursor`, …) |
|
|
306
306
|
|
|
307
|
-
**Error codes:** `VALIDATION` · `NOT_FOUND` · `UNSUPPORTED_GAME` · `UNAUTHORIZED` · `RATE_LIMIT` · `
|
|
307
|
+
**Error codes:** `VALIDATION` · `NOT_FOUND` · `UNSUPPORTED_GAME` · `UNAUTHORIZED` · `RATE_LIMIT` · `SERVICE_ERROR` · `PATH_NOT_ALLOWED` · `NOT_IMPLEMENTED`
|
|
308
308
|
|
|
309
309
|
**Ambiguity:** `resolve_entity` returns `ok: true` with `data.needsDisambiguation` + `data.candidates` — pick a candidate; do not wait for a hard error.
|
|
310
310
|
|
|
311
|
-
Retry only when `error.retryable` is true (typically `RATE_LIMIT`, `
|
|
311
|
+
Retry only when `error.retryable` is true (typically `RATE_LIMIT`, `SERVICE_ERROR`).
|
|
312
312
|
|
|
313
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
314
|
|
|
@@ -417,7 +417,7 @@ Use these as agent playbooks when scaffolding apps. Prefer one composite per scr
|
|
|
417
417
|
| URI | Purpose |
|
|
418
418
|
| --- | --- |
|
|
419
419
|
| `cito://capabilities` | Static tool / game / workflow summary |
|
|
420
|
-
| `cito://llms.txt` | Live product agent context
|
|
420
|
+
| `cito://llms.txt` | Live product agent context |
|
|
421
421
|
| `cito://openapi.json` | Live public OpenAPI for **typed clients** — not for tool generation |
|
|
422
422
|
|
|
423
423
|
---
|
|
@@ -560,9 +560,9 @@ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
|
560
560
|
|
|
561
561
|
### 0.5.6
|
|
562
562
|
|
|
563
|
-
- **Tennis `event_card` draw is a real bracket** (customer audit 2026-11-03). `draw.complete` is true only when every main round to the Final is played (it said true beside `missingRounds` F/SF/QF); every main round is listed with `status` (`completed` / `in_progress` / `not_started` / `incomplete_archive`), `expected` (byes excluded: a 56 draw's R64 holds 24), `byes`, `walkovers`, and `pending` (scheduled / live matches, never presented as results). `drawSize` is the real field (Paris 56, `
|
|
563
|
+
- **Tennis `event_card` draw is a real bracket** (customer audit 2026-11-03). `draw.complete` is true only when every main round to the Final is played (it said true beside `missingRounds` F/SF/QF); every main round is listed with `status` (`completed` / `in_progress` / `not_started` / `incomplete_archive`), `expected` (byes excluded: a 56 draw's R64 holds 24), `byes`, `walkovers`, and `pending` (scheduled / live matches, never presented as results). `drawSize` is the real field (Paris 56, `bracketSize`, `byes`); `matchesExpected` counts the main draw and is compared with `mainDrawMatchesReturned` (qualifying is reported apart). Each bout carries `winnerId` / `loserId` / `outcome` (a "6-3 6-7 RET" retirement says who won), `startTime`, `nextMatchId` (the bracket edge) and `bracketPosition`; rounds report `included` / `omitted` when `drawMaxMatches` trims them.
|
|
564
564
|
- **Tennis `head_to_head` honours `limit`**: it is sent to `/tennis/h2h`, which pages the meetings while every tally still covers all of them (`meetingsReturned` / `meetingsTotal`; the H2H `level_breakdown` now always sums to `total_matches`).
|
|
565
|
-
- Other tennis audit fixes in the same release: live stats as structured numerator / denominator / pct, `isSimulated`
|
|
565
|
+
- Other tennis audit fixes in the same release: live stats as structured numerator / denominator / pct, `isSimulated` on live rows, `setNumber` from `set_number`, and the schedule / rankings / player readers updated for the API changes of 2026-11-03.
|
|
566
566
|
|
|
567
567
|
### 0.5.5
|
|
568
568
|
|
|
@@ -597,12 +597,12 @@ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
|
597
597
|
|
|
598
598
|
### 0.4.2
|
|
599
599
|
|
|
600
|
-
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
|
|
600
|
+
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 fixed on the API; three in the MCP.
|
|
601
601
|
|
|
602
602
|
- **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.
|
|
603
603
|
- **`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.
|
|
604
|
-
- **`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.
|
|
605
|
-
- **`resolve_entity { q: "Nole" }` answered "M Canoles".** The
|
|
604
|
+
- **`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. The total is now exactly the qualifying population: 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`.
|
|
605
|
+
- **`resolve_entity { q: "Nole" }` answered "M Canoles".** The 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 search's own first row stand as the alias answer (labelled `meta.matchedBy: "top-hit"`) 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.
|
|
606
606
|
- **`/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).
|
|
607
607
|
- **`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**.
|
|
608
608
|
- **`bestOf`** now reads `best_of`, the only spelling the tennis route emits.
|
|
@@ -614,7 +614,7 @@ Tennis correctness pass, driven by the 2026-09-12 sweep (`reports/tennis-mcp-swe
|
|
|
614
614
|
- **`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`.
|
|
615
615
|
- **`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.
|
|
616
616
|
- **`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.
|
|
617
|
-
- **`tournaments level="WTA 1000"` returned ATP Masters events.** The
|
|
617
|
+
- **`tournaments level="WTA 1000"` returned ATP Masters events.** 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`.
|
|
618
618
|
- **`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`.
|
|
619
619
|
- **`tennis_schedule`** now counts the day's list/completed overlap, summarises statuses, and flags date-only `startsAt` placeholders.
|
|
620
620
|
- **`live_matches` labelled tennis rows by tournament**, not players, because the live route's `name` is the event.
|
package/dist/envelope.js
CHANGED
|
@@ -25,7 +25,7 @@ export const DEFAULT_RECOVER = {
|
|
|
25
25
|
'Verify CITO_API_KEY is set and valid',
|
|
26
26
|
'Call api_health to confirm tier and included games',
|
|
27
27
|
],
|
|
28
|
-
|
|
28
|
+
SERVICE_ERROR: [
|
|
29
29
|
'Retry this tool once',
|
|
30
30
|
'If it still fails, narrow params (limit, date range) or use a smaller tool',
|
|
31
31
|
],
|
|
@@ -64,8 +64,8 @@ export function mapHttpToCode(status, hints) {
|
|
|
64
64
|
if (status === 400 || status === 422)
|
|
65
65
|
return 'VALIDATION';
|
|
66
66
|
if (status >= 500 || status === 0)
|
|
67
|
-
return '
|
|
68
|
-
return '
|
|
67
|
+
return 'SERVICE_ERROR';
|
|
68
|
+
return 'SERVICE_ERROR';
|
|
69
69
|
}
|
|
70
70
|
export function newRequestId() {
|
|
71
71
|
return `mcp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -73,7 +73,7 @@ export function newRequestId() {
|
|
|
73
73
|
export function successEnvelope(args) {
|
|
74
74
|
const meta = {
|
|
75
75
|
game: args.game,
|
|
76
|
-
|
|
76
|
+
tool: args.source,
|
|
77
77
|
fetchedAt: args.fetchedAt ?? new Date().toISOString(),
|
|
78
78
|
};
|
|
79
79
|
if (args.requestId)
|
|
@@ -81,7 +81,7 @@ export function successEnvelope(args) {
|
|
|
81
81
|
if (args.tookMs !== undefined)
|
|
82
82
|
meta.tookMs = args.tookMs;
|
|
83
83
|
if (args.upstreamCalls !== undefined)
|
|
84
|
-
meta.
|
|
84
|
+
meta.apiCalls = args.upstreamCalls;
|
|
85
85
|
if (args.rateLimit)
|
|
86
86
|
meta.rateLimit = args.rateLimit;
|
|
87
87
|
if (args.warnings?.length)
|
|
@@ -103,10 +103,10 @@ export function successEnvelope(args) {
|
|
|
103
103
|
}
|
|
104
104
|
export function errorEnvelope(args) {
|
|
105
105
|
const retryable = args.retryable ??
|
|
106
|
-
(args.code === 'RATE_LIMIT' || args.code === '
|
|
106
|
+
(args.code === 'RATE_LIMIT' || args.code === 'SERVICE_ERROR');
|
|
107
107
|
const meta = {
|
|
108
108
|
game: args.game,
|
|
109
|
-
|
|
109
|
+
tool: args.source,
|
|
110
110
|
fetchedAt: args.fetchedAt ?? new Date().toISOString(),
|
|
111
111
|
};
|
|
112
112
|
if (args.requestId)
|
|
@@ -114,7 +114,7 @@ export function errorEnvelope(args) {
|
|
|
114
114
|
if (args.tookMs !== undefined)
|
|
115
115
|
meta.tookMs = args.tookMs;
|
|
116
116
|
if (args.upstreamCalls !== undefined)
|
|
117
|
-
meta.
|
|
117
|
+
meta.apiCalls = args.upstreamCalls;
|
|
118
118
|
if (args.rateLimit)
|
|
119
119
|
meta.rateLimit = args.rateLimit;
|
|
120
120
|
const error = {
|
|
@@ -139,7 +139,7 @@ export function errorEnvelope(args) {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
export function partialFromRejection(section, err) {
|
|
142
|
-
const code = err.code ?? '
|
|
142
|
+
const code = err.code ?? 'SERVICE_ERROR';
|
|
143
143
|
return {
|
|
144
144
|
section,
|
|
145
145
|
ok: false,
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const requestAuth = new AsyncLocalStorage();
|
|
|
21
21
|
function currentApiKey() {
|
|
22
22
|
return requestAuth.getStore()?.apiKey || ENV_API_KEY;
|
|
23
23
|
}
|
|
24
|
-
const OFFLINE_TOOLS = new Set(['list_capabilities']);
|
|
24
|
+
const OFFLINE_TOOLS = new Set(['list_capabilities', 'list_webhook_events', 'explain_webhook_setup']);
|
|
25
25
|
if (!ENV_API_KEY) {
|
|
26
26
|
console.error('[cito-mcp] CITO_API_KEY is not set. Serving tool catalog only; ' +
|
|
27
27
|
'API-backed tools will return MISSING_API_KEY until a key is provided ' +
|
|
@@ -95,7 +95,7 @@ function createCitoServer() {
|
|
|
95
95
|
{
|
|
96
96
|
uri: 'cito://openapi.json',
|
|
97
97
|
name: 'Cito OpenAPI (public fetch)',
|
|
98
|
-
description: 'Live public OpenAPI snapshot for typed clients (
|
|
98
|
+
description: 'Live public OpenAPI snapshot for typed clients (separate from the tool catalog).',
|
|
99
99
|
mimeType: 'application/json',
|
|
100
100
|
},
|
|
101
101
|
],
|
package/dist/instructions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const SERVER_INSTRUCTIONS = `# Cito MCP — agent operating manual
|
|
2
2
|
|
|
3
|
-
You are connected to Cito esports data
|
|
3
|
+
You are connected to Cito esports and sports data. Reads are safe. The only writes are the webhook tools, and they only touch this user's own webhooks and test inboxes. Prefer curated outcome tools over raw REST. Production apps must call Cito REST with the user's API key; use this MCP to design, prototype, and resolve IDs — not as a multi-tenant runtime bus.
|
|
4
4
|
|
|
5
5
|
## Hard rules
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ You are connected to Cito esports data (read-only). Prefer curated outcome tools
|
|
|
8
8
|
2. **Resolve before deep.** Name → ID/slug → summary/page → deep stats. Skip resolve only when the user already provided a Cito ID/slug.
|
|
9
9
|
3. **One screen, one composite.** Prefer a page/summary tool over stitching 4–6 thin GETs.
|
|
10
10
|
4. **Honor the envelope.** Parse ok, data, pagination, partial, error, meta.rateLimit, meta.entities. Partial section failures are not total failures — use successful sections.
|
|
11
|
-
5. **
|
|
11
|
+
5. **Reads are safe to retry** except where error.retryable is false for bad args / entitlement. **Writes** exist only for webhooks: create_webhook, update_webhook, delete_webhook, regenerate_webhook_secret, test_webhook, replay_webhook, catch_up_webhook and the inbox tools. Never call delete_webhook unless the user clearly asked to delete that webhook (it needs confirm: true). Do not retry create_webhook blindly: call list_webhooks first. Never repeat a webhook secret in chat: have the user store it.
|
|
12
12
|
|
|
13
13
|
## Game parameter
|
|
14
14
|
|
|
@@ -21,7 +21,8 @@ game enum (unless a tool documents otherwise): lol | cs2 | dota2 | ufc | cod | t
|
|
|
21
21
|
- RAF (Real American Freestyle wrestling) and BKFC (Bare Knuckle Fighting Championship) are NOT on the game enum: use the combat_* tools with league raf|bkfc (combat_events, combat_card, combat_bout, combat_athlete, combat_h2h, combat_rankings), or call_api /raf/... and /bkfc/.... RAF scores per PERIOD (wrestling points, takedowns); BKFC per ROUND (punches landed/thrown, knockdowns). Never mix either into UFC answers.
|
|
22
22
|
- MMA archives: Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction, plus /mma for a fighter's whole non-UFC career (recordByPromotion). combat_* tools with league bellator|pride|strikeforce|wec|dream|elitexc|affliction|mma, or call_api /{league}/.... Closed archives (1997-2017) with official per-ROUND stats in UFC's shape ("13 of 18", round 0 = whole fight); no live, rankings or upcoming cards (combat_card "last" = most recent card). Fighters share their UFC slug (ufcFighterSlug). Never mix these into UFC answers. Their stats predate the Free/Starter window: HISTORY_WINDOW_EXCEEDED means Pro, not a bad key.
|
|
23
23
|
- PFL (Professional Fighters League): combat_* with league pfl, or call_api /pfl/.... Every card since 2018, upcoming cards (combat_events when upcoming, combat_card "next") and live fight status (/pfl/live: bout status "live" with live.round / live.clock). Results only (winner, method, round, time): no strike or round stats exist for PFL. league mma includes PFL. Never mix into UFC answers.
|
|
24
|
-
- OKTAGON MMA: combat_* with league oktagon, or call_api /oktagon/.... Every card since Oktagon 1 (2016), upcoming cards and live fight status. Per-ROUND stats on most bouts, LANDED ONLY: significant/total strikes, head/body/leg and takedowns as plain counts (landed.*), plus submission attempts; attempts, accuracy, knockdowns and control time are null because
|
|
24
|
+
- OKTAGON MMA: combat_* with league oktagon, or call_api /oktagon/.... Every card since Oktagon 1 (2016), upcoming cards and live fight status. Per-ROUND stats on most bouts, LANDED ONLY: significant/total strikes, head/body/leg and takedowns as plain counts (landed.*), plus submission attempts; attempts, accuracy, knockdowns and control time are null because these aren't recorded for this promotion (each bout's statsCoverage says which fields exist). Fighters share their UFC slug when proven. league mma includes OKTAGON. Never mix into UFC answers.
|
|
25
|
+
- Tennis (ATP, WTA, Challenger, ITF, junior slams): career_summary and surface_breakdown count singles only (career_summary.doubles and .combined hold the other scopes). current_rank is null when a player is not on the tour's latest list, which is normal for former players: read last_rank and last_rank_date. status is active, inactive or retired (status_basis says why); is_active is true exactly when status is active. sets[].tiebreak.winner_points is the match winner's points; tiebreak_points.winner is the tiebreak winner's. Rankings and draws are singles only. /tennis/matches and /tennis/players/{id}/matches return singles by default: pass match_type=doubles for doubles or match_type=all for both. Point-by-point exists from May 2026 (WTA) and June 2026 (ATP) on tour-level matches, and from 2026 on Challenger, WTA 125 and ITF; nothing older. Tennis odds are not offered.
|
|
25
26
|
- UFC includes DWCS and Road to UFC (2022-2026, all 19 cards with official round stats; slugs like road-to-ufc-2-7, "Road to UFC 2.7").
|
|
26
27
|
|
|
27
28
|
## Preferred tool order
|
|
@@ -50,7 +51,7 @@ Lists use pagination: { limit, offset?, total?, hasMore, nextCursor, prevCursor
|
|
|
50
51
|
|
|
51
52
|
## Errors
|
|
52
53
|
|
|
53
|
-
ok:false → read error.code (VALIDATION, NOT_FOUND, UNSUPPORTED_GAME, RATE_LIMIT, UNAUTHORIZED,
|
|
54
|
+
ok:false → read error.code (VALIDATION, NOT_FOUND, UNSUPPORTED_GAME, RATE_LIMIT, UNAUTHORIZED, SERVICE_ERROR, PATH_NOT_ALLOWED, NOT_IMPLEMENTED). Follow error.recover[]. Retry only when retryable. RATE_LIMIT means cito-mcp already paced and retried: wait error.retryAfterMs before one more try, call tools one at a time, and never retry when retryable is false (monthly allowance used up: relay the upgrade link).
|
|
54
55
|
|
|
55
56
|
ok:true with partial[] → use successful sections; do not treat partial as total fail.
|
|
56
57
|
ok:true with meta.warnings[] → filters or depth degraded; do not assume ignored filters applied.
|
|
@@ -65,6 +66,11 @@ Player card: resolve_entity {type:player|fighter} → player_profile.
|
|
|
65
66
|
Fight night / event card: resolve_entity {type:event} → event_card {includeMatches:true} → match_preview for a featured bout.
|
|
66
67
|
Match preview (named sides): resolve_entity each side (optional) → match_preview {teamA, teamB}.
|
|
67
68
|
App scaffold: list_capabilities ∥ api_health ∥ live_matches, then one composite per screen.
|
|
69
|
+
Odds: match_details sections:["odds"] (UFC, CS2) or call_api /lol/matches/{id}/odds. CS2, LoL and UFC prediction-market odds update the moment the price changes (in play and between rounds/games); for an app that must react to every change, point the user at the sport's live WebSocket (wss://api.citoapi.com/api/v1/{cs2|lol|ufc}/live/ws, messages cs2.odds.updated / lol.odds.updated / ufc.odds.updated; odds plan + Live WebSockets), not MCP polling. Per-round / per-game prices: /cs2/matches/{id}/odds/rounds, /lol/matches/{id}/odds/games, /ufc/bouts/{id}/odds/rounds.
|
|
70
|
+
|
|
71
|
+
## Webhooks
|
|
72
|
+
|
|
73
|
+
When a user wants to react to changes, or describes polling, recommend webhooks: explain_webhook_setup shows their plan, existing webhooks and next steps. Flow: list_webhook_events → create_webhook → store the secret → test_webhook with event → webhook_deliveries. Only recommend events whose status is "active". Webhooks need Pro or Scale; on a plan error relay error.details.upgradeUrl. Any plan can use create_webhook_inbox and send_inbox_sample to see real payloads with no server. Outage recovery: events during a pause are not queued (status skipped_backoff); catch_up_webhook re-sends the match and result events.
|
|
68
74
|
|
|
69
75
|
## Resources
|
|
70
76
|
|
package/dist/scrub.js
CHANGED
|
@@ -59,17 +59,53 @@ const INTERNAL_KEYS = new Set([
|
|
|
59
59
|
'imageCandidates',
|
|
60
60
|
'image_candidates',
|
|
61
61
|
'source_url',
|
|
62
|
+
'source',
|
|
63
|
+
'sources',
|
|
64
|
+
'_liveSource',
|
|
65
|
+
'liveSource',
|
|
66
|
+
'live_feed',
|
|
67
|
+
'liveFeed',
|
|
68
|
+
'gameStateSource',
|
|
69
|
+
'bridge_source',
|
|
70
|
+
'rank_source',
|
|
71
|
+
'rankSource',
|
|
72
|
+
'dataSource',
|
|
73
|
+
'data_source',
|
|
74
|
+
'date_source',
|
|
75
|
+
'meta_source',
|
|
76
|
+
'esportsMatchId',
|
|
77
|
+
'esportsGameId',
|
|
78
|
+
'esports_match_id',
|
|
79
|
+
'riotId',
|
|
80
|
+
'riot_id',
|
|
81
|
+
'steamId',
|
|
82
|
+
'steam_id',
|
|
83
|
+
'oddsKey',
|
|
84
|
+
'odds_key',
|
|
85
|
+
'provider',
|
|
86
|
+
'providers',
|
|
62
87
|
]);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
88
|
+
const ORIGIN_WORD_KEY = /(^|_)sources?($|_)|[a-z]Sources?($|[A-Z])|^_?sources?[A-Z]/;
|
|
89
|
+
const PROVIDER_PREFIX_KEY = /^_?(hltv|open_?dota|polymarket|fight_?odds|ufc_?stats)/i;
|
|
90
|
+
const PORTRAIT_LICENCE_KEY = /^(portrait|image|photo|picture)_?source_?url$/i;
|
|
91
|
+
const STATUS_PARENTS = new Set(['dataAvailability', 'health']);
|
|
92
|
+
const STATUS_KEYS = new Set(['strategy', 'samples']);
|
|
65
93
|
const MEDIA_PARENT = /image|photo|portrait|picture/i;
|
|
66
94
|
const ERROR_KEYS = new Set(['errors', 'error', 'lastError', 'bodyPreview']);
|
|
67
95
|
const INTERNAL_ERROR_TEXT = /prisma|raw query failed|invocation|ECONNREFUSED|\bat \S+ \(/i;
|
|
68
96
|
export const INTERNAL_ERROR_PLACEHOLDER = 'internal_error';
|
|
97
|
+
const NO_RESULT_REASON_KEY = /^result_?unavailable_?reason$/i;
|
|
98
|
+
const RAW_NO_RESULT_REASON = 'source_published_no_result';
|
|
99
|
+
const NO_RESULT_REASON = 'published_no_result';
|
|
69
100
|
function isInternal(key, parentKey) {
|
|
70
101
|
if (key === 'sourceUrl' && MEDIA_PARENT.test(parentKey))
|
|
71
102
|
return false;
|
|
72
|
-
|
|
103
|
+
if (PORTRAIT_LICENCE_KEY.test(key))
|
|
104
|
+
return false;
|
|
105
|
+
return (INTERNAL_KEYS.has(key) ||
|
|
106
|
+
ORIGIN_WORD_KEY.test(key) ||
|
|
107
|
+
PROVIDER_PREFIX_KEY.test(key) ||
|
|
108
|
+
(STATUS_KEYS.has(key) && STATUS_PARENTS.has(parentKey)));
|
|
73
109
|
}
|
|
74
110
|
function scrub(node, parentKey, inError) {
|
|
75
111
|
if (typeof node === 'string') {
|
|
@@ -98,7 +134,9 @@ function scrub(node, parentKey, inError) {
|
|
|
98
134
|
delete out[key];
|
|
99
135
|
continue;
|
|
100
136
|
}
|
|
101
|
-
const next =
|
|
137
|
+
const next = value === RAW_NO_RESULT_REASON && NO_RESULT_REASON_KEY.test(key)
|
|
138
|
+
? NO_RESULT_REASON
|
|
139
|
+
: scrub(value, key, inError || ERROR_KEYS.has(key));
|
|
102
140
|
if (next !== value) {
|
|
103
141
|
out ??= { ...src };
|
|
104
142
|
out[key] = next;
|
package/dist/tools/combat.js
CHANGED
|
@@ -192,7 +192,7 @@ returns an empty list with data.note instead of failing.
|
|
|
192
192
|
Prefer over: call_api /{league}/events.
|
|
193
193
|
Do not use when: you already have the event slug and want its bouts (combat_card).
|
|
194
194
|
|
|
195
|
-
Parallel-safe: yes.
|
|
195
|
+
Parallel-safe: yes. API calls: 1.
|
|
196
196
|
Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
197
197
|
inputSchema: {
|
|
198
198
|
type: 'object',
|
|
@@ -264,7 +264,7 @@ MMA archives are closed: event defaults to "last" (the most recent card; for mma
|
|
|
264
264
|
Prefer over: combat_events + call_api per bout.
|
|
265
265
|
Do not use when: you need one bout's period/round breakdown (combat_bout).
|
|
266
266
|
|
|
267
|
-
Parallel-safe: yes.
|
|
267
|
+
Parallel-safe: yes. API calls: 2–3.
|
|
268
268
|
Example: { "league": "raf", "event": "last" }`,
|
|
269
269
|
inputSchema: {
|
|
270
270
|
type: 'object',
|
|
@@ -364,7 +364,7 @@ note while the result still returns.
|
|
|
364
364
|
Get boutId from combat_card or combat_athlete (e.g. "raf13--belal-muhammad--colby-covington", "bellator-5600").
|
|
365
365
|
MMA archive bout ids are "<promotion>-<fightId>"; a bare fight id resolves in any MMA archive league.
|
|
366
366
|
|
|
367
|
-
Parallel-safe: yes.
|
|
367
|
+
Parallel-safe: yes. API calls: 1–2.
|
|
368
368
|
Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cisneros--ryan-petersen" }`,
|
|
369
369
|
inputSchema: {
|
|
370
370
|
type: 'object',
|
|
@@ -439,7 +439,7 @@ Keep the records apart: archive fights never belong in a UFC answer.
|
|
|
439
439
|
|
|
440
440
|
Prefer over: call_api profile + history + stats separately.
|
|
441
441
|
|
|
442
|
-
Parallel-safe: yes.
|
|
442
|
+
Parallel-safe: yes. API calls: 2–4.
|
|
443
443
|
Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
|
|
444
444
|
inputSchema: {
|
|
445
445
|
type: 'object',
|
|
@@ -516,7 +516,7 @@ Accepts slugs or names for both sides (names resolve through the league's search
|
|
|
516
516
|
the ones on that promotion's cards (league "mma": every archived promotion); UFC meetings are a UFC question
|
|
517
517
|
(head_to_head).
|
|
518
518
|
|
|
519
|
-
Parallel-safe: yes.
|
|
519
|
+
Parallel-safe: yes. API calls: 1–3.
|
|
520
520
|
Example: { "league": "bkfc", "athleteA": "ryan-petersen", "athleteB": "gregoris-cisneros" }`,
|
|
521
521
|
inputSchema: {
|
|
522
522
|
type: 'object',
|
|
@@ -573,7 +573,7 @@ The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction,
|
|
|
573
573
|
non-retryable NOT_IMPLEMENTED.
|
|
574
574
|
Never answer with UFC rankings instead.
|
|
575
575
|
|
|
576
|
-
Parallel-safe: yes.
|
|
576
|
+
Parallel-safe: yes. API calls: 1.
|
|
577
577
|
Example: { "league": "bkfc", "division": "heavyweight" }`,
|
|
578
578
|
inputSchema: {
|
|
579
579
|
type: 'object',
|
package/dist/tools/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { cs2Tools } from './cs2.js';
|
|
|
12
12
|
import { tournamentTools } from './tournaments.js';
|
|
13
13
|
import { scheduleTools } from './schedule.js';
|
|
14
14
|
import { combatTools } from './combat.js';
|
|
15
|
+
import { webhookTools } from './webhooks.js';
|
|
15
16
|
export const allTools = [
|
|
16
17
|
...metaTools.filter((t) => t.name === 'list_capabilities' || t.name === 'api_health'),
|
|
17
18
|
...resolveTools,
|
|
@@ -27,6 +28,7 @@ export const allTools = [
|
|
|
27
28
|
...insightTools,
|
|
28
29
|
...cs2Tools,
|
|
29
30
|
...combatTools,
|
|
31
|
+
...webhookTools,
|
|
30
32
|
...metaTools.filter((t) => t.name === 'list_routes' || t.name === 'call_api'),
|
|
31
33
|
];
|
|
32
34
|
export function getTool(name) {
|
package/dist/tools/insight.js
CHANGED
|
@@ -26,6 +26,9 @@ async function loadSide(ctx, game, side, recentLimit, includeRosters) {
|
|
|
26
26
|
hand: pickString(data.hand) ?? null,
|
|
27
27
|
age: typeof data.age === 'number' ? data.age : null,
|
|
28
28
|
currentRank: typeof data.current_rank === 'number' ? data.current_rank : null,
|
|
29
|
+
lastRank: typeof data.last_rank === 'number' ? data.last_rank : null,
|
|
30
|
+
lastRankDate: pickString(data.last_rank_date) ?? null,
|
|
31
|
+
status: pickString(data.status) ?? null,
|
|
29
32
|
careerHighRank: typeof data.career_high_rank === 'number' ? data.career_high_rank : null,
|
|
30
33
|
careerRecord: career.matches_won != null && career.matches_lost != null
|
|
31
34
|
? `${career.matches_won}-${career.matches_lost}`
|
|
@@ -272,7 +275,7 @@ Prefer match_summary when match is completed; match_details for live in-game.
|
|
|
272
275
|
|
|
273
276
|
Do not use when: user wants final score/recap of a finished match.
|
|
274
277
|
|
|
275
|
-
Parallel-safe: yes.
|
|
278
|
+
Parallel-safe: yes. API calls: 4–8.
|
|
276
279
|
Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
|
|
277
280
|
Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "recentLimit": 5 }`,
|
|
278
281
|
inputSchema: {
|
|
@@ -520,7 +523,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
520
523
|
}
|
|
521
524
|
catch (e) {
|
|
522
525
|
partial.push(partialFromRejection('h2h', {
|
|
523
|
-
code: '
|
|
526
|
+
code: 'SERVICE_ERROR',
|
|
524
527
|
message: e.message,
|
|
525
528
|
}));
|
|
526
529
|
}
|
|
@@ -718,7 +721,7 @@ Bouts come back in card order — main event first, then prelims, then early pre
|
|
|
718
721
|
Each bout carries weightClass, titleBout, card placement, and (once fought) result
|
|
719
722
|
{ method, round, time, referee, winnerSlug }. Each corner carries images
|
|
720
723
|
{ headshotUrl, bodyImageUrl, imageUrl, proxiedImageUrl }, record, nickname, rank,
|
|
721
|
-
championStatus, country and flag when
|
|
724
|
+
championStatus, country and flag when available. Use proxiedImageUrl in
|
|
722
725
|
browsers — the image host sends no CORS header. You do not need call_api per fighter for faces.
|
|
723
726
|
|
|
724
727
|
Prefer over: agent-side resolve + call_api /ufc/events + bout expansion; N+1 match_summary for the card list only; per-fighter call_api just to fetch headshots.
|
|
@@ -730,14 +733,14 @@ Do not use when: you only need live scores (live_matches); single finished match
|
|
|
730
733
|
Tennis: pass a tournament id/slug to get the draw bracket — every main round from
|
|
731
734
|
the first to the Final (status completed / in_progress / not_started), then
|
|
732
735
|
qualifying (Q1..Q3). Each result carries winnerId/loserId/outcome (retirements
|
|
733
|
-
included), startTime (null with startTimeUnavailableReason when
|
|
736
|
+
included), startTime (null with startTimeUnavailableReason when no start time is
|
|
734
737
|
clock), nextMatchId (the bracket edge) and bracketPosition; scheduled and live
|
|
735
738
|
matches sit in rounds[].pending. draw carries drawSize (real field size, byes
|
|
736
739
|
excluded, e.g. 56), byes, matchesExpected (main draw) and complete (true only
|
|
737
740
|
when every main round to the Final is played). This is the primary tennis path
|
|
738
741
|
here; there is no separate draw tool.
|
|
739
742
|
|
|
740
|
-
Parallel-safe: yes.
|
|
743
|
+
Parallel-safe: yes. API calls: 1–4.
|
|
741
744
|
Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "includeStandings": false }`,
|
|
742
745
|
inputSchema: {
|
|
743
746
|
type: 'object',
|
|
@@ -751,7 +754,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
751
754
|
includeMatches: boolSchema('Include the bout/match list, or the tennis draw bracket (default true).', true),
|
|
752
755
|
includeStandings: boolSchema('Include standings/rankings snippet when API supports event/tournament/division scope (default false). ' +
|
|
753
756
|
'For UFC this also joins divisional rank and movement onto each bout corner as team.rank / team.rankMovement — ' +
|
|
754
|
-
'bout rows themselves carry no rank. Costs one extra
|
|
757
|
+
'bout rows themselves carry no rank. Costs one extra API call.', false),
|
|
755
758
|
limit: limitSchema({
|
|
756
759
|
default: 20,
|
|
757
760
|
max: 50,
|
|
@@ -1290,7 +1293,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1290
1293
|
? { startTimeUnavailableReason: pickString(mr.start_time_unavailable_reason) }
|
|
1291
1294
|
: {}),
|
|
1292
1295
|
nextMatchId: pickString(mr.next_match_id) ?? null,
|
|
1293
|
-
...(pickString(mr.next_match_source) ? {
|
|
1296
|
+
...(pickString(mr.next_match_source) ? { nextMatchBasis: pickString(mr.next_match_source)?.replace(/^source_/, '') } : {}),
|
|
1294
1297
|
bracketPosition: num(mr.bracket_position),
|
|
1295
1298
|
nextSlot: asRecord(mr.next_slot)
|
|
1296
1299
|
? {
|
|
@@ -1298,7 +1301,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1298
1301
|
position: num(asRecord(mr.next_slot)?.position),
|
|
1299
1302
|
}
|
|
1300
1303
|
: null,
|
|
1301
|
-
...(pickString(mr.source_round) ? {
|
|
1304
|
+
...(pickString(mr.source_round) ? { rawRound: pickString(mr.source_round) } : {}),
|
|
1302
1305
|
}, 'tennis');
|
|
1303
1306
|
};
|
|
1304
1307
|
const pendingMatch = (pr) => {
|
|
@@ -1315,7 +1318,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1315
1318
|
startTimeKnown: pr.start_time_known === true,
|
|
1316
1319
|
liveScore: pickString(pr.live_score) ?? null,
|
|
1317
1320
|
winnerId: null,
|
|
1318
|
-
...(pickString(pr.source_winner_id) ? {
|
|
1321
|
+
...(pickString(pr.source_winner_id) ? { reportedWinnerId: pickString(pr.source_winner_id) } : {}),
|
|
1319
1322
|
nextMatchId: pickString(pr.next_match_id) ?? null,
|
|
1320
1323
|
bracketPosition: num(pr.bracket_position),
|
|
1321
1324
|
};
|
|
@@ -1397,7 +1400,6 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1397
1400
|
}
|
|
1398
1401
|
drawSummary = {
|
|
1399
1402
|
drawSize,
|
|
1400
|
-
drawSizeSource: pickString(dd.draw_size_source) ?? null,
|
|
1401
1403
|
drawSizeVerified: typeof dd.draw_size_verified === 'boolean' ? dd.draw_size_verified : null,
|
|
1402
1404
|
drawSizeDerived: num(dd.draw_size_derived),
|
|
1403
1405
|
bracketSize: num(dd.bracket_size),
|
|
@@ -53,7 +53,7 @@ Do not use when: ranking position → standings with game tennis; week-over-week
|
|
|
53
53
|
|
|
54
54
|
Tennis-only. Season defaults to 2026; tour (ATP|WTA) is optional — omit it for the combined board. Rows carry aces + matches played, ordered most aces first.
|
|
55
55
|
|
|
56
|
-
Parallel-safe: yes.
|
|
56
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
57
57
|
inputSchema: {
|
|
58
58
|
type: 'object',
|
|
59
59
|
additionalProperties: false,
|
|
@@ -205,7 +205,7 @@ Do not use when: ranking position → standings with game tennis; week-over-week
|
|
|
205
205
|
|
|
206
206
|
Tennis-only. Season defaults to 2026; tour (ATP|WTA) is optional — omit it for the combined board. ${spec.minAttemptsNote}
|
|
207
207
|
|
|
208
|
-
Parallel-safe: yes.
|
|
208
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
209
209
|
inputSchema: {
|
|
210
210
|
type: 'object',
|
|
211
211
|
additionalProperties: false,
|
package/dist/tools/live.js
CHANGED
|
@@ -110,9 +110,9 @@ export function humanUfcEmptyNote(emptyReason, health) {
|
|
|
110
110
|
supervisor_warming: 'Card open / pre-gate; no bout live yet',
|
|
111
111
|
between_bouts: 'Between bouts; next may be armed',
|
|
112
112
|
armed_only: 'Next bout armed; not started',
|
|
113
|
-
worker_stale: 'Live
|
|
114
|
-
worker_down_inferred: 'Live
|
|
115
|
-
event_degraded: '
|
|
113
|
+
worker_stale: 'Live data is behind; scores may lag',
|
|
114
|
+
worker_down_inferred: 'Live data unavailable right now',
|
|
115
|
+
event_degraded: 'Live clock unavailable; not inventing stats',
|
|
116
116
|
};
|
|
117
117
|
const msg = human[reason] || `No live|watching bouts (${reason})`;
|
|
118
118
|
return `UFC: ${msg} (${workerPart}${lagPart}; emptyReason=${reason})`;
|
|
@@ -165,9 +165,10 @@ Prefer over: sequential per-game call_api live probes.
|
|
|
165
165
|
Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
|
|
166
166
|
|
|
167
167
|
CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
|
|
168
|
+
Tennis: ITF and junior rows have no serve statistics (stats_unavailable_code), so a missing stat there is expected. For push updates point the user at wss://api.citoapi.com/api/v1/tennis/live/ws.
|
|
168
169
|
UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
|
|
169
170
|
|
|
170
|
-
Parallel-safe: yes.
|
|
171
|
+
Parallel-safe: yes. API calls: 1–5 (allSettled).
|
|
171
172
|
Example: { "game": "all", "limitPerGame": 10 }`,
|
|
172
173
|
inputSchema: {
|
|
173
174
|
type: 'object',
|
|
@@ -359,12 +360,12 @@ Do not use when: only in-progress matches needed → live_matches.
|
|
|
359
360
|
|
|
360
361
|
Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
|
|
361
362
|
- lol: hours, team (slug), league (slug)
|
|
362
|
-
- cs2: team, from, to (ISO); hours not applied
|
|
363
|
+
- cs2: team, from, to (ISO); hours is not applied
|
|
363
364
|
- cod: team, tournamentId
|
|
364
365
|
- dota2: limit/cursor primarily; team may be client-filtered where data allows
|
|
365
366
|
- ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
|
|
366
367
|
|
|
367
|
-
Parallel-safe: yes.
|
|
368
|
+
Parallel-safe: yes. API calls: 1–2.
|
|
368
369
|
Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
369
370
|
inputSchema: {
|
|
370
371
|
type: 'object',
|
|
@@ -465,7 +466,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
465
466
|
...(to ? { to } : {}),
|
|
466
467
|
};
|
|
467
468
|
if (args.hours !== undefined)
|
|
468
|
-
noteIgnored('hours', 'CS2 uses from/to or unfiltered upcoming; hours is not applied
|
|
469
|
+
noteIgnored('hours', 'CS2 uses from/to or unfiltered upcoming; hours is not applied');
|
|
469
470
|
if (league)
|
|
470
471
|
noteIgnored('league', 'CS2 schedule does not accept league here — filter client-side or use event tools');
|
|
471
472
|
if (tournamentId)
|