@claudinho/mcp 0.10.0 → 0.10.1

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.
Files changed (2) hide show
  1. package/dist/index.js +292 -74
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -279,6 +279,8 @@ var EN = {
279
279
  "bracket.slot.tbd": "TBD",
280
280
  "live.data": "Live data: {source}",
281
281
  "standings.unavailable": "Live standings unavailable.",
282
+ "standings.partial": "Partial table \u2014 {n} rows could not be read.",
283
+ "competition.unsupported": "Not available for this competition yet.",
282
284
  "share.tryIt": "Try it: {line}",
283
285
  "stage.group": "Group {group}",
284
286
  "stage.groupStage": "Group stage",
@@ -309,6 +311,8 @@ var ES = {
309
311
  "bracket.slot.tbd": "Por definir",
310
312
  "live.data": "Datos en vivo: {source}",
311
313
  "standings.unavailable": "Tabla en vivo no disponible.",
314
+ "standings.partial": "Tabla parcial \u2014 no se pudieron leer {n} filas.",
315
+ "competition.unsupported": "A\xFAn no disponible para esta competici\xF3n.",
312
316
  "share.tryIt": "Pru\xE9balo: {line}",
313
317
  "stage.group": "Grupo {group}",
314
318
  "stage.groupStage": "Fase de grupos",
@@ -339,6 +343,8 @@ var PT = {
339
343
  "bracket.slot.tbd": "A definir",
340
344
  "live.data": "Dados ao vivo: {source}",
341
345
  "standings.unavailable": "Classifica\xE7\xE3o ao vivo indispon\xEDvel.",
346
+ "standings.partial": "Tabela parcial \u2014 {n} linhas n\xE3o puderam ser lidas.",
347
+ "competition.unsupported": "Ainda n\xE3o dispon\xEDvel para esta competi\xE7\xE3o.",
342
348
  "share.tryIt": "Experimente: {line}",
343
349
  "stage.group": "Grupo {group}",
344
350
  "stage.groupStage": "Fase de grupos",
@@ -369,6 +375,8 @@ var FR = {
369
375
  "bracket.slot.tbd": "\xC0 d\xE9finir",
370
376
  "live.data": "Donn\xE9es en direct : {source}",
371
377
  "standings.unavailable": "Classement en direct indisponible.",
378
+ "standings.partial": "Classement partiel \u2014 {n} lignes n'ont pas pu \xEAtre lues.",
379
+ "competition.unsupported": "Pas encore disponible pour cette comp\xE9tition.",
372
380
  "share.tryIt": "Essayez : {line}",
373
381
  "stage.group": "Groupe {group}",
374
382
  "stage.groupStage": "Phase de groupes",
@@ -2836,11 +2844,13 @@ function fixturesByGroup(group, fixtures = SCHEDULE) {
2836
2844
  const g = group.toUpperCase();
2837
2845
  return fixtures.filter((m) => (m.group ?? "").toUpperCase() === g).sort(byKickoff);
2838
2846
  }
2847
+ function isUpcoming(m, now = /* @__PURE__ */ new Date()) {
2848
+ if (m.status === "CANCELLED" || m.status === "POSTPONED") return false;
2849
+ return Date.parse(m.kickoff) >= now.getTime();
2850
+ }
2839
2851
  function nextFixtureForTeam(code, opts = {}) {
2840
2852
  const from = opts.from ?? /* @__PURE__ */ new Date();
2841
- return fixturesByTeam(code, opts.fixtures ?? SCHEDULE).find(
2842
- (m) => new Date(m.kickoff).getTime() >= from.getTime()
2843
- );
2853
+ return fixturesByTeam(code, opts.fixtures ?? SCHEDULE).find((m) => isUpcoming(m, from));
2844
2854
  }
2845
2855
  var LIVE_WINDOW_MS = 140 * 6e4;
2846
2856
  var KNOCKOUT_EXTRA_TIME_MS = 60 * 6e4;
@@ -3186,7 +3196,7 @@ function toParticipant(raw) {
3186
3196
  const providerId = opaqueId(raw.team?.id, ESPN_ID);
3187
3197
  const known = productFlag(name) !== nationToFlag("");
3188
3198
  return valid(
3189
- providerId && known ? { kind: "team", providerId, team } : { kind: "slot", team }
3199
+ providerId && known ? { kind: "team", providerId, team } : { kind: "slot", ...providerId ? { providerId } : {}, team }
3190
3200
  );
3191
3201
  }
3192
3202
  function mapStatus(st) {
@@ -3255,7 +3265,7 @@ function parseEspnEvent(raw, ctx = {}) {
3255
3265
  if (awayP.kind !== "valid") return awayP;
3256
3266
  const h = homeP.value;
3257
3267
  const a = awayP.value;
3258
- if (h.kind === "team" && a.kind === "team" && h.providerId === a.providerId) {
3268
+ if (h.providerId !== void 0 && h.providerId === a.providerId) {
3259
3269
  return definitiveNone("both competitors are the same team");
3260
3270
  }
3261
3271
  const home = homeP.value.team;
@@ -3408,8 +3418,12 @@ function parseEspnStandings(raw) {
3408
3418
  const seenProviderIds = /* @__PURE__ */ new Set();
3409
3419
  for (const child of children) {
3410
3420
  const label = humanLabel(child?.name ?? child?.abbreviation);
3411
- const letter = label.match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
3412
- if (!letter) continue;
3421
+ const letter = label.match(/Group\s+([A-L])(?![A-Za-z0-9])/i)?.[1]?.toUpperCase();
3422
+ if (!letter) {
3423
+ const rows = child?.standings?.entries;
3424
+ if (Array.isArray(rows) && rows.length > 0) complete = false;
3425
+ continue;
3426
+ }
3413
3427
  if (seenGroups.has(letter)) {
3414
3428
  complete = false;
3415
3429
  continue;
@@ -3420,9 +3434,11 @@ function parseEspnStandings(raw) {
3420
3434
  complete = false;
3421
3435
  continue;
3422
3436
  }
3437
+ let omitted = 0;
3423
3438
  if (rawEntries.length > MAX_GROUP_ROWS) {
3424
3439
  rowsTruncated = true;
3425
3440
  complete = false;
3441
+ omitted += rawEntries.length - MAX_GROUP_ROWS;
3426
3442
  }
3427
3443
  const entries = takeBounded(rawEntries, MAX_GROUP_ROWS);
3428
3444
  const seenCodes = /* @__PURE__ */ new Map();
@@ -3432,6 +3448,7 @@ function parseEspnStandings(raw) {
3432
3448
  const r = entryToRow(e);
3433
3449
  if (r.kind !== "valid") {
3434
3450
  if (r.kind !== "definitive-none") complete = false;
3451
+ omitted += 1;
3435
3452
  continue;
3436
3453
  }
3437
3454
  const { providerId, providerRank } = r.value;
@@ -3440,13 +3457,14 @@ function parseEspnStandings(raw) {
3440
3457
  const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
3441
3458
  if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
3442
3459
  complete = false;
3460
+ omitted += 1;
3443
3461
  continue;
3444
3462
  }
3445
3463
  seenCodes.set(code, providerId !== void 0);
3446
3464
  seenRanks.add(providerRank);
3447
3465
  if (providerId !== void 0) seenProviderIds.add(providerId);
3448
3466
  const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3449
- ranked.push({ row, rank });
3467
+ ranked.push({ row: { ...row, rank }, rank });
3450
3468
  }
3451
3469
  ranked.sort((a, b) => {
3452
3470
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
@@ -3458,7 +3476,11 @@ function parseEspnStandings(raw) {
3458
3476
  complete = false;
3459
3477
  continue;
3460
3478
  }
3461
- out.push({ group: letter, rows: ranked.map((x) => x.row) });
3479
+ out.push({
3480
+ group: letter,
3481
+ rows: ranked.map((x) => x.row),
3482
+ ...omitted > 0 ? { partial: { omitted } } : {}
3483
+ });
3462
3484
  }
3463
3485
  return {
3464
3486
  items: out,
@@ -3469,11 +3491,72 @@ function parseEspnStandings(raw) {
3469
3491
  complete: complete && !rowsTruncated
3470
3492
  };
3471
3493
  }
3494
+ var ResponseTooLargeError = class extends Error {
3495
+ constructor(bytes, limit) {
3496
+ super(`response body exceeds ${limit} bytes (${bytes} seen)`);
3497
+ this.bytes = bytes;
3498
+ this.limit = limit;
3499
+ this.name = "ResponseTooLargeError";
3500
+ }
3501
+ bytes;
3502
+ limit;
3503
+ };
3504
+ function isStream(v) {
3505
+ return typeof v?.getReader === "function";
3506
+ }
3507
+ async function readJsonBounded(res, maxBytes) {
3508
+ const r = res;
3509
+ const declared = Number(r.headers?.get?.("content-length"));
3510
+ if (Number.isFinite(declared) && declared > maxBytes) {
3511
+ if (isStream(r.body)) await r.body.cancel().catch(() => {
3512
+ });
3513
+ throw new ResponseTooLargeError(declared, maxBytes);
3514
+ }
3515
+ if (isStream(r.body)) {
3516
+ const reader = r.body.getReader();
3517
+ const chunks = [];
3518
+ let total = 0;
3519
+ for (; ; ) {
3520
+ const { done, value } = await reader.read();
3521
+ if (done) break;
3522
+ total += value.byteLength;
3523
+ if (total > maxBytes) {
3524
+ await reader.cancel().catch(() => {
3525
+ });
3526
+ throw new ResponseTooLargeError(total, maxBytes);
3527
+ }
3528
+ chunks.push(value);
3529
+ }
3530
+ return JSON.parse(new TextDecoder().decode(Buffer.concat(chunks)));
3531
+ }
3532
+ if (typeof r.text === "function") {
3533
+ const text = await r.text();
3534
+ const size2 = Buffer.byteLength(text);
3535
+ if (size2 > maxBytes) throw new ResponseTooLargeError(size2, maxBytes);
3536
+ return JSON.parse(text);
3537
+ }
3538
+ if (typeof r.json === "function") return r.json();
3539
+ throw new TypeError("response has no readable body");
3540
+ }
3472
3541
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3473
3542
  var DEFAULT_COMPETITION = "fifa.world";
3474
3543
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3475
- var USER_AGENT = `claudinho/${"0.10.0"} (+https://github.com/arturogarrido/claudinho)`;
3544
+ var USER_AGENT = `claudinho/${"0.10.1"} (+https://github.com/arturogarrido/claudinho)`;
3476
3545
  var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
3546
+ var DEFAULT_COOLDOWN_MS = 5 * 6e4;
3547
+ var MAX_COOLDOWN_MS = 15 * 6e4;
3548
+ function retryAfterMs(header, nowMs) {
3549
+ if (typeof header !== "string" || header.trim() === "") return DEFAULT_COOLDOWN_MS;
3550
+ const h = header.trim();
3551
+ let ms;
3552
+ if (/^\d+$/.test(h)) ms = Number(h) * 1e3;
3553
+ else {
3554
+ const at = Date.parse(h);
3555
+ if (Number.isFinite(at)) ms = at - nowMs;
3556
+ }
3557
+ if (ms === void 0 || !Number.isFinite(ms)) return DEFAULT_COOLDOWN_MS;
3558
+ return Math.min(Math.max(ms, 0), MAX_COOLDOWN_MS);
3559
+ }
3477
3560
  function competitionBase(slug) {
3478
3561
  return `${ESPN_SOCCER}/${slug}`;
3479
3562
  }
@@ -3482,6 +3565,8 @@ var STANDINGS_SHARE_MS = 3e4;
3482
3565
  var ProviderError = class extends Error {
3483
3566
  kind;
3484
3567
  status;
3568
+ /** For a throttle: how long the adapter will refuse to fetch (bounded). */
3569
+ retryAfterMs;
3485
3570
  constructor(message, kind, status) {
3486
3571
  super(message);
3487
3572
  this.name = "ProviderError";
@@ -3505,6 +3590,7 @@ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length
3505
3590
  var EspnAdapter = class {
3506
3591
  constructor(opts = {}) {
3507
3592
  this.opts = opts;
3593
+ this.clock = opts.now ?? (() => Date.now());
3508
3594
  const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3509
3595
  this.expectedStandingsGroups = expected ? [...expected] : void 0;
3510
3596
  this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
@@ -3530,6 +3616,53 @@ var EspnAdapter = class {
3530
3616
  * throttle (persist a backoff) from an ordinary blip.
3531
3617
  */
3532
3618
  lastError;
3619
+ /**
3620
+ * A retained throttle (audit A12): after a 429/403 every call inside the
3621
+ * window throws the provider's last answer WITHOUT a request. A server-
3622
+ * lifetime MCP adapter is covered by this alone; the CLI pre-arms each
3623
+ * process from its persisted cache via `armCooldown`.
3624
+ */
3625
+ cooldownUntilMs;
3626
+ cooldownError;
3627
+ cooldownListeners = /* @__PURE__ */ new Set();
3628
+ clock;
3629
+ /** Epoch ms until which requests are refused, when a cooldown is armed. */
3630
+ get cooldownUntil() {
3631
+ return this.cooldownUntilMs;
3632
+ }
3633
+ /**
3634
+ * Arm the cooldown from outside — a fresh CLI process reading the backoff
3635
+ * its refresher persisted. The retained error reads as a throttle so every
3636
+ * caller's `degraded` path and the refresher's persistence treat it as one.
3637
+ */
3638
+ armCooldown(untilMs, reason) {
3639
+ const nowMs = this.clock();
3640
+ const error = reason ?? new ProviderError("ESPN request skipped: provider cooldown in effect", "http", 429);
3641
+ error.retryAfterMs = Math.max(0, untilMs - nowMs);
3642
+ this.arm(untilMs, error);
3643
+ }
3644
+ /**
3645
+ * Be told whenever the cooldown window is armed or EXTENDED — the way a
3646
+ * caller persists a throttle that arrives from a still-running request after
3647
+ * its own call already returned (review P2 on #128). Returns unsubscribe.
3648
+ */
3649
+ onCooldown(listener) {
3650
+ this.cooldownListeners.add(listener);
3651
+ return () => {
3652
+ this.cooldownListeners.delete(listener);
3653
+ };
3654
+ }
3655
+ /**
3656
+ * The ONE place a window is set. Concurrent requests can each carry a
3657
+ * Retry-After; the LATEST expiry wins — a shorter one arriving second must
3658
+ * never shorten a longer active window (review P2 on #128).
3659
+ */
3660
+ arm(untilMs, error) {
3661
+ if (this.cooldownUntilMs !== void 0 && untilMs <= this.cooldownUntilMs) return;
3662
+ this.cooldownUntilMs = untilMs;
3663
+ this.cooldownError = error;
3664
+ for (const listener of this.cooldownListeners) listener(untilMs);
3665
+ }
3533
3666
  async fetchByDate(dateISO) {
3534
3667
  return this.fetchScoreboard(toEspnDate(dateISO));
3535
3668
  }
@@ -3617,6 +3750,11 @@ var EspnAdapter = class {
3617
3750
  return usableProviderItems("scoreboard", parsed);
3618
3751
  }
3619
3752
  async get(url) {
3753
+ const nowMs = this.clock();
3754
+ if (this.cooldownError && this.cooldownUntilMs !== void 0 && nowMs < this.cooldownUntilMs) {
3755
+ this.lastError = this.cooldownError;
3756
+ throw this.cooldownError;
3757
+ }
3620
3758
  const doFetch = this.opts.fetchImpl ?? fetch;
3621
3759
  const controller = new AbortController();
3622
3760
  const timer = setTimeout(
@@ -3638,19 +3776,24 @@ var EspnAdapter = class {
3638
3776
  throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3639
3777
  }
3640
3778
  if (!res.ok) {
3641
- throw new ProviderError(
3779
+ const pe = new ProviderError(
3642
3780
  `ESPN request failed: ${res.status} ${res.statusText}`,
3643
3781
  "http",
3644
3782
  res.status
3645
3783
  );
3646
- }
3647
- const length = Number(res.headers?.get?.("content-length"));
3648
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3649
- throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
3784
+ if (pe.throttled) {
3785
+ const receiptMs = this.clock();
3786
+ pe.retryAfterMs = retryAfterMs(res.headers?.get?.("retry-after"), receiptMs);
3787
+ this.arm(receiptMs + pe.retryAfterMs, pe);
3788
+ }
3789
+ throw pe;
3650
3790
  }
3651
3791
  try {
3652
- return await res.json();
3792
+ return await readJsonBounded(res, MAX_RESPONSE_BYTES);
3653
3793
  } catch (e) {
3794
+ if (e instanceof ResponseTooLargeError) {
3795
+ throw new ProviderError(`ESPN response too large: ${e.bytes} bytes`, "parse");
3796
+ }
3654
3797
  throw new ProviderError(
3655
3798
  `ESPN response unparseable: ${e?.message ?? e}`,
3656
3799
  "parse"
@@ -3693,14 +3836,30 @@ function hasGroupStarted(group, tables) {
3693
3836
  function matchesPerTeamInGroup(teamCount) {
3694
3837
  return Math.max(0, teamCount - 1);
3695
3838
  }
3696
- function isGroupStandingsComplete(table) {
3697
- const n = table?.rows.length ?? 0;
3839
+ function isGroupStandingsComplete(table, expectedTeams) {
3840
+ if (!table || table.partial) return false;
3841
+ const n = table.rows.length;
3698
3842
  if (n < 2) return false;
3699
- const required = matchesPerTeamInGroup(n);
3843
+ if (expectedTeams !== void 0 && n < expectedTeams) return false;
3844
+ const required = matchesPerTeamInGroup(Math.max(n, expectedTeams ?? n));
3700
3845
  return table.rows.every((r) => r.played >= required);
3701
3846
  }
3847
+ function bundledGroupSize(group) {
3848
+ const codes = /* @__PURE__ */ new Set();
3849
+ for (const m of fixturesByGroup(group)) {
3850
+ codes.add(m.home.code);
3851
+ codes.add(m.away.code);
3852
+ }
3853
+ return codes.size > 0 ? codes.size : void 0;
3854
+ }
3702
3855
  function isGroupComplete(group, tables) {
3703
- return isGroupStandingsComplete(tables.find((t2) => t2.group === group));
3856
+ return isGroupStandingsComplete(
3857
+ tables.find((t2) => t2.group === group),
3858
+ bundledGroupSize(group)
3859
+ );
3860
+ }
3861
+ function isGroupPartial(group, tables) {
3862
+ return tables.find((t2) => t2.group === group)?.partial !== void 0;
3704
3863
  }
3705
3864
  function resolveWinner(match) {
3706
3865
  if (!isFinished(match.status)) return void 0;
@@ -3755,7 +3914,7 @@ function resolveSlot(ref, ctx, liveTeam, fixtureInMergedSet = false) {
3755
3914
  return tbd(ref.label);
3756
3915
  case "group": {
3757
3916
  if (liveParticipant) return liveParticipant;
3758
- if (!ctx.standingsDegraded && hasGroupStarted(ref.group, ctx.tables)) {
3917
+ if (!ctx.standingsDegraded && !isGroupPartial(ref.group, ctx.tables) && hasGroupStarted(ref.group, ctx.tables)) {
3759
3918
  const team = teamFromStandings(ref.group, ref.position, ctx.tables);
3760
3919
  if (team) {
3761
3920
  const status = isGroupComplete(ref.group, ctx.tables) ? "confirmed" : "projected";
@@ -4389,13 +4548,17 @@ function resolveCompetition(explicit) {
4389
4548
  }
4390
4549
  return DEFAULT_COMPETITION;
4391
4550
  }
4551
+ var BUNDLE_COMPETITION = DEFAULT_COMPETITION;
4552
+ function bundleApplies(competition = resolveCompetition()) {
4553
+ return competition === BUNDLE_COMPETITION;
4554
+ }
4392
4555
  var KNOWN_SOURCES = ["espn"];
4393
4556
  function makeAdapter(source = "espn", opts = {}) {
4394
4557
  switch (source) {
4395
4558
  case "espn": {
4396
4559
  const competition = resolveCompetition();
4397
4560
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
4398
- return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups });
4561
+ return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups, now: opts.now });
4399
4562
  }
4400
4563
  default:
4401
4564
  throw new Error(
@@ -4413,7 +4576,7 @@ function liveSourceLabel(source) {
4413
4576
  return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
4414
4577
  }
4415
4578
  async function getMatchesForDate(adapter, dateISO) {
4416
- const base = allFixtures();
4579
+ const base = bundleApplies() ? allFixtures() : [];
4417
4580
  const day = dateISO.slice(0, 10);
4418
4581
  try {
4419
4582
  const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
@@ -4458,21 +4621,27 @@ function knockoutWindow() {
4458
4621
  return knockoutWindowMemo;
4459
4622
  }
4460
4623
  async function getBracket(adapter, opts = {}) {
4624
+ if (!bundleApplies()) {
4625
+ const view2 = { stages: [], degraded: false, standingsDegraded: false, unsupported: true };
4626
+ return { view: view2, degraded: false, standingsDegraded: false, unsupported: true };
4627
+ }
4461
4628
  const topology = loadBracketTopology();
4462
4629
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
4463
4630
  let matches = base;
4464
4631
  let liveDegraded = true;
4465
4632
  let source;
4466
- try {
4467
- const win = knockoutWindow();
4468
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4469
- matches = mergeLive(base, live);
4470
- liveDegraded = false;
4471
- source = adapter.name;
4472
- } catch {
4633
+ const win = knockoutWindow();
4634
+ if (adapter.fetchWindow && win) {
4635
+ try {
4636
+ const live = await adapter.fetchWindow(win.start, win.end);
4637
+ matches = mergeLive(base, live);
4638
+ liveDegraded = false;
4639
+ source = adapter.name;
4640
+ } catch {
4641
+ }
4473
4642
  }
4474
4643
  const standings = await getStandings(adapter);
4475
- if (!source && !standings.degraded && standings.source) {
4644
+ if (!source && !standings.degraded && standings.source && standings.tables.length > 0) {
4476
4645
  source = standings.source;
4477
4646
  }
4478
4647
  const view = buildBracketView(
@@ -4494,18 +4663,18 @@ async function getBracket(adapter, opts = {}) {
4494
4663
  }
4495
4664
  var EXTRA_TIME_SLACK_MS = 60 * 6e4;
4496
4665
  async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
4666
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4497
4667
  const nowMs = now.getTime();
4498
4668
  let fixtures = allFixtures();
4499
4669
  let overlayFailed = false;
4500
- try {
4501
- const win = knockoutWindow();
4502
- if (adapter.fetchWindow && win) {
4503
- fixtures = mergeLive(
4504
- fixtures,
4505
- await adapter.fetchWindow(win.start, win.end)
4506
- );
4670
+ const win = knockoutWindow();
4671
+ if (adapter.fetchWindow && win) {
4672
+ try {
4673
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
4674
+ } catch {
4675
+ overlayFailed = true;
4507
4676
  }
4508
- } catch {
4677
+ } else {
4509
4678
  overlayFailed = true;
4510
4679
  }
4511
4680
  const candidate = fixturesByTeam(code, fixtures).find((m) => {
@@ -4521,23 +4690,27 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4521
4690
  return { match: next, degraded: overlayFailed };
4522
4691
  }
4523
4692
  async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
4693
+ if (!bundleApplies()) return { fixture: void 0, degraded: false, unsupported: true };
4524
4694
  const base = allFixtures();
4525
4695
  let matches = base;
4526
4696
  let degraded = true;
4527
4697
  let liveById;
4528
- try {
4529
- const win = knockoutWindow();
4530
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4531
- matches = mergeLive(base, live);
4532
- degraded = false;
4533
- liveById = new Set(live.map((m) => m.id));
4534
- } catch {
4698
+ const win = knockoutWindow();
4699
+ if (adapter.fetchWindow && win) {
4700
+ try {
4701
+ const live = await adapter.fetchWindow(win.start, win.end);
4702
+ matches = mergeLive(base, live);
4703
+ degraded = false;
4704
+ liveById = new Set(live.map((m) => m.id));
4705
+ } catch {
4706
+ }
4535
4707
  }
4536
4708
  const fixture = nextFixtureForTeam(code, { from: now, fixtures: matches });
4537
4709
  const source = fixture && liveById?.has(fixture.id) ? adapter.name : void 0;
4538
4710
  return { fixture, degraded, source };
4539
4711
  }
4540
4712
  async function getMatchById(adapter, id) {
4713
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4541
4714
  const base = allFixtures().find((m) => m.id === id);
4542
4715
  if (!base) return { match: void 0, degraded: false };
4543
4716
  const day = base.kickoff.slice(0, 10);
@@ -4975,11 +5148,15 @@ var PolymarketProvider = class {
4975
5148
  if (!res.ok) {
4976
5149
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4977
5150
  }
4978
- const length = Number(res.headers?.get?.("content-length"));
4979
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4980
- throw new Error(`Polymarket response too large: ${length} bytes`);
5151
+ let data;
5152
+ try {
5153
+ data = await readJsonBounded(res, MAX_RESPONSE_BYTES);
5154
+ } catch (e) {
5155
+ if (e instanceof ResponseTooLargeError) {
5156
+ throw new Error(`Polymarket response too large: ${e.bytes} bytes`);
5157
+ }
5158
+ throw e;
4981
5159
  }
4982
- const data = await res.json();
4983
5160
  if (Array.isArray(data) && data.length > 1) {
4984
5161
  return ambiguous("slug returned more than one event");
4985
5162
  }
@@ -5386,10 +5563,13 @@ function formatShareTable(input, options = {}) {
5386
5563
  input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5387
5564
  );
5388
5565
  } else {
5389
- for (const { group, rows } of input.tables) {
5390
- blocks.push(
5391
- [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
5392
- );
5566
+ for (const { group, rows, partial } of input.tables) {
5567
+ const lines = [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, r.rank ?? i + 1))];
5568
+ if (partial) {
5569
+ const n = partial.omitted;
5570
+ lines.push("", `(partial table \u2014 ${n} row${n === 1 ? "" : "s"} unreadable; positions are the provider's ranks)`);
5571
+ }
5572
+ blocks.push(lines.join("\n"));
5393
5573
  }
5394
5574
  if (input.degraded) {
5395
5575
  blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
@@ -5754,9 +5934,19 @@ ${matchList(matches, "No matches in play right now.", opts)}`;
5754
5934
  };
5755
5935
  }
5756
5936
  async function toolGetMatch(args) {
5757
- const { match, degraded, source: liveSource } = await getMatchById(resolveAdapter(args), args.id);
5937
+ const { match, degraded, source: liveSource, unsupported } = await getMatchById(
5938
+ resolveAdapter(args),
5939
+ args.id
5940
+ );
5758
5941
  if (!match) {
5759
- return { text: withDisclaimer(`No match found with id ${args.id}.`), data: { match: null } };
5942
+ return {
5943
+ text: withDisclaimer(
5944
+ unsupported ? t(args.lang, "competition.unsupported") : `No match found with id ${args.id}.`,
5945
+ void 0,
5946
+ args.lang
5947
+ ),
5948
+ data: { match: null }
5949
+ };
5760
5950
  }
5761
5951
  const opts = fmtOpts(args);
5762
5952
  const now = args.now ?? /* @__PURE__ */ new Date();
@@ -5789,7 +5979,11 @@ ${marketBlock(marketSignal, match).join("\n")}` : base;
5789
5979
  async function toolGetStandings(args) {
5790
5980
  const { tables, degraded, source } = await getStandings(resolveAdapter(args), args.group);
5791
5981
  const boundedTables = boundedRecords(tables);
5792
- const shaped = boundedTables.items.map((tb) => ({ group: tb.group, standings: tb.rows }));
5982
+ const shaped = boundedTables.items.map((tb) => ({
5983
+ group: tb.group,
5984
+ standings: tb.rows,
5985
+ ...tb.partial ? { partial: tb.partial } : {}
5986
+ }));
5793
5987
  if (shaped.length === 0) {
5794
5988
  const g = args.group?.toUpperCase();
5795
5989
  const msg = degraded ? t(args.lang, "standings.unavailable") : g ? `No group "${g}".` : "No standings available.";
@@ -5798,7 +5992,11 @@ async function toolGetStandings(args) {
5798
5992
  data: { degraded, source: source ?? null, tables: args.group ? null : [] }
5799
5993
  };
5800
5994
  }
5801
- let text = shaped.map((t2) => standingsTable(t2.group, t2.standings)).join("\n\n");
5995
+ let text = shaped.map((tb) => {
5996
+ const block = standingsTable(tb.group, tb.standings);
5997
+ return tb.partial ? `${block}
5998
+ (${t(args.lang, "standings.partial", { n: String(tb.partial.omitted) })})` : block;
5999
+ }).join("\n\n");
5802
6000
  text += truncationNote(boundedTables);
5803
6001
  if (degraded) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5804
6002
  return {
@@ -5819,10 +6017,16 @@ async function toolGetBracket(args) {
5819
6017
  data: { view: null }
5820
6018
  };
5821
6019
  }
5822
- const { view, degraded, standingsDegraded, source } = await getBracket(
6020
+ const { view, degraded, standingsDegraded, source, unsupported } = await getBracket(
5823
6021
  resolveAdapter(args),
5824
6022
  filter ? { stage: filter, lang: args.lang } : { lang: args.lang }
5825
6023
  );
6024
+ if (unsupported) {
6025
+ return {
6026
+ text: withDisclaimer(t(args.lang, "competition.unsupported"), void 0, args.lang),
6027
+ data: { degraded, standingsDegraded, source: null, view }
6028
+ };
6029
+ }
5826
6030
  let text = formatBracketList(view, { footer: false, locale: args.lang, tz: args.tz });
5827
6031
  if (degraded) {
5828
6032
  text += `
@@ -5843,18 +6047,20 @@ async function standingsResourceText(group, adapter) {
5843
6047
  const { tables, degraded, source } = await getStandings(adapter, g);
5844
6048
  const tb = tables[0];
5845
6049
  let text = tb ? standingsTable(tb.group, tb.rows) : degraded ? "Live standings unavailable." : `No group ${g}.`;
6050
+ if (tb?.partial) text += `
6051
+ (${t(void 0, "standings.partial", { n: String(tb.partial.omitted) })})`;
5846
6052
  if (degraded && tb) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5847
6053
  return withDisclaimer(text, source);
5848
6054
  }
5849
6055
  async function toolGetNextFixture(args) {
5850
6056
  const code = args.team.toUpperCase();
5851
- const { fixture, degraded, source } = await getNextFixtureForTeam(
6057
+ const { fixture, degraded, source, unsupported } = await getNextFixtureForTeam(
5852
6058
  resolveAdapter(args),
5853
6059
  code,
5854
6060
  args.now ?? /* @__PURE__ */ new Date()
5855
6061
  );
5856
6062
  if (!fixture) {
5857
- const msg = degraded ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`;
6063
+ const msg = unsupported ? t(args.lang, "competition.unsupported") : degraded ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`;
5858
6064
  return {
5859
6065
  text: withDisclaimer(msg, void 0, args.lang),
5860
6066
  data: { team: code, fixture: null, degraded, source: source ?? null }
@@ -5886,12 +6092,12 @@ async function toolGetMarketSignal(args) {
5886
6092
  const provider = resolveMarketProvider(args);
5887
6093
  const now = args.now ?? /* @__PURE__ */ new Date();
5888
6094
  if (args.matchId) {
5889
- const { match } = await getMatchById(resolveAdapter(args), args.matchId);
6095
+ const { match, unsupported } = await getMatchById(resolveAdapter(args), args.matchId);
5890
6096
  const relevant = match ? marketRelevant(match, now) : false;
5891
6097
  const batch2 = match && relevant ? await getMarketSignals(provider, [match], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5892
6098
  const sig = match ? resolvedValues(batch2).get(match.id) : void 0;
5893
6099
  const shown2 = batch2.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
5894
- const text2 = !match ? `No match found with id ${args.matchId}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(match, args)} \u2014 this match could not be checked.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now);
6100
+ const text2 = !match ? unsupported ? t(args.lang, "competition.unsupported") : `No match found with id ${args.matchId}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(match, args)} \u2014 this match could not be checked.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now);
5895
6101
  return {
5896
6102
  text: withDisclaimer(text2),
5897
6103
  data: {
@@ -5904,12 +6110,16 @@ async function toolGetMarketSignal(args) {
5904
6110
  }
5905
6111
  if (args.team) {
5906
6112
  const code = args.team.toUpperCase();
5907
- const { match: fixture, degraded } = await marketFixtureForTeam(resolveAdapter(args), code, now);
6113
+ const { match: fixture, degraded, unsupported } = await marketFixtureForTeam(
6114
+ resolveAdapter(args),
6115
+ code,
6116
+ now
6117
+ );
5908
6118
  const relevant = fixture ? marketRelevant(fixture, now) : false;
5909
6119
  const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5910
6120
  const sig = fixture ? resolvedValues(batch2).get(fixture.id) : void 0;
5911
6121
  const shown2 = batch2.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
5912
- const text2 = !fixture ? degraded ? `Live feed unavailable \u2014 can't resolve ${code}'s next fixture right now.` : `No upcoming fixture found for ${code}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(fixture, args)} \u2014 this match could not be checked.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
6122
+ const text2 = !fixture ? unsupported ? t(args.lang, "competition.unsupported") : degraded ? `Live feed unavailable \u2014 can't resolve ${code}'s next fixture right now.` : `No upcoming fixture found for ${code}.` : !batch2.complete ? `Market data unavailable or incomplete for ${marketHeader(fixture, args)} \u2014 this match could not be checked.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
5913
6123
  return {
5914
6124
  text: withDisclaimer(text2),
5915
6125
  data: {
@@ -6069,7 +6279,12 @@ async function toolGetShareSnippet(args) {
6069
6279
  degraded: degraded2,
6070
6280
  informationalOnly: true,
6071
6281
  snippet,
6072
- tables: boundedRecords(tables).items.map((tb) => ({ group: tb.group, standings: tb.rows }))
6282
+ // The structured card keeps the verdict the snippet warns about (A01).
6283
+ tables: boundedRecords(tables).items.map((tb) => ({
6284
+ group: tb.group,
6285
+ standings: tb.rows,
6286
+ ...tb.partial ? { partial: tb.partial } : {}
6287
+ }))
6073
6288
  }
6074
6289
  };
6075
6290
  }
@@ -6085,7 +6300,7 @@ async function toolGetShareSnippet(args) {
6085
6300
  data: { kind: "bracket", view: null }
6086
6301
  };
6087
6302
  }
6088
- const { view, degraded: degraded2, source: source2 } = await getBracket(
6303
+ const { view, degraded: degraded2, source: source2, unsupported } = await getBracket(
6089
6304
  resolveAdapter(args),
6090
6305
  stageFilter ? { stage: stageFilter, lang: args.lang } : { lang: args.lang }
6091
6306
  );
@@ -6094,7 +6309,7 @@ async function toolGetShareSnippet(args) {
6094
6309
  view,
6095
6310
  source: degraded2 ? void 0 : source2,
6096
6311
  installLine: stageFilter ? `npx @claudinho/cli bracket ${stageFilter}` : "npx @claudinho/cli bracket",
6097
- emptyNote: t(args.lang, "bracket.empty")
6312
+ emptyNote: unsupported ? t(args.lang, "competition.unsupported") : t(args.lang, "bracket.empty")
6098
6313
  },
6099
6314
  { ...options, locale: args.lang, tz: args.tz }
6100
6315
  );
@@ -6113,7 +6328,10 @@ async function toolGetShareSnippet(args) {
6113
6328
  };
6114
6329
  }
6115
6330
  if (args.matchId) {
6116
- const { match, degraded: degraded2, source: source2 } = await getMatchById(resolveAdapter(args), args.matchId);
6331
+ const { match, degraded: degraded2, source: source2, unsupported } = await getMatchById(
6332
+ resolveAdapter(args),
6333
+ args.matchId
6334
+ );
6117
6335
  const matches = match ? [match] : [];
6118
6336
  const market2 = await signalsFor(matches);
6119
6337
  return shareResult(
@@ -6127,7 +6345,7 @@ async function toolGetShareSnippet(args) {
6127
6345
  marketComplete: market2.complete,
6128
6346
  source: source2,
6129
6347
  degraded: degraded2,
6130
- emptyNote: `No match found with id ${args.matchId}.`,
6348
+ emptyNote: unsupported ? t(args.lang, "competition.unsupported") : `No match found with id ${args.matchId}.`,
6131
6349
  installLine: `npx @claudinho/cli match ${args.matchId}`,
6132
6350
  tz: args.tz,
6133
6351
  locale: args.lang
@@ -6137,7 +6355,7 @@ async function toolGetShareSnippet(args) {
6137
6355
  }
6138
6356
  if (args.team) {
6139
6357
  const code = args.team.toUpperCase();
6140
- const { fixture, degraded: degraded2, source: source2 } = await getNextFixtureForTeam(
6358
+ const { fixture, degraded: degraded2, source: source2, unsupported } = await getNextFixtureForTeam(
6141
6359
  resolveAdapter(args),
6142
6360
  code,
6143
6361
  args.now ?? /* @__PURE__ */ new Date()
@@ -6158,7 +6376,7 @@ async function toolGetShareSnippet(args) {
6158
6376
  // with get_next_fixture (a static group fixture carries no source).
6159
6377
  source: source2,
6160
6378
  degraded: degraded2,
6161
- emptyNote: degraded2 ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`,
6379
+ emptyNote: unsupported ? t(args.lang, "competition.unsupported") : degraded2 ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`,
6162
6380
  installLine: `npx @claudinho/cli next ${code}`,
6163
6381
  tz: args.tz,
6164
6382
  locale: args.lang
@@ -6197,7 +6415,7 @@ async function toolGetShareSnippet(args) {
6197
6415
 
6198
6416
  // src/server.ts
6199
6417
  var SERVER_NAME = "claudinho";
6200
- var SERVER_VERSION = "0.10.0";
6418
+ var SERVER_VERSION = "0.10.1";
6201
6419
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
6202
6420
  Voice: when relaying scores, narrate with lively, regionally-appropriate football-commentary energy in the user's language. Each match line may end with a short exclamation ("\u2014 \xA1GOOOOL!") \u2014 use it as a tone cue. Keep every fact exact; never invent details and never impersonate or name a real commentator.`;
6203
6421
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -6648,7 +6866,7 @@ function buildServer() {
6648
6866
  },
6649
6867
  async (uri, variables) => {
6650
6868
  const group = String(variables.group ?? "");
6651
- const text = await standingsResourceText(group, makeAdapter());
6869
+ const text = await standingsResourceText(group, resolveAdapter({}));
6652
6870
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text }] };
6653
6871
  }
6654
6872
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "mcpName": "io.github.arturogarrido/claudinho",
5
5
  "description": "World Cup MCP server for the 2026 men's football tournament — live scores, fixtures, standings, read-only prediction-market signals, and paste-ready match cards. Works with Claude Code, Cursor, Codex, Windsurf, Zed. Not affiliated with FIFA or Anthropic.",
6
6
  "type": "module",
@@ -55,11 +55,11 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^22.20.2",
58
- "@vitest/coverage-v8": "^4.1.11",
58
+ "@vitest/coverage-v8": "^5.0.0",
59
59
  "tsup": "^8.0.0",
60
60
  "typescript": "^5.7.0",
61
- "vitest": "^4.1.11",
62
- "@claudinho/core": "0.10.0"
61
+ "vitest": "^5.0.0",
62
+ "@claudinho/core": "0.10.1"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsup",