cito-mcp 0.3.13 → 0.3.14
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 +30 -1
- package/dist/tools/live.js +25 -0
- package/dist/tools/match.js +5 -2
- package/dist/tools/player.js +22 -10
- package/dist/tools/standings.js +28 -2
- package/dist/tools/team.js +3 -0
- package/package.json +1 -1
package/dist/tools/insight.js
CHANGED
|
@@ -210,6 +210,10 @@ export function composeH2H(game, sideA, sideB, rows, limit) {
|
|
|
210
210
|
const bl = sideB.toLowerCase();
|
|
211
211
|
const meetings = rows
|
|
212
212
|
.map((row) => normalizeMatch(game, row))
|
|
213
|
+
// A meeting is a fight that happened. The upcoming bout between the two
|
|
214
|
+
// sides sits in the same history list and must not count as a prior
|
|
215
|
+
// meeting, or every preview reports the fight it is previewing as H2H.
|
|
216
|
+
.filter((m) => m.status === 'completed')
|
|
213
217
|
.filter((m) => {
|
|
214
218
|
const s1 = [m.team1?.id, m.team1?.slug, m.team1?.name].filter(Boolean).map((x) => String(x).toLowerCase());
|
|
215
219
|
const s2 = [m.team2?.id, m.team2?.slug, m.team2?.name].filter(Boolean).map((x) => String(x).toLowerCase());
|
|
@@ -223,7 +227,7 @@ export function composeH2H(game, sideA, sideB, rows, limit) {
|
|
|
223
227
|
return {
|
|
224
228
|
meetings: meetings.length,
|
|
225
229
|
lastMeetings: meetings.slice(0, 5),
|
|
226
|
-
recordNote: meetings.length ? `${meetings.length} meetings found in window` : 'No H2H meetings in window',
|
|
230
|
+
recordNote: meetings.length ? `${meetings.length} past meetings found in window` : 'No past H2H meetings in window',
|
|
227
231
|
};
|
|
228
232
|
}
|
|
229
233
|
export const matchPreview = {
|
|
@@ -434,6 +438,31 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
434
438
|
}
|
|
435
439
|
}
|
|
436
440
|
// Factual briefing lines only — never structural meta like "roster loaded".
|
|
441
|
+
// Called with teamA + teamB and no matchId, context stayed empty
|
|
442
|
+
// ({ matchId:null, event:null, startTime:null }) for a headliner whose
|
|
443
|
+
// own form list had all three. Side A's history carries the booked bout
|
|
444
|
+
// against side B; that IS the context.
|
|
445
|
+
if (!matchId && context.matchId == null) {
|
|
446
|
+
const bl = String(teamB ?? '').toLowerCase();
|
|
447
|
+
const booked = sideARes.recentForm.find((m) => {
|
|
448
|
+
if (!m || (m.status !== 'upcoming' && m.status !== 'live'))
|
|
449
|
+
return false;
|
|
450
|
+
const sides = [m.team1?.slug, m.team1?.id, m.team1?.name, m.team2?.slug, m.team2?.id, m.team2?.name]
|
|
451
|
+
.filter(Boolean).map((x) => String(x).toLowerCase());
|
|
452
|
+
return sides.some((x) => x === bl || x.includes(bl) || bl.includes(x));
|
|
453
|
+
});
|
|
454
|
+
if (booked) {
|
|
455
|
+
context = {
|
|
456
|
+
...context,
|
|
457
|
+
matchId: booked.matchId !== 'unknown' ? booked.matchId : null,
|
|
458
|
+
startTime: booked.startTime,
|
|
459
|
+
event: booked.event,
|
|
460
|
+
eventName: booked.event?.name ?? null,
|
|
461
|
+
status: booked.status,
|
|
462
|
+
label: booked.label,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
437
466
|
const talkingPoints = [];
|
|
438
467
|
const sideAName = pickString(asRecord(sideARes.entity)?.name, teamA) ?? teamA;
|
|
439
468
|
const sideBName = pickString(asRecord(sideBRes.entity)?.name, teamB) ?? teamB;
|
package/dist/tools/live.js
CHANGED
|
@@ -323,12 +323,37 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
323
323
|
: undefined;
|
|
324
324
|
const supervisor = normalized.length === 0 && events.length ? slimUfcSupervisorEvents(events) : undefined;
|
|
325
325
|
const nextCard = normalized.length === 0 ? slimUfcNextCard(payload) : undefined;
|
|
326
|
+
// A one-word answer to "is anything wrong?". health{} is the worker's
|
|
327
|
+
// raw view and on an idle day it reads ok:false / workerAlive:false,
|
|
328
|
+
// which looks broken. state says what the board means: idle (nothing
|
|
329
|
+
// on, nothing due), live, stale (a hot card with a dead feed), or
|
|
330
|
+
// degraded. nextEvent tells an idle board what comes next, fetched
|
|
331
|
+
// only when the board is empty so a live night pays nothing for it.
|
|
332
|
+
const state = normalized.length > 0 ? 'live'
|
|
333
|
+
: emptyReason === 'worker_stale' || emptyReason === 'worker_down_inferred' ? 'stale'
|
|
334
|
+
: emptyReason === 'event_degraded' ? 'degraded'
|
|
335
|
+
: 'idle';
|
|
336
|
+
let nextEvent;
|
|
337
|
+
if (state === 'idle') {
|
|
338
|
+
const up = await fetchJson(ctx, '/ufc/events/upcoming', { query: { limit: 1 } });
|
|
339
|
+
upstreamCalls += 1;
|
|
340
|
+
const first = asRecord(extractRows(up.ok ? up.data : null)[0]);
|
|
341
|
+
if (first) {
|
|
342
|
+
nextEvent = {
|
|
343
|
+
slug: pickString(first.slug) ?? null,
|
|
344
|
+
name: pickString(first.title, first.name) ?? null,
|
|
345
|
+
startsAt: pickString(first.startsAt, first.date, first.startTime) ?? null,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
326
349
|
sections.push({
|
|
327
350
|
game,
|
|
328
351
|
count: normalized.length,
|
|
329
352
|
ok: true,
|
|
353
|
+
state,
|
|
330
354
|
note,
|
|
331
355
|
emptyReason,
|
|
356
|
+
nextEvent,
|
|
332
357
|
health: healthStrip,
|
|
333
358
|
supervisor,
|
|
334
359
|
nextCard,
|
package/dist/tools/match.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
|
-
import { normalizeMatch } from './normalize.js';
|
|
6
|
+
import { normalizeMatch, normalizeUfcMethod } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, parseGame, stringSchema, } from './types.js';
|
|
8
8
|
async function getSection(ctx, path, query) {
|
|
9
9
|
return fetchJson(ctx, path, { query });
|
|
@@ -78,7 +78,10 @@ function matchCore(game, matchId, raw) {
|
|
|
78
78
|
team2: m.team2,
|
|
79
79
|
event: m.event,
|
|
80
80
|
league: m.league,
|
|
81
|
-
|
|
81
|
+
// One enum on every surface. match_summary carried the source spelling
|
|
82
|
+
// here while the profile carried the enum, so consumers normalised anyway.
|
|
83
|
+
method: (game === 'ufc' ? normalizeUfcMethod(pickString(r.method, r.resultMethod)) : pickString(r.method, r.resultMethod)) ?? null,
|
|
84
|
+
methodRaw: pickString(r.method, r.resultMethod) ?? null,
|
|
82
85
|
winner: (() => {
|
|
83
86
|
// Never name a winner while play is in progress: the score fallback
|
|
84
87
|
// below reads "who is ahead", which mid-match is a lead, not a result.
|
package/dist/tools/player.js
CHANGED
|
@@ -457,7 +457,9 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
457
457
|
if (res.ok) {
|
|
458
458
|
const metrics = unwrapPayload(res.data);
|
|
459
459
|
form = { summary: 'UFC fighter stats', trend: null, window: null, metrics };
|
|
460
|
-
seasonStats
|
|
460
|
+
// form.metrics and seasonStats were the same object twice, byte
|
|
461
|
+
// for byte. One copy. seasonStats stays for the games that have
|
|
462
|
+
// seasons; a fighter has a career, not a season.
|
|
461
463
|
}
|
|
462
464
|
else {
|
|
463
465
|
partial.push(partialFromRejection('form', {
|
|
@@ -551,15 +553,25 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
551
553
|
...(player.slug ? { slug: player.slug } : {}),
|
|
552
554
|
},
|
|
553
555
|
},
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
556
|
+
// Two rules, so a consumer can tell the difference. A field that can
|
|
557
|
+
// never apply to this game is OMITTED (a fighter has no team, no radar,
|
|
558
|
+
// no season). A field that applies but is unknown is NULL (division,
|
|
559
|
+
// flag). Sending team:null on every UFC profile forever said nothing.
|
|
560
|
+
data: game === 'ufc'
|
|
561
|
+
? {
|
|
562
|
+
player: (({ team, role, nationality, ...rest }) => { void team; void role; void nationality; return rest; })(player),
|
|
563
|
+
recentMatches,
|
|
564
|
+
form,
|
|
565
|
+
}
|
|
566
|
+
: {
|
|
567
|
+
player,
|
|
568
|
+
currentTeam,
|
|
569
|
+
recentMatches,
|
|
570
|
+
form,
|
|
571
|
+
career,
|
|
572
|
+
radar,
|
|
573
|
+
seasonStats,
|
|
574
|
+
},
|
|
563
575
|
});
|
|
564
576
|
},
|
|
565
577
|
};
|
package/dist/tools/standings.js
CHANGED
|
@@ -125,6 +125,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
125
125
|
stage: stringSchema('Stage key (COD / LoL).'),
|
|
126
126
|
division: stringSchema('UFC division key.'),
|
|
127
127
|
limit: limitSchema({ default: 50, max: 100 }),
|
|
128
|
+
cursor: stringSchema('Opaque cursor from pagination.nextCursor. UFC world scope spans every division; page with it rather than raising limit.'),
|
|
128
129
|
},
|
|
129
130
|
},
|
|
130
131
|
handler: async (args, ctx) => {
|
|
@@ -143,6 +144,9 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
143
144
|
}
|
|
144
145
|
const game = gameParse.game;
|
|
145
146
|
const limit = clampInt(args.limit, 50, 1, 100);
|
|
147
|
+
// Cursor is a plain offset. The whole table is in memory (UFC world scope
|
|
148
|
+
// is 176 rows), so nothing fancier is needed to walk every division.
|
|
149
|
+
const offset = Math.max(0, Number.parseInt(String(args.cursor ?? '0'), 10) || 0);
|
|
146
150
|
const scope = typeof args.scope === 'string' ? args.scope : undefined;
|
|
147
151
|
const leagueId = typeof args.leagueId === 'string' ? args.leagueId : undefined;
|
|
148
152
|
const tournamentId = typeof args.tournamentId === 'string' ? args.tournamentId : undefined;
|
|
@@ -321,7 +325,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
321
325
|
// so limit=50 stops a third of the way through flyweight; a consumer needs
|
|
322
326
|
// to know that happened rather than receive total:null and guess.
|
|
323
327
|
let allRows = extractRows(res.data).map((row, i) => normalizeStandingRow(row, i));
|
|
324
|
-
let rows = allRows.slice(
|
|
328
|
+
let rows = allRows.slice(offset, offset + limit);
|
|
325
329
|
// UFC rankings may be nested by division
|
|
326
330
|
if (game === 'ufc' && rows.length === 0) {
|
|
327
331
|
const obj = asRecord(res.data) ?? {};
|
|
@@ -334,8 +338,29 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
334
338
|
}
|
|
335
339
|
}
|
|
336
340
|
allRows = nested.map((row, i) => normalizeStandingRow(row, i));
|
|
337
|
-
rows = allRows.slice(
|
|
341
|
+
rows = allRows.slice(offset, offset + limit);
|
|
338
342
|
}
|
|
343
|
+
if (game === 'ufc') {
|
|
344
|
+
rows = rows.map((row) => {
|
|
345
|
+
const { played, wins, losses, draws, points, mapDiff, streak, ...rest } = row;
|
|
346
|
+
void played;
|
|
347
|
+
void wins;
|
|
348
|
+
void losses;
|
|
349
|
+
void draws;
|
|
350
|
+
void points;
|
|
351
|
+
void mapDiff;
|
|
352
|
+
void streak;
|
|
353
|
+
return rest;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const standingsPagination = {
|
|
357
|
+
limit,
|
|
358
|
+
offset,
|
|
359
|
+
total: allRows.length,
|
|
360
|
+
hasMore: offset + rows.length < allRows.length,
|
|
361
|
+
nextCursor: offset + rows.length < allRows.length ? String(offset + rows.length) : null,
|
|
362
|
+
prevCursor: offset > 0 ? String(Math.max(0, offset - limit)) : null,
|
|
363
|
+
};
|
|
339
364
|
const obj = asRecord(res.data);
|
|
340
365
|
const upstreamMeta = asRecord(obj?.meta) ?? {};
|
|
341
366
|
const dataQuality = asRecord(upstreamMeta.dataQuality) ?? {};
|
|
@@ -364,6 +389,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
364
389
|
: 'Rankings dataFreshness=cached or stale.');
|
|
365
390
|
}
|
|
366
391
|
return successEnvelope({
|
|
392
|
+
pagination: standingsPagination,
|
|
367
393
|
source: 'standings',
|
|
368
394
|
game,
|
|
369
395
|
requestId,
|
package/dist/tools/team.js
CHANGED
|
@@ -920,6 +920,9 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
920
920
|
}
|
|
921
921
|
const meetings = rows
|
|
922
922
|
.map((row) => normalizeMatch(game, row))
|
|
923
|
+
// Past fights only. The booked bout between the two sides is in the
|
|
924
|
+
// same list and is not a meeting yet.
|
|
925
|
+
.filter((m) => m.status === 'completed')
|
|
923
926
|
.filter((m) => sidesMatch(m, matchA, matchB))
|
|
924
927
|
.filter((m) => {
|
|
925
928
|
if (!fromIso && !toIso)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cito-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
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": {
|