cricketstudio-mcp 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1405 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
+ import { statSync } from "node:fs";
8
+ import { resolve as resolve2, dirname as dirname2 } from "node:path";
9
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
10
+ import { z } from "zod";
11
+
12
+ // src/telemetry.ts
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ function markerDir() {
17
+ return path.join(os.homedir(), ".cricketstudio-mcp");
18
+ }
19
+ function firstRunMarkerPath() {
20
+ return path.join(markerDir(), "first-run");
21
+ }
22
+ function isFirstRun() {
23
+ try {
24
+ return !fs.existsSync(firstRunMarkerPath());
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ function markFirstRunDone() {
30
+ try {
31
+ fs.mkdirSync(markerDir(), { recursive: true });
32
+ fs.writeFileSync(firstRunMarkerPath(), (/* @__PURE__ */ new Date()).toISOString(), { flag: "wx" });
33
+ } catch {
34
+ }
35
+ }
36
+ function resolveVersion() {
37
+ try {
38
+ const candidates = [
39
+ new URL("../../package.json", import.meta.url),
40
+ new URL("../package.json", import.meta.url)
41
+ ];
42
+ for (const candidate of candidates) {
43
+ try {
44
+ const raw = fs.readFileSync(candidate, "utf8");
45
+ const pkg = JSON.parse(raw);
46
+ if (pkg.version) return pkg.version;
47
+ } catch {
48
+ }
49
+ }
50
+ } catch {
51
+ }
52
+ return "0.0.0";
53
+ }
54
+ function printFirstRunMessage(version) {
55
+ const msg = `
56
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
57
+ \u2502 CricketStudio MCP v${version.padEnd(28)}\u2502
58
+ \u2502 29 tools \xB7 1,307 matches \xB7 309,992 deliveries \u2502
59
+ \u2502 \u2502
60
+ \u2502 Building something with cricket data? \u2502
61
+ \u2502 Register at cricketstudio.ai/developers for \u2502
62
+ \u2502 API updates and early hosted-transport access. \u2502
63
+ \u2502 \u2502
64
+ \u2502 Set CRICKETSTUDIO_NO_TELEMETRY=1 to silence. \u2502
65
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
66
+ `;
67
+ process.stderr.write(msg);
68
+ markFirstRunDone();
69
+ }
70
+ async function sendTelemetry(payload) {
71
+ const controller = new AbortController();
72
+ const timeout = setTimeout(() => controller.abort(), 5e3);
73
+ try {
74
+ await fetch("https://telemetry.cricketstudio.ai/mcp/startup", {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify(payload),
78
+ signal: controller.signal
79
+ });
80
+ } catch {
81
+ } finally {
82
+ clearTimeout(timeout);
83
+ }
84
+ }
85
+ async function runStartup() {
86
+ if (process.env.CRICKETSTUDIO_NO_TELEMETRY === "1") {
87
+ return;
88
+ }
89
+ const version = resolveVersion();
90
+ if (isFirstRun()) {
91
+ printFirstRunMessage(version);
92
+ }
93
+ const payload = {
94
+ version,
95
+ nodeVersion: process.version,
96
+ platform: process.platform
97
+ };
98
+ if (process.env.CRICKETSTUDIO_DEV_ID) {
99
+ payload.devId = process.env.CRICKETSTUDIO_DEV_ID;
100
+ }
101
+ sendTelemetry(payload).catch(() => void 0);
102
+ }
103
+
104
+ // src/snapshot.ts
105
+ import { readFileSync, existsSync } from "node:fs";
106
+ import { resolve, dirname } from "node:path";
107
+ import { fileURLToPath } from "node:url";
108
+ var __dirname = dirname(fileURLToPath(import.meta.url));
109
+ var PKG_ROOT = resolve(__dirname, "..");
110
+ var SNAPSHOT_DIR = resolve(PKG_ROOT, "data", "snapshot");
111
+ var _standings = null;
112
+ var _trends = null;
113
+ var _matches = null;
114
+ var _seasonStats = void 0;
115
+ function readJson(filename) {
116
+ const p = resolve(SNAPSHOT_DIR, filename);
117
+ if (!existsSync(p)) return null;
118
+ return JSON.parse(readFileSync(p, "utf8"));
119
+ }
120
+ function loadStandings() {
121
+ if (_standings !== null) return _standings;
122
+ const raw = readJson("standings.json");
123
+ _standings = raw ?? [];
124
+ return _standings;
125
+ }
126
+ function loadTrends() {
127
+ if (_trends !== null) return _trends;
128
+ const raw = readJson("trends.json");
129
+ if (!raw) {
130
+ _trends = [];
131
+ return _trends;
132
+ }
133
+ _trends = raw.map((t) => ({
134
+ id: t["id"] ?? "",
135
+ kind: t["kind"] ?? "",
136
+ hook: t["hook"] ?? t["tease"] ?? "",
137
+ bigStat: t["bigStat"],
138
+ numbers: t["numbers"]
139
+ }));
140
+ return _trends;
141
+ }
142
+ function loadMatches() {
143
+ if (_matches !== null) return _matches;
144
+ const raw = readJson("matches.json");
145
+ _matches = raw ?? [];
146
+ return _matches;
147
+ }
148
+ function loadSeasonStats() {
149
+ if (_seasonStats !== void 0) return _seasonStats;
150
+ _seasonStats = readJson("season-stats.json") ?? null;
151
+ return _seasonStats;
152
+ }
153
+ function getStandings() {
154
+ return loadStandings();
155
+ }
156
+ function getTrends() {
157
+ return loadTrends();
158
+ }
159
+ function getMatches(limit) {
160
+ const all = loadMatches();
161
+ return limit !== void 0 ? all.slice(0, limit) : all;
162
+ }
163
+ function getSeasonStats() {
164
+ return loadSeasonStats();
165
+ }
166
+
167
+ // src/tools/ipl-leaderboard.ts
168
+ var LEADERBOARD_ASPECTS = [
169
+ // ── Orange / Purple cap ─────────────────────────────────────────────
170
+ {
171
+ slug: "orange-cap",
172
+ title: "Orange cap \u2014 most runs",
173
+ description: "Career top run-scorers across the IPL historical archive (1,169 matches, 18 seasons 2007/08\u20132025). All-time orange-cap leaderboard.",
174
+ unit: "Runs",
175
+ ascending: false,
176
+ floorNote: null,
177
+ liveSeasonOnly: false
178
+ },
179
+ {
180
+ slug: "purple-cap",
181
+ title: "Purple cap \u2014 most wickets",
182
+ description: "Career top wicket-takers across the IPL historical archive (1,169 matches, 18 seasons 2007/08\u20132025).",
183
+ unit: "Wickets",
184
+ ascending: false,
185
+ floorNote: null,
186
+ liveSeasonOnly: false
187
+ },
188
+ // ── Batting aggregates ───────────────────────────────────────────────
189
+ {
190
+ slug: "strike-rate",
191
+ title: "Highest batting strike rates",
192
+ description: "Best batting strike rates (runs per 100 balls) across IPL career. Minimum sample floor applied.",
193
+ unit: "Strike rate",
194
+ ascending: false,
195
+ floorNote: "Minimum 150 balls faced.",
196
+ liveSeasonOnly: false
197
+ },
198
+ {
199
+ slug: "batting-average",
200
+ title: "Best batting average",
201
+ description: "Highest batting averages (runs per dismissal) across IPL career. Minimum sample floor applied.",
202
+ unit: "Average",
203
+ ascending: false,
204
+ floorNote: "Minimum 150 balls faced.",
205
+ liveSeasonOnly: false
206
+ },
207
+ {
208
+ slug: "most-fifties",
209
+ title: "Most half-centuries (50s)",
210
+ description: "Most innings of 50\u201399 runs in IPL career (2007/08\u20132025). Centuries (100+) are counted separately.",
211
+ unit: "50s",
212
+ ascending: false,
213
+ floorNote: "Minimum 3 innings.",
214
+ liveSeasonOnly: false
215
+ },
216
+ {
217
+ slug: "most-hundreds",
218
+ title: "Most centuries (100s)",
219
+ description: "Most innings of 100+ runs in IPL career (2007/08\u20132025). T20 centuries are rare \u2014 this is the complete list.",
220
+ unit: "100s",
221
+ ascending: false,
222
+ floorNote: null,
223
+ liveSeasonOnly: false
224
+ },
225
+ {
226
+ slug: "most-sixes",
227
+ title: "Most sixes",
228
+ description: "Top six-hitters across captured IPL matches (career).",
229
+ unit: "Sixes",
230
+ ascending: false,
231
+ floorNote: null,
232
+ liveSeasonOnly: false
233
+ },
234
+ {
235
+ slug: "most-fours",
236
+ title: "Most fours",
237
+ description: "Top boundary-hitters (fours) across captured IPL matches (career).",
238
+ unit: "Fours",
239
+ ascending: false,
240
+ floorNote: null,
241
+ liveSeasonOnly: false
242
+ },
243
+ // ── Bowling aggregates ───────────────────────────────────────────────
244
+ {
245
+ slug: "economy-leaders",
246
+ title: "Best bowling economy rates",
247
+ description: "Lowest bowling economy rates (runs per over) across IPL career. Lower is better. Minimum sample floor applied.",
248
+ unit: "Economy (RPO)",
249
+ ascending: true,
250
+ floorNote: "Minimum 240 legal deliveries bowled.",
251
+ liveSeasonOnly: false
252
+ },
253
+ {
254
+ slug: "bowling-average",
255
+ title: "Best bowling average",
256
+ description: "Lowest bowling averages (runs per wicket) across IPL career. Lower is better.",
257
+ unit: "Average (R/W)",
258
+ ascending: true,
259
+ floorNote: "Minimum 300 legal deliveries bowled.",
260
+ liveSeasonOnly: false
261
+ },
262
+ {
263
+ slug: "bowling-strike-rate",
264
+ title: "Best bowling strike rate",
265
+ description: "Fewest balls per wicket across IPL career. Lower is better \u2014 measures how quickly a bowler takes wickets.",
266
+ unit: "Balls per wicket",
267
+ ascending: true,
268
+ floorNote: "Minimum 300 legal deliveries bowled.",
269
+ liveSeasonOnly: false
270
+ },
271
+ {
272
+ slug: "most-dot-balls",
273
+ title: "Most dot balls bowled",
274
+ description: "Most legal deliveries where the batter scored 0 runs across IPL career. Measures consistent pressure bowling.",
275
+ unit: "Dot balls",
276
+ ascending: false,
277
+ floorNote: null,
278
+ liveSeasonOnly: false
279
+ },
280
+ {
281
+ slug: "runs-conceded",
282
+ title: "Most runs conceded (bowling)",
283
+ description: "Most runs conceded while bowling across IPL career \u2014 a workload indicator for high-volume bowlers.",
284
+ unit: "Runs conceded",
285
+ ascending: false,
286
+ floorNote: null,
287
+ liveSeasonOnly: false
288
+ },
289
+ {
290
+ slug: "maiden-overs",
291
+ title: "Most maiden overs",
292
+ description: "Most complete overs (6 legal deliveries) in which the bowler conceded zero runs \u2014 no batting runs, no wides, no no-ball penalties. IPL 2026.",
293
+ unit: "Maiden overs",
294
+ ascending: false,
295
+ floorNote: null,
296
+ liveSeasonOnly: true
297
+ },
298
+ {
299
+ slug: "hat-tricks",
300
+ title: "Hat-tricks",
301
+ description: "3 wickets on 3 consecutive legal deliveries by the same bowler in the same innings. Run-outs excluded. IPL 2026.",
302
+ unit: "Hat-tricks",
303
+ ascending: false,
304
+ floorNote: null,
305
+ liveSeasonOnly: true
306
+ },
307
+ // ── Milestone / speed ────────────────────────────────────────────────
308
+ {
309
+ slug: "fastest-fifty",
310
+ title: "Fastest half-centuries (fewest balls to 50)",
311
+ description: "Fewest deliveries faced to score 50 runs in an IPL 2026 innings. Ball-by-ball verified.",
312
+ unit: "Balls to 50",
313
+ ascending: true,
314
+ floorNote: null,
315
+ liveSeasonOnly: true
316
+ },
317
+ {
318
+ slug: "fastest-hundred",
319
+ title: "Fastest centuries (fewest balls to 100)",
320
+ description: "Fewest deliveries faced to score 100 runs in an IPL 2026 innings. Ball-by-ball verified.",
321
+ unit: "Balls to 100",
322
+ ascending: true,
323
+ floorNote: null,
324
+ liveSeasonOnly: true
325
+ },
326
+ // ── Powerplay batting ────────────────────────────────────────────────
327
+ {
328
+ slug: "pp-runs",
329
+ title: "Most powerplay runs (overs 1\u20136)",
330
+ description: "Most runs scored in powerplay overs (1\u20136) across IPL career. Measures powerplay batting contribution.",
331
+ unit: "PP runs",
332
+ ascending: false,
333
+ floorNote: "Minimum 150 balls faced in powerplay.",
334
+ liveSeasonOnly: false
335
+ },
336
+ {
337
+ slug: "powerplay-runs",
338
+ title: "Powerplay runs \u2014 career leaders",
339
+ description: "Aggregate powerplay runs (overs 1\u20136) across IPL career. Alias for pp-runs; same projector.",
340
+ unit: "PP runs",
341
+ ascending: false,
342
+ floorNote: "Minimum 150 balls faced in powerplay.",
343
+ liveSeasonOnly: false
344
+ },
345
+ {
346
+ slug: "pp-sr-batting",
347
+ title: "Highest powerplay batting strike rates",
348
+ description: "Best batting strike rates in the powerplay (overs 1\u20136) across IPL career.",
349
+ unit: "PP strike rate",
350
+ ascending: false,
351
+ floorNote: "Minimum 150 balls faced in powerplay overs 1\u20136.",
352
+ liveSeasonOnly: false
353
+ },
354
+ {
355
+ slug: "most-sixes-pp",
356
+ title: "Most sixes in the powerplay",
357
+ description: "Most sixes hit in powerplay overs (1\u20136) across IPL career.",
358
+ unit: "PP sixes",
359
+ ascending: false,
360
+ floorNote: "Minimum 150 balls faced in powerplay.",
361
+ liveSeasonOnly: false
362
+ },
363
+ {
364
+ slug: "most-fours-pp",
365
+ title: "Most fours in the powerplay",
366
+ description: "Most fours hit in powerplay overs (1\u20136) across IPL career.",
367
+ unit: "PP fours",
368
+ ascending: false,
369
+ floorNote: "Minimum 150 balls faced in powerplay.",
370
+ liveSeasonOnly: false
371
+ },
372
+ // ── Powerplay bowling ────────────────────────────────────────────────
373
+ {
374
+ slug: "powerplay-economy",
375
+ title: "Best powerplay economy (overs 1\u20136)",
376
+ description: "Lowest bowling economy rates in the powerplay (overs 1\u20136) across IPL career. Lower is better.",
377
+ unit: "PP econ (RPO)",
378
+ ascending: true,
379
+ floorNote: "Minimum 18 legal deliveries bowled in overs 1\u20136.",
380
+ liveSeasonOnly: false
381
+ },
382
+ {
383
+ slug: "powerplay-wickets",
384
+ title: "Most powerplay wickets (overs 1\u20136)",
385
+ description: "Most wickets taken in powerplay overs (1\u20136) across IPL career. Measures early-over bowling impact.",
386
+ unit: "PP wickets",
387
+ ascending: false,
388
+ floorNote: "Minimum 18 legal deliveries in powerplay.",
389
+ liveSeasonOnly: false
390
+ },
391
+ // ── Middle overs batting ─────────────────────────────────────────────
392
+ {
393
+ slug: "middle-runs",
394
+ title: "Most middle-overs runs (overs 7\u201315)",
395
+ description: "Most runs scored in middle overs (7\u201315) across IPL career. The anchor phase where dot balls and wickets set up the finish.",
396
+ unit: "Middle runs",
397
+ ascending: false,
398
+ floorNote: "Minimum 150 balls faced in middle overs.",
399
+ liveSeasonOnly: false
400
+ },
401
+ {
402
+ slug: "most-sixes-middle",
403
+ title: "Most sixes in the middle overs",
404
+ description: "Most sixes hit in middle overs (7\u201315) across IPL career.",
405
+ unit: "Middle sixes",
406
+ ascending: false,
407
+ floorNote: "Minimum 150 balls faced in middle overs.",
408
+ liveSeasonOnly: false
409
+ },
410
+ {
411
+ slug: "most-fours-middle",
412
+ title: "Most fours in the middle overs",
413
+ description: "Most fours hit in middle overs (7\u201315) across IPL career.",
414
+ unit: "Middle fours",
415
+ ascending: false,
416
+ floorNote: "Minimum 150 balls faced in middle overs.",
417
+ liveSeasonOnly: false
418
+ },
419
+ // ── Middle overs bowling ─────────────────────────────────────────────
420
+ {
421
+ slug: "middle-wickets",
422
+ title: "Most middle-overs wickets (overs 7\u201315)",
423
+ description: "Most wickets taken in the middle overs (7\u201315) across IPL career.",
424
+ unit: "Middle wickets",
425
+ ascending: false,
426
+ floorNote: "Minimum 240 legal deliveries in middle overs.",
427
+ liveSeasonOnly: false
428
+ },
429
+ {
430
+ slug: "middle-economy",
431
+ title: "Best middle-overs economy (overs 7\u201315)",
432
+ description: "Lowest bowling economy rate in the middle overs (7\u201315) across IPL career. Lower is better.",
433
+ unit: "Middle econ (RPO)",
434
+ ascending: true,
435
+ floorNote: "Minimum 240 legal deliveries in middle overs.",
436
+ liveSeasonOnly: false
437
+ },
438
+ // ── Death overs batting ──────────────────────────────────────────────
439
+ {
440
+ slug: "death-runs",
441
+ title: "Most death-overs runs (overs 16\u201320)",
442
+ description: "Most runs scored in death overs (16\u201320) across IPL career. The finisher's arena.",
443
+ unit: "Death runs",
444
+ ascending: false,
445
+ floorNote: "Minimum 20 balls faced in death overs.",
446
+ liveSeasonOnly: false
447
+ },
448
+ {
449
+ slug: "death-sr-batting",
450
+ title: "Highest death-overs batting strike rates",
451
+ description: "Best batting strike rates in death overs (overs 17\u201320) across IPL career.",
452
+ unit: "Death SR",
453
+ ascending: false,
454
+ floorNote: "Minimum 20 balls faced in death overs (17\u201320).",
455
+ liveSeasonOnly: false
456
+ },
457
+ {
458
+ slug: "most-sixes-death",
459
+ title: "Most sixes in death overs",
460
+ description: "Most sixes hit in death overs (16\u201320) across IPL career.",
461
+ unit: "Death sixes",
462
+ ascending: false,
463
+ floorNote: "Minimum 20 balls faced in death overs.",
464
+ liveSeasonOnly: false
465
+ },
466
+ {
467
+ slug: "most-fours-death",
468
+ title: "Most fours in death overs",
469
+ description: "Most fours hit in death overs (16\u201320) across IPL career.",
470
+ unit: "Death fours",
471
+ ascending: false,
472
+ floorNote: "Minimum 20 balls faced in death overs.",
473
+ liveSeasonOnly: false
474
+ },
475
+ // ── Death overs bowling ──────────────────────────────────────────────
476
+ {
477
+ slug: "death-wickets",
478
+ title: "Most death-overs wickets (overs 16\u201320)",
479
+ description: "Most wickets taken in death overs (16\u201320) across IPL career. Measures death-bowling impact.",
480
+ unit: "Death wickets",
481
+ ascending: false,
482
+ floorNote: "Minimum 240 legal deliveries in death overs.",
483
+ liveSeasonOnly: false
484
+ },
485
+ {
486
+ slug: "death-overs-economy",
487
+ title: "Best death-overs economy (overs 17\u201320)",
488
+ description: "Lowest bowling economy rates in death overs (overs 17\u201320) across IPL career. Lower is better.",
489
+ unit: "Death econ (RPO)",
490
+ ascending: true,
491
+ floorNote: "Minimum 240 legal deliveries bowled in overs 17\u201320.",
492
+ liveSeasonOnly: false
493
+ }
494
+ ];
495
+ function getAspectMeta(slug) {
496
+ return LEADERBOARD_ASPECTS.find((a) => a.slug === slug) ?? null;
497
+ }
498
+
499
+ // src/index.ts
500
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
501
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
502
+ var SNAPSHOT_DIR2 = resolve2(__dirname2, "..", "data", "snapshot");
503
+ var SITE = "https://players.cricketstudio.ai";
504
+ var MLC_HUB = `${SITE}/leagues/mlc`;
505
+ var IPL_HUB = `${SITE}/leagues/ipl`;
506
+ function readSnapshotJson(name) {
507
+ const p = resolve2(SNAPSHOT_DIR2, name);
508
+ if (!existsSync2(p)) return null;
509
+ return JSON.parse(readFileSync2(p, "utf8"));
510
+ }
511
+ var _players = null;
512
+ var _teams = null;
513
+ var _venues = null;
514
+ var _trends2 = null;
515
+ var _h2h = null;
516
+ var _teamH2h = null;
517
+ var _metadata = null;
518
+ var _seasonStats2 = null;
519
+ var _mlcPlayers = null;
520
+ var _mlcTeams = null;
521
+ var _mlcMatches = null;
522
+ var _mlcLeague = null;
523
+ var _mlcLeaderboards = null;
524
+ var _iplHistorical = void 0;
525
+ var _rawMatches = null;
526
+ var _rawStandings = null;
527
+ var _teamIdToCode = null;
528
+ function players() {
529
+ if (!_players) _players = readSnapshotJson("players.json") ?? {};
530
+ return _players;
531
+ }
532
+ function teams() {
533
+ if (!_teams) _teams = readSnapshotJson("teams.json") ?? [];
534
+ return _teams;
535
+ }
536
+ function venues() {
537
+ if (!_venues) _venues = readSnapshotJson("venues.json") ?? [];
538
+ return _venues;
539
+ }
540
+ function trendsList() {
541
+ if (!_trends2) _trends2 = getTrends();
542
+ return _trends2;
543
+ }
544
+ function h2hSummaries() {
545
+ if (!_h2h) _h2h = readSnapshotJson("h2h.json") ?? [];
546
+ return _h2h;
547
+ }
548
+ function teamH2h() {
549
+ if (!_teamH2h) _teamH2h = readSnapshotJson("team-h2h.json") ?? {};
550
+ return _teamH2h;
551
+ }
552
+ function metadata() {
553
+ if (!_metadata) _metadata = readSnapshotJson("metadata.json") ?? { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), counts: { players: 0, teams: 0, venues: 0, trends: 0, h2hPairs: 0 } };
554
+ return _metadata;
555
+ }
556
+ function seasonStats() {
557
+ if (!_seasonStats2) _seasonStats2 = readSnapshotJson("season-stats.json") ?? {};
558
+ return _seasonStats2;
559
+ }
560
+ function mlcPlayers() {
561
+ if (!_mlcPlayers) _mlcPlayers = readSnapshotJson("mlc-players.json") ?? {};
562
+ return _mlcPlayers;
563
+ }
564
+ function mlcTeams() {
565
+ if (!_mlcTeams) _mlcTeams = readSnapshotJson("mlc-teams.json") ?? [];
566
+ return _mlcTeams;
567
+ }
568
+ function mlcMatches() {
569
+ if (!_mlcMatches) _mlcMatches = readSnapshotJson("mlc-matches.json") ?? {};
570
+ return _mlcMatches;
571
+ }
572
+ function mlcLeague() {
573
+ if (!_mlcLeague) _mlcLeague = readSnapshotJson("mlc-league.json") ?? { seasons: [], teams: [], venues: [], playerCount: 0, totalMatches: 0, leaderboardAspects: [] };
574
+ return _mlcLeague;
575
+ }
576
+ function mlcLeaderboards() {
577
+ if (!_mlcLeaderboards) _mlcLeaderboards = readSnapshotJson("mlc-leaderboards.json") ?? {};
578
+ return _mlcLeaderboards;
579
+ }
580
+ function iplHistorical() {
581
+ if (_iplHistorical !== void 0) return _iplHistorical;
582
+ _iplHistorical = readSnapshotJson("ipl-historical.json");
583
+ return _iplHistorical;
584
+ }
585
+ function rawMatches() {
586
+ if (!_rawMatches) _rawMatches = readSnapshotJson("matches.json") ?? [];
587
+ return _rawMatches;
588
+ }
589
+ function rawMatch(id) {
590
+ return rawMatches().find((m) => m.id === id);
591
+ }
592
+ function rawStandings() {
593
+ if (!_rawStandings) _rawStandings = readSnapshotJson("standings.json") ?? [];
594
+ return _rawStandings;
595
+ }
596
+ function teamIdToCode(id) {
597
+ if (id === void 0 || id === null) return null;
598
+ if (!_teamIdToCode) _teamIdToCode = new Map(rawStandings().map((r) => [r.teamId, r.teamCode]));
599
+ return _teamIdToCode.get(id) ?? null;
600
+ }
601
+ var mlcPlayerUrl = (slug) => `${MLC_HUB}/players/${slug}`;
602
+ var mlcTeamUrl = (slug) => `${MLC_HUB}/teams/${slug}`;
603
+ var mlcMatchUrl = (id) => `${MLC_HUB}/matches/${id}`;
604
+ var mlcMatchClaimUrl = (id, kind) => `${MLC_HUB}/matches/${id}/c/${kind}`;
605
+ var mlcLeaderboardUrl = (aspect) => `${MLC_HUB}/leaderboards/${aspect}`;
606
+ function dataAsOf() {
607
+ try {
608
+ return statSync(resolve2(SNAPSHOT_DIR2, "metadata.json")).mtime.toISOString();
609
+ } catch {
610
+ return (/* @__PURE__ */ new Date()).toISOString();
611
+ }
612
+ }
613
+ function ok(payload, canonicalUrl) {
614
+ const enriched = { ...payload, dataAsOf: dataAsOf(), ...canonicalUrl ? { canonicalUrl } : {} };
615
+ return { content: [{ type: "text", text: JSON.stringify(enriched, null, 2) }] };
616
+ }
617
+ function notFound(message, canonicalUrl) {
618
+ return ok({ error: "not_found", message, hint: "Use search_players / list_trends / list_fixtures to discover valid keys." }, canonicalUrl);
619
+ }
620
+ var TOOLS = [
621
+ // ── GROUP 1: IPL 2026 Core ──────────────────────────────────────────
622
+ {
623
+ name: "get_dataset_summary",
624
+ description: "First call. Returns what CricketStudio covers \u2014 leagues (IPL 2026, IPL historical 18 seasons, MLC 2023\u20132026), corpus counts, surface URLs, 5 non-negotiables (sample-size floors, date windows, provenance, atomic claims, 4hr SLA), license. Use to ground subsequent queries against the real catalog of available entities.",
625
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
626
+ },
627
+ {
628
+ name: "search_players",
629
+ description: "Find IPL 2026 player slugs by substring match against name, slug, or team. Case-insensitive. Returns slug + fullName + team + role. Use before get_player_profile when you have a name but not a slug.",
630
+ inputSchema: { type: "object", properties: { query: { type: "string", description: "Substring to match (case-insensitive)" }, limit: { type: "number", description: "Max results (default 10, max 50)" } }, required: ["query"], additionalProperties: false }
631
+ },
632
+ {
633
+ name: "get_player_profile",
634
+ description: `Full IPL 2026 player profile + all computed claims across pillars P1\u2013P5. Each claim carries sample size, period, provenance. Use for: "How is Bumrah performing?", "What are Kohli's IPL 2026 stats?". Player slugs are kebab-case (jasprit-bumrah). Use search_players first if you need the slug.`,
635
+ inputSchema: { type: "object", properties: { playerSlug: { type: "string", description: "kebab-case player slug e.g. jasprit-bumrah" } }, required: ["playerSlug"], additionalProperties: false }
636
+ },
637
+ {
638
+ name: "get_player_pillar",
639
+ description: `One content pillar for an IPL 2026 player. P1=Match recaps, P2=Moments/milestones, P3=Form & phase (powerplay/middle/death), P4=Season comparatives, P5=Notebook/narrative. Use for: "How is Bumrah bowling at the death?" \u2192 P3. "What are Kohli's best moments?" \u2192 P2.`,
640
+ inputSchema: { type: "object", properties: { playerSlug: { type: "string" }, pillar: { type: "string", enum: ["P1", "P2", "P3", "P4", "P5"] } }, required: ["playerSlug", "pillar"], additionalProperties: false }
641
+ },
642
+ {
643
+ name: "get_standings",
644
+ description: "IPL 2026 final standings. RCB are champions. All 10 teams with Points/Won/Lost/NRR. Returns canonical URL \u2014 the live standings page refreshes within the 4-hour SLA; cite the URL for current data.",
645
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
646
+ },
647
+ {
648
+ name: "get_season_stats",
649
+ description: "IPL 2026 season leaderboard from SETU canonical aggregate. sortBy: runs, wickets, strike_rate, economy, ducks, single_digit_outs, catches, run_outs. Optional teamCode filter. Sample-size floors apply (\u226530 balls faced for SR, \u226515 balls bowled for economy).",
650
+ inputSchema: { type: "object", properties: { sortBy: { type: "string", enum: ["runs", "wickets", "strike_rate", "economy", "ducks", "single_digit_outs", "catches", "run_outs"] }, teamCode: { type: "string", description: "Optional 2\u20134 letter team code e.g. MI, RCB" }, limit: { type: "number", description: "Max rows (default 15, max 100)" } }, required: ["sortBy"], additionalProperties: false }
651
+ },
652
+ {
653
+ name: "get_match_state",
654
+ description: "Result, scoreboards, and status for one IPL 2026 match from the bundled snapshot. Returns home/away teams, innings totals, toss winner, and Man of the Match. Use list_fixtures to discover matchIds. Note: live ball-by-ball is at the canonical URL.",
655
+ inputSchema: { type: "object", properties: { matchId: { type: "string", description: "Match id (numeric string or slug form)" } }, required: ["matchId"], additionalProperties: false }
656
+ },
657
+ {
658
+ name: "get_match_recap",
659
+ description: 'Key performers and highlights for one finished IPL 2026 match \u2014 top batter, top bowler, MOTM, milestones. Use for: "Who won the MI vs RCB match?", "What happened in match 69635?". Use list_fixtures to discover matchIds.',
660
+ inputSchema: { type: "object", properties: { matchId: { type: "string" } }, required: ["matchId"], additionalProperties: false }
661
+ },
662
+ {
663
+ name: "list_fixtures",
664
+ description: "All 74 IPL 2026 fixtures with optional status/team filter. Returns id, date, home, away, venue, result. Use to discover matchIds for get_match_state and get_match_recap.",
665
+ inputSchema: { type: "object", properties: { status: { type: "string", enum: ["all", "finished", "upcoming"], description: "Default all" }, team: { type: "string", description: "Team slug or code filter" }, limit: { type: "number", description: "Default 20, max 74" } }, additionalProperties: false }
666
+ },
667
+ {
668
+ name: "get_trend",
669
+ description: "One cross-fixture trend insight by stable id. Each trend carries bigStat, hook, and supporting numbers[]. Use list_trends to discover ids.",
670
+ inputSchema: { type: "object", properties: { trendId: { type: "string" } }, required: ["trendId"], additionalProperties: false }
671
+ },
672
+ {
673
+ name: "list_trends",
674
+ description: "All IPL 2026 cross-fixture trends, optionally filtered by kind: conditional, momentum, venue, toss, anomaly. Returns id + kind + hook + canonicalUrl per row.",
675
+ inputSchema: { type: "object", properties: { kind: { type: "string", description: "Filter by kind: conditional / momentum / venue / toss / anomaly" }, limit: { type: "number", description: "Default 30, max 100" } }, additionalProperties: false }
676
+ },
677
+ {
678
+ name: "get_player_h2h",
679
+ description: "Batter-vs-bowler head-to-head record in IPL 2026. Sample-size floor: \u22655 deliveries faced. Returns deliveries, runs, SR, dismissals, and canonical URL. Both slugs are kebab-case.",
680
+ inputSchema: { type: "object", properties: { batterSlug: { type: "string" }, bowlerSlug: { type: "string" } }, required: ["batterSlug", "bowlerSlug"], additionalProperties: false }
681
+ },
682
+ {
683
+ name: "get_team_profile",
684
+ description: "IPL 2026 team metadata + canonical URL for the full server-rendered profile (record, at-home/away splits, phase strengths). Slugs: mi, csk, rcb, srh, kkr, dc, pbks, rr, lsg, gt.",
685
+ inputSchema: { type: "object", properties: { teamSlug: { type: "string", description: "Team slug (mi, csk, rcb, srh, kkr, dc, pbks, rr, lsg, gt)" } }, required: ["teamSlug"], additionalProperties: false }
686
+ },
687
+ {
688
+ name: "get_venue_hub",
689
+ description: "Venue metadata + canonical URL for the full hub page (par 1st-innings score, toss-decision split, phase scoring patterns, recent matches). Sample-size floor: \u22653 fixtures. Use list_fixtures to find venue slugs.",
690
+ inputSchema: { type: "object", properties: { venueSlug: { type: "string" } }, required: ["venueSlug"], additionalProperties: false }
691
+ },
692
+ {
693
+ name: "list_atomic_claims",
694
+ description: `Filtered query across the full IPL 2026 atomic-claim corpus. Each claim is a single-sentence retrieval target with provenance, sample size, and canonicalUrl. Answers: "What are RCB's best claims this season?", "Show me Bumrah's P3 claims". Filter by player name, team code, or pillar.`,
695
+ inputSchema: { type: "object", properties: { player: { type: "string", description: "Player name substring or slug" }, team: { type: "string", description: "Team name or code" }, pillar: { type: "string", enum: ["P1", "P2", "P3", "P4", "P5"] }, limit: { type: "number", description: "Default 25, max 200" } }, additionalProperties: false }
696
+ },
697
+ {
698
+ name: "get_team_h2h",
699
+ description: "Team-vs-team head-to-head record across IPL 2026: matches, wins each way, no-results, recent meetings. Pass slugs in any order (mi, csk, rcb, srh, kkr, dc, pbks, rr, lsg, gt). Returns atomic lead claim + canonical URL.",
700
+ inputSchema: { type: "object", properties: { teamSlugA: { type: "string" }, teamSlugB: { type: "string" } }, required: ["teamSlugA", "teamSlugB"], additionalProperties: false }
701
+ },
702
+ {
703
+ name: "get_partnerships",
704
+ description: "Partnership stats for an IPL 2026 player \u2014 top stand partners, average partnership runs, most productive wicket-stand. Returns available data from the snapshot or redirects to the canonical player page for the full view.",
705
+ inputSchema: { type: "object", properties: { playerSlug: { type: "string" } }, required: ["playerSlug"], additionalProperties: false }
706
+ },
707
+ {
708
+ name: "compare_players",
709
+ description: "Side-by-side comparison of 2\u20138 IPL 2026 players: team, role, claim count per pillar (P1\u2013P5), headline claim. Returns a canonical /compare/players?slugs=\u2026 URL. Use search_players to resolve slugs first.",
710
+ inputSchema: { type: "object", properties: { playerSlugs: { type: "array", items: { type: "string" }, minItems: 2, maxItems: 8 } }, required: ["playerSlugs"], additionalProperties: false }
711
+ },
712
+ {
713
+ name: "get_dismissal_analysis",
714
+ description: "IPL 2026 dismissal pattern analysis for a batter \u2014 dismissed by pace vs spin, powerplay vs death, most frequent dismissal modes. Returns available snapshot data or canonical URL for the full breakdown.",
715
+ inputSchema: { type: "object", properties: { playerSlug: { type: "string" } }, required: ["playerSlug"], additionalProperties: false }
716
+ },
717
+ {
718
+ name: "get_fielding_stats",
719
+ description: "IPL 2026 fielding: catches, run-out assists, total dismissals. Pass playerSlug for a single player, omit for the full leaderboard. Aggregated from the SETU canonical snapshot.",
720
+ inputSchema: { type: "object", properties: { playerSlug: { type: "string", description: "Omit for leaderboard" }, limit: { type: "number", description: "Leaderboard rows (default 15)" } }, additionalProperties: false }
721
+ },
722
+ // ── GROUP 2: MLC ────────────────────────────────────────────────────
723
+ {
724
+ name: "get_mlc_dataset_summary",
725
+ description: "First call for Major League Cricket (MLC) coverage. Returns seasons covered (2023\u20132026), corpus stats, surface URLs, 14 leaderboard aspects, and Cricsheet CC BY 3.0 attribution. MLC is distinct from IPL and lives under /leagues/mlc.",
726
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
727
+ },
728
+ {
729
+ name: "search_mlc_players",
730
+ description: 'Find MLC player slugs by substring match against name or slug. Cricsheet uses initials format ("F du Plessis" \u2192 f-du-plessis). Use before get_mlc_player_profile.',
731
+ inputSchema: { type: "object", properties: { query: { type: "string" }, limit: { type: "number", description: "Default 10, max 50" } }, required: ["query"], additionalProperties: false }
732
+ },
733
+ {
734
+ name: "get_mlc_player_profile",
735
+ description: "MLC player career profile: batting + bowling aggregates, per-season breakdown, identity bridge (Wikidata / ESPNcricinfo). Slug is kebab-case e.g. f-du-plessis. Use search_mlc_players to discover slugs.",
736
+ inputSchema: { type: "object", properties: { playerSlug: { type: "string" } }, required: ["playerSlug"], additionalProperties: false }
737
+ },
738
+ {
739
+ name: "get_mlc_team_profile",
740
+ description: "One of 6 MLC franchises: los-angeles-knight-riders, mi-new-york, san-francisco-unicorns, seattle-orcas, texas-super-kings, washington-freedom. Returns seasons, match count, and hub URL.",
741
+ inputSchema: { type: "object", properties: { teamSlug: { type: "string" } }, required: ["teamSlug"], additionalProperties: false }
742
+ },
743
+ {
744
+ name: "get_mlc_match",
745
+ description: 'Full detail for one MLC match: teams, venue, toss, result, innings summary, officials, player of the match, plus available atomic claim cards. matchId is a Cricsheet id (e.g. "1381361"). Use list_mlc_matches to discover ids.',
746
+ inputSchema: { type: "object", properties: { matchId: { type: "string" } }, required: ["matchId"], additionalProperties: false }
747
+ },
748
+ {
749
+ name: "get_mlc_match_claim",
750
+ description: "One atomic claim card from an MLC match. Kinds: top-batter, top-bowler, biggest-partnership, pp-control, death-domination. Permanent citable URL at /leagues/mlc/matches/{id}/c/{kind}. Sample-size floors enforced.",
751
+ inputSchema: { type: "object", properties: { matchId: { type: "string" }, kind: { type: "string", enum: ["top-batter", "top-bowler", "biggest-partnership", "pp-control", "death-domination"] } }, required: ["matchId", "kind"], additionalProperties: false }
752
+ },
753
+ {
754
+ name: "list_mlc_matches",
755
+ description: "List MLC matches, optionally filtered by season (2023/2024/2025) or team slug. Returns id, date, teams, venue, result, canonicalUrl per row. Use to discover matchIds for get_mlc_match.",
756
+ inputSchema: { type: "object", properties: { season: { type: "string", description: "2023, 2024, or 2025" }, teamSlug: { type: "string" }, limit: { type: "number", description: "Default 30, max 200" } }, additionalProperties: false }
757
+ },
758
+ {
759
+ name: "list_mlc_leaderboards",
760
+ description: "Top-N rows of one MLC leaderboard aspect. 14 aspects include: orange-cap, purple-cap, strike-rate, economy-leaders, most-sixes, most-fours, top-knocks, best-bowling, powerplay-strike-rate, death-overs-economy. Call get_mlc_dataset_summary for the full aspect list. Sample-size floors enforced.",
761
+ inputSchema: { type: "object", properties: { aspect: { type: "string", description: "Leaderboard aspect slug e.g. orange-cap" }, limit: { type: "number", description: "Default 20, max 100" } }, required: ["aspect"], additionalProperties: false }
762
+ },
763
+ // ── GROUP 3: IPL Historical ─────────────────────────────────────────
764
+ {
765
+ name: "get_ipl_leaderboard",
766
+ description: 'IPL historical leaderboard from the 18-season Cricsheet corpus (2007/08\u20132025). 35+ aspects: orange-cap, purple-cap, most-sixes, most-fours, strike-rate, economy-leaders, most-matches, most-fifties, most-hundreds, best-bowling-avg, most-ducks, powerplay-economy, death-sr, and per-season variants. Pass season to scope to one year (e.g. "ipl-2024"). Returns canonical URL at /leagues/ipl/leaderboards/{aspect}.',
767
+ inputSchema: { type: "object", properties: { aspect: { type: "string", description: "Leaderboard aspect e.g. orange-cap, purple-cap, most-sixes, economy-leaders" }, season: { type: "string", description: "Optional season slug e.g. ipl-2024 (omit for all-time)" }, limit: { type: "number", description: "Default 20, max 100" } }, required: ["aspect"], additionalProperties: false }
768
+ }
769
+ ];
770
+ var validators = {
771
+ get_dataset_summary: z.object({}).strict(),
772
+ search_players: z.object({ query: z.string(), limit: z.number().optional() }).strict(),
773
+ get_player_profile: z.object({ playerSlug: z.string() }).strict(),
774
+ get_player_pillar: z.object({ playerSlug: z.string(), pillar: z.enum(["P1", "P2", "P3", "P4", "P5"]) }).strict(),
775
+ get_standings: z.object({}).strict(),
776
+ get_season_stats: z.object({ sortBy: z.enum(["runs", "wickets", "strike_rate", "economy", "ducks", "single_digit_outs", "catches", "run_outs"]), teamCode: z.string().optional(), limit: z.number().optional() }).strict(),
777
+ get_match_state: z.object({ matchId: z.string() }).strict(),
778
+ get_match_recap: z.object({ matchId: z.string() }).strict(),
779
+ list_fixtures: z.object({ status: z.enum(["all", "finished", "upcoming"]).optional(), team: z.string().optional(), limit: z.number().optional() }).strict(),
780
+ get_trend: z.object({ trendId: z.string() }).strict(),
781
+ list_trends: z.object({ kind: z.string().optional(), limit: z.number().optional() }).strict(),
782
+ get_player_h2h: z.object({ batterSlug: z.string(), bowlerSlug: z.string() }).strict(),
783
+ get_team_profile: z.object({ teamSlug: z.string() }).strict(),
784
+ get_venue_hub: z.object({ venueSlug: z.string() }).strict(),
785
+ list_atomic_claims: z.object({ player: z.string().optional(), team: z.string().optional(), pillar: z.enum(["P1", "P2", "P3", "P4", "P5"]).optional(), limit: z.number().optional() }).strict(),
786
+ get_team_h2h: z.object({ teamSlugA: z.string(), teamSlugB: z.string() }).strict(),
787
+ get_partnerships: z.object({ playerSlug: z.string() }).strict(),
788
+ compare_players: z.object({ playerSlugs: z.array(z.string()).min(2).max(8) }).strict(),
789
+ get_dismissal_analysis: z.object({ playerSlug: z.string() }).strict(),
790
+ get_fielding_stats: z.object({ playerSlug: z.string().optional(), limit: z.number().optional() }).strict(),
791
+ get_mlc_dataset_summary: z.object({}).strict(),
792
+ search_mlc_players: z.object({ query: z.string(), limit: z.number().optional() }).strict(),
793
+ get_mlc_player_profile: z.object({ playerSlug: z.string() }).strict(),
794
+ get_mlc_team_profile: z.object({ teamSlug: z.string() }).strict(),
795
+ get_mlc_match: z.object({ matchId: z.string() }).strict(),
796
+ get_mlc_match_claim: z.object({ matchId: z.string(), kind: z.enum(["top-batter", "top-bowler", "biggest-partnership", "pp-control", "death-domination"]) }).strict(),
797
+ list_mlc_matches: z.object({ season: z.string().optional(), teamSlug: z.string().optional(), limit: z.number().optional() }).strict(),
798
+ list_mlc_leaderboards: z.object({ aspect: z.string(), limit: z.number().optional() }).strict(),
799
+ get_ipl_leaderboard: z.object({ aspect: z.string(), season: z.string().optional(), limit: z.number().optional() }).strict()
800
+ };
801
+ function handleDatasetSummary() {
802
+ const md = metadata();
803
+ return ok({
804
+ overview: "CricketStudio publishes citation-grade cricket data \u2014 atomic claims with provenance, sample-size floors, and stable canonical URLs. Covers IPL 2026 (complete \u2014 RCB champions, 74 matches), IPL historical (18 seasons, 2007/08\u20132025), and Major League Cricket (2023\u20132026). Free to read. Free to cite.",
805
+ coverage: {
806
+ ipl2026: { season: "IPL 2026", ...md.counts },
807
+ iplHistorical: { seasons: 18, description: "2007/08\u20132025, Cricsheet corpus, 1,169 matches" },
808
+ mlc: md.counts.mlc ?? { players: 0, teams: 6, matches: 0, leaderboards: 14 },
809
+ totalMatches: 1307,
810
+ totalDeliveries: 309992
811
+ },
812
+ surfaces: {
813
+ players: `${SITE}/players/{slug}`,
814
+ teams: `${SITE}/teams/{slug}`,
815
+ teamH2h: `${SITE}/teams/{a}/vs/{b}`,
816
+ venues: `${SITE}/venues/{slug}`,
817
+ matches: `${SITE}/matches/{fixtureId}`,
818
+ trends: `${SITE}/trends/{trendId}`,
819
+ h2h: `${SITE}/h2h/{batter-slug}-vs-{bowler-slug}`,
820
+ standings: `${SITE}/standings`,
821
+ iplHub: IPL_HUB,
822
+ mlcHub: MLC_HUB,
823
+ sitemap: `${SITE}/sitemap.xml`,
824
+ llmsTxt: `${SITE}/llms.txt`
825
+ },
826
+ otherLeagues: {
827
+ iplHistorical: "Full pre-2026 IPL corpus at /leagues/ipl \u2014 18 seasons, per-season hubs at /season/ipl-{year}. Use get_ipl_leaderboard for the 35-aspect leaderboard.",
828
+ mlc: "Major League Cricket at /leagues/mlc \u2014 2023\u20132026, Cricsheet CC BY 3.0. Use get_mlc_dataset_summary to start."
829
+ },
830
+ fiveNonNegotiables: [
831
+ "Sample-size floors enforced (\u226530 batting balls, \u226515 bowling deliveries, \u22653 venue fixtures, \u22655 H2H deliveries)",
832
+ "Date windows explicit on every claim",
833
+ "Provenance back to ball-by-ball (deterministic \u2014 numbers never pass through an LLM)",
834
+ "Atomic claim format under 30 words",
835
+ "Sub-4-hour data-freshness SLA for IPL 2026 (95th percentile)"
836
+ ],
837
+ license: { data: "CC BY 4.0", tools: "MIT", attribution: `CricketStudio \xB7 ${SITE}` },
838
+ snapshot: { generatedAt: md.generatedAt }
839
+ }, `${SITE}/`);
840
+ }
841
+ function handleSearchPlayers(args) {
842
+ const q = args.query.toLowerCase().trim();
843
+ const limit = Math.max(1, Math.min(50, args.limit ?? 10));
844
+ const results = Object.values(players()).filter((p) => p.slug.toLowerCase().includes(q) || p.fullName.toLowerCase().includes(q) || (p.team || "").toLowerCase().includes(q)).slice(0, limit).map((p) => ({ slug: p.slug, fullName: p.fullName, team: p.team, role: p.role, canonicalUrl: `${SITE}/players/${p.slug}` }));
845
+ return ok({ query: args.query, count: results.length, results });
846
+ }
847
+ function handlePlayerProfile(args) {
848
+ const p = players()[args.playerSlug];
849
+ if (!p) return notFound(`No player with slug "${args.playerSlug}". Try search_players first.`);
850
+ const headline = p.claims.find((c) => c.id === p.headlineClaimId) ?? p.claims[0] ?? null;
851
+ return ok({ slug: p.slug, fullName: p.fullName, team: p.team, role: p.role, sameAs: p.sameAs || {}, headlineClaim: headline, claims: p.claims, claimCount: p.claims.length }, `${SITE}/players/${p.slug}`);
852
+ }
853
+ function handlePlayerPillar(args) {
854
+ const p = players()[args.playerSlug];
855
+ if (!p) return notFound(`No player with slug "${args.playerSlug}".`);
856
+ const pillarNames = { P1: "Match recaps", P2: "Moments", P3: "Form & phase", P4: "Season comparatives", P5: "Notebook" };
857
+ const claims = p.claims.filter((c) => c.pillar === args.pillar);
858
+ return ok({ slug: p.slug, fullName: p.fullName, pillar: args.pillar, pillarName: pillarNames[args.pillar] ?? args.pillar, claims, claimCount: claims.length }, `${SITE}/players/${p.slug}`);
859
+ }
860
+ function handleStandings() {
861
+ const rows = getStandings();
862
+ if (rows.length === 0) {
863
+ return ok({ note: "Standings not bundled in this snapshot. See the canonical URL for the live points table." }, `${SITE}/standings`);
864
+ }
865
+ const table = rows.map((r, i) => ({
866
+ position: i + 1,
867
+ teamCode: r.teamCode,
868
+ played: r.played,
869
+ won: r.won,
870
+ lost: r.lost,
871
+ points: r.points,
872
+ nrr: typeof r.nrr === "number" ? Math.round(r.nrr * 1e3) / 1e3 : r.nrr
873
+ }));
874
+ return ok({
875
+ season: "IPL 2026",
876
+ status: "complete",
877
+ champion: "Royal Challengers Bengaluru (RCB)",
878
+ note: "IPL 2026 final league points table (round-robin stage \u2014 playoff knockouts excluded). RCB won the title. Top 4 qualified for the playoffs.",
879
+ table,
880
+ provenance: { source: "CricketStudio ball-by-ball aggregation", window: "IPL 2026 league stage" }
881
+ }, `${SITE}/standings`);
882
+ }
883
+ function handleSeasonStats(args) {
884
+ const stats = seasonStats();
885
+ const limit = Math.max(1, Math.min(100, args.limit ?? 15));
886
+ const specMap = {
887
+ runs: { block: "batting", field: "runs", descending: true },
888
+ wickets: { block: "bowling", field: "wickets", descending: true },
889
+ strike_rate: { block: "batting", field: "sr", descending: true, floorBalls: 30, floorDesc: "\u226530 balls faced" },
890
+ economy: { block: "bowling", field: "econ", descending: false, floorBalls: 15, floorDesc: "\u226515 balls bowled" },
891
+ ducks: { block: "batting", field: "ducks", descending: true },
892
+ single_digit_outs: { block: "batting", field: "singleDigitOuts", descending: true },
893
+ catches: { block: "fielding", field: "catches", descending: true },
894
+ run_outs: { block: "fielding", field: "runOutAssists", descending: true }
895
+ };
896
+ const spec = specMap[args.sortBy];
897
+ if (!spec) return notFound(`Unknown sortBy "${args.sortBy}".`);
898
+ const all = Object.entries(stats.bySlug ?? {});
899
+ const tc = args.teamCode ? args.teamCode.toUpperCase() : null;
900
+ const rows = all.filter(([, p]) => p && p[spec.block]).filter(([, p]) => tc ? (p.teamCode || "").toUpperCase() === tc : true).filter(([, p]) => p[spec.block][spec.field] !== void 0 && p[spec.block][spec.field] !== null).filter(([, p]) => spec.floorBalls ? Number(p[spec.block].balls || 0) >= spec.floorBalls : true).map(([slug, p]) => ({ slug, fullName: p.fullName, teamCode: p.teamCode, role: p.role, matches: p[spec.block].matches, balls: p[spec.block].balls, [spec.field]: p[spec.block][spec.field], canonicalUrl: `${SITE}/players/${slug}` })).sort((a, b) => spec.descending ? Number(b[spec.field]) - Number(a[spec.field]) : Number(a[spec.field]) - Number(b[spec.field])).slice(0, limit);
901
+ return ok({ sortBy: args.sortBy, sampleSizeFloor: spec.floorDesc, teamCode: args.teamCode, count: rows.length, rows }, `${SITE}/season/ipl-2026/${args.sortBy.replace(/_/g, "-")}`);
902
+ }
903
+ function handleMatchState(args) {
904
+ const m = rawMatch(args.matchId);
905
+ if (!m) return notFound(`No match "${args.matchId}" in snapshot. Use list_fixtures to discover valid ids.`, `${SITE}/matches/${args.matchId}`);
906
+ const tossCode = teamIdToCode(m.tossWinnerId);
907
+ const toss = tossCode ? { wonByCode: tossCode, elected: m.elected ?? null } : { tossElected: m.elected ?? null };
908
+ return ok({
909
+ fixtureId: m.id,
910
+ home: m.home,
911
+ homeName: m.homeName ?? null,
912
+ away: m.away,
913
+ awayName: m.awayName ?? null,
914
+ date: m.date,
915
+ venue: m.venue ?? null,
916
+ status: m.status,
917
+ result: m.result ?? null,
918
+ homeScore: m.homeScore ?? null,
919
+ awayScore: m.awayScore ?? null,
920
+ toss
921
+ }, `${SITE}/matches/${m.id}`);
922
+ }
923
+ function handleMatchRecap(args) {
924
+ const m = rawMatch(args.matchId);
925
+ if (!m) return notFound(`No match "${args.matchId}" in snapshot. Use list_fixtures to discover valid ids.`, `${SITE}/matches/${args.matchId}`);
926
+ const recap = `${m.result ?? "Result pending"}. ${m.home} ${m.homeScore ?? "\u2014"} vs ${m.away} ${m.awayScore ?? "\u2014"} at ${m.venue ?? "venue TBC"}.`;
927
+ return ok({
928
+ fixtureId: m.id,
929
+ recap,
930
+ home: m.home,
931
+ away: m.away,
932
+ date: m.date,
933
+ venue: m.venue ?? null,
934
+ result: m.result ?? null,
935
+ homeScore: m.homeScore ?? null,
936
+ awayScore: m.awayScore ?? null,
937
+ note: "Full 6-card recap pack (MOTM, top batter/bowler, milestones) at the canonical URL."
938
+ }, `${SITE}/matches/${m.id}`);
939
+ }
940
+ function handleListFixtures(args) {
941
+ const limit = Math.max(1, Math.min(74, args.limit ?? 20));
942
+ let all = getMatches();
943
+ if (args.status && args.status !== "all") {
944
+ const s = args.status.toLowerCase();
945
+ all = all.filter((m) => m.status.toLowerCase().includes(s));
946
+ }
947
+ if (args.team) {
948
+ const t = args.team.toLowerCase();
949
+ all = all.filter((m) => m.home.toLowerCase().includes(t) || m.away.toLowerCase().includes(t));
950
+ }
951
+ const rows = all.slice(0, limit).map((m) => ({ id: m.id, date: m.date, home: m.home, away: m.away, status: m.status, result: m.result ?? null, canonicalUrl: `${SITE}/matches/${m.id}` }));
952
+ return ok({ season: "IPL 2026", totalMatching: all.length, showing: rows.length, fixtures: rows }, `${SITE}/matches`);
953
+ }
954
+ function handleTrend(args) {
955
+ const t = trendsList().find((x) => x.id === args.trendId);
956
+ if (!t) return notFound(`No trend "${args.trendId}". Use list_trends to discover ids.`);
957
+ return ok({ ...t }, `${SITE}/trends/${t.id}`);
958
+ }
959
+ function handleListTrends(args) {
960
+ const limit = Math.max(1, Math.min(100, args.limit ?? 30));
961
+ let rows = trendsList();
962
+ if (args.kind) {
963
+ const k = args.kind.toLowerCase();
964
+ rows = rows.filter((t) => (t.kind || "").toLowerCase() === k);
965
+ }
966
+ return ok({ count: rows.length, kind: args.kind ?? "all", trends: rows.slice(0, limit).map((t) => ({ ...t, canonicalUrl: `${SITE}/trends/${t.id}` })) });
967
+ }
968
+ function handlePlayerH2H(args) {
969
+ const slug = `${args.batterSlug}-vs-${args.bowlerSlug}`;
970
+ const h = h2hSummaries().find((x) => x.slug === slug);
971
+ if (!h) {
972
+ return ok({
973
+ error: "not_found",
974
+ message: `No H2H record for "${slug}" in the bundled snapshot. This means one of: (a) the pair is below the \u22655-deliveries sample floor in IPL 2026, or (b) the pair is outside the bundled top-2000 matchups (the snapshot carries the highest-frequency pairs). The full head-to-head, if it exists, is rendered at the canonical URL.`,
975
+ pair: { batterSlug: args.batterSlug, bowlerSlug: args.bowlerSlug },
976
+ sampleSizeFloor: "\u22655 deliveries faced",
977
+ hint: "Use search_players to confirm both slugs, then retry. Canonical URL resolves the pair if it cleared the floor."
978
+ }, `${SITE}/h2h/${slug}`);
979
+ }
980
+ const runs = h.runs ?? 0;
981
+ const deliveries = h.deliveries ?? 0;
982
+ const dismissals = h.dismissals ?? 0;
983
+ const claim = `${h.batterName} scored ${runs} off ${deliveries} balls against ${h.bowlerName}${dismissals > 0 ? `, dismissed ${dismissals} time${dismissals === 1 ? "" : "s"}` : " (not dismissed)"} in IPL 2026.`;
984
+ return ok({ claim, batter: { slug: h.batterSlug, name: h.batterName }, bowler: { slug: h.bowlerSlug, name: h.bowlerName }, deliveries, runs, strikeRate: h.strikeRate ?? null, fours: h.fours ?? null, sixes: h.sixes ?? null, dotBalls: h.dotBalls ?? null, dismissals, window: "IPL 2026 to date", sampleSize: `${deliveries} deliveries`, sampleSizeFloor: "\u22655 deliveries faced", source: "CricketStudio ball-by-ball aggregation" }, `${SITE}/h2h/${slug}`);
985
+ }
986
+ function handleTeamProfile(args) {
987
+ const slug = args.teamSlug.toLowerCase();
988
+ const t = teams().find((x) => x.slug === slug || x.code.toLowerCase() === slug);
989
+ if (!t) return notFound(`No team "${args.teamSlug}". Valid slugs: ${teams().map((x) => x.slug).join(", ")}`);
990
+ const code = t.code.toLowerCase();
991
+ const standings = getStandings();
992
+ const idx = standings.findIndex((r) => (r.teamCode || "").toLowerCase() === code);
993
+ const sr = idx >= 0 ? standings[idx] : null;
994
+ const record = sr ? { position: idx + 1, played: sr.played, won: sr.won, lost: sr.lost, points: sr.points, nrr: typeof sr.nrr === "number" ? Math.round(sr.nrr * 1e3) / 1e3 : sr.nrr } : null;
995
+ const stats = getSeasonStats();
996
+ const bySlug = stats?.bySlug ?? {};
997
+ const squad = Object.entries(bySlug).filter(([, p]) => (p.teamCode || "").toLowerCase() === code);
998
+ const topBatters = squad.map(([s, p]) => ({ slug: s, name: p.fullName, runs: p.batting?.runs ?? 0, sr: p.batting?.sr ?? null, fifties: p.batting?.fifties ?? 0, hundreds: p.batting?.hundreds ?? 0 })).filter((r) => r.runs > 0).sort((a, b) => b.runs - a.runs).slice(0, 3);
999
+ const topBowlers = squad.map(([s, p]) => ({ slug: s, name: p.fullName, wickets: p.bowling?.wickets ?? 0, econ: p.bowling?.econ ?? null })).filter((r) => r.wickets > 0).sort((a, b) => b.wickets - a.wickets).slice(0, 3);
1000
+ return ok({
1001
+ slug: t.slug,
1002
+ name: t.name,
1003
+ code: t.code,
1004
+ wikidataQid: t.wikidataQid,
1005
+ season: "IPL 2026",
1006
+ seasonStatus: "complete",
1007
+ record,
1008
+ squadSize: squad.length,
1009
+ topBatters,
1010
+ topBowlers,
1011
+ note: "IPL 2026 season-complete team summary. Full per-phase splits + opponent breakdowns at the canonical URL."
1012
+ }, `${SITE}/teams/${t.slug}`);
1013
+ }
1014
+ function handleVenueHub(args) {
1015
+ const v = venues().find((x) => x.slug === args.venueSlug);
1016
+ if (!v) return notFound(`No venue "${args.venueSlug}". Use list_fixtures to find valid venue slugs.`);
1017
+ const normalize = (s) => (s || "").toLowerCase().replace(/[. ]/g, "").replace(/stadium/g, "");
1018
+ const target = normalize(v.name);
1019
+ const here = rawMatches().filter((m) => normalize(m.venue) === target);
1020
+ let chaseEligible = 0;
1021
+ let chaseWins = 0;
1022
+ for (const m of here) {
1023
+ if (!m.tossWinnerId || !m.winnerId || !m.elected) continue;
1024
+ const chasingTeamId = m.elected === "bowling" ? m.tossWinnerId : null;
1025
+ if (chasingTeamId === null) continue;
1026
+ chaseEligible += 1;
1027
+ if (m.winnerId === chasingTeamId) chaseWins += 1;
1028
+ }
1029
+ const recentResults = here.slice().sort((a, b) => (b.date || "").localeCompare(a.date || "")).slice(0, 3).map((m) => ({ date: m.date, teams: `${m.home} vs ${m.away}`, result: m.result ?? null }));
1030
+ return ok({
1031
+ slug: v.slug,
1032
+ name: v.name,
1033
+ city: v.city ?? null,
1034
+ geo: v.geo,
1035
+ matchCount: v.matchCount ?? here.length,
1036
+ fixturesCaptured: here.length,
1037
+ ...chaseEligible >= 3 ? { chaseWinRate: Math.round(chaseWins / chaseEligible * 100) / 100, chaseSample: `${chaseEligible} fixtures where toss-winner bowled first` } : {},
1038
+ recentResults,
1039
+ note: "Per-phase par scores + toss-decision splits at the canonical URL. Sample floor: \u22653 fixtures."
1040
+ }, `${SITE}/venues/${v.slug}`);
1041
+ }
1042
+ function handleListAtomicClaims(args) {
1043
+ const limit = Math.max(1, Math.min(200, args.limit ?? 25));
1044
+ const out = [];
1045
+ for (const p of Object.values(players())) {
1046
+ if (args.player && !p.fullName.toLowerCase().includes(args.player.toLowerCase()) && p.slug !== args.player) continue;
1047
+ if (args.team && (p.team || "").toLowerCase() !== args.team.toLowerCase()) continue;
1048
+ for (const c of p.claims) {
1049
+ if (args.pillar && c.pillar !== args.pillar) continue;
1050
+ out.push({ player: p.fullName, playerSlug: p.slug, team: p.team, ...c, canonicalUrl: `${SITE}/players/${p.slug}` });
1051
+ if (out.length >= limit) break;
1052
+ }
1053
+ if (out.length >= limit) break;
1054
+ }
1055
+ return ok({ count: out.length, claims: out });
1056
+ }
1057
+ function handleTeamH2H(args) {
1058
+ const a = args.teamSlugA.toLowerCase().trim();
1059
+ const b = args.teamSlugB.toLowerCase().trim();
1060
+ if (a === b) return notFound("Team H2H needs two different team slugs.");
1061
+ const sorted = [a, b].sort();
1062
+ const key = `${sorted[0]}-vs-${sorted[1]}`;
1063
+ const rec = teamH2h()[key];
1064
+ if (!rec) return notFound(`No captured IPL 2026 meetings between "${a}" and "${b}". Valid: ${teams().map((t) => t.slug).join(", ")}.`, `${SITE}/teams/${a}/vs/${b}`);
1065
+ const callerIsA = rec.a.slug === a;
1066
+ const first = callerIsA ? rec.a : rec.b;
1067
+ const second = callerIsA ? rec.b : rec.a;
1068
+ const fw = callerIsA ? rec.aWon : rec.bWon;
1069
+ const sw = callerIsA ? rec.bWon : rec.aWon;
1070
+ let claim;
1071
+ if (fw > sw) claim = `${first.code} lead ${second.code} ${fw}\u2013${sw} across ${rec.matches} captured IPL 2026 meeting${rec.matches === 1 ? "" : "s"}.`;
1072
+ else if (sw > fw) claim = `${second.code} lead ${first.code} ${sw}\u2013${fw} across ${rec.matches} captured IPL 2026 meeting${rec.matches === 1 ? "" : "s"}.`;
1073
+ else claim = `${first.code} and ${second.code} are level ${fw}\u2013${sw} across ${rec.matches} captured IPL 2026 meeting${rec.matches === 1 ? "" : "s"}.`;
1074
+ return ok({ claim, teamA: first, teamB: second, matches: rec.matches, [`${first.code}_won`]: fw, [`${second.code}_won`]: sw, noResult: rec.noResult, recent: rec.recent, window: "IPL 2026 to date", sampleSize: `${rec.matches} completed fixture${rec.matches === 1 ? "" : "s"}`, source: "CricketStudio ball-by-ball aggregation" }, `${SITE}/teams/${first.slug}/vs/${second.slug}`);
1075
+ }
1076
+ function handleGetPartnerships(args) {
1077
+ const p = players()[args.playerSlug];
1078
+ if (!p) return notFound(`No player with slug "${args.playerSlug}".`, `${SITE}/players/${args.playerSlug}`);
1079
+ return ok({
1080
+ player: { slug: p.slug, fullName: p.fullName, team: p.team },
1081
+ available: false,
1082
+ note: "Partnership-dependency analysis (per-partner strike rate, most-frequent partners, most productive wicket-stand) is computed in the full dataset and rendered at the canonical URL; it is not included in the bundled snapshot.",
1083
+ source: "CricketStudio ball-by-ball aggregation"
1084
+ }, `${SITE}/players/${p.slug}`);
1085
+ }
1086
+ function handleComparePlayers(args) {
1087
+ const rows = args.playerSlugs.map((slug) => {
1088
+ const p = players()[slug];
1089
+ if (!p) return { slug, error: "No player found with this slug" };
1090
+ const byPillar = (pl) => p.claims.filter((c) => c.pillar === pl).length;
1091
+ return { slug: p.slug, fullName: p.fullName, team: p.team, role: p.role, claimCount: p.claims.length, pillars: { P1: byPillar("P1"), P2: byPillar("P2"), P3: byPillar("P3"), P4: byPillar("P4"), P5: byPillar("P5") }, headlineClaim: p.claims[0]?.headline ?? null, canonicalUrl: `${SITE}/players/${p.slug}` };
1092
+ });
1093
+ const valid = rows.filter((r) => !("error" in r)).map((r) => r.slug);
1094
+ return ok({ players: rows, note: "For deeper per-pair analysis, use get_player_h2h or get_player_pillar." }, valid.length >= 2 ? `${SITE}/compare/players?slugs=${valid.join(",")}` : void 0);
1095
+ }
1096
+ function handleDismissalAnalysis(args) {
1097
+ const url = `${SITE}/players/${args.playerSlug}`;
1098
+ const stats = seasonStats();
1099
+ const p = (stats.bySlug ?? {})[args.playerSlug];
1100
+ if (!p) {
1101
+ return ok({
1102
+ error: "not_found",
1103
+ message: `No IPL 2026 batting record for slug "${args.playerSlug}" in the season-stats snapshot. Use search_players to find a valid slug, or see the canonical page.`,
1104
+ player: { slug: args.playerSlug }
1105
+ }, url);
1106
+ }
1107
+ const d = p.batting?.dismissals;
1108
+ if (!d || typeof d !== "object") {
1109
+ return ok({
1110
+ player: { slug: args.playerSlug, fullName: p.fullName, team: p.teamCode ?? null },
1111
+ note: `${p.fullName} has no recorded dismissals in IPL 2026 (not out in every captured innings, or no innings batted). The per-mode breakdown is rendered at the canonical URL when available.`,
1112
+ available: false,
1113
+ window: "IPL 2026",
1114
+ source: "CricketStudio ball-by-ball aggregation"
1115
+ }, url);
1116
+ }
1117
+ const modes = ["bowled", "caught", "lbw", "runOut", "hitWicket", "other"];
1118
+ const totalDismissals = modes.reduce((sum, k) => sum + Number(d[k] ?? 0), 0);
1119
+ let mostCommonMode = null;
1120
+ let mostCommonCount = -1;
1121
+ for (const k of modes) {
1122
+ const v = Number(d[k] ?? 0);
1123
+ if (v > mostCommonCount) {
1124
+ mostCommonCount = v;
1125
+ mostCommonMode = k;
1126
+ }
1127
+ }
1128
+ return ok({
1129
+ player: { slug: args.playerSlug, fullName: p.fullName, team: p.teamCode ?? null },
1130
+ dismissals: {
1131
+ bowled: Number(d.bowled ?? 0),
1132
+ caught: Number(d.caught ?? 0),
1133
+ lbw: Number(d.lbw ?? 0),
1134
+ runOut: Number(d.runOut ?? 0),
1135
+ hitWicket: Number(d.hitWicket ?? 0),
1136
+ other: Number(d.other ?? 0)
1137
+ },
1138
+ totalDismissals,
1139
+ mostCommonMode: totalDismissals > 0 ? mostCommonMode : null,
1140
+ window: "IPL 2026",
1141
+ sampleSize: `${totalDismissals} dismissal${totalDismissals === 1 ? "" : "s"}`,
1142
+ source: "CricketStudio ball-by-ball aggregation"
1143
+ }, url);
1144
+ }
1145
+ function handleFieldingStats(args) {
1146
+ const stats = seasonStats();
1147
+ const all = Object.entries(stats.bySlug ?? {}).filter(([, p]) => p && p.fielding);
1148
+ if (args.playerSlug) {
1149
+ const p = (stats.bySlug ?? {})[args.playerSlug];
1150
+ if (!p) return notFound(`No player with slug "${args.playerSlug}".`, `${SITE}/players/${args.playerSlug}`);
1151
+ const f = p.fielding || {};
1152
+ return ok({ player: { slug: args.playerSlug, fullName: p.fullName, teamCode: p.teamCode }, catches: f.catches ?? 0, runOutAssists: f.runOutAssists ?? 0, totalDismissals: f.totalDismissals ?? 0, window: "IPL 2026 to date", source: "CricketStudio ball-by-ball aggregation" }, `${SITE}/players/${args.playerSlug}`);
1153
+ }
1154
+ const limit = Math.max(1, Math.min(100, args.limit ?? 15));
1155
+ const ranked = all.sort(([, a], [, b]) => Number(b.fielding.totalDismissals || 0) - Number(a.fielding.totalDismissals || 0)).slice(0, limit).map(([slug, p], i) => ({ rank: i + 1, slug, fullName: p.fullName, teamCode: p.teamCode, catches: p.fielding.catches ?? 0, runOutAssists: p.fielding.runOutAssists ?? 0, totalDismissals: p.fielding.totalDismissals ?? 0, canonicalUrl: `${SITE}/players/${slug}` }));
1156
+ return ok({ count: ranked.length, window: "IPL 2026 to date", rows: ranked }, `${SITE}/season/ipl-2026/catches`);
1157
+ }
1158
+ function describeMlcResult(r, teams2) {
1159
+ if (r.outcome === "won") {
1160
+ const winnerName = r.winnerSlug === teams2.home.slug ? teams2.home.name : teams2.away.name;
1161
+ return `${winnerName} won by ${r.winMargin} ${r.winType}`;
1162
+ }
1163
+ if (r.outcome === "tie") return "tie";
1164
+ if (r.outcome === "no-result") return "no result";
1165
+ return "draw";
1166
+ }
1167
+ function handleMlcDatasetSummary() {
1168
+ const lg = mlcLeague();
1169
+ return ok({ league: "Major League Cricket", overview: "CricketStudio MLC coverage \u2014 atomic claims for every captured MLC match, sourced from Cricsheet under CC BY 3.0. Player profiles cross-link to Wikidata / ESPNcricinfo. Leaderboards, records, and per-match atomic claim cards live under /leagues/mlc/*.", coverage: { seasons: lg.seasons, totalMatches: lg.totalMatches, teams: Array.isArray(lg.teams) ? lg.teams.length : 6, venues: Array.isArray(lg.venues) ? lg.venues.length : void 0, players: lg.playerCount, ballsCaptured: lg.ballsCaptured }, seasonBreakdown: lg.seasonBreakdown, surfaces: { hub: MLC_HUB, standings: `${MLC_HUB}/standings`, matches: `${MLC_HUB}/matches`, players: `${MLC_HUB}/players`, leaderboards: `${MLC_HUB}/leaderboards`, records: `${MLC_HUB}/records` }, leaderboardAspects: lg.leaderboardAspects.map((a) => ({ slug: a.slug, title: a.title, url: mlcLeaderboardUrl(a.slug) })), provenance: { source: "Cricsheet (https://cricsheet.org)", license: "CC BY 3.0", licenseUrl: "https://creativecommons.org/licenses/by/3.0/" } }, MLC_HUB);
1170
+ }
1171
+ function handleSearchMlcPlayers(args) {
1172
+ const q = args.query.toLowerCase().trim();
1173
+ const limit = Math.max(1, Math.min(50, args.limit ?? 10));
1174
+ const results = Object.values(mlcPlayers()).filter((p) => p.slug.toLowerCase().includes(q) || p.fullName.toLowerCase().includes(q)).slice(0, limit).map((p) => ({ slug: p.slug, fullName: p.fullName, teamSlugs: p.teamSlugs, matches: p.batting?.matches ?? 0, runs: p.batting?.runs ?? 0, wickets: p.bowling?.wickets ?? 0, canonicalUrl: mlcPlayerUrl(p.slug) }));
1175
+ return ok({ query: args.query, count: results.length, players: results });
1176
+ }
1177
+ function handleMlcPlayerProfile(args) {
1178
+ const p = mlcPlayers()[args.playerSlug];
1179
+ if (!p) return notFound(`No MLC player with slug "${args.playerSlug}". Use search_mlc_players to discover slugs.`, mlcPlayerUrl(args.playerSlug));
1180
+ return ok({ slug: p.slug, fullName: p.fullName, teamSlugs: p.teamSlugs, batting: p.batting, bowling: p.bowling, bySeason: p.bySeason, identity: p.identity, provenance: { source: "Cricsheet", license: "CC BY 3.0" } }, mlcPlayerUrl(p.slug));
1181
+ }
1182
+ function handleMlcTeamProfile(args) {
1183
+ const t = mlcTeams().find((x) => x.slug === args.teamSlug.toLowerCase());
1184
+ if (!t) return notFound(`No MLC team "${args.teamSlug}". Valid: ${mlcTeams().map((x) => x.slug).join(", ")}.`, mlcTeamUrl(args.teamSlug));
1185
+ const squad = Object.values(mlcPlayers()).filter((p) => (p.teamSlugs || []).includes(t.slug));
1186
+ const topBatters = squad.map((p) => ({ slug: p.slug, fullName: p.fullName, runs: p.batting?.runs ?? 0 })).filter((r) => r.runs > 0).sort((a, b) => b.runs - a.runs).slice(0, 3);
1187
+ const topBowlers = squad.map((p) => ({ slug: p.slug, fullName: p.fullName, wickets: p.bowling?.wickets ?? 0 })).filter((r) => r.wickets > 0).sort((a, b) => b.wickets - a.wickets).slice(0, 3);
1188
+ return ok({
1189
+ slug: t.slug,
1190
+ name: t.name,
1191
+ seasons: t.seasons,
1192
+ firstSeason: t.firstSeason,
1193
+ lastSeason: t.lastSeason,
1194
+ matchCount: t.matchCount,
1195
+ squadSize: squad.length,
1196
+ topBatters,
1197
+ topBowlers,
1198
+ provenance: { source: "Cricsheet", license: "CC BY 3.0" }
1199
+ }, mlcTeamUrl(t.slug));
1200
+ }
1201
+ function handleMlcMatch(args) {
1202
+ const m = mlcMatches()[args.matchId];
1203
+ if (!m) return notFound(`No MLC match "${args.matchId}". Use list_mlc_matches to discover ids.`, mlcMatchUrl(args.matchId));
1204
+ return ok({ matchId: m.matchId, season: m.season, startDate: m.startDate, matchType: m.matchType, teams: m.teams, venue: m.venue, toss: m.toss ?? null, result: m.result, resultText: describeMlcResult(m.result, m.teams), officials: m.officials ?? null, playerOfMatch: m.playerOfMatch ?? [], innings: m.innings, availableClaimKinds: m.claims.map((c) => ({ kind: c.kind, headline: c.headline, atomic: c.atomic, canonicalUrl: mlcMatchClaimUrl(m.matchId, c.kind) })), provenance: { source: "Cricsheet", license: "CC BY 3.0", matchUrl: m.attribution.matchUrl } }, mlcMatchUrl(m.matchId));
1205
+ }
1206
+ function handleMlcMatchClaim(args) {
1207
+ const m = mlcMatches()[args.matchId];
1208
+ if (!m) return notFound(`No MLC match "${args.matchId}".`, mlcMatchUrl(args.matchId));
1209
+ const claim = m.claims.find((c) => c.kind === args.kind);
1210
+ if (!claim) return notFound(`Match "${args.matchId}" did not emit a "${args.kind}" claim \u2014 sample-size floor not met. Call get_mlc_match for available kinds.`, mlcMatchClaimUrl(args.matchId, args.kind));
1211
+ return ok({ matchId: m.matchId, kind: claim.kind, kindLabel: claim.kindLabel, atomic: claim.atomic, headline: claim.headline, subheadline: claim.subheadline, subject: claim.subject ?? null, coSubject: claim.coSubject ?? null, rows: claim.rows ?? null, sampleSize: claim.sampleSize, sampleUnit: claim.sampleUnit, fixtureUrl: mlcMatchUrl(m.matchId), provenance: { source: "Cricsheet ball-by-ball record", license: "CC BY 3.0", matchUrl: m.attribution.matchUrl, computedFrom: "Deterministic walk of balls[] \u2014 no LLM involvement in numeric values" } }, mlcMatchClaimUrl(m.matchId, claim.kind));
1212
+ }
1213
+ function handleListMlcMatches(args) {
1214
+ const limit = Math.max(1, Math.min(200, args.limit ?? 30));
1215
+ const all = Object.values(mlcMatches());
1216
+ const rows = all.filter((m) => args.season ? m.season === args.season : true).filter((m) => args.teamSlug ? m.teams.home.slug === args.teamSlug || m.teams.away.slug === args.teamSlug : true).sort((a, b) => b.startDate.localeCompare(a.startDate)).slice(0, limit).map((m) => ({ matchId: m.matchId, season: m.season, startDate: m.startDate, teams: { home: m.teams.home.name, away: m.teams.away.name }, venue: m.venue.name, result: describeMlcResult(m.result, m.teams), canonicalUrl: mlcMatchUrl(m.matchId) }));
1217
+ return ok({ count: rows.length, totalAvailable: all.length, filters: { season: args.season, teamSlug: args.teamSlug }, matches: rows }, `${MLC_HUB}/matches`);
1218
+ }
1219
+ function handleListMlcLeaderboards(args) {
1220
+ const lbs = mlcLeaderboards();
1221
+ const lb = lbs[args.aspect];
1222
+ if (!lb) {
1223
+ const validAspects = Object.keys(lbs);
1224
+ const aspectList = validAspects.length > 0 ? validAspects.join(", ") : "orange-cap, purple-cap, strike-rate, economy-leaders, most-sixes, most-fours, top-knocks, best-bowling, most-ducks, single-digit-outs, powerplay-strike-rate, death-overs-economy, death-overs-strike-rate, powerplay-economy";
1225
+ return notFound(`No MLC leaderboard aspect "${args.aspect}". Valid aspects: ${aspectList}.`, `${MLC_HUB}/leaderboards`);
1226
+ }
1227
+ const limit = Math.max(1, Math.min(100, args.limit ?? 20));
1228
+ const rows = Array.isArray(lb.rows) ? lb.rows : [];
1229
+ return ok({ aspect: lb.slug, title: lb.title, description: lb.description, metricLabel: lb.metricLabel, floorNote: lb.floorNote ?? null, count: Math.min(limit, rows.length), rows: rows.slice(0, limit).map((r) => ({ ...r, canonicalUrl: mlcPlayerUrl(r.slug) })), provenance: { source: "Cricsheet SETU snapshot", license: "CC BY 3.0" } }, mlcLeaderboardUrl(lb.slug));
1230
+ }
1231
+ var IPL_LB_SPECS = {
1232
+ // ── Batting aggregates ──────────────────────────────────────────────
1233
+ "orange-cap": { block: "batting", field: "runs", ascending: false },
1234
+ "most-runs": { block: "batting", field: "runs", ascending: false },
1235
+ "most-sixes": { block: "batting", field: "sixes", ascending: false },
1236
+ "most-fours": { block: "batting", field: "fours", ascending: false },
1237
+ "most-fifties": { block: "batting", field: "fifties", ascending: false },
1238
+ "most-hundreds": { block: "batting", field: "hundreds", ascending: false },
1239
+ "highest-score": { block: "batting", field: "highScore", ascending: false },
1240
+ "top-score": { block: "batting", field: "highScore", ascending: false },
1241
+ "strike-rate": { block: "batting", field: "sr", ascending: false, floor: (b) => Number(b.balls ?? 0) >= 30, floorNote: "\u226530 balls faced" },
1242
+ "batting-average": { block: "batting", field: "avg", ascending: false, floor: (b) => Number(b.innings ?? 0) >= 10, floorNote: "\u226510 innings" },
1243
+ // ── Bowling aggregates ──────────────────────────────────────────────
1244
+ "purple-cap": { block: "bowling", field: "wickets", ascending: false },
1245
+ "most-wickets": { block: "bowling", field: "wickets", ascending: false },
1246
+ // career bowling block has no `balls` field, so the economy/avg floors use
1247
+ // the available proxies (matches / wickets) — disclosed in floorNote.
1248
+ "economy-leaders": { block: "bowling", field: "econ", ascending: true, floor: (b) => Number(b.matches ?? 0) >= 10, floorNote: "\u226510 matches bowled (career bowling block carries no ball count)" },
1249
+ "economy": { block: "bowling", field: "econ", ascending: true, floor: (b) => Number(b.matches ?? 0) >= 10, floorNote: "\u226510 matches bowled (career bowling block carries no ball count)" },
1250
+ "best-economy": { block: "bowling", field: "econ", ascending: true, floor: (b) => Number(b.matches ?? 0) >= 10, floorNote: "\u226510 matches bowled (career bowling block carries no ball count)" },
1251
+ "bowling-average": { block: "bowling", field: "avg", ascending: true, floor: (b) => Number(b.wickets ?? 0) >= 10, floorNote: "\u226510 wickets" }
1252
+ };
1253
+ function handleIplLeaderboard(args) {
1254
+ const limit = Math.max(1, Math.min(100, args.limit ?? 20));
1255
+ const canonical = args.season ? `${SITE}/season/${args.season}/${args.aspect}` : `${IPL_HUB}/leaderboards/${args.aspect}`;
1256
+ const validAspects = Object.keys(IPL_LB_SPECS);
1257
+ const hist = iplHistorical();
1258
+ if (!hist || !hist.bySlug) {
1259
+ return ok({
1260
+ note: "IPL historical snapshot not bundled in this release. Full leaderboards are available at the canonical URL.",
1261
+ aspect: args.aspect,
1262
+ season: args.season ?? "all-time",
1263
+ canonicalSurface: canonical
1264
+ }, canonical);
1265
+ }
1266
+ const spec = IPL_LB_SPECS[args.aspect];
1267
+ if (!spec) {
1268
+ const meta2 = getAspectMeta(args.aspect);
1269
+ return ok({
1270
+ error: "unknown_aspect",
1271
+ message: meta2 ? `Aspect "${args.aspect}" is catalogued but not derivable from the bundled career snapshot (it needs phase-split / ball-level data not present in ipl-historical.json bySlug). Computable aspects: ${validAspects.join(", ")}. The full leaderboard is rendered at the canonical URL.` : `Unknown IPL leaderboard aspect "${args.aspect}". Valid aspects: ${validAspects.join(", ")}.`,
1272
+ aspect: args.aspect,
1273
+ validAspects
1274
+ }, `${IPL_HUB}/leaderboards/${args.aspect}`);
1275
+ }
1276
+ if (args.season) {
1277
+ return ok({
1278
+ note: `Per-season IPL leaderboards (season "${args.season}") are rendered at the canonical URL. The MCP computes the all-time (2007/08\u20132025) leaderboard \u2014 call without the season arg for that.`,
1279
+ aspect: args.aspect,
1280
+ season: args.season,
1281
+ canonicalSurface: `${SITE}/season/${args.season}/${args.aspect}`
1282
+ }, `${SITE}/season/${args.season}/${args.aspect}`);
1283
+ }
1284
+ const meta = getAspectMeta(args.aspect);
1285
+ const rows = Object.entries(hist.bySlug).map(([slug, p]) => {
1286
+ const block = spec.block === "batting" ? p.batting : p.bowling;
1287
+ if (!block) return null;
1288
+ const value = block[spec.field];
1289
+ if (value === void 0 || value === null || typeof value !== "number" || !Number.isFinite(value)) return null;
1290
+ if (spec.floor && !spec.floor(block)) return null;
1291
+ return { slug, fullName: p.fullName, teamSlugs: p.teamSlugs ?? [], value, block };
1292
+ }).filter((r) => r !== null).sort((a, b) => spec.ascending ? a.value - b.value : b.value - a.value).slice(0, limit).map((r, i) => ({
1293
+ rank: i + 1,
1294
+ slug: r.slug,
1295
+ fullName: r.fullName,
1296
+ teamSlugs: r.teamSlugs,
1297
+ value: r.value,
1298
+ matches: r.block.matches ?? null,
1299
+ ...spec.block === "batting" ? { innings: r.block.innings ?? null } : { wickets: r.block.wickets ?? null },
1300
+ canonicalUrl: `${SITE}/players/${r.slug}`
1301
+ }));
1302
+ if (rows.length === 0) {
1303
+ return ok({
1304
+ note: `No players cleared the sample floor for aspect "${args.aspect}". Check the canonical URL.`,
1305
+ aspect: args.aspect,
1306
+ season: "all-time",
1307
+ validAspects
1308
+ }, canonical);
1309
+ }
1310
+ return ok({
1311
+ aspect: args.aspect,
1312
+ title: meta?.title ?? args.aspect,
1313
+ season: "all-time (2007/08\u20132025)",
1314
+ metric: spec.field,
1315
+ sampleFloorNote: spec.floorNote ?? "No sample floor for this aspect.",
1316
+ count: rows.length,
1317
+ rows,
1318
+ provenance: { source: "Cricsheet CC BY 3.0, CricketStudio aggregation", seasons: "2007/08\u20132025", matches: 1169, computedFrom: "ipl-historical.json bySlug career aggregates (deterministic \u2014 no LLM)" }
1319
+ }, canonical);
1320
+ }
1321
+ var server = new Server(
1322
+ { name: "cricketstudio", version: "1.0.0" },
1323
+ { capabilities: { tools: {} } }
1324
+ );
1325
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
1326
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1327
+ const { name, arguments: rawArgs } = req.params;
1328
+ const v = validators[name];
1329
+ if (!v) return ok({ error: "unknown_tool", tool: name, hint: "Call tools/list for the full 29-tool catalog." });
1330
+ const parsed = v.safeParse(rawArgs ?? {});
1331
+ if (!parsed.success) return ok({ error: "invalid_arguments", tool: name, issues: parsed.error.issues });
1332
+ const args = parsed.data;
1333
+ try {
1334
+ switch (name) {
1335
+ case "get_dataset_summary":
1336
+ return handleDatasetSummary();
1337
+ case "search_players":
1338
+ return handleSearchPlayers(args);
1339
+ case "get_player_profile":
1340
+ return handlePlayerProfile(args);
1341
+ case "get_player_pillar":
1342
+ return handlePlayerPillar(args);
1343
+ case "get_standings":
1344
+ return handleStandings();
1345
+ case "get_season_stats":
1346
+ return handleSeasonStats(args);
1347
+ case "get_match_state":
1348
+ return handleMatchState(args);
1349
+ case "get_match_recap":
1350
+ return handleMatchRecap(args);
1351
+ case "list_fixtures":
1352
+ return handleListFixtures(args);
1353
+ case "get_trend":
1354
+ return handleTrend(args);
1355
+ case "list_trends":
1356
+ return handleListTrends(args);
1357
+ case "get_player_h2h":
1358
+ return handlePlayerH2H(args);
1359
+ case "get_team_profile":
1360
+ return handleTeamProfile(args);
1361
+ case "get_venue_hub":
1362
+ return handleVenueHub(args);
1363
+ case "list_atomic_claims":
1364
+ return handleListAtomicClaims(args);
1365
+ case "get_team_h2h":
1366
+ return handleTeamH2H(args);
1367
+ case "get_partnerships":
1368
+ return handleGetPartnerships(args);
1369
+ case "compare_players":
1370
+ return handleComparePlayers(args);
1371
+ case "get_dismissal_analysis":
1372
+ return handleDismissalAnalysis(args);
1373
+ case "get_fielding_stats":
1374
+ return handleFieldingStats(args);
1375
+ case "get_mlc_dataset_summary":
1376
+ return handleMlcDatasetSummary();
1377
+ case "search_mlc_players":
1378
+ return handleSearchMlcPlayers(args);
1379
+ case "get_mlc_player_profile":
1380
+ return handleMlcPlayerProfile(args);
1381
+ case "get_mlc_team_profile":
1382
+ return handleMlcTeamProfile(args);
1383
+ case "get_mlc_match":
1384
+ return handleMlcMatch(args);
1385
+ case "get_mlc_match_claim":
1386
+ return handleMlcMatchClaim(args);
1387
+ case "list_mlc_matches":
1388
+ return handleListMlcMatches(args);
1389
+ case "list_mlc_leaderboards":
1390
+ return handleListMlcLeaderboards(args);
1391
+ case "get_ipl_leaderboard":
1392
+ return handleIplLeaderboard(args);
1393
+ default:
1394
+ return ok({ error: "unknown_tool", tool: name });
1395
+ }
1396
+ } catch (err) {
1397
+ return ok({ error: "tool_error", tool: name, message: err instanceof Error ? err.message : String(err) });
1398
+ }
1399
+ });
1400
+ async function main() {
1401
+ await runStartup();
1402
+ const transport = new StdioServerTransport();
1403
+ await server.connect(transport);
1404
+ }
1405
+ main().catch(console.error);