cito-mcp 0.5.8 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -21
- package/dist/client.js +8 -8
- package/dist/envelope.js +11 -11
- package/dist/index.js +2 -2
- package/dist/install.js +15 -15
- package/dist/instructions.js +10 -4
- package/dist/pacing.js +2 -2
- package/dist/scrub.js +87 -11
- package/dist/tools/combat.js +96 -96
- package/dist/tools/index.js +2 -0
- package/dist/tools/insight.js +48 -46
- package/dist/tools/leaderboard.js +6 -6
- package/dist/tools/live.js +45 -44
- package/dist/tools/match.js +90 -43
- package/dist/tools/meta.js +108 -73
- package/dist/tools/normalize.js +3 -2
- package/dist/tools/player.js +67 -28
- package/dist/tools/rankings.js +42 -9
- package/dist/tools/resolve.js +61 -61
- package/dist/tools/schedule.js +15 -14
- package/dist/tools/standings.js +66 -46
- package/dist/tools/team.js +91 -89
- package/dist/tools/tournaments.js +7 -9
- package/dist/tools/types.js +1 -1
- package/dist/tools/webhooks.js +377 -0
- package/package.json +51 -52
package/dist/tools/insight.js
CHANGED
|
@@ -26,6 +26,9 @@ async function loadSide(ctx, game, side, recentLimit, includeRosters) {
|
|
|
26
26
|
hand: pickString(data.hand) ?? null,
|
|
27
27
|
age: typeof data.age === 'number' ? data.age : null,
|
|
28
28
|
currentRank: typeof data.current_rank === 'number' ? data.current_rank : null,
|
|
29
|
+
lastRank: typeof data.last_rank === 'number' ? data.last_rank : null,
|
|
30
|
+
lastRankDate: pickString(data.last_rank_date) ?? null,
|
|
31
|
+
status: pickString(data.status) ?? null,
|
|
29
32
|
careerHighRank: typeof data.career_high_rank === 'number' ? data.career_high_rank : null,
|
|
30
33
|
careerRecord: career.matches_won != null && career.matches_lost != null
|
|
31
34
|
? `${career.matches_won}-${career.matches_lost}`
|
|
@@ -272,7 +275,7 @@ Prefer match_summary when match is completed; match_details for live in-game.
|
|
|
272
275
|
|
|
273
276
|
Do not use when: user wants final score/recap of a finished match.
|
|
274
277
|
|
|
275
|
-
Parallel-safe: yes.
|
|
278
|
+
Parallel-safe: yes. API calls: 4–8.
|
|
276
279
|
Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
|
|
277
280
|
Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "recentLimit": 5 }`,
|
|
278
281
|
inputSchema: {
|
|
@@ -340,7 +343,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
340
343
|
});
|
|
341
344
|
}
|
|
342
345
|
const partial = [];
|
|
343
|
-
let
|
|
346
|
+
let apiCalls = 0;
|
|
344
347
|
let rateLimit = {};
|
|
345
348
|
let context = {
|
|
346
349
|
matchId: matchId || null,
|
|
@@ -364,7 +367,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
364
367
|
else if (game === 'tennis')
|
|
365
368
|
path = `/tennis/matches/${encodeURIComponent(matchId)}`;
|
|
366
369
|
const res = await fetchJson(ctx, path);
|
|
367
|
-
|
|
370
|
+
apiCalls += 1;
|
|
368
371
|
rateLimit = res.headers;
|
|
369
372
|
if (res.ok) {
|
|
370
373
|
const entity = unwrapPayload(res.data);
|
|
@@ -408,14 +411,14 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
408
411
|
source: 'match_preview',
|
|
409
412
|
requestId,
|
|
410
413
|
tookMs: Date.now() - started,
|
|
411
|
-
|
|
414
|
+
apiCalls,
|
|
412
415
|
});
|
|
413
416
|
}
|
|
414
417
|
const [sideARes, sideBRes] = await Promise.all([
|
|
415
418
|
loadSide(ctx, game, teamA, recentLimit, includeRosters),
|
|
416
419
|
loadSide(ctx, game, teamB, recentLimit, includeRosters),
|
|
417
420
|
]);
|
|
418
|
-
|
|
421
|
+
apiCalls += sideARes.calls + sideBRes.calls;
|
|
419
422
|
rateLimit = { ...rateLimit, ...sideARes.rateLimit, ...sideBRes.rateLimit };
|
|
420
423
|
partial.push(...sideARes.partial.map((p) => ({ ...p, section: `sideA.${p.section}` })));
|
|
421
424
|
partial.push(...sideBRes.partial.map((p) => ({ ...p, section: `sideB.${p.section}` })));
|
|
@@ -425,7 +428,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
425
428
|
let rows = [];
|
|
426
429
|
if (game === 'cs2') {
|
|
427
430
|
const res = await fetchJson(ctx, '/cs2/matches', { query: { team: teamA, limit: 40 } });
|
|
428
|
-
|
|
431
|
+
apiCalls += 1;
|
|
429
432
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
430
433
|
if (res.ok)
|
|
431
434
|
rows = extractRows(res.data);
|
|
@@ -441,21 +444,21 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
441
444
|
const res = await fetchJson(ctx, '/lol/schedule', {
|
|
442
445
|
query: { teamSlug: teamA, completed: 'true', limit: '40' },
|
|
443
446
|
});
|
|
444
|
-
|
|
447
|
+
apiCalls += 1;
|
|
445
448
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
446
449
|
if (res.ok)
|
|
447
450
|
rows = extractRows(res.data);
|
|
448
451
|
}
|
|
449
452
|
else if (game === 'cod') {
|
|
450
453
|
const res = await fetchJson(ctx, '/cod/matches', { query: { team: teamA, limit: 40 } });
|
|
451
|
-
|
|
454
|
+
apiCalls += 1;
|
|
452
455
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
453
456
|
if (res.ok)
|
|
454
457
|
rows = extractRows(res.data);
|
|
455
458
|
}
|
|
456
459
|
else if (game === 'dota2') {
|
|
457
460
|
const res = await fetchJson(ctx, '/dota2/matches/recent', { query: { limit: 40 } });
|
|
458
|
-
|
|
461
|
+
apiCalls += 1;
|
|
459
462
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
460
463
|
if (res.ok)
|
|
461
464
|
rows = extractRows(res.data);
|
|
@@ -465,7 +468,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
465
468
|
if (game === 'tennis' && surface)
|
|
466
469
|
h2hQuery.surface = surface;
|
|
467
470
|
const h2hRes = await fetchJson(ctx, '/tennis/h2h', { query: h2hQuery });
|
|
468
|
-
|
|
471
|
+
apiCalls += 1;
|
|
469
472
|
rateLimit = { ...rateLimit, ...h2hRes.headers };
|
|
470
473
|
if (h2hRes.ok) {
|
|
471
474
|
const rec = asRecord(unwrapPayload(h2hRes.data)) ?? {};
|
|
@@ -500,7 +503,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
500
503
|
const hist = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(teamA)}/fights`, {
|
|
501
504
|
query: { limit: 50 },
|
|
502
505
|
});
|
|
503
|
-
|
|
506
|
+
apiCalls += 1;
|
|
504
507
|
rateLimit = { ...rateLimit, ...hist.headers };
|
|
505
508
|
if (hist.ok) {
|
|
506
509
|
rows = extractRows(unwrapPayload(hist.data) ?? hist.data).map((row) => {
|
|
@@ -520,7 +523,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
520
523
|
}
|
|
521
524
|
catch (e) {
|
|
522
525
|
partial.push(partialFromRejection('h2h', {
|
|
523
|
-
code: '
|
|
526
|
+
code: 'SERVICE_ERROR',
|
|
524
527
|
message: e.message,
|
|
525
528
|
}));
|
|
526
529
|
}
|
|
@@ -593,7 +596,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
593
596
|
game,
|
|
594
597
|
requestId,
|
|
595
598
|
tookMs: Date.now() - started,
|
|
596
|
-
|
|
599
|
+
apiCalls,
|
|
597
600
|
rateLimit,
|
|
598
601
|
partial: partial.length ? partial : undefined,
|
|
599
602
|
entities: {
|
|
@@ -718,7 +721,7 @@ Bouts come back in card order — main event first, then prelims, then early pre
|
|
|
718
721
|
Each bout carries weightClass, titleBout, card placement, and (once fought) result
|
|
719
722
|
{ method, round, time, referee, winnerSlug }. Each corner carries images
|
|
720
723
|
{ headshotUrl, bodyImageUrl, imageUrl, proxiedImageUrl }, record, nickname, rank,
|
|
721
|
-
championStatus, country and flag when
|
|
724
|
+
championStatus, country and flag when available. Use proxiedImageUrl in
|
|
722
725
|
browsers — the image host sends no CORS header. You do not need call_api per fighter for faces.
|
|
723
726
|
|
|
724
727
|
Prefer over: agent-side resolve + call_api /ufc/events + bout expansion; N+1 match_summary for the card list only; per-fighter call_api just to fetch headshots.
|
|
@@ -730,14 +733,14 @@ Do not use when: you only need live scores (live_matches); single finished match
|
|
|
730
733
|
Tennis: pass a tournament id/slug to get the draw bracket — every main round from
|
|
731
734
|
the first to the Final (status completed / in_progress / not_started), then
|
|
732
735
|
qualifying (Q1..Q3). Each result carries winnerId/loserId/outcome (retirements
|
|
733
|
-
included), startTime (null with startTimeUnavailableReason when
|
|
736
|
+
included), startTime (null with startTimeUnavailableReason when no start time is
|
|
734
737
|
clock), nextMatchId (the bracket edge) and bracketPosition; scheduled and live
|
|
735
738
|
matches sit in rounds[].pending. draw carries drawSize (real field size, byes
|
|
736
739
|
excluded, e.g. 56), byes, matchesExpected (main draw) and complete (true only
|
|
737
740
|
when every main round to the Final is played). This is the primary tennis path
|
|
738
741
|
here; there is no separate draw tool.
|
|
739
742
|
|
|
740
|
-
Parallel-safe: yes.
|
|
743
|
+
Parallel-safe: yes. API calls: 1–4.
|
|
741
744
|
Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "includeStandings": false }`,
|
|
742
745
|
inputSchema: {
|
|
743
746
|
type: 'object',
|
|
@@ -751,7 +754,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
751
754
|
includeMatches: boolSchema('Include the bout/match list, or the tennis draw bracket (default true).', true),
|
|
752
755
|
includeStandings: boolSchema('Include standings/rankings snippet when API supports event/tournament/division scope (default false). ' +
|
|
753
756
|
'For UFC this also joins divisional rank and movement onto each bout corner as team.rank / team.rankMovement — ' +
|
|
754
|
-
'bout rows themselves carry no rank. Costs one extra
|
|
757
|
+
'bout rows themselves carry no rank. Costs one extra API call.', false),
|
|
755
758
|
limit: limitSchema({
|
|
756
759
|
default: 20,
|
|
757
760
|
max: 50,
|
|
@@ -799,12 +802,12 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
799
802
|
});
|
|
800
803
|
}
|
|
801
804
|
const partial = [];
|
|
802
|
-
let
|
|
805
|
+
let apiCalls = 0;
|
|
803
806
|
let rateLimit = {};
|
|
804
807
|
const warnings = [];
|
|
805
808
|
if (!eventKey && q) {
|
|
806
809
|
const resolved = await resolveEventKey(ctx, game, q);
|
|
807
|
-
|
|
810
|
+
apiCalls += resolved.calls;
|
|
808
811
|
rateLimit = { ...rateLimit, ...resolved.rateLimit };
|
|
809
812
|
if (resolved.key) {
|
|
810
813
|
eventKey = resolved.key;
|
|
@@ -817,7 +820,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
817
820
|
source: 'event_card',
|
|
818
821
|
requestId,
|
|
819
822
|
tookMs: Date.now() - started,
|
|
820
|
-
|
|
823
|
+
apiCalls,
|
|
821
824
|
rateLimit,
|
|
822
825
|
recover: [
|
|
823
826
|
'Call resolve_entity { type: "event", game, q } or search_entities, then retry with eventIdOrSlug',
|
|
@@ -856,7 +859,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
856
859
|
};
|
|
857
860
|
if (game === 'ufc') {
|
|
858
861
|
const detail = await fetchJson(ctx, `/ufc/events/${encodeURIComponent(eventKey)}`);
|
|
859
|
-
|
|
862
|
+
apiCalls += 1;
|
|
860
863
|
rateLimit = { ...rateLimit, ...detail.headers };
|
|
861
864
|
if (detail.ok) {
|
|
862
865
|
const data = asRecord(detail.data) ?? {};
|
|
@@ -891,7 +894,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
891
894
|
source: 'event_card',
|
|
892
895
|
requestId,
|
|
893
896
|
tookMs: Date.now() - started,
|
|
894
|
-
|
|
897
|
+
apiCalls,
|
|
895
898
|
rateLimit,
|
|
896
899
|
httpStatus: 404,
|
|
897
900
|
recover: [
|
|
@@ -908,14 +911,14 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
908
911
|
source: 'event_card',
|
|
909
912
|
requestId,
|
|
910
913
|
tookMs: Date.now() - started,
|
|
911
|
-
|
|
914
|
+
apiCalls,
|
|
912
915
|
rateLimit,
|
|
913
916
|
httpStatus: detail.status,
|
|
914
917
|
});
|
|
915
918
|
}
|
|
916
919
|
if (includeBouts && bouts.length === 0) {
|
|
917
920
|
const boutsRes = await fetchJson(ctx, `/ufc/events/${encodeURIComponent(String(event.slug ?? eventKey))}/bouts`);
|
|
918
|
-
|
|
921
|
+
apiCalls += 1;
|
|
919
922
|
rateLimit = { ...rateLimit, ...boutsRes.headers };
|
|
920
923
|
if (boutsRes.ok) {
|
|
921
924
|
bouts = sortByCardOrder(extractRows(boutsRes.data).map((row) => normalizeMatch('ufc', {
|
|
@@ -937,7 +940,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
937
940
|
bouts = bouts.map((b) => ({ ...b, startTime: b.startTime ?? cardStart }));
|
|
938
941
|
if (includeStandings) {
|
|
939
942
|
const ranks = await fetchJson(ctx, '/ufc/rankings');
|
|
940
|
-
|
|
943
|
+
apiCalls += 1;
|
|
941
944
|
rateLimit = { ...rateLimit, ...ranks.headers };
|
|
942
945
|
if (ranks.ok) {
|
|
943
946
|
const rankRows = extractRows(ranks.data)
|
|
@@ -1006,7 +1009,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1006
1009
|
}
|
|
1007
1010
|
else if (game === 'cs2') {
|
|
1008
1011
|
const detail = await fetchJson(ctx, `/cs2/events/${encodeURIComponent(eventKey)}`);
|
|
1009
|
-
|
|
1012
|
+
apiCalls += 1;
|
|
1010
1013
|
rateLimit = { ...rateLimit, ...detail.headers };
|
|
1011
1014
|
if (detail.ok) {
|
|
1012
1015
|
const data = asRecord(detail.data) ?? {};
|
|
@@ -1028,7 +1031,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1028
1031
|
source: 'event_card',
|
|
1029
1032
|
requestId,
|
|
1030
1033
|
tookMs: Date.now() - started,
|
|
1031
|
-
|
|
1034
|
+
apiCalls,
|
|
1032
1035
|
rateLimit,
|
|
1033
1036
|
httpStatus: 404,
|
|
1034
1037
|
});
|
|
@@ -1045,7 +1048,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1045
1048
|
const matchesRes = await fetchJson(ctx, '/cs2/matches', {
|
|
1046
1049
|
query: { event: eventKey, limit, page: 1 },
|
|
1047
1050
|
});
|
|
1048
|
-
|
|
1051
|
+
apiCalls += 1;
|
|
1049
1052
|
rateLimit = { ...rateLimit, ...matchesRes.headers };
|
|
1050
1053
|
if (matchesRes.ok) {
|
|
1051
1054
|
bouts = extractRows(matchesRes.data)
|
|
@@ -1062,7 +1065,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1062
1065
|
}
|
|
1063
1066
|
if (includeStandings) {
|
|
1064
1067
|
const st = await fetchJson(ctx, `/cs2/events/${encodeURIComponent(eventKey)}/standings`);
|
|
1065
|
-
|
|
1068
|
+
apiCalls += 1;
|
|
1066
1069
|
rateLimit = { ...rateLimit, ...st.headers };
|
|
1067
1070
|
if (st.ok)
|
|
1068
1071
|
standingsSnippet = { scope: 'event', raw: st.data };
|
|
@@ -1077,7 +1080,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1077
1080
|
}
|
|
1078
1081
|
else if (game === 'lol') {
|
|
1079
1082
|
const detail = await fetchJson(ctx, `/lol/tournaments/${encodeURIComponent(eventKey)}`);
|
|
1080
|
-
|
|
1083
|
+
apiCalls += 1;
|
|
1081
1084
|
rateLimit = { ...rateLimit, ...detail.headers };
|
|
1082
1085
|
if (detail.ok) {
|
|
1083
1086
|
const data = asRecord(detail.data) ?? {};
|
|
@@ -1105,7 +1108,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1105
1108
|
source: 'event_card',
|
|
1106
1109
|
requestId,
|
|
1107
1110
|
tookMs: Date.now() - started,
|
|
1108
|
-
|
|
1111
|
+
apiCalls,
|
|
1109
1112
|
rateLimit,
|
|
1110
1113
|
httpStatus: 404,
|
|
1111
1114
|
});
|
|
@@ -1115,7 +1118,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1115
1118
|
const sched = await fetchJson(ctx, '/lol/schedule', {
|
|
1116
1119
|
query: { tournamentId: eventKey, limit: String(limit) },
|
|
1117
1120
|
});
|
|
1118
|
-
|
|
1121
|
+
apiCalls += 1;
|
|
1119
1122
|
rateLimit = { ...rateLimit, ...sched.headers };
|
|
1120
1123
|
if (sched.ok) {
|
|
1121
1124
|
bouts = extractRows(sched.data)
|
|
@@ -1132,7 +1135,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1132
1135
|
}
|
|
1133
1136
|
if (includeStandings) {
|
|
1134
1137
|
const st = await fetchJson(ctx, `/lol/tournaments/${encodeURIComponent(eventKey)}/standings`);
|
|
1135
|
-
|
|
1138
|
+
apiCalls += 1;
|
|
1136
1139
|
rateLimit = { ...rateLimit, ...st.headers };
|
|
1137
1140
|
if (st.ok)
|
|
1138
1141
|
standingsSnippet = { scope: 'tournament', raw: st.data };
|
|
@@ -1147,7 +1150,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1147
1150
|
}
|
|
1148
1151
|
else if (game === 'cod') {
|
|
1149
1152
|
const detail = await fetchJson(ctx, `/cod/tournaments/${encodeURIComponent(eventKey)}`);
|
|
1150
|
-
|
|
1153
|
+
apiCalls += 1;
|
|
1151
1154
|
rateLimit = { ...rateLimit, ...detail.headers };
|
|
1152
1155
|
if (detail.ok) {
|
|
1153
1156
|
const data = asRecord(detail.data) ?? {};
|
|
@@ -1167,7 +1170,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1167
1170
|
source: 'event_card',
|
|
1168
1171
|
requestId,
|
|
1169
1172
|
tookMs: Date.now() - started,
|
|
1170
|
-
|
|
1173
|
+
apiCalls,
|
|
1171
1174
|
rateLimit,
|
|
1172
1175
|
httpStatus: 404,
|
|
1173
1176
|
});
|
|
@@ -1183,7 +1186,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1183
1186
|
const matchesRes = await fetchJson(ctx, '/cod/matches', {
|
|
1184
1187
|
query: { tournamentId: eventKey, limit },
|
|
1185
1188
|
});
|
|
1186
|
-
|
|
1189
|
+
apiCalls += 1;
|
|
1187
1190
|
rateLimit = { ...rateLimit, ...matchesRes.headers };
|
|
1188
1191
|
if (matchesRes.ok) {
|
|
1189
1192
|
bouts = extractRows(matchesRes.data)
|
|
@@ -1200,7 +1203,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1200
1203
|
}
|
|
1201
1204
|
if (includeStandings) {
|
|
1202
1205
|
const st = await fetchJson(ctx, '/cod/standings', { query: { tournamentId: eventKey } });
|
|
1203
|
-
|
|
1206
|
+
apiCalls += 1;
|
|
1204
1207
|
rateLimit = { ...rateLimit, ...st.headers };
|
|
1205
1208
|
if (st.ok)
|
|
1206
1209
|
standingsSnippet = { scope: 'tournament', raw: st.data };
|
|
@@ -1215,7 +1218,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1215
1218
|
}
|
|
1216
1219
|
else if (game === 'tennis') {
|
|
1217
1220
|
const detail = await fetchJson(ctx, `/tennis/tournaments/${encodeURIComponent(eventKey)}`);
|
|
1218
|
-
|
|
1221
|
+
apiCalls += 1;
|
|
1219
1222
|
rateLimit = { ...rateLimit, ...detail.headers };
|
|
1220
1223
|
if (detail.ok) {
|
|
1221
1224
|
const d = asRecord(unwrapPayload(detail.data)) ?? {};
|
|
@@ -1246,7 +1249,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1246
1249
|
}
|
|
1247
1250
|
if (includeBouts) {
|
|
1248
1251
|
const draw = await fetchJson(ctx, `/tennis/tournaments/${encodeURIComponent(eventKey)}/draw`);
|
|
1249
|
-
|
|
1252
|
+
apiCalls += 1;
|
|
1250
1253
|
rateLimit = { ...rateLimit, ...draw.headers };
|
|
1251
1254
|
if (draw.ok) {
|
|
1252
1255
|
const dd = asRecord(unwrapPayload(draw.data)) ?? {};
|
|
@@ -1290,7 +1293,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1290
1293
|
? { startTimeUnavailableReason: pickString(mr.start_time_unavailable_reason) }
|
|
1291
1294
|
: {}),
|
|
1292
1295
|
nextMatchId: pickString(mr.next_match_id) ?? null,
|
|
1293
|
-
...(pickString(mr.next_match_source) ? {
|
|
1296
|
+
...(pickString(mr.next_match_source) ? { nextMatchBasis: pickString(mr.next_match_source)?.replace(/^source_/, '') } : {}),
|
|
1294
1297
|
bracketPosition: num(mr.bracket_position),
|
|
1295
1298
|
nextSlot: asRecord(mr.next_slot)
|
|
1296
1299
|
? {
|
|
@@ -1298,7 +1301,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1298
1301
|
position: num(asRecord(mr.next_slot)?.position),
|
|
1299
1302
|
}
|
|
1300
1303
|
: null,
|
|
1301
|
-
...(pickString(mr.source_round) ? {
|
|
1304
|
+
...(pickString(mr.source_round) ? { rawRound: pickString(mr.source_round) } : {}),
|
|
1302
1305
|
}, 'tennis');
|
|
1303
1306
|
};
|
|
1304
1307
|
const pendingMatch = (pr) => {
|
|
@@ -1315,7 +1318,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1315
1318
|
startTimeKnown: pr.start_time_known === true,
|
|
1316
1319
|
liveScore: pickString(pr.live_score) ?? null,
|
|
1317
1320
|
winnerId: null,
|
|
1318
|
-
...(pickString(pr.source_winner_id) ? {
|
|
1321
|
+
...(pickString(pr.source_winner_id) ? { reportedWinnerId: pickString(pr.source_winner_id) } : {}),
|
|
1319
1322
|
nextMatchId: pickString(pr.next_match_id) ?? null,
|
|
1320
1323
|
bracketPosition: num(pr.bracket_position),
|
|
1321
1324
|
};
|
|
@@ -1397,7 +1400,6 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1397
1400
|
}
|
|
1398
1401
|
drawSummary = {
|
|
1399
1402
|
drawSize,
|
|
1400
|
-
drawSizeSource: pickString(dd.draw_size_source) ?? null,
|
|
1401
1403
|
drawSizeVerified: typeof dd.draw_size_verified === 'boolean' ? dd.draw_size_verified : null,
|
|
1402
1404
|
drawSizeDerived: num(dd.draw_size_derived),
|
|
1403
1405
|
bracketSize: num(dd.bracket_size),
|
|
@@ -1441,7 +1443,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1441
1443
|
else {
|
|
1442
1444
|
warnings.push('Dota2 event_card is thin; expect partial bouts/standings');
|
|
1443
1445
|
const detail = await fetchJson(ctx, `/dota2/tournaments/${encodeURIComponent(eventKey)}`);
|
|
1444
|
-
|
|
1446
|
+
apiCalls += 1;
|
|
1445
1447
|
rateLimit = { ...rateLimit, ...detail.headers };
|
|
1446
1448
|
if (detail.ok) {
|
|
1447
1449
|
const data = asRecord(detail.data) ?? {};
|
|
@@ -1463,7 +1465,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1463
1465
|
}
|
|
1464
1466
|
if (includeBouts) {
|
|
1465
1467
|
const matchesRes = await fetchJson(ctx, '/dota2/matches/upcoming', { query: { limit } });
|
|
1466
|
-
|
|
1468
|
+
apiCalls += 1;
|
|
1467
1469
|
rateLimit = { ...rateLimit, ...matchesRes.headers };
|
|
1468
1470
|
if (matchesRes.ok) {
|
|
1469
1471
|
const key = eventKey.toLowerCase();
|
|
@@ -1502,7 +1504,7 @@ Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "i
|
|
|
1502
1504
|
game,
|
|
1503
1505
|
requestId,
|
|
1504
1506
|
tookMs: Date.now() - started,
|
|
1505
|
-
|
|
1507
|
+
apiCalls,
|
|
1506
1508
|
rateLimit,
|
|
1507
1509
|
warnings: warnings.length ? warnings : undefined,
|
|
1508
1510
|
partial: partial.length ? partial : undefined,
|
|
@@ -53,7 +53,7 @@ Do not use when: ranking position → standings with game tennis; week-over-week
|
|
|
53
53
|
|
|
54
54
|
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.
|
|
55
55
|
|
|
56
|
-
Parallel-safe: yes.
|
|
56
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
57
57
|
inputSchema: {
|
|
58
58
|
type: 'object',
|
|
59
59
|
additionalProperties: false,
|
|
@@ -139,7 +139,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
139
139
|
source: 'leaderboard_aces',
|
|
140
140
|
requestId,
|
|
141
141
|
tookMs: Date.now() - started,
|
|
142
|
-
|
|
142
|
+
apiCalls: 1,
|
|
143
143
|
httpStatus: res.status,
|
|
144
144
|
rateLimit: res.headers,
|
|
145
145
|
});
|
|
@@ -161,7 +161,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
161
161
|
game,
|
|
162
162
|
requestId,
|
|
163
163
|
tookMs: Date.now() - started,
|
|
164
|
-
|
|
164
|
+
apiCalls: 1,
|
|
165
165
|
rateLimit: res.headers,
|
|
166
166
|
data: {
|
|
167
167
|
title: `${tourParse.tour ?? 'Combined'} aces leaderboard ${seasonParse.season}`,
|
|
@@ -205,7 +205,7 @@ Do not use when: ranking position → standings with game tennis; week-over-week
|
|
|
205
205
|
|
|
206
206
|
Tennis-only. Season defaults to 2026; tour (ATP|WTA) is optional — omit it for the combined board. ${spec.minAttemptsNote}
|
|
207
207
|
|
|
208
|
-
Parallel-safe: yes.
|
|
208
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
209
209
|
inputSchema: {
|
|
210
210
|
type: 'object',
|
|
211
211
|
additionalProperties: false,
|
|
@@ -291,7 +291,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
291
291
|
source: spec.name,
|
|
292
292
|
requestId,
|
|
293
293
|
tookMs: Date.now() - started,
|
|
294
|
-
|
|
294
|
+
apiCalls: 1,
|
|
295
295
|
httpStatus: res.status,
|
|
296
296
|
rateLimit: res.headers,
|
|
297
297
|
});
|
|
@@ -313,7 +313,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
313
313
|
game,
|
|
314
314
|
requestId,
|
|
315
315
|
tookMs: Date.now() - started,
|
|
316
|
-
|
|
316
|
+
apiCalls: 1,
|
|
317
317
|
rateLimit: res.headers,
|
|
318
318
|
data: {
|
|
319
319
|
title: `${tourParse.tour ?? 'Combined'} ${spec.titleNoun} leaderboard ${seasonParse.season}`,
|
package/dist/tools/live.js
CHANGED
|
@@ -110,9 +110,9 @@ export function humanUfcEmptyNote(emptyReason, health) {
|
|
|
110
110
|
supervisor_warming: 'Card open / pre-gate; no bout live yet',
|
|
111
111
|
between_bouts: 'Between bouts; next may be armed',
|
|
112
112
|
armed_only: 'Next bout armed; not started',
|
|
113
|
-
worker_stale: 'Live
|
|
114
|
-
worker_down_inferred: 'Live
|
|
115
|
-
event_degraded: '
|
|
113
|
+
worker_stale: 'Live data is behind; scores may lag',
|
|
114
|
+
worker_down_inferred: 'Live data unavailable right now',
|
|
115
|
+
event_degraded: 'Live clock unavailable; not inventing stats',
|
|
116
116
|
};
|
|
117
117
|
const msg = human[reason] || `No live|watching bouts (${reason})`;
|
|
118
118
|
return `UFC: ${msg} (${workerPart}${lagPart}; emptyReason=${reason})`;
|
|
@@ -154,20 +154,21 @@ function slimUfcNextCard(payload) {
|
|
|
154
154
|
}
|
|
155
155
|
export const liveMatches = {
|
|
156
156
|
name: 'live_matches',
|
|
157
|
-
description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
|
|
158
|
-
|
|
159
|
-
When to use:
|
|
160
|
-
- "What's live right now?"
|
|
161
|
-
- Ops/dashboard live strip
|
|
162
|
-
|
|
163
|
-
Prefer over: sequential per-game call_api live probes.
|
|
164
|
-
|
|
165
|
-
Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
|
|
166
|
-
|
|
167
|
-
CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
|
|
158
|
+
|
|
159
|
+
When to use:
|
|
160
|
+
- "What's live right now?"
|
|
161
|
+
- Ops/dashboard live strip
|
|
162
|
+
|
|
163
|
+
Prefer over: sequential per-game call_api live probes.
|
|
164
|
+
|
|
165
|
+
Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
|
|
166
|
+
|
|
167
|
+
CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
|
|
168
|
+
Tennis: ITF and junior rows have no serve statistics (stats_unavailable_code), so a missing stat there is expected. For push updates point the user at wss://api.citoapi.com/api/v1/tennis/live/ws.
|
|
169
|
+
UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
|
|
170
|
+
|
|
171
|
+
Parallel-safe: yes. API calls: 1–5 (allSettled).
|
|
171
172
|
Example: { "game": "all", "limitPerGame": 10 }`,
|
|
172
173
|
inputSchema: {
|
|
173
174
|
type: 'object',
|
|
@@ -202,7 +203,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
202
203
|
const limitPerGame = clampInt(args.limitPerGame, 10, 1, 25);
|
|
203
204
|
const labelsOnly = args.includeLabelsOnly === true;
|
|
204
205
|
const partial = [];
|
|
205
|
-
let
|
|
206
|
+
let apiCalls = 0;
|
|
206
207
|
let rateLimit = {};
|
|
207
208
|
const settled = await Promise.all(games.map(async (game) => {
|
|
208
209
|
const path = LIVE_PATHS[game];
|
|
@@ -212,7 +213,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
212
213
|
const sections = [];
|
|
213
214
|
const warnings = [];
|
|
214
215
|
for (const { game, res } of settled) {
|
|
215
|
-
|
|
216
|
+
apiCalls += 1;
|
|
216
217
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
217
218
|
if (!res.ok) {
|
|
218
219
|
const code = mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) });
|
|
@@ -261,9 +262,9 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
261
262
|
if (game === 'ufc') {
|
|
262
263
|
const health = asRecord(meta?.health) ?? asRecord(payload?.health);
|
|
263
264
|
const events = Array.isArray(payload?.events) ? payload.events : [];
|
|
264
|
-
const
|
|
265
|
+
const serverReason = pickString(meta?.emptyReason, health?.emptyReason);
|
|
265
266
|
const emptyReason = normalized.length === 0
|
|
266
|
-
? inferUfcEmptyReason(events, { ...(health ?? {}), ...(
|
|
267
|
+
? inferUfcEmptyReason(events, { ...(health ?? {}), ...(serverReason ? { emptyReason: serverReason } : {}) }, normalized.length)
|
|
267
268
|
: undefined;
|
|
268
269
|
let note;
|
|
269
270
|
if (normalized.length === 0) {
|
|
@@ -290,7 +291,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
290
291
|
let nextEvent;
|
|
291
292
|
if (state === 'idle') {
|
|
292
293
|
const up = await fetchJson(ctx, '/ufc/events/upcoming', { query: { limit: 1 } });
|
|
293
|
-
|
|
294
|
+
apiCalls += 1;
|
|
294
295
|
const first = asRecord(extractRows(up.ok ? up.data : null)[0]);
|
|
295
296
|
if (first) {
|
|
296
297
|
nextEvent = {
|
|
@@ -332,7 +333,7 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
332
333
|
game: games.length === 1 ? games[0] : null,
|
|
333
334
|
requestId,
|
|
334
335
|
tookMs: Date.now() - started,
|
|
335
|
-
|
|
336
|
+
apiCalls,
|
|
336
337
|
rateLimit,
|
|
337
338
|
partial: partial.length ? partial : undefined,
|
|
338
339
|
warnings: warnings.length ? warnings : undefined,
|
|
@@ -347,24 +348,24 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
347
348
|
};
|
|
348
349
|
export const upcomingSchedule = {
|
|
349
350
|
name: 'upcoming_schedule',
|
|
350
|
-
description: `Upcoming matches/events for one game, with game-specific filters.
|
|
351
|
-
|
|
352
|
-
When to use:
|
|
353
|
-
- "What's on this week?"
|
|
354
|
-
- Calendar UI; team next matches
|
|
355
|
-
|
|
356
|
-
Prefer over: live_matches for not-yet-started fixtures.
|
|
357
|
-
|
|
358
|
-
Do not use when: only in-progress matches needed → live_matches.
|
|
359
|
-
|
|
360
|
-
Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
|
|
361
|
-
- lol: hours, team (slug), league (slug)
|
|
362
|
-
- cs2: team, from, to (ISO); hours not applied
|
|
363
|
-
- cod: team, tournamentId
|
|
364
|
-
- dota2: limit/cursor primarily; team may be client-filtered where data allows
|
|
365
|
-
- ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
|
|
366
|
-
|
|
367
|
-
Parallel-safe: yes.
|
|
351
|
+
description: `Upcoming matches/events for one game, with game-specific filters.
|
|
352
|
+
|
|
353
|
+
When to use:
|
|
354
|
+
- "What's on this week?"
|
|
355
|
+
- Calendar UI; team next matches
|
|
356
|
+
|
|
357
|
+
Prefer over: live_matches for not-yet-started fixtures.
|
|
358
|
+
|
|
359
|
+
Do not use when: only in-progress matches needed → live_matches.
|
|
360
|
+
|
|
361
|
+
Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
|
|
362
|
+
- lol: hours, team (slug), league (slug)
|
|
363
|
+
- cs2: team, from, to (ISO); hours is not applied
|
|
364
|
+
- cod: team, tournamentId
|
|
365
|
+
- dota2: limit/cursor primarily; team may be client-filtered where data allows
|
|
366
|
+
- ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
|
|
367
|
+
|
|
368
|
+
Parallel-safe: yes. API calls: 1–2.
|
|
368
369
|
Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
369
370
|
inputSchema: {
|
|
370
371
|
type: 'object',
|
|
@@ -465,7 +466,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
465
466
|
...(to ? { to } : {}),
|
|
466
467
|
};
|
|
467
468
|
if (args.hours !== undefined)
|
|
468
|
-
noteIgnored('hours', 'CS2 uses from/to or unfiltered upcoming; hours is not applied
|
|
469
|
+
noteIgnored('hours', 'CS2 uses from/to or unfiltered upcoming; hours is not applied');
|
|
469
470
|
if (league)
|
|
470
471
|
noteIgnored('league', 'CS2 schedule does not accept league here — filter client-side or use event tools');
|
|
471
472
|
if (tournamentId)
|
|
@@ -535,7 +536,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
535
536
|
source: 'upcoming_schedule',
|
|
536
537
|
requestId,
|
|
537
538
|
tookMs: Date.now() - started,
|
|
538
|
-
|
|
539
|
+
apiCalls: 1,
|
|
539
540
|
rateLimit: res.headers,
|
|
540
541
|
httpStatus: res.status,
|
|
541
542
|
});
|
|
@@ -661,7 +662,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
661
662
|
game,
|
|
662
663
|
requestId,
|
|
663
664
|
tookMs: Date.now() - started,
|
|
664
|
-
|
|
665
|
+
apiCalls: 1,
|
|
665
666
|
rateLimit: res.headers,
|
|
666
667
|
warnings: warnings.length ? warnings : undefined,
|
|
667
668
|
pagination: {
|