cito-mcp 0.3.19 → 0.3.21
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 -5
- package/dist/tools/cs2.js +421 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/insight.js +28 -1
- package/dist/tools/leaderboard.js +427 -0
- package/dist/tools/meta.js +201 -1
- package/dist/tools/player.js +195 -1
- package/dist/tools/rankings.js +228 -0
- package/dist/tools/standings.js +4 -2
- package/dist/tools/team.js +183 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Standalone [MCP](https://modelcontextprotocol.io) server for the [Cito esports API](https://api.citoapi.com) — **curated outcome tools** for agents building esports apps, dashboards, bots, and research flows.
|
|
4
4
|
|
|
5
|
-
**Version:** `0.
|
|
5
|
+
**Version:** `0.3.20` · **Node:** `>=20` · **Install:** `npx cito-mcp`
|
|
6
6
|
|
|
7
|
-
Primary games: **lol · cs2 · dota2 · cod · ufc
|
|
7
|
+
Primary games & sports: **lol · cs2 · dota2 · cod · ufc · tennis**. Long-tail REST stays available via `call_api`.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ Primary games: **lol · cs2 · dota2 · cod · ufc**. Fortnite and long-tail RES
|
|
|
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.
|
|
15
|
+
**v0.3 ships 18 hand-authored tools** that answer *jobs* instead of mirroring REST:
|
|
16
16
|
|
|
17
17
|
| Job | Tool |
|
|
18
18
|
| --- | --- |
|
|
@@ -21,9 +21,11 @@ v0.1 exposed ~100+ tools auto-generated from OpenAPI. Agents had to pick among t
|
|
|
21
21
|
| Build a match card / recap | `match_summary` |
|
|
22
22
|
| Deep timeline / live state | `match_details` |
|
|
23
23
|
| Team page | `team_profile` |
|
|
24
|
-
| Player / fighter
|
|
24
|
+
| Player / fighter profile | `player_profile` |
|
|
25
|
+
| Player form & win streak | `player_form` |
|
|
25
26
|
| Table / rankings | `standings` |
|
|
26
|
-
|
|
|
27
|
+
| Ranking climbers & droppers | `rankings_movers` |
|
|
28
|
+
| Pre-match briefing (with surface) | `match_preview` |
|
|
27
29
|
| Event / fight-night card | `event_card` |
|
|
28
30
|
| Rivalry record | `head_to_head` |
|
|
29
31
|
| Fighter / team photos | `event_card` · `player_profile` |
|
|
@@ -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
|
+
];
|
package/dist/tools/index.js
CHANGED
|
@@ -5,8 +5,11 @@ import { matchTools } from './match.js';
|
|
|
5
5
|
import { playerTools } from './player.js';
|
|
6
6
|
import { teamTools } from './team.js';
|
|
7
7
|
import { standingsTools } from './standings.js';
|
|
8
|
+
import { rankingsTools } from './rankings.js';
|
|
9
|
+
import { leaderboardTools } from './leaderboard.js';
|
|
8
10
|
import { insightTools } from './insight.js';
|
|
9
|
-
|
|
11
|
+
import { cs2Tools } from './cs2.js';
|
|
12
|
+
/** Curated outcome-tool catalog. Order matches preferred cold-start ladder. */
|
|
10
13
|
export const allTools = [
|
|
11
14
|
...metaTools.filter((t) => t.name === 'list_capabilities' || t.name === 'api_health'),
|
|
12
15
|
...resolveTools,
|
|
@@ -15,7 +18,10 @@ export const allTools = [
|
|
|
15
18
|
...playerTools,
|
|
16
19
|
...teamTools,
|
|
17
20
|
...standingsTools,
|
|
21
|
+
...rankingsTools,
|
|
22
|
+
...leaderboardTools,
|
|
18
23
|
...insightTools,
|
|
24
|
+
...cs2Tools,
|
|
19
25
|
// Escape-hatch pair last: discover routes, then call one.
|
|
20
26
|
...metaTools.filter((t) => t.name === 'list_routes' || t.name === 'call_api'),
|
|
21
27
|
];
|
package/dist/tools/insight.js
CHANGED
|
@@ -291,6 +291,7 @@ Prefer match_summary when match is completed; match_details for live in-game.
|
|
|
291
291
|
Do not use when: user wants final score/recap of a finished match.
|
|
292
292
|
|
|
293
293
|
Parallel-safe: yes. Upstream cost: 4–8.
|
|
294
|
+
Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
|
|
294
295
|
Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "recentLimit": 5 }`,
|
|
295
296
|
inputSchema: {
|
|
296
297
|
type: 'object',
|
|
@@ -305,6 +306,7 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
305
306
|
includeH2H: boolSchema('Include composed H2H stub.', true),
|
|
306
307
|
includeRosters: boolSchema('Include roster snippets when available.', true),
|
|
307
308
|
recentLimit: limitSchema({ default: 5, max: 15, description: 'Form window per side (default 5, max 15).' }),
|
|
309
|
+
surface: stringSchema('Optional tennis H2H surface filter: Hard, Clay, or Grass. Ignored for other games.', 'Clay'),
|
|
308
310
|
},
|
|
309
311
|
},
|
|
310
312
|
handler: async (args, ctx) => {
|
|
@@ -329,6 +331,26 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
329
331
|
const includeH2H = args.includeH2H !== false;
|
|
330
332
|
const includeRosters = args.includeRosters !== false;
|
|
331
333
|
const recentLimit = clampInt(args.recentLimit, 5, 1, 15);
|
|
334
|
+
// Optional tennis H2H surface filter (TENNIS-04 follow-through: the
|
|
335
|
+
// /tennis/h2h endpoint accepts surface=Hard|Clay|Grass). Canonicalise
|
|
336
|
+
// case so 'clay'/'CLAY' still hit; anything else is a validation error
|
|
337
|
+
// rather than a silently ignored filter.
|
|
338
|
+
const surfaceRaw = typeof args.surface === 'string' ? args.surface.trim() : '';
|
|
339
|
+
let surface;
|
|
340
|
+
if (surfaceRaw) {
|
|
341
|
+
const canon = surfaceRaw.charAt(0).toUpperCase() + surfaceRaw.slice(1).toLowerCase();
|
|
342
|
+
if (game === 'tennis' && canon !== 'Hard' && canon !== 'Clay' && canon !== 'Grass') {
|
|
343
|
+
return errorEnvelope({
|
|
344
|
+
code: 'VALIDATION',
|
|
345
|
+
message: `surface must be one of Hard, Clay, Grass (got '${surfaceRaw}')`,
|
|
346
|
+
game,
|
|
347
|
+
source: 'match_preview',
|
|
348
|
+
requestId,
|
|
349
|
+
tookMs: Date.now() - started,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
surface = canon;
|
|
353
|
+
}
|
|
332
354
|
if (!matchId && (!teamA || !teamB)) {
|
|
333
355
|
return errorEnvelope({
|
|
334
356
|
code: 'VALIDATION',
|
|
@@ -469,7 +491,10 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
469
491
|
// Tennis has a first-class H2H endpoint with the full rivalry; use it
|
|
470
492
|
// rather than composing from a short form window, which reported
|
|
471
493
|
// Sinner vs Alcaraz as never having met.
|
|
472
|
-
const
|
|
494
|
+
const h2hQuery = { player1_id: teamA, player2_id: teamB };
|
|
495
|
+
if (game === 'tennis' && surface)
|
|
496
|
+
h2hQuery.surface = surface;
|
|
497
|
+
const h2hRes = await fetchJson(ctx, '/tennis/h2h', { query: h2hQuery });
|
|
473
498
|
upstreamCalls += 1;
|
|
474
499
|
rateLimit = { ...rateLimit, ...h2hRes.headers };
|
|
475
500
|
if (h2hRes.ok) {
|
|
@@ -594,6 +619,8 @@ Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "
|
|
|
594
619
|
talkingPoints.push(`${sideBName} key names: ${names.join(', ')}`);
|
|
595
620
|
}
|
|
596
621
|
const h2hRec = asRecord(h2h);
|
|
622
|
+
if (game === 'tennis' && surface && includeH2H)
|
|
623
|
+
talkingPoints.push(`H2H filtered to ${surface} courts`);
|
|
597
624
|
if (h2hRec?.recordNote)
|
|
598
625
|
talkingPoints.push(String(h2hRec.recordNote));
|
|
599
626
|
else if (Array.isArray(h2hRec?.meetings) && h2hRec.meetings.length) {
|