cito-mcp 0.1.0 → 0.2.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/README.md +396 -44
- package/dist/client.js +208 -0
- package/dist/envelope.js +210 -0
- package/dist/index.js +103 -201
- package/dist/instructions.js +80 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/insight.js +1013 -0
- package/dist/tools/live.js +447 -0
- package/dist/tools/match.js +464 -0
- package/dist/tools/meta.js +609 -0
- package/dist/tools/normalize.js +177 -0
- package/dist/tools/player.js +357 -0
- package/dist/tools/resolve.js +518 -0
- package/dist/tools/standings.js +319 -0
- package/dist/tools/team.js +704 -0
- package/dist/tools/types.js +85 -0
- package/package.json +3 -3
- package/dist/executor.js +0 -75
- package/dist/spec.js +0 -193
- package/dist/tools.js +0 -203
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* standings
|
|
3
|
+
*/
|
|
4
|
+
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
5
|
+
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
6
|
+
import { gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
7
|
+
function normalizeStandingRow(row, index) {
|
|
8
|
+
const r = asRecord(row) ?? {};
|
|
9
|
+
const entity = asRecord(r.team) ?? asRecord(r.fighter) ?? asRecord(r.org) ?? r;
|
|
10
|
+
const name = pickString(asRecord(entity)?.name, r.teamName, r.name, r.orgName, r.fighterName, asRecord(entity)?.slug) ?? `row-${index + 1}`;
|
|
11
|
+
// UFC official lists: champion has rank=null + rankText="C"; contenders 1..15.
|
|
12
|
+
// Never fall back to index+1 for null ranks — that produced two "#1" rows (champ + #1).
|
|
13
|
+
const championStatus = pickString(r.championStatus, asRecord(entity)?.championStatus);
|
|
14
|
+
const rankText = pickString(r.rankText);
|
|
15
|
+
const isChampion = championStatus === 'champion' ||
|
|
16
|
+
rankText === 'C' ||
|
|
17
|
+
r.isChampion === true ||
|
|
18
|
+
r.isTitleHolder === true;
|
|
19
|
+
let rank;
|
|
20
|
+
if (isChampion) {
|
|
21
|
+
rank = 'C';
|
|
22
|
+
}
|
|
23
|
+
else if (typeof r.rank === 'number') {
|
|
24
|
+
rank = r.rank;
|
|
25
|
+
}
|
|
26
|
+
else if (typeof r.position === 'number') {
|
|
27
|
+
rank = r.position;
|
|
28
|
+
}
|
|
29
|
+
else if (rankText && /^\d+$/.test(rankText)) {
|
|
30
|
+
rank = Number(rankText);
|
|
31
|
+
}
|
|
32
|
+
else if (r.rank === null && rankText) {
|
|
33
|
+
rank = rankText;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
rank = index + 1;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
rank,
|
|
40
|
+
rankText: rankText ?? (typeof rank === 'number' || typeof rank === 'string' ? String(rank) : null),
|
|
41
|
+
championStatus: isChampion ? 'champion' : championStatus === 'interim' ? 'interim' : 'none',
|
|
42
|
+
teamOrFighter: {
|
|
43
|
+
id: pickString(asRecord(entity)?.id, r.teamId, r.fighterId, r.orgId, r.id),
|
|
44
|
+
slug: pickString(asRecord(entity)?.slug, r.orgSlug, r.teamSlug, r.slug, r.fighterSlug),
|
|
45
|
+
name,
|
|
46
|
+
},
|
|
47
|
+
played: r.played ?? r.gamesPlayed ?? r.matches ?? null,
|
|
48
|
+
wins: r.wins ?? r.w ?? null,
|
|
49
|
+
losses: r.losses ?? r.l ?? null,
|
|
50
|
+
draws: r.draws ?? r.ties ?? null,
|
|
51
|
+
points: r.points ?? r.pts ?? r.score ?? null,
|
|
52
|
+
mapDiff: r.mapDiff ?? r.gameDiff ?? r.diff ?? null,
|
|
53
|
+
streak: r.streak ?? null,
|
|
54
|
+
meta: {
|
|
55
|
+
...(r.division ? { division: r.division } : {}),
|
|
56
|
+
...(r.normalizedDivision ? { normalizedDivision: r.normalizedDivision } : {}),
|
|
57
|
+
...(r.region ? { region: r.region } : {}),
|
|
58
|
+
...(isChampion ? { isChampion: true } : {}),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export const standings = {
|
|
63
|
+
name: 'standings',
|
|
64
|
+
description: `League/event standings or world/division rankings normalized to ranked rows.
|
|
65
|
+
|
|
66
|
+
When to use:
|
|
67
|
+
- Table / playoff picture
|
|
68
|
+
- UFC rankings; CS2 world or event standings; CDL standings; LoL league/tournament tables
|
|
69
|
+
|
|
70
|
+
Prefer over: raw standings via call_api for agent-normalized rows.
|
|
71
|
+
|
|
72
|
+
Do not use when: single team form → team_profile; live scores → live_matches.
|
|
73
|
+
Dota has no first-class standings (may NOT_IMPLEMENTED or weak worldRanking).
|
|
74
|
+
|
|
75
|
+
Required scope keys by game:
|
|
76
|
+
- lol: leagueId OR tournamentId
|
|
77
|
+
- cs2: omit for world rankings; eventId for event standings
|
|
78
|
+
- cod: optional season/stage
|
|
79
|
+
- ufc: optional division (scope=division)
|
|
80
|
+
- dota2: best-effort worldRanking only
|
|
81
|
+
|
|
82
|
+
Parallel-safe: yes. Upstream cost: 1–2.
|
|
83
|
+
Example: { "game": "cod", "season": "2026", "limit": 50 }`,
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
required: ['game'],
|
|
88
|
+
properties: {
|
|
89
|
+
game: gameSchema({ allowAll: false, required: true }),
|
|
90
|
+
scope: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
enum: ['league', 'tournament', 'event', 'world', 'division'],
|
|
93
|
+
description: 'Interpretation depends on game; defaults sensibly per game.',
|
|
94
|
+
},
|
|
95
|
+
leagueId: stringSchema('LoL league id for league standings.'),
|
|
96
|
+
tournamentId: stringSchema('LoL/COD tournament id.'),
|
|
97
|
+
eventId: stringSchema('CS2 event id for event standings.'),
|
|
98
|
+
season: stringSchema('Season key (COD).'),
|
|
99
|
+
stage: stringSchema('Stage key (COD / LoL).'),
|
|
100
|
+
division: stringSchema('UFC division key.'),
|
|
101
|
+
limit: limitSchema({ default: 50, max: 100 }),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
handler: async (args, ctx) => {
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
const requestId = newRequestId();
|
|
107
|
+
const gameParse = parseGame(args.game, { allowAll: false, required: true });
|
|
108
|
+
if (gameParse.error || !gameParse.game || !isPrimaryGame(gameParse.game)) {
|
|
109
|
+
return errorEnvelope({
|
|
110
|
+
code: gameParse.error?.includes('unsupported') ? 'UNSUPPORTED_GAME' : 'VALIDATION',
|
|
111
|
+
message: gameParse.error ?? 'game is required',
|
|
112
|
+
game: null,
|
|
113
|
+
source: 'standings',
|
|
114
|
+
requestId,
|
|
115
|
+
tookMs: Date.now() - started,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const game = gameParse.game;
|
|
119
|
+
const limit = clampInt(args.limit, 50, 1, 100);
|
|
120
|
+
const scope = typeof args.scope === 'string' ? args.scope : undefined;
|
|
121
|
+
const leagueId = typeof args.leagueId === 'string' ? args.leagueId : undefined;
|
|
122
|
+
const tournamentId = typeof args.tournamentId === 'string' ? args.tournamentId : undefined;
|
|
123
|
+
const eventId = typeof args.eventId === 'string' ? args.eventId : undefined;
|
|
124
|
+
const season = typeof args.season === 'string' ? args.season : undefined;
|
|
125
|
+
const stage = typeof args.stage === 'string' ? args.stage : undefined;
|
|
126
|
+
const division = typeof args.division === 'string' ? args.division : undefined;
|
|
127
|
+
let path = '';
|
|
128
|
+
let query = {};
|
|
129
|
+
let title = `${game} standings`;
|
|
130
|
+
let effectiveScope = scope ?? 'world';
|
|
131
|
+
if (game === 'lol') {
|
|
132
|
+
if (tournamentId || scope === 'tournament') {
|
|
133
|
+
if (!tournamentId) {
|
|
134
|
+
return errorEnvelope({
|
|
135
|
+
code: 'VALIDATION',
|
|
136
|
+
message: 'tournamentId required for LoL tournament standings',
|
|
137
|
+
game,
|
|
138
|
+
source: 'standings',
|
|
139
|
+
requestId,
|
|
140
|
+
tookMs: Date.now() - started,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
path = `/lol/tournaments/${encodeURIComponent(tournamentId)}/standings`;
|
|
144
|
+
effectiveScope = 'tournament';
|
|
145
|
+
title = `LoL tournament ${tournamentId}`;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
if (!leagueId) {
|
|
149
|
+
return errorEnvelope({
|
|
150
|
+
code: 'VALIDATION',
|
|
151
|
+
message: 'leagueId (or tournamentId) required for LoL standings',
|
|
152
|
+
game,
|
|
153
|
+
source: 'standings',
|
|
154
|
+
requestId,
|
|
155
|
+
tookMs: Date.now() - started,
|
|
156
|
+
recover: [
|
|
157
|
+
'Call search_entities or resolve_entity for the league',
|
|
158
|
+
'Retry standings with leagueId or tournamentId',
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
path = `/lol/leagues/${encodeURIComponent(leagueId)}/standings`;
|
|
163
|
+
query = { ...(season ? { season } : {}), ...(stage ? { stage } : {}) };
|
|
164
|
+
effectiveScope = 'league';
|
|
165
|
+
title = `LoL league ${leagueId}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (game === 'cs2') {
|
|
169
|
+
if (eventId || scope === 'event') {
|
|
170
|
+
if (!eventId) {
|
|
171
|
+
return errorEnvelope({
|
|
172
|
+
code: 'VALIDATION',
|
|
173
|
+
message: 'eventId required for CS2 event standings',
|
|
174
|
+
game,
|
|
175
|
+
source: 'standings',
|
|
176
|
+
requestId,
|
|
177
|
+
tookMs: Date.now() - started,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
path = `/cs2/events/${encodeURIComponent(eventId)}/standings`;
|
|
181
|
+
effectiveScope = 'event';
|
|
182
|
+
title = `CS2 event ${eventId}`;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
path = '/cs2/rankings/teams';
|
|
186
|
+
query = { limit };
|
|
187
|
+
effectiveScope = 'world';
|
|
188
|
+
title = 'CS2 world team rankings';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (game === 'cod') {
|
|
192
|
+
path = '/cod/standings';
|
|
193
|
+
query = {
|
|
194
|
+
...(season ? { season } : {}),
|
|
195
|
+
...(stage ? { stage } : {}),
|
|
196
|
+
};
|
|
197
|
+
effectiveScope = scope ?? 'league';
|
|
198
|
+
title = 'CDL standings';
|
|
199
|
+
}
|
|
200
|
+
else if (game === 'ufc') {
|
|
201
|
+
if (division || scope === 'division') {
|
|
202
|
+
path = division
|
|
203
|
+
? `/ufc/rankings/media/${encodeURIComponent(division)}`
|
|
204
|
+
: '/ufc/rankings/media';
|
|
205
|
+
effectiveScope = 'division';
|
|
206
|
+
title = division ? `UFC ${division} rankings` : 'UFC media rankings';
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
path = '/ufc/rankings';
|
|
210
|
+
effectiveScope = 'world';
|
|
211
|
+
title = 'UFC rankings';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (game === 'dota2') {
|
|
215
|
+
// No first-class standings — try team list worldRanking
|
|
216
|
+
const res = await fetchJson(ctx, '/dota2/teams', { query: { limit } });
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
return errorEnvelope({
|
|
219
|
+
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
220
|
+
message: 'Dota standings unavailable',
|
|
221
|
+
game,
|
|
222
|
+
source: 'standings',
|
|
223
|
+
requestId,
|
|
224
|
+
tookMs: Date.now() - started,
|
|
225
|
+
upstreamCalls: 1,
|
|
226
|
+
httpStatus: res.status,
|
|
227
|
+
rateLimit: res.headers,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const rows = extractRows(res.data)
|
|
231
|
+
.map((row, i) => {
|
|
232
|
+
const r = asRecord(row) ?? {};
|
|
233
|
+
const rank = r.worldRanking ?? r.rank ?? i + 1;
|
|
234
|
+
return normalizeStandingRow({ ...r, rank }, i);
|
|
235
|
+
})
|
|
236
|
+
.filter((row) => row.rank != null)
|
|
237
|
+
.sort((a, b) => Number(a.rank) - Number(b.rank))
|
|
238
|
+
.slice(0, limit);
|
|
239
|
+
if (rows.length === 0) {
|
|
240
|
+
return errorEnvelope({
|
|
241
|
+
code: 'NOT_IMPLEMENTED',
|
|
242
|
+
message: 'Dota 2 has no first-class standings endpoint',
|
|
243
|
+
game,
|
|
244
|
+
source: 'standings',
|
|
245
|
+
requestId,
|
|
246
|
+
tookMs: Date.now() - started,
|
|
247
|
+
upstreamCalls: 1,
|
|
248
|
+
recover: [
|
|
249
|
+
'Use team_profile with radar for style/form',
|
|
250
|
+
'Use call_api if a standings path is added later',
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return successEnvelope({
|
|
255
|
+
source: 'standings',
|
|
256
|
+
game,
|
|
257
|
+
requestId,
|
|
258
|
+
tookMs: Date.now() - started,
|
|
259
|
+
upstreamCalls: 1,
|
|
260
|
+
rateLimit: res.headers,
|
|
261
|
+
warnings: ['Dota standings composed from team worldRanking fields; sparse'],
|
|
262
|
+
data: {
|
|
263
|
+
scope: 'world',
|
|
264
|
+
title: 'Dota 2 world ranking (best-effort)',
|
|
265
|
+
season: null,
|
|
266
|
+
stage: null,
|
|
267
|
+
rows,
|
|
268
|
+
updatedAt: null,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const res = await fetchJson(ctx, path, { query });
|
|
273
|
+
if (!res.ok) {
|
|
274
|
+
return errorEnvelope({
|
|
275
|
+
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
276
|
+
message: `Standings failed (HTTP ${res.status})`,
|
|
277
|
+
game,
|
|
278
|
+
source: 'standings',
|
|
279
|
+
requestId,
|
|
280
|
+
tookMs: Date.now() - started,
|
|
281
|
+
upstreamCalls: 1,
|
|
282
|
+
httpStatus: res.status,
|
|
283
|
+
rateLimit: res.headers,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
let rows = extractRows(res.data).map((row, i) => normalizeStandingRow(row, i)).slice(0, limit);
|
|
287
|
+
// UFC rankings may be nested by division
|
|
288
|
+
if (game === 'ufc' && rows.length === 0) {
|
|
289
|
+
const obj = asRecord(res.data) ?? {};
|
|
290
|
+
const nested = [];
|
|
291
|
+
for (const value of Object.values(obj)) {
|
|
292
|
+
if (Array.isArray(value))
|
|
293
|
+
nested.push(...value);
|
|
294
|
+
else if (asRecord(value) && Array.isArray(asRecord(value).rankings)) {
|
|
295
|
+
nested.push(...asRecord(value).rankings);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
rows = nested.map((row, i) => normalizeStandingRow(row, i)).slice(0, limit);
|
|
299
|
+
}
|
|
300
|
+
const obj = asRecord(res.data);
|
|
301
|
+
return successEnvelope({
|
|
302
|
+
source: 'standings',
|
|
303
|
+
game,
|
|
304
|
+
requestId,
|
|
305
|
+
tookMs: Date.now() - started,
|
|
306
|
+
upstreamCalls: 1,
|
|
307
|
+
rateLimit: res.headers,
|
|
308
|
+
data: {
|
|
309
|
+
scope: effectiveScope,
|
|
310
|
+
title,
|
|
311
|
+
season: season ?? null,
|
|
312
|
+
stage: stage ?? null,
|
|
313
|
+
rows,
|
|
314
|
+
updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated) ?? null,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
export const standingsTools = [standings];
|