cito-mcp 0.5.8 → 0.6.1

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.
@@ -26,6 +26,9 @@ async function loadSide(ctx, game, side, recentLimit, includeRosters) {
26
26
  hand: pickString(data.hand) ?? null,
27
27
  age: typeof data.age === 'number' ? data.age : null,
28
28
  currentRank: typeof data.current_rank === 'number' ? data.current_rank : null,
29
+ lastRank: typeof data.last_rank === 'number' ? data.last_rank : null,
30
+ lastRankDate: pickString(data.last_rank_date) ?? null,
31
+ status: pickString(data.status) ?? null,
29
32
  careerHighRank: typeof data.career_high_rank === 'number' ? data.career_high_rank : null,
30
33
  careerRecord: career.matches_won != null && career.matches_lost != null
31
34
  ? `${career.matches_won}-${career.matches_lost}`
@@ -272,7 +275,7 @@ Prefer match_summary when match is completed; match_details for live in-game.
272
275
 
273
276
  Do not use when: user wants final score/recap of a finished match.
274
277
 
275
- Parallel-safe: yes. Upstream cost: 4–8.
278
+ Parallel-safe: yes. API calls: 4–8.
276
279
  Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
277
280
  Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "recentLimit": 5 }`,
278
281
  inputSchema: {
@@ -340,7 +343,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
340
343
  });
341
344
  }
342
345
  const partial = [];
343
- let upstreamCalls = 0;
346
+ let apiCalls = 0;
344
347
  let rateLimit = {};
345
348
  let context = {
346
349
  matchId: matchId || null,
@@ -364,7 +367,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
364
367
  else if (game === 'tennis')
365
368
  path = `/tennis/matches/${encodeURIComponent(matchId)}`;
366
369
  const res = await fetchJson(ctx, path);
367
- upstreamCalls += 1;
370
+ apiCalls += 1;
368
371
  rateLimit = res.headers;
369
372
  if (res.ok) {
370
373
  const entity = unwrapPayload(res.data);
@@ -408,14 +411,14 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
408
411
  source: 'match_preview',
409
412
  requestId,
410
413
  tookMs: Date.now() - started,
411
- upstreamCalls,
414
+ apiCalls,
412
415
  });
413
416
  }
414
417
  const [sideARes, sideBRes] = await Promise.all([
415
418
  loadSide(ctx, game, teamA, recentLimit, includeRosters),
416
419
  loadSide(ctx, game, teamB, recentLimit, includeRosters),
417
420
  ]);
418
- upstreamCalls += sideARes.calls + sideBRes.calls;
421
+ apiCalls += sideARes.calls + sideBRes.calls;
419
422
  rateLimit = { ...rateLimit, ...sideARes.rateLimit, ...sideBRes.rateLimit };
420
423
  partial.push(...sideARes.partial.map((p) => ({ ...p, section: `sideA.${p.section}` })));
421
424
  partial.push(...sideBRes.partial.map((p) => ({ ...p, section: `sideB.${p.section}` })));
@@ -425,7 +428,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
425
428
  let rows = [];
426
429
  if (game === 'cs2') {
427
430
  const res = await fetchJson(ctx, '/cs2/matches', { query: { team: teamA, limit: 40 } });
428
- upstreamCalls += 1;
431
+ apiCalls += 1;
429
432
  rateLimit = { ...rateLimit, ...res.headers };
430
433
  if (res.ok)
431
434
  rows = extractRows(res.data);
@@ -441,21 +444,21 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
441
444
  const res = await fetchJson(ctx, '/lol/schedule', {
442
445
  query: { teamSlug: teamA, completed: 'true', limit: '40' },
443
446
  });
444
- upstreamCalls += 1;
447
+ apiCalls += 1;
445
448
  rateLimit = { ...rateLimit, ...res.headers };
446
449
  if (res.ok)
447
450
  rows = extractRows(res.data);
448
451
  }
449
452
  else if (game === 'cod') {
450
453
  const res = await fetchJson(ctx, '/cod/matches', { query: { team: teamA, limit: 40 } });
451
- upstreamCalls += 1;
454
+ apiCalls += 1;
452
455
  rateLimit = { ...rateLimit, ...res.headers };
453
456
  if (res.ok)
454
457
  rows = extractRows(res.data);
455
458
  }
456
459
  else if (game === 'dota2') {
457
460
  const res = await fetchJson(ctx, '/dota2/matches/recent', { query: { limit: 40 } });
458
- upstreamCalls += 1;
461
+ apiCalls += 1;
459
462
  rateLimit = { ...rateLimit, ...res.headers };
460
463
  if (res.ok)
461
464
  rows = extractRows(res.data);
@@ -465,7 +468,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
465
468
  if (game === 'tennis' && surface)
466
469
  h2hQuery.surface = surface;
467
470
  const h2hRes = await fetchJson(ctx, '/tennis/h2h', { query: h2hQuery });
468
- upstreamCalls += 1;
471
+ apiCalls += 1;
469
472
  rateLimit = { ...rateLimit, ...h2hRes.headers };
470
473
  if (h2hRes.ok) {
471
474
  const rec = asRecord(unwrapPayload(h2hRes.data)) ?? {};
@@ -500,7 +503,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
500
503
  const hist = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(teamA)}/fights`, {
501
504
  query: { limit: 50 },
502
505
  });
