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/team.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* team_profile + head_to_head
|
|
3
|
-
*/
|
|
4
1
|
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
2
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
3
|
import { normalizeMatch } from './normalize.js';
|
|
@@ -9,9 +6,6 @@ function teamIdentity(game, raw, idHint, slugHint) {
|
|
|
9
6
|
const r = asRecord(raw) ?? {};
|
|
10
7
|
const nested = asRecord(r.data) ?? r;
|
|
11
8
|
const slug = pickString(nested.slug, nested.orgSlug, slugHint);
|
|
12
|
-
// When the caller asks for an alias the API resolves it, and the resolved
|
|
13
|
-
// slug is the truth. Falling back to the requested value for `id` produced
|
|
14
|
-
// id:"t1a" beside slug:"t1-challengers", a row that disagreed with itself.
|
|
15
9
|
const resolved = pickString(nested.slug, nested.orgSlug);
|
|
16
10
|
const id = pickString(nested.id, nested.teamId) ??
|
|
17
11
|
resolved ??
|
|
@@ -24,16 +18,11 @@ function teamIdentity(game, raw, idHint, slugHint) {
|
|
|
24
18
|
name,
|
|
25
19
|
game,
|
|
26
20
|
region: pickString(nested.region, nested.country) ?? null,
|
|
27
|
-
// CS2 publishes countryName/countryCode and worldRanking on the team
|
|
28
|
-
// resource and none of it reached this card: a team page rendered with no
|
|
29
|
-
// flag and no ladder position while the API had both. Spirit came back as
|
|
30
|
-
// {id, slug, name, game, region} with world #1 nowhere in sight.
|
|
31
21
|
country: pickString(nested.countryName, nested.country, nested.countryCode) ?? null,
|
|
32
22
|
countryCode: pickString(nested.countryCode) ?? null,
|
|
33
23
|
worldRanking: numberOrNull(nested.worldRanking ?? nested.ranking ?? nested.rank),
|
|
34
24
|
};
|
|
35
25
|
}
|
|
36
|
-
/** A finite number, or null. Ranks arrive as number or numeric string. */
|
|
37
26
|
function numberOrNull(value) {
|
|
38
27
|
if (value === null || value === undefined || value === '')
|
|
39
28
|
return null;
|
|
@@ -60,10 +49,6 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
60
49
|
additionalProperties: false,
|
|
61
50
|
required: ['game'],
|
|
62
51
|
properties: {
|
|
63
|
-
// Tennis is deliberately absent: singles has players, not team rosters,
|
|
64
|
-
// and advertising it here only to throw NOT_IMPLEMENTED on every call is
|
|
65
|
-
// a trap — the schema told the caller the request was valid. player_profile
|
|
66
|
-
// and standings{game:"tennis"} cover the same ground honestly.
|
|
67
52
|
game: {
|
|
68
53
|
type: 'string',
|
|
69
54
|
enum: PRIMARY_GAMES.filter((g) => g !== 'tennis'),
|
|
@@ -166,9 +151,7 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
166
151
|
teamRaw = res.data;
|
|
167
152
|
}
|
|
168
153
|
else if (game === 'cs2') {
|
|
169
|
-
|
|
170
|
-
// name-searching, which could silently fall back to rows[0] of an unrelated search.
|
|
171
|
-
const directId = /^(cs2|hltv)-team-\d+$/i.test(idOrSlug);
|
|
154
|
+
const directId = /^[a-z0-9]{3,4}-team-\d+$/i.test(idOrSlug);
|
|
172
155
|
const res = directId
|
|
173
156
|
? await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(idOrSlug)}`)
|
|
174
157
|
: await fetchJson(ctx, '/cs2/teams', { query: { search: idOrSlug, limit: 5 } });
|
|
@@ -314,11 +297,6 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
314
297
|
}
|
|
315
298
|
})());
|
|
316
299
|
tasks.push((async () => {
|
|
317
|
-
// The matches index, not /lol/schedule. Schedule returned the team's
|
|
318
|
-
// oldest fixtures: a T1 profile showed MSI 2023 as "recent" while the
|
|
319
|
-
// index had the 2026 LCK split, newest first. Sort here anyway rather
|
|
320
|
-
// than trusting upstream order, so the contract holds if either
|
|
321
|
-
// endpoint changes.
|
|
322
300
|
const slug = team.slug || idOrSlug;
|
|
323
301
|
const res = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(slug)}/matches`, {
|
|
324
302
|
query: { limit: String(recentLimit) },
|
|
@@ -330,10 +308,6 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
330
308
|
const t = Date.parse(String(row?.startTime ?? ''));
|
|
331
309
|
return Number.isFinite(t) ? t : -Infinity;
|
|
332
310
|
};
|
|
333
|
-
// RECENT means finished AND in the past. The index also carries
|
|
334
|
-
// scheduled fixtures, and this path forced every row to
|
|
335
|
-
// "completed", so Gen.G and HLE both listed their 2026-09-05
|
|
336
|
-
// playoff match as a completed result days before it was played.
|
|
337
311
|
const nowMs = Date.now();
|
|
338
312
|
const isFinished = (row) => {
|
|
339
313
|
const r = asRecord(row) ?? {};
|
|
@@ -343,7 +317,6 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
343
317
|
const state = String(r.state ?? r.status ?? '').toLowerCase();
|
|
344
318
|
if (/unstarted|scheduled|upcoming|not_?started|in_?progress|live/.test(state))
|
|
345
319
|
return false;
|
|
346
|
-
// No usable state: fall back to the clock plus a decided result.
|
|
347
320
|
if (!state)
|
|
348
321
|
return Number.isFinite(t) && t <= nowMs;
|
|
349
322
|
return /complete|finish|final|ended/.test(state);
|
|
@@ -388,9 +361,6 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
388
361
|
const res = await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(tid)}/trends`);
|
|
389
362
|
upstreamCalls += 1;
|
|
390
363
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
391
|
-
// res.data is the whole upstream body, so this nested
|
|
392
|
-
// {success, data:{...}} inside our own envelope. Every other field
|
|
393
|
-
// here is normalized; this one leaked the REST wrapper to the agent.
|
|
394
364
|
if (res.ok)
|
|
395
365
|
form = unwrapPayload(res.data) ?? res.data;
|
|
396
366
|
else {
|
|
@@ -631,13 +601,9 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
631
601
|
let upstreamCalls = 0;
|
|
632
602
|
let rateLimit = {};
|
|
633
603
|
let rows = [];
|
|
634
|
-
// Names used for client-side side matching; replaced with resolved team names
|
|
635
|
-
// when the id-based REST H2H succeeds (short inputs like "navi" never substring-match "Natus Vincere").
|
|
636
604
|
let matchA = sideA;
|
|
637
605
|
let matchB = sideB;
|
|
638
606
|
if (game === 'cs2') {
|
|
639
|
-
// Prefer the purpose-built id-based H2H endpoint; /cs2/matches?team= name-substring
|
|
640
|
-
// filtering misses short names ("navi" vs stored "Natus Vincere").
|
|
641
607
|
const resolveCs2Side = async (side) => {
|
|
642
608
|
const res = await fetchJson(ctx, '/cs2/teams', { query: { search: side, limit: 5 } });
|
|
643
609
|
upstreamCalls += 1;
|
|
@@ -662,8 +628,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
662
628
|
upstreamCalls += 1;
|
|
663
629
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
664
630
|
if (res.ok) {
|
|
665
|
-
// Rows are side-A-perspective summaries (summarizeMatchForTeam); reshape into
|
|
666
|
-
// flat match rows so the shared normalize/score pipeline applies unchanged.
|
|
667
631
|
rows = extractRows(res.data).map((row) => {
|
|
668
632
|
const r = asRecord(row) ?? {};
|
|
669
633
|
return {
|
|
@@ -726,11 +690,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
726
690
|
rows = extractRows(res.data);
|
|
727
691
|
}
|
|
728
692
|
else if (game === 'lol') {
|
|
729
|
-
// First-class H2H. The old path pulled sideA's /lol/schedule and filtered
|
|
730
|
-
// client-side, which missed meetings and returned the oldest first, so a
|
|
731
|
-
// T1 vs HLE rivalry showed 2023 games and called the earliest one the
|
|
732
|
-
// "last meeting". This endpoint states the record outright (22 meetings,
|
|
733
|
-
// 8-13) and lists them newest first.
|
|
734
693
|
const h2hRes = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(sideA)}/h2h/${encodeURIComponent(sideB)}`, { query: { limit: String(limit) } });
|
|
735
694
|
upstreamCalls += 1;
|
|
736
695
|
rateLimit = { ...rateLimit, ...h2hRes.headers };
|
|
@@ -753,9 +712,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
753
712
|
matchId: pickString(r.matchId) ?? null,
|
|
754
713
|
startTime: pickString(r.date) ?? null,
|
|
755
714
|
tournamentName: pickString(r.tournamentName) ?? null,
|
|
756
|
-
// The feed gives scores in the match's own team order, which it
|
|
757
|
-
// does not label, so they are reported as-is rather than being
|
|
758
|
-
// assigned to a side we would be guessing at.
|
|
759
715
|
team1Score: typeof r.team1Score === 'number' ? r.team1Score : null,
|
|
760
716
|
team2Score: typeof r.team2Score === 'number' ? r.team2Score : null,
|
|
761
717
|
winnerSlug: pickString(r.winner) ?? null,
|
|
@@ -829,7 +785,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
829
785
|
rows = extractRows(res.data);
|
|
830
786
|
}
|
|
831
787
|
else if (game === 'ufc') {
|
|
832
|
-
// Use fighter fight history (not global /bouts page 1, which rarely contains both fighters).
|
|
833
788
|
const [aRes, bRes, histA, histB] = await Promise.all([
|
|
834
789
|
fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(sideA)}`),
|
|
835
790
|
fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(sideB)}`),
|
|
@@ -854,15 +809,11 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
854
809
|
});
|
|
855
810
|
}
|
|
856
811
|
const toBouts = (payload) => extractRows(payload).map((row) => {
|
|
857
|
-
// History rows carry the two corners as fighterSlug/opponent, not on
|
|
858
|
-
// the nested bout. Dropping them here is why Jones vs Cormier reported
|
|
859
|
-
// zero meetings while the profile listed both fights.
|
|
860
812
|
const r = asRecord(row) ?? {};
|
|
861
813
|
return { ...(asRecord(r.bout) ?? {}), ...r };
|
|
862
814
|
});
|
|
863
815
|
const fromA = histA.ok ? toBouts(histA.data) : [];
|
|
864
816
|
const fromB = histB.ok ? toBouts(histB.data) : [];
|
|
865
|
-
// Union by bout id
|
|
866
817
|
const byId = new Map();
|
|
867
818
|
for (const bout of [...fromA, ...fromB]) {
|
|
868
819
|
const m = normalizeMatch('ufc', bout);
|
|
@@ -876,7 +827,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
876
827
|
}
|
|
877
828
|
}
|
|
878
829
|
if (game === 'tennis') {
|
|
879
|
-
// Tennis has a first-class H2H endpoint; resolve names to player ids first when needed.
|
|
880
830
|
const resolveTennisSide = async (side) => {
|
|
881
831
|
if (/^(atp|wta)_\d+$/i.test(side))
|
|
882
832
|
return { id: side, name: side };
|
|
@@ -928,14 +878,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
928
878
|
}
|
|
929
879
|
const envelope = asRecord(res.data) ?? {};
|
|
930
880
|
const h2h = asRecord(envelope.data) ?? envelope;
|
|
931
|
-
// The id-shaped shortcut above never resolves a name — sideA:"atp_207989"
|
|
932
|
-
// came in already looking like an id, so resolveTennisSide handed back
|
|
933
|
-
// {id: side, name: side} without ever asking who that id belongs to. The
|
|
934
|
-
// /tennis/h2h payload already states both names one level down (its own
|
|
935
|
-
// player1/player2, keyed by the same player1_id/player2_id this request
|
|
936
|
-
// just sent), so read from there rather than trusting the resolver's
|
|
937
|
-
// name at all: it is authoritative and always available once the H2H
|
|
938
|
-
// call itself succeeded.
|
|
939
881
|
const h2hP1 = asRecord(h2h.player1);
|
|
940
882
|
const h2hP2 = asRecord(h2h.player2);
|
|
941
883
|
const sideAName = pickString(h2hP1?.name) ?? tA.name;
|
|
@@ -957,15 +899,13 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
957
899
|
}
|
|
958
900
|
const meetings = rows
|
|
959
901
|
.map((row) => normalizeMatch(game, row))
|
|
960
|
-
// Past fights only. The booked bout between the two sides is in the
|
|
961
|
-
// same list and is not a meeting yet.
|
|
962
902
|
.filter((m) => m.status === 'completed')
|
|
963
903
|
.filter((m) => sidesMatch(m, matchA, matchB))
|
|
964
904
|
.filter((m) => {
|
|
965
905
|
if (!fromIso && !toIso)
|
|
966
906
|
return true;
|
|
967
907
|
if (!m.startTime)
|
|
968
|
-
return true;
|
|
908
|
+
return true;
|
|
969
909
|
const t = Date.parse(m.startTime);
|
|
970
910
|
if (Number.isNaN(t))
|
|
971
911
|
return true;
|
|
@@ -975,10 +915,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
975
915
|
return false;
|
|
976
916
|
return true;
|
|
977
917
|
})
|
|
978
|
-
// Newest first. Nothing sorted this before, so `lastMeeting` below was
|
|
979
|
-
// whichever row the upstream happened to return first, which on an
|
|
980
|
-
// ascending feed is the OLDEST meeting. It also means a limit cut off the
|
|
981
|
-
// recent games rather than the ancient ones.
|
|
982
918
|
.sort((a, b) => {
|
|
983
919
|
const ta = a.startTime ? Date.parse(a.startTime) : NaN;
|
|
984
920
|
const tb = b.startTime ? Date.parse(b.startTime) : NaN;
|
|
@@ -1022,21 +958,6 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
1022
958
|
});
|
|
1023
959
|
},
|
|
1024
960
|
};
|
|
1025
|
-
/**
|
|
1026
|
-
* h2h_matrix (TENNIS-24, cycle 60).
|
|
1027
|
-
*
|
|
1028
|
-
* Tennis-only: multi-player head-to-head comparison grid from the first-class
|
|
1029
|
-
* GET /tennis/h2h/matrix route (2-16 player ids, comma-separated). Upstream
|
|
1030
|
-
* shape (verified 2026-09-09): { players: [{ id, name, ioc, wins }], matrix:
|
|
1031
|
-
* { [rowId]: { [colId]: "W-L" | "-" } } }. Cell "7-6" at [A][B] means A won 7
|
|
1032
|
-
* and B won 6 of their meetings; "-" marks the self diagonal. Unknown ids get
|
|
1033
|
-
* honest 0-0 cells and no players entry (no 404); fewer than 2 or more than 16
|
|
1034
|
-
* ids 422, which surfaces as VALIDATION here.
|
|
1035
|
-
*
|
|
1036
|
-
* Names are resolved to atp_/wta_ ids via /tennis/players/search first (same
|
|
1037
|
-
* id-shaped shortcut as the head_to_head tennis branch); duplicates collapse
|
|
1038
|
-
* to one grid row.
|
|
1039
|
-
*/
|
|
1040
961
|
export const h2hMatrix = {
|
|
1041
962
|
name: 'h2h_matrix',
|
|
1042
963
|
description: `Multi-player tennis head-to-head grid: every pair's series record in one comparison matrix.
|
|
@@ -1145,8 +1066,6 @@ Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
|
|
|
1145
1066
|
],
|
|
1146
1067
|
});
|
|
1147
1068
|
}
|
|
1148
|
-
// Dedupe resolved ids preserving order: REST dict keys would collapse
|
|
1149
|
-
// anyway, but players_info would list the duplicate twice.
|
|
1150
1069
|
const ids = [...new Set(resolved.map((r) => r.id))];
|
|
1151
1070
|
if (ids.length < 2) {
|
|
1152
1071
|
return errorEnvelope({
|
|
@@ -1,21 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tournaments — tennis tournament discovery (TENNIS-25).
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS EXISTS
|
|
5
|
-
* event_card already renders a tournament Profile/Draw, but only once you know a
|
|
6
|
-
* tournament id. There was no way to ASK for tournaments — so "list the 2026
|
|
7
|
-
* WTA 1000 events" or "what ATP 250s are on clay" was impossible without
|
|
8
|
-
* call_api, even though /tennis/tournaments supports exactly those filters
|
|
9
|
-
* (year, tour, level, surface, country) and the level filter was recently made
|
|
10
|
-
* to accept names like "WTA 1000" rather than only raw tier codes.
|
|
11
|
-
*
|
|
12
|
-
* This tool is the discovery half; event_card stays the detail half.
|
|
13
|
-
*/
|
|
14
1
|
import { asRecord, clampInt, fetchJson, gameNotIncludedHint, pickString, } from '../client.js';
|
|
15
2
|
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
16
3
|
import { gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
17
4
|
const SURFACES = ['Hard', 'Clay', 'Grass', 'Carpet'];
|
|
18
|
-
/** Human level names the API accepts on ?level=, plus the raw tier codes. */
|
|
19
5
|
const LEVEL_NAMES = [
|
|
20
6
|
'Grand Slam',
|
|
21
7
|
'Masters 1000',
|
|
@@ -42,9 +28,6 @@ function normalizeTournament(row) {
|
|
|
42
28
|
name: pickString(r.name) ?? null,
|
|
43
29
|
tour: pickString(r.tour) ?? null,
|
|
44
30
|
year: num(r.year),
|
|
45
|
-
// `tier` is the human level the API derives from the tier code; `level` is
|
|
46
|
-
// the raw code. Both are surfaced because callers filter on the name but
|
|
47
|
-
// cross-reference by the code.
|
|
48
31
|
level: pickString(r.tier) ?? pickString(r.level) ?? null,
|
|
49
32
|
levelCode: pickString(r.tier_code, r.level) ?? null,
|
|
50
33
|
category: pickString(r.category) ?? null,
|
|
@@ -174,25 +157,6 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
174
157
|
});
|
|
175
158
|
}
|
|
176
159
|
}
|
|
177
|
-
/**
|
|
178
|
-
* A level name that starts with a tour token constrains the tour, and the
|
|
179
|
-
* upstream filter does not know that.
|
|
180
|
-
*
|
|
181
|
-
* The API's reverse level map is not tour-aware: `_TIER_WTA` carries BOTH
|
|
182
|
-
* "PM" and "M" as "WTA 1000" (M is the fallback label for a WTA row stored
|
|
183
|
-
* with the ATP code), so `?level=WTA 1000` resolves to `tier_code IN
|
|
184
|
-
* ('PM','M')` and returns every ATP Masters 1000 event alongside the WTA
|
|
185
|
-
* ones. Verified live: asking for "WTA 1000" returned Rome Masters, Madrid
|
|
186
|
-
* Masters, Monte Carlo, Miami Masters and Indian Wells Masters (all ATP).
|
|
187
|
-
* The same widening hits "WTA 125" (code C, shared with ATP Challengers) and
|
|
188
|
-
* "ATP 500"/"ATP 250" (code A, shared with WTA tour-level rows).
|
|
189
|
-
*
|
|
190
|
-
* The tool therefore asserts the invariant the label states: it sends the
|
|
191
|
-
* tour, and it verifies the rows that come back. Rows that contradict the
|
|
192
|
-
* requested level are dropped and counted rather than passed through as
|
|
193
|
-
* "close enough" — a filter that returns other-tour events is worse than a
|
|
194
|
-
* filter that returns nothing, because the caller has no way to notice.
|
|
195
|
-
*/
|
|
196
160
|
const levelTourPrefix = (() => {
|
|
197
161
|
const m = /^(ATP|WTA)\b/i.exec(levelRaw);
|
|
198
162
|
return m ? m[1].toUpperCase() : null;
|
|
@@ -246,7 +210,6 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
246
210
|
const root = asRecord(res.data) ?? {};
|
|
247
211
|
const body = asRecord(root.data) ?? root;
|
|
248
212
|
const rawItems = (Array.isArray(body.items) ? body.items : []).map(normalizeTournament);
|
|
249
|
-
// Verify the invariant the level label states. See levelTourPrefix above.
|
|
250
213
|
const contradictions = levelTourPrefix
|
|
251
214
|
? rawItems.filter((t) => t.tour && t.tour.toUpperCase() !== levelTourPrefix)
|
|
252
215
|
: [];
|
package/dist/tools/types.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ENVELOPE_OUTPUT_SCHEMA, toMcpResult } from '../envelope.js';
|
|
2
|
-
/** All curated tools fetch Cito data. None delete, post, or mutate user state. */
|
|
3
2
|
export const READ_TOOL_ANNOTATIONS = {
|
|
4
3
|
readOnlyHint: true,
|
|
5
4
|
destructiveHint: false,
|
|
@@ -79,7 +78,6 @@ export function stringSchema(description, example) {
|
|
|
79
78
|
schema.examples = [example];
|
|
80
79
|
return schema;
|
|
81
80
|
}
|
|
82
|
-
/** Levenshtein distance, capped for short arg names. */
|
|
83
81
|
function editDistance(a, b) {
|
|
84
82
|
const m = a.length;
|
|
85
83
|
const n = b.length;
|
|
@@ -93,7 +91,6 @@ function editDistance(a, b) {
|
|
|
93
91
|
}
|
|
94
92
|
return prev[n];
|
|
95
93
|
}
|
|
96
|
-
/** Closest known parameter to a mistyped one, if it is plausibly a typo. */
|
|
97
94
|
function suggestParam(unknown, known) {
|
|
98
95
|
const u = unknown.toLowerCase();
|
|
99
96
|
let best = null;
|
|
@@ -104,24 +101,12 @@ function suggestParam(unknown, known) {
|
|
|
104
101
|
}
|
|
105
102
|
if (!best)
|
|
106
103
|
return null;
|
|
107
|
-
// Accept near-misses, plus prefix relationships like query -> q.
|
|
108
104
|
const threshold = Math.max(2, Math.floor(Math.max(u.length, best.name.length) / 3));
|
|
109
105
|
if (best.d <= threshold)
|
|
110
106
|
return best.name;
|
|
111
107
|
const prefixHit = known.find((k) => u.startsWith(k.toLowerCase()) || k.toLowerCase().startsWith(u));
|
|
112
108
|
return prefixHit ?? null;
|
|
113
109
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Args the tool does not declare, when its schema says additionalProperties:false.
|
|
116
|
-
*
|
|
117
|
-
* Every tool already declared that, but nothing enforced it: the MCP SDK does
|
|
118
|
-
* not validate arguments against inputSchema, so a plausible-but-wrong name was
|
|
119
|
-
* dropped on the floor and the tool ran with a default. search_entities
|
|
120
|
-
* { game:"ufc", query:"Jon Jones" } — `q` is the real parameter — returned
|
|
121
|
-
* ok:true with an unrelated roster. Silently wrong beats loudly wrong for a
|
|
122
|
-
* human skimming output, but for an agent it is far worse: it has no way to
|
|
123
|
-
* tell a real answer from a discarded question.
|
|
124
|
-
*/
|
|
125
110
|
export function unknownArgKeys(def, args) {
|
|
126
111
|
const schema = def.inputSchema ?? {};
|
|
127
112
|
if (schema.additionalProperties !== false)
|
|
@@ -132,7 +117,6 @@ export function unknownArgKeys(def, args) {
|
|
|
132
117
|
const known = Object.keys(props);
|
|
133
118
|
return Object.keys(args ?? {}).filter((k) => !known.includes(k));
|
|
134
119
|
}
|
|
135
|
-
/** Normalize MCP handler return to MCP content result. */
|
|
136
120
|
export async function runTool(def, args, ctx) {
|
|
137
121
|
try {
|
|
138
122
|
const unknown = unknownArgKeys(def, args ?? {});
|
|
@@ -144,7 +128,6 @@ export async function runTool(def, args, ctx) {
|
|
|
144
128
|
return toResult(errorEnvelope({
|
|
145
129
|
code: 'VALIDATION',
|
|
146
130
|
message: `${def.name}: unknown argument${unknown.length > 1 ? 's' : ''} ` +
|
|
147
|
-
// Avoid "?." when the last hint is a did-you-mean question.
|
|
148
131
|
`${hints.join('; ')}${hints[hints.length - 1].endsWith('?') ? '' : '.'} ` +
|
|
149
132
|
`Accepted: ${known.join(', ')}.`,
|
|
150
133
|
game: typeof args?.game === 'string' ? args.game : null,
|
package/dist/version.js
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
/**
|
|
3
|
-
* Single source of truth for the server version.
|
|
4
|
-
*
|
|
5
|
-
* This used to be hardcoded in three places — package.json, PACKAGE_VERSION in
|
|
6
|
-
* index.ts, and CATALOG_VERSION in tools/meta.ts — which had already drifted:
|
|
7
|
-
* a test asserted 0.2.4 while the package shipped 0.2.5. Agents read the version
|
|
8
|
-
* out of list_capabilities, so a stale value there is a support problem, not a
|
|
9
|
-
* cosmetic one. Read it from the manifest instead, so `npm version` is the only
|
|
10
|
-
* edit a release needs.
|
|
11
|
-
*
|
|
12
|
-
* createRequire rather than a JSON import: import assertions are still awkward
|
|
13
|
-
* across the Node versions this package supports (>=20), and this resolves the
|
|
14
|
-
* same from src/ under tsx and from dist/ once built, because package.json is
|
|
15
|
-
* always published alongside dist.
|
|
16
|
-
*/
|
|
17
2
|
const require = createRequire(import.meta.url);
|
|
18
3
|
function readVersion() {
|
|
19
4
|
try {
|
|
@@ -21,8 +6,6 @@ function readVersion() {
|
|
|
21
6
|
return typeof pkg.version === 'string' && pkg.version ? pkg.version : '0.0.0';
|
|
22
7
|
}
|
|
23
8
|
catch {
|
|
24
|
-
// Never let a packaging quirk crash the server on boot; a wrong-but-present
|
|
25
|
-
// version is far better than a failed start.
|
|
26
9
|
return '0.0.0';
|
|
27
10
|
}
|
|
28
11
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cito-mcp",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Standalone MCP server for the Cito esports and sports API —
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"description": "Standalone MCP server for the Cito esports and sports API — curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"homepage": "https://cito.gg",
|
|
7
|
+
"//REPOSITORY": "Intentionally NOT set. cito-api-scraper and cito.gg are PRIVATE repos and must stay that way, so a repository URL pointing at them would render as a broken 404 link on the public npm page and would not satisfy MCP Registry npm ownership validation. Set this only if a PUBLIC repo for this package is ever created (e.g. achillesscriptsvip/cito-mcp with this directory as root) — then use `git+https://github.com/OWNER/REPO.git` and mirror it in the `mcpName`/server.json `name` pair. Do not point it at the private monorepo.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mcp",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"esports",
|
|
12
|
+
"cs2",
|
|
13
|
+
"counter-strike",
|
|
14
|
+
"counter-strike-2",
|
|
15
|
+
"lol",
|
|
16
|
+
"league-of-legends",
|
|
17
|
+
"dota2",
|
|
18
|
+
"cod",
|
|
19
|
+
"ufc",
|
|
20
|
+
"mma",
|
|
21
|
+
"tennis",
|
|
22
|
+
"live-scores",
|
|
23
|
+
"sports-data",
|
|
24
|
+
"esports-api"
|
|
25
|
+
],
|
|
6
26
|
"bin": {
|
|
7
27
|
"cito-mcp": "dist/index.js"
|
|
8
28
|
},
|