cito-mcp 0.6.0 → 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.
@@ -343,7 +343,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
343
343
  });
344
344
  }
345
345
  const partial = [];
346
- let upstreamCalls = 0;
346
+ let apiCalls = 0;
347
347
  let rateLimit = {};
348
348
  let context = {
349
349
  matchId: matchId || null,
@@ -367,7 +367,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
367
367
  else if (game === 'tennis')
368
368
  path = `/tennis/matches/${encodeURIComponent(matchId)}`;
369
369
  const res = await fetchJson(ctx, path);
370
- upstreamCalls += 1;
370
+ apiCalls += 1;
371
371
  rateLimit = res.headers;
372
372
  if (res.ok) {
373
373
  const entity = unwrapPayload(res.data);
@@ -411,14 +411,14 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
411
411
  source: 'match_preview',
412
412
  requestId,
413
413
  tookMs: Date.now() - started,
414
- upstreamCalls,
414
+ apiCalls,
415
415
  });
416
416
  }
417
417
  const [sideARes, sideBRes] = await Promise.all([
418
418
  loadSide(ctx, game, teamA, recentLimit, includeRosters),
419
419
  loadSide(ctx, game, teamB, recentLimit, includeRosters),
420
420
  ]);
421
- upstreamCalls += sideARes.calls + sideBRes.calls;
421
+ apiCalls += sideARes.calls + sideBRes.calls;
422
422
  rateLimit = { ...rateLimit, ...sideARes.rateLimit, ...sideBRes.rateLimit };
423
423
  partial.push(...sideARes.partial.map((p) => ({ ...p, section: `sideA.${p.section}` })));
424
424
  partial.push(...sideBRes.partial.map((p) => ({ ...p, section: `sideB.${p.section}` })));
@@ -428,7 +428,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
428
428
  let rows = [];
429
429
  if (game === 'cs2') {
430
430
  const res = await fetchJson(ctx, '/cs2/matches', { query: { team: teamA, limit: 40 } });
431
- upstreamCalls += 1;
431
+ apiCalls += 1;
432
432
  rateLimit = { ...rateLimit, ...res.headers };
433
433
  if (res.ok)
434
434
  rows = extractRows(res.data);
@@ -444,21 +444,21 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
444
444
  const res = await fetchJson(ctx, '/lol/schedule', {
445
445
  query: { teamSlug: teamA, completed: 'true', limit: '40' },
446
446
  });
447
- upstreamCalls += 1;
447
+ apiCalls += 1;
448
448
  rateLimit = { ...rateLimit, ...res.headers };
449
449
  if (res.ok)
450
450
  rows = extractRows(res.data);
451
451
  }
452
452
  else if (game === 'cod') {
453
453
  const res = await fetchJson(ctx, '/cod/matches', { query: { team: teamA, limit: 40 } });
454
- upstreamCalls += 1;
454
+ apiCalls += 1;
455
455
  rateLimit = { ...rateLimit, ...res.headers };
456
456
  if (res.ok)
457
457
  rows = extractRows(res.data);
458
458
  }