503
- upstreamCalls += 1;
506
+ apiCalls += 1;
504
507
  rateLimit = { ...rateLimit, ...hist.headers };
505
508
  if (hist.ok) {
506
509
  rows = extractRows(unwrapPayload(hist.data) ?? hist.data).map((row) => {
@@ -520,7 +523,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
520
523
  }
521
524
  catch (e) {
522
525
  partial.push(partialFromRejection('h2h', {
523
- code: 'UPSTREAM',
526
+ code: 'SERVICE_ERROR',
524
527
  message: e.message,
525
528
  }));
526
529
  }
@@ -593,7 +596,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
593
596
  game,
594
597
  requestId,
595
598
  tookMs: Date.now() - started,
596
- upstreamCalls,
599
+ apiCalls,
597
600
  rateLimit,
598
601
  partial: partial.length ? partial : undefined,
599
602
  entities: {
@@ -718,7 +721,7 @@ Bouts come back in card order — main event first, then prelims, then early pre
718
721
  Each bout carries weightClass, titleBout, card placement, and (once fought) result
719
722
  { method, round, time, referee, winnerSlug }. Each corner carries images
720
723
  { headshotUrl, bodyImageUrl, imageUrl, proxiedImageUrl }, record, nickname, rank,
721
- championStatus, country and flag when upstream supplies them. Use proxiedImageUrl in
724
+ championStatus, country and flag when available. Use proxiedImageUrl in
722
725
  browsers — the image host sends no CORS header. You do not need call_api per fighter for faces.
723
726
 
724
727
  Prefer over: agent-side resolve + call_api /ufc/events + bout expansion; N+1 match_summary for the card list only; per-fighter call_api just to fetch headshots.
@@ -730,14 +733,14 @@ Do not use when: you only need live scores (live_matches); single finished match
730
733
  Tennis: pass a tournament id/slug to get the draw bracket — every main round from
731
734
  the first to the Final (status completed / in_progress / not_started), then
732
735
  qualifying (Q1..Q3). Each result carries winnerId/loserId/outcome (retirements
733
- included), startTime (null with startTimeUnavailableReason when the source has no
736
+ included), startTime (null with startTimeUnavailableReason when no start time is
734
737
  clock), nextMatchId (the bracket edge) and bracketPosition; scheduled and live
735
738
  matches sit in rounds[].pending. draw carries drawSize (real field size, byes
736
739
  excluded, e.g. 56), byes, matchesExpected (main draw) and complete (true only
737
740
  when every main round to the Final is played). This is the primary tennis path
738
741
  here; there is no separate draw tool.
739
742
 
740
- Parallel-safe: yes. Upstream cost: 1–4.
743
+ Parallel-safe: yes. API calls: 1–4.
741
744
  Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "includeStandings": false }`,
742
745
  inputSchema: {
743
746
  type: 'object',
@@ -751,7 +754,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
751
754
  includeMatches: boolSchema('Include the bout/match list, or the tennis draw bracket (default true).', true),
752
755
  includeStandings: boolSchema('Include standings/rankings snippet when API supports event/tournament/division scope (default false). ' +
753
756
  'For UFC this also joins divisional rank and movement onto each bout corner as team.rank / team.rankMovement — ' +
754
- 'bout rows themselves carry no rank. Costs one extra upstream call.', false),
757
+ 'bout rows themselves carry no rank. Costs one extra API call.', false),
755
758
  limit: limitSchema({
756
759
  default: 20,
757
760
  max: 50,
@@ -799,12 +802,12 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
799
802
  });
800
803
  }
801
804
  const partial = [];
802
- let upstreamCalls = 0;
805
+ let apiCalls = 0;
803
806
  let rateLimit = {};
804
807
  const warnings = [];
805
808
  if (!eventKey && q) {
806
809
  const resolved = await resolveEventKey(ctx, game, q);
807
- upstreamCalls += resolved.calls;
810
+ apiCalls += resolved.calls;
808
811
  rateLimit = { ...rateLimit, ...resolved.rateLimit };
809
812
  if (resolved.key) {
810
813
  eventKey = resolved.key;
@@ -817,7 +820,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
817
820
  source: 'event_card',
818
821
  requestId,
819
822
  tookMs: Date.now() - started,
820
- upstreamCalls,
823
+ apiCalls,
821
824
  rateLimit,
822
825
  recover: [
823
826
  'Call resolve_entity { type: "event", game, q } or search_entities, then retry with eventIdOrSlug',
@@ -856,7 +859,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
856
859
  };
857
860
  if (game === 'ufc') {
858
861
  const detail = await fetchJson(ctx, `/ufc/events/${encodeURIComponent(eventKey)}`);
859
- upstreamCalls += 1;
862
+ apiCalls += 1;
860
863
  rateLimit = { ...rateLimit, ...detail.headers };
861
864
  if (detail.ok) {
862
865
  const data = asRecord(detail.data) ?? {};
@@ -891,7 +894,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
891
894
  source: 'event_card',
892
895
  requestId,
893
896
  tookMs: Date.now() - started,
894
- upstreamCalls,
897
+ apiCalls,
895
898
  rateLimit,
896
899
  httpStatus: 404,
897
900
  recover: [
@@ -908,14 +911,14 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
908
911
  source: 'event_card',
909
912
  requestId,
910
913
  tookMs: Date.now() - started,
911
- upstreamCalls,
914
+ apiCalls,
912
915
  rateLimit,
913
916
  httpStatus: detail.status,
914
917
  });
915
918
  }
916
919
  if (includeBouts && bouts.length === 0) {
917
920
  const boutsRes = await fetchJson(ctx, `/ufc/events/${encodeURIComponent(String(event.slug ?? eventKey))}/bouts`);
918
- upstreamCalls += 1;
921
+ apiCalls += 1;
919
922
  rateLimit = { ...rateLimit, ...boutsRes.headers };
920
923
  if (boutsRes.ok) {
921
924
  bouts = sortByCardOrder(extractRows(boutsRes.data).map((row) => normalizeMatch('ufc', {
@@ -937,7 +940,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
937
940
  bouts = bouts.map((b) => ({ ...b, startTime: b.startTime ?? cardStart }));
938
941
  if (includeStandings) {
939
942
  const ranks = await fetchJson(ctx, '/ufc/rankings');
940
- upstreamCalls += 1;
943
+ apiCalls += 1;
941
944
  rateLimit = { ...rateLimit, ...ranks.headers };
942
945
  if (ranks.ok) {
943
946
  const rankRows = extractRows(ranks.data)
@@ -1006,7 +1009,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1006
1009
  }
1007
1010
  else if (game === 'cs2') {
1008
1011
  const detail = await fetchJson(ctx, `/cs2/events/${encodeURIComponent(eventKey)}`);
1009
- upstreamCalls += 1;
1012
+ apiCalls += 1;
1010
1013
  rateLimit = { ...rateLimit, ...detail.headers };
1011
1014
  if (detail.ok) {
1012
1015
  const data = asRecord(detail.data) ?? {};
@@ -1028,7 +1031,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1028
1031
  source: 'event_card',
1029
1032
  requestId,
1030
1033
  tookMs: Date.now() - started,
1031
- upstreamCalls,
1034
+ apiCalls,
1032
1035
  rateLimit,
1033
1036
  httpStatus: 404,
1034
1037
  });
@@ -1045,7 +1048,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1045
1048
  const matchesRes = await fetchJson(ctx, '/cs2/matches', {
1046
1049
  query: { event: eventKey, limit, page: 1 },
1047
1050
  });
1048
- upstreamCalls += 1;
1051
+ apiCalls += 1;
1049
1052
  rateLimit = { ...rateLimit, ...matchesRes.headers };
1050
1053
  if (matchesRes.ok) {
1051
1054
  bouts = extractRows(matchesRes.data)
@@ -1062,7 +1065,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1062
1065
  }
1063
1066
  if (includeStandings) {
1064
1067
  const st = await fetchJson(ctx, `/cs2/events/${encodeURIComponent(eventKey)}/standings`);
1065
- upstreamCalls += 1;
1068
+ apiCalls += 1;
1066
1069
  rateLimit = { ...rateLimit, ...st.headers };
1067
1070
  if (st.ok)
1068
1071
  standingsSnippet = { scope: 'event', raw: st.data };
@@ -1077,7 +1080,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1077
1080
  }
1078
1081
  else if (game === 'lol') {
1079
1082
  const detail = await fetchJson(ctx, `/lol/tournaments/${encodeURIComponent(eventKey)}`);
1080
- upstreamCalls += 1;
1083
+ apiCalls += 1;
1081
1084
  rateLimit = { ...rateLimit, ...detail.headers };
1082
1085
  if (detail.ok) {
1083
1086
  const data = asRecord(detail.data) ?? {};
@@ -1105,7 +1108,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1105
1108
  source: 'event_card',
1106
1109
  requestId,
1107
1110
  tookMs: Date.now() - started,
1108
- upstreamCalls,
1111
+ apiCalls,
1109
1112
  rateLimit,
1110
1113
  httpStatus: 404,
1111
1114
  });
@@ -1115,7 +1118,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1115
1118
  const sched = await fetchJson(ctx, '/lol/schedule', {
1116
1119
  query: { tournamentId: eventKey, limit: String(limit) },
1117
1120
  });
1118
- upstreamCalls += 1;
1121
+ apiCalls += 1;
1119
1122
  rateLimit = { ...rateLimit, ...sched.headers };
1120
1123
  if (sched.ok) {
1121
1124
  bouts = extractRows(sched.data)
@@ -1132,7 +1135,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1132
1135
  }
1133
1136
  if (includeStandings) {
1134
1137
  const st = await fetchJson(ctx, `/lol/tournaments/${encodeURIComponent(eventKey)}/standings`);
1135
- upstreamCalls += 1;
1138
+ apiCalls += 1;
1136
1139
  rateLimit = { ...rateLimit, ...st.headers };
1137
1140
  if (st.ok)
1138
1141
  standingsSnippet = { scope: 'tournament', raw: st.data };
@@ -1147,7 +1150,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1147
1150
  }
1148
1151
  else if (game === 'cod') {
1149
1152
  const detail = await fetchJson(ctx, `/cod/tournaments/${encodeURIComponent(eventKey)}`);
1150
- upstreamCalls += 1;
1153
+ apiCalls += 1;
1151
1154
  rateLimit = { ...rateLimit, ...detail.headers };
1152
1155
  if (detail.ok) {
1153
1156
  const data = asRecord(detail.data) ?? {};
@@ -1167,7 +1170,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1167
1170
  source: 'event_card',
1168
1171
  requestId,
1169
1172
  tookMs: Date.now() - started,
1170
- upstreamCalls,
1173
+ apiCalls,
1171
1174
  rateLimit,
1172
1175
  httpStatus: 404,
1173
1176
  });
@@ -1183,7 +1186,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1183
1186
  const matchesRes = await fetchJson(ctx, '/cod/matches', {
1184
1187
  query: { tournamentId: eventKey, limit },
1185
1188
  });
1186
- upstreamCalls += 1;
1189
+ apiCalls += 1;
1187
1190
  rateLimit = { ...rateLimit, ...matchesRes.headers };
1188
1191
  if (matchesRes.ok) {
1189
1192
  bouts = extractRows(matchesRes.data)
@@ -1200,7 +1203,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1200
1203
  }
1201
1204
  if (includeStandings) {
1202
1205
  const st = await fetchJson(ctx, '/cod/standings', { query: { tournamentId: eventKey } });
1203
- upstreamCalls += 1;
1206
+ apiCalls += 1;
1204
1207
  rateLimit = { ...rateLimit, ...st.headers };
1205
1208
  if (st.ok)
1206
1209
  standingsSnippet = { scope: 'tournament', raw: st.data };
@@ -1215,7 +1218,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1215
1218
  }
1216
1219
  else if (game === 'tennis') {
1217
1220
  const detail = await fetchJson(ctx, `/tennis/tournaments/${encodeURIComponent(eventKey)}`);
1218
- upstreamCalls += 1;
1221
+ apiCalls += 1;
1219
1222
  rateLimit = { ...rateLimit, ...detail.headers };
1220
1223
  if (detail.ok) {
1221
1224
  const d = asRecord(unwrapPayload(detail.data)) ?? {};
@@ -1246,7 +1249,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1246
1249
  }
1247
1250
  if (includeBouts) {
1248
1251
  const draw = await fetchJson(ctx, `/tennis/tournaments/${encodeURIComponent(eventKey)}/draw`);
1249
- upstreamCalls += 1;
1252
+ apiCalls += 1;
1250
1253
  rateLimit = { ...rateLimit, ...draw.headers };
1251
1254
  if (draw.ok) {
1252
1255
  const dd = asRecord(unwrapPayload(draw.data)) ?? {};
@@ -1290,7 +1293,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1290
1293
  ? { startTimeUnavailableReason: pickString(mr.start_time_unavailable_reason) }
1291
1294
  : {}),
1292
1295
  nextMatchId: pickString(mr.next_match_id) ?? null,
1293
- ...(pickString(mr.next_match_source) ? { nextMatchSource: pickString(mr.next_match_source) } : {}),
1296
+ ...(pickString(mr.next_match_source) ? { nextMatchBasis: pickString(mr.next_match_source)?.replace(/^source_/, '') } : {}),
1294
1297
  bracketPosition: num(mr.bracket_position),
1295
1298
  nextSlot: asRecord(mr.next_slot)
1296
1299
  ? {
@@ -1298,7 +1301,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1298
1301
  position: num(asRecord(mr.next_slot)?.position),
1299
1302
  }
1300
1303
  : null,
1301
- ...(pickString(mr.source_round) ? { sourceRound: pickString(mr.source_round) } : {}),
1304
+ ...(pickString(mr.source_round) ? { rawRound: pickString(mr.source_round) } : {}),
1302
1305
  }, 'tennis');
1303
1306
  };
1304
1307
  const pendingMatch = (pr) => {
@@ -1315,7 +1318,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1315
1318
  startTimeKnown: pr.start_time_known === true,
1316
1319
  liveScore: pickString(pr.live_score) ?? null,
1317
1320
  winnerId: null,
1318
- ...(pickString(pr.source_winner_id) ? { sourceWinnerId: pickString(pr.source_winner_id) } : {}),
1321
+ ...(pickString(pr.source_winner_id) ? { reportedWinnerId: pickString(pr.source_winner_id) } : {}),
1319
1322
  nextMatchId: pickString(pr.next_match_id) ?? null,
1320
1323
  bracketPosition: num(pr.bracket_position),
1321
1324
  };
@@ -1397,7 +1400,6 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1397
1400
  }
1398
1401
  drawSummary = {
1399
1402
  drawSize,
1400
- drawSizeSource: pickString(dd.draw_size_source) ?? null,
1401
1403
  drawSizeVerified: typeof dd.draw_size_verified === 'boolean' ? dd.draw_size_verified : null,
1402
1404
  drawSizeDerived: num(dd.draw_size_derived),
1403
1405
  bracketSize: num(dd.bracket_size),
@@ -1441,7 +1443,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1441
1443
  else {
1442
1444
  warnings.push('Dota2 event_card is thin; expect partial bouts/standings');
1443
1445
  const detail = await fetchJson(ctx, `/dota2/tournaments/${encodeURIComponent(eventKey)}`);
1444
- upstreamCalls += 1;
1446
+ apiCalls += 1;
1445
1447
  rateLimit = { ...rateLimit, ...detail.headers };
1446
1448
  if (detail.ok) {
1447
1449
  const data = asRecord(detail.data) ?? {};
@@ -1463,7 +1465,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1463
1465
  }
1464
1466
  if (includeBouts) {
1465
1467
  const matchesRes = await fetchJson(ctx, '/dota2/matches/upcoming', { query: { limit } });
1466
- upstreamCalls += 1;
1468
+ apiCalls += 1;
1467
1469
  rateLimit = { ...rateLimit, ...matchesRes.headers };
1468
1470
  if (matchesRes.ok) {
1469
1471
  const key = eventKey.toLowerCase();
@@ -1502,7 +1504,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1502
1504
  game,
1503
1505
  requestId,
1504
1506
  tookMs: Date.now() - started,
1505
- upstreamCalls,
1507
+ apiCalls,
1506
1508
  rateLimit,
1507
1509
  warnings: warnings.length ? warnings : undefined,
1508
1510
  partial: partial.length ? partial : undefined,
@@ -53,7 +53,7 @@ Do not use when: ranking position → standings with game tennis; week-over-week
53
53
 
54
54
  Tennis-only. Season defaults to 2026; tour (ATP|WTA) is optional — omit it for the combined board. Rows carry aces + matches played, ordered most aces first.
55
55
 
56
- Parallel-safe: yes. Upstream cost: 1.`,
56
+ Parallel-safe: yes. API calls: 1.`,
57
57
  inputSchema: {
58
58
  type: 'object',
59
59
  additionalProperties: false,
@@ -139,7 +139,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
139
139
  source: 'leaderboard_aces',
140
140
  requestId,
141
141
  tookMs: Date.now() - started,
142
- upstreamCalls: 1,
142
+ apiCalls: 1,
143
143
  httpStatus: res.status,
144
144
  rateLimit: res.headers,
145
145
  });
@@ -161,7 +161,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
161
161
  game,
162
162
  requestId,
163
163
  tookMs: Date.now() - started,
164
- upstreamCalls: 1,
164
+ apiCalls: 1,
165
165
  rateLimit: res.headers,
166
166
  data: {
167
167
  title: `${tourParse.tour ?? 'Combined'} aces leaderboard ${seasonParse.season}`,
@@ -205,7 +205,7 @@ Do not use when: ranking position → standings with game tennis; week-over-week
205
205
 
206
206
  Tennis-only. Season defaults to 2026; tour (ATP|WTA) is optional — omit it for the combined board. ${spec.minAttemptsNote}
207
207
 
208
- Parallel-safe: yes. Upstream cost: 1.`,
208
+ Parallel-safe: yes. API calls: 1.`,
209
209
  inputSchema: {
210
210
  type: 'object',
211
211
  additionalProperties: false,
@@ -291,7 +291,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
291
291
  source: spec.name,
292
292
  requestId,
293
293
  tookMs: Date.now() - started,
294
- upstreamCalls: 1,
294
+ apiCalls: 1,
295
295
  httpStatus: res.status,
296
296
  rateLimit: res.headers,
297
297
  });
@@ -313,7 +313,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
313
313
  game,
314
314
  requestId,
315
315
  tookMs: Date.now() - started,
316
- upstreamCalls: 1,
316
+ apiCalls: 1,
317
317
  rateLimit: res.headers,
318
318
  data: {
319
319
  title: `${tourParse.tour ?? 'Combined'} ${spec.titleNoun} leaderboard ${seasonParse.season}`,
@@ -110,9 +110,9 @@ export function humanUfcEmptyNote(emptyReason, health) {
110
110
  supervisor_warming: 'Card open / pre-gate; no bout live yet',
111
111
  between_bouts: 'Between bouts; next may be armed',
112
112
  armed_only: 'Next bout armed; not started',
113
- worker_stale: 'Live feed is behind; scores may lag',
114
- worker_down_inferred: 'Live feed unavailable right now',
115
- event_degraded: 'Sources empty clock / event degraded; not inventing stats',
113
+ worker_stale: 'Live data is behind; scores may lag',
114
+ worker_down_inferred: 'Live data unavailable right now',
115
+ event_degraded: 'Live clock unavailable; not inventing stats',
116
116
  };
117
117
  const msg = human[reason] || `No live|watching bouts (${reason})`;
118
118
  return `UFC: ${msg} (${workerPart}${lagPart}; emptyReason=${reason})`;
@@ -154,20 +154,21 @@ function slimUfcNextCard(payload) {
154
154
  }
155
155
  export const liveMatches = {
156
156
  name: 'live_matches',
157
- description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
158
-
159
- When to use:
160
- - "What's live right now?"
161
- - Ops/dashboard live strip
162
-
163
- Prefer over: sequential per-game call_api live probes.
164
-
165
- Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
166
-
167
- CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
168
- UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
169
-
170
- Parallel-safe: yes. Upstream cost: 1–5 (allSettled).
157
+ description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
158
+
159
+ When to use:
160
+ - "What's live right now?"
161
+ - Ops/dashboard live strip
162
+
163
+ Prefer over: sequential per-game call_api live probes.
164
+
165
+ Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
166
+
167
+ CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
168
+ Tennis: ITF and junior rows have no serve statistics (stats_unavailable_code), so a missing stat there is expected. For push updates point the user at wss://api.citoapi.com/api/v1/tennis/live/ws.
169
+ UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
170
+
171
+ Parallel-safe: yes. API calls: 1–5 (allSettled).
171
172
  Example: { "game": "all", "limitPerGame": 10 }`,
172
173
  inputSchema: {
173
174
  type: 'object',
@@ -202,7 +203,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
202
203
  const limitPerGame = clampInt(args.limitPerGame, 10, 1, 25);
203
204
  const labelsOnly = args.includeLabelsOnly === true;
204
205
  const partial = [];
205
- let upstreamCalls = 0;
206
+ let apiCalls = 0;
206
207
  let rateLimit = {};
207
208
  const settled = await Promise.all(games.map(async (game) => {
208
209
  const path = LIVE_PATHS[game];
@@ -212,7 +213,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
212
213
  const sections = [];
213
214
  const warnings = [];
214
215
  for (const { game, res } of settled) {
215
- upstreamCalls += 1;
216
+ apiCalls += 1;
216
217
  rateLimit = { ...rateLimit, ...res.headers };
217
218
  if (!res.ok) {
218
219
  const code = mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) });
@@ -261,9 +262,9 @@ Example: { "game": "all", "limitPerGame": 10 }`,
261
262
  if (game === 'ufc') {
262
263
  const health = asRecord(meta?.health) ?? asRecord(payload?.health);
263
264
  const events = Array.isArray(payload?.events) ? payload.events : [];
264
- const upstreamReason = pickString(meta?.emptyReason, health?.emptyReason);
265
+ const serverReason = pickString(meta?.emptyReason, health?.emptyReason);
265
266
  const emptyReason = normalized.length === 0
266
- ? inferUfcEmptyReason(events, { ...(health ?? {}), ...(upstreamReason ? { emptyReason: upstreamReason } : {}) }, normalized.length)
267
+ ? inferUfcEmptyReason(events, { ...(health ?? {}), ...(serverReason ? { emptyReason: serverReason } : {}) }, normalized.length)
267
268
  : undefined;
268
269
  let note;
269
270
  if (normalized.length === 0) {
@@ -290,7 +291,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
290
291
  let nextEvent;
291
292
  if (state === 'idle') {
292
293
  const up = await fetchJson(ctx, '/ufc/events/upcoming', { query: { limit: 1 } });
293
- upstreamCalls += 1;
294
+ apiCalls += 1;
294
295
  const first = asRecord(extractRows(up.ok ? up.data : null)[0]);
295
296
  if (first) {
296
297
  nextEvent = {
@@ -332,7 +333,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
332
333
  game: games.length === 1 ? games[0] : null,
333
334
  requestId,
334
335
  tookMs: Date.now() - started,
335
- upstreamCalls,
336
+ apiCalls,
336
337
  rateLimit,
337
338
  partial: partial.length ? partial : undefined,
338
339
  warnings: warnings.length ? warnings : undefined,
@@ -347,24 +348,24 @@ Example: { "game": "all", "limitPerGame": 10 }`,
347
348
  };
348
349
  export const upcomingSchedule = {
349
350
  name: 'upcoming_schedule',
350
- description: `Upcoming matches/events for one game, with game-specific filters.
351
-
352
- When to use:
353
- - "What's on this week?"
354
- - Calendar UI; team next matches
355
-
356
- Prefer over: live_matches for not-yet-started fixtures.
357
-
358
- Do not use when: only in-progress matches needed → live_matches.
359
-
360
- Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
361
- - lol: hours, team (slug), league (slug)
362
- - cs2: team, from, to (ISO); hours not applied upstream
363
- - cod: team, tournamentId
364
- - dota2: limit/cursor primarily; team may be client-filtered where data allows
365
- - ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
366
-
367
- Parallel-safe: yes. Upstream cost: 1–2.
351
+ description: `Upcoming matches/events for one game, with game-specific filters.
352
+
353
+ When to use:
354
+ - "What's on this week?"
355
+ - Calendar UI; team next matches
356
+
357
+ Prefer over: live_matches for not-yet-started fixtures.
358
+
359
+ Do not use when: only in-progress matches needed → live_matches.
360
+
361
+ Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
362
+ - lol: hours, team (slug), league (slug)
363
+ - cs2: team, from, to (ISO); hours is not applied
364
+ - cod: team, tournamentId
365
+ - dota2: limit/cursor primarily; team may be client-filtered where data allows
366
+ - ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
367
+
368
+ Parallel-safe: yes. API calls: 1–2.
368
369
  Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
369
370
  inputSchema: {
370
371
  type: 'object',
@@ -465,7 +466,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
465
466
  ...(to ? { to } : {}),
466
467
  };
467
468
  if (args.hours !== undefined)
468
- noteIgnored('hours', 'CS2 uses from/to or unfiltered upcoming; hours is not applied upstream');
469
+ noteIgnored('hours', 'CS2 uses from/to or unfiltered upcoming; hours is not applied');
469
470
  if (league)
470
471
  noteIgnored('league', 'CS2 schedule does not accept league here — filter client-side or use event tools');
471
472
  if (tournamentId)
@@ -535,7 +536,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
535
536
  source: 'upcoming_schedule',
536
537
  requestId,
537
538
  tookMs: Date.now() - started,
538
- upstreamCalls: 1,
539
+ apiCalls: 1,
539
540
  rateLimit: res.headers,
540
541
  httpStatus: res.status,
541
542
  });
@@ -661,7 +662,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
661
662
  game,
662
663
  requestId,
663
664
  tookMs: Date.now() - started,
664
- upstreamCalls: 1,
665
+ apiCalls: 1,
665
666
  rateLimit: res.headers,
666
667
  warnings: warnings.length ? warnings : undefined,
667
668
  pagination: {