@claudinho/mcp 0.9.4 → 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 +311 -83
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  // src/server.ts
7
7
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { types as utilTypes } from "util";
9
- import { z } from "zod";
9
+ import { z } from "zod/v3";
10
10
 
11
11
  // ../core/dist/index.js
12
12
  var REGIONAL_INDICATOR_A = 127462;
@@ -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,30 +3434,37 @@ 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
- const seenTeams = /* @__PURE__ */ new Set();
3444
+ const seenCodes = /* @__PURE__ */ new Map();
3429
3445
  const seenRanks = /* @__PURE__ */ new Set();
3430
3446
  const ranked = [];
3431
3447
  for (const e of entries) {
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
- const key = r.value.providerId ?? r.value.team.code;
3438
- if (seenTeams.has(key) || seenRanks.has(r.value.providerRank) || r.value.providerId !== void 0 && seenProviderIds.has(r.value.providerId)) {
3454
+ const { providerId, providerRank } = r.value;
3455
+ const code = r.value.team.code;
3456
+ const priorHadId = seenCodes.get(code);
3457
+ const codeCollision = priorHadId !== void 0 && (providerId === void 0 || priorHadId === false);
3458
+ if (codeCollision || seenRanks.has(providerRank) || providerId !== void 0 && seenProviderIds.has(providerId)) {
3439
3459
  complete = false;
3460
+ omitted += 1;
3440
3461
  continue;
3441
3462
  }
3442
- seenTeams.add(key);
3443
- seenRanks.add(r.value.providerRank);
3444
- if (r.value.providerId !== void 0) seenProviderIds.add(r.value.providerId);
3463
+ seenCodes.set(code, providerId !== void 0);
3464
+ seenRanks.add(providerRank);
3465
+ if (providerId !== void 0) seenProviderIds.add(providerId);
3445
3466
  const { providerId: _dropId, providerRank: rank, ...row } = r.value;
3446
- ranked.push({ row, rank });
3467
+ ranked.push({ row: { ...row, rank }, rank });
3447
3468
  }
3448
3469
  ranked.sort((a, b) => {
3449
3470
  if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
@@ -3455,7 +3476,11 @@ function parseEspnStandings(raw) {
3455
3476
  complete = false;
3456
3477
  continue;
3457
3478
  }
3458
- 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
+ });
3459
3484
  }
3460
3485
  return {
3461
3486
  items: out,
@@ -3466,11 +3491,72 @@ function parseEspnStandings(raw) {
3466
3491
  complete: complete && !rowsTruncated
3467
3492
  };
3468
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
+ }
3469
3541
  var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
3470
3542
  var DEFAULT_COMPETITION = "fifa.world";
3471
3543
  var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
3472
- var USER_AGENT = `claudinho/${"0.9.4"} (+https://github.com/arturogarrido/claudinho)`;
3544
+ var USER_AGENT = `claudinho/${"0.10.1"} (+https://github.com/arturogarrido/claudinho)`;
3473
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
+ }
3474
3560
  function competitionBase(slug) {
3475
3561
  return `${ESPN_SOCCER}/${slug}`;
3476
3562
  }