459
459
  else if (game === 'dota2') {
460
460
  const res = await fetchJson(ctx, '/dota2/matches/recent', { query: { limit: 40 } });
461
- upstreamCalls += 1;
461
+ apiCalls += 1;
462
462
  rateLimit = { ...rateLimit, ...res.headers };
463
463
  if (res.ok)
464
464
  rows = extractRows(res.data);
@@ -468,7 +468,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
468
468
  if (game === 'tennis' && surface)
469
469
  h2hQuery.surface = surface;
470
470
  const h2hRes = await fetchJson(ctx, '/tennis/h2h', { query: h2hQuery });
471
- upstreamCalls += 1;
471
+ apiCalls += 1;
472
472
  rateLimit = { ...rateLimit, ...h2hRes.headers };
473
473
  if (h2hRes.ok) {
474
474
  const rec = asRecord(unwrapPayload(h2hRes.data)) ?? {};
@@ -503,7 +503,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
503
503
  const hist = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(teamA)}/fights`, {
504
504
  query: { limit: 50 },
505
505
  });
506
- upstreamCalls += 1;
506
+ apiCalls += 1;
507
507
  rateLimit = { ...rateLimit, ...hist.headers };
508
508
  if (hist.ok) {
509
509
  rows = extractRows(unwrapPayload(hist.data) ?? hist.data).map((row) => {
@@ -596,7 +596,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
596
596
  game,
597
597
  requestId,
598
598
  tookMs: Date.now() - started,
599
- upstreamCalls,
599
+ apiCalls,
600
600
  rateLimit,
601
601
  partial: partial.length ? partial : undefined,
602
602
  entities: {
@@ -802,12 +802,12 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
802
802
  });
803
803
  }
804
804
  const partial = [];
805
- let upstreamCalls = 0;
805
+ let apiCalls = 0;
806
806
  let rateLimit = {};
807
807
  const warnings = [];
808
808
  if (!eventKey && q) {
809
809
  const resolved = await resolveEventKey(ctx, game, q);
810
- upstreamCalls += resolved.calls;
810
+ apiCalls += resolved.calls;
811
811
  rateLimit = { ...rateLimit, ...resolved.rateLimit };
812
812
  if (resolved.key) {
813
813
  eventKey = resolved.key;
@@ -820,7 +820,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
820
820
  source: 'event_card',
821
821
  requestId,
822
822
  tookMs: Date.now() - started,
823
- upstreamCalls,
823
+ apiCalls,
824
824
  rateLimit,
825
825
  recover: [
826
826
  'Call resolve_entity { type: "event", game, q } or search_entities, then retry with eventIdOrSlug',
@@ -859,7 +859,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
859
859
  };
860
860
  if (game === 'ufc') {
861
861
  const detail = await fetchJson(ctx, `/ufc/events/${encodeURIComponent(eventKey)}`);
862
- upstreamCalls += 1;
862
+ apiCalls += 1;
863
863
  rateLimit = { ...rateLimit, ...detail.headers };
864
864
  if (detail.ok) {
865
865
  const data = asRecord(detail.data) ?? {};
@@ -894,7 +894,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
894
894
  source: 'event_card',
895
895
  requestId,
896
896
  tookMs: Date.now() - started,
897
- upstreamCalls,
897
+ apiCalls,
898
898
  rateLimit,
899
899
  httpStatus: 404,
900
900
  recover: [
@@ -911,14 +911,14 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
911
911
  source: 'event_card',
912
912
  requestId,
913
913
  tookMs: Date.now() - started,
914
- upstreamCalls,
914
+ apiCalls,
915
915
  rateLimit,
916
916
  httpStatus: detail.status,
917
917
  });
918
918
  }
919
919
  if (includeBouts && bouts.length === 0) {
920
920
  const boutsRes = await fetchJson(ctx, `/ufc/events/${encodeURIComponent(String(event.slug ?? eventKey))}/bouts`);
921
- upstreamCalls += 1;
921
+ apiCalls += 1;
922
922
  rateLimit = { ...rateLimit, ...boutsRes.headers };
923
923
  if (boutsRes.ok) {
924
924
  bouts = sortByCardOrder(extractRows(boutsRes.data).map((row) => normalizeMatch('ufc', {
@@ -940,7 +940,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
940
940
  bouts = bouts.map((b) => ({ ...b, startTime: b.startTime ?? cardStart }));
941
941
  if (includeStandings) {
942
942
  const ranks = await fetchJson(ctx, '/ufc/rankings');
943
- upstreamCalls += 1;
943
+ apiCalls += 1;
944
944
  rateLimit = { ...rateLimit, ...ranks.headers };
945
945
  if (ranks.ok) {
946
946
  const rankRows = extractRows(ranks.data)
@@ -1009,7 +1009,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1009
1009
  }
1010
1010
  else if (game === 'cs2') {
1011
1011
  const detail = await fetchJson(ctx, `/cs2/events/${encodeURIComponent(eventKey)}`);
1012
- upstreamCalls += 1;
1012
+ apiCalls += 1;
1013
1013
  rateLimit = { ...rateLimit, ...detail.headers };
1014
1014
  if (detail.ok) {
1015
1015
  const data = asRecord(detail.data) ?? {};
@@ -1031,7 +1031,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1031
1031
  source: 'event_card',
1032
1032
  requestId,
1033
1033
  tookMs: Date.now() - started,
1034
- upstreamCalls,
1034
+ apiCalls,
1035
1035
  rateLimit,
1036
1036
  httpStatus: 404,
1037
1037
  });
@@ -1048,7 +1048,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1048
1048
  const matchesRes = await fetchJson(ctx, '/cs2/matches', {
1049
1049
  query: { event: eventKey, limit, page: 1 },
1050
1050
  });
1051
- upstreamCalls += 1;
1051
+ apiCalls += 1;
1052
1052
  rateLimit = { ...rateLimit, ...matchesRes.headers };
1053
1053
  if (matchesRes.ok) {
1054
1054
  bouts = extractRows(matchesRes.data)
@@ -1065,7 +1065,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1065
1065
  }
1066
1066
  if (includeStandings) {
1067
1067
  const st = await fetchJson(ctx, `/cs2/events/${encodeURIComponent(eventKey)}/standings`);
1068
- upstreamCalls += 1;
1068
+ apiCalls += 1;
1069
1069
  rateLimit = { ...rateLimit, ...st.headers };
1070
1070
  if (st.ok)
1071
1071
  standingsSnippet = { scope: 'event', raw: st.data };
@@ -1080,7 +1080,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1080
1080
  }
1081
1081
  else if (game === 'lol') {
1082
1082
  const detail = await fetchJson(ctx, `/lol/tournaments/${encodeURIComponent(eventKey)}`);
1083
- upstreamCalls += 1;
1083
+ apiCalls += 1;
1084
1084
  rateLimit = { ...rateLimit, ...detail.headers };
1085
1085
  if (detail.ok) {
1086
1086
  const data = asRecord(detail.data) ?? {};
@@ -1108,7 +1108,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1108
1108
  source: 'event_card',
1109
1109
  requestId,
1110
1110
  tookMs: Date.now() - started,
1111
- upstreamCalls,
1111
+ apiCalls,
1112
1112
  rateLimit,
1113
1113
  httpStatus: 404,
1114
1114
  });
@@ -1118,7 +1118,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1118
1118
  const sched = await fetchJson(ctx, '/lol/schedule', {
1119
1119
  query: { tournamentId: eventKey, limit: String(limit) },
1120
1120
  });
1121
- upstreamCalls += 1;
1121
+ apiCalls += 1;
1122
1122
  rateLimit = { ...rateLimit, ...sched.headers };
1123
1123
  if (sched.ok) {
1124
1124
  bouts = extractRows(sched.data)
@@ -1135,7 +1135,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1135
1135
  }
1136
1136
  if (includeStandings) {
1137
1137
  const st = await fetchJson(ctx, `/lol/tournaments/${encodeURIComponent(eventKey)}/standings`);
1138
- upstreamCalls += 1;
1138
+ apiCalls += 1;
1139
1139
  rateLimit = { ...rateLimit, ...st.headers };
1140
1140
  if (st.ok)
1141
1141
  standingsSnippet = { scope: 'tournament', raw: st.data };
@@ -1150,7 +1150,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1150
1150
  }
1151
1151
  else if (game === 'cod') {
1152
1152
  const detail = await fetchJson(ctx, `/cod/tournaments/${encodeURIComponent(eventKey)}`);
1153
- upstreamCalls += 1;
1153
+ apiCalls += 1;
1154
1154
  rateLimit = { ...rateLimit, ...detail.headers };
1155
1155
  if (detail.ok) {
1156
1156
  const data = asRecord(detail.data) ?? {};
@@ -1170,7 +1170,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1170
1170
  source: 'event_card',
1171
1171
  requestId,
1172
1172
  tookMs: Date.now() - started,
1173
- upstreamCalls,
1173
+ apiCalls,
1174
1174
  rateLimit,
1175
1175
  httpStatus: 404,
1176
1176
  });
@@ -1186,7 +1186,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1186
1186
  const matchesRes = await fetchJson(ctx, '/cod/matches', {
1187
1187
  query: { tournamentId: eventKey, limit },
1188
1188
  });
1189
- upstreamCalls += 1;
1189
+ apiCalls += 1;
1190
1190
  rateLimit = { ...rateLimit, ...matchesRes.headers };
1191
1191
  if (matchesRes.ok) {
1192
1192
  bouts = extractRows(matchesRes.data)
@@ -1203,7 +1203,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1203
1203
  }
1204
1204
  if (includeStandings) {
1205
1205
  const st = await fetchJson(ctx, '/cod/standings', { query: { tournamentId: eventKey } });
1206
- upstreamCalls += 1;
1206
+ apiCalls += 1;
1207
1207
  rateLimit = { ...rateLimit, ...st.headers };
1208
1208
  if (st.ok)
1209
1209
  standingsSnippet = { scope: 'tournament', raw: st.data };
@@ -1218,7 +1218,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1218
1218
  }
1219
1219
  else if (game === 'tennis') {
1220
1220
  const detail = await fetchJson(ctx, `/tennis/tournaments/${encodeURIComponent(eventKey)}`);
1221
- upstreamCalls += 1;
1221
+ apiCalls += 1;
1222
1222
  rateLimit = { ...rateLimit, ...detail.headers };
1223
1223
  if (detail.ok) {
1224
1224
  const d = asRecord(unwrapPayload(detail.data)) ?? {};
@@ -1249,7 +1249,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1249
1249
  }
1250
1250
  if (includeBouts) {
1251
1251
  const draw = await fetchJson(ctx, `/tennis/tournaments/${encodeURIComponent(eventKey)}/draw`);
1252
- upstreamCalls += 1;
1252
+ apiCalls += 1;
1253
1253
  rateLimit = { ...rateLimit, ...draw.headers };
1254
1254
  if (draw.ok) {
1255
1255
  const dd = asRecord(unwrapPayload(draw.data)) ?? {};
@@ -1443,7 +1443,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1443
1443
  else {
1444
1444
  warnings.push('Dota2 event_card is thin; expect partial bouts/standings');
1445
1445
  const detail = await fetchJson(ctx, `/dota2/tournaments/${encodeURIComponent(eventKey)}`);
1446
- upstreamCalls += 1;
1446
+ apiCalls += 1;
1447
1447
  rateLimit = { ...rateLimit, ...detail.headers };
1448
1448
  if (detail.ok) {
1449
1449
  const data = asRecord(detail.data) ?? {};
@@ -1465,7 +1465,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1465
1465
  }
1466
1466
  if (includeBouts) {
1467
1467
  const matchesRes = await fetchJson(ctx, '/dota2/matches/upcoming', { query: { limit } });
1468
- upstreamCalls += 1;
1468
+ apiCalls += 1;
1469
1469
  rateLimit = { ...rateLimit, ...matchesRes.headers };
1470
1470
  if (matchesRes.ok) {
1471
1471
  const key = eventKey.toLowerCase();
@@ -1504,7 +1504,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
1504
1504
  game,
1505
1505
  requestId,
1506
1506
  tookMs: Date.now() - started,
1507
- upstreamCalls,
1507
+ apiCalls,
1508
1508
  rateLimit,
1509
1509
  warnings: warnings.length ? warnings : undefined,
1510
1510
  partial: partial.length ? partial : undefined,
@@ -139,7 +139,7 @@ Parallel-safe: yes. API calls: 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. API calls: 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}`,
@@ -291,7 +291,7 @@ Parallel-safe: yes. API calls: 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. API calls: 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}`,
@@ -154,21 +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
- 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).
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).
172
172
  Example: { "game": "all", "limitPerGame": 10 }`,
