cito-mcp 0.4.8 → 0.4.9
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 +2 -0
- package/dist/tools/combat.js +133 -40
- package/dist/tools/meta.js +27 -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 **Bellator MMA archive** (2014-2017) 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 Bellator are not on the `game` enum. `combat_events`, `combat_card`, `combat_bout`, `combat_athlete`, `combat_h2h` and `combat_rankings` take `league: "raf" | "bkfc" | "bellator"`, and `call_api` reaches `/raf`, `/bkfc` and `/bellator`. 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
|
+
| `bellator` | per ROUND, official stats in UFC's shape (`"13 of 18"`, `controlTime "1:42"`, round 0 = whole fight; knockdowns, submission attempts, reversals) | Closed archive: Bellator 131-172 (Nov 2014 - Feb 2017), ~20 cards / ~230 bouts. Bout ids like `bellator-5600`, event slugs like `bellator-142` (or `142`). 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` · `/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
|
+
- **Bellator MMA archive** (`/api/v1/bellator`): `league: "bellator"` on every `combat_*` tool, and `/bellator` on the `call_api` / `list_routes` allowlist. Bellator 131-172 (Nov 2014 - Feb 2017) with official per-round stats in `/ufc`'s shape; fighters with a UFC record share the UFC slug (`combat_athlete` returns `ufcFighterSlug`). 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`): Bellator is 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 /bellator',
|
|
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,8 @@ 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 MMA is not on the enum either: combat_* tools with league bellator, or call_api /bellator/.... A closed 2014-2017 archive (Bellator 131-172) 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). A fighter with a UFC record keeps the SAME slug as /ufc/fighters/{slug} (ufcFighterSlug). Never mix Bellator fights into UFC answers. Its stats predate the Free/Starter window: HISTORY_WINDOW_EXCEEDED means Pro, not a bad key.
|
|
22
|
+
- 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
23
|
|
|
22
24
|
## Preferred tool order
|
|
23
25
|
|
package/dist/tools/combat.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
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 COMBAT_LEAGUES = ['raf', 'bkfc'];
|
|
4
|
+
export const COMBAT_LEAGUES = ['raf', 'bkfc', 'bellator'];
|
|
5
5
|
const LEAGUE_NAMES = {
|
|
6
6
|
raf: 'Real American Freestyle (pro freestyle wrestling)',
|
|
7
7
|
bkfc: 'Bare Knuckle Fighting Championship',
|
|
8
|
+
bellator: 'Bellator MMA',
|
|
8
9
|
};
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
const ARCHIVES = {
|
|
11
|
+
bellator: 'Bellator is a closed archive (Bellator 131-172, November 2014 to February 2017)',
|
|
12
|
+
};
|
|
13
|
+
const PEOPLE = { raf: 'athletes', bkfc: 'fighters', bellator: 'fighters' };
|
|
14
|
+
const HISTORY = { raf: 'bouts', bkfc: 'fights', bellator: 'fights' };
|
|
11
15
|
const leagueSchema = {
|
|
12
16
|
type: 'string',
|
|
13
17
|
enum: [...COMBAT_LEAGUES],
|
|
14
|
-
description: 'League: "raf" (Real American Freestyle wrestling)
|
|
18
|
+
description: 'League: "raf" (Real American Freestyle wrestling), "bkfc" (Bare Knuckle Fighting Championship) or "bellator" ' +
|
|
19
|
+
'(Bellator MMA archive, 2014-2017). Example: "bkfc".',
|
|
15
20
|
};
|
|
16
21
|
function parseLeague(value) {
|
|
17
22
|
const v = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
@@ -20,17 +25,25 @@ function parseLeague(value) {
|
|
|
20
25
|
function leagueError(d, value) {
|
|
21
26
|
return errorEnvelope({
|
|
22
27
|
code: 'VALIDATION',
|
|
23
|
-
message: `league must be raf or
|
|
28
|
+
message: `league must be raf, bkfc or bellator (got ${JSON.stringify(value ?? null)})`,
|
|
24
29
|
game: null,
|
|
25
30
|
source: d.source,
|
|
26
31
|
requestId: d.requestId,
|
|
27
32
|
tookMs: Date.now() - d.started,
|
|
28
33
|
});
|
|
29
34
|
}
|
|
35
|
+
function isHistoryWindow(res) {
|
|
36
|
+
return res.status === 403 && asRecord(asRecord(res.data)?.error)?.code === 'HISTORY_WINDOW_EXCEEDED';
|
|
37
|
+
}
|
|
38
|
+
const HISTORY_WINDOW_RECOVER = [
|
|
39
|
+
'Free reads 30 days and Starter 90 days of in-depth stats; Pro reads the full archive',
|
|
40
|
+
'Summary sections (combat_card bouts, combat_athlete history) still return the records without stat blocks',
|
|
41
|
+
];
|
|
30
42
|
function httpError(d, league, res, what) {
|
|
31
43
|
const body = asRecord(res.data);
|
|
32
44
|
const err = asRecord(body?.error);
|
|
33
45
|
return errorEnvelope({
|
|
46
|
+
...(isHistoryWindow(res) ? { recover: HISTORY_WINDOW_RECOVER } : {}),
|
|
34
47
|
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
35
48
|
message: (typeof err?.message === 'string' && err.message) || `${what} failed (HTTP ${res.status})`,
|
|
36
49
|
game: league,
|
|
@@ -45,16 +58,41 @@ function payload(res) {
|
|
|
45
58
|
const body = asRecord(res.data);
|
|
46
59
|
return body && 'data' in body ? body.data : res.data;
|
|
47
60
|
}
|
|
48
|
-
function
|
|
61
|
+
function upstreamNotes(res) {
|
|
62
|
+
const meta = asRecord(asRecord(res.data)?.meta);
|
|
63
|
+
const out = {};
|
|
64
|
+
if (typeof meta?.note === 'string')
|
|
65
|
+
out.note = meta.note;
|
|
66
|
+
if (meta?.historyWindow)
|
|
67
|
+
out.historyWindow = meta.historyWindow;
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function sectionFailure(section, res, league) {
|
|
49
71
|
const body = asRecord(res.data);
|
|
50
72
|
const err = asRecord(body?.error);
|
|
51
73
|
const code = mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) });
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
let message = err?.message || `${section} unavailable (HTTP ${res.status})`;
|
|
75
|
+
if (isHistoryWindow(res)) {
|
|
76
|
+
message =
|
|
77
|
+
`${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 && ARCHIVES[league] ? `. ${ARCHIVES[league]}, so all of its stats need Pro` : '');
|
|
79
|
+
}
|
|
80
|
+
else if (res.status === 403) {
|
|
81
|
+
message = `${section} needs the Starter plan or above for this league (${err?.message || 'plan restriction'})`;
|
|
82
|
+
}
|
|
83
|
+
return partialFromRejection(section, { code, httpStatus: res.status, message });
|
|
84
|
+
}
|
|
85
|
+
function archiveGap(d, league, message, recover, calls = 0) {
|
|
86
|
+
return errorEnvelope({
|
|
87
|
+
code: 'NOT_IMPLEMENTED',
|
|
88
|
+
message,
|
|
89
|
+
game: league,
|
|
90
|
+
source: d.source,
|
|
91
|
+
requestId: d.requestId,
|
|
92
|
+
tookMs: Date.now() - d.started,
|
|
93
|
+
upstreamCalls: calls,
|
|
94
|
+
retryable: false,
|
|
95
|
+
recover,
|
|
58
96
|
});
|
|
59
97
|
}
|
|
60
98
|
async function resolvePerson(ctx, league, input) {
|
|
@@ -116,15 +154,17 @@ function begin(source) {
|
|
|
116
154
|
}
|
|
117
155
|
export const combatEvents = {
|
|
118
156
|
name: 'combat_events',
|
|
119
|
-
description: `RAF wrestling
|
|
157
|
+
description: `RAF wrestling, BKFC bare-knuckle or Bellator MMA events: upcoming cards or recent results.
|
|
120
158
|
|
|
121
159
|
When to use:
|
|
122
|
-
- "When is the next RAF / BKFC card?", "recent BKFC results", schedule pages
|
|
160
|
+
- "When is the next RAF / BKFC card?", "recent BKFC results", "Bellator cards from 2015", schedule pages
|
|
123
161
|
- Getting an event slug for combat_card
|
|
124
162
|
|
|
125
163
|
Each event carries slug, name, date, local start time + timezone, venue/location, status and bout counts.
|
|
164
|
+
Bellator is a closed archive (Bellator 131-172, Nov 2014 - Feb 2017): "when" defaults to "recent" (newest card
|
|
165
|
+
first), and "upcoming" returns an empty list with data.note instead of failing.
|
|
126
166
|
|
|
127
|
-
Prefer over: call_api /raf/events or /
|
|
167
|
+
Prefer over: call_api /raf/events, /bkfc/events or /bellator/events.
|
|
128
168
|
Do not use when: you already have the event slug and want its bouts (combat_card).
|
|
129
169
|
|
|
130
170
|
Parallel-safe: yes. Upstream cost: 1.
|
|
@@ -139,7 +179,8 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
139
179
|
type: 'string',
|
|
140
180
|
enum: ['upcoming', 'recent'],
|
|
141
181
|
default: 'upcoming',
|
|
142
|
-
description: 'upcoming = scheduled cards, soonest first; recent = completed cards, newest first.
|
|
182
|
+
description: 'upcoming = scheduled cards, soonest first; recent = completed cards, newest first. Default "upcoming" ' +
|
|
183
|
+
'("recent" for bellator, an archive). Example: "recent".',
|
|
143
184
|
},
|
|
144
185
|
limit: limitSchema({ default: 10, max: 50, description: 'Max events (default 10, max 50).' }),
|
|
145
186
|
},
|
|
@@ -149,14 +190,28 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
149
190
|
const league = parseLeague(args.league);
|
|
150
191
|
if (!league)
|
|
151
192
|
return leagueError(d, args.league);
|
|
152
|
-
const
|
|
193
|
+
const archive = ARCHIVES[league];
|
|
194
|
+
const when = args.when === 'recent' || args.when === 'upcoming' ? args.when : archive ? 'recent' : 'upcoming';
|
|
153
195
|
const limit = clampInt(args.limit, 10, 1, 50);
|
|
196
|
+
if (archive && when === 'upcoming') {
|
|
197
|
+
return successEnvelope({
|
|
198
|
+
data: {
|
|
199
|
+
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.`,
|
|
201
|
+
},
|
|
202
|
+
game: league,
|
|
203
|
+
source: d.source,
|
|
204
|
+
requestId: d.requestId,
|
|
205
|
+
tookMs: Date.now() - d.started,
|
|
206
|
+
upstreamCalls: 0,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
154
209
|
const res = await fetchJson(ctx, `/${league}/events/${when}`, { query: { limit } });
|
|
155
210
|
if (!res.ok)
|
|
156
211
|
return httpError(d, league, res, `${league} ${when} events`);
|
|
157
212
|
const events = extractRows(res.data).slice(0, limit);
|
|
158
213
|
return successEnvelope({
|
|
159
|
-
data: { league, leagueName: LEAGUE_NAMES[league], when, count: events.length, events },
|
|
214
|
+
data: { league, leagueName: LEAGUE_NAMES[league], when, count: events.length, events, ...upstreamNotes(res) },
|
|
160
215
|
game: league,
|
|
161
216
|
source: d.source,
|
|
162
217
|
requestId: d.requestId,
|
|
@@ -168,15 +223,18 @@ Example: { "league": "bkfc", "when": "upcoming", "limit": 5 }`,
|
|
|
168
223
|
};
|
|
169
224
|
export const combatCard = {
|
|
170
225
|
name: 'combat_card',
|
|
171
|
-
description: `COMPOSITE fight card for one RAF or
|
|
226
|
+
description: `COMPOSITE fight card for one RAF, BKFC or Bellator event: event identity + bouts in running order with results.
|
|
172
227
|
|
|
173
228
|
When to use:
|
|
174
|
-
- "RAF 14 card", "BKFC 95 results", "who won the main event at the last BKFC?"
|
|
229
|
+
- "RAF 14 card", "BKFC 95 results", "Bellator 142 card", "who won the main event at the last BKFC?"
|
|
175
230
|
- Event pages and results pages
|
|
176
231
|
|
|
177
|
-
event accepts a slug (e.g. "raf13", "bkfc-95-newark") or "next" / "last". Bouts come main
|
|
178
|
-
weight class, title stakes, both sides (athletes for RAF, red/blue fighters for BKFC with
|
|
179
|
-
|
|
232
|
+
event accepts a slug (e.g. "raf13", "bkfc-95-newark", "bellator-142" or "142") or "next" / "last". Bouts come main
|
|
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 Bellator with UFC-format totals and ufcFighterSlug), and once finished the winner
|
|
235
|
+
and result (RAF: score, takedowns, method; BKFC/Bellator: method, round, time).
|
|
236
|
+
Bellator is an archive: event defaults to "last" (its most recent card, Bellator 172), and "next" is a
|
|
237
|
+
non-retryable NOT_IMPLEMENTED (there are no upcoming cards).
|
|
180
238
|
|
|
181
239
|
Prefer over: combat_events + call_api per bout.
|
|
182
240
|
Do not use when: you need one bout's period/round breakdown (combat_bout).
|
|
@@ -189,7 +247,7 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
189
247
|
required: ['league'],
|
|
190
248
|
properties: {
|
|
191
249
|
league: leagueSchema,
|
|
192
|
-
event: stringSchema('Event slug, or "next" / "last" (default "next"). Example: "raf13".', 'next'),
|
|
250
|
+
event: stringSchema('Event slug, or "next" / "last" (default "next"; "last" for bellator). Example: "raf13".', 'next'),
|
|
193
251
|
limit: limitSchema({ default: 30, max: 40, description: 'Max bouts on the card (default 30, max 40).' }),
|
|
194
252
|
},
|
|
195
253
|
},
|
|
@@ -199,8 +257,14 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
199
257
|
if (!league)
|
|
200
258
|
return leagueError(d, args.league);
|
|
201
259
|
const limit = clampInt(args.limit, 30, 1, 40);
|
|
202
|
-
|
|
260
|
+
const archive = ARCHIVES[league];
|
|
261
|
+
let slug = typeof args.event === 'string' && args.event.trim() ? args.event.trim().toLowerCase() : archive ? 'last' : 'next';
|
|
203
262
|
let calls = 0;
|
|
263
|
+
if (archive && slug === 'next') {
|
|
264
|
+
return archiveGap(d, league, `${archive}: there is no next card.`, [
|
|
265
|
+
`Use event "last" for the most recent ${league} card, or a slug from combat_events { league: "${league}", when: "recent" }`,
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
204
268
|
if (slug === 'next' || slug === 'last') {
|
|
205
269
|
const which = slug === 'next' ? 'upcoming' : 'recent';
|
|
206
270
|
const list = await fetchJson(ctx, `/${league}/events/${which}`, { query: { limit: 1 } });
|
|
@@ -209,20 +273,24 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
209
273
|
return httpError(d, league, list, `${league} ${which} events`);
|
|
210
274
|
const first = asRecord(extractRows(list.data)[0]);
|
|
211
275
|
if (!first?.slug) {
|
|
276
|
+
const windowed = !!upstreamNotes(list).historyWindow;
|
|
212
277
|
return errorEnvelope({
|
|
213
278
|
code: 'NOT_FOUND',
|
|
214
|
-
message:
|
|
279
|
+
message: windowed
|
|
280
|
+
? `No ${which} ${league.toUpperCase()} event inside this key's history window (meta.historyWindow); pass an event slug instead`
|
|
281
|
+
: `No ${which} ${league.toUpperCase()} event`,
|
|
215
282
|
game: league,
|
|
216
283
|
source: d.source,
|
|
217
284
|
requestId: d.requestId,
|
|
218
285
|
tookMs: Date.now() - d.started,
|
|
219
286
|
upstreamCalls: calls,
|
|
287
|
+
...(windowed ? { retryable: false, details: { historyWindow: upstreamNotes(list).historyWindow } } : {}),
|
|
220
288
|
});
|
|
221
289
|
}
|
|
222
290
|
slug = String(first.slug);
|
|
223
291
|
}
|
|
224
292
|
const [ev, bouts] = await Promise.all([
|
|
225
|
-
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}
|
|
293
|
+
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}`, league === 'bellator' ? { query: { includeBouts: 'false' } } : undefined),
|
|
226
294
|
fetchJson(ctx, `/${league}/events/${encodeURIComponent(slug)}/bouts`),
|
|
227
295
|
]);
|
|
228
296
|
calls += 2;
|
|
@@ -235,10 +303,10 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
235
303
|
rows = Array.isArray(p) ? p : extractRows(asRecord(p)?.bouts ?? p);
|
|
236
304
|
}
|
|
237
305
|
else {
|
|
238
|
-
partial.push(sectionFailure('bouts', bouts));
|
|
306
|
+
partial.push(sectionFailure('bouts', bouts, league));
|
|
239
307
|
}
|
|
240
308
|
const eventData = asRecord(payload(ev)) ?? {};
|
|
241
|
-
const { bouts: _ignored, ...event } = eventData;
|
|
309
|
+
const { bouts: _ignored, card: _card, ...event } = eventData;
|
|
242
310
|
return successEnvelope({
|
|
243
311
|
data: { league, event, boutCount: rows.length, bouts: rows.slice(0, limit) },
|
|
244
312
|
game: league,
|
|
@@ -256,13 +324,18 @@ Example: { "league": "raf", "event": "last" }`,
|
|
|
256
324
|
};
|
|
257
325
|
export const combatBout = {
|
|
258
326
|
name: 'combat_bout',
|
|
259
|
-
description: `One RAF or
|
|
327
|
+
description: `One RAF, BKFC or Bellator bout in full, with its scoring breakdown.
|
|
260
328
|
|
|
261
329
|
RAF: winner, method (decision / tech fall / pin / injury), final score, takedowns, and per-PERIOD scores.
|
|
262
330
|
BKFC: winner, method, round, time, and per-ROUND punch stats for red and blue (landed/thrown, head/body,
|
|
263
331
|
distance/clinch, knockdowns). BKFC round stats need the Starter plan; on Free they come back as a partial note.
|
|
332
|
+
Bellator: winner, method, round, time, finish (position / submission), referee, judges' scores, each corner's
|
|
333
|
+
whole-fight totals, and per-ROUND official stats in UFC's format (significantStrikes "13 of 18", totalStrikes,
|
|
334
|
+
takedowns, head/body/leg, distance/clinch/ground, knockdowns, submissionAttempts, reversals, controlTime "1:42").
|
|
335
|
+
The archive is 2014-2017, older than the Free/Starter history window: on those plans rounds come back as a
|
|
336
|
+
HISTORY_WINDOW_EXCEEDED partial note while the result still returns.
|
|
264
337
|
|
|
265
|
-
Get boutId from combat_card or combat_athlete (e.g. "raf13--belal-muhammad--colby-covington").
|
|
338
|
+
Get boutId from combat_card or combat_athlete (e.g. "raf13--belal-muhammad--colby-covington", "bellator-5600").
|
|
266
339
|
|
|
267
340
|
Parallel-safe: yes. Upstream cost: 1–2.
|
|
268
341
|
Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cisneros--ryan-petersen" }`,
|
|
@@ -272,8 +345,8 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
272
345
|
required: ['league', 'boutId'],
|
|
273
346
|
properties: {
|
|
274
347
|
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),
|
|
348
|
+
boutId: stringSchema('Bout id from combat_card / combat_athlete (Bellator: "bellator-5600"). Example: "raf13--belal-muhammad--colby-covington".'),
|
|
349
|
+
includeBreakdown: boolSchema('Include per-period (RAF) / per-round (BKFC, Bellator) breakdown (default true).', true),
|
|
277
350
|
},
|
|
278
351
|
},
|
|
279
352
|
handler: async (args, ctx) => {
|
|
@@ -301,11 +374,13 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
301
374
|
if (parts.ok)
|
|
302
375
|
data[sub] = asRecord(payload(parts))?.[sub] ?? payload(parts);
|
|
303
376
|
else
|
|
304
|
-
partial.push(sectionFailure(sub, parts));
|
|
377
|
+
partial.push(sectionFailure(sub, parts, league));
|
|
305
378
|
}
|
|
306
379
|
const b = asRecord(data.bout);
|
|
307
380
|
if (league === 'bkfc' && b && 'rounds' in b && data.rounds)
|
|
308
381
|
delete b.rounds;
|
|
382
|
+
if (league === 'bellator' && b && 'roundStats' in b && (data.rounds || !breakdown))
|
|
383
|
+
delete b.roundStats;
|
|
309
384
|
return successEnvelope({
|
|
310
385
|
data,
|
|
311
386
|
game: league,
|
|
@@ -320,15 +395,19 @@ Example: { "league": "bkfc", "boutId": "bkfc-fight-night-bethlehem--gregoris-cis
|
|
|
320
395
|
};
|
|
321
396
|
export const combatAthlete = {
|
|
322
397
|
name: 'combat_athlete',
|
|
323
|
-
description: `COMPOSITE RAF athlete / BKFC fighter page: profile + recent bouts + career stats.
|
|
398
|
+
description: `COMPOSITE RAF athlete / BKFC or Bellator fighter page: profile + recent bouts + career stats.
|
|
324
399
|
|
|
325
400
|
When to use:
|
|
326
|
-
- "Kyle Snyder's RAF record", "Arlovski BKFC stats", athlete/fighter profile pages
|
|
401
|
+
- "Kyle Snyder's RAF record", "Arlovski BKFC stats", "Tito Ortiz in Bellator", athlete/fighter profile pages
|
|
327
402
|
|
|
328
403
|
athlete accepts a slug ("kyle-snyder") or a name ("Andrei Arlovski"); names are resolved through the league's
|
|
329
404
|
search, and several matches return AMBIGUOUS_ENTITY with candidates. Profile carries record, division, nickname,
|
|
330
405
|
country, official headshot (+ its credit — show it with the image). Career stats need the Starter plan; on Free
|
|
331
406
|
they come back as a partial note while profile and history still return.
|
|
407
|
+
Bellator: profile is bio (DOB, height, reach, stance, hometown) + Bellator-only record (2014-2017 archive, not the
|
|
408
|
+
whole career); stats are career totals in UFC's format. A fighter we also hold a UFC record for keeps the SAME slug
|
|
409
|
+
as /ufc/fighters/{slug}: data.ufcFighterSlug is set then (a UFC slug is accepted here too). Keep the two records
|
|
410
|
+
apart: Bellator fights never belong in a UFC answer.
|
|
332
411
|
|
|
333
412
|
Prefer over: call_api profile + history + stats separately.
|
|
334
413
|
|
|
@@ -370,17 +449,21 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
|
|
|
370
449
|
return httpError(d, league, profile, `${league} ${PEOPLE[league]} ${r.slug}`);
|
|
371
450
|
const partial = [];
|
|
372
451
|
const data = { league, slug: r.slug, profile: payload(profile) };
|
|
452
|
+
if (league === 'bellator') {
|
|
453
|
+
const ufc = asRecord(asRecord(data.profile)?.ufc);
|
|
454
|
+
data.ufcFighterSlug = typeof ufc?.ufcFighterSlug === 'string' ? ufc.ufcFighterSlug : null;
|
|
455
|
+
}
|
|
373
456
|
if (history) {
|
|
374
457
|
if (history.ok)
|
|
375
458
|
data.history = extractRows(history.data).slice(0, historyLimit);
|
|
376
459
|
else
|
|
377
|
-
partial.push(sectionFailure('history', history));
|
|
460
|
+
partial.push(sectionFailure('history', history, league));
|
|
378
461
|
}
|
|
379
462
|
if (stats) {
|
|
380
463
|
if (stats.ok)
|
|
381
464
|
data.stats = payload(stats);
|
|
382
465
|
else
|
|
383
|
-
partial.push(sectionFailure('stats', stats));
|
|
466
|
+
partial.push(sectionFailure('stats', stats, league));
|
|
384
467
|
}
|
|
385
468
|
return successEnvelope({
|
|
386
469
|
data,
|
|
@@ -396,9 +479,10 @@ Example: { "league": "raf", "athlete": "Kyle Snyder", "historyLimit": 5 }`,
|
|
|
396
479
|
};
|
|
397
480
|
export const combatH2h = {
|
|
398
481
|
name: 'combat_h2h',
|
|
399
|
-
description: `Head-to-head between two RAF athletes or two
|
|
482
|
+
description: `Head-to-head between two RAF athletes, two BKFC fighters or two Bellator fighters: record and every meeting.
|
|
400
483
|
|
|
401
|
-
Accepts slugs or names for both sides (names resolve through the league's search).
|
|
484
|
+
Accepts slugs or names for both sides (names resolve through the league's search). Bellator meetings are only
|
|
485
|
+
the ones on Bellator cards (2014-2017); UFC meetings between the same two are a UFC question (head_to_head).
|
|
402
486
|
|
|
403
487
|
Parallel-safe: yes. Upstream cost: 1–3.
|
|
404
488
|
Example: { "league": "bkfc", "athleteA": "ryan-petersen", "athleteB": "gregoris-cisneros" }`,
|
|
@@ -453,6 +537,8 @@ export const combatRankings = {
|
|
|
453
537
|
BKFC: omit division for every division's ranked list (champion, interim champion, ranked fighters), or pass one
|
|
454
538
|
(e.g. "heavyweight", "womens-flyweight"). RAF has no ranked lists; it returns every title and its holder, or one
|
|
455
539
|
division's detail when division is given (e.g. "men-heavyweight").
|
|
540
|
+
Bellator has no rankings (a 2014-2017 archive): league "bellator" returns a non-retryable NOT_IMPLEMENTED.
|
|
541
|
+
Never answer with UFC rankings instead.
|
|
456
542
|
|
|
457
543
|
Parallel-safe: yes. Upstream cost: 1.
|
|
458
544
|
Example: { "league": "bkfc", "division": "heavyweight" }`,
|
|
@@ -470,6 +556,13 @@ Example: { "league": "bkfc", "division": "heavyweight" }`,
|
|
|
470
556
|
const league = parseLeague(args.league);
|
|
471
557
|
if (!league)
|
|
472
558
|
return leagueError(d, args.league);
|
|
559
|
+
if (league === 'bellator') {
|
|
560
|
+
return archiveGap(d, league, `Bellator has no rankings or champions lists: ${ARCHIVES.bellator}, and the API serves its results only.`, [
|
|
561
|
+
'Do not retry: no Bellator rankings exist on the API',
|
|
562
|
+
'Title fights: call_api /bellator/bouts with query { "title": "true" }; results: combat_events { league: "bellator", when: "recent" }',
|
|
563
|
+
'Never substitute UFC rankings for Bellator',
|
|
564
|
+
]);
|
|
565
|
+
}
|
|
473
566
|
const division = typeof args.division === 'string' ? args.division.trim().toLowerCase() : '';
|
|
474
567
|
const path = league === 'bkfc'
|
|
475
568
|
? division ? `/bkfc/rankings/${encodeURIComponent(division)}` : '/bkfc/rankings'
|
package/dist/tools/meta.js
CHANGED
|
@@ -6,19 +6,19 @@ const COMBAT_CATALOG = [
|
|
|
6
6
|
{
|
|
7
7
|
name: 'combat_events',
|
|
8
8
|
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',
|
|
9
|
+
outcome: 'RAF wrestling / BKFC bare-knuckle / Bellator MMA events: upcoming cards or recent results, with date, local time and venue (Bellator: archive, upcoming is empty with a note)',
|
|
10
10
|
parallelSafe: true,
|
|
11
|
-
games: ['raf', 'bkfc'],
|
|
11
|
+
games: ['raf', 'bkfc', 'bellator'],
|
|
12
12
|
jobs: ['schedule'],
|
|
13
13
|
exampleArgs: { league: 'bkfc', when: 'upcoming', limit: 5 },
|
|
14
|
-
preferOver: ['call_api /raf/events or /
|
|
14
|
+
preferOver: ['call_api /raf/events, /bkfc/events or /bellator/events'],
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
name: 'combat_card',
|
|
18
18
|
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"',
|
|
19
|
+
outcome: 'One RAF/BKFC/Bellator event card in running order with results; event = slug, "next" or "last" (Bellator: "last" = most recent archived card, no "next")',
|
|
20
20
|
parallelSafe: true,
|
|
21
|
-
games: ['raf', 'bkfc'],
|
|
21
|
+
games: ['raf', 'bkfc', 'bellator'],
|
|
22
22
|
jobs: ['event_card', 'schedule', 'media'],
|
|
23
23
|
exampleArgs: { league: 'raf', event: 'last' },
|
|
24
24
|
preferOver: ['combat_events + call_api per bout'],
|
|
@@ -27,9 +27,9 @@ const COMBAT_CATALOG = [
|
|
|
27
27
|
name: 'combat_bout',
|
|
28
28
|
doNotUse: 'Whole card → combat_card',
|
|
29
29
|
preferOver: [],
|
|
30
|
-
outcome: 'One bout in full: RAF per-period scores / BKFC per-round punch stats (Starter)',
|
|
30
|
+
outcome: 'One bout in full: RAF per-period scores / BKFC per-round punch stats (Starter) / Bellator per-round official stats in UFC format (Pro: the archive is older than the Free/Starter window)',
|
|
31
31
|
parallelSafe: true,
|
|
32
|
-
games: ['raf', 'bkfc'],
|
|
32
|
+
games: ['raf', 'bkfc', 'bellator'],
|
|
33
33
|
jobs: ['match_page'],
|
|
34
34
|
exampleArgs: { league: 'raf', boutId: 'raf13--belal-muhammad--colby-covington' },
|
|
35
35
|
},
|
|
@@ -37,9 +37,9 @@ const COMBAT_CATALOG = [
|
|
|
37
37
|
name: 'combat_athlete',
|
|
38
38
|
doNotUse: "Two athletes' meetings → combat_h2h",
|
|
39
39
|
preferOver: [],
|
|
40
|
-
outcome: 'RAF athlete / BKFC fighter page: profile + headshot, recent bouts, career stats (Starter); slug or name',
|
|
40
|
+
outcome: 'RAF athlete / BKFC or Bellator fighter page: profile + headshot, recent bouts, career stats (Starter); slug or name; Bellator fighters share the UFC slug (ufcFighterSlug)',
|
|
41
41
|
parallelSafe: true,
|
|
42
|
-
games: ['raf', 'bkfc'],
|
|
42
|
+
games: ['raf', 'bkfc', 'bellator'],
|
|
43
43
|
jobs: ['player_form', 'media'],
|
|
44
44
|
exampleArgs: { league: 'raf', athlete: 'Kyle Snyder', historyLimit: 5 },
|
|
45
45
|
},
|
|
@@ -47,19 +47,19 @@ const COMBAT_CATALOG = [
|
|
|
47
47
|
name: 'combat_h2h',
|
|
48
48
|
doNotUse: "One athlete's whole history → combat_athlete",
|
|
49
49
|
preferOver: [],
|
|
50
|
-
outcome: 'Head-to-head between two RAF athletes or two
|
|
50
|
+
outcome: 'Head-to-head between two RAF athletes, two BKFC fighters or two Bellator fighters',
|
|
51
51
|
parallelSafe: true,
|
|
52
|
-
games: ['raf', 'bkfc'],
|
|
52
|
+
games: ['raf', 'bkfc', 'bellator'],
|
|
53
53
|
jobs: ['h2h', 'preview'],
|
|
54
54
|
exampleArgs: { league: 'bkfc', athleteA: 'ryan-petersen', athleteB: 'gregoris-cisneros' },
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: 'combat_rankings',
|
|
58
|
-
doNotUse: 'UFC rankings → standings / rankings tools (RAF/BKFC never appear in UFC answers)',
|
|
58
|
+
doNotUse: 'UFC rankings → standings / rankings tools (RAF/BKFC/Bellator never appear in UFC answers); Bellator has no rankings (NOT_IMPLEMENTED)',
|
|
59
59
|
preferOver: [],
|
|
60
60
|
outcome: 'RAF champions per division / BKFC official rankings and champions',
|
|
61
61
|
parallelSafe: true,
|
|
62
|
-
games: ['raf', 'bkfc'],
|
|
62
|
+
games: ['raf', 'bkfc', 'bellator'],
|
|
63
63
|
jobs: ['standings'],
|
|
64
64
|
exampleArgs: { league: 'bkfc', division: 'heavyweight' },
|
|
65
65
|
},
|
|
@@ -547,7 +547,14 @@ const GAMES_META = [
|
|
|
547
547
|
{ id: 'cs2', label: 'Counter-Strike 2', depth: 'deep', notes: 'Prefixed ids; live path /cs2/live; trends/rankings' },
|
|
548
548
|
{ id: 'dota2', label: 'Dota 2', depth: 'thin', notes: 'Numeric ids; radar; no standings/roster endpoints' },
|
|
549
549
|
{ id: 'cod', label: 'Call of Duty (CDL)', depth: 'medium', notes: 'Org slugs; CDL standings; UUID match ids' },
|
|
550
|
-
{
|
|
550
|
+
{
|
|
551
|
+
id: 'ufc',
|
|
552
|
+
label: 'UFC / MMA',
|
|
553
|
+
depth: 'medium',
|
|
554
|
+
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 is NOT UFC: use the ' +
|
|
556
|
+
'combat_* tools with league bellator, and never mix its fights into UFC answers.',
|
|
557
|
+
},
|
|
551
558
|
];
|
|
552
559
|
export const listCapabilities = {
|
|
553
560
|
name: 'list_capabilities',
|
|
@@ -789,6 +796,7 @@ const ALLOWLIST_PREFIXES = [
|
|
|
789
796
|
'/tennis',
|
|
790
797
|
'/raf',
|
|
791
798
|
'/bkfc',
|
|
799
|
+
'/bellator',
|
|
792
800
|
'/openapi.json',
|
|
793
801
|
];
|
|
794
802
|
function pathAllowed(path) {
|
|
@@ -812,7 +820,7 @@ Prefer curated tools for all standard jobs (live, schedule, profiles, standings,
|
|
|
812
820
|
|
|
813
821
|
Do not use when: a curated tool covers the outcome. Avoid parallel storms; same plan rate limits apply.
|
|
814
822
|
|
|
815
|
-
Path must start with / and match allowlisted prefixes: /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc.
|
|
823
|
+
Path must start with / and match allowlisted prefixes: /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc, /bellator.
|
|
816
824
|
Rejects absolute URLs and path traversal → PATH_NOT_ALLOWED.
|
|
817
825
|
|
|
818
826
|
Parallel-safe: yes but discouraged in bulk. Upstream cost: 1.
|
|
@@ -873,7 +881,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
873
881
|
source: 'call_api',
|
|
874
882
|
requestId,
|
|
875
883
|
tookMs: Date.now() - started,
|
|
876
|
-
hint: 'Use prefixes /health /lol /cs2 /dota2 /cod /ufc /fortnite /tennis',
|
|
884
|
+
hint: 'Use prefixes /health /lol /cs2 /dota2 /cod /ufc /fortnite /tennis /raf /bkfc /bellator',
|
|
877
885
|
});
|
|
878
886
|
}
|
|
879
887
|
let query = {};
|
|
@@ -974,10 +982,10 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
974
982
|
const started = Date.now();
|
|
975
983
|
const requestId = newRequestId();
|
|
976
984
|
const gameRaw = typeof args.game === 'string' ? args.game.toLowerCase().trim() : '';
|
|
977
|
-
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc)$/)) {
|
|
985
|
+
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator)$/)) {
|
|
978
986
|
return errorEnvelope({
|
|
979
987
|
code: 'UNSUPPORTED_GAME',
|
|
980
|
-
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc`,
|
|
988
|
+
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis|raf|bkfc|bellator`,
|
|
981
989
|
game: null,
|
|
982
990
|
source: 'list_routes',
|
|
983
991
|
requestId,
|
|
@@ -1000,7 +1008,7 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
1000
1008
|
httpStatus: res.status,
|
|
1001
1009
|
recover: [
|
|
1002
1010
|
'Call list_capabilities for curated tools that need no route knowledge',
|
|
1003
|
-
'call_api accepts /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite',
|
|
1011
|
+
'call_api accepts /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /tennis, /raf, /bkfc, /bellator',
|
|
1004
1012
|
],
|
|
1005
1013
|
});
|
|
1006
1014
|
}
|
package/package.json
CHANGED
|
@@ -1,51 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cito-mcp",
|
|
3
|
-
"version": "0.4.
|
|
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.4.9",
|
|
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
|
+
}
|