cito-mcp 0.3.20 → 0.4.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 CHANGED
@@ -12,7 +12,7 @@ Primary games & sports: **lol · cs2 · dota2 · cod · ufc · tennis**. Long-ta
12
12
 
13
13
  v0.1 exposed ~100+ tools auto-generated from OpenAPI. Agents had to pick among thin path wrappers, invent IDs, and stitch multi-call UI screens themselves. That catalog was hard to select against and brittle across games.
14
14
 
15
- **v0.3 ships 18 hand-authored tools** that answer *jobs* instead of mirroring REST:
15
+ **Ships 42 curated outcome tools** that answer *jobs* instead of mirroring REST:
16
16
 
17
17
  | Job | Tool |
18
18
  | --- | --- |
@@ -28,6 +28,12 @@ v0.1 exposed ~100+ tools auto-generated from OpenAPI. Agents had to pick among t
28
28
  | Pre-match briefing (with surface) | `match_preview` |
29
29
  | Event / fight-night card | `event_card` |
30
30
  | Rivalry record | `head_to_head` |
31
+ | Tennis betting odds | `tennis_odds` |
32
+ | Tennis tournament catalog | `tournaments` |
33
+ | One day of tennis | `tennis_schedule` |
34
+ | Tennis player match log | `player_matches` |
35
+ | Tennis career statistics | `player_stats` |
36
+ | Tennis ranking trajectory | `player_rankings_history` |
31
37
  | Fighter / team photos | `event_card` · `player_profile` |
32
38
  | Which raw REST route exists? | `list_routes` |