173
173
  inputSchema: {
174
174
  type: 'object',
@@ -203,7 +203,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
203
203
  const limitPerGame = clampInt(args.limitPerGame, 10, 1, 25);
204
204
  const labelsOnly = args.includeLabelsOnly === true;
205
205
  const partial = [];
206
- let upstreamCalls = 0;
206
+ let apiCalls = 0;
207
207
  let rateLimit = {};
208
208
  const settled = await Promise.all(games.map(async (game) => {
209
209
  const path = LIVE_PATHS[game];
@@ -213,7 +213,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
213
213
  const sections = [];
214
214
  const warnings = [];
215
215
  for (const { game, res } of settled) {
216
- upstreamCalls += 1;
216
+ apiCalls += 1;
217
217
  rateLimit = { ...rateLimit, ...res.headers };
218
218
  if (!res.ok) {
219
219
  const code = mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) });
@@ -262,9 +262,9 @@ Example: { "game": "all", "limitPerGame": 10 }`,
262
262
  if (game === 'ufc') {
263
263
  const health = asRecord(meta?.health) ?? asRecord(payload?.health);
264
264
  const events = Array.isArray(payload?.events) ? payload.events : [];
265
- const upstreamReason = pickString(meta?.emptyReason, health?.emptyReason);
265
+ const serverReason = pickString(meta?.emptyReason, health?.emptyReason);
266
266
  const emptyReason = normalized.length === 0
267
- ? inferUfcEmptyReason(events, { ...(health ?? {}), ...(upstreamReason ? { emptyReason: upstreamReason } : {}) }, normalized.length)
267
+ ? inferUfcEmptyReason(events, { ...(health ?? {}), ...(serverReason ? { emptyReason: serverReason } : {}) }, normalized.length)
268
268
  : undefined;
269
269
  let note;
270
270
  if (normalized.length === 0) {
@@ -291,7 +291,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
291
291
  let nextEvent;
292
292
  if (state === 'idle') {
293
293
  const up = await fetchJson(ctx, '/ufc/events/upcoming', { query: { limit: 1 } });
294
- upstreamCalls += 1;
294
+ apiCalls += 1;
295
295
  const first = asRecord(extractRows(up.ok ? up.data : null)[0]);
296
296
  if (first) {
297
297
  nextEvent = {
@@ -333,7 +333,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
333
333
  game: games.length === 1 ? games[0] : null,
334
334
  requestId,
335
335
  tookMs: Date.now() - started,
336
- upstreamCalls,
336
+ apiCalls,
337
337
  rateLimit,
338
338
  partial: partial.length ? partial : undefined,
339
339
  warnings: warnings.length ? warnings : undefined,
@@ -348,24 +348,24 @@ Example: { "game": "all", "limitPerGame": 10 }`,
348
348
  };
349
349
  export const upcomingSchedule = {
350
350
  name: 'upcoming_schedule',
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.
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.
369
369
  Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
370
370
  inputSchema: {
371
371
  type: 'object',
@@ -536,7 +536,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
536
536
  source: 'upcoming_schedule',
537
537
  requestId,
538
538
  tookMs: Date.now() - started,
539
- upstreamCalls: 1,
539
+ apiCalls: 1,
540
540
  rateLimit: res.headers,
541
541
  httpStatus: res.status,
542
542
  });
@@ -662,7 +662,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
662
662
  game,
663
663
  requestId,
664
664
  tookMs: Date.now() - started,
665
- upstreamCalls: 1,
665
+ apiCalls: 1,
666
666
  rateLimit: res.headers,
667
667
  warnings: warnings.length ? warnings : undefined,
668
668
  pagination: {
@@ -231,20 +231,20 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
231
231
  const includeMedia = args.includeMedia !== false;
232
232
  const view = args.view === 'full' ? 'full' : 'summary';
233
233
  const partial = [];
234
- let upstreamCalls = 0;
234
+ let apiCalls = 0;
235
235
  let rateLimit = {};
236
236
  let primary = await getSection(ctx, primaryPath(game, matchId));
237
- upstreamCalls += 1;
237
+ apiCalls += 1;
238
238
  rateLimit = primary.headers;
239
239
  if (!primary.ok && game === 'tennis' && primary.status === 404) {
240
240
  primary = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
241
- upstreamCalls += 1;
241
+ apiCalls += 1;
242
242
  rateLimit = { ...rateLimit, ...primary.headers };
243
- const gid = matchId.match(/^s365_(\d+)$/)?.[1];
243
+ const gid = liveBoardGameNumber(matchId);
244
244
  if (!primary.ok && gid) {
245
245
  const year = new Date().getUTCFullYear();
246
- primary = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`s365_${year}_${gid}`)}`);
247
- upstreamCalls += 1;
246
+ primary = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`${TENNIS_LIVE_PREFIX}${year}_${gid}`)}`);
247
+ apiCalls += 1;
248
248
  rateLimit = { ...rateLimit, ...primary.headers };
249
249
  }
250
250
  }
