cito-mcp 0.4.4 → 0.4.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 +7 -7
- package/dist/client.js +39 -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 +76 -80
- package/dist/scrub.js +111 -0
- package/dist/tools/cs2.js +1 -48
- package/dist/tools/index.js +0 -11
- package/dist/tools/insight.js +42 -174
- package/dist/tools/leaderboard.js +0 -21
- package/dist/tools/live.js +36 -127
- package/dist/tools/match.js +29 -184
- package/dist/tools/meta.js +7 -52
- package/dist/tools/normalize.js +6 -229
- package/dist/tools/odds.js +0 -66
- package/dist/tools/player.js +45 -179
- 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 +21 -65
- package/dist/tools/team.js +2 -83
- package/dist/tools/tournaments.js +0 -37
- package/dist/tools/types.js +0 -17
- package/dist/version.js +0 -17
- package/package.json +22 -2
package/dist/tools/match.js
CHANGED
|
@@ -1,24 +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
|
-
// summarizeTennisOdds was used here for the tennis odds section. Tennis odds were
|
|
8
|
-
// withdrawn 2026-09-13 (see docs/tennis-risk-register.md), so this module no
|
|
9
|
-
// longer imports it. The shaper still exists and is still exported from
|
|
10
|
-
// ./odds.js for the day the surface is restored.
|
|
11
4
|
import { boolSchema, gameSchema, isPrimaryGame, parseGame, stringSchema, } from './types.js';
|
|
12
5
|
async function getSection(ctx, path, query) {
|
|
13
6
|
return fetchJson(ctx, path, { query });
|
|
14
7
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Series length. The LoL feed states it as `strategy: "Bo5"`, which nothing was
|
|
17
|
-
* reading, so every match reported bestOf: null. Accept the spelled forms and
|
|
18
|
-
* the object shape other titles use, and fall back to the declared game count.
|
|
19
|
-
* Only 1/3/5/7/9 are real series lengths, so anything else is refused rather
|
|
20
|
-
* than guessed.
|
|
21
|
-
*/
|
|
22
8
|
export function parseBestOf(r) {
|
|
23
9
|
const fromNumber = (v) => {
|
|
24
10
|
const n = Number(v);
|
|
@@ -36,7 +22,6 @@ export function parseBestOf(r) {
|
|
|
36
22
|
return n;
|
|
37
23
|
}
|
|
38
24
|
}
|
|
39
|
-
// { type: "bestOf", count: 5 }
|
|
40
25
|
if (strat && typeof strat === 'object') {
|
|
41
26
|
const o = strat;
|
|
42
27
|
const n = fromNumber(o.count) ?? fromNumber(o.value) ?? fromNumber(o.bestOf);
|
|
@@ -65,11 +50,6 @@ function matchCore(game, matchId, raw) {
|
|
|
65
50
|
const scoreline = m.team1 || m.team2
|
|
66
51
|
? `${m.team1?.name ?? '?'} ${s1 ?? '-'} : ${s2 ?? '-'} ${m.team2?.name ?? '?'}`
|
|
67
52
|
: null;
|
|
68
|
-
/**
|
|
69
|
-
* Tennis: `score` is the set-by-set game score ("4-6 6-3 6-3 7-5"), which is
|
|
70
|
-
* a DIFFERENT fact from the sets-won scoreline. "3 : 1" is the result; it
|
|
71
|
-
* cannot distinguish 6-0 6-0 from three tiebreaks. Both are carried.
|
|
72
|
-
*/
|
|
73
53
|
const scoreDetail = game === 'tennis' ? (pickString(r.score, r.score_raw) ?? null) : null;
|
|
74
54
|
const core = {
|
|
75
55
|
matchId: m.matchId !== 'unknown' ? m.matchId : matchId,
|
|
@@ -89,11 +69,6 @@ function matchCore(game, matchId, raw) {
|
|
|
89
69
|
team2: m.team2,
|
|
90
70
|
event: m.event,
|
|
91
71
|
league: m.league,
|
|
92
|
-
// method/methodRaw/weightClass/referee are combat-sport vocabulary. They
|
|
93
|
-
// stayed on every match_summary/match_details card regardless of game, so
|
|
94
|
-
// a tennis card carried referee:null and weightClass:null — foreign
|
|
95
|
-
// fields, not merely empty ones. UFC keeps them; every other game omits
|
|
96
|
-
// the keys entirely rather than shipping them as null.
|
|
97
72
|
...(game === 'ufc'
|
|
98
73
|
? {
|
|
99
74
|
method: normalizeUfcMethod(pickString(r.method, r.resultMethod)) ?? null,
|
|
@@ -101,18 +76,13 @@ function matchCore(game, matchId, raw) {
|
|
|
101
76
|
}
|
|
102
77
|
: {}),
|
|
103
78
|
winner: (() => {
|
|
104
|
-
// Never name a winner while play is in progress: the score fallback
|
|
105
|
-
// below reads "who is ahead", which mid-match is a lead, not a result.
|
|
106
79
|
if (m.status === 'live' || m.status === 'upcoming')
|
|
107
80
|
return null;
|
|
108
|
-
// UFC rows carry an explicit winner slug/fighter ref; tennis archive
|
|
109
|
-
// rows nest a winner object.
|
|
110
81
|
const explicit = pickString(r.winnerFighterSlug, r.winnerSlug, r.winner, asRecord(r.winner)?.id);
|
|
111
82
|
if (explicit)
|
|
112
83
|
return explicit;
|
|
113
84
|
if (game === 'ufc')
|
|
114
85
|
return null;
|
|
115
|
-
// CS2 rows expose winnerTeamId instead; map it to a side, else fall back to scores.
|
|
116
86
|
const sideLabel = (side) => side ? pickString(side.slug, side.id, side.name) ?? null : null;
|
|
117
87
|
const winnerTeamId = pickString(r.winnerTeamId, r.winner_team_id);
|
|
118
88
|
if (winnerTeamId) {
|
|
@@ -159,8 +129,6 @@ function matchCore(game, matchId, raw) {
|
|
|
159
129
|
}
|
|
160
130
|
: {}),
|
|
161
131
|
rawStatus: pickString(r.status, r.state) ?? null,
|
|
162
|
-
// Point-level live state (tennis): current game score like "40-AD", which
|
|
163
|
-
// side is serving, and the set in progress. Absent for other games.
|
|
164
132
|
...(game === 'tennis' && (r.game_score || r.server)
|
|
165
133
|
? {
|
|
166
134
|
liveState: {
|
|
@@ -174,13 +142,6 @@ function matchCore(game, matchId, raw) {
|
|
|
174
142
|
};
|
|
175
143
|
return core;
|
|
176
144
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Rename team1/team2 -> player1/player2 (top level and the nested score
|
|
179
|
-
* sub-object) for tennis only, at the point a matchCore object leaves this
|
|
180
|
-
* module. Kept separate from matchCore itself so every internal reader
|
|
181
|
-
* (scoreline strings, winner lookup, sideLabel) keeps working against the
|
|
182
|
-
* stable team1/team2 shape; only the client-facing copy is renamed.
|
|
183
|
-
*/
|
|
184
145
|
function toClientMatch(core, game) {
|
|
185
146
|
return presentSides({ ...core, score: presentSides(core.score, game) }, game);
|
|
186
147
|
}
|
|
@@ -202,18 +163,18 @@ function primaryPath(game, matchId) {
|
|
|
202
163
|
}
|
|
203
164
|
export const matchSummary = {
|
|
204
165
|
name: 'match_summary',
|
|
205
|
-
description: `COMPOSITE match card: scoreline, key context, player performances, and VOD/demo links when available.
|
|
206
|
-
|
|
207
|
-
When to use:
|
|
208
|
-
- Match recap / default match UI
|
|
209
|
-
- After user selects a live or completed matchId
|
|
210
|
-
|
|
211
|
-
Prefer over match_details for chat answers and default UIs.
|
|
212
|
-
Prefer match_details for timelines, full map trees, live state, advanced packages.
|
|
213
|
-
|
|
214
|
-
Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
|
|
215
|
-
|
|
216
|
-
Parallel-safe: yes. Upstream cost: 2–5.
|
|
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.
|
|
217
178
|
Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includePlayerStats": true }`,
|
|
218
179
|
inputSchema: {
|
|
219
180
|
type: 'object',
|
|
@@ -267,15 +228,10 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
267
228
|
let primary = await getSection(ctx, primaryPath(game, matchId));
|
|
268
229
|
upstreamCalls += 1;
|
|
269
230
|
rateLimit = primary.headers;
|
|
270
|
-
// Tennis live board hands out s365_* ids that live at /matches/live/{id}
|
|
271
|
-
// until the match finishes and lands in the archive. Without this fallback
|
|
272
|
-
// the board gives out ids that match_summary immediately 404s on.
|
|
273
231
|
if (!primary.ok && game === 'tennis' && primary.status === 404) {
|
|
274
232
|
primary = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
275
233
|
upstreamCalls += 1;
|
|
276
234
|
rateLimit = { ...rateLimit, ...primary.headers };
|
|
277
|
-
// Finished live matches archive under s365_{year}_{gid} while the live
|
|
278
|
-
// board handed out s365_{gid} — insert the year before giving up.
|
|
279
235
|
const gid = matchId.match(/^s365_(\d+)$/)?.[1];
|
|
280
236
|
if (!primary.ok && gid) {
|
|
281
237
|
const year = new Date().getUTCFullYear();
|
|
@@ -327,9 +283,6 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
327
283
|
upstreamCalls += 1;
|
|
328
284
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
329
285
|
if (!res.ok) {
|
|
330
|
-
// Live tennis matches have no archive stats row yet, but the live
|
|
331
|
-
// detail payload (already fetched as `primary`) carries the full
|
|
332
|
-
// per-side stat block — serve that instead of a 404 partial.
|
|
333
286
|
if (game === 'tennis' && res.status === 404) {
|
|
334
287
|
const livePayload = asRecord(asRecord(primary.data)?.data) ?? asRecord(primary.data);
|
|
335
288
|
const liveStats = livePayload?.stats;
|
|
@@ -369,25 +322,12 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
369
322
|
}
|
|
370
323
|
})());
|
|
371
324
|
}
|
|
372
|
-
// games/maps
|
|
373
325
|
enrich.push((async () => {
|
|
374
|
-
// Tennis has no per-set endpoint of its own; the set-by-set score is
|
|
375
|
-
// already sitting in `primary` (the same payload matchCore built the
|
|
376
|
-
// scoreline from). Pulling it out here means match_summary keeps the
|
|
377
|
-
// set breakdown instead of shipping gamesOrMaps: [] next to a fully
|
|
378
|
-
// populated `sets` array one field over.
|
|
379
326
|
if (game === 'tennis') {
|
|
380
327
|
const payload = asRecord(asRecord(primary.data)?.data) ?? asRecord(primary.data);
|
|
381
328
|
const sets = Array.isArray(payload?.sets) ? payload.sets : [];
|
|
382
329
|
gamesOrMaps = sets.map((s) => {
|
|
383
330
|
const sr = asRecord(s) ?? {};
|
|
384
|
-
// The live route (/tennis/matches/live/{id}) spells this set_num +
|
|
385
|
-
// score + is_completed; the archive route (/tennis/matches/{id})
|
|
386
|
-
// spells the same set set_number, with no per-set score string
|
|
387
|
-
// (the match-level `score` already holds "6-4, 6-4, 2-1") and no
|
|
388
|
-
// completion flag. Read both spellings rather than nulling out
|
|
389
|
-
// real data because match_summary happened to resolve the other
|
|
390
|
-
// route this time.
|
|
391
331
|
const p1g = typeof sr.player1_games === 'number' ? sr.player1_games : null;
|
|
392
332
|
const p2g = typeof sr.player2_games === 'number' ? sr.player2_games : null;
|
|
393
333
|
return {
|
|
@@ -396,9 +336,6 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
396
336
|
player1Games: p1g,
|
|
397
337
|
player2Games: p2g,
|
|
398
338
|
tiebreak: sr.tiebreak ?? null,
|
|
399
|
-
// is_completed is a LIVE-route field. The archive route omits it,
|
|
400
|
-
// so every set of a finished match read completed:false. Fall
|
|
401
|
-
// back to the games, which always decide whether a set is over.
|
|
402
339
|
completed: tennisSetCompleted(sr),
|
|
403
340
|
};
|
|
404
341
|
});
|
|
@@ -459,28 +396,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
459
396
|
});
|
|
460
397
|
},
|
|
461
398
|
};
|
|
462
|
-
/**
|
|
463
|
-
* Fold a UFC bout's raw odds payload into something an agent can read.
|
|
464
|
-
*
|
|
465
|
-
* The REST response is deliberately complete: every bookmaker, every market, up
|
|
466
|
-
* to roughly 950 outcomes for a single bout. Handing that back whole would bury
|
|
467
|
-
* the two numbers a caller almost always wants, and the server's own
|
|
468
|
-
* instructions say not to flood odds. So the moneyline is summarised per fighter
|
|
469
|
-
* and every other market is reduced to a count, with the raw endpoint named so
|
|
470
|
-
* anyone who genuinely needs the full book can go and get it.
|
|
471
|
-
*
|
|
472
|
-
* isAvailable is carried through as currentlyOffered rather than filtered on. It
|
|
473
|
-
* means "a book is offering this right now", so a finished fight's closing line
|
|
474
|
-
* comes back with it false, and that is information rather than absence.
|
|
475
|
-
* Filtering here would recreate the bug where whole settled cards looked like
|
|
476
|
-
* they had no odds at all.
|
|
477
|
-
*/
|
|
478
399
|
export function summarizeUfcOdds(data) {
|
|
479
|
-
// fetchJson hands back the whole REST envelope ({ success, data, meta }), not
|
|
480
|
-
// the inner payload, so unwrap one level when it is there. Accepting both
|
|
481
|
-
// shapes because the unit tests feed the inner object directly; reading only
|
|
482
|
-
// the outer one returned a perfectly shaped, perfectly empty summary, which
|
|
483
|
-
// looked like "this bout has no odds" rather than like a bug.
|
|
484
400
|
const root = (data ?? {});
|
|
485
401
|
const payload = (root.data && typeof root.data === 'object' && !Array.isArray(root.data)
|
|
486
402
|
? root.data
|
|
@@ -523,9 +439,6 @@ export function summarizeUfcOdds(data) {
|
|
|
523
439
|
otherMarkets.set(type, other);
|
|
524
440
|
}
|
|
525
441
|
}
|
|
526
|
-
// American odds: the best price is the highest number both for an underdog
|
|
527
|
-
// (+250 beats +180) and for a favourite (-110 beats -200), so it is simply the
|
|
528
|
-
// maximum in both cases.
|
|
529
442
|
const moneyline = [...byFighter.values()].map((entry) => {
|
|
530
443
|
const sorted = [...entry.prices].sort((a, b) => a - b);
|
|
531
444
|
const mid = sorted.length
|
|
@@ -555,33 +468,12 @@ export function summarizeUfcOdds(data) {
|
|
|
555
468
|
fullBookPath: '/ufc/bouts/{boutId}/odds',
|
|
556
469
|
};
|
|
557
470
|
}
|
|
558
|
-
/**
|
|
559
|
-
* Per-period ("ALL / 1ST / 2ND / 3RD") statistics.
|
|
560
|
-
*
|
|
561
|
-
* The REST route gained `set_stats`: the same serving/returning figures split by
|
|
562
|
-
* set. Every field is nullable and a null is a STATEMENT, not a zero -- 0 aces is
|
|
563
|
-
* a claim, "we do not know" is not. `missing` names the nulls per side and `gaps`
|
|
564
|
-
* says why each one is absent, so a caller can tell these three apart:
|
|
565
|
-
* PERIOD_NO_STATS no stats exist for that period at all
|
|
566
|
-
* INCOMPLETE_PERIOD_STATS the period exists but a field is unknown
|
|
567
|
-
* SOURCE_UNAVAILABLE / SOURCE_NOT_MAPPED
|
|
568
|
-
* a source was consulted and did not deliver
|
|
569
|
-
* Collapsing any of that into zero, or dropping `gaps`, would delete the very
|
|
570
|
-
* information that makes the block trustworthy.
|
|
571
|
-
*/
|
|
572
471
|
export function summarizeTennisSetStats(block) {
|
|
573
472
|
const b = asRecord(block);
|
|
574
473
|
if (!b)
|
|
575
474
|
return null;
|
|
576
475
|
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
577
476
|
const strings = (v) => arr(v).map(String);
|
|
578
|
-
/**
|
|
579
|
-
* An object carrying nothing at all is not a block: the API sends
|
|
580
|
-
* `set_stats: null` when it has no rows, so `{}` can only be a caller's stub,
|
|
581
|
-
* and `setStats: null` says "no information" more honestly than a block of
|
|
582
|
-
* empty arrays. A block with ONLY gaps is kept, because the gaps are the
|
|
583
|
-
* information.
|
|
584
|
-
*/
|
|
585
477
|
if (!arr(b.periods).length && !arr(b.gaps).length && !arr(b.sources).length
|
|
586
478
|
&& typeof b.note !== 'string') {
|
|
587
479
|
return null;
|
|
@@ -645,11 +537,6 @@ export function summarizeTennisSetStats(block) {
|
|
|
645
537
|
note: str(b.note),
|
|
646
538
|
};
|
|
647
539
|
}
|
|
648
|
-
/**
|
|
649
|
-
* One line per stated absence, for the envelope's meta.warnings. The gaps also
|
|
650
|
-
* ride inside the section; this is what makes them visible to a client that only
|
|
651
|
-
* reads the envelope.
|
|
652
|
-
*/
|
|
653
540
|
export function tennisSetStatsWarnings(block) {
|
|
654
541
|
const shaped = summarizeTennisSetStats(block);
|
|
655
542
|
if (!shaped)
|
|
@@ -660,28 +547,11 @@ export function tennisSetStatsWarnings(block) {
|
|
|
660
547
|
const where = [gap.period, gap.side].filter(Boolean).join(' ');
|
|
661
548
|
out.push(`set_stats ${gap.code}${where ? ` (${where})` : ''}: ${gap.message}`);
|
|
662
549
|
}
|
|
663
|
-
// One warning per distinct code is enough for a client that only needs to know
|
|
664
|
-
// the block is not fully populated; the section carries the detail.
|
|
665
550
|
const note = shaped.note;
|
|
666
551
|
if (gaps.length && typeof note === 'string')
|
|
667
552
|
out.unshift(`set_stats: ${note}`);
|
|
668
553
|
return out.slice(0, 6);
|
|
669
554
|
}
|
|
670
|
-
/**
|
|
671
|
-
* Tennis per-match stats projection.
|
|
672
|
-
*
|
|
673
|
-
* `/tennis/matches/{id}/stats` does NOT return a row per player the way every
|
|
674
|
-
* other game's player-stats route does. It returns
|
|
675
|
-
* `{ stats: { winner: {...}, loser: {...} }, sets: [...], score, set_stats }` —
|
|
676
|
-
* two objects keyed by OUTCOME. `match_details sections:["playerStats"]` had no
|
|
677
|
-
* tennis arm at all, so it answered `playerStats: null` while `match_summary`
|
|
678
|
-
* was filling `playerPerformances` from the same route.
|
|
679
|
-
*
|
|
680
|
-
* The sides stay keyed by outcome rather than being mislabelled as player1 /
|
|
681
|
-
* player2, because the payload carries no names and the ordering is not
|
|
682
|
-
* guaranteed to match the base row. `match.player1.is_winner` on the base
|
|
683
|
-
* section is what maps a name onto these numbers, and `sidesKeyedBy` says so.
|
|
684
|
-
*/
|
|
685
555
|
export function summarizeTennisMatchStats(data) {
|
|
686
556
|
const root = asRecord(data) ?? {};
|
|
687
557
|
const payload = asRecord(root.data) ?? root;
|
|
@@ -710,31 +580,29 @@ export function summarizeTennisMatchStats(data) {
|
|
|
710
580
|
completed: tennisSetCompleted(sr),
|
|
711
581
|
};
|
|
712
582
|
}),
|
|
713
|
-
// ALL / 1ST / 2ND / 3RD ... Null when the API has no per-period rows for
|
|
714
|
-
// this match, which is a different statement from an ingested empty period.
|
|
715
583
|
setStats: summarizeTennisSetStats(payload.set_stats),
|
|
716
584
|
rawPath: '/tennis/matches/{matchId}/stats',
|
|
717
585
|
};
|
|
718
586
|
}
|
|
719
587
|
export const matchDetails = {
|
|
720
588
|
name: 'match_details',
|
|
721
|
-
description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
|
|
722
|
-
|
|
723
|
-
When to use:
|
|
724
|
-
- Analyst deep dive
|
|
725
|
-
- Live in-game window (LoL/CS2/UFC)
|
|
726
|
-
- Full demo list
|
|
727
|
-
|
|
728
|
-
Prefer over match_summary only when summary is insufficient.
|
|
729
|
-
Prefer match_summary for short answers and default cards.
|
|
730
|
-
|
|
731
|
-
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
732
|
-
|
|
733
|
-
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
734
|
-
UFC betting lines: sections:["odds"] (opt-in, never in the default set).
|
|
735
|
-
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
736
|
-
|
|
737
|
-
Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
|
|
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).
|
|
738
606
|
Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "includeLiveState": false }`,
|
|
739
607
|
inputSchema: {
|
|
740
608
|
type: 'object',
|
|
@@ -810,19 +678,11 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
810
678
|
media: null,
|
|
811
679
|
advanced: null,
|
|
812
680
|
};
|
|
813
|
-
// base is ALWAYS fetched, whatever sections were asked for. Requesting
|
|
814
|
-
// sections:["timeline"] returned match:null, so a caller got a timeline with
|
|
815
|
-
// nothing to attach it to and no way to tell which match it belonged to.
|
|
816
|
-
// The header is the identity of the response, not an optional section.
|
|
817
681
|
wanted.add('base');
|
|
818
682
|
if (wanted.has('base')) {
|
|
819
683
|
let res = await getSection(ctx, primaryPath(game, matchId));
|
|
820
684
|
upstreamCalls += 1;
|
|
821
685
|
rateLimit = res.headers;
|
|
822
|
-
// Same fallback match_summary already has: the live board hands out
|
|
823
|
-
// s365_* ids that only resolve at /matches/live/{id} until the match
|
|
824
|
-
// finishes and archives. Without this, a matchId taken straight off
|
|
825
|
-
// live_matches 404s here even though match_summary resolves it fine.
|
|
826
686
|
if (!res.ok && game === 'tennis' && res.status === 404) {
|
|
827
687
|
res = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
828
688
|
upstreamCalls += 1;
|
|
@@ -869,10 +729,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
869
729
|
}
|
|
870
730
|
})());
|
|
871
731
|
};
|
|
872
|
-
// Same as load(), but folds the payload through a shaper first. Raw odds are
|
|
873
|
-
// far too big to hand back whole: one bout carries up to ~950 outcomes across
|
|
874
|
-
// every bookmaker and prop market, which would bury the answer it was fetched
|
|
875
|
-
// to give.
|
|
876
732
|
const loadWith = (section, path, shape, query) => {
|
|
877
733
|
tasks.push((async () => {
|
|
878
734
|
const res = await getSection(ctx, path, query);
|
|
@@ -902,8 +758,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
902
758
|
load('playerStats', `/cod/matches/${encodeURIComponent(matchId)}/player-stats`);
|
|
903
759
|
if (game === 'ufc')
|
|
904
760
|
load('playerStats', `/ufc/bouts/${encodeURIComponent(matchId)}/stats`);
|
|
905
|
-
// Tennis has a real per-match stats route; the section simply had no arm
|
|
906
|
-
// for it and shipped playerStats:null forever.
|
|
907
761
|
if (game === 'tennis') {
|
|
908
762
|
loadWith('playerStats', `/tennis/matches/${encodeURIComponent(matchId)}/stats`, summarizeTennisMatchStats);
|
|
909
763
|
}
|
|
@@ -916,7 +770,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
916
770
|
if (game === 'ufc')
|
|
917
771
|
load('gamesOrMaps', `/ufc/bouts/${encodeURIComponent(matchId)}/rounds`);
|
|
918
772
|
if (game === 'cs2' || game === 'dota2') {
|
|
919
|
-
// no dedicated maps list — leave null with note via partial only if requested alone
|
|
920
773
|
sections.gamesOrMaps = sections.base;
|
|
921
774
|
}
|
|
922
775
|
}
|
|
@@ -954,11 +807,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
954
807
|
loadWith('odds', `/ufc/bouts/${encodeURIComponent(matchId)}/odds`, summarizeUfcOdds);
|
|
955
808
|
}
|
|
956
809
|
else {
|
|
957
|
-
// Tennis odds were REMOVED 2026-09-13 alongside the /tennis/odds/* routes.
|
|
958
|
-
// Betting data concentrates the legal risk and is the subject of every
|
|
959
|
-
// significant dispute researched (see docs/tennis-risk-register.md), so
|
|
960
|
-
// it is no longer offered. Requesting the section is now an explicit
|
|
961
|
-
// NOT_IMPLEMENTED rather than a call to a route that returns 404.
|
|
962
810
|
partial.push(partialFromRejection('odds', {
|
|
963
811
|
code: 'NOT_IMPLEMENTED',
|
|
964
812
|
message: `Odds are not curated for ${game}. Currently available: UFC (/ufc/bouts/{id}/odds). Tennis odds were withdrawn.`,
|
|
@@ -988,9 +836,6 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
988
836
|
}
|
|
989
837
|
}
|
|
990
838
|
await Promise.all(tasks);
|
|
991
|
-
// Per-period coverage gaps ride in meta.warnings as well as inside the
|
|
992
|
-
// playerStats section, so a client that only reads the envelope still learns
|
|
993
|
-
// that the ALL / 1ST / 2ND / 3RD block is not fully populated and why.
|
|
994
839
|
const warnings = [];
|
|
995
840
|
if (game === 'tennis') {
|
|
996
841
|
const ps = asRecord(sections.playerStats);
|
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';
|
|
@@ -368,7 +365,7 @@ const TOOL_CATALOG = [
|
|
|
368
365
|
parallelSafe: true,
|
|
369
366
|
games: ['cs2'],
|
|
370
367
|
jobs: ['team_page', 'preview'],
|
|
371
|
-
exampleArgs: { teamId: '
|
|
368
|
+
exampleArgs: { teamId: 'cs2-team-4608', days: 90 },
|
|
372
369
|
preferOver: ['manual match history scraping'],
|
|
373
370
|
doNotUse: 'For non-CS2 games',
|
|
374
371
|
},
|
|
@@ -433,8 +430,6 @@ const JOBS = [
|
|
|
433
430
|
{ id: 'schedule', description: 'Upcoming fixtures/events', recommendedTools: ['upcoming_schedule', 'event_card'] },
|
|
434
431
|
{ id: 'preview', description: 'Pre-match briefing', recommendedTools: ['match_preview'] },
|
|
435
432
|
{ id: 'event_card', description: 'Event / fight-night card page', recommendedTools: ['event_card', 'resolve_entity', 'match_preview'] },
|
|
436
|
-
// Agents asked "does this API have photos?" and, finding no job for it,
|
|
437
|
-
// assumed no. Photos ship on every UFC corner and profile; make that findable.
|
|
438
433
|
{
|
|
439
434
|
id: 'media',
|
|
440
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.',
|
|
@@ -643,7 +638,6 @@ Example: { "includeGameProbes": true }`,
|
|
|
643
638
|
});
|
|
644
639
|
}
|
|
645
640
|
else {
|
|
646
|
-
// try product root
|
|
647
641
|
const cs2 = await fetchJson(ctx, '/cs2');
|
|
648
642
|
upstreamCalls += 1;
|
|
649
643
|
rateLimit = cs2.headers;
|
|
@@ -660,11 +654,10 @@ Example: { "includeGameProbes": true }`,
|
|
|
660
654
|
}
|
|
661
655
|
}
|
|
662
656
|
else {
|
|
663
|
-
// health may be unauthenticated — confirm key with light authed probe
|
|
664
657
|
const probe = await fetchJson(ctx, '/lol/leagues');
|
|
665
658
|
upstreamCalls += 1;
|
|
666
659
|
rateLimit = { ...rateLimit, ...probe.headers };
|
|
667
|
-
keyValid = probe.ok || probe.status === 403;
|
|
660
|
+
keyValid = probe.ok || probe.status === 403;
|
|
668
661
|
if (probe.ok)
|
|
669
662
|
answeredBy = '/health+/lol/leagues';
|
|
670
663
|
if (probe.status === 401) {
|
|
@@ -693,10 +686,8 @@ Example: { "includeGameProbes": true }`,
|
|
|
693
686
|
if (res.ok)
|
|
694
687
|
includedGames.push(game);
|
|
695
688
|
else if (res.status === 403 && gameNotIncludedHint(res.data)) {
|
|
696
|
-
// not included
|
|
697
689
|
}
|
|
698
690
|
else if (res.status !== 401 && res.status !== 404) {
|
|
699
|
-
// ambiguous — still list as probed
|
|
700
691
|
}
|
|
701
692
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
702
693
|
}
|
|
@@ -733,8 +724,6 @@ const ALLOWLIST_PREFIXES = [
|
|
|
733
724
|
'/ufc',
|
|
734
725
|
'/fortnite',
|
|
735
726
|
'/tennis',
|
|
736
|
-
// The spec describes the surface call_api is allowed to reach; refusing to
|
|
737
|
-
// serve it left route discovery impossible except by guessing.
|
|
738
727
|
'/openapi.json',
|
|
739
728
|
];
|
|
740
729
|
function pathAllowed(path) {
|
|
@@ -770,9 +759,9 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
770
759
|
properties: {
|
|
771
760
|
method: {
|
|
772
761
|
type: 'string',
|
|
773
|
-
enum: ['GET'
|
|
762
|
+
enum: ['GET'],
|
|
774
763
|
default: 'GET',
|
|
775
|
-
description: 'HTTP method.
|
|
764
|
+
description: 'HTTP method. GET only: call_api is read-only. Example: "GET".',
|
|
776
765
|
},
|
|
777
766
|
path: stringSchema('Path starting with /. Allowlisted prefixes only. Example: "/cs2/rankings/teams".', '/cs2/rankings/teams'),
|
|
778
767
|
queryJson: {
|
|
@@ -784,10 +773,6 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
784
773
|
type: 'object',
|
|
785
774
|
description: 'Query params as a plain object (alternative to queryJson). Example: {"year":2026}.',
|
|
786
775
|
},
|
|
787
|
-
bodyJson: {
|
|
788
|
-
type: 'string',
|
|
789
|
-
description: 'Stringified JSON body for POST only (rare).',
|
|
790
|
-
},
|
|
791
776
|
},
|
|
792
777
|
},
|
|
793
778
|
handler: async (args, ctx) => {
|
|
@@ -805,10 +790,10 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
805
790
|
tookMs: Date.now() - started,
|
|
806
791
|
});
|
|
807
792
|
}
|
|
808
|
-
if (method !== 'GET'
|
|
793
|
+
if (method !== 'GET') {
|
|
809
794
|
return errorEnvelope({
|
|
810
795
|
code: 'VALIDATION',
|
|
811
|
-
message: 'method must be GET
|
|
796
|
+
message: 'method must be GET; call_api is read-only',
|
|
812
797
|
game: null,
|
|
813
798
|
source: 'call_api',
|
|
814
799
|
requestId,
|
|
@@ -827,8 +812,6 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
827
812
|
});
|
|
828
813
|
}
|
|
829
814
|
let query = {};
|
|
830
|
-
// Agents pass query as a plain object at least as often as the stringified
|
|
831
|
-
// queryJson form; silently dropping it produced confusing upstream 422s.
|
|
832
815
|
if (args.query && typeof args.query === 'object' && !Array.isArray(args.query)) {
|
|
833
816
|
query = { ...args.query };
|
|
834
817
|
}
|
|
@@ -851,23 +834,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
851
834
|
});
|
|
852
835
|
}
|
|
853
836
|
}
|
|
854
|
-
|
|
855
|
-
if (method === 'POST' && typeof args.bodyJson === 'string' && args.bodyJson.trim()) {
|
|
856
|
-
try {
|
|
857
|
-
body = JSON.parse(args.bodyJson);
|
|
858
|
-
}
|
|
859
|
-
catch (e) {
|
|
860
|
-
return errorEnvelope({
|
|
861
|
-
code: 'VALIDATION',
|
|
862
|
-
message: `Invalid bodyJson: ${e.message}`,
|
|
863
|
-
game: null,
|
|
864
|
-
source: 'call_api',
|
|
865
|
-
requestId,
|
|
866
|
-
tookMs: Date.now() - started,
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
const res = await fetchJson(ctx, path, { method, query, body });
|
|
837
|
+
const res = await fetchJson(ctx, path, { method, query, raw: true });
|
|
871
838
|
if (!res.ok) {
|
|
872
839
|
return errorEnvelope({
|
|
873
840
|
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
@@ -899,14 +866,6 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
899
866
|
});
|
|
900
867
|
},
|
|
901
868
|
};
|
|
902
|
-
/**
|
|
903
|
-
* Games whose routes the published spec does not describe.
|
|
904
|
-
*
|
|
905
|
-
* The spec has 123 paths and zero under /lol, yet every LoL route works —
|
|
906
|
-
* api_health itself answers partly from /lol/leagues. An agent that treats the
|
|
907
|
-
* spec as the whole surface concludes LoL is unsupported and stops, so say so
|
|
908
|
-
* explicitly rather than returning an empty list that reads as a verdict.
|
|
909
|
-
*/
|
|
910
869
|
const SPEC_OMITS = {
|
|
911
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.',
|
|
912
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.',
|
|
@@ -949,9 +908,6 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
949
908
|
handler: async (args, ctx) => {
|
|
950
909
|
const started = Date.now();
|
|
951
910
|
const requestId = newRequestId();
|
|
952
|
-
// Accept fortnite here even though it is not a PRIMARY_GAME: it has 21
|
|
953
|
-
// documented paths and no curated tools, so it is exactly what call_api
|
|
954
|
-
// callers come looking for.
|
|
955
911
|
const gameRaw = typeof args.game === 'string' ? args.game.toLowerCase().trim() : '';
|
|
956
912
|
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis)$/)) {
|
|
957
913
|
return errorEnvelope({
|
|
@@ -1042,5 +998,4 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
1042
998
|
},
|
|
1043
999
|
};
|
|
1044
1000
|
export const metaTools = [listCapabilities, apiHealth, listRoutes, callApi];
|
|
1045
|
-
// silence unused import in case extractRows needed later
|
|
1046
1001
|
void extractRows;
|