cito-mcp 0.3.9 → 0.3.11

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.
@@ -426,8 +426,31 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
426
426
  // also moves paging client-side, because an upstream offset would be
427
427
  // counted against rows we are about to discard.
428
428
  let clientTeamFilter = false;
429
+ // Set when this tool, not the upstream, is responsible for paging.
430
+ let clientDatePaging = false;
429
431
  let query = { limit, offset };
430
- if (game === 'lol') {
432
+ lolBranch: if (game === 'lol') {
433
+ // from/to means a date WINDOW, which /lol/schedule/upcoming cannot serve:
434
+ // it only knows `hours` forward from now, so asking for last month
435
+ // silently returned next week's fixtures with an "ignored filter"
436
+ // warning. /lol/schedule does accept from/to, so route there instead and
437
+ // let callers list completed matches.
438
+ if (from || to) {
439
+ path = '/lol/schedule';
440
+ clientDatePaging = true;
441
+ query = {
442
+ limit: String(Math.min(200, (offset + limit) * 4)),
443
+ ...(from ? { from } : {}),
444
+ ...(to ? { to } : {}),
445
+ ...(league ? { leagueSlug: league } : {}),
446
+ ...(team ? { teamSlug: team } : {}),
447
+ };
448
+ if (team)
449
+ clientTeamFilter = true;
450
+ if (tournamentId)
451
+ noteIgnored('tournamentId', 'use league filter or standings for LoL tournaments');
452
+ break lolBranch;
453
+ }
431
454
  path = '/lol/schedule/upcoming';
432
455
  // The upstream accepts teamSlug but does not apply it: asking for t1
433
456
  // returned Dplus KIA, Team WE and NightBirds. That is a silently wrong
@@ -437,10 +460,13 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
437
460
  // here instead.
438
461
  if (team)
439
462
  clientTeamFilter = true;
463
+ // The upstream accepts `offset` and ignores it, so page two came back
464
+ // byte for byte identical to page one while the cursor advanced in the
465
+ // metadata. Over-fetch and slice here instead of trusting it.
466
+ clientDatePaging = true;
440
467
  query = {
441
468
  hours: String(hours),
442
- limit: String(clientTeamFilter ? Math.min(200, (offset + limit) * 4) : limit),
443
- ...(clientTeamFilter ? {} : { offset: String(offset) }),
469
+ limit: String(Math.min(200, (offset + limit) * 4)),
444
470
  ...(league ? { leagueSlug: league } : {}),
445
471
  ...(team ? { teamSlug: team } : {}),
446
472
  };
@@ -664,8 +690,13 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
664
690
  if (team && beforeTeamFilter > 0 && rows.length === 0) {
665
691
  warnings.push(`team "${team}" matched 0 of ${beforeTeamFilter} upcoming fixtures in this window — widen hours or check the slug`);
666
692
  }
667
- const items = (clientTeamFilter ? rows.slice(offset, offset + limit) : rows.slice(0, limit))
668
- .map((row) => normalizeMatch(game, row, 'upcoming'));
693
+ const pageHere = clientTeamFilter || clientDatePaging;
694
+ const windowed = pageHere
695
+ ? rows.slice(Number(offset), Number(offset) + Number(limit))
696
+ : rows.slice(0, limit);
697
+ // A date window can contain finished matches, so do not force "upcoming"
698
+ // onto rows that already carry a real state.
699
+ const items = windowed.map((row) => from || to ? normalizeMatch(game, row) : normalizeMatch(game, row, 'upcoming'));
669
700
  const obj = asRecord(res.data);
670
701
  const total = typeof obj?.total === 'number' ? obj.total : null;
671
702
  const hasMore = (typeof obj?.hasMore === 'boolean' ? obj.hasMore : items.length >= limit) ||
@@ -481,7 +481,11 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
481
481
  media: null,
482
482
  advanced: null,
483
483
  };
484
- // base is primary
484
+ // base is ALWAYS fetched, whatever sections were asked for. Requesting
485
+ // sections:["timeline"] returned match:null, so a caller got a timeline with
486
+ // nothing to attach it to and no way to tell which match it belonged to.
487
+ // The header is the identity of the response, not an optional section.
488
+ wanted.add('base');
485
489
  if (wanted.has('base')) {
486
490
  const res = await getSection(ctx, primaryPath(game, matchId));
487
491
  upstreamCalls += 1;
@@ -8,8 +8,15 @@ import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, stringSc
8
8
  function teamIdentity(game, raw, idHint, slugHint) {
9
9
  const r = asRecord(raw) ?? {};
10
10
  const nested = asRecord(r.data) ?? r;
11
- const id = pickString(nested.id, nested.teamId, idHint, slugHint) ?? 'unknown';
12
11
  const slug = pickString(nested.slug, nested.orgSlug, slugHint);
12
+ // When the caller asks for an alias the API resolves it, and the resolved
13
+ // slug is the truth. Falling back to the requested value for `id` produced
14
+ // id:"t1a" beside slug:"t1-challengers", a row that disagreed with itself.
15
+ const resolved = pickString(nested.slug, nested.orgSlug);
16
+ const id = pickString(nested.id, nested.teamId) ??
17
+ resolved ??
18
+ pickString(idHint, slugHint) ??
19
+ 'unknown';
13
20
  const name = pickString(nested.name, nested.tag, nested.code, slug, id) ?? id;
14
21
  return {
15
22
  id,
@@ -301,8 +308,26 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
301
308
  const t = Date.parse(String(row?.startTime ?? ''));
302
309
  return Number.isFinite(t) ? t : -Infinity;
303
310
  };
311
+ // RECENT means finished AND in the past. The index also carries
312
+ // scheduled fixtures, and this path forced every row to
313
+ // "completed", so Gen.G and HLE both listed their 2026-09-05
314
+ // playoff match as a completed result days before it was played.
315
+ const nowMs = Date.now();
316
+ const isFinished = (row) => {
317
+ const r = asRecord(row) ?? {};
318
+ const t = Date.parse(String(r.startTime ?? ''));
319
+ if (Number.isFinite(t) && t > nowMs)
320
+ return false;
321
+ const state = String(r.state ?? r.status ?? '').toLowerCase();
322
+ if (/unstarted|scheduled|upcoming|not_?started|in_?progress|live/.test(state))
323
+ return false;
324
+ // No usable state: fall back to the clock plus a decided result.
325
+ if (!state)
326
+ return Number.isFinite(t) && t <= nowMs;
327
+ return /complete|finish|final|ended/.test(state);
328
+ };
304
329
  recentMatches = extractRows(res.data)
305
- .slice()
330
+ .filter(isFinished)
306
331
  .sort((a, b) => startMs(b) - startMs(a))
307
332
  .slice(0, recentLimit)
308
333
  .map((row) => normalizeMatch('lol', row, 'completed'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cito-mcp",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Standalone MCP server for the Cito esports API — 15 curated outcome tools for agents (live, schedule, profiles, standings, previews, event cards).",
5
5
  "type": "module",
6
6
  "bin": {