@@ -3479,6 +3565,8 @@ var STANDINGS_SHARE_MS = 3e4;
3479
3565
  var ProviderError = class extends Error {
3480
3566
  kind;
3481
3567
  status;
3568
+ /** For a throttle: how long the adapter will refuse to fetch (bounded). */
3569
+ retryAfterMs;
3482
3570
  constructor(message, kind, status) {
3483
3571
  super(message);
3484
3572
  this.name = "ProviderError";
@@ -3502,6 +3590,7 @@ function usableProviderItems(kind, parsed, hasUsableRecord = parsed.items.length
3502
3590
  var EspnAdapter = class {
3503
3591
  constructor(opts = {}) {
3504
3592
  this.opts = opts;
3593
+ this.clock = opts.now ?? (() => Date.now());
3505
3594
  const expected = opts.expectedStandingsGroups ?? (opts.baseUrl === void 0 ? groups() : void 0);
3506
3595
  this.expectedStandingsGroups = expected ? [...expected] : void 0;
3507
3596
  this.standingsFallbackGroups = opts.baseUrl === void 0 && expected ? [...expected] : void 0;
@@ -3527,6 +3616,53 @@ var EspnAdapter = class {
3527
3616
  * throttle (persist a backoff) from an ordinary blip.
3528
3617
  */
3529
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
+ }
3530
3666
  async fetchByDate(dateISO) {
3531
3667
  return this.fetchScoreboard(toEspnDate(dateISO));
3532
3668
  }
@@ -3614,6 +3750,11 @@ var EspnAdapter = class {
3614
3750
  return usableProviderItems("scoreboard", parsed);
3615
3751
  }
3616
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
+ }
3617
3758
  const doFetch = this.opts.fetchImpl ?? fetch;
3618
3759
  const controller = new AbortController();
3619
3760
  const timer = setTimeout(
@@ -3635,19 +3776,24 @@ var EspnAdapter = class {
3635
3776
  throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
3636
3777
  }
3637
3778
  if (!res.ok) {
3638
- throw new ProviderError(
3779
+ const pe = new ProviderError(
3639
3780
  `ESPN request failed: ${res.status} ${res.statusText}`,
3640
3781
  "http",
3641
3782
  res.status
3642
3783
  );
3643
- }
3644
- const length = Number(res.headers?.get?.("content-length"));
3645
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
3646
- 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;
3647
3790
  }
3648
3791
  try {
3649
- return await res.json();
3792
+ return await readJsonBounded(res, MAX_RESPONSE_BYTES);
3650
3793
  } catch (e) {
3794
+ if (e instanceof ResponseTooLargeError) {
3795
+ throw new ProviderError(`ESPN response too large: ${e.bytes} bytes`, "parse");
3796
+ }
3651
3797
  throw new ProviderError(
3652
3798
  `ESPN response unparseable: ${e?.message ?? e}`,
3653
3799
  "parse"
@@ -3690,14 +3836,30 @@ function hasGroupStarted(group, tables) {
3690
3836
  function matchesPerTeamInGroup(teamCount) {
3691
3837
  return Math.max(0, teamCount - 1);
3692
3838
  }
3693
- function isGroupStandingsComplete(table) {
3694
- const n = table?.rows.length ?? 0;
3839
+ function isGroupStandingsComplete(table, expectedTeams) {
3840
+ if (!table || table.partial) return false;
3841
+ const n = table.rows.length;
3695
3842
  if (n < 2) return false;
3696
- const required = matchesPerTeamInGroup(n);
3843
+ if (expectedTeams !== void 0 && n < expectedTeams) return false;
3844
+ const required = matchesPerTeamInGroup(Math.max(n, expectedTeams ?? n));
3697
3845
  return table.rows.every((r) => r.played >= required);
3698
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
+ }
3699
3855
  function isGroupComplete(group, tables) {
3700
- 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;
3701
3863
  }
3702
3864
  function resolveWinner(match) {
3703
3865
  if (!isFinished(match.status)) return void 0;
@@ -3752,7 +3914,7 @@ function resolveSlot(ref, ctx, liveTeam, fixtureInMergedSet = false) {
3752
3914
  return tbd(ref.label);
3753
3915
  case "group": {
3754
3916
  if (liveParticipant) return liveParticipant;
3755
- if (!ctx.standingsDegraded && hasGroupStarted(ref.group, ctx.tables)) {
3917
+ if (!ctx.standingsDegraded && !isGroupPartial(ref.group, ctx.tables) && hasGroupStarted(ref.group, ctx.tables)) {
3756
3918
  const team = teamFromStandings(ref.group, ref.position, ctx.tables);
3757
3919
  if (team) {
3758
3920
  const status = isGroupComplete(ref.group, ctx.tables) ? "confirmed" : "projected";
@@ -4386,13 +4548,17 @@ function resolveCompetition(explicit) {
4386
4548
  }
4387
4549
  return DEFAULT_COMPETITION;
4388
4550
  }
4551
+ var BUNDLE_COMPETITION = DEFAULT_COMPETITION;
4552
+ function bundleApplies(competition = resolveCompetition()) {
4553
+ return competition === BUNDLE_COMPETITION;
4554
+ }
4389
4555
  var KNOWN_SOURCES = ["espn"];
4390
- function makeAdapter(source = "espn") {
4556
+ function makeAdapter(source = "espn", opts = {}) {
4391
4557
  switch (source) {
4392
4558
  case "espn": {
4393
4559
  const competition = resolveCompetition();
4394
4560
  const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
4395
- return new EspnAdapter({ baseUrl });
4561
+ return new EspnAdapter({ baseUrl, enrichGroups: opts.enrichGroups, now: opts.now });
4396
4562
  }
4397
4563
  default:
4398
4564
  throw new Error(
@@ -4410,7 +4576,7 @@ function liveSourceLabel(source) {
4410
4576
  return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
4411
4577
  }
4412
4578
  async function getMatchesForDate(adapter, dateISO) {
4413
- const base = allFixtures();
4579
+ const base = bundleApplies() ? allFixtures() : [];
4414
4580
  const day = dateISO.slice(0, 10);
4415
4581
  try {
4416
4582
  const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
@@ -4455,21 +4621,27 @@ function knockoutWindow() {
4455
4621
  return knockoutWindowMemo;
4456
4622
  }
4457
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
+ }
4458
4628
  const topology = loadBracketTopology();
4459
4629
  const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
4460
4630
  let matches = base;
4461
4631
  let liveDegraded = true;
4462
4632
  let source;
4463
- try {
4464
- const win = knockoutWindow();
4465
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4466
- matches = mergeLive(base, live);
4467
- liveDegraded = false;
4468
- source = adapter.name;
4469
- } 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
+ }
4470
4642
  }
4471
4643
  const standings = await getStandings(adapter);
4472
- if (!source && !standings.degraded && standings.source) {
4644
+ if (!source && !standings.degraded && standings.source && standings.tables.length > 0) {
4473
4645
  source = standings.source;
4474
4646
  }
4475
4647
  const view = buildBracketView(
@@ -4491,18 +4663,18 @@ async function getBracket(adapter, opts = {}) {
4491
4663
  }
4492
4664
  var EXTRA_TIME_SLACK_MS = 60 * 6e4;
4493
4665
  async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
4666
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4494
4667
  const nowMs = now.getTime();
4495
4668
  let fixtures = allFixtures();
4496
4669
  let overlayFailed = false;
4497
- try {
4498
- const win = knockoutWindow();
4499
- if (adapter.fetchWindow && win) {
4500
- fixtures = mergeLive(
4501
- fixtures,
4502
- await adapter.fetchWindow(win.start, win.end)
4503
- );
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;
4504
4676
  }
4505
- } catch {
4677
+ } else {
4506
4678
  overlayFailed = true;
4507
4679
  }
4508
4680
  const candidate = fixturesByTeam(code, fixtures).find((m) => {
@@ -4518,23 +4690,27 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
4518
4690
  return { match: next, degraded: overlayFailed };
4519
4691
  }
4520
4692
  async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
4693
+ if (!bundleApplies()) return { fixture: void 0, degraded: false, unsupported: true };
4521
4694
  const base = allFixtures();
4522
4695
  let matches = base;
4523
4696
  let degraded = true;
4524
4697
  let liveById;
4525
- try {
4526
- const win = knockoutWindow();
4527
- const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
4528
- matches = mergeLive(base, live);
4529
- degraded = false;
4530
- liveById = new Set(live.map((m) => m.id));
4531
- } 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
+ }
4532
4707
  }
4533
4708
  const fixture = nextFixtureForTeam(code, { from: now, fixtures: matches });
4534
4709
  const source = fixture && liveById?.has(fixture.id) ? adapter.name : void 0;
4535
4710
  return { fixture, degraded, source };
4536
4711
  }
4537
4712
  async function getMatchById(adapter, id) {
4713
+ if (!bundleApplies()) return { match: void 0, degraded: false, unsupported: true };
4538
4714
  const base = allFixtures().find((m) => m.id === id);
4539
4715
  if (!base) return { match: void 0, degraded: false };
4540
4716
  const day = base.kickoff.slice(0, 10);
@@ -4972,11 +5148,15 @@ var PolymarketProvider = class {
4972
5148
  if (!res.ok) {
4973
5149
  throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
4974
5150
  }
4975
- const length = Number(res.headers?.get?.("content-length"));
4976
- if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
4977
- 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;
4978
5159
  }
4979
- const data = await res.json();
4980
5160
  if (Array.isArray(data) && data.length > 1) {
4981
5161
  return ambiguous("slug returned more than one event");
4982
5162
  }
@@ -5237,6 +5417,10 @@ function numberish(v) {
5237
5417
  }
5238
5418
  return void 0;
5239
5419
  }
5420
+ var MARKET_COMPETITIONS = /* @__PURE__ */ new Set([DEFAULT_COMPETITION]);
5421
+ function marketsCoverCompetition(competition = resolveCompetition()) {
5422
+ return MARKET_COMPETITIONS.has(competition);
5423
+ }
5240
5424
  function resolveMarketSource(explicit) {
5241
5425
  if (explicit) return explicit;
5242
5426
  if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
@@ -5253,6 +5437,7 @@ function makeMarketProvider(source) {
5253
5437
  return new FakeMarketProvider();
5254
5438
  // no synth → yields no signals, no network
5255
5439
  default:
5440
+ if (!marketsCoverCompetition()) return new FakeMarketProvider();
5256
5441
  return new PolymarketProvider();
5257
5442
  }
5258
5443
  }
@@ -5378,10 +5563,13 @@ function formatShareTable(input, options = {}) {
5378
5563
  input.emptyNote ?? (input.degraded ? "Live standings unavailable." : "No standings available.")
5379
5564
  );
5380
5565
  } else {
5381
- for (const { group, rows } of input.tables) {
5382
- blocks.push(
5383
- [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
5384
- );
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"));
5385
5573
  }
5386
5574
  if (input.degraded) {
5387
5575
  blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
@@ -5584,7 +5772,9 @@ function marketText(m, sig, args) {
5584
5772
  return `${marketHeader(m, args)}
5585
5773
  ${marketBlock(sig, m).join("\n")}`;
5586
5774
  }
5775
+ var MARKETS_SCOPE_NOTE = "Market signals cover the World Cup only; none are read for this competition.";
5587
5776
  function noSignalText(m, args, now) {
5777
+ if (!marketsCoverCompetition()) return `${marketHeader(m, args)} \u2014 ${MARKETS_SCOPE_NOTE}`;
5588
5778
  if (marketRelevant(m, now)) return `No reliable market signal for ${marketHeader(m, args)}.`;
5589
5779
  const verb = isFinished(m.status) ? "has finished" : "appears to have finished";
5590
5780
  return `${marketHeader(m, args)} ${verb} \u2014 market signals are pre-match and in-play reads.`;
@@ -5744,9 +5934,19 @@ ${matchList(matches, "No matches in play right now.", opts)}`;
5744
5934
  };
5745
5935
  }
5746
5936
  async function toolGetMatch(args) {
5747
- 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
+ );
5748
5941
  if (!match) {
5749
- 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
+ };
5750
5950
  }
5751
5951
  const opts = fmtOpts(args);
5752
5952
  const now = args.now ?? /* @__PURE__ */ new Date();
@@ -5779,7 +5979,11 @@ ${marketBlock(marketSignal, match).join("\n")}` : base;
5779
5979
  async function toolGetStandings(args) {
5780
5980
  const { tables, degraded, source } = await getStandings(resolveAdapter(args), args.group);
5781
5981
  const boundedTables = boundedRecords(tables);
5782
- 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
+ }));
5783
5987
  if (shaped.length === 0) {
5784
5988
  const g = args.group?.toUpperCase();
5785
5989
  const msg = degraded ? t(args.lang, "standings.unavailable") : g ? `No group "${g}".` : "No standings available.";
@@ -5788,7 +5992,11 @@ async function toolGetStandings(args) {
5788
5992
  data: { degraded, source: source ?? null, tables: args.group ? null : [] }
5789
5993
  };
5790
5994
  }
5791
- 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");
5792
6000
  text += truncationNote(boundedTables);
5793
6001
  if (degraded) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5794
6002
  return {
@@ -5809,10 +6017,16 @@ async function toolGetBracket(args) {
5809
6017
  data: { view: null }
5810
6018
  };
5811
6019
  }
5812
- const { view, degraded, standingsDegraded, source } = await getBracket(
6020
+ const { view, degraded, standingsDegraded, source, unsupported } = await getBracket(
5813
6021
  resolveAdapter(args),
5814
6022
  filter ? { stage: filter, lang: args.lang } : { lang: args.lang }
5815
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
+ }
5816
6030
  let text = formatBracketList(view, { footer: false, locale: args.lang, tz: args.tz });
5817
6031
  if (degraded) {
5818
6032
  text += `
@@ -5833,18 +6047,20 @@ async function standingsResourceText(group, adapter) {
5833
6047
  const { tables, degraded, source } = await getStandings(adapter, g);
5834
6048
  const tb = tables[0];
5835
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) })})`;
5836
6052
  if (degraded && tb) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
5837
6053
  return withDisclaimer(text, source);
5838
6054
  }
5839
6055
  async function toolGetNextFixture(args) {
5840
6056
  const code = args.team.toUpperCase();
5841
- const { fixture, degraded, source } = await getNextFixtureForTeam(
6057
+ const { fixture, degraded, source, unsupported } = await getNextFixtureForTeam(
5842
6058
  resolveAdapter(args),
5843
6059
  code,
5844
6060
  args.now ?? /* @__PURE__ */ new Date()
5845
6061
  );
5846
6062
  if (!fixture) {
5847
- 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}.`;
5848
6064
  return {
5849
6065
  text: withDisclaimer(msg, void 0, args.lang),
5850
6066
  data: { team: code, fixture: null, degraded, source: source ?? null }
@@ -5876,12 +6092,12 @@ async function toolGetMarketSignal(args) {
5876
6092
  const provider = resolveMarketProvider(args);
5877
6093
  const now = args.now ?? /* @__PURE__ */ new Date();
5878
6094
  if (args.matchId) {
5879
- const { match } = await getMatchById(resolveAdapter(args), args.matchId);
6095
+ const { match, unsupported } = await getMatchById(resolveAdapter(args), args.matchId);
5880
6096
  const relevant = match ? marketRelevant(match, now) : false;
5881
6097
  const batch2 = match && relevant ? await getMarketSignals(provider, [match], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5882
6098
  const sig = match ? resolvedValues(batch2).get(match.id) : void 0;
5883
6099
  const shown2 = batch2.complete && match && sig && marketDisplayable(match, sig) ? sig : void 0;
5884
- 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);
5885
6101
  return {
5886
6102
  text: withDisclaimer(text2),
5887
6103
  data: {
@@ -5894,12 +6110,16 @@ async function toolGetMarketSignal(args) {
5894
6110
  }
5895
6111
  if (args.team) {
5896
6112
  const code = args.team.toUpperCase();
5897
- 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
+ );
5898
6118
  const relevant = fixture ? marketRelevant(fixture, now) : false;
5899
6119
  const batch2 = fixture && relevant ? await getMarketSignals(provider, [fixture], MARKETS_TOOL_OPTS) : { results: /* @__PURE__ */ new Map(), complete: true };
5900
6120
  const sig = fixture ? resolvedValues(batch2).get(fixture.id) : void 0;
5901
6121
  const shown2 = batch2.complete && fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
5902
- 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);
5903
6123
  return {
5904
6124
  text: withDisclaimer(text2),
5905
6125
  data: {
@@ -5929,7 +6149,7 @@ ${shown.items.map(({ match, signal }) => marketText(match, signal, args)).join("
5929
6149
  // so "we could not reach the market data" rendered as the confident
5930
6150
  // "there is none", which is the failure this project refuses everywhere
5931
6151
  // else.
5932
- batch.complete ? `No reliable market signals on ${date}.` : `Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
6152
+ !marketsCoverCompetition() ? `${MARKETS_SCOPE_NOTE} (${date})` : batch.complete ? `No reliable market signals on ${date}.` : `Market data unavailable or incomplete for ${date} \u2014 not all fixtures could be checked.`
5933
6153
  );
5934
6154
  if (shown.shown > 0 && !batch.complete) {
5935
6155
  text += `
@@ -6059,7 +6279,12 @@ async function toolGetShareSnippet(args) {
6059
6279
  degraded: degraded2,
6060
6280
  informationalOnly: true,
6061
6281
  snippet,
6062
- 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
+ }))
6063
6288
  }
6064
6289
  };
6065
6290
  }
@@ -6075,7 +6300,7 @@ async function toolGetShareSnippet(args) {
6075
6300
  data: { kind: "bracket", view: null }
6076
6301
  };
6077
6302
  }
6078
- const { view, degraded: degraded2, source: source2 } = await getBracket(
6303
+ const { view, degraded: degraded2, source: source2, unsupported } = await getBracket(
6079
6304
  resolveAdapter(args),
6080
6305
  stageFilter ? { stage: stageFilter, lang: args.lang } : { lang: args.lang }
6081
6306
  );
@@ -6084,7 +6309,7 @@ async function toolGetShareSnippet(args) {
6084
6309
  view,
6085
6310
  source: degraded2 ? void 0 : source2,
6086
6311
  installLine: stageFilter ? `npx @claudinho/cli bracket ${stageFilter}` : "npx @claudinho/cli bracket",
6087
- emptyNote: t(args.lang, "bracket.empty")
6312
+ emptyNote: unsupported ? t(args.lang, "competition.unsupported") : t(args.lang, "bracket.empty")
6088
6313
  },
6089
6314
  { ...options, locale: args.lang, tz: args.tz }
6090
6315
  );
@@ -6103,7 +6328,10 @@ async function toolGetShareSnippet(args) {
6103
6328
  };
6104
6329
  }
6105
6330
  if (args.matchId) {
6106
- 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
+ );
6107
6335
  const matches = match ? [match] : [];
6108
6336
  const market2 = await signalsFor(matches);
6109
6337
  return shareResult(
@@ -6117,7 +6345,7 @@ async function toolGetShareSnippet(args) {
6117
6345
  marketComplete: market2.complete,
6118
6346
  source: source2,
6119
6347
  degraded: degraded2,
6120
- emptyNote: `No match found with id ${args.matchId}.`,
6348
+ emptyNote: unsupported ? t(args.lang, "competition.unsupported") : `No match found with id ${args.matchId}.`,
6121
6349
  installLine: `npx @claudinho/cli match ${args.matchId}`,
6122
6350
  tz: args.tz,
6123
6351
  locale: args.lang
@@ -6127,7 +6355,7 @@ async function toolGetShareSnippet(args) {
6127
6355
  }
6128
6356
  if (args.team) {
6129
6357
  const code = args.team.toUpperCase();
6130
- const { fixture, degraded: degraded2, source: source2 } = await getNextFixtureForTeam(
6358
+ const { fixture, degraded: degraded2, source: source2, unsupported } = await getNextFixtureForTeam(
6131
6359
  resolveAdapter(args),
6132
6360
  code,
6133
6361
  args.now ?? /* @__PURE__ */ new Date()
@@ -6148,7 +6376,7 @@ async function toolGetShareSnippet(args) {
6148
6376
  // with get_next_fixture (a static group fixture carries no source).
6149
6377
  source: source2,
6150
6378
  degraded: degraded2,
6151
- 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}.`,
6152
6380
  installLine: `npx @claudinho/cli next ${code}`,
6153
6381
  tz: args.tz,
6154
6382
  locale: args.lang
@@ -6187,7 +6415,7 @@ async function toolGetShareSnippet(args) {
6187
6415
 
6188
6416
  // src/server.ts
6189
6417
  var SERVER_NAME = "claudinho";
6190
- var SERVER_VERSION = "0.9.4";
6418
+ var SERVER_VERSION = "0.10.1";
6191
6419
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
6192
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.`;
6193
6421
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -6638,7 +6866,7 @@ function buildServer() {
6638
6866
  },
6639
6867
  async (uri, variables) => {
6640
6868
  const group = String(variables.group ?? "");
6641
- const text = await standingsResourceText(group, makeAdapter());
6869
+ const text = await standingsResourceText(group, resolveAdapter({}));
6642
6870
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text }] };
6643
6871
  }
6644
6872
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.9.4",
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",
@@ -51,15 +51,15 @@
51
51
  ],
52
52
  "dependencies": {
53
53
  "@modelcontextprotocol/sdk": "^1.30.0",
54
- "zod": "^3.25.0"
54
+ "zod": "^4.6.2"
55
55
  },
56
56
  "devDependencies": {
57
- "@types/node": "^22.20.1",
58
- "@vitest/coverage-v8": "^4.1.10",
57
+ "@types/node": "^22.20.2",
58
+ "@vitest/coverage-v8": "^5.0.0",
59
59
  "tsup": "^8.0.0",
60
60
  "typescript": "^5.7.0",
61
- "vitest": "^4.1.10",
62
- "@claudinho/core": "0.9.4"
61
+ "vitest": "^5.0.0",
62
+ "@claudinho/core": "0.10.1"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsup",