cito-mcp 0.3.19 → 0.3.21
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 +7 -5
- package/dist/tools/cs2.js +421 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/insight.js +28 -1
- package/dist/tools/leaderboard.js +427 -0
- package/dist/tools/meta.js +201 -1
- package/dist/tools/player.js +195 -1
- package/dist/tools/rankings.js +228 -0
- package/dist/tools/standings.js +4 -2
- package/dist/tools/team.js +183 -1
- package/package.json +2 -2
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* leaderboard_aces (TENNIS-18 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* Tennis-only: season aces leaderboard from real match_stats aggregates
|
|
5
|
+
* (GET /tennis/leaderboards/aces). Upstream shape (verified 2026-09-09):
|
|
6
|
+
* { season, tour, stat: 'aces', items: [{ player_id, full_name, aces, matches }], total }.
|
|
7
|
+
* Upstream defaults: season=2026, tour=null (both tours combined), and it
|
|
8
|
+
* 422s on a bad tour or a non-year season — both surface as VALIDATION here.
|
|
9
|
+
*/
|
|
10
|
+
import { asRecord, clampInt, fetchJson, gameNotIncludedHint, pickString, } from '../client.js';
|
|
11
|
+
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
12
|
+
import { gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
13
|
+
const DEFAULT_SEASON = 2026;
|
|
14
|
+
const MIN_SEASON = 1968;
|
|
15
|
+
const MAX_SEASON = 2100;
|
|
16
|
+
function parseOptionalTour(args) {
|
|
17
|
+
const rawTour = typeof args.tour === 'string' ? args.tour.trim() : '';
|
|
18
|
+
const rawDivision = typeof args.division === 'string' ? args.division.trim() : '';
|
|
19
|
+
if (rawTour && rawDivision && rawTour.toUpperCase() !== rawDivision.toUpperCase()) {
|
|
20
|
+
return { error: `tour ('${rawTour}') and division ('${rawDivision}') disagree; pass one of ATP|WTA` };
|
|
21
|
+
}
|
|
22
|
+
const raw = rawTour || rawDivision;
|
|
23
|
+
if (!raw)
|
|
24
|
+
return {};
|
|
25
|
+
const upper = raw.toUpperCase();
|
|
26
|
+
if (upper !== 'ATP' && upper !== 'WTA') {
|
|
27
|
+
return { error: `tour must be ATP or WTA (got '${raw}')` };
|
|
28
|
+
}
|
|
29
|
+
return { tour: upper };
|
|
30
|
+
}
|
|
31
|
+
function parseSeason(args) {
|
|
32
|
+
const raw = args.season;
|
|
33
|
+
if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
34
|
+
return { season: DEFAULT_SEASON };
|
|
35
|
+
}
|
|
36
|
+
const num = typeof raw === 'number' ? raw : Number(String(raw).trim());
|
|
37
|
+
if (!Number.isInteger(num) || num < MIN_SEASON || num > MAX_SEASON) {
|
|
38
|
+
return { error: `season must be a YYYY year ${MIN_SEASON}-${MAX_SEASON} (got '${String(raw)}')` };
|
|
39
|
+
}
|
|
40
|
+
return { season: num };
|
|
41
|
+
}
|
|
42
|
+
function normalizeAceRow(row) {
|
|
43
|
+
const r = asRecord(row) ?? {};
|
|
44
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
45
|
+
return {
|
|
46
|
+
playerId: pickString(r.player_id) ?? null,
|
|
47
|
+
playerName: pickString(r.full_name, r.player_name) ?? null,
|
|
48
|
+
aces: num(r.aces),
|
|
49
|
+
matches: num(r.matches),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export const leaderboardAces = {
|
|
53
|
+
name: 'leaderboard_aces',
|
|
54
|
+
description: `Tennis season aces leaderboard: most aces served in a season, from real match-stats aggregates.
|
|
55
|
+
|
|
56
|
+
When to use:
|
|
57
|
+
- Who leads the tour in aces this season; season serving-leader tables.
|
|
58
|
+
|
|
59
|
+
Prefer over: raw leaderboard via call_api for agent-normalized rows.
|
|
60
|
+
|
|
61
|
+
Do not use when: ranking position → standings with game tennis; week-over-week movement → rankings_movers; one player's recent form → player_form.
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
Parallel-safe: yes. Upstream cost: 1.`,
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
required: ['game'],
|
|
70
|
+
properties: {
|
|
71
|
+
game: gameSchema({ allowAll: false, required: true }),
|
|
72
|
+
tour: stringSchema('ATP or WTA tour; omit for the combined board. Synonym: division.', 'WTA'),
|
|
73
|
+
division: stringSchema('Synonym for tour (ATP or WTA), matching the standings tool spelling.'),
|
|
74
|
+
season: {
|
|
75
|
+
type: 'integer',
|
|
76
|
+
minimum: MIN_SEASON,
|
|
77
|
+
maximum: MAX_SEASON,
|
|
78
|
+
default: DEFAULT_SEASON,
|
|
79
|
+
description: 'Season year (YYYY). Default 2026.',
|
|
80
|
+
},
|
|
81
|
+
limit: limitSchema({ default: 10, max: 100 }),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
handler: async (args, ctx) => {
|
|
85
|
+
const started = Date.now();
|
|
86
|
+
const requestId = newRequestId();
|
|
87
|
+
const gameParse = parseGame(args.game, { allowAll: false, required: true });
|
|
88
|
+
if (gameParse.error || !gameParse.game || !isPrimaryGame(gameParse.game)) {
|
|
89
|
+
return errorEnvelope({
|
|
90
|
+
code: gameParse.error?.includes('unsupported') ? 'UNSUPPORTED_GAME' : 'VALIDATION',
|
|
91
|
+
message: gameParse.error ?? 'game is required',
|
|
92
|
+
game: null,
|
|
93
|
+
source: 'leaderboard_aces',
|
|
94
|
+
requestId,
|
|
95
|
+
tookMs: Date.now() - started,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const game = gameParse.game;
|
|
99
|
+
if (game !== 'tennis') {
|
|
100
|
+
return errorEnvelope({
|
|
101
|
+
code: 'NOT_IMPLEMENTED',
|
|
102
|
+
message: `leaderboard_aces is tennis-only (game '${game}' has no aces leaderboard)`,
|
|
103
|
+
game,
|
|
104
|
+
source: 'leaderboard_aces',
|
|
105
|
+
requestId,
|
|
106
|
+
tookMs: Date.now() - started,
|
|
107
|
+
recover: [
|
|
108
|
+
'Use standings for the latest snapshot of this game',
|
|
109
|
+
'Retry leaderboard_aces with game tennis and an optional tour ATP|WTA',
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const tourParse = parseOptionalTour(args);
|
|
114
|
+
if (tourParse.error) {
|
|
115
|
+
return errorEnvelope({
|
|
116
|
+
code: 'VALIDATION',
|
|
117
|
+
message: tourParse.error,
|
|
118
|
+
game,
|
|
119
|
+
source: 'leaderboard_aces',
|
|
120
|
+
requestId,
|
|
121
|
+
tookMs: Date.now() - started,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const seasonParse = parseSeason(args);
|
|
125
|
+
if (seasonParse.error || seasonParse.season === undefined) {
|
|
126
|
+
return errorEnvelope({
|
|
127
|
+
code: 'VALIDATION',
|
|
128
|
+
message: seasonParse.error ?? 'season is invalid',
|
|
129
|
+
game,
|
|
130
|
+
source: 'leaderboard_aces',
|
|
131
|
+
requestId,
|
|
132
|
+
tookMs: Date.now() - started,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const limit = clampInt(args.limit, 10, 1, 100);
|
|
136
|
+
const res = await fetchJson(ctx, '/tennis/leaderboards/aces', {
|
|
137
|
+
query: {
|
|
138
|
+
season: seasonParse.season,
|
|
139
|
+
limit,
|
|
140
|
+
...(tourParse.tour ? { tour: tourParse.tour } : {}),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
return errorEnvelope({
|
|
145
|
+
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
146
|
+
message: `Aces leaderboard failed (HTTP ${res.status})`,
|
|
147
|
+
game,
|
|
148
|
+
source: 'leaderboard_aces',
|
|
149
|
+
requestId,
|
|
150
|
+
tookMs: Date.now() - started,
|
|
151
|
+
upstreamCalls: 1,
|
|
152
|
+
httpStatus: res.status,
|
|
153
|
+
rateLimit: res.headers,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const root = asRecord(res.data);
|
|
157
|
+
const body = asRecord(root?.data) ?? root ?? {};
|
|
158
|
+
const items = (Array.isArray(body.items) ? body.items : []).map(normalizeAceRow);
|
|
159
|
+
const total = typeof body.total === 'number' ? body.total : items.length;
|
|
160
|
+
return successEnvelope({
|
|
161
|
+
pagination: {
|
|
162
|
+
limit,
|
|
163
|
+
offset: 0,
|
|
164
|
+
total,
|
|
165
|
+
hasMore: false,
|
|
166
|
+
nextCursor: null,
|
|
167
|
+
prevCursor: null,
|
|
168
|
+
},
|
|
169
|
+
source: 'leaderboard_aces',
|
|
170
|
+
game,
|
|
171
|
+
requestId,
|
|
172
|
+
tookMs: Date.now() - started,
|
|
173
|
+
upstreamCalls: 1,
|
|
174
|
+
rateLimit: res.headers,
|
|
175
|
+
data: {
|
|
176
|
+
title: `${tourParse.tour ?? 'Combined'} aces leaderboard ${seasonParse.season}`,
|
|
177
|
+
season: typeof body.season === 'number' ? body.season : seasonParse.season,
|
|
178
|
+
tour: pickString(body.tour) ?? tourParse.tour ?? null,
|
|
179
|
+
stat: pickString(body.stat) ?? 'aces',
|
|
180
|
+
items,
|
|
181
|
+
total,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
function normalizePctRow(numKey, denKey) {
|
|
187
|
+
return (row) => {
|
|
188
|
+
const r = asRecord(row) ?? {};
|
|
189
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
190
|
+
return {
|
|
191
|
+
playerId: pickString(r.player_id) ?? null,
|
|
192
|
+
playerName: pickString(r.full_name, r.player_name) ?? null,
|
|
193
|
+
attempts: num(r[numKey]),
|
|
194
|
+
total: num(r[denKey]),
|
|
195
|
+
pct: num(r.pct),
|
|
196
|
+
matches: num(r.matches),
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function makePctLeaderboardTool(spec) {
|
|
201
|
+
const normalizeRow = normalizePctRow(spec.numKey, spec.denKey);
|
|
202
|
+
return {
|
|
203
|
+
name: spec.name,
|
|
204
|
+
description: `Tennis season ${spec.titleNoun} leaderboard: ${spec.blurb}
|
|
205
|
+
|
|
206
|
+
When to use:
|
|
207
|
+
- ${spec.blurb} season tables; clutch/serve-efficiency leader queries.
|
|
208
|
+
|
|
209
|
+
Prefer over: raw leaderboard via call_api for agent-normalized rows.
|
|
210
|
+
|
|
211
|
+
Do not use when: ranking position → standings with game tennis; week-over-week movement → rankings_movers; one player's recent form → player_form.
|
|
212
|
+
|
|
213
|
+
Tennis-only. Season defaults to 2026; tour (ATP|WTA) is optional — omit it for the combined board. ${spec.minAttemptsNote}
|
|
214
|
+
|
|
215
|
+
Parallel-safe: yes. Upstream cost: 1.`,
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
required: ['game'],
|
|
220
|
+
properties: {
|
|
221
|
+
game: gameSchema({ allowAll: false, required: true }),
|
|
222
|
+
tour: stringSchema('ATP or WTA tour; omit for the combined board. Synonym: division.', 'WTA'),
|
|
223
|
+
division: stringSchema('Synonym for tour (ATP or WTA), matching the standings tool spelling.'),
|
|
224
|
+
season: {
|
|
225
|
+
type: 'integer',
|
|
226
|
+
minimum: MIN_SEASON,
|
|
227
|
+
maximum: MAX_SEASON,
|
|
228
|
+
default: DEFAULT_SEASON,
|
|
229
|
+
description: 'Season year (YYYY). Default 2026.',
|
|
230
|
+
},
|
|
231
|
+
limit: limitSchema({ default: 10, max: 100 }),
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
handler: async (args, ctx) => {
|
|
235
|
+
const started = Date.now();
|
|
236
|
+
const requestId = newRequestId();
|
|
237
|
+
const gameParse = parseGame(args.game, { allowAll: false, required: true });
|
|
238
|
+
if (gameParse.error || !gameParse.game || !isPrimaryGame(gameParse.game)) {
|
|
239
|
+
return errorEnvelope({
|
|
240
|
+
code: gameParse.error?.includes('unsupported') ? 'UNSUPPORTED_GAME' : 'VALIDATION',
|
|
241
|
+
message: gameParse.error ?? 'game is required',
|
|
242
|
+
game: null,
|
|
243
|
+
source: spec.name,
|
|
244
|
+
requestId,
|
|
245
|
+
tookMs: Date.now() - started,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const game = gameParse.game;
|
|
249
|
+
if (game !== 'tennis') {
|
|
250
|
+
return errorEnvelope({
|
|
251
|
+
code: 'NOT_IMPLEMENTED',
|
|
252
|
+
message: `${spec.name} is tennis-only (game '${game}' has no ${spec.titleNoun} leaderboard)`,
|
|
253
|
+
game,
|
|
254
|
+
source: spec.name,
|
|
255
|
+
requestId,
|
|
256
|
+
tookMs: Date.now() - started,
|
|
257
|
+
recover: [
|
|
258
|
+
'Use standings for the latest snapshot of this game',
|
|
259
|
+
`Retry ${spec.name} with game tennis and an optional tour ATP|WTA`,
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const tourParse = parseOptionalTour(args);
|
|
264
|
+
if (tourParse.error) {
|
|
265
|
+
return errorEnvelope({
|
|
266
|
+
code: 'VALIDATION',
|
|
267
|
+
message: tourParse.error,
|
|
268
|
+
game,
|
|
269
|
+
source: spec.name,
|
|
270
|
+
requestId,
|
|
271
|
+
tookMs: Date.now() - started,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const seasonParse = parseSeason(args);
|
|
275
|
+
if (seasonParse.error || seasonParse.season === undefined) {
|
|
276
|
+
return errorEnvelope({
|
|
277
|
+
code: 'VALIDATION',
|
|
278
|
+
message: seasonParse.error ?? 'season is invalid',
|
|
279
|
+
game,
|
|
280
|
+
source: spec.name,
|
|
281
|
+
requestId,
|
|
282
|
+
tookMs: Date.now() - started,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const limit = clampInt(args.limit, 10, 1, 100);
|
|
286
|
+
const res = await fetchJson(ctx, `/tennis${spec.path}`, {
|
|
287
|
+
query: {
|
|
288
|
+
season: seasonParse.season,
|
|
289
|
+
limit,
|
|
290
|
+
...(tourParse.tour ? { tour: tourParse.tour } : {}),
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
if (!res.ok) {
|
|
294
|
+
return errorEnvelope({
|
|
295
|
+
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
296
|
+
message: `${spec.titleNoun} leaderboard failed (HTTP ${res.status})`,
|
|
297
|
+
game,
|
|
298
|
+
source: spec.name,
|
|
299
|
+
requestId,
|
|
300
|
+
tookMs: Date.now() - started,
|
|
301
|
+
upstreamCalls: 1,
|
|
302
|
+
httpStatus: res.status,
|
|
303
|
+
rateLimit: res.headers,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const root = asRecord(res.data);
|
|
307
|
+
const body = asRecord(root?.data) ?? root ?? {};
|
|
308
|
+
const items = (Array.isArray(body.items) ? body.items : []).map(normalizeRow);
|
|
309
|
+
const total = typeof body.total === 'number' ? body.total : items.length;
|
|
310
|
+
return successEnvelope({
|
|
311
|
+
pagination: {
|
|
312
|
+
limit,
|
|
313
|
+
offset: 0,
|
|
314
|
+
total,
|
|
315
|
+
hasMore: false,
|
|
316
|
+
nextCursor: null,
|
|
317
|
+
prevCursor: null,
|
|
318
|
+
},
|
|
319
|
+
source: spec.name,
|
|
320
|
+
game,
|
|
321
|
+
requestId,
|
|
322
|
+
tookMs: Date.now() - started,
|
|
323
|
+
upstreamCalls: 1,
|
|
324
|
+
rateLimit: res.headers,
|
|
325
|
+
data: {
|
|
326
|
+
title: `${tourParse.tour ?? 'Combined'} ${spec.titleNoun} leaderboard ${seasonParse.season}`,
|
|
327
|
+
season: typeof body.season === 'number' ? body.season : seasonParse.season,
|
|
328
|
+
tour: pickString(body.tour) ?? tourParse.tour ?? null,
|
|
329
|
+
stat: pickString(body.stat) ?? spec.stat,
|
|
330
|
+
items,
|
|
331
|
+
total,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
export const leaderboardBreakPointsSaved = makePctLeaderboardTool({
|
|
338
|
+
name: 'leaderboard_break_points_saved',
|
|
339
|
+
path: '/leaderboards/break-points-saved',
|
|
340
|
+
stat: 'break_points_saved',
|
|
341
|
+
numKey: 'break_points_saved',
|
|
342
|
+
denKey: 'break_points_faced',
|
|
343
|
+
titleNoun: 'break-points-saved',
|
|
344
|
+
blurb: 'Share of break points saved per player, from real match-stats aggregates, ordered highest pct first.',
|
|
345
|
+
minAttemptsNote: 'Only players facing at least 50 break points qualify.',
|
|
346
|
+
});
|
|
347
|
+
export const leaderboardFirstServeWon = makePctLeaderboardTool({
|
|
348
|
+
name: 'leaderboard_first_serve_won',
|
|
349
|
+
path: '/leaderboards/1st-serve-won',
|
|
350
|
+
stat: 'first_serve_won',
|
|
351
|
+
numKey: 'first_serve_points_won',
|
|
352
|
+
denKey: 'first_serves_in',
|
|
353
|
+
titleNoun: 'first-serve-won',
|
|
354
|
+
blurb: 'Share of first-serve points won per player, from real match-stats aggregates, ordered highest pct first.',
|
|
355
|
+
minAttemptsNote: 'Only players with at least 300 first serves in qualify.',
|
|
356
|
+
});
|
|
357
|
+
export const leaderboardTiebreakWinPct = makePctLeaderboardTool({
|
|
358
|
+
name: 'leaderboard_tiebreak_win_pct',
|
|
359
|
+
path: '/leaderboards/tiebreak-win-pct',
|
|
360
|
+
stat: 'tiebreak_win_pct',
|
|
361
|
+
numKey: 'tiebreaks_won',
|
|
362
|
+
denKey: 'tiebreaks_played',
|
|
363
|
+
titleNoun: 'tiebreak-win-pct',
|
|
364
|
+
blurb: 'Share of classic 7-point tiebreaks won per player, from real match-set aggregates, ordered highest pct first.',
|
|
365
|
+
minAttemptsNote: 'Only players contesting at least 10 tiebreaks qualify.',
|
|
366
|
+
});
|
|
367
|
+
export const leaderboardBreakConversion = makePctLeaderboardTool({
|
|
368
|
+
name: 'leaderboard_break_conversion',
|
|
369
|
+
path: '/leaderboards/break-conversion',
|
|
370
|
+
stat: 'break_conversion',
|
|
371
|
+
numKey: 'break_points_won',
|
|
372
|
+
denKey: 'break_opportunities',
|
|
373
|
+
titleNoun: 'break-conversion',
|
|
374
|
+
blurb: 'Share of break opportunities converted per player, from real match-stats aggregates read return-side, ordered highest pct first.',
|
|
375
|
+
minAttemptsNote: 'Only players with at least 50 break opportunities qualify.',
|
|
376
|
+
});
|
|
377
|
+
export const leaderboardReturnGamesWon = makePctLeaderboardTool({
|
|
378
|
+
name: 'leaderboard_return_games_won',
|
|
379
|
+
path: '/leaderboards/return-games-won',
|
|
380
|
+
stat: 'return_games_won',
|
|
381
|
+
numKey: 'return_games_won',
|
|
382
|
+
denKey: 'return_games_played',
|
|
383
|
+
titleNoun: 'return-games-won',
|
|
384
|
+
blurb: 'Share of return games won per player (breaks over opponent service games played), from real match-stats aggregates read return-side, ordered highest pct first.',
|
|
385
|
+
minAttemptsNote: 'Only players with at least 100 return games played qualify.',
|
|
386
|
+
});
|
|
387
|
+
export const leaderboardFinalsRecord = makePctLeaderboardTool({
|
|
388
|
+
name: 'leaderboard_finals_record',
|
|
389
|
+
path: '/leaderboards/finals-record',
|
|
390
|
+
stat: 'finals_record',
|
|
391
|
+
numKey: 'finals_won',
|
|
392
|
+
denKey: 'finals_played',
|
|
393
|
+
titleNoun: 'finals-record',
|
|
394
|
+
blurb: 'Share of finals won (titles clutch) per player, from real completed finals rows, ordered highest pct first.',
|
|
395
|
+
minAttemptsNote: 'Only players contesting at least 3 finals qualify.',
|
|
396
|
+
});
|
|
397
|
+
export const leaderboardComebackWins = makePctLeaderboardTool({
|
|
398
|
+
name: 'leaderboard_comeback_wins',
|
|
399
|
+
path: '/leaderboards/comeback-wins',
|
|
400
|
+
stat: 'comeback_wins',
|
|
401
|
+
numKey: 'comebacks_won',
|
|
402
|
+
denKey: 'matches_with_first_set',
|
|
403
|
+
titleNoun: 'comeback-wins',
|
|
404
|
+
blurb: 'Share of matches won after losing the first set per player, from real first-set rows joined to completed matches, ordered highest pct first.',
|
|
405
|
+
minAttemptsNote: 'Only players with first-set data on at least 20 matches qualify.',
|
|
406
|
+
});
|
|
407
|
+
export const leaderboardDecidingSetRecord = makePctLeaderboardTool({
|
|
408
|
+
name: 'leaderboard_deciding_set_record',
|
|
409
|
+
path: '/leaderboards/deciding-set-record',
|
|
410
|
+
stat: 'deciding_set_record',
|
|
411
|
+
numKey: 'deciders_won',
|
|
412
|
+
denKey: 'deciders_played',
|
|
413
|
+
titleNoun: 'deciding-set-record',
|
|
414
|
+
blurb: 'Share of deciding sets (full-length final sets) won per player, from real match rows, ordered highest pct first.',
|
|
415
|
+
minAttemptsNote: 'Only players contesting at least 10 deciders qualify.',
|
|
416
|
+
});
|
|
417
|
+
export const leaderboardTools = [
|
|
418
|
+
leaderboardAces,
|
|
419
|
+
leaderboardBreakPointsSaved,
|
|
420
|
+
leaderboardFirstServeWon,
|
|
421
|
+
leaderboardTiebreakWinPct,
|
|
422
|
+
leaderboardBreakConversion,
|
|
423
|
+
leaderboardReturnGamesWon,
|
|
424
|
+
leaderboardDecidingSetRecord,
|
|
425
|
+
leaderboardComebackWins,
|
|
426
|
+
leaderboardFinalsRecord,
|
|
427
|
+
];
|
package/dist/tools/meta.js
CHANGED
|
@@ -116,6 +116,16 @@ const TOOL_CATALOG = [
|
|
|
116
116
|
preferOver: ['agent-side double match-list filtering'],
|
|
117
117
|
doNotUse: 'Single-side form only → team_profile or player_profile',
|
|
118
118
|
},
|
|
119
|
+
{
|
|
120
|
+
name: 'h2h_matrix',
|
|
121
|
+
outcome: 'Tennis multi-player H2H grid: every pair series record in one matrix',
|
|
122
|
+
parallelSafe: true,
|
|
123
|
+
games: ['tennis'],
|
|
124
|
+
jobs: ['h2h', 'preview'],
|
|
125
|
+
exampleArgs: { game: 'tennis', players: ['atp_207989', 'atp_100644'] },
|
|
126
|
+
preferOver: ['N head_to_head calls for an N-player field', 'raw matrix via call_api'],
|
|
127
|
+
doNotUse: 'Two-player deep-dive with tiebreak splits → head_to_head; season leaders → leaderboard_*',
|
|
128
|
+
},
|
|
119
129
|
{
|
|
120
130
|
name: 'standings',
|
|
121
131
|
outcome: 'League/event standings or world/division rankings',
|
|
@@ -126,6 +136,116 @@ const TOOL_CATALOG = [
|
|
|
126
136
|
preferOver: ['raw standings via call_api'],
|
|
127
137
|
doNotUse: 'Single team form → team_profile; live scores → live_matches',
|
|
128
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'rankings_movers',
|
|
141
|
+
outcome: 'Tennis ranking movers: climbers/fallers plus new entries and drop-outs',
|
|
142
|
+
parallelSafe: true,
|
|
143
|
+
games: ['tennis'],
|
|
144
|
+
jobs: ['standings'],
|
|
145
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', direction: 'up', limit: 20 },
|
|
146
|
+
preferOver: ['raw movers via call_api'],
|
|
147
|
+
doNotUse: 'Latest snapshot without deltas → standings',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'player_form',
|
|
151
|
+
outcome: 'Tennis recent form: W/L record, current streak, per-match rows',
|
|
152
|
+
parallelSafe: true,
|
|
153
|
+
games: ['tennis'],
|
|
154
|
+
jobs: ['player_form', 'preview', 'match_page'],
|
|
155
|
+
exampleArgs: { game: 'tennis', playerId: 'atp_207989', surface: 'Clay', limit: 5 },
|
|
156
|
+
preferOver: ['raw form via call_api'],
|
|
157
|
+
doNotUse: 'Career totals/titles → player_profile; ranking deltas → rankings_movers',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'leaderboard_aces',
|
|
161
|
+
outcome: 'Tennis season aces leaderboard: most aces served, ordered first',
|
|
162
|
+
parallelSafe: true,
|
|
163
|
+
games: ['tennis'],
|
|
164
|
+
jobs: ['standings'],
|
|
165
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
166
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
167
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'leaderboard_break_points_saved',
|
|
171
|
+
outcome: 'Tennis season break-points-saved pct leaderboard, min 50 faced',
|
|
172
|
+
parallelSafe: true,
|
|
173
|
+
games: ['tennis'],
|
|
174
|
+
jobs: ['standings'],
|
|
175
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
176
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
177
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'leaderboard_first_serve_won',
|
|
181
|
+
outcome: 'Tennis season first-serve-won pct leaderboard, min 300 in',
|
|
182
|
+
parallelSafe: true,
|
|
183
|
+
games: ['tennis'],
|
|
184
|
+
jobs: ['standings'],
|
|
185
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
186
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
187
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'leaderboard_tiebreak_win_pct',
|
|
191
|
+
outcome: 'Tennis season tiebreak-win-pct leaderboard, min 10 tiebreaks',
|
|
192
|
+
parallelSafe: true,
|
|
193
|
+
games: ['tennis'],
|
|
194
|
+
jobs: ['standings'],
|
|
195
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
196
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
197
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'leaderboard_break_conversion',
|
|
201
|
+
outcome: 'Tennis season break-conversion pct leaderboard, min 50 opportunities',
|
|
202
|
+
parallelSafe: true,
|
|
203
|
+
games: ['tennis'],
|
|
204
|
+
jobs: ['standings'],
|
|
205
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
206
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
207
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'leaderboard_return_games_won',
|
|
211
|
+
outcome: 'Tennis season return-games-won pct leaderboard, min 100 played',
|
|
212
|
+
parallelSafe: true,
|
|
213
|
+
games: ['tennis'],
|
|
214
|
+
jobs: ['standings'],
|
|
215
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
216
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
217
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'leaderboard_deciding_set_record',
|
|
221
|
+
outcome: 'Tennis season deciding-set-record pct leaderboard, min 10 deciders',
|
|
222
|
+
parallelSafe: true,
|
|
223
|
+
games: ['tennis'],
|
|
224
|
+
jobs: ['standings'],
|
|
225
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
226
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
227
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'leaderboard_comeback_wins',
|
|
231
|
+
outcome: 'Tennis season comeback-wins pct leaderboard, min 20 first sets',
|
|
232
|
+
parallelSafe: true,
|
|
233
|
+
games: ['tennis'],
|
|
234
|
+
jobs: ['standings'],
|
|
235
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
236
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
237
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'leaderboard_finals_record',
|
|
241
|
+
outcome: 'Tennis season finals-record pct leaderboard, min 3 finals',
|
|
242
|
+
parallelSafe: true,
|
|
243
|
+
games: ['tennis'],
|
|
244
|
+
jobs: ['standings'],
|
|
245
|
+
exampleArgs: { game: 'tennis', tour: 'ATP', season: 2026, limit: 10 },
|
|
246
|
+
preferOver: ['raw leaderboard via call_api'],
|
|
247
|
+
doNotUse: 'Ranking position → standings; week-over-week movement → rankings_movers',
|
|
248
|
+
},
|
|
129
249
|
{
|
|
130
250
|
name: 'match_preview',
|
|
131
251
|
outcome: 'Pre-match briefing: sides, rosters/form, H2H stub',
|
|
@@ -152,6 +272,86 @@ const TOOL_CATALOG = [
|
|
|
152
272
|
],
|
|
153
273
|
doNotUse: 'Live-only strip → live_matches; single match recap → match_summary',
|
|
154
274
|
},
|
|
275
|
+
{
|
|
276
|
+
name: 'cs2_live_scoreboard',
|
|
277
|
+
outcome: 'Live real-time in-game scoreboard for an active CS2 match',
|
|
278
|
+
parallelSafe: true,
|
|
279
|
+
games: ['cs2'],
|
|
280
|
+
jobs: ['live_board', 'match_page'],
|
|
281
|
+
exampleArgs: { matchId: 'cs2-match-2397733' },
|
|
282
|
+
preferOver: ['polling call_api for live scoreboard'],
|
|
283
|
+
doNotUse: 'For completed matches without live state',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'cs2_round_economy',
|
|
287
|
+
outcome: 'Round-by-round tactical economy and buy types for a CS2 map',
|
|
288
|
+
parallelSafe: true,
|
|
289
|
+
games: ['cs2'],
|
|
290
|
+
jobs: ['match_page'],
|
|
291
|
+
exampleArgs: { mapId: '7716-map-1' },
|
|
292
|
+
preferOver: ['raw round event parsing'],
|
|
293
|
+
doNotUse: 'For overall match scorelines → match_summary',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: 'cs2_opening_duels',
|
|
297
|
+
outcome: 'First blood and opening duel statistics by player or leaderboard',
|
|
298
|
+
parallelSafe: true,
|
|
299
|
+
games: ['cs2'],
|
|
300
|
+
jobs: ['player_form', 'match_page'],
|
|
301
|
+
exampleArgs: { playerId: 'chucky', limit: 20 },
|
|
302
|
+
preferOver: ['manual kill log counting'],
|
|
303
|
+
doNotUse: 'For general player stats → player_profile',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'cs2_clutches',
|
|
307
|
+
outcome: '1vX clutch situation success records (1v1 to 1v5)',
|
|
308
|
+
parallelSafe: true,
|
|
309
|
+
games: ['cs2'],
|
|
310
|
+
jobs: ['player_form', 'match_page'],
|
|
311
|
+
exampleArgs: { playerId: 'xm1nd', limit: 20 },
|
|
312
|
+
preferOver: ['manual demo parse analysis'],
|
|
313
|
+
doNotUse: 'For regular player stats → player_profile',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'cs2_team_map_stats',
|
|
317
|
+
outcome: 'Team win rates and CT/T side splits across competitive map pool',
|
|
318
|
+
parallelSafe: true,
|
|
319
|
+
games: ['cs2'],
|
|
320
|
+
jobs: ['team_page', 'preview'],
|
|
321
|
+
exampleArgs: { teamId: 'hltv-team-4608', days: 90 },
|
|
322
|
+
preferOver: ['manual match history scraping'],
|
|
323
|
+
doNotUse: 'For non-CS2 games',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'cs2_utility_leaderboard',
|
|
327
|
+
outcome: 'Sub-second leaderboard of CS2 utility and flashbang efficiency',
|
|
328
|
+
parallelSafe: true,
|
|
329
|
+
games: ['cs2'],
|
|
330
|
+
jobs: ['standings', 'player_form'],
|
|
331
|
+
exampleArgs: { limit: 20 },
|
|
332
|
+
preferOver: ['unindexed round event aggregation'],
|
|
333
|
+
doNotUse: 'For standard weapon ratings',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'cs2_veto_sequence',
|
|
337
|
+
outcome: 'Map pick/ban sequence and veto history for a CS2 match',
|
|
338
|
+
parallelSafe: true,
|
|
339
|
+
games: ['cs2'],
|
|
340
|
+
jobs: ['match_page', 'preview'],
|
|
341
|
+
exampleArgs: { matchId: '2397603' },
|
|
342
|
+
preferOver: ['scraping match page text'],
|
|
343
|
+
doNotUse: 'For map scores → match_summary',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'cs2_roster_transfers',
|
|
347
|
+
outcome: 'Recent pro CS2 roster changes, benchings, stand-ins, and transfers',
|
|
348
|
+
parallelSafe: true,
|
|
349
|
+
games: ['cs2'],
|
|
350
|
+
jobs: ['team_page', 'schedule'],
|
|
351
|
+
exampleArgs: { limit: 20 },
|
|
352
|
+
preferOver: ['manual news feed scanning'],
|
|
353
|
+
doNotUse: 'For current active rosters → team_profile',
|
|
354
|
+
},
|
|
155
355
|
{
|
|
156
356
|
name: 'list_routes',
|
|
157
357
|
outcome: 'Index of raw REST routes from the live OpenAPI spec (method, path, summary)',
|
|
@@ -179,7 +379,7 @@ const JOBS = [
|
|
|
179
379
|
{ id: 'team_page', description: 'Team/org profile screen', recommendedTools: ['resolve_entity', 'team_profile'] },
|
|
180
380
|
{ id: 'player_form', description: 'Player/fighter form card', recommendedTools: ['resolve_entity', 'player_profile'] },
|
|
181
381
|
{ id: 'standings', description: 'Tables and rankings', recommendedTools: ['standings'] },
|
|
182
|
-
{ id: 'h2h', description: 'Historical rivalry record', recommendedTools: ['head_to_head'] },
|
|
382
|
+
{ id: 'h2h', description: 'Historical rivalry record', recommendedTools: ['head_to_head', 'h2h_matrix'] },
|
|
183
383
|
{ id: 'schedule', description: 'Upcoming fixtures/events', recommendedTools: ['upcoming_schedule', 'event_card'] },
|
|
184
384
|
{ id: 'preview', description: 'Pre-match briefing', recommendedTools: ['match_preview'] },
|
|
185
385
|
{ id: 'event_card', description: 'Event / fight-night card page', recommendedTools: ['event_card', 'resolve_entity', 'match_preview'] },
|