33
39
  | Name → ID | `resolve_entity` / `search_entities` |
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Dedicated CS2 esports analytics & live scorebot MCP tools.
3
+ *
4
+ * Exposes deep CS2 domain data from the 161 backend endpoints:
5
+ * - Live real-time scoreboards & round state
6
+ * - Round-by-round tactical economies & buy classifications
7
+ * - First blood / opening duels per player & map
8
+ * - 1vX clutch success rates (1v1 through 1v5)
9
+ * - Team map pool analytics (Mirage, Inferno, Nuke, Dust2, Ancient, Anubis, Vertigo)
10
+ * - Sub-second utility & flashbang efficiency leaderboard
11
+ * - Match map pick/ban veto sequence
12
+ * - Pro roster changes & transfers
13
+ */
14
+ import { asRecord, clampInt, fetchJson, unwrapPayload, } from '../client.js';
15
+ import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
16
+ import { limitSchema, READ_TOOL_ANNOTATIONS, stringSchema, } from './types.js';
17
+ /** Unwraps Cito envelope whether inner entity is an object or array */
18
+ function unwrap(val) {
19
+ const p = unwrapPayload(val);
20
+ const r = asRecord(p);
21
+ if (r && Array.isArray(r.data))
22
+ return r.data;
23
+ if (r && Array.isArray(r.items))
24
+ return r.items;
25
+ return p;
26
+ }
27
+ /**
28
+ * 1. cs2_live_scoreboard
29
+ * Real-time round state, bomb status, and player combat stats for an active match.
30
+ */
31
+ export const cs2LiveScoreboard = {
32
+ name: 'cs2_live_scoreboard',
33
+ description: 'Live real-time in-game scoreboard for an active CS2 match. Returns current round, bomb status, team scores, and individual player stats (K/A/D, ADR, HS%, alive status, HP, armor, weapon, and equipment value).',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ matchId: stringSchema('Match ID (e.g. "cs2-match-2397733" or "2397733"). Required.', 'cs2-match-2397733'),
38
+ },
39
+ required: ['matchId'],
40
+ additionalProperties: false,
41
+ },
42
+ annotations: READ_TOOL_ANNOTATIONS,
43
+ handler: async (args, ctx) => {
44
+ const reqId = newRequestId();
45
+ const rawId = typeof args.matchId === 'string' ? args.matchId.trim() : '';
46
+ if (!rawId) {
47
+ return errorEnvelope({
48
+ code: 'VALIDATION',
49
+ message: 'matchId is required',
50
+ game: 'cs2',
51
+ source: 'cs2_live_scoreboard',
52
+ requestId: reqId,
53
+ });
54
+ }
55
+ const cleanId = rawId.replace(/^cs2-match-/, '');
56
+ const path = `/cs2/live/${encodeURIComponent(cleanId)}/scoreboard`;
57
+ const res = await fetchJson(ctx, path);
58
+ if (!res.ok) {
59
+ return errorEnvelope({
60
+ code: mapHttpToCode(res.status),
61
+ message: `Live scoreboard fetch failed: ${res.status} on ${path}`,
62
+ game: 'cs2',
63
+ source: 'cs2_live_scoreboard',
64
+ requestId: reqId,
65
+ httpStatus: res.status,
66
+ });
67
+ }
68
+ const payload = unwrap(res.data);
69
+ return successEnvelope({
70
+ data: payload,
71
+ game: 'cs2',
72
+ source: 'cs2_live_scoreboard',
73
+ requestId: reqId,
74
+ rateLimit: res.headers,
75
+ });
76
+ },
77
+ };
78
+ /**
79
+ * 2. cs2_round_economy
80
+ * Tactical round-by-round buy classification and equipment values for a map.
81
+ */
82
+ export const cs2RoundEconomy = {
83
+ name: 'cs2_round_economy',
84
+ description: 'Round-by-round tactical economy for a CS2 map. Classifies every round buy type (Full Buy, Force Buy, Semi-Eco, Full Eco), team equipment spend, freeze-time equipment values, and loss bonus counter.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {
88
+ mapId: stringSchema('Map ID or Game ID (e.g. "7716-map-1" or "cs2-game-123"). Required.', '7716-map-1'),
89
+ },
90
+ required: ['mapId'],
91
+ additionalProperties: false,
92
+ },
93
+ annotations: READ_TOOL_ANNOTATIONS,
94
+ handler: async (args, ctx) => {
95
+ const reqId = newRequestId();
96
+ const rawMapId = typeof args.mapId === 'string' ? args.mapId.trim() : '';
97
+ if (!rawMapId) {
98
+ return errorEnvelope({
99
+ code: 'VALIDATION',
100
+ message: 'mapId is required',
101
+ game: 'cs2',
102
+ source: 'cs2_round_economy',
103
+ requestId: reqId,
104
+ });
105
+ }
106
+ const path = `/cs2/maps/${encodeURIComponent(rawMapId)}/rounds`;
107
+ let res = await fetchJson(ctx, path);
108
+ if (!res.ok && res.status === 404) {
109
+ res = await fetchJson(ctx, `/cs2/games/${encodeURIComponent(rawMapId)}/rounds`);
110
+ }
111
+ if (!res.ok) {
112
+ return errorEnvelope({
113
+ code: mapHttpToCode(res.status),
114
+ message: `Round economy fetch failed: ${res.status} on ${path}`,
115
+ game: 'cs2',
116
+ source: 'cs2_round_economy',
117
+ requestId: reqId,
118
+ httpStatus: res.status,
119
+ });
120
+ }
121
+ const payload = unwrap(res.data);
122
+ return successEnvelope({
123
+ data: payload,
124
+ game: 'cs2',
125
+ source: 'cs2_round_economy',
126
+ requestId: reqId,
127
+ rateLimit: res.headers,
128
+ });
129
+ },
130
+ };
131
+ /**
132
+ * 3. cs2_opening_duels
133
+ * First blood / opening kill statistics by player or overall leaderboard.
134
+ */
135
+ export const cs2OpeningDuels = {
136
+ name: 'cs2_opening_duels',
137
+ description: 'First blood and opening duel statistics. Pass playerId to view a pro player\'s First Kills (FK), First Deaths (FD), and opening duel conversion %, or omit playerId to view the global CS2 opening duel leaderboard.',
138
+ inputSchema: {
139
+ type: 'object',
140
+ properties: {
141
+ playerId: stringSchema('Optional player ID or slug (e.g. "chucky" or "cs2-player-123"). If omitted, returns global leaderboard.', 'chucky'),
142
+ limit: limitSchema({ default: 20, max: 100 }),
143
+ },
144
+ additionalProperties: false,
145
+ },
146
+ annotations: READ_TOOL_ANNOTATIONS,
147
+ handler: async (args, ctx) => {
148
+ const reqId = newRequestId();
149
+ const rawPlayer = typeof args.playerId === 'string' ? args.playerId.trim() : '';
150
+ const limit = clampInt(args.limit, 20, 1, 100);
151
+ const path = rawPlayer
152
+ ? `/cs2/players/${encodeURIComponent(rawPlayer)}/opening-duels`
153
+ : `/cs2/stats/opening-duels`;
154
+ const res = await fetchJson(ctx, path, { query: { limit } });
155
+ if (!res.ok) {
156
+ return errorEnvelope({
157
+ code: mapHttpToCode(res.status),
158
+ message: `Opening duels fetch failed: ${res.status} on ${path}`,
159
+ game: 'cs2',
160
+ source: 'cs2_opening_duels',
161
+ requestId: reqId,
162
+ httpStatus: res.status,
163
+ });
164
+ }
165
+ const payload = unwrap(res.data);
166
+ return successEnvelope({
167
+ data: payload,
168
+ game: 'cs2',
169
+ source: 'cs2_opening_duels',
170
+ requestId: reqId,
171
+ rateLimit: res.headers,
172
+ });
173
+ },
174
+ };
175
+ /**
176
+ * 4. cs2_clutches
177
+ * 1vX clutch situation success records (1v1 through 1v5).
178
+ */
179
+ export const cs2Clutches = {
180
+ name: 'cs2_clutches',
181
+ description: '1vX clutch situation success records. Pass playerId for a specific player\'s clutch breakdown (1v1, 1v2, 1v3, 1v4, 1v5 attempted vs won), or omit to view the global clutch leaderboard.',
182
+ inputSchema: {
183
+ type: 'object',
184
+ properties: {
185
+ playerId: stringSchema('Optional player ID or slug (e.g. "xm1nd" or "cs2-player-456"). If omitted, returns global leaderboard.', 'xm1nd'),
186
+ limit: limitSchema({ default: 20, max: 100 }),
187
+ },
188
+ additionalProperties: false,
189
+ },
190
+ annotations: READ_TOOL_ANNOTATIONS,
191
+ handler: async (args, ctx) => {
192
+ const reqId = newRequestId();
193
+ const rawPlayer = typeof args.playerId === 'string' ? args.playerId.trim() : '';
194
+ const limit = clampInt(args.limit, 20, 1, 100);
195
+ const path = rawPlayer
196
+ ? `/cs2/players/${encodeURIComponent(rawPlayer)}/clutches`
197
+ : `/cs2/stats/clutches`;
198
+ const res = await fetchJson(ctx, path, { query: { limit } });
199
+ if (!res.ok) {
200
+ return errorEnvelope({
201
+ code: mapHttpToCode(res.status),
202
+ message: `Clutches fetch failed: ${res.status} on ${path}`,
203
+ game: 'cs2',
204
+ source: 'cs2_clutches',
205
+ requestId: reqId,
206
+ httpStatus: res.status,
207
+ });
208
+ }
209
+ const payload = unwrap(res.data);
210
+ return successEnvelope({
211
+ data: payload,
212
+ game: 'cs2',
213
+ source: 'cs2_clutches',
214
+ requestId: reqId,
215
+ rateLimit: res.headers,
216
+ });
217
+ },
218
+ };
219
+ /**
220
+ * 5. cs2_team_map_stats
221
+ * Team performance across the competitive map pool.
222
+ */
223
+ export const cs2TeamMapStats = {
224
+ name: 'cs2_team_map_stats',
225
+ description: 'Team win rates, round win rates, and CT/T side win splits across the competitive map pool (Mirage, Inferno, Nuke, Dust2, Ancient, Anubis, Vertigo) over the last 30 or 90 days.',
226
+ inputSchema: {
227
+ type: 'object',
228
+ properties: {
229
+ teamId: stringSchema('Team ID or slug (e.g. "hltv-team-4608", "cs2-team-4608", or "natus-vincere"). Required.', 'hltv-team-4608'),
230
+ days: {
231
+ type: 'integer',
232
+ description: 'Timeframe in days: 30, 90, 180, or 365 (default 90).',
233
+ default: 90,
234
+ },
235
+ },
236
+ required: ['teamId'],
237
+ additionalProperties: false,
238
+ },
239
+ annotations: READ_TOOL_ANNOTATIONS,
240
+ handler: async (args, ctx) => {
241
+ const reqId = newRequestId();
242
+ const rawTeam = typeof args.teamId === 'string' ? args.teamId.trim() : '';
243
+ if (!rawTeam) {
244
+ return errorEnvelope({
245
+ code: 'VALIDATION',
246
+ message: 'teamId is required',
247
+ game: 'cs2',
248
+ source: 'cs2_team_map_stats',
249
+ requestId: reqId,
250
+ });
251
+ }
252
+ const days = typeof args.days === 'number' ? args.days : 90;
253
+ const path = `/cs2/teams/${encodeURIComponent(rawTeam)}/map-stats`;
254
+ const res = await fetchJson(ctx, path, { query: { days } });
255
+ if (!res.ok) {
256
+ return errorEnvelope({
257
+ code: mapHttpToCode(res.status),
258
+ message: `Team map stats fetch failed: ${res.status} on ${path}`,
259
+ game: 'cs2',
260
+ source: 'cs2_team_map_stats',
261
+ requestId: reqId,
262
+ httpStatus: res.status,
263
+ });
264
+ }
265
+ const payload = unwrap(res.data);
266
+ return successEnvelope({
267
+ data: payload,
268
+ game: 'cs2',
269
+ source: 'cs2_team_map_stats',
270
+ requestId: reqId,
271
+ rateLimit: res.headers,
272
+ });
273
+ },
274
+ };
275
+ /**
276
+ * 6. cs2_utility_leaderboard
277
+ * Sub-second cached leaderboard for flashbang & utility efficiency.
278
+ */
279
+ export const cs2UtilityLeaderboard = {
280
+ name: 'cs2_utility_leaderboard',
281
+ description: 'Sub-second cached leaderboard of CS2 utility and flashbang efficiency. Returns pro player rankings for effective blind duration per flash, enemy blind time, flashes thrown, and grenade ADR.',
282
+ inputSchema: {
283
+ type: 'object',
284
+ properties: {
285
+ limit: limitSchema({ default: 20, max: 100 }),
286
+ },
287
+ additionalProperties: false,
288
+ },
289
+ annotations: READ_TOOL_ANNOTATIONS,
290
+ handler: async (args, ctx) => {
291
+ const reqId = newRequestId();
292
+ const limit = clampInt(args.limit, 20, 1, 100);
293
+ const path = `/cs2/stats/utility`;
294
+ const res = await fetchJson(ctx, path, { query: { limit } });
295
+ if (!res.ok) {
296
+ return errorEnvelope({
297
+ code: mapHttpToCode(res.status),
298
+ message: `Utility leaderboard fetch failed: ${res.status} on ${path}`,
299
+ game: 'cs2',
300
+ source: 'cs2_utility_leaderboard',
301
+ requestId: reqId,
302
+ httpStatus: res.status,
303
+ });
304
+ }
305
+ const payload = unwrap(res.data);
306
+ return successEnvelope({
307
+ data: payload,
308
+ game: 'cs2',
309
+ source: 'cs2_utility_leaderboard',
310
+ requestId: reqId,
311
+ rateLimit: res.headers,
312
+ });
313
+ },
314
+ };
315
+ /**
316
+ * 7. cs2_veto_sequence
317
+ * Map pick/ban sequence and veto history for a match.
318
+ */
319
+ export const cs2VetoSequence = {
320
+ name: 'cs2_veto_sequence',
321
+ description: 'Map pick/ban veto sequence for a CS2 match. Shows which team banned which map, map picks, and decider map in chronological order.',
322
+ inputSchema: {
323
+ type: 'object',
324
+ properties: {
325
+ matchId: stringSchema('Match ID (e.g. "2397603" or "cs2-match-2397603"). Required.', '2397603'),
326
+ },
327
+ required: ['matchId'],
328
+ additionalProperties: false,
329
+ },
330
+ annotations: READ_TOOL_ANNOTATIONS,
331
+ handler: async (args, ctx) => {
332
+ const reqId = newRequestId();
333
+ const rawId = typeof args.matchId === 'string' ? args.matchId.trim() : '';
334
+ if (!rawId) {
335
+ return errorEnvelope({
336
+ code: 'VALIDATION',
337
+ message: 'matchId is required',
338
+ game: 'cs2',
339
+ source: 'cs2_veto_sequence',
340
+ requestId: reqId,
341
+ });
342
+ }
343
+ const cleanId = rawId.replace(/^cs2-match-/, '');
344
+ const path = `/cs2/matches/${encodeURIComponent(cleanId)}/head-to-head`;
345
+ const res = await fetchJson(ctx, path);
346
+ if (!res.ok) {
347
+ return errorEnvelope({
348
+ code: mapHttpToCode(res.status),
349
+ message: `Veto sequence fetch failed: ${res.status} on ${path}`,
350
+ game: 'cs2',
351
+ source: 'cs2_veto_sequence',
352
+ requestId: reqId,
353
+ httpStatus: res.status,
354
+ });
355
+ }
356
+ const payload = unwrap(res.data);
357
+ const r = asRecord(payload);
358
+ const veto = r?.veto ?? r?.vetoSequence ?? payload;
359
+ return successEnvelope({
360
+ data: veto,
361
+ game: 'cs2',
362
+ source: 'cs2_veto_sequence',
363
+ requestId: reqId,
364
+ rateLimit: res.headers,
365
+ });
366
+ },
367
+ };
368
+ /**
369
+ * 8. cs2_roster_transfers
370
+ * Recent professional roster changes, benchings, and transfers.
371
+ */
372
+ export const cs2RosterTransfers = {
373
+ name: 'cs2_roster_transfers',
374
+ description: 'Recent professional CS2 roster changes, player benchings, stand-ins, and team transfers.',
375
+ inputSchema: {
376
+ type: 'object',
377
+ properties: {
378
+ limit: limitSchema({ default: 20, max: 100 }),
379
+ },
380
+ additionalProperties: false,
381
+ },
382
+ annotations: READ_TOOL_ANNOTATIONS,
383
+ handler: async (args, ctx) => {
384
+ const reqId = newRequestId();
385
+ const limit = clampInt(args.limit, 20, 1, 100);
386
+ const path = `/cs2/transfers/recent`;
387
+ let res = await fetchJson(ctx, path, { query: { limit } });
388
+ if (!res.ok && res.status === 404) {
389
+ res = await fetchJson(ctx, `/cs2/roster-changes/recent`, { query: { limit } });
390
+ }
391
+ if (!res.ok) {
392
+ return errorEnvelope({
393
+ code: mapHttpToCode(res.status),
394
+ message: `Roster transfers fetch failed: ${res.status} on ${path}`,
395
+ game: 'cs2',
396
+ source: 'cs2_roster_transfers',
397
+ requestId: reqId,
398
+ httpStatus: res.status,
399
+ });
400
+ }
401
+ const payload = unwrap(res.data);
402
+ return successEnvelope({
403
+ data: payload,
404
+ game: 'cs2',
405
+ source: 'cs2_roster_transfers',
406
+ requestId: reqId,
407
+ rateLimit: res.headers,
408
+ });
409
+ },
410
+ };
411
+ /** All curated CS2 tools */
412
+ export const cs2Tools = [
413
+ cs2LiveScoreboard,
414
+ cs2RoundEconomy,
415
+ cs2OpeningDuels,
416
+ cs2Clutches,
417
+ cs2TeamMapStats,
418
+ cs2UtilityLeaderboard,
419
+ cs2VetoSequence,
420
+ cs2RosterTransfers,
421
+ ];
@@ -6,8 +6,13 @@ import { playerTools } from './player.js';
6
6
  import { teamTools } from './team.js';
