cito-mcp 0.5.4 → 0.5.6
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/README.md +13 -0
- package/dist/tools/cs2.js +40 -15
- package/dist/tools/insight.js +180 -67
- package/dist/tools/match.js +57 -32
- package/dist/tools/normalize.js +5 -2
- package/dist/tools/player.js +50 -49
- package/dist/tools/schedule.js +25 -7
- package/dist/tools/team.js +14 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -550,6 +550,19 @@ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
|
550
550
|
|
|
551
551
|
## Changelog (summary)
|
|
552
552
|
|
|
553
|
+
### 0.5.6
|
|
554
|
+
|
|
555
|
+
- **Tennis `event_card` draw is a real bracket** (customer audit 2026-11-03). `draw.complete` is true only when every main round to the Final is played (it said true beside `missingRounds` F/SF/QF); every main round is listed with `status` (`completed` / `in_progress` / `not_started` / `incomplete_archive`), `expected` (byes excluded: a 56 draw's R64 holds 24), `byes`, `walkovers`, and `pending` (scheduled / live matches, never presented as results). `drawSize` is the real field (Paris 56, `drawSizeSource`, `bracketSize`, `byes`); `matchesExpected` counts the main draw and is compared with `mainDrawMatchesReturned` (qualifying is reported apart). Each bout carries `winnerId` / `loserId` / `outcome` (a "6-3 6-7 RET" retirement says who won), `startTime`, `nextMatchId` (the bracket edge) and `bracketPosition`; rounds report `included` / `omitted` when `drawMaxMatches` trims them.
|
|
556
|
+
- **Tennis `head_to_head` honours `limit`**: it is sent to `/tennis/h2h`, which pages the meetings while every tally still covers all of them (`meetingsReturned` / `meetingsTotal`; the H2H `level_breakdown` now always sums to `total_matches`).
|
|
557
|
+
- Other tennis audit fixes in the same release: live stats as structured numerator / denominator / pct, `isSimulated` / `liveFeed` on live rows, `setNumber` from `set_number`, and the schedule / rankings / player readers updated for the API changes of 2026-11-03.
|
|
558
|
+
|
|
559
|
+
### 0.5.5
|
|
560
|
+
|
|
561
|
+
- **`cs2_veto_sequence` returns the veto.** It called the match head-to-head route and answered with the two teams' head-to-head record (`cs2_team_h2h`) instead of the veto (0.5.1 to 0.5.4). It now calls `/cs2/matches/{matchId}/veto`: `vetoOrder` (every ban, pick and the decider, in order), `vetoStatus` (`complete`, `pending`, `forfeit_no_veto`, `unavailable`), `bestOf` and the maps.
|
|
562
|
+
- **`cs2_opening_duels` with `playerId` works.** It called a route the API does not have (every call was a 404). A player's opening duels now come from the career totals: `openingKills` / `openingDeaths` (null when fewer than 90% of the player's maps record them), the recorded sums, coverage and success rate.
|
|
563
|
+
- **`cs2_team_map_stats` takes `opponentId`** for a map-by-map comparison of two teams (`/cs2/teams/{team}/map-pool/vs/{opponent}`: win rates, round difference, pick/ban/decider rates, their meetings per map, edge). Map rows carry `inPool` / `poolStatus`.
|
|
564
|
+
- `cs2_round_economy` points to `/cs2/maps/{mapId}/economy` for per-player money each round.
|
|
565
|
+
|
|
553
566
|
### 0.5.4
|
|
554
567
|
|
|
555
568
|
- **Apex Legends through `call_api`.** `/apex/...` paths (ALGS events, standings, scores) were refused with `PATH_NOT_ALLOWED` although they are a live REST product. They are allowed now and listed in `list_capabilities`.
|
package/dist/tools/cs2.js
CHANGED
|
@@ -59,7 +59,8 @@ export const cs2LiveScoreboard = {
|
|
|
59
59
|
};
|
|
60
60
|
export const cs2RoundEconomy = {
|
|
61
61
|
name: 'cs2_round_economy',
|
|
62
|
-
description: 'Round-by-round tactical economy for a CS2 map. Classifies every round buy type (Full Buy, Force Buy, Semi-Eco, Full Eco), team equipment spend, freeze-time equipment values, and loss bonus counter.'
|
|
62
|
+
description: 'Round-by-round tactical economy for a CS2 map. Classifies every round buy type (Full Buy, Force Buy, Semi-Eco, Full Eco), team equipment spend, freeze-time equipment values, and loss bonus counter. '
|
|
63
|
+
+ 'For money per PLAYER each round (startMoney, moneySpent, moneyLeft, equipmentValue, buyType, purchases) use call_api GET /cs2/maps/{mapId}/economy (by=player|team|round; paid plans).',
|
|
63
64
|
inputSchema: {
|
|
64
65
|
type: 'object',
|
|
65
66
|
properties: {
|
|
@@ -123,9 +124,9 @@ export const cs2OpeningDuels = {
|
|
|
123
124
|
const rawPlayer = typeof args.playerId === 'string' ? args.playerId.trim() : '';
|
|
124
125
|
const limit = clampInt(args.limit, 20, 1, 100);
|
|
125
126
|
const path = rawPlayer
|
|
126
|
-
? `/cs2/players/${encodeURIComponent(rawPlayer)}/
|
|
127
|
+
? `/cs2/players/${encodeURIComponent(rawPlayer)}/career`
|
|
127
128
|
: `/cs2/stats/opening-duels`;
|
|
128
|
-
const res = await fetchJson(ctx, path, { query: { limit } });
|
|
129
|
+
const res = await fetchJson(ctx, path, rawPlayer ? {} : { query: { limit } });
|
|
129
130
|
if (!res.ok) {
|
|
130
131
|
return errorEnvelope({
|
|
131
132
|
code: mapHttpToCode(res.status),
|
|
@@ -137,8 +138,29 @@ export const cs2OpeningDuels = {
|
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
const payload = unwrap(res.data);
|
|
141
|
+
let data = payload;
|
|
142
|
+
if (rawPlayer) {
|
|
143
|
+
const p = asRecord(payload) ?? {};
|
|
144
|
+
const t = asRecord(p.totals) ?? {};
|
|
145
|
+
const fk = typeof t.openingKillsRecorded === 'number' ? t.openingKillsRecorded : null;
|
|
146
|
+
const fd = typeof t.openingDeathsRecorded === 'number' ? t.openingDeathsRecorded : null;
|
|
147
|
+
data = {
|
|
148
|
+
entityType: 'cs2_player_opening_duels',
|
|
149
|
+
playerId: p.playerId ?? null,
|
|
150
|
+
playerNickname: p.playerNickname ?? null,
|
|
151
|
+
maps: t.maps ?? null,
|
|
152
|
+
openingKills: t.openingKills ?? null,
|
|
153
|
+
openingDeaths: t.openingDeaths ?? null,
|
|
154
|
+
openingKillsRecorded: fk,
|
|
155
|
+
openingDeathsRecorded: fd,
|
|
156
|
+
openingDuelMaps: t.openingDuelMaps ?? null,
|
|
157
|
+
openingDuelCoverage: t.openingDuelCoverage ?? null,
|
|
158
|
+
openingDuelSuccessRate: fk !== null && fd !== null && fk + fd > 0 ? Math.round((fk / (fk + fd)) * 10000) / 10000 : null,
|
|
159
|
+
source: `/cs2/players/${rawPlayer}/career`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
140
162
|
return successEnvelope({
|
|
141
|
-
data
|
|
163
|
+
data,
|
|
142
164
|
game: 'cs2',
|
|
143
165
|
source: 'cs2_opening_duels',
|
|
144
166
|
requestId: reqId,
|
|
@@ -188,16 +210,18 @@ export const cs2Clutches = {
|
|
|
188
210
|
};
|
|
189
211
|
export const cs2TeamMapStats = {
|
|
190
212
|
name: 'cs2_team_map_stats',
|
|
191
|
-
description: 'Team win rates, round win rates, and CT/T side win splits
|
|
213
|
+
description: 'Team win rates, round win rates, and CT/T side win splits per map over the last N days. Each map carries inPool / poolStatus (current Active Duty pool) and removedAt. '
|
|
214
|
+
+ 'Pass opponentId to compare two teams map by map instead (GET /cs2/teams/{team}/map-pool/vs/{opponent}): win rates, round difference, pick/ban/decider rates, their meetings on each map and an edge per map.',
|
|
192
215
|
inputSchema: {
|
|
193
216
|
type: 'object',
|
|
194
217
|
properties: {
|
|
195
218
|
teamId: stringSchema('Team ID or slug (e.g. "cs2-team-4608" or "natus-vincere"). Required.', 'cs2-team-4608'),
|
|
196
219
|
days: {
|
|
197
220
|
type: 'integer',
|
|
198
|
-
description: 'Timeframe in days: 30, 90, 180, or 365 (default 90).',
|
|
221
|
+
description: 'Timeframe in days: 30, 90, 180, or 365 (default 90; with opponentId the API default is 180).',
|
|
199
222
|
default: 90,
|
|
200
223
|
},
|
|
224
|
+
opponentId: stringSchema('Optional opponent team ID or slug (e.g. "mouz"): returns the map-pool head-to-head of the two teams.', 'mouz'),
|
|
201
225
|
},
|
|
202
226
|
required: ['teamId'],
|
|
203
227
|
additionalProperties: false,
|
|
@@ -215,9 +239,12 @@ export const cs2TeamMapStats = {
|
|
|
215
239
|
requestId: reqId,
|
|
216
240
|
});
|
|
217
241
|
}
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
const
|
|
242
|
+
const rawOpponent = typeof args.opponentId === 'string' ? args.opponentId.trim() : '';
|
|
243
|
+
const days = typeof args.days === 'number' ? args.days : rawOpponent ? undefined : 90;
|
|
244
|
+
const path = rawOpponent
|
|
245
|
+
? `/cs2/teams/${encodeURIComponent(rawTeam)}/map-pool/vs/${encodeURIComponent(rawOpponent)}`
|
|
246
|
+
: `/cs2/teams/${encodeURIComponent(rawTeam)}/map-stats`;
|
|
247
|
+
const res = await fetchJson(ctx, path, days === undefined ? {} : { query: { days } });
|
|
221
248
|
if (!res.ok) {
|
|
222
249
|
return errorEnvelope({
|
|
223
250
|
code: mapHttpToCode(res.status),
|
|
@@ -276,7 +303,8 @@ export const cs2UtilityLeaderboard = {
|
|
|
276
303
|
};
|
|
277
304
|
export const cs2VetoSequence = {
|
|
278
305
|
name: 'cs2_veto_sequence',
|
|
279
|
-
description: 'Map pick/ban veto sequence for a CS2 match
|
|
306
|
+
description: 'Map pick/ban veto sequence for a CS2 match (GET /cs2/matches/{matchId}/veto). Returns vetoOrder (each ban, pick and the decider in order, with the team), '
|
|
307
|
+
+ 'vetoStatus (complete | pending | forfeit_no_veto | unavailable; vetoOrder is [] unless complete), bestOf and the maps.',
|
|
280
308
|
inputSchema: {
|
|
281
309
|
type: 'object',
|
|
282
310
|
properties: {
|
|
@@ -298,8 +326,7 @@ export const cs2VetoSequence = {
|
|
|
298
326
|
requestId: reqId,
|
|
299
327
|
});
|
|
300
328
|
}
|
|
301
|
-
const
|
|
302
|
-
const path = `/cs2/matches/${encodeURIComponent(cleanId)}/head-to-head`;
|
|
329
|
+
const path = `/cs2/matches/${encodeURIComponent(rawId)}/veto`;
|
|
303
330
|
const res = await fetchJson(ctx, path);
|
|
304
331
|
if (!res.ok) {
|
|
305
332
|
return errorEnvelope({
|
|
@@ -312,10 +339,8 @@ export const cs2VetoSequence = {
|
|
|
312
339
|
});
|
|
313
340
|
}
|
|
314
341
|
const payload = unwrap(res.data);
|
|
315
|
-
const r = asRecord(payload);
|
|
316
|
-
const veto = r?.veto ?? r?.vetoSequence ?? payload;
|
|
317
342
|
return successEnvelope({
|
|
318
|
-
data:
|
|
343
|
+
data: payload,
|
|
319
344
|
game: 'cs2',
|
|
320
345
|
source: 'cs2_veto_sequence',
|
|
321
346
|
requestId: reqId,
|
package/dist/tools/insight.js
CHANGED
|
@@ -260,20 +260,20 @@ export function composeH2H(game, sideA, sideB, rows, limit) {
|
|
|
260
260
|
}
|
|
261
261
|
export const matchPreview = {
|
|
262
262
|
name: 'match_preview',
|
|
263
|
-
description: `COMPOSITE pre-match briefing: sides, roster/form snippets, H2H stub, event context — for pick'ems, articles, and match-page before state.
|
|
264
|
-
|
|
265
|
-
When to use:
|
|
266
|
-
- Upcoming match deep link
|
|
267
|
-
- "Who should I watch before this game?"
|
|
268
|
-
- App scaffold for preview cards
|
|
269
|
-
|
|
270
|
-
Prefer over: manually chaining team_profile ×2 + head_to_head + schedule.
|
|
271
|
-
Prefer match_summary when match is completed; match_details for live in-game.
|
|
272
|
-
|
|
273
|
-
Do not use when: user wants final score/recap of a finished match.
|
|
274
|
-
|
|
275
|
-
Parallel-safe: yes. Upstream cost: 4–8.
|
|
276
|
-
Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
|
|
263
|
+
description: `COMPOSITE pre-match briefing: sides, roster/form snippets, H2H stub, event context — for pick'ems, articles, and match-page before state.
|
|
264
|
+
|
|
265
|
+
When to use:
|
|
266
|
+
- Upcoming match deep link
|
|
267
|
+
- "Who should I watch before this game?"
|
|
268
|
+
- App scaffold for preview cards
|
|
269
|
+
|
|
270
|
+
Prefer over: manually chaining team_profile ×2 + head_to_head + schedule.
|
|
271
|
+
Prefer match_summary when match is completed; match_details for live in-game.
|
|
272
|
+
|
|
273
|
+
Do not use when: user wants final score/recap of a finished match.
|
|
274
|
+
|
|
275
|
+
Parallel-safe: yes. Upstream cost: 4–8.
|
|
276
|
+
Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
|
|
277
277
|
Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "recentLimit": 5 }`,
|
|
278
278
|
inputSchema: {
|
|
279
279
|
type: 'object',
|
|
@@ -707,31 +707,37 @@ async function resolveEventKey(ctx, game, q) {
|
|
|
707
707
|
}
|
|
708
708
|
export const eventCard = {
|
|
709
709
|
name: 'event_card',
|
|
710
|
-
description: `COMPOSITE event / fight-night card: identity, bout or match list, optional standings snippet.
|
|
711
|
-
|
|
712
|
-
When to use:
|
|
713
|
-
- UFC fight night / numbered event page ("UFC 300 card", "Fight Night")
|
|
714
|
-
- CS2 event hub with match list
|
|
715
|
-
- Tournament/event overview before match_preview drill-down
|
|
716
|
-
|
|
717
|
-
Bouts come back in card order — main event first, then prelims, then early prelims.
|
|
718
|
-
Each bout carries weightClass, titleBout, card placement, and (once fought) result
|
|
719
|
-
{ method, round, time, referee, winnerSlug }. Each corner carries images
|
|
720
|
-
{ headshotUrl, bodyImageUrl, imageUrl, proxiedImageUrl }, record, nickname, rank,
|
|
721
|
-
championStatus, country and flag when upstream supplies them. Use proxiedImageUrl in
|
|
722
|
-
browsers — the image host sends no CORS header. You do not need call_api per fighter for faces.
|
|
723
|
-
|
|
724
|
-
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.
|
|
725
|
-
|
|
726
|
-
Prefer match_preview for one bout/match briefing; match_summary for completed recaps; live_matches for live-only strips; standings alone for pure tables.
|
|
727
|
-
|
|
728
|
-
Do not use when: you only need live scores (live_matches); single finished match recap (match_summary); no event name/id yet and game unknown.
|
|
729
|
-
|
|
730
|
-
Tennis: pass a tournament id/slug to get the draw bracket —
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
710
|
+
description: `COMPOSITE event / fight-night card: identity, bout or match list, optional standings snippet.
|
|
711
|
+
|
|
712
|
+
When to use:
|
|
713
|
+
- UFC fight night / numbered event page ("UFC 300 card", "Fight Night")
|
|
714
|
+
- CS2 event hub with match list
|
|
715
|
+
- Tournament/event overview before match_preview drill-down
|
|
716
|
+
|
|
717
|
+
Bouts come back in card order — main event first, then prelims, then early prelims.
|
|
718
|
+
Each bout carries weightClass, titleBout, card placement, and (once fought) result
|
|
719
|
+
{ method, round, time, referee, winnerSlug }. Each corner carries images
|
|
720
|
+
{ headshotUrl, bodyImageUrl, imageUrl, proxiedImageUrl }, record, nickname, rank,
|
|
721
|
+
championStatus, country and flag when upstream supplies them. Use proxiedImageUrl in
|
|
722
|
+
browsers — the image host sends no CORS header. You do not need call_api per fighter for faces.
|
|
723
|
+
|
|
724
|
+
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.
|
|
725
|
+
|
|
726
|
+
Prefer match_preview for one bout/match briefing; match_summary for completed recaps; live_matches for live-only strips; standings alone for pure tables.
|
|
727
|
+
|
|
728
|
+
Do not use when: you only need live scores (live_matches); single finished match recap (match_summary); no event name/id yet and game unknown.
|
|
729
|
+
|
|
730
|
+
Tennis: pass a tournament id/slug to get the draw bracket — every main round from
|
|
731
|
+
the first to the Final (status completed / in_progress / not_started), then
|
|
732
|
+
qualifying (Q1..Q3). Each result carries winnerId/loserId/outcome (retirements
|
|
733
|
+
included), startTime (null with startTimeUnavailableReason when the source has no
|
|
734
|
+
clock), nextMatchId (the bracket edge) and bracketPosition; scheduled and live
|
|
735
|
+
matches sit in rounds[].pending. draw carries drawSize (real field size, byes
|
|
736
|
+
excluded, e.g. 56), byes, matchesExpected (main draw) and complete (true only
|
|
737
|
+
when every main round to the Final is played). This is the primary tennis path
|
|
738
|
+
here; there is no separate draw tool.
|
|
739
|
+
|
|
740
|
+
Parallel-safe: yes. Upstream cost: 1–4.
|
|
735
741
|
Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "includeStandings": false }`,
|
|
736
742
|
inputSchema: {
|
|
737
743
|
type: 'object',
|
|
@@ -1245,9 +1251,74 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1245
1251
|
const dd = asRecord(unwrapPayload(draw.data)) ?? {};
|
|
1246
1252
|
const rawRounds = Array.isArray(dd.rounds) ? dd.rounds : [];
|
|
1247
1253
|
const drawSize = typeof dd.draw_size === 'number' ? dd.draw_size : null;
|
|
1254
|
+
const apiSummary = asRecord(dd.summary);
|
|
1255
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
1248
1256
|
const flat = [];
|
|
1249
1257
|
let budget = drawMaxMatches ?? Number.POSITIVE_INFINITY;
|
|
1250
1258
|
let omittedMatches = 0;
|
|
1259
|
+
const drawMatch = (mr, roundName) => {
|
|
1260
|
+
const p1 = asRecord(mr.player1);
|
|
1261
|
+
const p2 = asRecord(mr.player2);
|
|
1262
|
+
const winnerId = pickString(mr.winner_id) ??
|
|
1263
|
+
(p1?.is_winner === true ? pickString(p1?.id) : p2?.is_winner === true ? pickString(p2?.id) : undefined) ??
|
|
1264
|
+
null;
|
|
1265
|
+
const loserId = pickString(mr.loser_id) ??
|
|
1266
|
+
(winnerId && winnerId === pickString(p1?.id) ? pickString(p2?.id) : winnerId ? pickString(p1?.id) : undefined) ??
|
|
1267
|
+
null;
|
|
1268
|
+
const row = {
|
|
1269
|
+
...mr,
|
|
1270
|
+
id: mr.match_id,
|
|
1271
|
+
round: roundName,
|
|
1272
|
+
tournament_name: event.name,
|
|
1273
|
+
status: 'completed',
|
|
1274
|
+
startTime: pickString(mr.start_time) ?? null,
|
|
1275
|
+
};
|
|
1276
|
+
flat.push(row);
|
|
1277
|
+
return presentSides({
|
|
1278
|
+
...normalizeMatch('tennis', row),
|
|
1279
|
+
startTime: pickString(mr.start_time) ?? null,
|
|
1280
|
+
score: pickString(mr.score) ?? null,
|
|
1281
|
+
matchNum: typeof mr.match_num === 'number' ? mr.match_num : null,
|
|
1282
|
+
winnerId,
|
|
1283
|
+
loserId,
|
|
1284
|
+
outcome: pickString(mr.outcome) ?? null,
|
|
1285
|
+
resultStatus: pickString(mr.status) ?? 'completed',
|
|
1286
|
+
matchDate: pickString(mr.match_date) ?? null,
|
|
1287
|
+
startTimeKnown: mr.start_time_known === true,
|
|
1288
|
+
...(pickString(mr.start_time_unavailable_reason)
|
|
1289
|
+
? { startTimeUnavailableReason: pickString(mr.start_time_unavailable_reason) }
|
|
1290
|
+
: {}),
|
|
1291
|
+
nextMatchId: pickString(mr.next_match_id) ?? null,
|
|
1292
|
+
...(pickString(mr.next_match_source) ? { nextMatchSource: pickString(mr.next_match_source) } : {}),
|
|
1293
|
+
bracketPosition: num(mr.bracket_position),
|
|
1294
|
+
nextSlot: asRecord(mr.next_slot)
|
|
1295
|
+
? {
|
|
1296
|
+
roundCode: pickString(asRecord(mr.next_slot)?.round_code) ?? null,
|
|
1297
|
+
position: num(asRecord(mr.next_slot)?.position),
|
|
1298
|
+
}
|
|
1299
|
+
: null,
|
|
1300
|
+
...(pickString(mr.source_round) ? { sourceRound: pickString(mr.source_round) } : {}),
|
|
1301
|
+
}, 'tennis');
|
|
1302
|
+
};
|
|
1303
|
+
const pendingMatch = (pr) => {
|
|
1304
|
+
const side = (v) => {
|
|
1305
|
+
const s = asRecord(v) ?? {};
|
|
1306
|
+
return { id: pickString(s.id) ?? null, name: pickString(s.name) ?? null, seed: s.seed ?? null };
|
|
1307
|
+
};
|
|
1308
|
+
return {
|
|
1309
|
+
matchId: pickString(pr.match_id) ?? null,
|
|
1310
|
+
status: pickString(pr.status) ?? null,
|
|
1311
|
+
player1: side(pr.player1),
|
|
1312
|
+
player2: side(pr.player2),
|
|
1313
|
+
startTime: pickString(pr.start_time) ?? null,
|
|
1314
|
+
startTimeKnown: pr.start_time_known === true,
|
|
1315
|
+
liveScore: pickString(pr.live_score) ?? null,
|
|
1316
|
+
winnerId: null,
|
|
1317
|
+
...(pickString(pr.source_winner_id) ? { sourceWinnerId: pickString(pr.source_winner_id) } : {}),
|
|
1318
|
+
nextMatchId: pickString(pr.next_match_id) ?? null,
|
|
1319
|
+
bracketPosition: num(pr.bracket_position),
|
|
1320
|
+
};
|
|
1321
|
+
};
|
|
1251
1322
|
drawRounds = rawRounds.map((rr) => {
|
|
1252
1323
|
const rec = asRecord(rr) ?? {};
|
|
1253
1324
|
const roundCode = pickString(rec.round_code, rec.roundCode) ?? null;
|
|
@@ -1256,58 +1327,100 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1256
1327
|
const kept = all.slice(0, Math.max(0, budget));
|
|
1257
1328
|
omittedMatches += all.length - kept.length;
|
|
1258
1329
|
budget -= kept.length;
|
|
1259
|
-
const matches = kept.map((mm) => {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
return presentSides({
|
|
1264
|
-
...normalizeMatch('tennis', row),
|
|
1265
|
-
score: pickString(mr.score) ?? null,
|
|
1266
|
-
matchNum: typeof mr.match_num === 'number' ? mr.match_num : null,
|
|
1267
|
-
nextMatchId: pickString(mr.next_match_id) ?? null,
|
|
1268
|
-
}, 'tennis');
|
|
1269
|
-
});
|
|
1330
|
+
const matches = kept.map((mm) => drawMatch(asRecord(mm) ?? {}, roundName));
|
|
1331
|
+
const pendingRaw = Array.isArray(rec.pending_matches) ? rec.pending_matches : [];
|
|
1332
|
+
const expected = num(rec.expected_matches) ?? expectedMatchesForRound(roundCode, drawSize);
|
|
1333
|
+
const apiStatus = pickString(rec.status) ?? null;
|
|
1270
1334
|
return {
|
|
1271
1335
|
roundCode,
|
|
1272
1336
|
roundName,
|
|
1273
1337
|
matches,
|
|
1338
|
+
pending: pendingRaw.map((p) => pendingMatch(asRecord(p) ?? {})),
|
|
1274
1339
|
returned: all.length,
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1340
|
+
included: kept.length,
|
|
1341
|
+
omitted: all.length - kept.length,
|
|
1342
|
+
expected,
|
|
1343
|
+
byes: num(rec.byes),
|
|
1344
|
+
walkovers: num(rec.walkovers),
|
|
1345
|
+
status: apiStatus ?? (all.length === 0 ? 'not_started' : expected !== null && all.length >= expected ? 'completed' : 'in_progress'),
|
|
1346
|
+
complete: typeof rec.complete === 'boolean' ? rec.complete : expected === null ? null : all.length >= expected,
|
|
1347
|
+
isQualifying: rec.is_qualifying === true || /^Q\d?$/.test(roundCode ?? ''),
|
|
1280
1348
|
};
|
|
1281
1349
|
});
|
|
1282
|
-
bouts = flat.slice(0, limit).map((row) =>
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1350
|
+
bouts = flat.slice(0, limit).map((row) => {
|
|
1351
|
+
const n = normalizeMatch('tennis', row);
|
|
1352
|
+
const r = row;
|
|
1353
|
+
const p1 = asRecord(r.player1);
|
|
1354
|
+
const p2 = asRecord(r.player2);
|
|
1355
|
+
return {
|
|
1356
|
+
...n,
|
|
1357
|
+
startTime: pickString(r.start_time) ?? null,
|
|
1358
|
+
winnerId: pickString(r.winner_id) ?? (p1?.is_winner === true ? pickString(p1?.id) : p2?.is_winner === true ? pickString(p2?.id) : undefined) ?? null,
|
|
1359
|
+
loserId: pickString(r.loser_id) ?? null,
|
|
1360
|
+
outcome: pickString(r.outcome) ?? null,
|
|
1361
|
+
nextMatchId: pickString(r.next_match_id) ?? null,
|
|
1362
|
+
};
|
|
1363
|
+
});
|
|
1364
|
+
if (!bouts.length) {
|
|
1365
|
+
const anyPending = drawRounds.some((r) => r.pending.length > 0);
|
|
1366
|
+
warnings.push(anyPending
|
|
1367
|
+
? 'No results yet: every match of this draw is still scheduled or in progress (see draw.rounds[].pending)'
|
|
1368
|
+
: 'No draw yet: the tournament has no archived results');
|
|
1369
|
+
}
|
|
1370
|
+
const mainRounds = drawRounds.filter((r) => !r.isQualifying);
|
|
1371
|
+
const expectedTotalLocal = drawRounds.reduce((sum, r) => sum + (r.expected ?? 0), 0);
|
|
1286
1372
|
const returnedTotal = drawRounds.reduce((sum, r) => sum + r.returned, 0);
|
|
1373
|
+
const mainReturned = mainRounds.reduce((sum, r) => sum + r.returned, 0);
|
|
1287
1374
|
const shortRounds = drawRounds
|
|
1288
|
-
.filter((r) => r.complete === false)
|
|
1375
|
+
.filter((r) => r.complete === false && r.status !== 'not_started')
|
|
1289
1376
|
.map((r) => `${r.roundCode ?? r.roundName ?? '?'} ${r.returned}/${r.expected}`);
|
|
1290
1377
|
const haveCodes = new Set(drawRounds.map((r) => (r.roundCode ?? '').toUpperCase()));
|
|
1291
|
-
const missingRounds = ['F', 'SF', 'QF']
|
|
1292
|
-
|
|
1293
|
-
|
|
1378
|
+
const missingRounds = ['F', 'SF', 'QF'].filter((c) => !haveCodes.has(c));
|
|
1379
|
+
const roundsNotStarted = mainRounds.filter((r) => r.status === 'not_started').map((r) => r.roundCode);
|
|
1380
|
+
const roundsInProgress = mainRounds.filter((r) => r.status === 'in_progress').map((r) => r.roundCode);
|
|
1381
|
+
const roundsIncompleteArchive = mainRounds
|
|
1382
|
+
.filter((r) => r.status === 'incomplete_archive')
|
|
1383
|
+
.map((r) => r.roundCode);
|
|
1384
|
+
const matchesExpected = num(apiSummary?.main_draw_matches_expected) ?? (expectedTotalLocal || null);
|
|
1385
|
+
const complete = typeof apiSummary?.complete === 'boolean'
|
|
1386
|
+
? apiSummary.complete &&
|
|
1387
|
+
shortRounds.length === 0 &&
|
|
1388
|
+
missingRounds.length === 0 &&
|
|
1389
|
+
roundsNotStarted.length === 0
|
|
1390
|
+
: matchesExpected !== null &&
|
|
1391
|
+
mainReturned >= matchesExpected &&
|
|
1392
|
+
shortRounds.length === 0 &&
|
|
1393
|
+
missingRounds.length === 0;
|
|
1394
|
+
if (drawSize !== null && event.drawSize !== drawSize) {
|
|
1395
|
+
event = { ...event, drawSizeRecorded: event.drawSize ?? null, drawSize };
|
|
1396
|
+
}
|
|
1294
1397
|
drawSummary = {
|
|
1295
1398
|
drawSize,
|
|
1399
|
+
drawSizeSource: pickString(dd.draw_size_source) ?? null,
|
|
1400
|
+
bracketSize: num(dd.bracket_size),
|
|
1401
|
+
byes: num(dd.byes),
|
|
1296
1402
|
roundCount: drawRounds.length,
|
|
1403
|
+
status: pickString(apiSummary?.status) ?? (complete ? 'completed' : returnedTotal ? 'in_progress' : 'not_started'),
|
|
1297
1404
|
matchesReturned: returnedTotal,
|
|
1298
|
-
|
|
1299
|
-
|
|
1405
|
+
mainDrawMatchesReturned: mainReturned,
|
|
1406
|
+
qualifyingMatchesReturned: returnedTotal - mainReturned,
|
|
1407
|
+
matchesExpected,
|
|
1408
|
+
matchesPending: drawRounds.reduce((sum, r) => sum + r.pending.length, 0),
|
|
1409
|
+
complete,
|
|
1300
1410
|
shortRounds,
|
|
1301
1411
|
missingRounds,
|
|
1412
|
+
roundsNotStarted,
|
|
1413
|
+
roundsInProgress,
|
|
1414
|
+
roundsIncompleteArchive,
|
|
1302
1415
|
totalMatchesInCard: flat.length,
|
|
1303
1416
|
returnedInBouts: bouts.length,
|
|
1304
1417
|
truncated: omittedMatches > 0,
|
|
1305
1418
|
omittedMatches,
|
|
1306
1419
|
maxMatches: drawMaxMatches,
|
|
1307
|
-
note: '
|
|
1420
|
+
note: 'matchesExpected counts the MAIN draw (byes excluded) and is compared with mainDrawMatchesReturned; matchesReturned also counts qualifying. Per round: returned = archived results, included = how many of them this response carries (drawMaxMatches), pending = scheduled / in-progress matches. complete is true only when every main round to the Final is completed.',
|
|
1308
1421
|
};
|
|
1309
1422
|
if (shortRounds.length) {
|
|
1310
|
-
warnings.push(`Draw is partial: ${shortRounds.join(', ')}. The archive does not hold every match of this tournament yet
|
|
1423
|
+
warnings.push(`Draw is partial: ${shortRounds.join(', ')}. ${roundsInProgress.length ? 'The tournament is in progress.' : 'The archive does not hold every match of this tournament yet.'}`);
|
|
1311
1424
|
}
|
|
1312
1425
|
if (omittedMatches > 0) {
|
|
1313
1426
|
warnings.push(`drawMaxMatches=${drawMaxMatches} dropped ${omittedMatches} early-round match(es) from draw.rounds; raise or omit it for the complete bracket.`);
|
package/dist/tools/match.js
CHANGED
|
@@ -139,6 +139,14 @@ function matchCore(game, matchId, raw) {
|
|
|
139
139
|
},
|
|
140
140
|
}
|
|
141
141
|
: {}),
|
|
142
|
+
...(game === 'tennis'
|
|
143
|
+
? {
|
|
144
|
+
canonicalId: pickString(r.canonical_id, r.id, r.match_id) ?? null,
|
|
145
|
+
tournamentDisplayName: pickString(r.tournament_display_name, asRecord(r.tournament)?.display_name, r.tournament_name) ?? null,
|
|
146
|
+
resultAvailable: typeof r.result_available === 'boolean' ? r.result_available : Boolean(pickString(r.winner_id)),
|
|
147
|
+
resultUnavailableReason: pickString(r.result_unavailable_reason) ?? null,
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
142
150
|
};
|
|
143
151
|
return core;
|
|
144
152
|
}
|
|
@@ -163,18 +171,18 @@ function primaryPath(game, matchId) {
|
|
|
163
171
|
}
|
|
164
172
|
export const matchSummary = {
|
|
165
173
|
name: 'match_summary',
|
|
166
|
-
description: `COMPOSITE match card: scoreline, key context, player performances, and VOD/demo links when available.
|
|
167
|
-
|
|
168
|
-
When to use:
|
|
169
|
-
- Match recap / default match UI
|
|
170
|
-
- After user selects a live or completed matchId
|
|
171
|
-
|
|
172
|
-
Prefer over match_details for chat answers and default UIs.
|
|
173
|
-
Prefer match_details for timelines, full map trees, live state, advanced packages.
|
|
174
|
-
|
|
175
|
-
Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
|
|
176
|
-
|
|
177
|
-
Parallel-safe: yes. Upstream cost: 2–5.
|
|
174
|
+
description: `COMPOSITE match card: scoreline, key context, player performances, and VOD/demo links when available.
|
|
175
|
+
|
|
176
|
+
When to use:
|
|
177
|
+
- Match recap / default match UI
|
|
178
|
+
- After user selects a live or completed matchId
|
|
179
|
+
|
|
180
|
+
Prefer over match_details for chat answers and default UIs.
|
|
181
|
+
Prefer match_details for timelines, full map trees, live state, advanced packages.
|
|
182
|
+
|
|
183
|
+
Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
|
|
184
|
+
|
|
185
|
+
Parallel-safe: yes. Upstream cost: 2–5.
|
|
178
186
|
Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includePlayerStats": true }`,
|
|
179
187
|
inputSchema: {
|
|
180
188
|
type: 'object',
|
|
@@ -331,7 +339,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
331
339
|
const p1g = typeof sr.player1_games === 'number' ? sr.player1_games : null;
|
|
332
340
|
const p2g = typeof sr.player2_games === 'number' ? sr.player2_games : null;
|
|
333
341
|
return {
|
|
334
|
-
setNumber: typeof sr.
|
|
342
|
+
setNumber: typeof sr.set_number === 'number' ? sr.set_number : typeof sr.set_num === 'number' ? sr.set_num : null,
|
|
335
343
|
score: pickString(sr.score) ?? (p1g != null && p2g != null ? `${p1g}-${p2g}` : null),
|
|
336
344
|
player1Games: p1g,
|
|
337
345
|
player2Games: p2g,
|
|
@@ -558,10 +566,27 @@ export function summarizeTennisMatchStats(data) {
|
|
|
558
566
|
const stats = asRecord(payload.stats);
|
|
559
567
|
const sets = Array.isArray(payload.sets) ? payload.sets : [];
|
|
560
568
|
const toNum = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
569
|
+
const liveRows = Array.isArray(payload.stats) ? payload.stats.map((x) => asRecord(x) ?? {}) : null;
|
|
570
|
+
const liveStats = liveRows
|
|
571
|
+
? liveRows.map((r) => ({
|
|
572
|
+
side: toNum(r.side),
|
|
573
|
+
key: pickString(r.key) ?? null,
|
|
574
|
+
name: pickString(r.name) ?? null,
|
|
575
|
+
category: pickString(r.category) ?? null,
|
|
576
|
+
kind: pickString(r.kind) ?? null,
|
|
577
|
+
numerator: toNum(r.numerator),
|
|
578
|
+
denominator: toNum(r.denominator),
|
|
579
|
+
pct: toNum(r.pct),
|
|
580
|
+
display: pickString(r.display) ?? null,
|
|
581
|
+
}))
|
|
582
|
+
: null;
|
|
561
583
|
return {
|
|
562
584
|
matchId: pickString(payload.match_id) ?? null,
|
|
563
585
|
score: pickString(payload.score) ?? null,
|
|
564
|
-
sidesKeyedBy: 'outcome',
|
|
586
|
+
sidesKeyedBy: liveStats ? 'side' : 'outcome',
|
|
587
|
+
...(liveStats ? { liveStats } : {}),
|
|
588
|
+
statsAvailable: typeof payload.stats_available === 'boolean' ? payload.stats_available : null,
|
|
589
|
+
statsUnavailableCode: pickString(payload.stats_unavailable_code) ?? null,
|
|
565
590
|
players: stats
|
|
566
591
|
? {
|
|
567
592
|
winner: asRecord(stats.winner) ?? null,
|
|
@@ -571,7 +596,7 @@ export function summarizeTennisMatchStats(data) {
|
|
|
571
596
|
sets: sets.map((s) => {
|
|
572
597
|
const sr = asRecord(s) ?? {};
|
|
573
598
|
return {
|
|
574
|
-
setNumber: toNum(sr.
|
|
599
|
+
setNumber: toNum(sr.set_number) ?? toNum(sr.set_num),
|
|
575
600
|
winnerGames: toNum(sr.winner_games),
|
|
576
601
|
loserGames: toNum(sr.loser_games),
|
|
577
602
|
player1Games: toNum(sr.player1_games),
|
|
@@ -586,23 +611,23 @@ export function summarizeTennisMatchStats(data) {
|
|
|
586
611
|
}
|
|
587
612
|
export const matchDetails = {
|
|
588
613
|
name: 'match_details',
|
|
589
|
-
description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
|
|
590
|
-
|
|
591
|
-
When to use:
|
|
592
|
-
- Analyst deep dive
|
|
593
|
-
- Live in-game window (LoL/CS2/UFC)
|
|
594
|
-
- Full demo list
|
|
595
|
-
|
|
596
|
-
Prefer over match_summary only when summary is insufficient.
|
|
597
|
-
Prefer match_summary for short answers and default cards.
|
|
598
|
-
|
|
599
|
-
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
600
|
-
|
|
601
|
-
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
602
|
-
UFC betting lines: sections:["odds"] (opt-in, never in the default set).
|
|
603
|
-
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
604
|
-
|
|
605
|
-
Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
|
|
614
|
+
description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
|
|
615
|
+
|
|
616
|
+
When to use:
|
|
617
|
+
- Analyst deep dive
|
|
618
|
+
- Live in-game window (LoL/CS2/UFC)
|
|
619
|
+
- Full demo list
|
|
620
|
+
|
|
621
|
+
Prefer over match_summary only when summary is insufficient.
|
|
622
|
+
Prefer match_summary for short answers and default cards.
|
|
623
|
+
|
|
624
|
+
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
625
|
+
|
|
626
|
+
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
627
|
+
UFC betting lines: sections:["odds"] (opt-in, never in the default set).
|
|
628
|
+
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
629
|
+
|
|
630
|
+
Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
|
|
606
631
|
Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "includeLiveState": false }`,
|
|
607
632
|
inputSchema: {
|
|
608
633
|
type: 'object',
|
package/dist/tools/normalize.js
CHANGED
|
@@ -136,7 +136,7 @@ function imagesFrom(game, ...sources) {
|
|
|
136
136
|
headshotUrl: pick('headshotUrl', 'headshot', 'portrait_url'),
|
|
137
137
|
bodyImageUrl: pick('bodyImageUrl', 'fullBodyImageUrl'),
|
|
138
138
|
imageUrl: pick('imageUrl', 'image', 'photoUrl', 'logoUrl', 'logo', 'portrait_url'),
|
|
139
|
-
proxiedImageUrl: pick('proxiedImageUrl', 'proxiedHeadshotUrl'),
|
|
139
|
+
proxiedImageUrl: pick('proxiedImageUrl', 'proxiedHeadshotUrl', 'proxied_image_url'),
|
|
140
140
|
};
|
|
141
141
|
if (!images.proxiedImageUrl) {
|
|
142
142
|
images.proxiedImageUrl =
|
|
@@ -508,7 +508,7 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
508
508
|
const p1g = numOrNull(sr.player1_games);
|
|
509
509
|
const p2g = numOrNull(sr.player2_games);
|
|
510
510
|
return {
|
|
511
|
-
setNumber: numOrNull(sr.
|
|
511
|
+
setNumber: numOrNull(sr.set_number) ?? numOrNull(sr.set_num),
|
|
512
512
|
score: pickString(sr.score) ?? (p1g !== null && p2g !== null ? `${p1g}-${p2g}` : null),
|
|
513
513
|
player1Games: p1g,
|
|
514
514
|
player2Games: p2g,
|
|
@@ -557,6 +557,9 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
557
557
|
...(tennisSets
|
|
558
558
|
? { sets: tennisSets, gameScore: tennisGameScore, server: tennisServer, currentSet: tennisCurrentSet }
|
|
559
559
|
: {}),
|
|
560
|
+
...(game === 'tennis' && typeof r.is_simulated === 'boolean'
|
|
561
|
+
? { isSimulated: r.is_simulated, liveFeed: pickString(r.live_feed) ?? null }
|
|
562
|
+
: {}),
|
|
560
563
|
...(isCancelled
|
|
561
564
|
? { cancelled: { isCancelled: true, reason: pickString(r.cancellationReason) ?? null } }
|
|
562
565
|
: {}),
|
package/dist/tools/player.js
CHANGED
|
@@ -35,7 +35,7 @@ function identityFrom(game, raw, idHint, slugHint) {
|
|
|
35
35
|
headshotUrl: pickString(r.headshotUrl, r.headshot, r.portrait_url) ?? null,
|
|
36
36
|
bodyImageUrl: pickString(r.bodyImageUrl, r.fullBodyImageUrl) ?? null,
|
|
37
37
|
imageUrl: pickString(r.imageUrl, r.image, r.photoUrl, r.portrait_url) ?? null,
|
|
38
|
-
proxiedImageUrl: pickString(r.proxiedImageUrl, r.proxiedHeadshotUrl) ?? null,
|
|
38
|
+
proxiedImageUrl: pickString(r.proxiedImageUrl, r.proxiedHeadshotUrl, r.proxied_image_url) ?? null,
|
|
39
39
|
license: pickString(r.portrait_license) ?? null,
|
|
40
40
|
attribution: pickString(r.portrait_attribution) ?? null,
|
|
41
41
|
sourceUrl: pickString(r.portrait_source_url) ?? null,
|
|
@@ -44,17 +44,17 @@ function identityFrom(game, raw, idHint, slugHint) {
|
|
|
44
44
|
}
|
|
45
45
|
export const playerProfile = {
|
|
46
46
|
name: 'player_profile',
|
|
47
|
-
description: `Player or UFC fighter profile: identity, current team, recent matches, and form/trends/radar when available.
|
|
48
|
-
|
|
49
|
-
When to use:
|
|
50
|
-
- "How is X playing lately?"
|
|
51
|
-
- Player page scaffold; form inputs for previews
|
|
52
|
-
|
|
53
|
-
Prefer over: manual multi-call career/trends/matches via call_api.
|
|
54
|
-
|
|
55
|
-
Do not use when: full team roster needed → team_profile; unresolved name → resolve_entity first.
|
|
56
|
-
|
|
57
|
-
Parallel-safe: yes. Upstream cost: 2–5.
|
|
47
|
+
description: `Player or UFC fighter profile: identity, current team, recent matches, and form/trends/radar when available.
|
|
48
|
+
|
|
49
|
+
When to use:
|
|
50
|
+
- "How is X playing lately?"
|
|
51
|
+
- Player page scaffold; form inputs for previews
|
|
52
|
+
|
|
53
|
+
Prefer over: manual multi-call career/trends/matches via call_api.
|
|
54
|
+
|
|
55
|
+
Do not use when: full team roster needed → team_profile; unresolved name → resolve_entity first.
|
|
56
|
+
|
|
57
|
+
Parallel-safe: yes. Upstream cost: 2–5.
|
|
58
58
|
Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includeTrends": true }`,
|
|
59
59
|
inputSchema: {
|
|
60
60
|
type: 'object',
|
|
@@ -70,9 +70,9 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
70
70
|
includeTrends: boolSchema('Include trends/radar/career enrichment when available.', true),
|
|
71
71
|
view: {
|
|
72
72
|
type: 'string',
|
|
73
|
-
enum: ['summary', 'full'],
|
|
73
|
+
enum: ['summary', 'full', 'compact'],
|
|
74
74
|
default: 'summary',
|
|
75
|
-
description: 'Payload weight. Default summary.',
|
|
75
|
+
description: 'Payload weight. Default summary. compact (tennis): recentMatches as one slim row per match from the player side (result, opponent{id,name,rank}, score, event, round) instead of full match rows that repeat both players three times.',
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
78
|
},
|
|
@@ -124,7 +124,7 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
124
124
|
const idOrSlug = playerId || slug;
|
|
125
125
|
const recentLimit = clampInt(args.recentLimit, 10, 1, 25);
|
|
126
126
|
const includeTrends = args.includeTrends !== false;
|
|
127
|
-
const view = args.view === 'full' ? 'full' : 'summary';
|
|
127
|
+
const view = args.view === 'full' ? 'full' : args.view === 'compact' ? 'compact' : 'summary';
|
|
128
128
|
const partial = [];
|
|
129
129
|
let upstreamCalls = 0;
|
|
130
130
|
let rateLimit = {};
|
|
@@ -295,7 +295,7 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
295
295
|
}
|
|
296
296
|
tasks.push((async () => {
|
|
297
297
|
const res = await fetchJson(ctx, `/tennis/players/${encodeURIComponent(idOrSlug)}/matches`, {
|
|
298
|
-
query: { page_size: recentLimit },
|
|
298
|
+
query: { page_size: recentLimit, ...(view === 'compact' ? { view: 'summary' } : {}) },
|
|
299
299
|
});
|
|
300
300
|
upstreamCalls += 1;
|
|
301
301
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
@@ -534,6 +534,7 @@ function normalizeFormMatch(row) {
|
|
|
534
534
|
result: pickString(r.result) ?? null,
|
|
535
535
|
opponentId: pickString(r.opponent_id) ?? null,
|
|
536
536
|
opponentRank: num(r.opponent_rank),
|
|
537
|
+
opponentRankDate: pickString(r.opponent_rank_date) ?? null,
|
|
537
538
|
score: pickString(r.score) ?? null,
|
|
538
539
|
surface: pickString(r.surface) ?? null,
|
|
539
540
|
tournamentId: pickString(r.tournament_id) ?? null,
|
|
@@ -541,17 +542,17 @@ function normalizeFormMatch(row) {
|
|
|
541
542
|
}
|
|
542
543
|
export const playerForm = {
|
|
543
544
|
name: 'player_form',
|
|
544
|
-
description: `Tennis player form: W/L record over the last N completed matches, current win/loss streak, and per-match rows (opponent, score, surface). Optional surface filter (Hard/Clay/Grass).
|
|
545
|
-
|
|
546
|
-
When to use:
|
|
547
|
-
- "How is X playing lately?"; current streak; surface-specific record (e.g. clay last 5).
|
|
548
|
-
|
|
549
|
-
Prefer over: player_profile (identity + career aggregates, no streak); raw form via call_api for agent-normalized rows.
|
|
550
|
-
|
|
551
|
-
Do not use when: career totals/titles → player_profile with game tennis (wires /stats); ranking deltas → rankings_movers.
|
|
552
|
-
|
|
553
|
-
Tennis-only. Limit defaults to 10 (max 50, matching the API). Upstream rows arrive newest-first; the streak is the leading run of that order.
|
|
554
|
-
|
|
545
|
+
description: `Tennis player form: W/L record over the last N completed matches, current win/loss streak, and per-match rows (opponent, score, surface). Optional surface filter (Hard/Clay/Grass).
|
|
546
|
+
|
|
547
|
+
When to use:
|
|
548
|
+
- "How is X playing lately?"; current streak; surface-specific record (e.g. clay last 5).
|
|
549
|
+
|
|
550
|
+
Prefer over: player_profile (identity + career aggregates, no streak); raw form via call_api for agent-normalized rows.
|
|
551
|
+
|
|
552
|
+
Do not use when: career totals/titles → player_profile with game tennis (wires /stats); ranking deltas → rankings_movers.
|
|
553
|
+
|
|
554
|
+
Tennis-only. Limit defaults to 10 (max 50, matching the API). Upstream rows arrive newest-first; the streak is the leading run of that order.
|
|
555
|
+
|
|
555
556
|
Parallel-safe: yes. Upstream cost: 1.`,
|
|
556
557
|
inputSchema: {
|
|
557
558
|
type: 'object',
|
|
@@ -700,18 +701,18 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
700
701
|
};
|
|
701
702
|
export const playerMatches = {
|
|
702
703
|
name: 'player_matches',
|
|
703
|
-
description: `Tennis player match log: every archived match for one player, newest first, with opponent, tournament, round, surface and score.
|
|
704
|
-
|
|
705
|
-
When to use:
|
|
706
|
-
- "Show me Shelton's last 20 matches"; full match history; every match at a given tournament; filtering a player's record by surface.
|
|
707
|
-
|
|
708
|
-
Prefer over: player_form (a W/L summary with a small window and a streak, not the log); call_api for /tennis/players/{id}/matches.
|
|
709
|
-
|
|
710
|
-
Do not use when: aggregate totals, titles, or win% → player_stats; identity → player_profile; one match's box score → match_details.
|
|
711
|
-
|
|
712
|
-
Tennis-only. Limit defaults to 20 (max 50). Rows arrive newest-first. Use
|
|
713
|
-
surface to narrow to Hard/Clay/Grass; tournamentId pins one event.
|
|
714
|
-
|
|
704
|
+
description: `Tennis player match log: every archived match for one player, newest first, with opponent, tournament, round, surface and score.
|
|
705
|
+
|
|
706
|
+
When to use:
|
|
707
|
+
- "Show me Shelton's last 20 matches"; full match history; every match at a given tournament; filtering a player's record by surface.
|
|
708
|
+
|
|
709
|
+
Prefer over: player_form (a W/L summary with a small window and a streak, not the log); call_api for /tennis/players/{id}/matches.
|
|
710
|
+
|
|
711
|
+
Do not use when: aggregate totals, titles, or win% → player_stats; identity → player_profile; one match's box score → match_details.
|
|
712
|
+
|
|
713
|
+
Tennis-only. Limit defaults to 20 (max 50). Rows arrive newest-first. Use
|
|
714
|
+
surface to narrow to Hard/Clay/Grass; tournamentId pins one event.
|
|
715
|
+
|
|
715
716
|
Parallel-safe: yes. Upstream cost: 1.`,
|
|
716
717
|
inputSchema: {
|
|
717
718
|
type: 'object',
|
|
@@ -869,16 +870,16 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
869
870
|
};
|
|
870
871
|
export const playerStats = {
|
|
871
872
|
name: 'player_stats',
|
|
872
|
-
description: `Tennis player career statistics: W/L totals, win percentage, titles, Grand Slam and Masters titles, plus per-surface and per-level breakdowns. Optional surface/year filters.
|
|
873
|
-
|
|
874
|
-
When to use:
|
|
875
|
-
- "How many titles does Alcaraz have?"; career win%; Grand Slam title count; record on clay; a season-scoped record.
|
|
876
|
-
|
|
877
|
-
Prefer over: player_profile (identity + a summary block, no breakdowns); player_matches (individual rows, no aggregates).
|
|
878
|
-
|
|
879
|
-
Do not use when: the match log → player_matches; ranking over time → player_rankings_history.
|
|
880
|
-
|
|
881
|
-
Tennis-only. Filters are surface (Hard/Clay/Grass/Carpet) and yearFrom/yearTo.
|
|
873
|
+
description: `Tennis player career statistics: W/L totals, win percentage, titles, Grand Slam and Masters titles, plus per-surface and per-level breakdowns. Optional surface/year filters.
|
|
874
|
+
|
|
875
|
+
When to use:
|
|
876
|
+
- "How many titles does Alcaraz have?"; career win%; Grand Slam title count; record on clay; a season-scoped record.
|
|
877
|
+
|
|
878
|
+
Prefer over: player_profile (identity + a summary block, no breakdowns); player_matches (individual rows, no aggregates).
|
|
879
|
+
|
|
880
|
+
Do not use when: the match log → player_matches; ranking over time → player_rankings_history.
|
|
881
|
+
|
|
882
|
+
Tennis-only. Filters are surface (Hard/Clay/Grass/Carpet) and yearFrom/yearTo.
|
|
882
883
|
Upstream cost: 1.`,
|
|
883
884
|
inputSchema: {
|
|
884
885
|
type: 'object',
|
package/dist/tools/schedule.js
CHANGED
|
@@ -10,8 +10,11 @@ function normalizeScheduled(row) {
|
|
|
10
10
|
const p2 = asRecord(r.player2) ?? asRecord(r.away) ?? {};
|
|
11
11
|
return {
|
|
12
12
|
matchId: pickString(r.id, r.match_id) ?? null,
|
|
13
|
+
canonicalId: pickString(r.canonical_id, r.id, r.match_id) ?? null,
|
|
13
14
|
tournamentId: pickString(r.tournament_id) ?? null,
|
|
14
15
|
tournamentName: pickString(r.tournament_name, r.tournament) ?? null,
|
|
16
|
+
tournamentDisplayName: pickString(r.tournament_display_name, r.tournament_name) ?? null,
|
|
17
|
+
tournamentShortName: pickString(r.tournament_short_name) ?? null,
|
|
15
18
|
tour: pickString(r.tour) ?? null,
|
|
16
19
|
level: pickString(r.level, r.tier) ?? null,
|
|
17
20
|
surface: pickString(r.surface) ?? null,
|
|
@@ -20,9 +23,23 @@ function normalizeScheduled(row) {
|
|
|
20
23
|
startsAt: pickString(r.starts_at, r.commence_time, r.match_date) ?? null,
|
|
21
24
|
startTimeKnown: typeof r.start_time_known === 'boolean' ? r.start_time_known : null,
|
|
22
25
|
status: pickString(r.status, r.outcome) ?? null,
|
|
23
|
-
player1: {
|
|
24
|
-
|
|
26
|
+
player1: {
|
|
27
|
+
id: pickString(p1.id, p1.player_id) ?? null,
|
|
28
|
+
canonicalId: pickString(p1.canonical_id, p1.id, p1.player_id) ?? null,
|
|
29
|
+
name: pickString(p1.name) ?? null,
|
|
30
|
+
},
|
|
31
|
+
player2: {
|
|
32
|
+
id: pickString(p2.id, p2.player_id) ?? null,
|
|
33
|
+
canonicalId: pickString(p2.canonical_id, p2.id, p2.player_id) ?? null,
|
|
34
|
+
name: pickString(p2.name) ?? null,
|
|
35
|
+
},
|
|
36
|
+
winnerId: pickString(r.winner_id) ?? null,
|
|
25
37
|
score: pickString(r.score, r.score_raw) ?? null,
|
|
38
|
+
liveScore: pickString(r.live_score) ?? null,
|
|
39
|
+
resultAvailable: typeof r.result_available === 'boolean'
|
|
40
|
+
? r.result_available
|
|
41
|
+
: Boolean(pickString(r.winner_id) && pickString(r.score, r.score_raw)),
|
|
42
|
+
resultUnavailableReason: pickString(r.result_unavailable_reason) ?? null,
|
|
26
43
|
};
|
|
27
44
|
}
|
|
28
45
|
function isPlayed(row) {
|
|
@@ -116,7 +133,7 @@ Parallel-safe: yes. Upstream cost: 1 or 2.`,
|
|
|
116
133
|
const limit = clampInt(args.limit, 20, 1, 50);
|
|
117
134
|
const wantScheduled = includeRaw === 'both' || includeRaw === 'scheduled';
|
|
118
135
|
const wantCompleted = includeRaw === 'both' || includeRaw === 'completed';
|
|
119
|
-
const upstreamPage =
|
|
136
|
+
const upstreamPage = 500;
|
|
120
137
|
const settled = await Promise.allSettled([
|
|
121
138
|
wantScheduled
|
|
122
139
|
? fetchJson(ctx, '/tennis/schedule', { query: { date, limit: upstreamPage } })
|
|
@@ -197,14 +214,14 @@ Parallel-safe: yes. Upstream cost: 1 or 2.`,
|
|
|
197
214
|
const scheduledWindow = scheduled.slice(0, limit);
|
|
198
215
|
const completedWindow = completed.slice(0, limit);
|
|
199
216
|
const completedBySchedule = scheduled.filter(isPlayed);
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
.filter((id) => Boolean(id)));
|
|
217
|
+
const idOf = (row) => pickString(asRecord(row)?.canonical_id, asRecord(row)?.id, asRecord(row)?.match_id);
|
|
218
|
+
const completedIds = new Set(completed.map(idOf).filter((id) => Boolean(id)));
|
|
203
219
|
const overlapIds = [
|
|
204
220
|
...new Set(completedBySchedule
|
|
205
|
-
.map(
|
|
221
|
+
.map(idOf)
|
|
206
222
|
.filter((id) => typeof id === 'string' && completedIds.has(id))),
|
|
207
223
|
];
|
|
224
|
+
const resultMissing = scheduled.filter((row) => isPlayed(row) && asRecord(row)?.result_available === false).length;
|
|
208
225
|
const statusCounts = {};
|
|
209
226
|
for (const row of scheduled) {
|
|
210
227
|
const key = (pickString(asRecord(row)?.status, asRecord(row)?.outcome) ?? 'UNKNOWN').toUpperCase();
|
|
@@ -222,6 +239,7 @@ Parallel-safe: yes. Upstream cost: 1 or 2.`,
|
|
|
222
239
|
completedCount: completedTotal ?? completed.length,
|
|
223
240
|
completedReturned: completedWindow.length,
|
|
224
241
|
statusBreakdown: statusCounts,
|
|
242
|
+
playedWithoutResult: resultMissing,
|
|
225
243
|
overlap: {
|
|
226
244
|
count: overlapIds.length,
|
|
227
245
|
matchIds: overlapIds.slice(0, 50),
|
package/dist/tools/team.js
CHANGED
|
@@ -858,9 +858,10 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
858
858
|
],
|
|
859
859
|
});
|
|
860
860
|
}
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
861
|
+
const tennisQuery = { player1_id: tA.id, player2_id: tB.id };
|
|
862
|
+
if (args.limit !== undefined && args.limit !== null)
|
|
863
|
+
tennisQuery.limit = limit;
|
|
864
|
+
const res = await fetchJson(ctx, '/tennis/h2h', { query: tennisQuery });
|
|
864
865
|
upstreamCalls += 1;
|
|
865
866
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
866
867
|
if (!res.ok) {
|
|
@@ -893,7 +894,16 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
893
894
|
sideA: { idOrSlug: tA.id, name: sideAName },
|
|
894
895
|
sideB: { idOrSlug: tB.id, name: sideBName },
|
|
895
896
|
h2h,
|
|
896
|
-
|
|
897
|
+
meetingsReturned: Array.isArray(h2h.matches) ? h2h.matches.length : null,
|
|
898
|
+
meetingsTotal: typeof asRecord(h2h.matches_pagination)?.total === 'number'
|
|
899
|
+
? asRecord(h2h.matches_pagination)?.total
|
|
900
|
+
: typeof asRecord(h2h.summary)?.total_matches === 'number'
|
|
901
|
+
? asRecord(h2h.summary)?.total_matches
|
|
902
|
+
: null,
|
|
903
|
+
notes: [
|
|
904
|
+
'Record and meetings come from the first-class /tennis/h2h endpoint',
|
|
905
|
+
'limit bounds h2h.matches only; summary and every breakdown (surface, level, format) cover all meetings and sum to total_matches (h2h.breakdown_totals).',
|
|
906
|
+
],
|
|
897
907
|
},
|
|
898
908
|
});
|
|
899
909
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cito-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Standalone MCP server for the Cito esports and sports API — curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://cito.gg",
|