cito-mcp 0.4.3 → 0.4.5
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 +6 -7
- package/dist/client.js +0 -35
- package/dist/envelope.js +3 -13
- package/dist/http.js +0 -16
- package/dist/index.js +0 -39
- package/dist/install.js +0 -44
- package/dist/instructions.js +0 -4
- package/dist/scrub.js +67 -0
- package/dist/tools/cs2.js +1 -48
- package/dist/tools/index.js +0 -6
- package/dist/tools/insight.js +4 -136
- package/dist/tools/leaderboard.js +0 -21
- package/dist/tools/live.js +4 -95
- package/dist/tools/match.js +94 -123
- package/dist/tools/meta.js +7 -62
- package/dist/tools/normalize.js +6 -229
- package/dist/tools/odds.js +0 -66
- package/dist/tools/player.js +1 -135
- package/dist/tools/rankings.js +0 -38
- package/dist/tools/resolve.js +0 -136
- package/dist/tools/schedule.js +0 -55
- package/dist/tools/standings.js +1 -45
- package/dist/tools/team.js +39 -120
- package/dist/tools/tournaments.js +0 -37
- package/dist/tools/types.js +0 -17
- package/dist/version.js +0 -17
- package/package.json +1 -1
package/dist/tools/match.js
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* match_summary + match_details
|
|
3
|
-
*/
|
|
4
1
|
import { extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
2
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
3
|
import { normalizeMatch, normalizeUfcMethod, presentSides, tennisSetCompleted } from './normalize.js';
|
|
7
|
-
import { summarizeTennisOdds } from './odds.js';
|
|
8
4
|
import { boolSchema, gameSchema, isPrimaryGame, parseGame, stringSchema, } from './types.js';
|
|
9
5
|
async function getSection(ctx, path, query) {
|
|
10
6
|
return fetchJson(ctx, path, { query });
|
|
11
7
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Series length. The LoL feed states it as `strategy: "Bo5"`, which nothing was
|
|
14
|
-
* reading, so every match reported bestOf: null. Accept the spelled forms and
|
|
15
|
-
* the object shape other titles use, and fall back to the declared game count.
|
|
16
|
-
* Only 1/3/5/7/9 are real series lengths, so anything else is refused rather
|
|
17
|
-
* than guessed.
|
|
18
|
-
*/
|
|
19
8
|
export function parseBestOf(r) {
|
|
20
9
|
const fromNumber = (v) => {
|
|
21
10
|
const n = Number(v);
|
|
@@ -33,7 +22,6 @@ export function parseBestOf(r) {
|
|
|
33
22
|
return n;
|
|
34
23
|
}
|
|
35
24
|
}
|
|
36
|
-
// { type: "bestOf", count: 5 }
|
|
37
25
|
if (strat && typeof strat === 'object') {
|
|
38
26
|
const o = strat;
|
|
39
27
|
const n = fromNumber(o.count) ?? fromNumber(o.value) ?? fromNumber(o.bestOf);
|
|
@@ -62,11 +50,6 @@ function matchCore(game, matchId, raw) {
|
|
|
62
50
|
const scoreline = m.team1 || m.team2
|
|
63
51
|
? `${m.team1?.name ?? '?'} ${s1 ?? '-'} : ${s2 ?? '-'} ${m.team2?.name ?? '?'}`
|
|
64
52
|
: null;
|
|
65
|
-
/**
|
|
66
|
-
* Tennis: `score` is the set-by-set game score ("4-6 6-3 6-3 7-5"), which is
|
|
67
|
-
* a DIFFERENT fact from the sets-won scoreline. "3 : 1" is the result; it
|
|
68
|
-
* cannot distinguish 6-0 6-0 from three tiebreaks. Both are carried.
|
|
69
|
-
*/
|
|
70
53
|
const scoreDetail = game === 'tennis' ? (pickString(r.score, r.score_raw) ?? null) : null;
|
|
71
54
|
const core = {
|
|
72
55
|
matchId: m.matchId !== 'unknown' ? m.matchId : matchId,
|
|
@@ -86,11 +69,6 @@ function matchCore(game, matchId, raw) {
|
|
|
86
69
|
team2: m.team2,
|
|
87
70
|
event: m.event,
|
|
88
71
|
league: m.league,
|
|
89
|
-
// method/methodRaw/weightClass/referee are combat-sport vocabulary. They
|
|
90
|
-
// stayed on every match_summary/match_details card regardless of game, so
|
|
91
|
-
// a tennis card carried referee:null and weightClass:null — foreign
|
|
92
|
-
// fields, not merely empty ones. UFC keeps them; every other game omits
|
|
93
|
-
// the keys entirely rather than shipping them as null.
|
|
94
72
|
...(game === 'ufc'
|
|
95
73
|
? {
|
|
96
74
|
method: normalizeUfcMethod(pickString(r.method, r.resultMethod)) ?? null,
|
|
@@ -98,18 +76,13 @@ function matchCore(game, matchId, raw) {
|
|
|
98
76
|
}
|
|
99
77
|
: {}),
|
|
100
78
|
winner: (() => {
|
|
101
|
-
// Never name a winner while play is in progress: the score fallback
|
|
102
|
-
// below reads "who is ahead", which mid-match is a lead, not a result.
|
|
103
79
|
if (m.status === 'live' || m.status === 'upcoming')
|
|
104
80
|
return null;
|
|
105
|
-
// UFC rows carry an explicit winner slug/fighter ref; tennis archive
|
|
106
|
-
// rows nest a winner object.
|
|
107
81
|
const explicit = pickString(r.winnerFighterSlug, r.winnerSlug, r.winner, asRecord(r.winner)?.id);
|
|
108
82
|
if (explicit)
|
|
109
83
|
return explicit;
|
|
110
84
|
if (game === 'ufc')
|
|
111
85
|
return null;
|
|
112
|
-
// CS2 rows expose winnerTeamId instead; map it to a side, else fall back to scores.
|
|
113
86
|
const sideLabel = (side) => side ? pickString(side.slug, side.id, side.name) ?? null : null;
|
|
114
87
|
const winnerTeamId = pickString(r.winnerTeamId, r.winner_team_id);
|
|
115
88
|
if (winnerTeamId) {
|
|
@@ -156,8 +129,6 @@ function matchCore(game, matchId, raw) {
|
|
|
156
129
|
}
|
|
157
130
|
: {}),
|
|
158
131
|
rawStatus: pickString(r.status, r.state) ?? null,
|
|
159
|
-
// Point-level live state (tennis): current game score like "40-AD", which
|
|
160
|
-
// side is serving, and the set in progress. Absent for other games.
|
|
161
132
|
...(game === 'tennis' && (r.game_score || r.server)
|
|
162
133
|
? {
|
|
163
134
|
liveState: {
|
|
@@ -171,13 +142,6 @@ function matchCore(game, matchId, raw) {
|
|
|
171
142
|
};
|
|
172
143
|
return core;
|
|
173
144
|
}
|
|
174
|
-
/**
|
|
175
|
-
* Rename team1/team2 -> player1/player2 (top level and the nested score
|
|
176
|
-
* sub-object) for tennis only, at the point a matchCore object leaves this
|
|
177
|
-
* module. Kept separate from matchCore itself so every internal reader
|
|
178
|
-
* (scoreline strings, winner lookup, sideLabel) keeps working against the
|
|
179
|
-
* stable team1/team2 shape; only the client-facing copy is renamed.
|
|
180
|
-
*/
|
|
181
145
|
function toClientMatch(core, game) {
|
|
182
146
|
return presentSides({ ...core, score: presentSides(core.score, game) }, game);
|
|
183
147
|
}
|
|
@@ -264,15 +228,10 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
264
228
|
let primary = await getSection(ctx, primaryPath(game, matchId));
|
|
265
229
|
upstreamCalls += 1;
|
|
266
230
|
rateLimit = primary.headers;
|
|
267
|
-
// Tennis live board hands out s365_* ids that live at /matches/live/{id}
|
|
268
|
-
// until the match finishes and lands in the archive. Without this fallback
|
|
269
|
-
// the board gives out ids that match_summary immediately 404s on.
|
|
270
231
|
if (!primary.ok && game === 'tennis' && primary.status === 404) {
|
|
271
232
|
primary = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
272
233
|
upstreamCalls += 1;
|
|
273
234
|
rateLimit = { ...rateLimit, ...primary.headers };
|
|
274
|
-
// Finished live matches archive under s365_{year}_{gid} while the live
|
|
275
|
-
// board handed out s365_{gid} — insert the year before giving up.
|
|
276
235
|
const gid = matchId.match(/^s365_(\d+)$/)?.[1];
|
|
277
236
|
if (!primary.ok && gid) {
|
|
278
237
|
const year = new Date().getUTCFullYear();
|
|
@@ -324,9 +283,6 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
324
283
|
upstreamCalls += 1;
|
|
325
284
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
326
285
|
if (!res.ok) {
|
|
327
|
-
// Live tennis matches have no archive stats row yet, but the live
|
|
328
|
-
// detail payload (already fetched as `primary`) carries the full
|
|
329
|
-
// per-side stat block — serve that instead of a 404 partial.
|
|
330
286
|
if (game === 'tennis' && res.status === 404) {
|
|
331
287
|
const livePayload = asRecord(asRecord(primary.data)?.data) ?? asRecord(primary.data);
|
|
332
288
|
const liveStats = livePayload?.stats;
|
|
@@ -366,25 +322,12 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
366
322
|
}
|
|
367
323
|
})());
|
|
368
324
|
}
|
|
369
|
-
// games/maps
|
|
370
325
|
enrich.push((async () => {
|
|
371
|
-
// Tennis has no per-set endpoint of its own; the set-by-set score is
|
|
372
|
-
// already sitting in `primary` (the same payload matchCore built the
|
|
373
|
-
// scoreline from). Pulling it out here means match_summary keeps the
|
|
374
|
-
// set breakdown instead of shipping gamesOrMaps: [] next to a fully
|
|
375
|
-
// populated `sets` array one field over.
|
|
376
326
|
if (game === 'tennis') {
|
|
377
327
|
const payload = asRecord(asRecord(primary.data)?.data) ?? asRecord(primary.data);
|
|
378
328
|
const sets = Array.isArray(payload?.sets) ? payload.sets : [];
|
|
379
329
|
gamesOrMaps = sets.map((s) => {
|
|
380
330
|
const sr = asRecord(s) ?? {};
|
|
381
|
-
// The live route (/tennis/matches/live/{id}) spells this set_num +
|
|
382
|
-
// score + is_completed; the archive route (/tennis/matches/{id})
|
|
383
|
-
// spells the same set set_number, with no per-set score string
|
|
384
|
-
// (the match-level `score` already holds "6-4, 6-4, 2-1") and no
|
|
385
|
-
// completion flag. Read both spellings rather than nulling out
|
|
386
|
-
// real data because match_summary happened to resolve the other
|
|
387
|
-
// route this time.
|
|
388
331
|
const p1g = typeof sr.player1_games === 'number' ? sr.player1_games : null;
|
|
389
332
|
const p2g = typeof sr.player2_games === 'number' ? sr.player2_games : null;
|
|
390
333
|
return {
|
|
@@ -393,9 +336,6 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
393
336
|
player1Games: p1g,
|
|
394
337
|
player2Games: p2g,
|
|
395
338
|
tiebreak: sr.tiebreak ?? null,
|
|
396
|
-
// is_completed is a LIVE-route field. The archive route omits it,
|
|
397
|
-
// so every set of a finished match read completed:false. Fall
|
|
398
|
-
// back to the games, which always decide whether a set is over.
|
|
399
339
|
completed: tennisSetCompleted(sr),
|
|
400
340
|
};
|
|
401
341
|
});
|
|
@@ -456,28 +396,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
456
396
|
});
|
|
457
397
|
},
|
|
458
398
|
};
|
|
459
|
-
/**
|
|
460
|
-
* Fold a UFC bout's raw odds payload into something an agent can read.
|
|
461
|
-
*
|
|
462
|
-
* The REST response is deliberately complete: every bookmaker, every market, up
|
|
463
|
-
* to roughly 950 outcomes for a single bout. Handing that back whole would bury
|
|
464
|
-
* the two numbers a caller almost always wants, and the server's own
|
|
465
|
-
* instructions say not to flood odds. So the moneyline is summarised per fighter
|
|
466
|
-
* and every other market is reduced to a count, with the raw endpoint named so
|
|
467
|
-
* anyone who genuinely needs the full book can go and get it.
|
|
468
|
-
*
|
|
469
|
-
* isAvailable is carried through as currentlyOffered rather than filtered on. It
|
|
470
|
-
* means "a book is offering this right now", so a finished fight's closing line
|
|
471
|
-
* comes back with it false, and that is information rather than absence.
|
|
472
|
-
* Filtering here would recreate the bug where whole settled cards looked like
|
|
473
|
-
* they had no odds at all.
|
|
474
|
-
*/
|
|
475
399
|
export function summarizeUfcOdds(data) {
|
|
476
|
-
// fetchJson hands back the whole REST envelope ({ success, data, meta }), not
|
|
477
|
-
// the inner payload, so unwrap one level when it is there. Accepting both
|
|
478
|
-
// shapes because the unit tests feed the inner object directly; reading only
|
|
479
|
-
// the outer one returned a perfectly shaped, perfectly empty summary, which
|
|
480
|
-
// looked like "this bout has no odds" rather than like a bug.
|
|
481
400
|
const root = (data ?? {});
|
|
482
401
|
const payload = (root.data && typeof root.data === 'object' && !Array.isArray(root.data)
|
|
483
402
|
? root.data
|
|
@@ -520,9 +439,6 @@ export function summarizeUfcOdds(data) {
|
|
|
520
439
|
otherMarkets.set(type, other);
|
|
521
440
|
}
|
|
522
441
|
}
|
|
523
|
-
// American odds: the best price is the highest number both for an underdog
|
|
524
|
-
// (+250 beats +180) and for a favourite (-110 beats -200), so it is simply the
|
|
525
|
-
// maximum in both cases.
|
|
526
442
|
const moneyline = [...byFighter.values()].map((entry) => {
|
|
527
443
|
const sorted = [...entry.prices].sort((a, b) => a - b);
|
|
528
444
|
const mid = sorted.length
|
|
@@ -552,21 +468,90 @@ export function summarizeUfcOdds(data) {
|
|
|
552
468
|
fullBookPath: '/ufc/bouts/{boutId}/odds',
|
|
553
469
|
};
|
|
554
470
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
471
|
+
export function summarizeTennisSetStats(block) {
|
|
472
|
+
const b = asRecord(block);
|
|
473
|
+
if (!b)
|
|
474
|
+
return null;
|
|
475
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
476
|
+
const strings = (v) => arr(v).map(String);
|
|
477
|
+
if (!arr(b.periods).length && !arr(b.gaps).length && !arr(b.sources).length
|
|
478
|
+
&& typeof b.note !== 'string') {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
482
|
+
const str = (v) => (typeof v === 'string' ? v : null);
|
|
483
|
+
const line = (side) => {
|
|
484
|
+
const s = asRecord(side) ?? {};
|
|
485
|
+
return {
|
|
486
|
+
aces: num(s.aces),
|
|
487
|
+
doubleFaults: num(s.double_faults),
|
|
488
|
+
firstServePct: num(s.first_serve_pct),
|
|
489
|
+
firstServeWonPct: num(s.first_serve_win_pct),
|
|
490
|
+
secondServeWonPct: num(s.second_serve_win_pct),
|
|
491
|
+
breakPointsSaved: num(s.break_points_saved),
|
|
492
|
+
breakPointsFaced: num(s.break_points_faced),
|
|
493
|
+
breakPointsConverted: num(s.break_points_converted),
|
|
494
|
+
breakPointOpportunities: num(s.break_point_opportunities),
|
|
495
|
+
totalPointsWon: num(s.total_points_won),
|
|
496
|
+
servicePointsPlayed: num(s.serve_points),
|
|
497
|
+
servicePointsWon: num(s.service_points_won),
|
|
498
|
+
gamesWon: num(s.games_won),
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
periods: arr(b.periods).map((p) => {
|
|
503
|
+
const pr = asRecord(p) ?? {};
|
|
504
|
+
const missing = asRecord(pr.missing) ?? {};
|
|
505
|
+
return {
|
|
506
|
+
period: str(pr.period) ?? 'ALL',
|
|
507
|
+
setNumber: num(pr.set_number) ?? 0,
|
|
508
|
+
status: str(pr.status) ?? 'partial',
|
|
509
|
+
sources: strings(pr.sources),
|
|
510
|
+
winner: line(pr.winner),
|
|
511
|
+
loser: line(pr.loser),
|
|
512
|
+
missing: { winner: strings(missing.winner), loser: strings(missing.loser) },
|
|
513
|
+
};
|
|
514
|
+
}),
|
|
515
|
+
sources: arr(b.sources).map((s) => {
|
|
516
|
+
const sr = asRecord(s) ?? {};
|
|
517
|
+
return {
|
|
518
|
+
source: str(sr.source) ?? 'unknown',
|
|
519
|
+
status: str(sr.status) ?? 'unknown',
|
|
520
|
+
fields: strings(sr.fields),
|
|
521
|
+
detail: str(sr.detail),
|
|
522
|
+
};
|
|
523
|
+
}),
|
|
524
|
+
gaps: arr(b.gaps).map((g) => {
|
|
525
|
+
const gr = asRecord(g) ?? {};
|
|
526
|
+
return {
|
|
527
|
+
code: str(gr.code) ?? 'UNKNOWN',
|
|
528
|
+
period: str(gr.period),
|
|
529
|
+
setNumber: num(gr.set_number),
|
|
530
|
+
side: str(gr.side),
|
|
531
|
+
fields: strings(gr.fields),
|
|
532
|
+
source: str(gr.source),
|
|
533
|
+
status: str(gr.status),
|
|
534
|
+
message: str(gr.message) ?? '',
|
|
535
|
+
};
|
|
536
|
+
}),
|
|
537
|
+
note: str(b.note),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
export function tennisSetStatsWarnings(block) {
|
|
541
|
+
const shaped = summarizeTennisSetStats(block);
|
|
542
|
+
if (!shaped)
|
|
543
|
+
return [];
|
|
544
|
+
const gaps = shaped.gaps ?? [];
|
|
545
|
+
const out = [];
|
|
546
|
+
for (const gap of gaps) {
|
|
547
|
+
const where = [gap.period, gap.side].filter(Boolean).join(' ');
|
|
548
|
+
out.push(`set_stats ${gap.code}${where ? ` (${where})` : ''}: ${gap.message}`);
|
|
549
|
+
}
|
|
550
|
+
const note = shaped.note;
|
|
551
|
+
if (gaps.length && typeof note === 'string')
|
|
552
|
+
out.unshift(`set_stats: ${note}`);
|
|
553
|
+
return out.slice(0, 6);
|
|
554
|
+
}
|
|
570
555
|
export function summarizeTennisMatchStats(data) {
|
|
571
556
|
const root = asRecord(data) ?? {};
|
|
572
557
|
const payload = asRecord(root.data) ?? root;
|
|
@@ -595,6 +580,7 @@ export function summarizeTennisMatchStats(data) {
|
|
|
595
580
|
completed: tennisSetCompleted(sr),
|
|
596
581
|
};
|
|
597
582
|
}),
|
|
583
|
+
setStats: summarizeTennisSetStats(payload.set_stats),
|
|
598
584
|
rawPath: '/tennis/matches/{matchId}/stats',
|
|
599
585
|
};
|
|
600
586
|
}
|
|
@@ -632,7 +618,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
632
618
|
type: 'string',
|
|
633
619
|
enum: ['base', 'playerStats', 'gamesOrMaps', 'timeline', 'liveState', 'media', 'advanced', 'odds'],
|
|
634
620
|
},
|
|
635
|
-
description: 'Explicit section list; defaults to base+playerStats+gamesOrMaps+media. "odds" is opt-in: UFC returns moneyline summarised per fighter with bookmaker count, best and median American price and implied probability plus a count of every other market (closing lines for a finished fight come back with currentlyOffered=false rather than being omitted)
|
|
621
|
+
description: 'Explicit section list; defaults to base+playerStats+gamesOrMaps+media. "odds" is opt-in: UFC returns moneyline summarised per fighter with bookmaker count, best and median American price and implied probability plus a count of every other market (closing lines for a finished fight come back with currentlyOffered=false rather than being omitted). Tennis odds were withdrawn alongside the /tennis/odds/* routes, so for any other game the section reports NOT_IMPLEMENTED. Odds exist for UFC only.',
|
|
636
622
|
},
|
|
637
623
|
includeTimeline: boolSchema('Include timeline section (heavy).', false),
|
|
638
624
|
includeLiveState: boolSchema('Include live state/snapshots.', false),
|
|
@@ -692,19 +678,11 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
692
678
|
media: null,
|
|
693
679
|
advanced: null,
|
|
694
680
|
};
|
|
695
|
-
// base is ALWAYS fetched, whatever sections were asked for. Requesting
|
|
696
|
-
// sections:["timeline"] returned match:null, so a caller got a timeline with
|
|
697
|
-
// nothing to attach it to and no way to tell which match it belonged to.
|
|
698
|
-
// The header is the identity of the response, not an optional section.
|
|
699
681
|
wanted.add('base');
|
|
700
682
|
if (wanted.has('base')) {
|
|
701
683
|
let res = await getSection(ctx, primaryPath(game, matchId));
|
|
702
684
|
upstreamCalls += 1;
|
|
703
685
|
rateLimit = res.headers;
|
|
704
|
-
// Same fallback match_summary already has: the live board hands out
|
|
705
|
-
// s365_* ids that only resolve at /matches/live/{id} until the match
|
|
706
|
-
// finishes and archives. Without this, a matchId taken straight off
|
|
707
|
-
// live_matches 404s here even though match_summary resolves it fine.
|
|
708
686
|
if (!res.ok && game === 'tennis' && res.status === 404) {
|
|
709
687
|
res = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
710
688
|
upstreamCalls += 1;
|
|
@@ -751,10 +729,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
751
729
|
}
|
|
752
730
|
})());
|
|
753
731
|
};
|
|
754
|
-
// Same as load(), but folds the payload through a shaper first. Raw odds are
|
|
755
|
-
// far too big to hand back whole: one bout carries up to ~950 outcomes across
|
|
756
|
-
// every bookmaker and prop market, which would bury the answer it was fetched
|
|
757
|
-
// to give.
|
|
758
732
|
const loadWith = (section, path, shape, query) => {
|
|
759
733
|
tasks.push((async () => {
|
|
760
734
|
const res = await getSection(ctx, path, query);
|
|
@@ -784,8 +758,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
784
758
|
load('playerStats', `/cod/matches/${encodeURIComponent(matchId)}/player-stats`);
|
|
785
759
|
if (game === 'ufc')
|
|
786
760
|
load('playerStats', `/ufc/bouts/${encodeURIComponent(matchId)}/stats`);
|
|
787
|
-
// Tennis has a real per-match stats route; the section simply had no arm
|
|
788
|
-
// for it and shipped playerStats:null forever.
|
|
789
761
|
if (game === 'tennis') {
|
|
790
762
|
loadWith('playerStats', `/tennis/matches/${encodeURIComponent(matchId)}/stats`, summarizeTennisMatchStats);
|
|
791
763
|
}
|
|
@@ -798,7 +770,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
798
770
|
if (game === 'ufc')
|
|
799
771
|
load('gamesOrMaps', `/ufc/bouts/${encodeURIComponent(matchId)}/rounds`);
|
|
800
772
|
if (game === 'cs2' || game === 'dota2') {
|
|
801
|
-
// no dedicated maps list — leave null with note via partial only if requested alone
|
|
802
773
|
sections.gamesOrMaps = sections.base;
|
|
803
774
|
}
|
|
804
775
|
}
|
|
@@ -835,17 +806,10 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
835
806
|
if (game === 'ufc') {
|
|
836
807
|
loadWith('odds', `/ufc/bouts/${encodeURIComponent(matchId)}/odds`, summarizeUfcOdds);
|
|
837
808
|
}
|
|
838
|
-
else if (game === 'tennis') {
|
|
839
|
-
// The gate that produced "Odds not curated for tennis; UFC only today"
|
|
840
|
-
// was written before /tennis/odds/{id} existed. It does: the same match
|
|
841
|
-
// returned FanDuel 1.105 / Matchbook 1.13 from tennis_odds while this
|
|
842
|
-
// section denied odds existed. Both surfaces now share one projection.
|
|
843
|
-
loadWith('odds', `/tennis/odds/${encodeURIComponent(matchId)}`, summarizeTennisOdds);
|
|
844
|
-
}
|
|
845
809
|
else {
|
|
846
810
|
partial.push(partialFromRejection('odds', {
|
|
847
811
|
code: 'NOT_IMPLEMENTED',
|
|
848
|
-
message: `Odds are not curated for ${game}.
|
|
812
|
+
message: `Odds are not curated for ${game}. Currently available: UFC (/ufc/bouts/{id}/odds). Tennis odds were withdrawn.`,
|
|
849
813
|
}));
|
|
850
814
|
}
|
|
851
815
|
}
|
|
@@ -872,6 +836,12 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
872
836
|
}
|
|
873
837
|
}
|
|
874
838
|
await Promise.all(tasks);
|
|
839
|
+
const warnings = [];
|
|
840
|
+
if (game === 'tennis') {
|
|
841
|
+
const ps = asRecord(sections.playerStats);
|
|
842
|
+
if (ps)
|
|
843
|
+
warnings.push(...tennisSetStatsWarnings(ps.setStats));
|
|
844
|
+
}
|
|
875
845
|
return successEnvelope({
|
|
876
846
|
source: 'match_details',
|
|
877
847
|
game,
|
|
@@ -879,6 +849,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
879
849
|
tookMs: Date.now() - started,
|
|
880
850
|
upstreamCalls,
|
|
881
851
|
rateLimit,
|
|
852
|
+
warnings: warnings.length ? warnings : undefined,
|
|
882
853
|
partial: partial.length ? partial : undefined,
|
|
883
854
|
entities: { games: [game], ids: { matchId, ...(gameId ? { gameId } : {}) } },
|
|
884
855
|
data: {
|
package/dist/tools/meta.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Meta / power tools: list_capabilities, api_health, call_api.
|
|
3
|
-
*/
|
|
4
1
|
import { extractRows, fetchJson, gameNotIncludedHint, present } from '../client.js';
|
|
5
2
|
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
6
3
|
import { boolSchema, gameSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
@@ -186,16 +183,6 @@ const TOOL_CATALOG = [
|
|
|
186
183
|
preferOver: ['raw rankings history via call_api'],
|
|
187
184
|
doNotUse: 'Latest snapshot → standings; week-over-week delta → rankings_movers',
|
|
188
185
|
},
|
|
189
|
-
{
|
|
190
|
-
name: 'tennis_odds',
|
|
191
|
-
outcome: 'Tennis betting odds: upcoming matches with prices, plus pre-match and in-play for one match; upcoming rows carry a joinKey and playerIds because the feed leaves match_id null',
|
|
192
|
-
parallelSafe: true,
|
|
193
|
-
games: ['tennis'],
|
|
194
|
-
jobs: ['preview', 'match_page', 'odds'],
|
|
195
|
-
exampleArgs: { game: 'tennis', scope: 'upcoming', limit: 20 },
|
|
196
|
-
preferOver: ['raw odds via call_api', 'match_preview (no prices)'],
|
|
197
|
-
doNotUse: 'The result → match_details; rankings → standings',
|
|
198
|
-
},
|
|
199
186
|
{
|
|
200
187
|
name: 'tournaments',
|
|
201
188
|
outcome: 'Tennis tournament catalog: filter by year, tour, level, surface or country; a level name that names a tour also fixes it',
|
|
@@ -378,7 +365,7 @@ const TOOL_CATALOG = [
|
|
|
378
365
|
parallelSafe: true,
|
|
379
366
|
games: ['cs2'],
|
|
380
367
|
jobs: ['team_page', 'preview'],
|
|
381
|
-
exampleArgs: { teamId: '
|
|
368
|
+
exampleArgs: { teamId: 'cs2-team-4608', days: 90 },
|
|
382
369
|
preferOver: ['manual match history scraping'],
|
|
383
370
|
doNotUse: 'For non-CS2 games',
|
|
384
371
|
},
|
|
@@ -443,8 +430,6 @@ const JOBS = [
|
|
|
443
430
|
{ id: 'schedule', description: 'Upcoming fixtures/events', recommendedTools: ['upcoming_schedule', 'event_card'] },
|
|
444
431
|
{ id: 'preview', description: 'Pre-match briefing', recommendedTools: ['match_preview'] },
|
|
445
432
|
{ id: 'event_card', description: 'Event / fight-night card page', recommendedTools: ['event_card', 'resolve_entity', 'match_preview'] },
|
|
446
|
-
// Agents asked "does this API have photos?" and, finding no job for it,
|
|
447
|
-
// assumed no. Photos ship on every UFC corner and profile; make that findable.
|
|
448
433
|
{
|
|
449
434
|
id: 'media',
|
|
450
435
|
description: 'Headshots, full-body shots and team logos for visual UIs. UFC fighters carry headshotUrl / bodyImageUrl / imageUrl plus a CORS-safe proxiedImageUrl; use the proxied URL in a browser. Available on event_card corners and player_profile without any extra call.',
|
|
@@ -653,7 +638,6 @@ Example: { "includeGameProbes": true }`,
|
|
|
653
638
|
});
|
|
654
639
|
}
|
|
655
640
|
else {
|
|
656
|
-
// try product root
|
|
657
641
|
const cs2 = await fetchJson(ctx, '/cs2');
|
|
658
642
|
upstreamCalls += 1;
|
|
659
643
|
rateLimit = cs2.headers;
|
|
@@ -670,11 +654,10 @@ Example: { "includeGameProbes": true }`,
|
|
|
670
654
|
}
|
|
671
655
|
}
|
|
672
656
|
else {
|
|
673
|
-
// health may be unauthenticated — confirm key with light authed probe
|
|
674
657
|
const probe = await fetchJson(ctx, '/lol/leagues');
|
|
675
658
|
upstreamCalls += 1;
|
|
676
659
|
rateLimit = { ...rateLimit, ...probe.headers };
|
|
677
|
-
keyValid = probe.ok || probe.status === 403;
|
|
660
|
+
keyValid = probe.ok || probe.status === 403;
|
|
678
661
|
if (probe.ok)
|
|
679
662
|
answeredBy = '/health+/lol/leagues';
|
|
680
663
|
if (probe.status === 401) {
|
|
@@ -703,10 +686,8 @@ Example: { "includeGameProbes": true }`,
|
|
|
703
686
|
if (res.ok)
|
|
704
687
|
includedGames.push(game);
|
|
705
688
|
else if (res.status === 403 && gameNotIncludedHint(res.data)) {
|
|
706
|
-
// not included
|
|
707
689
|
}
|
|
708
690
|
else if (res.status !== 401 && res.status !== 404) {
|
|
709
|
-
// ambiguous — still list as probed
|
|
710
691
|
}
|
|
711
692
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
712
693
|
}
|
|
@@ -743,8 +724,6 @@ const ALLOWLIST_PREFIXES = [
|
|
|
743
724
|
'/ufc',
|
|
744
725
|
'/fortnite',
|
|
745
726
|
'/tennis',
|
|
746
|
-
// The spec describes the surface call_api is allowed to reach; refusing to
|
|
747
|
-
// serve it left route discovery impossible except by guessing.
|
|
748
727
|
'/openapi.json',
|
|
749
728
|
];
|
|
750
729
|
function pathAllowed(path) {
|
|
@@ -780,9 +759,9 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
780
759
|
properties: {
|
|
781
760
|
method: {
|
|
782
761
|
type: 'string',
|
|
783
|
-
enum: ['GET'
|
|
762
|
+
enum: ['GET'],
|
|
784
763
|
default: 'GET',
|
|
785
|
-
description: 'HTTP method.
|
|
764
|
+
description: 'HTTP method. GET only: call_api is read-only. Example: "GET".',
|
|
786
765
|
},
|
|
787
766
|
path: stringSchema('Path starting with /. Allowlisted prefixes only. Example: "/cs2/rankings/teams".', '/cs2/rankings/teams'),
|
|
788
767
|
queryJson: {
|
|
@@ -794,10 +773,6 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
794
773
|
type: 'object',
|
|
795
774
|
description: 'Query params as a plain object (alternative to queryJson). Example: {"year":2026}.',
|
|
796
775
|
},
|
|
797
|
-
bodyJson: {
|
|
798
|
-
type: 'string',
|
|
799
|
-
description: 'Stringified JSON body for POST only (rare).',
|
|
800
|
-
},
|
|
801
776
|
},
|
|
802
777
|
},
|
|
803
778
|
handler: async (args, ctx) => {
|
|
@@ -815,10 +790,10 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
815
790
|
tookMs: Date.now() - started,
|
|
816
791
|
});
|
|
817
792
|
}
|
|
818
|
-
if (method !== 'GET'
|
|
793
|
+
if (method !== 'GET') {
|
|
819
794
|
return errorEnvelope({
|
|
820
795
|
code: 'VALIDATION',
|
|
821
|
-
message: 'method must be GET
|
|
796
|
+
message: 'method must be GET; call_api is read-only',
|
|
822
797
|
game: null,
|
|
823
798
|
source: 'call_api',
|
|
824
799
|
requestId,
|
|
@@ -837,8 +812,6 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
837
812
|
});
|
|
838
813
|
}
|
|
839
814
|
let query = {};
|
|
840
|
-
// Agents pass query as a plain object at least as often as the stringified
|
|
841
|
-
// queryJson form; silently dropping it produced confusing upstream 422s.
|
|
842
815
|
if (args.query && typeof args.query === 'object' && !Array.isArray(args.query)) {
|
|
843
816
|
query = { ...args.query };
|
|
844
817
|
}
|
|
@@ -861,23 +834,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
861
834
|
});
|
|
862
835
|
}
|
|
863
836
|
}
|
|
864
|
-
|
|
865
|
-
if (method === 'POST' && typeof args.bodyJson === 'string' && args.bodyJson.trim()) {
|
|
866
|
-
try {
|
|
867
|
-
body = JSON.parse(args.bodyJson);
|
|
868
|
-
}
|
|
869
|
-
catch (e) {
|
|
870
|
-
return errorEnvelope({
|
|
871
|
-
code: 'VALIDATION',
|
|
872
|
-
message: `Invalid bodyJson: ${e.message}`,
|
|
873
|
-
game: null,
|
|
874
|
-
source: 'call_api',
|
|
875
|
-
requestId,
|
|
876
|
-
tookMs: Date.now() - started,
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
const res = await fetchJson(ctx, path, { method, query, body });
|
|
837
|
+
const res = await fetchJson(ctx, path, { method, query });
|
|
881
838
|
if (!res.ok) {
|
|
882
839
|
return errorEnvelope({
|
|
883
840
|
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
@@ -909,14 +866,6 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
909
866
|
});
|
|
910
867
|
},
|
|
911
868
|
};
|
|
912
|
-
/**
|
|
913
|
-
* Games whose routes the published spec does not describe.
|
|
914
|
-
*
|
|
915
|
-
* The spec has 123 paths and zero under /lol, yet every LoL route works —
|
|
916
|
-
* api_health itself answers partly from /lol/leagues. An agent that treats the
|
|
917
|
-
* spec as the whole surface concludes LoL is unsupported and stops, so say so
|
|
918
|
-
* explicitly rather than returning an empty list that reads as a verdict.
|
|
919
|
-
*/
|
|
920
869
|
const SPEC_OMITS = {
|
|
921
870
|
tennis: 'The published OpenAPI spec documents no /tennis paths, but tennis routes exist and work (players, matches, h2h, rankings, tournaments, live). Use the curated tools with game tennis, or call_api with /tennis/* paths.',
|
|
922
871
|
lol: 'The published OpenAPI spec documents no /lol paths, but LoL routes exist and work. Use the curated LoL tools (live_matches, upcoming_schedule, team_profile, standings, player_profile); for raw access, /lol/* is allowlisted for call_api even though it is undocumented.',
|
|
@@ -959,9 +908,6 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
959
908
|
handler: async (args, ctx) => {
|
|
960
909
|
const started = Date.now();
|
|
961
910
|
const requestId = newRequestId();
|
|
962
|
-
// Accept fortnite here even though it is not a PRIMARY_GAME: it has 21
|
|
963
|
-
// documented paths and no curated tools, so it is exactly what call_api
|
|
964
|
-
// callers come looking for.
|
|
965
911
|
const gameRaw = typeof args.game === 'string' ? args.game.toLowerCase().trim() : '';
|
|
966
912
|
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis)$/)) {
|
|
967
913
|
return errorEnvelope({
|
|
@@ -1052,5 +998,4 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
1052
998
|
},
|
|
1053
999
|
};
|
|
1054
1000
|
export const metaTools = [listCapabilities, apiHealth, listRoutes, callApi];
|
|
1055
|
-
// silence unused import in case extractRows needed later
|
|
1056
1001
|
void extractRows;
|