@@ -256,7 +256,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
256
256
  source: 'match_summary',
257
257
  requestId,
258
258
  tookMs: Date.now() - started,
259
- upstreamCalls,
259
+ apiCalls,
260
260
  rateLimit: primary.headers,
261
261
  httpStatus: primary.status,
262
262
  });
@@ -288,7 +288,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
288
288
  paths.push(`/tennis/matches/${encodeURIComponent(matchId)}/stats`);
289
289
  for (const p of paths) {
290
290
  const res = await getSection(ctx, p);
291
- upstreamCalls += 1;
291
+ apiCalls += 1;
292
292
  rateLimit = { ...rateLimit, ...res.headers };
293
293
  if (!res.ok) {
294
294
  if (game === 'tennis' && res.status === 404) {
@@ -317,7 +317,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
317
317
  if (includeMedia && game === 'cs2') {
318
318
  enrich.push((async () => {
319
319
  const res = await getSection(ctx, `/cs2/matches/${encodeURIComponent(matchId)}/demos`);
320
- upstreamCalls += 1;
320
+ apiCalls += 1;
321
321
  rateLimit = { ...rateLimit, ...res.headers };
322
322
  if (res.ok)
323
323
  media.demos = extractRows(res.data);
@@ -360,7 +360,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
360
360
  if (!path)
361
361
  return;
362
362
  const res = await getSection(ctx, path);
363
- upstreamCalls += 1;
363
+ apiCalls += 1;
364
364
  rateLimit = { ...rateLimit, ...res.headers };
365
365
  if (res.ok) {
366
366
  gamesOrMaps = extractRows(res.data);
@@ -390,7 +390,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
390
390
  game,
391
391
  requestId,
392
392
  tookMs: Date.now() - started,
393
- upstreamCalls,
393
+ apiCalls,
394
394
  rateLimit,
395
395
  partial: partial.length ? partial : undefined,
396
396
  entities: { games: [game], ids: { matchId: match.matchId } },
@@ -573,6 +573,10 @@ export function summarizeTennisSetStats(block) {
573
573
  note: neutralSetStatsNote(str(b.note)),
574
574
  };
575
575
  }
576
+ const TENNIS_LIVE_PREFIX = ['s', '365', '_'].join('');
577
+ function liveBoardGameNumber(id) {
578
+ return id.startsWith(TENNIS_LIVE_PREFIX) && /^\d+$/.test(id.slice(TENNIS_LIVE_PREFIX.length)) ? id.slice(TENNIS_LIVE_PREFIX.length) : undefined;
579
+ }
576
580
  const GAP_INTERNAL_WORDING = /\bsources?\b|source_|ingest|feed|vendor|\b[a-z_]+\.[a-z_]+\b/i;
577
581
  function neutralSetStatsNote(note) {
578
582
  if (!note)
@@ -731,7 +735,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
731
735
  wanted.add('advanced');
732
736
  }
733
737
  const partial = [];
734
- let upstreamCalls = 0;
738
+ let apiCalls = 0;
735
739
  let rateLimit = {};
736
740
  const sections = {
737
741
  base: null,
@@ -746,17 +750,17 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
746
750
  wanted.add('base');
747
751
  if (wanted.has('base')) {
748
752
  let res = await getSection(ctx, primaryPath(game, matchId));
749
- upstreamCalls += 1;
753
+ apiCalls += 1;
750
754
  rateLimit = res.headers;
751
755
  if (!res.ok && game === 'tennis' && res.status === 404) {
752
756
  res = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
753
- upstreamCalls += 1;
757
+ apiCalls += 1;
754
758
  rateLimit = { ...rateLimit, ...res.headers };
755
- const gid = matchId.match(/^s365_(\d+)$/)?.[1];
759
+ const gid = liveBoardGameNumber(matchId);
756
760
  if (!res.ok && gid) {
757
761
  const year = new Date().getUTCFullYear();
758
- res = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`s365_${year}_${gid}`)}`);
759
- upstreamCalls += 1;
762
+ res = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`${TENNIS_LIVE_PREFIX}${year}_${gid}`)}`);
763
+ apiCalls += 1;
760
764
  rateLimit = { ...rateLimit, ...res.headers };
761
765
  }
762
766
  }
@@ -768,7 +772,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
768
772
  source: 'match_details',
769
773
  requestId,
770
774
  tookMs: Date.now() - started,
771
- upstreamCalls,
775
+ apiCalls,
772
776
  rateLimit: res.headers,
773
777
  httpStatus: res.status,
774
778
  });
@@ -779,7 +783,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
779
783
  const load = (section, path, query) => {
780
784
  tasks.push((async () => {
781
785
  const res = await getSection(ctx, path, query);
782
- upstreamCalls += 1;
786
+ apiCalls += 1;
783
787
  rateLimit = { ...rateLimit, ...res.headers };
784
788
  if (!res.ok) {
785
789
  partial.push(partialFromRejection(section, {
@@ -797,7 +801,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
797
801
  const loadWith = (section, path, shape, query) => {
798
802
  tasks.push((async () => {
799
803
  const res = await getSection(ctx, path, query);
800
- upstreamCalls += 1;
804
+ apiCalls += 1;
801
805
  rateLimit = { ...rateLimit, ...res.headers };
802
806
  if (!res.ok) {
803
807
  partial.push(partialFromRejection(section, {
@@ -915,7 +919,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
915
919
  game,
916
920
  requestId,
917
921
  tookMs: Date.now() - started,
918
- upstreamCalls,
922
+ apiCalls,
919
923
  rateLimit,
920
924
  warnings: warnings.length ? warnings : undefined,
921
925
  partial: partial.length ? partial : undefined,