cito-mcp 0.3.0 → 0.3.2
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/dist/tools/match.js +18 -1
- package/dist/tools/meta.js +12 -2
- package/dist/tools/normalize.js +24 -12
- package/dist/tools/resolve.js +8 -1
- package/dist/tools/standings.js +1 -1
- package/package.json +1 -1
package/dist/tools/match.js
CHANGED
|
@@ -167,9 +167,26 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
167
167
|
const partial = [];
|
|
168
168
|
let upstreamCalls = 0;
|
|
169
169
|
let rateLimit = {};
|
|
170
|
-
|
|
170
|
+
let primary = await getSection(ctx, primaryPath(game, matchId));
|
|
171
171
|
upstreamCalls += 1;
|
|
172
172
|
rateLimit = primary.headers;
|
|
173
|
+
// Tennis live board hands out s365_* ids that live at /matches/live/{id}
|
|
174
|
+
// until the match finishes and lands in the archive. Without this fallback
|
|
175
|
+
// the board gives out ids that match_summary immediately 404s on.
|
|
176
|
+
if (!primary.ok && game === 'tennis' && primary.status === 404) {
|
|
177
|
+
primary = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
178
|
+
upstreamCalls += 1;
|
|
179
|
+
rateLimit = { ...rateLimit, ...primary.headers };
|
|
180
|
+
// Finished live matches archive under s365_{year}_{gid} while the live
|
|
181
|
+
// board handed out s365_{gid} — insert the year before giving up.
|
|
182
|
+
const gid = matchId.match(/^s365_(\d+)$/)?.[1];
|
|
183
|
+
if (!primary.ok && gid) {
|
|
184
|
+
const year = new Date().getUTCFullYear();
|
|
185
|
+
primary = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`s365_${year}_${gid}`)}`);
|
|
186
|
+
upstreamCalls += 1;
|
|
187
|
+
rateLimit = { ...rateLimit, ...primary.headers };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
173
190
|
if (!primary.ok) {
|
|
174
191
|
return errorEnvelope({
|
|
175
192
|
code: mapHttpToCode(primary.status, { gameNotIncluded: gameNotIncludedHint(primary.data) }),
|
package/dist/tools/meta.js
CHANGED
|
@@ -430,6 +430,7 @@ Example: { "includeGameProbes": true }`,
|
|
|
430
430
|
{ game: 'dota2', path: '/dota2' },
|
|
431
431
|
{ game: 'cod', path: '/cod' },
|
|
432
432
|
{ game: 'ufc', path: '/ufc/live/health' },
|
|
433
|
+
{ game: 'tennis', path: '/tennis/rankings/top?tour=ATP&top_n=1' },
|
|
433
434
|
];
|
|
434
435
|
const results = await Promise.all(paths.map(async ({ game, path }) => {
|
|
435
436
|
const res = await fetchJson(ctx, path);
|
|
@@ -528,6 +529,10 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
528
529
|
default: '{}',
|
|
529
530
|
description: 'Stringified JSON object of query params. Example: "{\\"page\\":1,\\"limit\\":20}".',
|
|
530
531
|
},
|
|
532
|
+
query: {
|
|
533
|
+
type: 'object',
|
|
534
|
+
description: 'Query params as a plain object (alternative to queryJson). Example: {"year":2026}.',
|
|
535
|
+
},
|
|
531
536
|
bodyJson: {
|
|
532
537
|
type: 'string',
|
|
533
538
|
description: 'Stringified JSON body for POST only (rare).',
|
|
@@ -571,6 +576,11 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
571
576
|
});
|
|
572
577
|
}
|
|
573
578
|
let query = {};
|
|
579
|
+
// Agents pass query as a plain object at least as often as the stringified
|
|
580
|
+
// queryJson form; silently dropping it produced confusing upstream 422s.
|
|
581
|
+
if (args.query && typeof args.query === 'object' && !Array.isArray(args.query)) {
|
|
582
|
+
query = { ...args.query };
|
|
583
|
+
}
|
|
574
584
|
if (typeof args.queryJson === 'string' && args.queryJson.trim()) {
|
|
575
585
|
try {
|
|
576
586
|
const parsed = JSON.parse(args.queryJson);
|
|
@@ -692,10 +702,10 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
692
702
|
// documented paths and no curated tools, so it is exactly what call_api
|
|
693
703
|
// callers come looking for.
|
|
694
704
|
const gameRaw = typeof args.game === 'string' ? args.game.toLowerCase().trim() : '';
|
|
695
|
-
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite)$/)) {
|
|
705
|
+
if (gameRaw && !gameRaw.match(/^(lol|cs2|dota2|cod|ufc|fortnite|tennis)$/)) {
|
|
696
706
|
return errorEnvelope({
|
|
697
707
|
code: 'UNSUPPORTED_GAME',
|
|
698
|
-
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite`,
|
|
708
|
+
message: `unsupported game "${gameRaw}"; use lol|cs2|dota2|cod|ufc|fortnite|tennis`,
|
|
699
709
|
game: null,
|
|
700
710
|
source: 'list_routes',
|
|
701
711
|
requestId,
|
package/dist/tools/normalize.js
CHANGED
|
@@ -171,21 +171,33 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
171
171
|
if (s != null)
|
|
172
172
|
team2 = { ...team2, score: s };
|
|
173
173
|
}
|
|
174
|
-
// Tennis: live
|
|
175
|
-
//
|
|
174
|
+
// Tennis: the live board nests player1/player2 objects ({id,name,sets_won});
|
|
175
|
+
// raw live rows carry player1_name/player2_name; archive rows only carry
|
|
176
|
+
// winner_id/loser_id (ids, no names). Map all three onto sides so labels are
|
|
177
|
+
// never "? vs ?" and live scores surface as sets won.
|
|
176
178
|
if (game === 'tennis' && !team1 && !team2) {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
+
const p1obj = asRecord(r.player1);
|
|
180
|
+
const p2obj = asRecord(r.player2);
|
|
181
|
+
const p1 = pickString(p1obj?.name, r.player1_name);
|
|
182
|
+
const p2 = pickString(p2obj?.name, r.player2_name);
|
|
179
183
|
if (p1 || p2) {
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
const score1 = typeof p1obj?.sets_won === 'number' ? p1obj.sets_won : undefined;
|
|
185
|
+
const score2 = typeof p2obj?.sets_won === 'number' ? p2obj.sets_won : undefined;
|
|
186
|
+
team1 = sideFrom(p1, pickString(p1obj?.id, r.player1_id), undefined, score1);
|
|
187
|
+
team2 = sideFrom(p2, pickString(p2obj?.id, r.player2_id), undefined, score2);
|
|
182
188
|
}
|
|
183
189
|
else {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
// Archive detail nests winner/loser objects with real names; list rows
|
|
191
|
+
// may only carry winner_id/loser_id (ids double as last-resort labels).
|
|
192
|
+
const wObj = asRecord(r.winner);
|
|
193
|
+
const lObj = asRecord(r.loser);
|
|
194
|
+
const wName = pickString(wObj?.name, r.winner_name);
|
|
195
|
+
const lName = pickString(lObj?.name, r.loser_name);
|
|
196
|
+
const wId = pickString(wObj?.id, r.winner_id);
|
|
197
|
+
const lId = pickString(lObj?.id, r.loser_id);
|
|
198
|
+
if (wName || lName || wId || lId) {
|
|
199
|
+
team1 = sideFrom(wName ?? wId, wId, undefined, undefined);
|
|
200
|
+
team2 = sideFrom(lName ?? lId, lId, undefined, undefined);
|
|
189
201
|
}
|
|
190
202
|
}
|
|
191
203
|
}
|
|
@@ -286,7 +298,7 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
286
298
|
}
|
|
287
299
|
}
|
|
288
300
|
const ev = asRecord(r.event);
|
|
289
|
-
const eventName = pickString(ev?.name, ev?.title, r.eventName, typeof r.event === 'string' ? r.event : undefined, asRecord(r.tournament)?.name, asRecord(r.tournament)?.title, r.tournamentName);
|
|
301
|
+
const eventName = pickString(ev?.name, ev?.title, r.eventName, typeof r.event === 'string' ? r.event : undefined, asRecord(r.tournament)?.name, asRecord(r.tournament)?.title, r.tournamentName, r.tournament_name);
|
|
290
302
|
const eventId = pickString(ev?.id, r.eventId, asRecord(r.tournament)?.id, r.tournamentId);
|
|
291
303
|
const eventSlug = pickString(ev?.slug, r.eventSlug, asRecord(r.tournament)?.slug);
|
|
292
304
|
const leagueName = pickString(asRecord(r.league)?.name, r.leagueName, r.league, r.leagueSlug);
|
package/dist/tools/resolve.js
CHANGED
|
@@ -10,7 +10,14 @@ function pushCandidates(out, game, type, rows, q, limit) {
|
|
|
10
10
|
const ref = entityRef(row, type, game);
|
|
11
11
|
const r = asRecord(row) ?? {};
|
|
12
12
|
const nickname = pickString(r.nickname, asRecord(r.profile)?.nickname, ref.meta?.nickname);
|
|
13
|
-
|
|
13
|
+
let score = rankScore(q, ref.name, ref.id, ref.slug, nickname);
|
|
14
|
+
// Ranked-player tiebreaker: a surname query like "Alcaraz" fuzzy-ties the
|
|
15
|
+
// world #2 with a 1991 journeyman; the row's current_rank breaks the tie
|
|
16
|
+
// toward whoever is actually active/ranked (tennis search supplies it).
|
|
17
|
+
const currentRank = Number(r.current_rank);
|
|
18
|
+
if (score > 0 && Number.isFinite(currentRank) && currentRank > 0) {
|
|
19
|
+
score += currentRank <= 100 ? 8 : currentRank <= 1000 ? 4 : 2;
|
|
20
|
+
}
|
|
14
21
|
// Prefer positive fuzzy hits; keep weak API hits at floor 1 so real search results are not wiped.
|
|
15
22
|
if (q && score <= 0) {
|
|
16
23
|
// still allow through at floor so dedicated search endpoints aren't empty on odd nicknames
|
package/dist/tools/standings.js
CHANGED
|
@@ -62,7 +62,7 @@ export function normalizeStandingRow(row, index) {
|
|
|
62
62
|
: null),
|
|
63
63
|
championStatus: resolvedChampionStatus,
|
|
64
64
|
teamOrFighter: {
|
|
65
|
-
id: pickString(asRecord(entity)?.id, r.teamId, r.fighterId, r.orgId, r.id),
|
|
65
|
+
id: pickString(asRecord(entity)?.id, r.teamId, r.fighterId, r.orgId, r.id, r.player_id),
|
|
66
66
|
slug: pickString(asRecord(entity)?.slug, r.orgSlug, r.teamSlug, r.slug, r.fighterSlug),
|
|
67
67
|
name,
|
|
68
68
|
},
|
package/package.json
CHANGED