7
7
  import { standingsTools } from './standings.js';
8
8
  import { rankingsTools } from './rankings.js';
9
+ import { leaderboardTools } from './leaderboard.js';
9
10
  import { insightTools } from './insight.js';
10
- /** Curated outcome-tool catalog (17 tools). Order matches preferred cold-start ladder. */
11
+ import { cs2Tools } from './cs2.js';
12
+ import { oddsTools } from './odds.js';
13
+ import { tournamentTools } from './tournaments.js';
14
+ import { scheduleTools } from './schedule.js';
15
+ /** Curated outcome-tool catalog. Order matches preferred cold-start ladder. */
11
16
  export const allTools = [
12
17
  ...metaTools.filter((t) => t.name === 'list_capabilities' || t.name === 'api_health'),
13
18
  ...resolveTools,
@@ -17,7 +22,14 @@ export const allTools = [
17
22
  ...teamTools,
18
23
  ...standingsTools,
19
24
  ...rankingsTools,
25
+ ...leaderboardTools,
26
+ // Tennis depth added after the coverage audit: odds, tournament discovery and
27
+ // the daily schedule were reachable only through call_api.
28
+ ...oddsTools,
29
+ ...tournamentTools,
30
+ ...scheduleTools,
20
31
  ...insightTools,
32
+ ...cs2Tools,
21
33
  // Escape-hatch pair last: discover routes, then call one.
22
34
  ...metaTools.filter((t) => t.name === 'list_routes' || t.name === 'call_api'),
23
35
  ];