cito-mcp 0.5.7 → 0.6.0
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 +25 -21
- package/dist/envelope.js +9 -9
- package/dist/index.js +2 -2
- package/dist/instructions.js +10 -4
- package/dist/scrub.js +42 -4
- package/dist/tools/combat.js +6 -6
- package/dist/tools/index.js +2 -0
- package/dist/tools/insight.js +12 -10
- package/dist/tools/leaderboard.js +2 -2
- package/dist/tools/live.js +8 -7
- package/dist/tools/match.js +65 -22
- package/dist/tools/meta.js +46 -11
- package/dist/tools/normalize.js +3 -2
- package/dist/tools/player.js +44 -5
- package/dist/tools/rankings.js +63 -12
- package/dist/tools/resolve.js +11 -11
- package/dist/tools/schedule.js +7 -6
- package/dist/tools/standings.js +43 -23
- package/dist/tools/team.js +8 -6
- package/dist/tools/tournaments.js +5 -7
- package/dist/tools/types.js +1 -1
- package/dist/tools/webhooks.js +377 -0
- package/package.json +1 -1
package/dist/tools/resolve.js
CHANGED
|
@@ -31,7 +31,7 @@ function pushCandidates(out, game, type, rows, q, limit) {
|
|
|
31
31
|
let matchedBy;
|
|
32
32
|
if (q && score <= 0 && index === 0) {
|
|
33
33
|
score = 95;
|
|
34
|
-
matchedBy = '
|
|
34
|
+
matchedBy = 'top-hit';
|
|
35
35
|
}
|
|
36
36
|
const secondary = {};
|
|
37
37
|
for (const key of ['lolPlayerId', 'codPlayerId', 'matchId', 'boutId', 'tournamentId', 'eventId']) {
|
|
@@ -366,7 +366,7 @@ async function searchGame(ctx, game, q, type, limit) {
|
|
|
366
366
|
candidates,
|
|
367
367
|
calls,
|
|
368
368
|
error: partialFromRejection(`game:${game}`, {
|
|
369
|
-
code: '
|
|
369
|
+
code: 'SERVICE_ERROR',
|
|
370
370
|
message: e.message,
|
|
371
371
|
}),
|
|
372
372
|
};
|
|
@@ -389,7 +389,7 @@ Do not use when: you already have a stable id/slug from a prior tool.
|
|
|
389
389
|
|
|
390
390
|
Empty/ambiguous results still return ok:true with best=null or needsDisambiguation=true — pick from candidates or refine q/game/type. Does not emit AMBIGUOUS_ENTITY as a hard error.
|
|
391
391
|
|
|
392
|
-
Parallel-safe: yes.
|
|
392
|
+
Parallel-safe: yes. API calls: 1–5.
|
|
393
393
|
Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
394
394
|
inputSchema: {
|
|
395
395
|
type: 'object',
|
|
@@ -507,7 +507,7 @@ Do not use when: fetching a known entity profile — use team_profile or player_
|
|
|
507
507
|
|
|
508
508
|
UFC: with q set, results are ranked (exact name > multi-token match > nickname). "Jon Jones" should return jon-jones first — never the generic P4P list.
|
|
509
509
|
|
|
510
|
-
Parallel-safe: yes.
|
|
510
|
+
Parallel-safe: yes. API calls: 1–3.
|
|
511
511
|
Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
512
512
|
inputSchema: {
|
|
513
513
|
type: 'object',
|
|
@@ -792,11 +792,11 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
792
792
|
meta: {
|
|
793
793
|
...(it.meta ?? {}),
|
|
794
794
|
score: raw,
|
|
795
|
-
...(isProtected && raw < RELEVANCE_FLOOR ? { matchedBy: '
|
|
795
|
+
...(isProtected && raw < RELEVANCE_FLOOR ? { matchedBy: 'top-hit' } : {}),
|
|
796
796
|
},
|
|
797
797
|
};
|
|
798
798
|
});
|
|
799
|
-
const keep = (it) => it.meta?.score >= RELEVANCE_FLOOR || it.meta?.matchedBy === '
|
|
799
|
+
const keep = (it) => it.meta?.score >= RELEVANCE_FLOOR || it.meta?.matchedBy === 'top-hit';
|
|
800
800
|
droppedBelowFloor = items
|
|
801
801
|
.filter((it) => !keep(it))
|
|
802
802
|
.map((it) => ({ id: it.id, name: it.name, score: it.meta?.score }));
|
|
@@ -805,8 +805,8 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
805
805
|
}
|
|
806
806
|
if (q && !preserveUpstreamOrder) {
|
|
807
807
|
items.sort((a, b) => {
|
|
808
|
-
const am = a.meta?.matchedBy === '
|
|
809
|
-
const bm = b.meta?.matchedBy === '
|
|
808
|
+
const am = a.meta?.matchedBy === 'top-hit' ? 1 : 0;
|
|
809
|
+
const bm = b.meta?.matchedBy === 'top-hit' ? 1 : 0;
|
|
810
810
|
return bm - am || scoreOf(b) - scoreOf(a) || a.name.localeCompare(b.name);
|
|
811
811
|
});
|
|
812
812
|
}
|
|
@@ -820,8 +820,8 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
820
820
|
.slice(0, 5)
|
|
821
821
|
.map((d) => `${d.name} (${d.id})`)
|
|
822
822
|
.join(', ');
|
|
823
|
-
floorWarnings.push(`Dropped ${droppedBelowFloor.length} result(s) that do not match "${q}" at all
|
|
824
|
-
`substring match with no relevance floor, so it pads every page: ${sample}. ` +
|
|
823
|
+
floorWarnings.push(`Dropped ${droppedBelowFloor.length} result(s) that do not match "${q}" at all (the name search is a ` +
|
|
824
|
+
`substring match with no relevance floor, so it pads every page): ${sample}. ` +
|
|
825
825
|
`If one of these is what you wanted, query it by name or id directly.`);
|
|
826
826
|
}
|
|
827
827
|
return successEnvelope({
|
|
@@ -849,7 +849,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
849
849
|
matched: items.length,
|
|
850
850
|
droppedBelowFloor: droppedBelowFloor.length,
|
|
851
851
|
dropped: droppedBelowFloor.slice(0, 25),
|
|
852
|
-
|
|
852
|
+
unfilteredTotal: upstreamTotal ?? null,
|
|
853
853
|
},
|
|
854
854
|
}
|
|
855
855
|
: {}),
|
package/dist/tools/schedule.js
CHANGED
|
@@ -54,7 +54,7 @@ export const tennisSchedule = {
|
|
|
54
54
|
When to use:
|
|
55
55
|
- "What tennis is on today?"; a specific day's fixtures; results completed on a given date; building a daily digest.
|
|
56
56
|
|
|
57
|
-
Prefer over: call_api for /tennis/schedule and /tennis/matches/completed (
|
|
57
|
+
Prefer over: call_api for /tennis/schedule and /tennis/matches/completed (this tool supplies and validates the date, and bounds the rows); live_matches (in-progress only, right now); upcoming_schedule (a forward window, not a specific day).
|
|
58
58
|
|
|
59
59
|
Do not use when: matches currently in play → live_matches; a named player's history → player_matches.
|
|
60
60
|
|
|
@@ -62,13 +62,14 @@ Tennis-only. 'date' defaults to today (UTC) and MUST be YYYY-MM-DD. Choose with
|
|
|
62
62
|
'include': both (default), scheduled, or completed. Both halves are fetched in
|
|
63
63
|
parallel, so either can fail on its own without losing the other.
|
|
64
64
|
|
|
65
|
-
'limit' bounds EACH half (the
|
|
66
|
-
bound is applied here); scheduledCount/completedCount report how many rows the
|
|
65
|
+
'limit' bounds EACH half (the bound is applied by this tool); scheduledCount/completedCount report how many rows the
|
|
67
66
|
day held and *Returned how many came back. The two halves overlap: the day's
|
|
68
67
|
list includes matches already played, so the same matchId can appear in both —
|
|
69
68
|
see the overlap block before de-duplicating or counting.
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
Rows: start_time_known false means no start time is held (scheduled_at is then midnight). start_time_kind says whether scheduled_at is when the match actually started (actual) or the published order-of-play time (scheduled). Read status for where a match is now and outcome for how it ended.
|
|
71
|
+
|
|
72
|
+
Parallel-safe: yes. API calls: 1 or 2.`,
|
|
72
73
|
inputSchema: {
|
|
73
74
|
type: 'object',
|
|
74
75
|
additionalProperties: false,
|
|
@@ -172,7 +173,7 @@ Parallel-safe: yes. Upstream cost: 1 or 2.`,
|
|
|
172
173
|
else if (schedRes.status === 'rejected') {
|
|
173
174
|
rejected.push({
|
|
174
175
|
section: 'schedule',
|
|
175
|
-
code: '
|
|
176
|
+
code: 'SERVICE_ERROR',
|
|
176
177
|
message: String(schedRes.reason).slice(0, 200),
|
|
177
178
|
});
|
|
178
179
|
}
|
|
@@ -194,7 +195,7 @@ Parallel-safe: yes. Upstream cost: 1 or 2.`,
|
|
|
194
195
|
else if (compRes.status === 'rejected') {
|
|
195
196
|
rejected.push({
|
|
196
197
|
section: 'completed',
|
|
197
|
-
code: '
|
|
198
|
+
code: 'SERVICE_ERROR',
|
|
198
199
|
message: String(compRes.reason).slice(0, 200),
|
|
199
200
|
});
|
|
200
201
|
}
|
package/dist/tools/standings.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeCoverage } from './rankings.js';
|
|
1
2
|
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
2
3
|
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
3
4
|
import { gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
@@ -77,26 +78,26 @@ export function normalizeStandingRow(row, index) {
|
|
|
77
78
|
}
|
|
78
79
|
export const standings = {
|
|
79
80
|
name: 'standings',
|
|
80
|
-
description: `League/event standings or world/division rankings normalized to ranked rows.
|
|
81
|
-
|
|
82
|
-
When to use:
|
|
83
|
-
- Table / playoff picture
|
|
84
|
-
- UFC rankings; CS2 world or event standings; CDL standings; LoL league/tournament tables
|
|
85
|
-
|
|
86
|
-
Prefer over: raw standings via call_api for agent-normalized rows.
|
|
87
|
-
|
|
88
|
-
Do not use when: single team form → team_profile; live scores → live_matches.
|
|
89
|
-
Dota has no first-class standings (may NOT_IMPLEMENTED or weak worldRanking).
|
|
90
|
-
|
|
91
|
-
Required scope keys by game:
|
|
92
|
-
- lol: leagueId OR tournamentId
|
|
93
|
-
- cs2: omit for world rankings; eventId for event standings
|
|
94
|
-
- cod: optional season/stage
|
|
95
|
-
- ufc: optional division (scope=division)
|
|
96
|
-
- tennis: optional division (ATP or WTA tour; default ATP)
|
|
97
|
-
- dota2: best-effort worldRanking only
|
|
98
|
-
|
|
99
|
-
Parallel-safe: yes.
|
|
81
|
+
description: `League/event standings or world/division rankings normalized to ranked rows.
|
|
82
|
+
|
|
83
|
+
When to use:
|
|
84
|
+
- Table / playoff picture
|
|
85
|
+
- UFC rankings; CS2 world or event standings; CDL standings; LoL league/tournament tables
|
|
86
|
+
|
|
87
|
+
Prefer over: raw standings via call_api for agent-normalized rows.
|
|
88
|
+
|
|
89
|
+
Do not use when: single team form → team_profile; live scores → live_matches.
|
|
90
|
+
Dota has no first-class standings (may NOT_IMPLEMENTED or weak worldRanking).
|
|
91
|
+
|
|
92
|
+
Required scope keys by game:
|
|
93
|
+
- lol: leagueId OR tournamentId
|
|
94
|
+
- cs2: omit for world rankings; eventId for event standings
|
|
95
|
+
- cod: optional season/stage
|
|
96
|
+
- ufc: optional division (scope=division)
|
|
97
|
+
- tennis: optional division (ATP or WTA tour; default ATP); optional season YYYY = that season's last published list (default: latest). listCoverage says when an archived list is held only in part
|
|
98
|
+
- dota2: best-effort worldRanking only
|
|
99
|
+
|
|
100
|
+
Parallel-safe: yes. API calls: 1–2.
|
|
100
101
|
Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
101
102
|
inputSchema: {
|
|
102
103
|
type: 'object',
|
|
@@ -112,7 +113,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
112
113
|
leagueId: stringSchema('LoL league id for league standings.'),
|
|
113
114
|
tournamentId: stringSchema('LoL/COD tournament id.'),
|
|
114
115
|
eventId: stringSchema('CS2 event id for event standings.'),
|
|
115
|
-
season: stringSchema('Season key (COD).'),
|
|
116
|
+
season: stringSchema('Season key (COD). Tennis: a year (YYYY) gives that season\'s last published ranking list.'),
|
|
116
117
|
stage: stringSchema('Stage key (COD / LoL).'),
|
|
117
118
|
division: stringSchema('UFC division key.'),
|
|
118
119
|
limit: limitSchema({ default: 50, max: 100 }),
|
|
@@ -145,7 +146,11 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
145
146
|
const leagueId = typeof args.leagueId === 'string' ? args.leagueId : undefined;
|
|
146
147
|
const tournamentId = typeof args.tournamentId === 'string' ? args.tournamentId : undefined;
|
|
147
148
|
const eventId = typeof args.eventId === 'string' ? args.eventId : undefined;
|
|
148
|
-
const season = typeof args.season === 'string'
|
|
149
|
+
const season = typeof args.season === 'string'
|
|
150
|
+
? args.season
|
|
151
|
+
: typeof args.season === 'number' && Number.isFinite(args.season)
|
|
152
|
+
? String(Math.trunc(args.season))
|
|
153
|
+
: undefined;
|
|
149
154
|
const stage = typeof args.stage === 'string' ? args.stage : undefined;
|
|
150
155
|
const division = typeof args.division === 'string' ? args.division : undefined;
|
|
151
156
|
const tennisPage = typeof args.page === 'number' && Number.isFinite(args.page) && args.page >= 1
|
|
@@ -241,7 +246,18 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
241
246
|
else if (game === 'tennis') {
|
|
242
247
|
const tour = String(division ?? 'ATP').toUpperCase() === 'WTA' ? 'WTA' : 'ATP';
|
|
243
248
|
path = '/tennis/standings';
|
|
244
|
-
|
|
249
|
+
const tennisSeason = season && /^\d{4}$/.test(season.trim()) ? season.trim() : undefined;
|
|
250
|
+
if (season && !tennisSeason) {
|
|
251
|
+
return errorEnvelope({
|
|
252
|
+
code: 'VALIDATION',
|
|
253
|
+
message: `season must be a year (YYYY) for tennis (got '${season}')`,
|
|
254
|
+
game,
|
|
255
|
+
source: 'standings',
|
|
256
|
+
requestId,
|
|
257
|
+
tookMs: Date.now() - started,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
query = { tour, top_n: Math.min(limit, 100), page: tennisPage, ...(tennisSeason ? { season: tennisSeason } : {}) };
|
|
245
261
|
effectiveScope = 'world';
|
|
246
262
|
title = `${tour} singles rankings`;
|
|
247
263
|
}
|
|
@@ -371,6 +387,9 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
371
387
|
};
|
|
372
388
|
pushWarning(upstreamMeta.warning);
|
|
373
389
|
pushWarning(dataQuality.warning);
|
|
390
|
+
const tennisCoverage = game === 'tennis' ? asRecord(obj?.list_coverage) : null;
|
|
391
|
+
if (tennisCoverage && tennisCoverage.status === 'partial')
|
|
392
|
+
pushWarning(tennisCoverage.note);
|
|
374
393
|
if (Array.isArray(upstreamMeta.warnings)) {
|
|
375
394
|
for (const w of upstreamMeta.warnings)
|
|
376
395
|
pushWarning(w);
|
|
@@ -412,6 +431,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
|
412
431
|
...(game === 'tennis'
|
|
413
432
|
? {
|
|
414
433
|
rankingDate: pickString(obj?.ranking_date) ?? null,
|
|
434
|
+
listCoverage: tennisCoverage ? normalizeCoverage(tennisCoverage) : null,
|
|
415
435
|
updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, upstreamMeta.syncedAt, upstreamMeta.fetchedAt, obj?.ranking_date) ?? null,
|
|
416
436
|
}
|
|
417
437
|
: {
|
package/dist/tools/team.js
CHANGED
|
@@ -42,7 +42,7 @@ Prefer over: separate roster + matches + detail via call_api.
|
|
|
42
42
|
Do not use when: UFC fighters → player_profile; unknown name → resolve_entity first.
|
|
43
43
|
Dota may return partial roster (API gap). Prefer slug for lol/cod; teamId for cs2.
|
|
44
44
|
|
|
45
|
-
Parallel-safe: yes.
|
|
45
|
+
Parallel-safe: yes. API calls: 2–4.
|
|
46
46
|
Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
47
47
|
inputSchema: {
|
|
48
48
|
type: 'object',
|
|
@@ -532,7 +532,7 @@ function winnerSide(match, a) {
|
|
|
532
532
|
}
|
|
533
533
|
export const headToHead = {
|
|
534
534
|
name: 'head_to_head',
|
|
535
|
-
description: `
|
|
535
|
+
description: `Head-to-head record between two teams, two UFC fighters, or two tennis players. Tennis reads the tennis head-to-head route; other games filter match history server-side.
|
|
536
536
|
|
|
537
537
|
When to use:
|
|
538
538
|
- Rivalry / series record questions
|
|
@@ -544,7 +544,9 @@ Do not use when: single-side form only → team_profile or player_profile.
|
|
|
544
544
|
|
|
545
545
|
Caveat: Dota filters are weaker; expect meta.warnings when data is sparse.
|
|
546
546
|
|
|
547
|
-
|
|
547
|
+
Tennis: pass entityType "player" with sideA and sideB (ids like atp_207989, or names). This tool has no players param; for three or more players use h2h_matrix, which does. limit pages the meetings list only (max 50); every tally covers all meetings. In the payload current_rank is null when a player is not on the tour's latest list: read last_rank. The tiebreak and level_breakdown keys are in data.h2h.
|
|
548
|
+
|
|
549
|
+
Parallel-safe: yes. API calls: 2–4.
|
|
548
550
|
Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
549
551
|
inputSchema: {
|
|
550
552
|
type: 'object',
|
|
@@ -558,8 +560,8 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
|
558
560
|
default: 'team',
|
|
559
561
|
description: 'team (default), fighter (UFC), or player (tennis).',
|
|
560
562
|
},
|
|
561
|
-
sideA: stringSchema('Id or slug for side A.', 'faze'),
|
|
562
|
-
sideB: stringSchema('Id or slug for side B.', 'navi'),
|
|
563
|
+
sideA: stringSchema('Id or slug for side A (tennis: atp_207989 or a player name).', 'faze'),
|
|
564
|
+
sideB: stringSchema('Id or slug for side B (tennis: atp_206173 or a player name).', 'navi'),
|
|
563
565
|
limit: limitSchema({ default: 20, max: 50, description: 'Max meetings returned.' }),
|
|
564
566
|
from: stringSchema('ISO-8601 lower bound on meeting startTime (client filter).'),
|
|
565
567
|
to: stringSchema('ISO-8601 upper bound on meeting startTime (client filter).'),
|
|
@@ -982,7 +984,7 @@ Do not use when: a two-player rivalry deep-dive with tiebreak/decider splits →
|
|
|
982
984
|
|
|
983
985
|
Tennis-only. players takes 2-16 ids or names (names resolve via player search). Each matrix cell is "W-L" from the row player's perspective, "-" on the diagonal.
|
|
984
986
|
|
|
985
|
-
Parallel-safe: yes.
|
|
987
|
+
Parallel-safe: yes. API calls: 1 + one search per unresolved name.`,
|
|
986
988
|
inputSchema: {
|
|
987
989
|
type: 'object',
|
|
988
990
|
additionalProperties: false,
|
|
@@ -54,10 +54,10 @@ Tennis-only. Levels accept names (Grand Slam, WTA 1000, ATP 500, Challenger,
|
|
|
54
54
|
ITF World Tennis Tour, Davis Cup, ...) or raw tier codes (G, M, A, C, D).
|
|
55
55
|
A level name that starts with ATP or WTA also fixes the tour (and conflicts with
|
|
56
56
|
a contradicting tour= argument are rejected), so "WTA 1000" cannot return ATP
|
|
57
|
-
Masters events
|
|
58
|
-
|
|
57
|
+
Masters events.
|
|
58
|
+
The category field reports ATP 500 and ATP 250 both as atp_tour (read tier to tell them apart), and WTA 1000 events as wta_1000.
|
|
59
59
|
|
|
60
|
-
Parallel-safe: yes.
|
|
60
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
61
61
|
inputSchema: {
|
|
62
62
|
type: 'object',
|
|
63
63
|
additionalProperties: false,
|
|
@@ -232,10 +232,8 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
232
232
|
.slice(0, 5)
|
|
233
233
|
.map((t) => `${t.id} (${t.tour})`)
|
|
234
234
|
.join(', ');
|
|
235
|
-
warnings.push(`The
|
|
236
|
-
`${sample}. They were dropped
|
|
237
|
-
`PM and M), which is a server-side defect; this tool sends tour=${levelTourPrefix} as well, so a repeat of this ` +
|
|
238
|
-
`warning means the upstream filter is widening again.`);
|
|
235
|
+
warnings.push(`The level filter returned ${contradictions.length} tournament(s) that contradict level="${levelRaw}": ` +
|
|
236
|
+
`${sample}. They were dropped; this tool also sends tour=${levelTourPrefix}.`);
|
|
239
237
|
}
|
|
240
238
|
return successEnvelope({
|
|
241
239
|
pagination: {
|
package/dist/tools/types.js
CHANGED
|
@@ -156,7 +156,7 @@ export async function runTool(def, args, ctx) {
|
|
|
156
156
|
catch (error) {
|
|
157
157
|
const { errorEnvelope, toMcpResult: toResult } = await import('../envelope.js');
|
|
158
158
|
return toResult(errorEnvelope({
|
|
159
|
-
code: '
|
|
159
|
+
code: 'SERVICE_ERROR',
|
|
160
160
|
message: error.message || 'Unhandled tool error',
|
|
161
161
|
game: null,
|
|
162
162
|
source: def.name,
|