cito-mcp 0.3.12 → 0.3.13
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/dist/tools/insight.js +16 -5
- package/dist/tools/live.js +25 -8
- package/dist/tools/normalize.js +99 -8
- package/dist/tools/player.js +13 -4
- package/dist/tools/standings.js +13 -3
- package/dist/tools/team.js +4 -1
- package/package.json +2 -2
package/dist/tools/insight.js
CHANGED
|
@@ -47,10 +47,14 @@ async function loadSide(ctx, game, side, recentLimit, includeRosters) {
|
|
|
47
47
|
recentForm = extractRows(unwrapPayload(fights.data) ?? fights.data)
|
|
48
48
|
.slice(0, recentLimit)
|
|
49
49
|
.map((row) => {
|
|
50
|
-
//
|
|
50
|
+
// History rows nest the bout and carry the fighter's own side as
|
|
51
|
+
// fighterSlug/opponent/outcome. Merge so normalizeMatch sees both,
|
|
52
|
+
// and do NOT force 'completed': the fighter's next scheduled bout is
|
|
53
|
+
// in this list too, and stamping it completed put an unfought fight
|
|
54
|
+
// at the top of the form with no result.
|
|
51
55
|
const r = asRecord(row) ?? {};
|
|
52
|
-
const bout = asRecord(r.bout) ??
|
|
53
|
-
return normalizeMatch('ufc', { ...bout, ...
|
|
56
|
+
const bout = asRecord(r.bout) ?? {};
|
|
57
|
+
return normalizeMatch('ufc', { ...bout, ...r });
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
const stats = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(side)}/stats`);
|
|
@@ -201,7 +205,7 @@ async function loadSide(ctx, game, side, recentLimit, includeRosters) {
|
|
|
201
205
|
}
|
|
202
206
|
return { entity, roster, recentForm, keyPlayers, calls, partial, rateLimit };
|
|
203
207
|
}
|
|
204
|
-
function composeH2H(game, sideA, sideB, rows, limit) {
|
|
208
|
+
export function composeH2H(game, sideA, sideB, rows, limit) {
|
|
205
209
|
const al = sideA.toLowerCase();
|
|
206
210
|
const bl = sideB.toLowerCase();
|
|
207
211
|
const meetings = rows
|
|
@@ -406,8 +410,10 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
406
410
|
rateLimit = { ...rateLimit, ...hist.headers };
|
|
407
411
|
if (hist.ok) {
|
|
408
412
|
rows = extractRows(unwrapPayload(hist.data) ?? hist.data).map((row) => {
|
|
413
|
+
// Keep fighterSlug/opponent/outcome alongside the bout; they are
|
|
414
|
+
// the only place the two corners exist on a history row.
|
|
409
415
|
const r = asRecord(row) ?? {};
|
|
410
|
-
return asRecord(r.bout) ??
|
|
416
|
+
return { ...(asRecord(r.bout) ?? {}), ...r };
|
|
411
417
|
});
|
|
412
418
|
}
|
|
413
419
|
else {
|
|
@@ -757,6 +763,11 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeBouts": true, "inc
|
|
|
757
763
|
}));
|
|
758
764
|
}
|
|
759
765
|
}
|
|
766
|
+
// Bout rows carry no start time of their own; inherit the card's. Without
|
|
767
|
+
// this the same bout said startTime:null here and the event time on
|
|
768
|
+
// upcoming_schedule, and a caller had to fetch both to learn when it is.
|
|
769
|
+
const cardStart = typeof event.date === 'string' ? event.date : null;
|
|
770
|
+
bouts = bouts.map((b) => ({ ...b, startTime: b.startTime ?? cardStart }));
|
|
760
771
|
if (includeStandings) {
|
|
761
772
|
const ranks = await fetchJson(ctx, '/ufc/rankings');
|
|
762
773
|
upstreamCalls += 1;
|
package/dist/tools/live.js
CHANGED
|
@@ -77,10 +77,16 @@ export function extractLiveRows(data, game) {
|
|
|
77
77
|
export function inferUfcEmptyReason(events, health, liveCount) {
|
|
78
78
|
if (liveCount > 0)
|
|
79
79
|
return undefined;
|
|
80
|
+
const pulses = events.map((ev) => asRecord(ev) ?? {});
|
|
81
|
+
const hotNow = pulses.some((p) => ['live', 'warming', 'between_bouts', 'degraded'].includes(String(pickString(p.status, p.eventStatus) ?? '').toLowerCase()));
|
|
82
|
+
// The API's own health block is trusted first, EXCEPT when it blames the
|
|
83
|
+
// worker on a day nothing is live. It computes worker_stale from heartbeat
|
|
84
|
+
// lag alone, so an idle Thursday reads the same as a dead worker on fight
|
|
85
|
+
// night. With no hot card there is nothing to be stale about.
|
|
80
86
|
const fromHealth = pickString(health?.emptyReason);
|
|
81
|
-
if (fromHealth)
|
|
87
|
+
if (fromHealth && !(['worker_stale', 'worker_down_inferred'].includes(fromHealth) && !hotNow)) {
|
|
82
88
|
return fromHealth;
|
|
83
|
-
|
|
89
|
+
}
|
|
84
90
|
if (!pulses.length) {
|
|
85
91
|
const alive = health?.workerAlive ?? health?.workerStarted;
|
|
86
92
|
if (alive === false)
|
|
@@ -94,9 +100,13 @@ export function inferUfcEmptyReason(events, health, liveCount) {
|
|
|
94
100
|
const freshest = lags.length ? Math.min(...lags) : null;
|
|
95
101
|
const workerAlive = health?.workerAlive ?? health?.workerStarted;
|
|
96
102
|
const hot = statuses.some((s) => ['live', 'warming', 'between_bouts', 'degraded'].includes(s));
|
|
97
|
-
|
|
103
|
+
// Only a HOT card can be stale. On an idle day every event is "scheduled"
|
|
104
|
+
// and the heartbeat lags because there is nothing to beat about; reporting
|
|
105
|
+
// that as worker_stale made a quiet Thursday indistinguishable from a dead
|
|
106
|
+
// worker on fight night. freshest stays computed for the health block.
|
|
107
|
+
void freshest;
|
|
108
|
+
if (workerAlive === false && hot)
|
|
98
109
|
return 'worker_stale';
|
|
99
|
-
}
|
|
100
110
|
if (statuses.some((s) => s === 'degraded'))
|
|
101
111
|
return 'event_degraded';
|
|
102
112
|
if (statuses.some((s) => s === 'warming'))
|
|
@@ -126,8 +136,10 @@ export function humanUfcEmptyNote(emptyReason, health) {
|
|
|
126
136
|
supervisor_warming: 'Card open / pre-gate; no bout live yet',
|
|
127
137
|
between_bouts: 'Between bouts; next may be armed',
|
|
128
138
|
armed_only: 'Next bout armed; not started',
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
// Customer-facing. The pm2/VPS operator hint used to be in here and went
|
|
140
|
+
// out in every empty board; health{} already carries the numbers for us.
|
|
141
|
+
worker_stale: 'Live feed is behind; scores may lag',
|
|
142
|
+
worker_down_inferred: 'Live feed unavailable right now',
|
|
131
143
|
event_degraded: 'Sources empty clock / event degraded; not inventing stats',
|
|
132
144
|
};
|
|
133
145
|
const msg = human[reason] || `No live|watching bouts (${reason})`;
|
|
@@ -284,9 +296,14 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
284
296
|
if (game === 'ufc') {
|
|
285
297
|
const health = asRecord(meta?.health) ?? asRecord(payload?.health);
|
|
286
298
|
const events = Array.isArray(payload?.events) ? payload.events : [];
|
|
299
|
+
// One decision point. This used to short-circuit on the API's own
|
|
300
|
+
// emptyReason and only fall back to inferUfcEmptyReason when the API
|
|
301
|
+
// said nothing, so the idle-day rule inside it never ran: the API
|
|
302
|
+
// blames the worker on heartbeat lag alone and an empty Thursday came
|
|
303
|
+
// back as worker_stale. Hand the hint in and let the function weigh it.
|
|
304
|
+
const upstreamReason = pickString(meta?.emptyReason, health?.emptyReason);
|
|
287
305
|
const emptyReason = normalized.length === 0
|
|
288
|
-
?
|
|
289
|
-
inferUfcEmptyReason(events, health, normalized.length)
|
|
306
|
+
? inferUfcEmptyReason(events, { ...(health ?? {}), ...(upstreamReason ? { emptyReason: upstreamReason } : {}) }, normalized.length)
|
|
290
307
|
: undefined;
|
|
291
308
|
let note;
|
|
292
309
|
if (normalized.length === 0) {
|
package/dist/tools/normalize.js
CHANGED
|
@@ -119,9 +119,17 @@ function sideExtras(o, game) {
|
|
|
119
119
|
...(rank ? { rank } : {}),
|
|
120
120
|
// "none" carries no signal for a page; only surface an actual belt state.
|
|
121
121
|
...(championStatus && championStatus.toLowerCase() !== 'none' ? { championStatus } : {}),
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// UFC corners always carry these keys. A debutant with no ranked division
|
|
123
|
+
// was shipping with the key ABSENT while every other corner had it, and a
|
|
124
|
+
// fighter from a country with no flag mapping did the same, which breaks
|
|
125
|
+
// strict parsers. Null says "not known"; absence says nothing.
|
|
126
|
+
...(game === 'ufc'
|
|
127
|
+
? { division: division ?? null, country: country ?? null, flag: flag ?? null }
|
|
128
|
+
: {
|
|
129
|
+
...(division ? { division } : {}),
|
|
130
|
+
...(country ? { country } : {}),
|
|
131
|
+
...(flag ? { flag } : {}),
|
|
132
|
+
}),
|
|
125
133
|
...(outcome ? { outcome } : {}),
|
|
126
134
|
};
|
|
127
135
|
}
|
|
@@ -151,6 +159,40 @@ function scoreNum(v) {
|
|
|
151
159
|
return Number(v);
|
|
152
160
|
return null;
|
|
153
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Collapse the six spellings one outcome arrives in ("SUB" / "Submission",
|
|
164
|
+
* "U-DEC" / "Decision - Unanimous", "KO/TKO" / "TKO") onto one enum. The raw
|
|
165
|
+
* string is kept alongside as methodRaw; this is a stable key for consumers,
|
|
166
|
+
* not a rewrite of the source.
|
|
167
|
+
*/
|
|
168
|
+
export function normalizeUfcMethod(raw) {
|
|
169
|
+
if (!raw)
|
|
170
|
+
return undefined;
|
|
171
|
+
const s = raw.trim().toLowerCase();
|
|
172
|
+
if (!s)
|
|
173
|
+
return undefined;
|
|
174
|
+
if (/overturn/.test(s))
|
|
175
|
+
return 'Overturned';
|
|
176
|
+
if (/no[\s-]?contest|^nc$/.test(s))
|
|
177
|
+
return 'NC';
|
|
178
|
+
if (/^dq$|disqualif/.test(s))
|
|
179
|
+
return 'DQ';
|
|
180
|
+
if (/draw/.test(s))
|
|
181
|
+
return 'Draw';
|
|
182
|
+
if (/sub/.test(s))
|
|
183
|
+
return 'SUB';
|
|
184
|
+
if (/ko|tko|knock|stoppage|strikes|punch|kick|elbow|knee/.test(s))
|
|
185
|
+
return 'KO/TKO';
|
|
186
|
+
if (/unanim|^u-?dec$/.test(s))
|
|
187
|
+
return 'U-DEC';
|
|
188
|
+
if (/split|^s-?dec$/.test(s))
|
|
189
|
+
return 'S-DEC';
|
|
190
|
+
if (/major|^m-?dec$/.test(s))
|
|
191
|
+
return 'M-DEC';
|
|
192
|
+
if (/dec/.test(s))
|
|
193
|
+
return 'DEC';
|
|
194
|
+
return raw.trim();
|
|
195
|
+
}
|
|
154
196
|
export function normalizeMatch(game, row, forcedStatus) {
|
|
155
197
|
// Always peel { success, data } so UFC bout fighters[] / status are visible.
|
|
156
198
|
const r = asRecord(unwrapPayload(row)) ?? asRecord(row) ?? {};
|
|
@@ -216,7 +258,11 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
216
258
|
const profile = asRecord(o.profile);
|
|
217
259
|
const fighter = asRecord(o.fighter);
|
|
218
260
|
const name = pickString(o.name, o.fighterName, o.displayName, o.nickname, profile?.name, profile?.nickname, fighter?.name, fighter?.nickname);
|
|
219
|
-
|
|
261
|
+
// o.id on a serialized corner is the ufc_bout_fighters ROW id, unique per
|
|
262
|
+
// bout, so the same human got a different "id" on every card. Prefer the
|
|
263
|
+
// fighter's own id wherever the API supplies it; fall back to the row id
|
|
264
|
+
// only when nothing better exists.
|
|
265
|
+
const id = pickString(o.fighterId, profile?.id, fighter?.id, o.id);
|
|
220
266
|
const slug = pickString(o.slug, o.fighterSlug, profile?.slug, fighter?.slug);
|
|
221
267
|
const score = scoreNum(o.score ?? o.points);
|
|
222
268
|
return sideFrom(name, id, slug, score, sideExtras(o, game));
|
|
@@ -247,6 +293,31 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
247
293
|
team2 = team2 ?? fromCornerObj(blueF);
|
|
248
294
|
}
|
|
249
295
|
}
|
|
296
|
+
// UFC fighter-history rows (/ufc/fighters/{slug}/fights). These carry the
|
|
297
|
+
// fighter as fighterSlug/fighterName, the other corner under opponent, and
|
|
298
|
+
// the result as outcome, with NO fighters[] and no team1/team2. Every reader
|
|
299
|
+
// was left with two null sides: head_to_head filtered on those nulls and
|
|
300
|
+
// reported Jones vs Cormier as never having met, the profile showed
|
|
301
|
+
// team1:null with the division as the label, and the preview form could not
|
|
302
|
+
// say who won. One shape, three symptoms.
|
|
303
|
+
if (game === 'ufc' && !team1 && !team2 && typeof r.fighterSlug === 'string') {
|
|
304
|
+
const opp = asRecord(r.opponent);
|
|
305
|
+
const selfCorner = pickString(r.corner)?.toLowerCase();
|
|
306
|
+
const self = sideFrom(pickString(r.fighterName) ?? String(r.fighterSlug), undefined, String(r.fighterSlug), undefined, sideExtras(r, game));
|
|
307
|
+
const other = opp
|
|
308
|
+
? sideFrom(pickString(opp.name) ?? pickString(opp.slug), undefined, pickString(opp.slug), undefined, sideExtras(opp, game))
|
|
309
|
+
: null;
|
|
310
|
+
// Keep red on team1 when we know the corner, so a card and a history row
|
|
311
|
+
// agree on which side is which.
|
|
312
|
+
if (selfCorner === 'blue' && other) {
|
|
313
|
+
team1 = other;
|
|
314
|
+
team2 = self;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
team1 = self;
|
|
318
|
+
team2 = other;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
250
321
|
// COD sometimes uses teams[]
|
|
251
322
|
if ((!team1 || !team2) && Array.isArray(r.teams)) {
|
|
252
323
|
team1 = team1 ?? nestedSide(r.teams[0], game);
|
|
@@ -296,9 +367,20 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
296
367
|
else if (hasResult)
|
|
297
368
|
status = 'completed';
|
|
298
369
|
}
|
|
370
|
+
// The winner, resolved once. Explicit winner fields first; otherwise a
|
|
371
|
+
// history row states the result from the fighter's own side (outcome: win /
|
|
372
|
+
// loss), which translates back into a slug. Both the score below and the
|
|
373
|
+
// result block use this, so a fight the profile shows as a win also counts
|
|
374
|
+
// as a win in head_to_head's tally, which reads the scores.
|
|
375
|
+
const historyOutcome = pickString(r.outcome)?.toLowerCase();
|
|
376
|
+
const historyOpponent = pickString(asRecord(r.opponent)?.slug);
|
|
377
|
+
const resolvedWinnerSlug = pickString(r.winnerFighterSlug, r.winnerSlug, r.winner) ??
|
|
378
|
+
(historyOutcome === 'win' && typeof r.fighterSlug === 'string' ? r.fighterSlug
|
|
379
|
+
: historyOutcome === 'loss' && historyOpponent ? historyOpponent
|
|
380
|
+
: undefined);
|
|
299
381
|
// Winner → score 1-0 for UFC card display when numeric scores absent
|
|
300
382
|
if (game === 'ufc' && team1 && team2 && team1.score == null && team2.score == null) {
|
|
301
|
-
const winner =
|
|
383
|
+
const winner = resolvedWinnerSlug?.toLowerCase();
|
|
302
384
|
if (winner) {
|
|
303
385
|
const t1hit = [team1.slug, team1.id, team1.name].some((x) => x && (String(x).toLowerCase() === winner || String(x).toLowerCase().includes(winner) || winner.includes(String(x).toLowerCase())));
|
|
304
386
|
const t2hit = [team2.slug, team2.id, team2.name].some((x) => x && (String(x).toLowerCase() === winner || String(x).toLowerCase().includes(winner) || winner.includes(String(x).toLowerCase())));
|
|
@@ -324,7 +406,14 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
324
406
|
// When both sides lack names, prefer event / weight / bout id over opaque "? vs ?"
|
|
325
407
|
// so agent boards (live_matches, upcoming_schedule, event_card) stay legible.
|
|
326
408
|
const vsLabel = `${team1?.name ?? '?'} vs ${team2?.name ?? '?'}`;
|
|
327
|
-
const
|
|
409
|
+
const explicitRaw = pickString(r.label, r.title, r.name);
|
|
410
|
+
// UFC history rows put the division where a title would go ("Heavyweight"),
|
|
411
|
+
// which then displaced "Jon Jones vs Stipe Miocic" as the label. A label that
|
|
412
|
+
// merely repeats the weight class is not a matchup; use the sides instead.
|
|
413
|
+
const explicit = game === 'ufc' && explicitRaw && (team1?.name || team2?.name) &&
|
|
414
|
+
explicitRaw.toLowerCase() === (pickString(r.weightClass, r.division) ?? '').toLowerCase()
|
|
415
|
+
? undefined
|
|
416
|
+
: explicitRaw;
|
|
328
417
|
const explicitIsPlaceholder = explicit != null && /^\?\s*vs\s*\?$/i.test(explicit.trim());
|
|
329
418
|
const weightOrClass = pickString(r.weightClass, r.division, r.weight_class, r.boutClass);
|
|
330
419
|
let label;
|
|
@@ -348,12 +437,13 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
348
437
|
const cardSectionOrder = numOrNull(r.cardSectionOrder);
|
|
349
438
|
const boutOrder = numOrNull(r.boutOrder);
|
|
350
439
|
const hasCard = cardSection != null || cardPosition != null || cardSectionOrder != null || boutOrder != null;
|
|
351
|
-
const
|
|
440
|
+
const methodRaw = pickString(r.method);
|
|
441
|
+
const method = normalizeUfcMethod(methodRaw);
|
|
352
442
|
const methodDetails = pickString(r.methodDetails);
|
|
353
443
|
const resultTime = pickString(r.resultTime);
|
|
354
444
|
// UFC sends referee as { id, name, firstName, lastName } — not a string.
|
|
355
445
|
const referee = pickString(r.referee, asRecord(r.referee)?.name);
|
|
356
|
-
const winnerSlug =
|
|
446
|
+
const winnerSlug = resolvedWinnerSlug;
|
|
357
447
|
const resultRound = numOrNull(r.resultRound);
|
|
358
448
|
const hasResultDetail = method != null || resultRound != null || winnerSlug != null || resultTime != null;
|
|
359
449
|
const isCancelled = r.isCancelled === true;
|
|
@@ -383,6 +473,7 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
383
473
|
? {
|
|
384
474
|
result: {
|
|
385
475
|
method: method ?? null,
|
|
476
|
+
methodRaw: methodRaw ?? null,
|
|
386
477
|
methodDetails: methodDetails ?? null,
|
|
387
478
|
round: resultRound,
|
|
388
479
|
time: resultTime ?? null,
|
package/dist/tools/player.js
CHANGED
|
@@ -479,7 +479,11 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
479
479
|
.slice(0, recentLimit)
|
|
480
480
|
.map((row) => {
|
|
481
481
|
const r = asRecord(row) ?? {};
|
|
482
|
-
|
|
482
|
+
// Merge the row's own corner fields (fighterSlug / opponent /
|
|
483
|
+
// outcome) over the nested bout: they are the only place the two
|
|
484
|
+
// sides exist on a history row, and without them every entry
|
|
485
|
+
// came back team1:null, team2:null with the division as label.
|
|
486
|
+
const bout = { ...(asRecord(r.bout) ?? {}), ...r };
|
|
483
487
|
// Never force 'completed'. A fighter's history endpoint also
|
|
484
488
|
// returns bouts that are booked but not yet fought, and forcing
|
|
485
489
|
// the status told agents that a future main event had already
|
|
@@ -488,11 +492,16 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
488
492
|
// reported as completed with result null). Let normalizeMatch
|
|
489
493
|
// derive it from the result and the start time instead.
|
|
490
494
|
const m = normalizeMatch('ufc', bout);
|
|
495
|
+
// Keep the normalized result OBJECT (method, round, time,
|
|
496
|
+
// winnerSlug), the same shape match_preview and match_summary
|
|
497
|
+
// return. This used to overwrite it with the string "win",
|
|
498
|
+
// giving the profile a fourth result schema of its own. The
|
|
499
|
+
// fighter's-eye view lives on outcome instead.
|
|
491
500
|
return {
|
|
492
501
|
...m,
|
|
493
|
-
|
|
502
|
+
outcome: pickString(r.outcome)?.toLowerCase() ?? null,
|
|
494
503
|
opponent: pickString(r.opponentName, asRecord(r.opponent)?.name) ?? null,
|
|
495
|
-
method:
|
|
504
|
+
method: m.result?.method ?? null,
|
|
496
505
|
};
|
|
497
506
|
});
|
|
498
507
|
}
|
|
@@ -508,7 +517,7 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
508
517
|
.slice(0, recentLimit)
|
|
509
518
|
.map((row) => {
|
|
510
519
|
const r = asRecord(row) ?? {};
|
|
511
|
-
const bout = asRecord(r.bout) ??
|
|
520
|
+
const bout = { ...(asRecord(r.bout) ?? {}), ...r };
|
|
512
521
|
// Same as above: derive, never assert. See the note there.
|
|
513
522
|
return normalizeMatch('ufc', bout);
|
|
514
523
|
});
|
package/dist/tools/standings.js
CHANGED
|
@@ -21,7 +21,10 @@ export function normalizeStandingRow(row, index) {
|
|
|
21
21
|
isInterim;
|
|
22
22
|
let rank;
|
|
23
23
|
if (isChampion) {
|
|
24
|
-
rank
|
|
24
|
+
// rank is numeric or null; the belt lives in rankText ("C"/"IC") and
|
|
25
|
+
// championStatus. Mixing a string into a numeric column forced every
|
|
26
|
+
// consumer to special-case it.
|
|
27
|
+
rank = null;
|
|
25
28
|
}
|
|
26
29
|
else if (typeof r.rank === 'number') {
|
|
27
30
|
rank = r.rank;
|
|
@@ -314,7 +317,11 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
314
317
|
rateLimit: res.headers,
|
|
315
318
|
});
|
|
316
319
|
}
|
|
317
|
-
|
|
320
|
+
// Keep the unsliced count. UFC world scope is every division back to back,
|
|
321
|
+
// so limit=50 stops a third of the way through flyweight; a consumer needs
|
|
322
|
+
// to know that happened rather than receive total:null and guess.
|
|
323
|
+
let allRows = extractRows(res.data).map((row, i) => normalizeStandingRow(row, i));
|
|
324
|
+
let rows = allRows.slice(0, limit);
|
|
318
325
|
// UFC rankings may be nested by division
|
|
319
326
|
if (game === 'ufc' && rows.length === 0) {
|
|
320
327
|
const obj = asRecord(res.data) ?? {};
|
|
@@ -326,7 +333,8 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
326
333
|
nested.push(...asRecord(value).rankings);
|
|
327
334
|
}
|
|
328
335
|
}
|
|
329
|
-
|
|
336
|
+
allRows = nested.map((row, i) => normalizeStandingRow(row, i));
|
|
337
|
+
rows = allRows.slice(0, limit);
|
|
330
338
|
}
|
|
331
339
|
const obj = asRecord(res.data);
|
|
332
340
|
const upstreamMeta = asRecord(obj?.meta) ?? {};
|
|
@@ -369,6 +377,8 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
369
377
|
season: season ?? null,
|
|
370
378
|
stage: stage ?? null,
|
|
371
379
|
rows,
|
|
380
|
+
total: allRows.length,
|
|
381
|
+
hasMore: allRows.length > rows.length,
|
|
372
382
|
updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, upstreamMeta.syncedAt, upstreamMeta.fetchedAt) ??
|
|
373
383
|
null,
|
|
374
384
|
...(dataFreshness ? { dataFreshness } : {}),
|
package/dist/tools/team.js
CHANGED
|
@@ -829,8 +829,11 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
829
829
|
});
|
|
830
830
|
}
|
|
831
831
|
const toBouts = (payload) => extractRows(payload).map((row) => {
|
|
832
|
+
// History rows carry the two corners as fighterSlug/opponent, not on
|
|
833
|
+
// the nested bout. Dropping them here is why Jones vs Cormier reported
|
|
834
|
+
// zero meetings while the profile listed both fights.
|
|
832
835
|
const r = asRecord(row) ?? {};
|
|
833
|
-
return asRecord(r.bout) ??
|
|
836
|
+
return { ...(asRecord(r.bout) ?? {}), ...r };
|
|
834
837
|
});
|
|
835
838
|
const fromA = histA.ok ? toBouts(histA.data) : [];
|
|
836
839
|
const fromB = histB.ok ? toBouts(histB.data) : [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cito-mcp",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Standalone MCP server for the Cito esports API
|
|
3
|
+
"version": "0.3.13",
|
|
4
|
+
"description": "Standalone MCP server for the Cito esports API \u2014 15 curated outcome tools for agents (live, schedule, profiles, standings, previews, event cards).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cito-mcp": "dist/index.js"
|