cito-mcp 0.4.8 → 0.5.1
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 +23 -4
- package/dist/envelope.js +1 -1
- package/dist/instructions.js +4 -0
- package/dist/tools/combat.js +173 -46
- package/dist/tools/meta.js +37 -19
- package/package.json +52 -51
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
**Version:** `0.4.
|
|
5
|
+
**Version:** `0.4.9` · **Node:** `>=20` · **Install:** `npx cito-mcp`
|
|
6
6
|
|
|
7
|
-
Primary games & sports: **lol · cs2 · dota2 · cod · ufc · tennis**, plus **RAF wrestling
|
|
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
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -240,7 +240,17 @@ upstream, so a non-null `headshotUrl` always has a `license` beside it.
|
|
|
240
240
|
| `cs2` | deep | Prefixed ids; live path **`GET /cs2/live`**; trends / rankings |
|
|
241
241
|
| `dota2` | thin | Numeric ids; radar; weak/no standings & roster endpoints |
|
|
242
242
|
| `cod` | medium | Org slugs; CDL standings; UUID match ids |
|
|
243
|
-
| `ufc` | medium | Fighter slugs; `boutId`; live + rankings |
|
|
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. |
|
|
244
254
|
|
|
245
255
|
### UFC projection notes (0.2.4)
|
|
246
256
|
|
|
@@ -259,7 +269,7 @@ Fortnite: **`call_api` only** until promoted into the primary enum.
|
|
|
259
269
|
|
|
260
270
|
Path must start with `/` and match one of:
|
|
261
271
|
|
|
262
|
-
`/health` · `/lol` · `/cs2` · `/dota2` · `/cod` · `/ufc` · `/fortnite`
|
|
272
|
+
`/health` · `/lol` · `/cs2` · `/dota2` · `/cod` · `/ufc` · `/fortnite` · `/tennis` · `/raf` · `/bkfc` · `/bellator` · `/pride` · `/strikeforce` · `/wec` · `/dream` · `/elitexc` · `/affliction` · `/mma` · `/openapi.json`
|
|
263
273
|
|
|
264
274
|
Absolute URLs and `..` traversal → `PATH_NOT_ALLOWED`. Prefer curated tools for all standard jobs.
|
|
265
275
|
|
|
@@ -540,6 +550,15 @@ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
|
540
550
|
|
|
541
551
|
## Changelog (summary)
|
|
542
552
|
|
|
553
|
+
### 0.4.9
|
|
554
|
+
|
|
555
|
+
- **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.
|
|
556
|
+
- 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`).
|
|
557
|
+
|
|
558
|
+
### 0.4.8
|
|
559
|
+
|
|
560
|
+
- RAF wrestling and BKFC bare knuckle: six league-parameterised `combat_*` tools, and `/raf` + `/bkfc` on the `call_api` allowlist.
|
|
561
|
+
|
|
543
562
|
### 0.4.7
|
|
544
563
|
|
|
545
564
|
- Removed the unregistered tennis odds module and its README entries; `match_details` `sections:["odds"]` stays UFC-only and answers `NOT_IMPLEMENTED` for other games.
|
package/dist/envelope.js
CHANGED
|
@@ -38,7 +38,7 @@ export const DEFAULT_RECOVER = {
|
|
|
38
38
|
'Call api_health to see which games your plan includes',
|
|
39
39
|
],
|
|
40
40
|
PATH_NOT_ALLOWED: [
|
|
41
|
-
'Use only allowlisted path prefixes: /health /lol /cs2 /dota2 /cod /ufc /fortnite',
|
|
41
|
+
'Use only allowlisted path prefixes: /health /lol /cs2 /dota2 /cod /ufc /fortnite /tennis /raf /bkfc, MMA archives /bellator /pride /strikeforce /wec /dream /elitexc /affliction /pfl /oktagon /mma',
|
|
42
42
|
'Prefer a curated tool from list_capabilities when one covers the outcome',
|
|
43
43
|
],
|
|
44
44
|
NOT_IMPLEMENTED: [
|
package/dist/instructions.js
CHANGED
|
@@ -18,6 +18,10 @@ game enum (unless a tool documents otherwise): lol | cs2 | dota2 | ufc | cod | t
|
|
|
18
18
|
- On UNSUPPORTED_GAME / 403 for a title: stop retrying that game; call api_health; report plan gaps.
|
|
19
19
|
- Fortnite and other titles may appear via call_api/resources; do not assume they are in the curated enum until list_capabilities says so.
|
|
20
20
|
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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 the source does not log them (each bout's statsCoverage says which fields exist). Fighters share their UFC slug when proven. league mma includes OKTAGON. Never mix into UFC answers.
|
|
24
|
+
- 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").
|
|
21
25
|
|
|
22
26
|
## Preferred tool order
|
|
23
27
|
|
package/dist/tools/combat.js
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
1
|
import { asRecord, clampInt, extractRows, fetchJson, gameNotIncludedHint } from '../client.js';
|
|
2
2
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
3
3
|
import { boolSchema, limitSchema, stringSchema } from './types.js';
|
|
4
|
-
export const
|
|
4
|
+
export const MMA_ARCHIVE_LEAGUES = ['bellator', 'pride', 'strikeforce', 'wec', 'dream', 'elitexc', 'affliction'];
|
|
5
|
+
export const MMA_LEAGUES = [...MMA_ARCHIVE_LEAGUES, 'pfl', 'oktagon', 'mma'];
|
|
6
|
+
export const COMBAT_LEAGUES = ['raf', 'bkfc', ...MMA_LEAGUES];
|
|
7
|
+
const isMma = (league) => MMA_LEAGUES.includes(league);
|
|
5
8
|
const LEAGUE_NAMES = {
|
|
6
9
|
raf: 'Real American Freestyle (pro freestyle wrestling)',
|
|
7
10
|
bkfc: 'Bare Knuckle Fighting Championship',
|
|
11
|
+
bellator: 'Bellator MMA',
|
|
12
|
+
pride: 'PRIDE Fighting Championships',
|
|
13
|
+
strikeforce: 'Strikeforce',
|
|
14
|
+
wec: 'World Extreme Cagefighting',
|
|
15
|
+
dream: 'DREAM',
|
|
16
|
+
elitexc: 'EliteXC',
|
|
17
|
+
affliction: 'Affliction Entertainment',
|
|
18
|
+
pfl: 'Professional Fighters League',
|
|
19
|
+
oktagon: 'OKTAGON MMA',
|
|
20
|
+
mma: 'MMA beyond the UFC (every non-UFC promotion we hold, PFL and OKTAGON included)',
|
|
8
21
|
};
|
|
9
|
-
const
|
|
10
|
-
|
|
22
|
+
const ARCHIVES = {
|
|
23
|
+
bellator: { short: 'Bellator', note: 'Bellator is a closed archive (2014-2024; official round stats for Bellator 131-172, results only after that)' },
|
|
24
|
+
pride: { short: 'PRIDE', note: 'PRIDE is a closed archive (1997-2007; PRIDE rules: a 10-minute first round, then 5-minute rounds)' },
|
|
25
|
+
strikeforce: { short: 'Strikeforce', note: 'Strikeforce is a closed archive (2006-2013)' },
|
|
26
|
+
wec: { short: 'WEC', note: 'WEC is a closed archive (2001-2010, until it merged into the UFC)' },
|
|
27
|
+
dream: { short: 'DREAM', note: 'DREAM is a closed archive (2008-2012)' },
|
|
28
|
+
elitexc: { short: 'EliteXC', note: 'EliteXC is a closed archive (2007-2008)' },
|
|
29
|
+
affliction: { short: 'Affliction', note: 'Affliction is a closed archive (2008-2009)' },
|
|
30
|
+
};
|
|
31
|
+
function archiveOf(league) {
|
|
32
|
+
return ARCHIVES[league];
|
|
33
|
+
}
|
|
34
|
+
const PEOPLE = (league) => (league === 'raf' ? 'athletes' : 'fighters');
|
|
35
|
+
const HISTORY = (league) => (league === 'raf' ? 'bouts' : 'fights');
|
|
11
36
|
const leagueSchema = {
|
|
12
37
|
type: 'string',
|
|
13
38
|
enum: [...COMBAT_LEAGUES],
|
|
14
|
-
description: 'League: "raf" (Real American Freestyle wrestling)
|
|
39
|
+
description: 'League: "raf" (Real American Freestyle wrestling), "bkfc" (Bare Knuckle Fighting Championship), or an MMA archive: ' +
|
|
40
|
+
'"bellator" (2014-2017), "pride" (1997-2007), "strikeforce" (2006-2013), "wec" (2001-2010), "dream" (2008-2012), ' +
|
|
41
|
+
'"elitexc" (2007-2008), "affliction" (2008-2009), "pfl" (PFL, 2018 to now: upcoming cards, live fight status, results only), ' +
|
|
42
|
+
'"oktagon" (OKTAGON MMA, 2016 to now: upcoming cards, live fight status, per-round LANDED strikes by target and takedowns), ' +
|
|
43
|
+
'or "mma" (all of them at once). Example: "bkfc".',
|
|
15
44
|
};
|
|
16
45
|
function parseLeague(value) {
|
|
17
46
|
const v = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
@@ -20,17 +49,25 @@ function parseLeague(value) {
|
|
|
20
49
|
function leagueError(d, value) {
|
|
21
50
|
return errorEnvelope({
|
|
22
51
|
code: 'VALIDATION',
|
|
23
|
-
message: `league must be
|
|
52
|
+
message: `league must be one of ${COMBAT_LEAGUES.join(', ')} (got ${JSON.stringify(value ?? null)})`,
|
|
24
53
|
game: null,
|
|
25
54
|
source: d.source,
|
|
26
55
|
requestId: d.requestId,
|
|
27
56
|
tookMs: Date.now() - d.started,
|
|
28
57
|
});
|
|
29
58
|
}
|
|
59
|
+
function isHistoryWindow(res) {
|
|
60
|
+
return res.status === 403 && asRecord(asRecord(res.data)?.error)?.code === 'HISTORY_WINDOW_EXCEEDED';
|
|
61
|
+
}
|
|
62
|
+
const HISTORY_WINDOW_RECOVER = [
|
|
63
|
+
'Free reads 30 days and Starter 90 days of in-depth stats; Pro reads the full archive',
|
|
64
|
+
'Summary sections (combat_card bouts, combat_athlete history) still return the records without stat blocks',
|
|
65
|
+
];
|
|
30
66
|
function httpError(d, league, res, what) {
|
|
31
67
|
const body = asRecord(res.data);
|
|
32
68
|
const err = asRecord(body?.error);
|
|
33
69
|
return errorEnvelope({
|
|
70
|
+
...(isHistoryWindow(res) ? { recover: HISTORY_WINDOW_RECOVER } : {}),
|
|
34
71
|
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
35
72
|
message: (typeof err?.message === 'string' && err.message) || `${what} failed (HTTP ${res.status})`,
|
|
36
73
|
game: league,
|
|
@@ -45,22 +82,47 @@ function payload(res) {
|
|
|
45
82
|
const body = asRecord(res.data);
|
|
46
83
|
return body && 'data' in body ? body.data : res.data;
|
|
47
84
|
}
|
|
48
|
-
function
|
|
85
|
+
function upstreamNotes(res) {
|
|
86
|
+
const meta = asRecord(asRecord(res.data)?.meta);
|
|
87
|
+
const out = {};
|
|
88
|
+
if (typeof meta?.note === 'string')
|
|
89
|
+
out.note = meta.note;
|
|
90
|
+
if (meta?.historyWindow)
|
|
91
|
+
out.historyWindow = meta.historyWindow;
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
function sectionFailure(section, res, league) {
|
|
49
95
|
const body = asRecord(res.data);
|
|
50
96
|
const err = asRecord(body?.error);
|
|
51
97
|
const code = mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) });
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
let message = err?.message || `${section} unavailable (HTTP ${res.status})`;
|
|
99
|
+
if (isHistoryWindow(res)) {
|
|
100
|
+
message =
|
|
101
|
+
`${section} is older than this key's history window (HISTORY_WINDOW_EXCEEDED: Free 30 days, Starter 90 days; ` +
|
|
102
|
+
`Pro reads the full archive)` + (league && archiveOf(league) ? `. ${archiveOf(league).note}, so all of its stats need Pro` : '');
|
|
103
|
+
}
|
|
104
|
+
else if (res.status === 403) {
|
|
105
|
+
message = `${section} needs the Starter plan or above for this league (${err?.message || 'plan restriction'})`;
|
|
106
|
+
}
|
|
107
|
+
return partialFromRejection(section, { code, httpStatus: res.status, message });
|
|
108
|
+
}
|
|
109
|
+
function archiveGap(d, league, message, recover, calls = 0) {
|
|
110
|
+
return errorEnvelope({
|
|
111
|
+
code: 'NOT_IMPLEMENTED',
|
|
112
|
+
message,
|
|
113
|
+
game: league,
|
|
114
|
+
source: d.source,
|
|
115
|
+
requestId: d.requestId,
|
|
116
|
+
tookMs: Date.now() - d.started,
|
|
117
|
+
upstreamCalls: calls,
|
|
118
|
+
retryable: false,
|
|
119
|
+
recover,
|
|
58
120
|
});
|
|
59
121
|
}
|
|
60
122
|
async function resolvePerson(ctx, league, input) {
|
|
61
123
|
const raw = input.trim();
|
|
62
124
|
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(raw)) {
|
|
63
|
-
const probe = await fetchJson(ctx, `/${league}/${PEOPLE
|
|
125
|
+
const probe = await fetchJson(ctx, `/${league}/${PEOPLE(league)}/${encodeURIComponent(raw)}`);
|
|
64
126
|
if (probe.ok)
|
|
65
127
|
return { slug: raw };
|
|
66
128
|
if (probe.status !== 404)
|
|
@@ -91,7 +153,7 @@ async function resolveOrFail(d, ctx, league, input, argName) {
|
|
|
91
153
|
return {
|
|
92
154
|
fail: errorEnvelope({
|
|
93
155
|
code: 'AMBIGUOUS_ENTITY',
|
|
94
|
-
message: `"${input}" matches several ${PEOPLE
|
|
156
|
+
message: `"${input}" matches several ${PEOPLE(league)}; pass one slug from details.candidates`,
|
|
95
157
|
game: league,
|
|
96
158
|
source: d.source,
|
|
97
159
|
requestId: d.requestId,
|
|
@@ -103,7 +165,7 @@ async function resolveOrFail(d, ctx, league, input, argName) {
|
|
|
103
165
|
return {
|
|
104
166
|
fail: errorEnvelope({
|
|
105
167
|
code: 'NOT_FOUND',
|
|
106
|
-
message: `No ${league.toUpperCase()} ${PEOPLE
|
|
168
|
+
message: `No ${league.toUpperCase()} ${PEOPLE(league).slice(0, -1)} matches "${input}"`,
|
|
107
169
|
game: league,
|
|
108
170
|
source: d.source,
|
|
109
171
|
requestId: d.requestId,
|
|
@@ -116,15 +178,18 @@ function begin(source) {
|
|
|
116
178
|
}
|
|
117
179
|
export const combatEvents = {
|
|
118
180
|
name: 'combat_events',
|
|
119
|
-
description: `RAF wrestling
|
|
181
|
+
description: `RAF wrestling, BKFC bare-knuckle or MMA archive (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction) events.
|
|
120
182
|
|
|
121
183
|
When to use:
|
|
122
|
-
- "When is the next RAF / BKFC card?", "recent BKFC results", schedule pages
|
|
184
|
+
- "When is the next RAF / BKFC card?", "recent BKFC results", "PRIDE results", "Bellator cards from 2015", schedule pages
|
|
123
185
|
- Getting an event slug for combat_card
|
|
124
186
|
|
|
125
187
|
Each event carries slug, name, date, local start time + timezone, venue/location, status and bout counts.
|
|
188
|
+
The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction, and mma = all of them, each
|
|
189
|
+
event tagged with its real promotion) are closed: "when" defaults to "recent" (newest card first), and "upcoming"
|
|
190
|
+
returns an empty list with data.note instead of failing.
|
|
126
191
|
|
|
127
|
-
Prefer over: call_api /
|
|
192
|
+
Prefer over: call_api /{league}/events.
|
|
128
193
|
Do not use when: you already have the event slug and want its bouts (combat_card).
|
|
129
194
|
|
|
130
195
|
Parallel-safe: yes. Upstream cost: 1.
|
|
@@ -139,7 +204,8 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
139
204
|
type: 'string',
|
|
140
205
|
enum: ['upcoming', 'recent'],
|
|
141
206
|
default: 'upcoming',
|
|
142
|
-
description: 'upcoming = scheduled cards, soonest first; recent = completed cards, newest first.
|
|
207
|
+
description: 'upcoming = scheduled cards, soonest first; recent = completed cards, newest first. Default "upcoming" ' +
|
|
208
|
+
'("recent" for the MMA archives). Example: "recent".',
|
|
143
209
|
},
|
|
144
210
|
limit: limitSchema({ default: 10, max: 50, description: 'Max events (default 10, max 50).' }),
|
|
145
211
|
},
|
|
@@ -149,14 +215,28 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
149
215
|
const league = parseLeague(args.league);
|
|
150
216
|
if (!league)
|
|
151
217
|
return leagueError(d, args.league);
|
|
152
|
-
const
|
|
218
|
+
const archive = archiveOf(league);
|
|
219
|
+
const when = args.when === 'recent' || args.when === 'upcoming' ? args.when : archive ? 'recent' : 'upcoming';
|
|
153
220
|
const limit = clampInt(args.limit, 10, 1, 50);
|
|
221
|
+
if (archive && when === 'upcoming') {
|
|
222
|
+
return successEnvelope({
|
|
223
|
+
data: {
|
|
224
|
+
league, leagueName: LEAGUE_NAMES[league], when, count: 0, events: [],
|
|
225
|
+
note: `${archive.note}: there are no upcoming cards. Use when "recent" for its cards, newest first.`,
|
|
226
|
+
},
|
|
227
|
+
game: league,
|
|
228
|
+
source: d.source,
|
|
229
|
+
requestId: d.requestId,
|
|
230
|
+
tookMs: Date.now() - d.started,
|
|
231
|
+
upstreamCalls: 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
154
234
|
const res = await fetchJson(ctx, `/${league}/events/${when}`, { query: { limit } });
|
|
155
235
|
if (!res.ok)
|
|
156
236
|
return httpError(d, league, res, `${league} ${when} events`);
|
|
157
237
|
const events = extractRows(res.data).slice(0, limit);
|
|
158
238
|
return successEnvelope({
|
|
159
|
-
data: { league, leagueName: LEAGUE_NAMES[league], when, count: events.length, events },
|
|
239
|
+
data: { league, leagueName: LEAGUE_NAMES[league], when, count: events.length, events, ...upstreamNotes(res) },
|
|
160
240
|
game: league,
|
|
161
241
|
source: d.source,
|
|
162
242
|
requestId: d.requestId,
|
|
@@ -168,15 +248,18 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
168
248
|
};
|
|
169
249
|
export const combatCard = {
|
|
170
250
|
name: 'combat_card',
|
|
171
|
-
description: `COMPOSITE fight card for one RAF or
|
|
251
|
+
description: `COMPOSITE fight card for one RAF, BKFC or MMA archive event: event identity + bouts in running order with results.
|
|
172
252
|
|
|
173
253
|
When to use:
|
|
174
|
-
- "RAF 14 card", "BKFC 95 results", "who won the main event at the last BKFC?"
|
|
254
|
+
- "RAF 14 card", "BKFC 95 results", "Bellator 142 card", "PRIDE 34", "who won the main event at the last BKFC?"
|
|
175
255
|
- Event pages and results pages
|
|
176
256
|
|
|
177
|
-
event accepts a slug (e.g. "raf13", "bkfc-95-newark") or "next" / "last".
|
|
178
|
-
weight class, title stakes, both sides (athletes for RAF, red/blue fighters for BKFC with
|
|
179
|
-
|
|
257
|
+
event accepts a slug (e.g. "raf13", "bkfc-95-newark", "bellator-142", "pride-34", "wec-48") or "next" / "last".
|
|
258
|
+
Bouts come main event first; each carries weight class, title stakes, both sides (athletes for RAF, red/blue fighters for BKFC with
|
|
259
|
+
punch totals, red/blue fighters for the MMA archives with UFC-format totals and ufcFighterSlug), and once finished
|
|
260
|
+
the winner and result (RAF: score, takedowns, method; BKFC/MMA archives: method, round, time).
|
|
261
|
+
MMA archives are closed: event defaults to "last" (the most recent card; for mma, across every promotion), and
|
|
262
|
+
"next" is a non-retryable NOT_IMPLEMENTED (there are no upcoming cards).
|
|
180
263
|
|
|
181
264
|
Prefer over: combat_events + call_api per bout.
|
|
182
265
|
Do not use when: you need one bout's period/round breakdown (combat_bout).
|
|
@@ -189,7 +272,7 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
189
272
|
required: ['league'],
|
|
190
273
|
properties: {
|
|
191
274
|
league: leagueSchema,
|
|
192
|
-
event: stringSchema('Event slug, or "next" / "last" (default "next"). Example: "raf13".', 'next'),
|
|
275
|
+
event: stringSchema('Event slug, or "next" / "last" (default "next"; "last" for the MMA archives). Example: "raf13".', 'next'),
|
|
193
276
|
limit: limitSchema({ default: 30, max: 40, description: 'Max bouts on the card (default 30, max 40).' }),
|
|
194
277
|
},
|
|
195
278
|
},
|
|
@@ -199,8 +282,14 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
199
282
|
if (!league)
|
|
200
283
|
return leagueError(d, args.league);
|
|
201
284
|
const limit = clampInt(args.limit, 30, 1, 40);
|
|
202
|
-
|
|
285
|
+
const archive = archiveOf(league);
|
|
286
|
+
let slug = typeof args.event === 'string' && args.event.trim() ? args.event.trim().toLowerCase() : archive ? 'last' : 'next';
|
|
203
287
|
let calls = 0;
|
|
288
|
+
if (archive && slug === 'next') {
|
|
289
|
+
return archiveGap(d, league, `${archive.note}: there is no next card.`, [
|
|
290
|
+
`Use event "last" for the most recent ${league} card, or a slug from combat_events { league: "${league}", when: "recent" }`,
|
|
291
|
+
]);
|
|
292
|
+
}
|
|
204
293
|
if (slug === 'next' || slug === 'last') {
|
|
205
294
|
const which = slug === 'next' ? 'upcoming' : 'recent';
|
|
206
295
|
const list = await fetchJson(ctx, `/${league}/events/${which}`, { query: { limit: 1 } });
|
|
@@ -209,20 +298,24 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
209
298
|
return httpError(d, league, list, `${league} ${which} events`);
|
|
210
299
|
const first = asRecord(extractRows(list.data)[0]);
|
|
211
300
|
if (!first?.slug) {
|
|
301
|
+
const windowed = !!upstreamNotes(list).historyWindow;
|
|
212
302
|
return errorEnvelope({
|
|
213
303
|
code: 'NOT_FOUND',
|
|
214
|
-
message:
|
|
304
|
+
message: windowed
|
|
305
|
+
? `No ${which} ${league.toUpperCase()} event inside this key's history window (meta.historyWindow); pass an event slug instead`
|
|
306
|
+
: `No ${which} ${league.toUpperCase()} event`,
|
|
215
307
|
game: league,
|
|
216
308
|
source: d.source,
|
|
217
309
|
requestId: d.requestId,
|
|
218
310
|
tookMs: Date.now() - d.started,
|
|
219
311
|
upstreamCalls: calls,
|
|
312
|
+
...(windowed ? { retryable: false, details: { historyWindow: upstreamNotes(list).historyWindow } } : {}),
|
|
220
313
|
});
|
|
221
314
|
}
|
|
222
315
|
slug = String(first.slug);
|
|
223
316
|
}
|
|
224
317
|
const [ev, bouts] = await Promise.all([
|
|
225
|
-
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}
|
|
318
|
+
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}`, archive ? { query: { includeBouts: 'false' } } : undefined),
|
|
226
319
|
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}/bouts`),
|
|
227
320
|
]);
|
|
228
321
|
calls += 2;
|
|
@@ -235,10 +328,10 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
235
328
|
rows = Array.isArray(p) ? p : extractRows(asRecord(p)?.bouts ?? p);
|
|
236
329
|
}
|
|
237
330
|
else {
|
|
238
|
-
partial.push(sectionFailure('bouts', bouts));
|
|
331
|
+
partial.push(sectionFailure('bouts', bouts, league));
|
|
239
332
|
}
|
|
240
333
|
const eventData = asRecord(payload(ev)) ?? {};
|
|
241
|
-
const { bouts: _ignored, ...event } = eventData;
|
|
334
|
+
const { bouts: _ignored, card: _card, ...event } = eventData;
|
|
242
335
|
return successEnvelope({
|
|
243
336
|
data: { league, event, boutCount: rows.length, bouts: rows.slice(0, limit) },
|
|
244
337
|
game: league,
|
|
@@ -256,13 +349,20 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
256
349
|
};
|
|
257
350
|
export const combatBout = {
|
|
258
351
|
name: 'combat_bout',
|
|
259
|
-
description: `One RAF or
|
|
352
|
+
description: `One RAF, BKFC or MMA archive bout in full, with its scoring breakdown.
|
|
260
353
|
|
|
261
354
|
RAF: winner, method (decision / tech fall / pin / injury), final score, takedowns, and per-PERIOD scores.
|
|
262
355
|
BKFC: winner, method, round, time, and per-ROUND punch stats for red and blue (landed/thrown, head/body,
|
|
263
356
|
distance/clinch, knockdowns). BKFC round stats need the Starter plan; on Free they come back as a partial note.
|
|
357
|
+
MMA archives (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction, mma): winner, method, round, time,
|
|
358
|
+
finish (position / submission), referee, judges' scores, each corner's whole-fight totals, and per-ROUND official
|
|
359
|
+
stats in UFC's format (significantStrikes "13 of 18", totalStrikes, takedowns, head/body/leg, distance/clinch/ground,
|
|
360
|
+
knockdowns, submissionAttempts, reversals, controlTime "1:42"). PRIDE's first round is 10 minutes. The archives are
|
|
361
|
+
older than the Free/Starter history window: on those plans rounds come back as a HISTORY_WINDOW_EXCEEDED partial
|
|
362
|
+
note while the result still returns.
|
|
264
363
|
|
|
265
|
-
Get boutId from combat_card or combat_athlete (e.g. "raf13--belal-muhammad--colby-covington").
|
|
364
|
+
Get boutId from combat_card or combat_athlete (e.g. "raf13--belal-muhammad--colby-covington", "bellator-5600").
|
|
365
|
+
MMA archive bout ids are "<promotion>-<fightId>"; a bare fight id resolves in any MMA archive league.
|
|
266
366
|
|
|
267
367
|
Parallel-safe: yes. Upstream cost: 1–2.
|
|
268
368
|
Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cisneros--ryan-petersen" }`,
|
|
@@ -272,8 +372,8 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
272
372
|
required: ['league', 'boutId'],
|
|
273
373
|
properties: {
|
|
274
374
|
league: leagueSchema,
|
|
275
|
-
boutId: stringSchema('Bout id from combat_card / combat_athlete. Example: "raf13--belal-muhammad--colby-covington".'),
|
|
276
|
-
includeBreakdown: boolSchema('Include per-period (RAF) / per-round (BKFC) breakdown (default true).', true),
|
|
375
|
+
boutId: stringSchema('Bout id from combat_card / combat_athlete (MMA archives: "<promotion>-<fightId>"). Example: "raf13--belal-muhammad--colby-covington".'),
|
|
376
|
+
includeBreakdown: boolSchema('Include per-period (RAF) / per-round (BKFC, MMA archives) breakdown (default true).', true),
|
|
277
377
|
},
|
|
278
378
|
},
|
|
279
379
|
handler: async (args, ctx) => {
|
|
@@ -301,11 +401,13 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
301
401
|
if (parts.ok)
|
|
302
402
|
data[sub] = asRecord(payload(parts))?.[sub] ?? payload(parts);
|
|
303
403
|
else
|
|
304
|
-
partial.push(sectionFailure(sub, parts));
|
|
404
|
+
partial.push(sectionFailure(sub, parts, league));
|
|
305
405
|
}
|
|
306
406
|
const b = asRecord(data.bout);
|
|
307
407
|
if (league === 'bkfc' && b && 'rounds' in b && data.rounds)
|
|
308
408
|
delete b.rounds;
|
|
409
|
+
if (archiveOf(league) && b && 'roundStats' in b && (data.rounds || !breakdown))
|
|
410
|
+
delete b.roundStats;
|
|
309
411
|
return successEnvelope({
|
|
310
412
|
data,
|
|
311
413
|
game: league,
|
|
@@ -320,15 +422,20 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
320
422
|
};
|
|
321
423
|
export const combatAthlete = {
|
|
322
424
|
name: 'combat_athlete',
|
|
323
|
-
description: `COMPOSITE RAF athlete / BKFC fighter page: profile + recent bouts + career stats.
|
|
425
|
+
description: `COMPOSITE RAF athlete / BKFC or MMA archive fighter page: profile + recent bouts + career stats.
|
|
324
426
|
|
|
325
427
|
When to use:
|
|
326
|
-
- "Kyle Snyder's RAF record", "Arlovski BKFC stats", athlete/fighter profile pages
|
|
428
|
+
- "Kyle Snyder's RAF record", "Arlovski BKFC stats", "Tito Ortiz in Bellator", "Fedor's PRIDE record", athlete/fighter profile pages
|
|
327
429
|
|
|
328
430
|
athlete accepts a slug ("kyle-snyder") or a name ("Andrei Arlovski"); names are resolved through the league's
|
|
329
431
|
search, and several matches return AMBIGUOUS_ENTITY with candidates. Profile carries record, division, nickname,
|
|
330
432
|
country, official headshot (+ its credit — show it with the image). Career stats need the Starter plan; on Free
|
|
331
433
|
they come back as a partial note while profile and history still return.
|
|
434
|
+
MMA archives: profile is bio (DOB, height, reach, stance, hometown) + the record in that promotion's archive (not the
|
|
435
|
+
whole career); stats are career totals in UFC's format. league "mma" covers every archived promotion at once, and
|
|
436
|
+
data.recordByPromotion splits the record ({ pride: {...}, strikeforce: {...} }). A fighter we also hold a UFC record
|
|
437
|
+
for keeps the SAME slug as /ufc/fighters/{slug}: data.ufcFighterSlug is set then (a UFC slug is accepted here too).
|
|
438
|
+
Keep the records apart: archive fights never belong in a UFC answer.
|
|
332
439
|
|
|
333
440
|
Prefer over: call_api profile + history + stats separately.
|
|
334
441
|
|
|
@@ -359,28 +466,35 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
|
|
|
359
466
|
const r = await resolveOrFail(d, ctx, league, input, 'athlete');
|
|
360
467
|
if ('fail' in r)
|
|
361
468
|
return r.fail;
|
|
362
|
-
const base = `/${league}/${PEOPLE
|
|
469
|
+
const base = `/${league}/${PEOPLE(league)}/${encodeURIComponent(r.slug)}`;
|
|
363
470
|
const historyLimit = clampInt(args.historyLimit, 10, 1, 50);
|
|
364
471
|
const [profile, history, stats] = await Promise.all([
|
|
365
472
|
fetchJson(ctx, base),
|
|
366
|
-
args.includeHistory === false ? Promise.resolve(null) : fetchJson(ctx, `${base}/${HISTORY
|
|
473
|
+
args.includeHistory === false ? Promise.resolve(null) : fetchJson(ctx, `${base}/${HISTORY(league)}`, { query: { limit: historyLimit } }),
|
|
367
474
|
args.includeStats === false ? Promise.resolve(null) : fetchJson(ctx, `${base}/stats`),
|
|
368
475
|
]);
|
|
369
476
|
if (!profile.ok)
|
|
370
|
-
return httpError(d, league, profile, `${league} ${PEOPLE
|
|
477
|
+
return httpError(d, league, profile, `${league} ${PEOPLE(league)} ${r.slug}`);
|
|
371
478
|
const partial = [];
|
|
372
479
|
const data = { league, slug: r.slug, profile: payload(profile) };
|
|
480
|
+
if (isMma(league)) {
|
|
481
|
+
const ufc = asRecord(asRecord(data.profile)?.ufc);
|
|
482
|
+
data.ufcFighterSlug = typeof ufc?.ufcFighterSlug === 'string' ? ufc.ufcFighterSlug : null;
|
|
483
|
+
const byPromotion = asRecord(data.profile)?.recordByPromotion;
|
|
484
|
+
if (league === 'mma' || byPromotion)
|
|
485
|
+
data.recordByPromotion = asRecord(byPromotion) ?? null;
|
|
486
|
+
}
|
|
373
487
|
if (history) {
|
|
374
488
|
if (history.ok)
|
|
375
489
|
data.history = extractRows(history.data).slice(0, historyLimit);
|
|
376
490
|
else
|
|
377
|
-
partial.push(sectionFailure('history', history));
|
|
491
|
+
partial.push(sectionFailure('history', history, league));
|
|
378
492
|
}
|
|
379
493
|
if (stats) {
|
|
380
494
|
if (stats.ok)
|
|
381
495
|
data.stats = payload(stats);
|
|
382
496
|
else
|
|
383
|
-
partial.push(sectionFailure('stats', stats));
|
|
497
|
+
partial.push(sectionFailure('stats', stats, league));
|
|
384
498
|
}
|
|
385
499
|
return successEnvelope({
|
|
386
500
|
data,
|
|
@@ -396,9 +510,11 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
|
|
|
396
510
|
};
|
|
397
511
|
export const combatH2h = {
|
|
398
512
|
name: 'combat_h2h',
|
|
399
|
-
description: `Head-to-head between two RAF athletes or two
|
|
513
|
+
description: `Head-to-head between two RAF athletes, two BKFC fighters or two MMA archive fighters: record and every meeting.
|
|
400
514
|
|
|
401
|
-
Accepts slugs or names for both sides (names resolve through the league's search).
|
|
515
|
+
Accepts slugs or names for both sides (names resolve through the league's search). MMA archive meetings are only
|
|
516
|
+
the ones on that promotion's cards (league "mma": every archived promotion); UFC meetings are a UFC question
|
|
517
|
+
(head_to_head).
|
|
402
518
|
|
|
403
519
|
Parallel-safe: yes. Upstream cost: 1–3.
|
|
404
520
|
Example: { "league": "bkfc", "athleteA": "ryan-petersen", "athleteB": "gregoris-cisneros" }`,
|
|
@@ -453,6 +569,9 @@ export const combatRankings = {
|
|
|
453
569
|
BKFC: omit division for every division's ranked list (champion, interim champion, ranked fighters), or pass one
|
|
454
570
|
(e.g. "heavyweight", "womens-flyweight"). RAF has no ranked lists; it returns every title and its holder, or one
|
|
455
571
|
division's detail when division is given (e.g. "men-heavyweight").
|
|
572
|
+
The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction, mma) have no rankings: they return a
|
|
573
|
+
non-retryable NOT_IMPLEMENTED.
|
|
574
|
+
Never answer with UFC rankings instead.
|
|
456
575
|
|
|
457
576
|
Parallel-safe: yes. Upstream cost: 1.
|
|
458
577
|
Example: { "league": "bkfc", "division": "heavyweight" }`,
|
|
@@ -470,6 +589,14 @@ Example: { "league": "bkfc", "division": "heavyweight" }`,
|
|
|
470
589
|
const league = parseLeague(args.league);
|
|
471
590
|
if (!league)
|
|
472
591
|
return leagueError(d, args.league);
|
|
592
|
+
const archive = archiveOf(league) ?? (isMma(league) ? { short: league === 'pfl' ? 'PFL' : league === 'oktagon' ? 'OKTAGON' : 'The MMA namespace', note: 'the API carries its cards and results' } : undefined);
|
|
593
|
+
if (archive) {
|
|
594
|
+
return archiveGap(d, league, `${archive.short} has no rankings or champions lists: ${archive.note}, and the API serves its results only.`, [
|
|
595
|
+
`Do not retry: no ${league} rankings exist on the API`,
|
|
596
|
+
`Title fights: call_api /${league}/bouts with query { "title": "true" }; results: combat_events { league: "${league}", when: "recent" }`,
|
|
597
|
+
`Never substitute UFC rankings for ${archive.short}`,
|
|
598
|
+
]);
|
|
599
|
+
}
|
|
473
600
|
const division = typeof args.division === 'string' ? args.division.trim().toLowerCase() : '';
|
|
474
601
|
const path = league === 'bkfc'
|
|
475
602
|
? division ? `/bkfc/rankings/${encodeURIComponent(division)}` : '/bkfc/rankings'
|
package/dist/tools/meta.js
CHANGED
|
@@ -2,23 +2,24 @@ import { extractRows, fetchJson, gameNotIncludedHint, present } from '../client.
|
|
|
2
2
|
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
3
3
|
import { boolSchema, gameSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
4
4
|
import { PACKAGE_VERSION as CATALOG_VERSION } from '../version.js';
|
|
5
|
+
import { COMBAT_LEAGUES } from './combat.js';
|
|
5
6
|
const COMBAT_CATALOG = [
|
|
6
7
|
{
|
|
7
8
|
name: 'combat_events',
|
|
8
9
|
doNotUse: 'You already have the event slug → combat_card',
|
|
9
|
-
outcome: 'RAF wrestling / BKFC bare-knuckle events: upcoming cards or recent results, with date, local time and venue',
|
|
10
|
+
outcome: 'RAF wrestling / BKFC bare-knuckle / MMA archive (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction, mma = all) events: upcoming cards or recent results, with date, local time and venue (archives: upcoming is empty with a note)',
|
|
10
11
|
parallelSafe: true,
|
|
11
|
-
games: [
|
|
12
|
+
games: [...COMBAT_LEAGUES],
|
|
12
13
|
jobs: ['schedule'],
|
|
13
14
|
exampleArgs: { league: 'bkfc', when: 'upcoming', limit: 5 },
|
|
14
|
-
preferOver: ['call_api /
|
|
15
|
+
preferOver: ['call_api /{league}/events'],
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
name: 'combat_card',
|
|
18
19
|
doNotUse: "One bout's period/round breakdown → combat_bout",
|
|
19
|
-
outcome: 'One RAF/BKFC event card in running order with results; event = slug, "next" or "last"',
|
|
20
|
+
outcome: 'One RAF/BKFC/MMA archive event card in running order with results; event = slug, "next" or "last" (archives: "last" = most recent card, across promotions for mma; no "next")',
|
|
20
21
|
parallelSafe: true,
|
|
21
|
-
games: [
|
|
22
|
+
games: [...COMBAT_LEAGUES],
|
|
22
23
|
jobs: ['event_card', 'schedule', 'media'],
|
|
23
24
|
exampleArgs: { league: 'raf', event: 'last' },
|
|
24
25
|
preferOver: ['combat_events + call_api per bout'],
|
|
@@ -27,9 +28,9 @@ const COMBAT_CATALOG = [
|
|
|
27
28
|
name: 'combat_bout',
|
|
28
29
|
doNotUse: 'Whole card → combat_card',
|
|
29
30
|
preferOver: [],
|
|
30
|
-
outcome: 'One bout in full: RAF per-period scores / BKFC per-round punch stats (Starter)',
|
|
31
|
+
outcome: 'One bout in full: RAF per-period scores / BKFC per-round punch stats (Starter) / MMA archive per-round official stats in UFC format (Pro: the archives are older than the Free/Starter window)',
|
|
31
32
|
parallelSafe: true,
|
|
32
|
-
games: [
|
|
33
|
+
games: [...COMBAT_LEAGUES],
|
|
33
34
|
jobs: ['match_page'],
|
|
34
35
|
exampleArgs: { league: 'raf', boutId: 'raf13--belal-muhammad--colby-covington' },
|
|
35
36
|
},
|
|
@@ -37,9 +38,9 @@ const COMBAT_CATALOG = [
|
|
|
37
38
|
name: 'combat_athlete',
|
|
38
39
|
doNotUse: "Two athletes' meetings → combat_h2h",
|
|
39
40
|
preferOver: [],
|
|
40
|
-
outcome: 'RAF athlete / BKFC fighter page: profile + headshot, recent bouts, career stats (Starter); slug or name',
|
|
41
|
+
outcome: 'RAF athlete / BKFC or MMA archive fighter page: profile + headshot, recent bouts, career stats (Starter); slug or name; archive fighters share the UFC slug (ufcFighterSlug); league mma = whole non-UFC career with recordByPromotion',
|
|
41
42
|
parallelSafe: true,
|
|
42
|
-
games: [
|
|
43
|
+
games: [...COMBAT_LEAGUES],
|
|
43
44
|
jobs: ['player_form', 'media'],
|
|
44
45
|
exampleArgs: { league: 'raf', athlete: 'Kyle Snyder', historyLimit: 5 },
|
|
45
46
|
},
|
|
@@ -47,19 +48,19 @@ const COMBAT_CATALOG = [
|
|
|
47
48
|
name: 'combat_h2h',
|
|
48
49
|
doNotUse: "One athlete's whole history → combat_athlete",
|
|
49
50
|
preferOver: [],
|
|
50
|
-
outcome: 'Head-to-head between two RAF athletes or two
|
|
51
|
+
outcome: 'Head-to-head between two RAF athletes, two BKFC fighters or two MMA archive fighters',
|
|
51
52
|
parallelSafe: true,
|
|
52
|
-
games: [
|
|
53
|
+
games: [...COMBAT_LEAGUES],
|
|
53
54
|
jobs: ['h2h', 'preview'],
|
|
54
55
|
exampleArgs: { league: 'bkfc', athleteA: 'ryan-petersen', athleteB: 'gregoris-cisneros' },
|
|
55
56
|
},
|
|
56
57
|
{
|
|
57
58
|
name: 'combat_rankings',
|
|
58
|
-
doNotUse: 'UFC rankings → standings / rankings tools (RAF/BKFC never appear in UFC answers)',
|
|
59
|
+
doNotUse: 'UFC rankings → standings / rankings tools (RAF/BKFC/MMA archives never appear in UFC answers); the MMA archives have no rankings (NOT_IMPLEMENTED)',
|
|
59
60
|
preferOver: [],
|
|
60
61
|
outcome: 'RAF champions per division / BKFC official rankings and champions',
|
|
61
62
|
parallelSafe: true,
|
|
62
|
-
games: [
|
|
63
|
+
games: [...COMBAT_LEAGUES],
|
|
63
64
|
jobs: ['standings'],
|
|
64
65
|
exampleArgs: { league: 'bkfc', division: 'heavyweight' },
|
|
65
66
|
},
|
|
@@ -547,7 +548,14 @@ const GAMES_META = [
|
|
|
547
548
|
{ id: 'cs2', label: 'Counter-Strike 2', depth: 'deep', notes: 'Prefixed ids; live path /cs2/live; trends/rankings' },
|
|
548
549
|
{ id: 'dota2', label: 'Dota 2', depth: 'thin', notes: 'Numeric ids; radar; no standings/roster endpoints' },
|
|
549
550
|
{ id: 'cod', label: 'Call of Duty (CDL)', depth: 'medium', notes: 'Org slugs; CDL standings; UUID match ids' },
|
|
550
|
-
{
|
|
551
|
+
{
|
|
552
|
+
id: 'ufc',
|
|
553
|
+
label: 'UFC / MMA',
|
|
554
|
+
depth: 'medium',
|
|
555
|
+
notes: 'Fighter slugs; boutId; live + rankings. Includes Dana White\'s Contender Series and Road to UFC (2022-2026, all 19 cards ' +
|
|
556
|
+
'with official round stats; event slugs like road-to-ufc-2-7, titles "Road to UFC 2.7"). Bellator, PRIDE, Strikeforce, WEC, ' +
|
|
557
|
+
'DREAM, EliteXC and Affliction are NOT UFC: combat_* tools with that league (or mma), never mixed into UFC answers.',
|
|
558
|
+
},
|
|
551
559
|
];
|
|
552
560
|
export const listCapabilities = {
|
|
553
561
|
name: 'list_capabilities',
|
|
@@ -789,6 +797,16 @@ const ALLOWLIST_PREFIXES = [
|
|
|
789
797
|
'/tennis',
|
|
790
798
|
'/raf',
|
|
791
799
|
'/bkfc',
|
|
800
|
+
'/bellator',
|
|
801
|
+
'/pride',
|
|
802
|
+
'/strikeforce',
|
|
803
|
+
'/wec',
|
|
804
|
+
'/dream',
|
|
805
|
+
'/elitexc',
|
|
806
|
+
'/affliction',
|
|
807
|
+
'/pfl',
|
|
808
|
+
'/oktagon',
|
|
809
|
+
'/mma',
|
|
792
810
|
'/openapi.json',
|
|
793
811
|
];
|
|
794
812
|
function pathAllowed(path) {
|
|
@@ -812,7 +830,7 @@ Prefer curated tools for all standard jobs (live, schedule, profiles, standings,
|
|
|
812
830
|
|
|
813
831
|
Do not use when: a curated tool covers the outcome. Avoid parallel storms; same plan rate limits apply.
|
|
814
832
|
|
|
815
|
-
Path must start with / and match allowlisted prefixes: /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc.
|
|
833
|
+
Path must start with / and match allowlisted prefixes: /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc, /bellator, /pride, /strikeforce, /wec, /dream, /elitexc, /affliction, /pfl, /oktagon, /mma.
|
|
816
834
|
Rejects absolute URLs and path traversal → PATH_NOT_ALLOWED.
|
|
817
835
|
|
|
818
836
|
Parallel-safe: yes but discouraged in bulk. Upstream cost: 1.
|
|
@@ -873,7 +891,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
873
891
|
source: 'call_api',
|
|
874
892
|
requestId,
|
|
875
893
|
tookMs: Date.now() - started,
|
|
876
|
-
hint: 'Use prefixes /health /lol /cs2 /dota2 /cod /ufc /fortnite /tennis',
|
|
894
|
+
hint: 'Use prefixes /health /lol /cs2 /dota2 /cod /ufc /fortnite /tennis /raf /bkfc /bellator /pride /strikeforce /wec /dream /elitexc /affliction /pfl /oktagon /mma',
|
|
877
895
|
});
|
|
878
896
|
}
|
|
879
897
|
let query = {};
|
|
@@ -974,10 +992,10 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
974
992
|
const started = Date.now();
|
|
975
993
|
const requestId = newRequestId();
|
|
976
994
|
const gameRaw = typeof args.game === 'string' ? args.game.toLowerCase().trim() : '';
|
|
977
|
-
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc)$/)) {
|
|
995
|
+
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator|pride|strikeforce|wec|dream|elitexc|affliction|pfl|oktagon|mma)$/)) {
|
|
978
996
|
return errorEnvelope({
|
|
979
997
|
code: 'UNSUPPORTED_GAME',
|
|
980
|
-
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc`,
|
|
998
|
+
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator|pride|strikeforce|wec|dream|elitexc|affliction|pfl|oktagon|mma`,
|
|
981
999
|
game: null,
|
|
982
1000
|
source: 'list_routes',
|
|
983
1001
|
requestId,
|
|
@@ -1000,7 +1018,7 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
1000
1018
|
httpStatus: res.status,
|
|
1001
1019
|
recover: [
|
|
1002
1020
|
'Call list_capabilities for curated tools that need no route knowledge',
|
|
1003
|
-
'call_api accepts /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite',
|
|
1021
|
+
'call_api accepts /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc, /bellator, /pride, /strikeforce, /wec, /dream, /elitexc, /affliction, /pfl, /oktagon, /mma',
|
|
1004
1022
|
],
|
|
1005
1023
|
});
|
|
1006
1024
|
}
|
package/package.json
CHANGED
|
@@ -1,51 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cito-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Standalone MCP server for the Cito esports and sports API \u2014 curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"homepage": "https://cito.gg",
|
|
7
|
-
"//REPOSITORY": "Intentionally NOT set. cito-api-scraper and cito.gg are PRIVATE repos and must stay that way, so a repository URL pointing at them would render as a broken 404 link on the public npm page and would not satisfy MCP Registry npm ownership validation. Set this only if a PUBLIC repo for this package is ever created (e.g. achillesscriptsvip/cito-mcp with this directory as root) \u2014 then use `git+https://github.com/OWNER/REPO.git` and mirror it in the `mcpName`/server.json `name` pair. Do not point it at the private monorepo.",
|
|
8
|
-
"keywords": [
|
|
9
|
-
"mcp",
|
|
10
|
-
"model-context-protocol",
|
|
11
|
-
"esports",
|
|
12
|
-
"cs2",
|
|
13
|
-
"counter-strike",
|
|
14
|
-
"counter-strike-2",
|
|
15
|
-
"lol",
|
|
16
|
-
"league-of-legends",
|
|
17
|
-
"dota2",
|
|
18
|
-
"cod",
|
|
19
|
-
"ufc",
|
|
20
|
-
"mma",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "cito-mcp",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Standalone MCP server for the Cito esports and sports API \u2014 curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://cito.gg",
|
|
7
|
+
"//REPOSITORY": "Intentionally NOT set. cito-api-scraper and cito.gg are PRIVATE repos and must stay that way, so a repository URL pointing at them would render as a broken 404 link on the public npm page and would not satisfy MCP Registry npm ownership validation. Set this only if a PUBLIC repo for this package is ever created (e.g. achillesscriptsvip/cito-mcp with this directory as root) \u2014 then use `git+https://github.com/OWNER/REPO.git` and mirror it in the `mcpName`/server.json `name` pair. Do not point it at the private monorepo.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mcp",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"esports",
|
|
12
|
+
"cs2",
|
|
13
|
+
"counter-strike",
|
|
14
|
+
"counter-strike-2",
|
|
15
|
+
"lol",
|
|
16
|
+
"league-of-legends",
|
|
17
|
+
"dota2",
|
|
18
|
+
"cod",
|
|
19
|
+
"ufc",
|
|
20
|
+
"mma",
|
|
21
|
+
"bellator",
|
|
22
|
+
"tennis",
|
|
23
|
+
"live-scores",
|
|
24
|
+
"sports-data",
|
|
25
|
+
"esports-api"
|
|
26
|
+
],
|
|
27
|
+
"bin": {
|
|
28
|
+
"cito-mcp": "dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
39
|
+
"test": "tsx --test src/**/*.test.ts src/*.test.ts",
|
|
40
|
+
"start": "node dist/index.js",
|
|
41
|
+
"prepublishOnly": "npm run build && npm test && npm run smoke",
|
|
42
|
+
"smoke": "node scripts/smoke.mjs"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "1.29.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.0",
|
|
49
|
+
"tsx": "^4.23.1",
|
|
50
|
+
"typescript": "^5.9.2"
|
|
51
|
+
}
|
|
52
|
+
}
|