cito-mcp 0.3.0 → 0.3.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.
@@ -167,9 +167,17 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
167
167
  const partial = [];
168
168
  let upstreamCalls = 0;
169
169
  let rateLimit = {};
170
- const primary = await getSection(ctx, primaryPath(game, matchId));
170
+ let primary = await getSection(ctx, primaryPath(game, matchId));
171
171
  upstreamCalls += 1;
172
172
  rateLimit = primary.headers;
173
+ // Tennis live board hands out s365_* ids that live at /matches/live/{id}
174
+ // until the match finishes and lands in the archive. Without this fallback
175
+ // the board gives out ids that match_summary immediately 404s on.
176
+ if (!primary.ok && game === 'tennis' && primary.status === 404) {
177
+ primary = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
178
+ upstreamCalls += 1;
179
+ rateLimit = { ...rateLimit, ...primary.headers };
180
+ }
173
181
  if (!primary.ok) {
174
182
  return errorEnvelope({
175
183
  code: mapHttpToCode(primary.status, { gameNotIncluded: gameNotIncludedHint(primary.data) }),
@@ -430,6 +430,7 @@ Example: { "includeGameProbes": true }`,
430
430
  { game: 'dota2', path: '/dota2' },
431
431
  { game: 'cod', path: '/cod' },
432
432
  { game: 'ufc', path: '/ufc/live/health' },
433
+ { game: 'tennis', path: '/tennis/rankings/top?tour=ATP&top_n=1' },
433
434
  ];
434
435
  const results = await Promise.all(paths.map(async ({ game, path }) => {
435
436
  const res = await fetchJson(ctx, path);
@@ -692,10 +693,10 @@ Example: { "game": "ufc", "q": "rankings" }`,
692
693
  // documented paths and no curated tools, so it is exactly what call_api
693
694
  // callers come looking for.
694
695
  const gameRaw = typeof args.game === 'string' ? args.game.toLowerCase().trim() : '';
695
- if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite)$/)) {
696
+ if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis)$/)) {
696
697
  return errorEnvelope({
697
698
  code: 'UNSUPPORTED_GAME',
698
- message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite`,
699
+ message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis`,
699
700
  game: null,
700
701
  source: 'list_routes',
701
702
  requestId,
@@ -171,14 +171,20 @@ export function normalizeMatch(game, row, forcedStatus) {
171
171
  if (s != null)
172
172
  team2 = { ...team2, score: s };
173
173
  }
174
- // Tennis: live rows carry player1_name/player2_name; archive rows only carry
175
- // winner_id/loser_id (ids, no names). Map both onto sides so labels are never "? vs ?".
174
+ // Tennis: the live board nests player1/player2 objects ({id,name,sets_won});
175
+ // raw live rows carry player1_name/player2_name; archive rows only carry
176
+ // winner_id/loser_id (ids, no names). Map all three onto sides so labels are
177
+ // never "? vs ?" and live scores surface as sets won.
176
178
  if (game === 'tennis' && !team1 && !team2) {
177
- const p1 = pickString(r.player1_name);
178
- const p2 = pickString(r.player2_name);
179
+ const p1obj = asRecord(r.player1);
180
+ const p2obj = asRecord(r.player2);
181
+ const p1 = pickString(p1obj?.name, r.player1_name);
182
+ const p2 = pickString(p2obj?.name, r.player2_name);
179
183
  if (p1 || p2) {
180
- team1 = sideFrom(p1, pickString(r.player1_id), undefined, undefined);
181
- team2 = sideFrom(p2, pickString(r.player2_id), undefined, undefined);
184
+ const score1 = typeof p1obj?.sets_won === 'number' ? p1obj.sets_won : undefined;
185
+ const score2 = typeof p2obj?.sets_won === 'number' ? p2obj.sets_won : undefined;
186
+ team1 = sideFrom(p1, pickString(p1obj?.id, r.player1_id), undefined, score1);
187
+ team2 = sideFrom(p2, pickString(p2obj?.id, r.player2_id), undefined, score2);
182
188
  }
183
189
  else {
184
190
  const w = pickString(r.winner_id);
@@ -286,7 +292,7 @@ export function normalizeMatch(game, row, forcedStatus) {
286
292
  }
287
293
  }
288
294
  const ev = asRecord(r.event);
289
- const eventName = pickString(ev?.name, ev?.title, r.eventName, typeof r.event === 'string' ? r.event : undefined, asRecord(r.tournament)?.name, asRecord(r.tournament)?.title, r.tournamentName);
295
+ const eventName = pickString(ev?.name, ev?.title, r.eventName, typeof r.event === 'string' ? r.event : undefined, asRecord(r.tournament)?.name, asRecord(r.tournament)?.title, r.tournamentName, r.tournament_name);
290
296
  const eventId = pickString(ev?.id, r.eventId, asRecord(r.tournament)?.id, r.tournamentId);
291
297
  const eventSlug = pickString(ev?.slug, r.eventSlug, asRecord(r.tournament)?.slug);
292
298
  const leagueName = pickString(asRecord(r.league)?.name, r.leagueName, r.league, r.leagueSlug);
@@ -10,7 +10,14 @@ function pushCandidates(out, game, type, rows, q, limit) {
10
10
  const ref = entityRef(row, type, game);
11
11
  const r = asRecord(row) ?? {};
12
12
  const nickname = pickString(r.nickname, asRecord(r.profile)?.nickname, ref.meta?.nickname);
13
- const score = rankScore(q, ref.name, ref.id, ref.slug, nickname);
13
+ let score = rankScore(q, ref.name, ref.id, ref.slug, nickname);
14
+ // Ranked-player tiebreaker: a surname query like "Alcaraz" fuzzy-ties the
15
+ // world #2 with a 1991 journeyman; the row's current_rank breaks the tie
16
+ // toward whoever is actually active/ranked (tennis search supplies it).
17
+ const currentRank = Number(r.current_rank);
18
+ if (score > 0 && Number.isFinite(currentRank) && currentRank > 0) {
19
+ score += currentRank <= 100 ? 8 : currentRank <= 1000 ? 4 : 2;
20
+ }
14
21
  // Prefer positive fuzzy hits; keep weak API hits at floor 1 so real search results are not wiped.
15
22
  if (q && score <= 0) {
16
23
  // still allow through at floor so dedicated search endpoints aren't empty on odd nicknames
@@ -62,7 +62,7 @@ export function normalizeStandingRow(row, index) {
62
62
  : null),
63
63
  championStatus: resolvedChampionStatus,
64
64
  teamOrFighter: {
65
- id: pickString(asRecord(entity)?.id, r.teamId, r.fighterId, r.orgId, r.id),
65
+ id: pickString(asRecord(entity)?.id, r.teamId, r.fighterId, r.orgId, r.id, r.player_id),
66
66
  slug: pickString(asRecord(entity)?.slug, r.orgSlug, r.teamSlug, r.slug, r.fighterSlug),
67
67
  name,
68
68
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cito-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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": {