cito-mcp 0.4.9 → 0.5.2
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 +6 -6
- package/dist/envelope.js +1 -1
- package/dist/instructions.js +3 -1
- package/dist/tools/combat.js +93 -59
- package/dist/tools/cs2.js +74 -0
- package/dist/tools/meta.js +40 -20
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Standalone [MCP](https://modelcontextprotocol.io) server for the [Cito esports A
|
|
|
4
4
|
|
|
5
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**, **BKFC bare knuckle** and the **
|
|
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
|
|
|
@@ -244,13 +244,13 @@ upstream, so a non-null `headshotUrl` always has a `license` beside it.
|
|
|
244
244
|
|
|
245
245
|
### Combat leagues (`combat_*`, `league` argument)
|
|
246
246
|
|
|
247
|
-
RAF, BKFC and
|
|
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
248
|
|
|
249
249
|
| League | Scoring | Notes |
|
|
250
250
|
| --- | --- | --- |
|
|
251
251
|
| `raf` | per PERIOD (wrestling points, takedowns) | Athletes; champions per division |
|
|
252
252
|
| `bkfc` | per ROUND (punches landed/thrown, knockdowns) | Fighters; official rankings + champions |
|
|
253
|
-
| `bellator` | per ROUND, official stats in UFC's shape (`"13 of 18"`, `controlTime "1:42"`, round 0 = whole fight; knockdowns, submission attempts, reversals) | Closed
|
|
253
|
+
| MMA archives: `bellator`, `pride`, `strikeforce`, `wec`, `dream`, `elitexc`, `affliction`, `mma` | per ROUND, official stats in UFC's shape (`"13 of 18"`, `controlTime "1:42"`, round 0 = whole fight; knockdowns, submission attempts, reversals) | Closed archives on one API router with identical routes: Bellator 131-172 (2014-2017), PRIDE 1997-2007 (PRIDE rules: 10-minute first round, then 5-minute rounds), Strikeforce 2006-2013, WEC 2001-2010, DREAM 2008-2012, EliteXC 2007-2008, Affliction 2008-2009. `mma` is every promotion at once: a fighter's whole non-UFC career, with `recordByPromotion` on `combat_athlete`, and `recent` / `last` across promotions; every bout and event carries its real `promotion`. Event slugs like `bellator-142`, `pride-34`, `wec-48`, `strikeforce-barnett-vs-cormier`; bout ids `<promotion>-<fightId>` (a bare fight id resolves in any archive). A fighter we also hold a UFC record for keeps the **same slug** as `/ufc/fighters/{slug}` (`ufcFighterSlug`). No live data, rankings or upcoming cards: `combat_events` `when:"upcoming"` returns `[]` with a note (default is `recent`), `combat_card` `event:"last"` is the most recent card (the default) and `"next"` is a non-retryable `NOT_IMPLEMENTED`, `combat_rankings` is a non-retryable `NOT_IMPLEMENTED`. All of it is older than the Free (30-day) / Starter (90-day) history window, so its stats are Pro: on those plans round stats come back as a `HISTORY_WINDOW_EXCEEDED` partial note while results still return. |
|
|
254
254
|
|
|
255
255
|
### UFC projection notes (0.2.4)
|
|
256
256
|
|
|
@@ -269,7 +269,7 @@ Fortnite: **`call_api` only** until promoted into the primary enum.
|
|
|
269
269
|
|
|
270
270
|
Path must start with `/` and match one of:
|
|
271
271
|
|
|
272
|
-
`/health` · `/lol` · `/cs2` · `/dota2` · `/cod` · `/ufc` · `/fortnite` · `/tennis` · `/raf` · `/bkfc` · `/bellator` · `/openapi.json`
|
|
272
|
+
`/health` · `/lol` · `/cs2` · `/dota2` · `/cod` · `/ufc` · `/fortnite` · `/tennis` · `/raf` · `/bkfc` · `/bellator` · `/pride` · `/strikeforce` · `/wec` · `/dream` · `/elitexc` · `/affliction` · `/mma` · `/openapi.json`
|
|
273
273
|
|
|
274
274
|
Absolute URLs and `..` traversal → `PATH_NOT_ALLOWED`. Prefer curated tools for all standard jobs.
|
|
275
275
|
|
|
@@ -552,8 +552,8 @@ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
|
552
552
|
|
|
553
553
|
### 0.4.9
|
|
554
554
|
|
|
555
|
-
- **
|
|
556
|
-
- Agent guidance (server instructions, `list_capabilities`):
|
|
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
557
|
|
|
558
558
|
### 0.4.8
|
|
559
559
|
|
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 /tennis /raf /bkfc /bellator',
|
|
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,7 +18,9 @@ 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
|
-
- Bellator
|
|
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.
|
|
22
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").
|
|
23
25
|
|
|
24
26
|
## Preferred tool order
|
package/dist/tools/combat.js
CHANGED
|
@@ -1,22 +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',
|
|
8
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)',
|
|
9
21
|
};
|
|
10
22
|
const ARCHIVES = {
|
|
11
|
-
bellator: 'Bellator is a closed archive (Bellator 131-172,
|
|
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)' },
|
|
12
30
|
};
|
|
13
|
-
|
|
14
|
-
|
|
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');
|
|
15
36
|
const leagueSchema = {
|
|
16
37
|
type: 'string',
|
|
17
38
|
enum: [...COMBAT_LEAGUES],
|
|
18
|
-
description: 'League: "raf" (Real American Freestyle wrestling), "bkfc" (Bare Knuckle Fighting Championship) or
|
|
19
|
-
'(
|
|
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".',
|
|
20
44
|
};
|
|
21
45
|
function parseLeague(value) {
|
|
22
46
|
const v = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
@@ -25,7 +49,7 @@ function parseLeague(value) {
|
|
|
25
49
|
function leagueError(d, value) {
|
|
26
50
|
return errorEnvelope({
|
|
27
51
|
code: 'VALIDATION',
|
|
28
|
-
message: `league must be
|
|
52
|
+
message: `league must be one of ${COMBAT_LEAGUES.join(', ')} (got ${JSON.stringify(value ?? null)})`,
|
|
29
53
|
game: null,
|
|
30
54
|
source: d.source,
|
|
31
55
|
requestId: d.requestId,
|
|
@@ -75,7 +99,7 @@ function sectionFailure(section, res, league) {
|
|
|
75
99
|
if (isHistoryWindow(res)) {
|
|
76
100
|
message =
|
|
77
101
|
`${section} is older than this key's history window (HISTORY_WINDOW_EXCEEDED: Free 30 days, Starter 90 days; ` +
|
|
78
|
-
`Pro reads the full archive)` + (league &&
|
|
102
|
+
`Pro reads the full archive)` + (league && archiveOf(league) ? `. ${archiveOf(league).note}, so all of its stats need Pro` : '');
|
|
79
103
|
}
|
|
80
104
|
else if (res.status === 403) {
|
|
81
105
|
message = `${section} needs the Starter plan or above for this league (${err?.message || 'plan restriction'})`;
|
|
@@ -98,7 +122,7 @@ function archiveGap(d, league, message, recover, calls = 0) {
|
|
|
98
122
|
async function resolvePerson(ctx, league, input) {
|
|
99
123
|
const raw = input.trim();
|
|
100
124
|
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(raw)) {
|
|
101
|
-
const probe = await fetchJson(ctx, `/${league}/${PEOPLE
|
|
125
|
+
const probe = await fetchJson(ctx, `/${league}/${PEOPLE(league)}/${encodeURIComponent(raw)}`);
|
|
102
126
|
if (probe.ok)
|
|
103
127
|
return { slug: raw };
|
|
104
128
|
if (probe.status !== 404)
|
|
@@ -129,7 +153,7 @@ async function resolveOrFail(d, ctx, league, input, argName) {
|
|
|
129
153
|
return {
|
|
130
154
|
fail: errorEnvelope({
|
|
131
155
|
code: 'AMBIGUOUS_ENTITY',
|
|
132
|
-
message: `"${input}" matches several ${PEOPLE
|
|
156
|
+
message: `"${input}" matches several ${PEOPLE(league)}; pass one slug from details.candidates`,
|
|
133
157
|
game: league,
|
|
134
158
|
source: d.source,
|
|
135
159
|
requestId: d.requestId,
|
|
@@ -141,7 +165,7 @@ async function resolveOrFail(d, ctx, league, input, argName) {
|
|
|
141
165
|
return {
|
|
142
166
|
fail: errorEnvelope({
|
|
143
167
|
code: 'NOT_FOUND',
|
|
144
|
-
message: `No ${league.toUpperCase()} ${PEOPLE
|
|
168
|
+
message: `No ${league.toUpperCase()} ${PEOPLE(league).slice(0, -1)} matches "${input}"`,
|
|
145
169
|
game: league,
|
|
146
170
|
source: d.source,
|
|
147
171
|
requestId: d.requestId,
|
|
@@ -154,17 +178,18 @@ function begin(source) {
|
|
|
154
178
|
}
|
|
155
179
|
export const combatEvents = {
|
|
156
180
|
name: 'combat_events',
|
|
157
|
-
description: `RAF wrestling, BKFC bare-knuckle or Bellator
|
|
181
|
+
description: `RAF wrestling, BKFC bare-knuckle or MMA archive (Bellator, PRIDE, Strikeforce, WEC, DREAM, EliteXC, Affliction) events.
|
|
158
182
|
|
|
159
183
|
When to use:
|
|
160
|
-
- "When is the next RAF / BKFC card?", "recent BKFC results", "Bellator cards from 2015", schedule pages
|
|
184
|
+
- "When is the next RAF / BKFC card?", "recent BKFC results", "PRIDE results", "Bellator cards from 2015", schedule pages
|
|
161
185
|
- Getting an event slug for combat_card
|
|
162
186
|
|
|
163
187
|
Each event carries slug, name, date, local start time + timezone, venue/location, status and bout counts.
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
166
191
|
|
|
167
|
-
Prefer over: call_api /
|
|
192
|
+
Prefer over: call_api /{league}/events.
|
|
168
193
|
Do not use when: you already have the event slug and want its bouts (combat_card).
|
|
169
194
|
|
|
170
195
|
Parallel-safe: yes. Upstream cost: 1.
|
|
@@ -180,7 +205,7 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
180
205
|
enum: ['upcoming', 'recent'],
|
|
181
206
|
default: 'upcoming',
|
|
182
207
|
description: 'upcoming = scheduled cards, soonest first; recent = completed cards, newest first. Default "upcoming" ' +
|
|
183
|
-
'("recent" for
|
|
208
|
+
'("recent" for the MMA archives). Example: "recent".',
|
|
184
209
|
},
|
|
185
210
|
limit: limitSchema({ default: 10, max: 50, description: 'Max events (default 10, max 50).' }),
|
|
186
211
|
},
|
|
@@ -190,14 +215,14 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
190
215
|
const league = parseLeague(args.league);
|
|
191
216
|
if (!league)
|
|
192
217
|
return leagueError(d, args.league);
|
|
193
|
-
const archive =
|
|
218
|
+
const archive = archiveOf(league);
|
|
194
219
|
const when = args.when === 'recent' || args.when === 'upcoming' ? args.when : archive ? 'recent' : 'upcoming';
|
|
195
220
|
const limit = clampInt(args.limit, 10, 1, 50);
|
|
196
221
|
if (archive && when === 'upcoming') {
|
|
197
222
|
return successEnvelope({
|
|
198
223
|
data: {
|
|
199
224
|
league, leagueName: LEAGUE_NAMES[league], when, count: 0, events: [],
|
|
200
|
-
note: `${archive}: there are no upcoming cards. Use when "recent" for its cards, newest first.`,
|
|
225
|
+
note: `${archive.note}: there are no upcoming cards. Use when "recent" for its cards, newest first.`,
|
|
201
226
|
},
|
|
202
227
|
game: league,
|
|
203
228
|
source: d.source,
|
|
@@ -223,18 +248,18 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
223
248
|
};
|
|
224
249
|
export const combatCard = {
|
|
225
250
|
name: 'combat_card',
|
|
226
|
-
description: `COMPOSITE fight card for one RAF, BKFC or
|
|
251
|
+
description: `COMPOSITE fight card for one RAF, BKFC or MMA archive event: event identity + bouts in running order with results.
|
|
227
252
|
|
|
228
253
|
When to use:
|
|
229
|
-
- "RAF 14 card", "BKFC 95 results", "Bellator 142 card", "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?"
|
|
230
255
|
- Event pages and results pages
|
|
231
256
|
|
|
232
|
-
event accepts a slug (e.g. "raf13", "bkfc-95-newark", "bellator-142"
|
|
233
|
-
event first; each carries weight class, title stakes, both sides (athletes for RAF, red/blue fighters for BKFC with
|
|
234
|
-
punch totals, red/blue fighters for
|
|
235
|
-
and result (RAF: score, takedowns, method; BKFC/
|
|
236
|
-
|
|
237
|
-
non-retryable NOT_IMPLEMENTED (there are no upcoming cards).
|
|
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).
|
|
238
263
|
|
|
239
264
|
Prefer over: combat_events + call_api per bout.
|
|
240
265
|
Do not use when: you need one bout's period/round breakdown (combat_bout).
|
|
@@ -247,7 +272,7 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
247
272
|
required: ['league'],
|
|
248
273
|
properties: {
|
|
249
274
|
league: leagueSchema,
|
|
250
|
-
event: stringSchema('Event slug, or "next" / "last" (default "next"; "last" for
|
|
275
|
+
event: stringSchema('Event slug, or "next" / "last" (default "next"; "last" for the MMA archives). Example: "raf13".', 'next'),
|
|
251
276
|
limit: limitSchema({ default: 30, max: 40, description: 'Max bouts on the card (default 30, max 40).' }),
|
|
252
277
|
},
|
|
253
278
|
},
|
|
@@ -257,11 +282,11 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
257
282
|
if (!league)
|
|
258
283
|
return leagueError(d, args.league);
|
|
259
284
|
const limit = clampInt(args.limit, 30, 1, 40);
|
|
260
|
-
const archive =
|
|
285
|
+
const archive = archiveOf(league);
|
|
261
286
|
let slug = typeof args.event === 'string' && args.event.trim() ? args.event.trim().toLowerCase() : archive ? 'last' : 'next';
|
|
262
287
|
let calls = 0;
|
|
263
288
|
if (archive && slug === 'next') {
|
|
264
|
-
return archiveGap(d, league, `${archive}: there is no next card.`, [
|
|
289
|
+
return archiveGap(d, league, `${archive.note}: there is no next card.`, [
|
|
265
290
|
`Use event "last" for the most recent ${league} card, or a slug from combat_events { league: "${league}", when: "recent" }`,
|
|
266
291
|
]);
|
|
267
292
|
}
|
|
@@ -290,7 +315,7 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
290
315
|
slug = String(first.slug);
|
|
291
316
|
}
|
|
292
317
|
const [ev, bouts] = await Promise.all([
|
|
293
|
-
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}`,
|
|
318
|
+
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}`, archive ? { query: { includeBouts: 'false' } } : undefined),
|
|
294
319
|
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}/bouts`),
|
|
295
320
|
]);
|
|
296
321
|
calls += 2;
|
|
@@ -324,18 +349,20 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
324
349
|
};
|
|
325
350
|
export const combatBout = {
|
|
326
351
|
name: 'combat_bout',
|
|
327
|
-
description: `One RAF, BKFC or
|
|
352
|
+
description: `One RAF, BKFC or MMA archive bout in full, with its scoring breakdown.
|
|
328
353
|
|
|
329
354
|
RAF: winner, method (decision / tech fall / pin / injury), final score, takedowns, and per-PERIOD scores.
|
|
330
355
|
BKFC: winner, method, round, time, and per-ROUND punch stats for red and blue (landed/thrown, head/body,
|
|
331
356
|
distance/clinch, knockdowns). BKFC round stats need the Starter plan; on Free they come back as a partial note.
|
|
332
|
-
Bellator
|
|
333
|
-
whole-fight totals, and per-ROUND official
|
|
334
|
-
takedowns, head/body/leg, distance/clinch/ground,
|
|
335
|
-
|
|
336
|
-
|
|
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.
|
|
337
363
|
|
|
338
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.
|
|
339
366
|
|
|
340
367
|
Parallel-safe: yes. Upstream cost: 1–2.
|
|
341
368
|
Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cisneros--ryan-petersen" }`,
|
|
@@ -345,8 +372,8 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
345
372
|
required: ['league', 'boutId'],
|
|
346
373
|
properties: {
|
|
347
374
|
league: leagueSchema,
|
|
348
|
-
boutId: stringSchema('Bout id from combat_card / combat_athlete (
|
|
349
|
-
includeBreakdown: boolSchema('Include per-period (RAF) / per-round (BKFC,
|
|
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),
|
|
350
377
|
},
|
|
351
378
|
},
|
|
352
379
|
handler: async (args, ctx) => {
|
|
@@ -379,7 +406,7 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
379
406
|
const b = asRecord(data.bout);
|
|
380
407
|
if (league === 'bkfc' && b && 'rounds' in b && data.rounds)
|
|
381
408
|
delete b.rounds;
|
|
382
|
-
if (league
|
|
409
|
+
if (archiveOf(league) && b && 'roundStats' in b && (data.rounds || !breakdown))
|
|
383
410
|
delete b.roundStats;
|
|
384
411
|
return successEnvelope({
|
|
385
412
|
data,
|
|
@@ -395,19 +422,20 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
395
422
|
};
|
|
396
423
|
export const combatAthlete = {
|
|
397
424
|
name: 'combat_athlete',
|
|
398
|
-
description: `COMPOSITE RAF athlete / BKFC or
|
|
425
|
+
description: `COMPOSITE RAF athlete / BKFC or MMA archive fighter page: profile + recent bouts + career stats.
|
|
399
426
|
|
|
400
427
|
When to use:
|
|
401
|
-
- "Kyle Snyder's RAF record", "Arlovski BKFC stats", "Tito Ortiz in Bellator", 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
|
|
402
429
|
|
|
403
430
|
athlete accepts a slug ("kyle-snyder") or a name ("Andrei Arlovski"); names are resolved through the league's
|
|
404
431
|
search, and several matches return AMBIGUOUS_ENTITY with candidates. Profile carries record, division, nickname,
|
|
405
432
|
country, official headshot (+ its credit — show it with the image). Career stats need the Starter plan; on Free
|
|
406
433
|
they come back as a partial note while profile and history still return.
|
|
407
|
-
|
|
408
|
-
whole career); stats are career totals in UFC's format.
|
|
409
|
-
|
|
410
|
-
|
|
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.
|
|
411
439
|
|
|
412
440
|
Prefer over: call_api profile + history + stats separately.
|
|
413
441
|
|
|
@@ -438,20 +466,23 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
|
|
|
438
466
|
const r = await resolveOrFail(d, ctx, league, input, 'athlete');
|
|
439
467
|
if ('fail' in r)
|
|
440
468
|
return r.fail;
|
|
441
|
-
const base = `/${league}/${PEOPLE
|
|
469
|
+
const base = `/${league}/${PEOPLE(league)}/${encodeURIComponent(r.slug)}`;
|
|
442
470
|
const historyLimit = clampInt(args.historyLimit, 10, 1, 50);
|
|
443
471
|
const [profile, history, stats] = await Promise.all([
|
|
444
472
|
fetchJson(ctx, base),
|
|
445
|
-
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 } }),
|
|
446
474
|
args.includeStats === false ? Promise.resolve(null) : fetchJson(ctx, `${base}/stats`),
|
|
447
475
|
]);
|
|
448
476
|
if (!profile.ok)
|
|
449
|
-
return httpError(d, league, profile, `${league} ${PEOPLE
|
|
477
|
+
return httpError(d, league, profile, `${league} ${PEOPLE(league)} ${r.slug}`);
|
|
450
478
|
const partial = [];
|
|
451
479
|
const data = { league, slug: r.slug, profile: payload(profile) };
|
|
452
|
-
if (league
|
|
480
|
+
if (isMma(league)) {
|
|
453
481
|
const ufc = asRecord(asRecord(data.profile)?.ufc);
|
|
454
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;
|
|
455
486
|
}
|
|
456
487
|
if (history) {
|
|
457
488
|
if (history.ok)
|
|
@@ -479,10 +510,11 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
|
|
|
479
510
|
};
|
|
480
511
|
export const combatH2h = {
|
|
481
512
|
name: 'combat_h2h',
|
|
482
|
-
description: `Head-to-head between two RAF athletes, two BKFC fighters or two
|
|
513
|
+
description: `Head-to-head between two RAF athletes, two BKFC fighters or two MMA archive fighters: record and every meeting.
|
|
483
514
|
|
|
484
|
-
Accepts slugs or names for both sides (names resolve through the league's search).
|
|
485
|
-
the ones on
|
|
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).
|
|
486
518
|
|
|
487
519
|
Parallel-safe: yes. Upstream cost: 1–3.
|
|
488
520
|
Example: { "league": "bkfc", "athleteA": "ryan-petersen", "athleteB": "gregoris-cisneros" }`,
|
|
@@ -537,7 +569,8 @@ export const combatRankings = {
|
|
|
537
569
|
BKFC: omit division for every division's ranked list (champion, interim champion, ranked fighters), or pass one
|
|
538
570
|
(e.g. "heavyweight", "womens-flyweight"). RAF has no ranked lists; it returns every title and its holder, or one
|
|
539
571
|
division's detail when division is given (e.g. "men-heavyweight").
|
|
540
|
-
|
|
572
|
+
The MMA archives (bellator, pride, strikeforce, wec, dream, elitexc, affliction, mma) have no rankings: they return a
|
|
573
|
+
non-retryable NOT_IMPLEMENTED.
|
|
541
574
|
Never answer with UFC rankings instead.
|
|
542
575
|
|
|
543
576
|
Parallel-safe: yes. Upstream cost: 1.
|
|
@@ -556,11 +589,12 @@ Example: { "league": "bkfc", "division": "heavyweight" }`,
|
|
|
556
589
|
const league = parseLeague(args.league);
|
|
557
590
|
if (!league)
|
|
558
591
|
return leagueError(d, args.league);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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}`,
|
|
564
598
|
]);
|
|
565
599
|
}
|
|
566
600
|
const division = typeof args.division === 'string' ? args.division.trim().toLowerCase() : '';
|
package/dist/tools/cs2.js
CHANGED
|
@@ -362,6 +362,79 @@ export const cs2RosterTransfers = {
|
|
|
362
362
|
});
|
|
363
363
|
},
|
|
364
364
|
};
|
|
365
|
+
const POSITION_VIEWS = ['grenades', 'positions', 'heatmap'];
|
|
366
|
+
const POSITION_FILTERS = ['type', 'side', 'round', 'player', 'team', 'kind', 'grid', 'map', 'days', 'limit'];
|
|
367
|
+
export const cs2Positions = {
|
|
368
|
+
name: 'cs2_positions',
|
|
369
|
+
description: 'CS2 positional data from parsed demos. With mapId: view=grenades (every grenade with landing spot, thrower, round, duration), '
|
|
370
|
+
+ 'view=positions (grenades, bomb plants/defuses and kills with attacker/victim spots in one timeline) or view=heatmap (counts per radar cell; '
|
|
371
|
+
+ 'kind=utility|smoke|flash|he|molotov|decoy|bomb_plant|bomb_defuse|kill|death). With playerId instead: that player\'s grenades across recent maps '
|
|
372
|
+
+ '(pass map for a heatmap). Every point has game coordinates and a 0..1 radar point. Read the response notes and null fields for what is not recorded.',
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
properties: {
|
|
376
|
+
mapId: stringSchema('Map/game id from match maps, e.g. "cs2-match-2399437-map-1".', 'cs2-match-2399437-map-1'),
|
|
377
|
+
playerId: stringSchema('Player id or slug (e.g. "ropz"). Use instead of mapId for one player across maps.', 'ropz'),
|
|
378
|
+
view: { type: 'string', enum: [...POSITION_VIEWS], description: 'For mapId: grenades (default), positions or heatmap.' },
|
|
379
|
+
type: stringSchema('Grenade type: smoke, flash, he, molotov or decoy.'),
|
|
380
|
+
side: stringSchema('T or CT.'),
|
|
381
|
+
round: { type: 'integer', minimum: 1, description: 'Only this round.' },
|
|
382
|
+
player: stringSchema('With mapId: only this player (id or nickname).'),
|
|
383
|
+
team: stringSchema('With mapId + view=grenades: only this team (id or name).'),
|
|
384
|
+
kind: stringSchema('view=heatmap: utility, smoke, flash, he, molotov, decoy, bomb_plant, bomb_defuse, kill or death. view=positions: grenade,bomb,kill.'),
|
|
385
|
+
grid: { type: 'integer', minimum: 4, maximum: 128, description: 'Heatmap cells per side (default 32).' },
|
|
386
|
+
map: stringSchema('With playerId: map name (Mirage, Inferno...) to get a heatmap.'),
|
|
387
|
+
days: { type: 'integer', minimum: 1, maximum: 3650, description: 'With playerId: look-back window in days (default 180).' },
|
|
388
|
+
limit: limitSchema({ default: 200, max: 5000 }),
|
|
389
|
+
},
|
|
390
|
+
additionalProperties: false,
|
|
391
|
+
},
|
|
392
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
393
|
+
handler: async (args, ctx) => {
|
|
394
|
+
const reqId = newRequestId();
|
|
395
|
+
const mapId = typeof args.mapId === 'string' ? args.mapId.trim() : '';
|
|
396
|
+
const playerId = typeof args.playerId === 'string' ? args.playerId.trim() : '';
|
|
397
|
+
if (!mapId && !playerId) {
|
|
398
|
+
return errorEnvelope({
|
|
399
|
+
code: 'VALIDATION',
|
|
400
|
+
message: 'mapId or playerId is required',
|
|
401
|
+
game: 'cs2',
|
|
402
|
+
source: 'cs2_positions',
|
|
403
|
+
requestId: reqId,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const view = POSITION_VIEWS.includes(args.view) ? String(args.view) : 'grenades';
|
|
407
|
+
const path = playerId
|
|
408
|
+
? `/cs2/players/${encodeURIComponent(playerId)}/utility`
|
|
409
|
+
: `/cs2/maps/${encodeURIComponent(mapId)}/${view === 'grenades' ? 'utility' : view}`;
|
|
410
|
+
const query = {};
|
|
411
|
+
for (const key of POSITION_FILTERS) {
|
|
412
|
+
const value = args[key];
|
|
413
|
+
if (typeof value === 'string' && value.trim())
|
|
414
|
+
query[key] = value.trim();
|
|
415
|
+
else if (typeof value === 'number' && Number.isFinite(value))
|
|
416
|
+
query[key] = value;
|
|
417
|
+
}
|
|
418
|
+
const res = await fetchJson(ctx, path, { query });
|
|
419
|
+
if (!res.ok) {
|
|
420
|
+
return errorEnvelope({
|
|
421
|
+
code: mapHttpToCode(res.status),
|
|
422
|
+
message: `CS2 positions fetch failed: ${res.status} on ${path}`,
|
|
423
|
+
game: 'cs2',
|
|
424
|
+
source: 'cs2_positions',
|
|
425
|
+
requestId: reqId,
|
|
426
|
+
httpStatus: res.status,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return successEnvelope({
|
|
430
|
+
data: unwrapPayload(res.data),
|
|
431
|
+
game: 'cs2',
|
|
432
|
+
source: 'cs2_positions',
|
|
433
|
+
requestId: reqId,
|
|
434
|
+
rateLimit: res.headers,
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
};
|
|
365
438
|
export const cs2Tools = [
|
|
366
439
|
cs2LiveScoreboard,
|
|
367
440
|
cs2RoundEconomy,
|
|
@@ -371,4 +444,5 @@ export const cs2Tools = [
|
|
|
371
444
|
cs2UtilityLeaderboard,
|
|
372
445
|
cs2VetoSequence,
|
|
373
446
|
cs2RosterTransfers,
|
|
447
|
+
cs2Positions,
|
|
374
448
|
];
|
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 / Bellator
|
|
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/
|
|
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 or
|
|
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, two BKFC fighters 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/
|
|
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
|
},
|
|
@@ -441,6 +442,16 @@ const TOOL_CATALOG = [
|
|
|
441
442
|
preferOver: ['unindexed round event aggregation'],
|
|
442
443
|
doNotUse: 'For standard weapon ratings',
|
|
443
444
|
},
|
|
445
|
+
{
|
|
446
|
+
name: 'cs2_positions',
|
|
447
|
+
outcome: 'CS2 grenade landing spots, bomb plants, kill/death spots and radar heatmaps for a map or a player',
|
|
448
|
+
parallelSafe: true,
|
|
449
|
+
games: ['cs2'],
|
|
450
|
+
jobs: ['match_page', 'player_form'],
|
|
451
|
+
exampleArgs: { mapId: 'cs2-match-2399437-map-1', view: 'heatmap', kind: 'smoke' },
|
|
452
|
+
preferOver: ['call_api /cs2/maps/{mapId}/utility|positions|heatmap', 'parsing /cs2/games/{id}/rounds events for coordinates'],
|
|
453
|
+
doNotUse: 'For live matches (positions come from the demo, after the map ends)',
|
|
454
|
+
},
|
|
444
455
|
{
|
|
445
456
|
name: 'cs2_veto_sequence',
|
|
446
457
|
outcome: 'Map pick/ban sequence and veto history for a CS2 match',
|
|
@@ -552,8 +563,8 @@ const GAMES_META = [
|
|
|
552
563
|
label: 'UFC / MMA',
|
|
553
564
|
depth: 'medium',
|
|
554
565
|
notes: 'Fighter slugs; boutId; live + rankings. Includes Dana White\'s Contender Series and Road to UFC (2022-2026, all 19 cards ' +
|
|
555
|
-
'with official round stats; event slugs like road-to-ufc-2-7, titles "Road to UFC 2.7"). Bellator
|
|
556
|
-
'combat_* tools with league
|
|
566
|
+
'with official round stats; event slugs like road-to-ufc-2-7, titles "Road to UFC 2.7"). Bellator, PRIDE, Strikeforce, WEC, ' +
|
|
567
|
+
'DREAM, EliteXC and Affliction are NOT UFC: combat_* tools with that league (or mma), never mixed into UFC answers.',
|
|
557
568
|
},
|
|
558
569
|
];
|
|
559
570
|
export const listCapabilities = {
|
|
@@ -797,6 +808,15 @@ const ALLOWLIST_PREFIXES = [
|
|
|
797
808
|
'/raf',
|
|
798
809
|
'/bkfc',
|
|
799
810
|
'/bellator',
|
|
811
|
+
'/pride',
|
|
812
|
+
'/strikeforce',
|
|
813
|
+
'/wec',
|
|
814
|
+
'/dream',
|
|
815
|
+
'/elitexc',
|
|
816
|
+
'/affliction',
|
|
817
|
+
'/pfl',
|
|
818
|
+
'/oktagon',
|
|
819
|
+
'/mma',
|
|
800
820
|
'/openapi.json',
|
|
801
821
|
];
|
|
802
822
|
function pathAllowed(path) {
|
|
@@ -820,7 +840,7 @@ Prefer curated tools for all standard jobs (live, schedule, profiles, standings,
|
|
|
820
840
|
|
|
821
841
|
Do not use when: a curated tool covers the outcome. Avoid parallel storms; same plan rate limits apply.
|
|
822
842
|
|
|
823
|
-
Path must start with / and match allowlisted prefixes: /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc, /bellator.
|
|
843
|
+
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.
|
|
824
844
|
Rejects absolute URLs and path traversal → PATH_NOT_ALLOWED.
|
|
825
845
|
|
|
826
846
|
Parallel-safe: yes but discouraged in bulk. Upstream cost: 1.
|
|
@@ -881,7 +901,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
881
901
|
source: 'call_api',
|
|
882
902
|
requestId,
|
|
883
903
|
tookMs: Date.now() - started,
|
|
884
|
-
hint: 'Use prefixes /health /lol /cs2 /dota2 /cod /ufc /fortnite /tennis /raf /bkfc /bellator',
|
|
904
|
+
hint: 'Use prefixes /health /lol /cs2 /dota2 /cod /ufc /fortnite /tennis /raf /bkfc /bellator /pride /strikeforce /wec /dream /elitexc /affliction /pfl /oktagon /mma',
|
|
885
905
|
});
|
|
886
906
|
}
|
|
887
907
|
let query = {};
|
|
@@ -982,10 +1002,10 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
982
1002
|
const started = Date.now();
|
|
983
1003
|
const requestId = newRequestId();
|
|
984
1004
|
const gameRaw = typeof args.game === 'string' ? args.game.toLowerCase().trim() : '';
|
|
985
|
-
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator)$/)) {
|
|
1005
|
+
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator|pride|strikeforce|wec|dream|elitexc|affliction|pfl|oktagon|mma)$/)) {
|
|
986
1006
|
return errorEnvelope({
|
|
987
1007
|
code: 'UNSUPPORTED_GAME',
|
|
988
|
-
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator`,
|
|
1008
|
+
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator|pride|strikeforce|wec|dream|elitexc|affliction|pfl|oktagon|mma`,
|
|
989
1009
|
game: null,
|
|
990
1010
|
source: 'list_routes',
|
|
991
1011
|
requestId,
|
|
@@ -1008,7 +1028,7 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
1008
1028
|
httpStatus: res.status,
|
|
1009
1029
|
recover: [
|
|
1010
1030
|
'Call list_capabilities for curated tools that need no route knowledge',
|
|
1011
|
-
'call_api accepts /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc, /bellator',
|
|
1031
|
+
'call_api accepts /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc, /bellator, /pride, /strikeforce, /wec, /dream, /elitexc, /affliction, /pfl, /oktagon, /mma',
|
|
1012
1032
|
],
|
|
1013
1033
|
});
|
|
1014
1034
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cito-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Standalone MCP server for the Cito esports and sports API
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "Standalone MCP server for the Cito esports and sports API — curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
|
|
5
5
|
"type": "module",
|
|
6
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)
|
|
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) — 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
8
|
"keywords": [
|
|
9
9
|
"mcp",
|
|
10
10
|
"model-context-protocol",
|