@claudinho/mcp 0.4.2 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/index.js +255 -101
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -35,10 +35,10 @@ Developer → Edit Config, then restart. Codex config file: `~/.codex/config.tom
35
35
  | `get_today` | fixtures for a date (default: today), grouped in the caller's `tz`, live scores overlaid |
36
36
  | `get_live` | matches in play right now |
37
37
  | `get_match` | a single match by id |
38
- | `get_standings` | group table(s) — one group `A`–`L`, or all |
38
+ | `get_standings` | live cumulative group table(s) — one group `A`–`L`, or all |
39
39
  | `get_next_fixture` | a team's next match (3-letter code, e.g. `MEX`) — fully offline |
40
- | `get_market_signal` | read-only prediction-market signal for a match, a team's next fixture, or a date — informational only |
41
- | `get_share_snippet` | a copy-pasteable plain-text match card — hand the returned snippet to the user as-is |
40
+ | `get_market_signal` | read-only prediction-market signal for a match, a team's current-or-next fixture (in-play preferred while live), or a date — informational only |
41
+ | `get_share_snippet` | a copy-pasteable plain-text card — for a match, a team's next fixture, a group's standings table (`group`), a date, or live — hand the returned snippet to the user as-is |
42
42
 
43
43
  All tools are **read-only** (`readOnlyHint`) and accept optional `tz`, `lang`
44
44
  (`en`/`es`/`pt`/`fr`), and `flavor` (`off`/`subtle`/`full`). Every response carries
@@ -50,6 +50,7 @@ the prediction-market read).
50
50
 
51
51
  ## Market signals
52
52
 
53
+ Market signals are pre-match and in-play reads — finished matches never show one.
53
54
  `get_today` / `get_match` include a short market line when a reliable market exists
54
55
  (slugs are auto-derived per fixture; matching fails closed). **Read-only and
55
56
  informational only — not betting advice:** market-implied percentages with Polymarket
package/dist/index.js CHANGED
@@ -2614,6 +2614,7 @@ function nextFixtureForTeam(code, opts = {}) {
2614
2614
  (m) => new Date(m.kickoff).getTime() >= from.getTime()
2615
2615
  );
2616
2616
  }
2617
+ var LIVE_WINDOW_MS = 140 * 6e4;
2617
2618
  function groups(fixtures = SCHEDULE) {
2618
2619
  const set = /* @__PURE__ */ new Set();
2619
2620
  for (const m of fixtures) if (m.group) set.add(m.group);
@@ -2759,6 +2760,43 @@ function mapEspnEvent(ev, ctx = {}) {
2759
2760
  function toEspnDate(d) {
2760
2761
  return d.replace(/\D/g, "").slice(0, 8);
2761
2762
  }
2763
+ function statVal(stats, name) {
2764
+ const v = stats?.find((s) => s.name === name)?.value;
2765
+ return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
2766
+ }
2767
+ function entryToRow(e) {
2768
+ return {
2769
+ team: toTeam(e.team),
2770
+ played: statVal(e.stats, "gamesPlayed"),
2771
+ won: statVal(e.stats, "wins"),
2772
+ drawn: statVal(e.stats, "ties"),
2773
+ lost: statVal(e.stats, "losses"),
2774
+ goalsFor: statVal(e.stats, "pointsFor"),
2775
+ goalsAgainst: statVal(e.stats, "pointsAgainst"),
2776
+ goalDiff: statVal(e.stats, "pointDifferential"),
2777
+ points: statVal(e.stats, "points")
2778
+ };
2779
+ }
2780
+ function parseStandings(data) {
2781
+ const out = [];
2782
+ for (const child of data.children ?? []) {
2783
+ const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
2784
+ if (!letter) continue;
2785
+ const ranked = (child.standings?.entries ?? []).map((e) => ({
2786
+ row: entryToRow(e),
2787
+ rank: statVal(e.stats, "rank")
2788
+ }));
2789
+ ranked.sort((a, b) => {
2790
+ if (a.rank && b.rank && a.rank !== b.rank) return a.rank - b.rank;
2791
+ const r = a.row;
2792
+ const s = b.row;
2793
+ return s.points - r.points || s.goalDiff - r.goalDiff || s.goalsFor - r.goalsFor || r.team.code.localeCompare(s.team.code);
2794
+ });
2795
+ out.push({ group: letter, rows: ranked.map((x) => x.row) });
2796
+ }
2797
+ out.sort((a, b) => a.group.localeCompare(b.group));
2798
+ return out;
2799
+ }
2762
2800
  var EspnAdapter = class {
2763
2801
  constructor(opts = {}) {
2764
2802
  this.opts = opts;
@@ -2778,25 +2816,30 @@ var EspnAdapter = class {
2778
2816
  const today = await this.fetchScoreboard();
2779
2817
  return today.filter((m) => m.status === "LIVE" || m.status === "HT");
2780
2818
  }
2819
+ /** Standings endpoint URL (lives under apis/v2, not site/v2; derived from base). */
2820
+ standingsUrl() {
2821
+ const base = this.opts.baseUrl ?? DEFAULT_BASE;
2822
+ return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
2823
+ }
2824
+ /**
2825
+ * Authoritative, cumulative group tables from the standings endpoint. Throws
2826
+ * on fetch/parse failure (the caller decides the fallback). Group-stage only:
2827
+ * non-group `children` are filtered out by {@link parseStandings}.
2828
+ */
2829
+ async fetchStandings() {
2830
+ return parseStandings(await this.get(this.standingsUrl()));
2831
+ }
2781
2832
  /**
2782
2833
  * Build (and cache) a team-code -> group-letter map from the standings
2783
- * endpoint. Best-effort: returns {} if standings are unavailable.
2834
+ * endpoint. Best-effort: returns {} if standings are unavailable. Reuses the
2835
+ * same parse as {@link fetchStandings}, so the two never drift.
2784
2836
  */
2785
2837
  async fetchGroupMap(force = false) {
2786
2838
  if (this.groupMap && !force) return this.groupMap;
2787
- const base = this.opts.baseUrl ?? DEFAULT_BASE;
2788
- const standingsUrl = `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
2789
2839
  const map = {};
2790
2840
  try {
2791
- const data = await this.get(standingsUrl);
2792
- for (const child of data.children ?? []) {
2793
- const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
2794
- if (!letter) continue;
2795
- for (const e of child.standings?.entries ?? []) {
2796
- const code = e.team?.abbreviation?.toUpperCase();
2797
- if (code) map[code] = letter;
2798
- }
2799
- }
2841
+ const tables = parseStandings(await this.get(this.standingsUrl()));
2842
+ for (const t of tables) for (const r of t.rows) map[r.team.code] = t.group;
2800
2843
  } catch {
2801
2844
  }
2802
2845
  this.groupMap = map;
@@ -2867,10 +2910,53 @@ async function getMatchesForDate(adapter, dateISO) {
2867
2910
  return { matches: base, degraded: true };
2868
2911
  }
2869
2912
  }
2913
+ async function getStandings(adapter, group) {
2914
+ const want = group?.toUpperCase();
2915
+ if (adapter.fetchStandings) {
2916
+ try {
2917
+ const all = await adapter.fetchStandings();
2918
+ const tables2 = (want ? all.filter((t) => t.group === want) : all).sort(
2919
+ (a, b) => a.group.localeCompare(b.group)
2920
+ );
2921
+ return { tables: tables2, degraded: false, source: adapter.name };
2922
+ } catch {
2923
+ }
2924
+ }
2925
+ const letters = want ? [want] : groups();
2926
+ const tables = letters.map((g) => ({ group: g, rows: computeStandings(fixturesByGroup(g)) })).filter((t) => t.rows.length > 0);
2927
+ return { tables, degraded: true };
2928
+ }
2870
2929
  function shiftUtcDate(dateISO, days) {
2871
2930
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
2872
2931
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
2873
2932
  }
2933
+ var EXTRA_TIME_SLACK_MS = 60 * 6e4;
2934
+ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
2935
+ const nowMs = now.getTime();
2936
+ const candidate = fixturesByTeam(code).find((m) => {
2937
+ const k = Date.parse(m.kickoff);
2938
+ return nowMs >= k && nowMs <= k + LIVE_WINDOW_MS + EXTRA_TIME_SLACK_MS;
2939
+ });
2940
+ if (candidate) {
2941
+ const r = await getMatchById(adapter, candidate.id);
2942
+ const m = r.match ?? candidate;
2943
+ if (!isFinished(m.status)) return { ...r, match: m };
2944
+ }
2945
+ const next = nextFixtureForTeam(code, { from: now });
2946
+ return { match: next, degraded: false };
2947
+ }
2948
+ async function getMatchById(adapter, id) {
2949
+ const base = allFixtures().find((m) => m.id === id);
2950
+ if (!base) return { match: void 0, degraded: false };
2951
+ const day = base.kickoff.slice(0, 10);
2952
+ try {
2953
+ const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
2954
+ const hit = live.find((m) => m.id === id);
2955
+ return { match: hit ?? base, degraded: false, source: hit ? adapter.name : void 0 };
2956
+ } catch {
2957
+ return { match: base, degraded: true };
2958
+ }
2959
+ }
2874
2960
  async function getLiveMatches(adapter) {
2875
2961
  try {
2876
2962
  return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
@@ -2879,6 +2965,12 @@ async function getLiveMatches(adapter) {
2879
2965
  }
2880
2966
  }
2881
2967
  var DEFAULT_MAX_AGE_MS = 15 * 6e4;
2968
+ function marketRelevant(match, now = /* @__PURE__ */ new Date()) {
2969
+ if (isLive(match.status)) return true;
2970
+ if (isFinished(match.status)) return false;
2971
+ const k = Date.parse(match.kickoff);
2972
+ return !Number.isFinite(k) || now.getTime() <= k + LIVE_WINDOW_MS;
2973
+ }
2882
2974
  function normalizeOutcomes(outcomes) {
2883
2975
  const sum = outcomes.reduce(
2884
2976
  (s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
@@ -3373,11 +3465,50 @@ function formatShareSnippet(input, options = {}) {
3373
3465
  blocks.push(card.join("\n"));
3374
3466
  }
3375
3467
  }
3468
+ blocks.push(
3469
+ shareFooter({
3470
+ source: input.source,
3471
+ installLine: input.installLine,
3472
+ includeHashtag,
3473
+ includeInstall
3474
+ })
3475
+ );
3476
+ return blocks.join("\n\n");
3477
+ }
3478
+ function shareFooter(opts) {
3376
3479
  const footer = [];
3377
- if (input.source) footer.push(`Live data: ${liveSourceLabel(input.source)}`);
3378
- footer.push([includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 "));
3379
- if (includeInstall && input.installLine) footer.push(`Try it: ${input.installLine}`);
3380
- blocks.push(footer.join("\n"));
3480
+ if (opts.source) footer.push(`Live data: ${liveSourceLabel(opts.source)}`);
3481
+ footer.push(
3482
+ [opts.includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 ")
3483
+ );
3484
+ if (opts.includeInstall && opts.installLine) footer.push(`Try it: ${opts.installLine}`);
3485
+ return footer.join("\n");
3486
+ }
3487
+ function gd(n) {
3488
+ return n > 0 ? `+${n}` : `${n}`;
3489
+ }
3490
+ function tableRow(r, rank) {
3491
+ return `${rank}. ${r.team.flag} ${r.team.code} ${r.points} pts \xB7 ${r.won}-${r.drawn}-${r.lost} \xB7 ${gd(r.goalDiff)}`;
3492
+ }
3493
+ function formatShareTable(input, options = {}) {
3494
+ const includeHashtag = options.includeHashtag !== false;
3495
+ const includeInstall = options.includeInstallLine !== false;
3496
+ const blocks = [];
3497
+ if (input.tables.length === 0) {
3498
+ blocks.push(input.emptyNote ?? "No standings available.");
3499
+ } else {
3500
+ for (const { group, rows } of input.tables) {
3501
+ blocks.push(
3502
+ [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
3503
+ );
3504
+ }
3505
+ if (input.degraded) {
3506
+ blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
3507
+ }
3508
+ }
3509
+ blocks.push(
3510
+ shareFooter({ source: input.source, installLine: input.installLine, includeHashtag, includeInstall })
3511
+ );
3381
3512
  return blocks.join("\n\n");
3382
3513
  }
3383
3514
 
@@ -3434,8 +3565,8 @@ function standingsTable(group, rows) {
3434
3565
  const cols = "Team P W D L GD Pts";
3435
3566
  const lines = rows.map((r) => {
3436
3567
  const name = `${r.team.flag} ${r.team.name}`.padEnd(24).slice(0, 24);
3437
- const gd = (r.goalDiff > 0 ? `+${r.goalDiff}` : `${r.goalDiff}`).padStart(3);
3438
- return `${name} ${pad(r.played)} ${pad(r.won)} ${pad(r.drawn)} ${pad(r.lost)} ${gd} ${pad(r.points)}`;
3568
+ const gd2 = (r.goalDiff > 0 ? `+${r.goalDiff}` : `${r.goalDiff}`).padStart(3);
3569
+ return `${name} ${pad(r.played)} ${pad(r.won)} ${pad(r.drawn)} ${pad(r.lost)} ${gd2} ${pad(r.points)}`;
3439
3570
  });
3440
3571
  return [header, cols, ...lines].join("\n");
3441
3572
  }
@@ -3454,13 +3585,19 @@ function resolveMarketProvider(args) {
3454
3585
  function marketDisplayable(sig) {
3455
3586
  return !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
3456
3587
  }
3457
- function marketHeader(m) {
3458
- return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag}`;
3588
+ function marketHeader(m, args) {
3589
+ const when = formatDate(m.kickoff, { tz: args.tz, locale: args.lang });
3590
+ return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag} (${when})`;
3459
3591
  }
3460
- function marketText(m, sig) {
3461
- return `${marketHeader(m)}
3592
+ function marketText(m, sig, args) {
3593
+ return `${marketHeader(m, args)}
3462
3594
  ${marketBlock(sig, m).join("\n")}`;
3463
3595
  }
3596
+ function noSignalText(m, args, now) {
3597
+ if (marketRelevant(m, now)) return `No reliable market signal for ${marketHeader(m, args)}.`;
3598
+ const verb = isFinished(m.status) ? "has finished" : "appears to have finished";
3599
+ return `${marketHeader(m, args)} ${verb} \u2014 market signals are pre-match and in-play reads.`;
3600
+ }
3464
3601
  function marketData(sig) {
3465
3602
  return {
3466
3603
  matchId: sig.matchId,
@@ -3521,10 +3658,12 @@ async function cachedMarketSignals(args, matches) {
3521
3658
  }
3522
3659
  async function reliableMarketData(args, matches) {
3523
3660
  if (!marketsEnabled()) return void 0;
3524
- const signals = await cachedMarketSignals(args, matches);
3525
- const now = /* @__PURE__ */ new Date();
3661
+ const now = args.now ?? /* @__PURE__ */ new Date();
3662
+ const relevant = matches.filter((m) => marketRelevant(m, now));
3663
+ if (relevant.length === 0) return void 0;
3664
+ const signals = await cachedMarketSignals(args, relevant);
3526
3665
  const out = {};
3527
- for (const m of matches) {
3666
+ for (const m of relevant) {
3528
3667
  const s = signals.get(m.id);
3529
3668
  if (s && isReliableMarketSignal(s, { now })) out[m.id] = marketData(s);
3530
3669
  }
@@ -3577,27 +3716,16 @@ ${matchList(matches, "No matches in play right now.", opts)}`;
3577
3716
  };
3578
3717
  }
3579
3718
  async function toolGetMatch(args) {
3580
- let match = allFixtures().find((m) => m.id === args.id);
3581
- let degraded = false;
3582
- let liveSource;
3583
- if (match) {
3584
- try {
3585
- const adapter = resolveAdapter(args);
3586
- const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
3587
- match = live.find((m) => m.id === args.id) ?? match;
3588
- liveSource = adapter.name;
3589
- } catch {
3590
- degraded = true;
3591
- }
3592
- }
3719
+ const { match, degraded, source: liveSource } = await getMatchById(resolveAdapter(args), args.id);
3593
3720
  if (!match) {
3594
3721
  return { text: withDisclaimer(`No match found with id ${args.id}.`), data: { match: null } };
3595
3722
  }
3596
3723
  const opts = fmtOpts(args);
3724
+ const now = args.now ?? /* @__PURE__ */ new Date();
3597
3725
  let marketSignal;
3598
- if (marketsEnabled()) {
3726
+ if (marketsEnabled() && marketRelevant(match, now)) {
3599
3727
  const s = (await cachedMarketSignals(args, [match])).get(match.id);
3600
- if (s && isReliableMarketSignal(s, { now: /* @__PURE__ */ new Date() })) marketSignal = s;
3728
+ if (s && isReliableMarketSignal(s, { now })) marketSignal = s;
3601
3729
  }
3602
3730
  const base = matchLine(match, opts);
3603
3731
  const text = marketSignal ? `${base}
@@ -3613,34 +3741,34 @@ ${marketBlock(marketSignal, match).join("\n")}` : base;
3613
3741
  };
3614
3742
  }
3615
3743
  async function toolGetStandings(args) {
3616
- const { matches, degraded, source } = await getMatchesForDate(
3617
- resolveAdapter(args),
3618
- localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz)
3619
- );
3620
- const known = groups(matches);
3621
- if (args.group) {
3622
- const g = args.group.toUpperCase();
3623
- if (!known.includes(g)) {
3624
- return {
3625
- text: withDisclaimer(`No group "${g}". Groups are ${known.join(", ")}.`, source),
3626
- data: { degraded, source: source ?? null, tables: null }
3627
- };
3628
- }
3744
+ const { tables, degraded, source } = await getStandings(resolveAdapter(args), args.group);
3745
+ const shaped = tables.map((tb) => ({ group: tb.group, standings: tb.rows }));
3746
+ if (shaped.length === 0) {
3747
+ const g = args.group?.toUpperCase();
3748
+ const msg = g ? `No group "${g}". Groups are A\u2013L.` : "No standings available.";
3749
+ return {
3750
+ text: withDisclaimer(degraded ? `${msg} (Live standings unavailable.)` : msg, source),
3751
+ data: { degraded, source: source ?? null, tables: args.group ? null : [] }
3752
+ };
3629
3753
  }
3630
- const wanted = args.group ? [args.group.toUpperCase()] : known;
3631
- const tables = wanted.map((g) => ({
3632
- group: g,
3633
- standings: computeStandings(fixturesByGroup(g, matches))
3634
- }));
3635
- const text = tables.map((t) => standingsTable(t.group, t.standings)).join("\n\n");
3754
+ let text = shaped.map((t) => standingsTable(t.group, t.standings)).join("\n\n");
3755
+ if (degraded) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
3636
3756
  return {
3637
- text: withDisclaimer(text || `No group found.`, source),
3638
- data: { degraded, source: source ?? null, tables: args.group ? tables[0] ?? null : tables }
3757
+ text: withDisclaimer(text, source),
3758
+ data: { degraded, source: source ?? null, tables: args.group ? shaped[0] ?? null : shaped }
3639
3759
  };
3640
3760
  }
3761
+ async function standingsResourceText(group, adapter) {
3762
+ const g = group.toUpperCase();
3763
+ const { tables, degraded, source } = await getStandings(adapter, g);
3764
+ const tb = tables[0];
3765
+ let text = tb ? standingsTable(tb.group, tb.rows) : `No group ${g}.`;
3766
+ if (degraded && tb) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
3767
+ return withDisclaimer(text, source);
3768
+ }
3641
3769
  async function toolGetNextFixture(args) {
3642
3770
  const code = args.team.toUpperCase();
3643
- const fixture = nextFixtureForTeam(code);
3771
+ const fixture = nextFixtureForTeam(code, { from: args.now ?? /* @__PURE__ */ new Date() });
3644
3772
  if (!fixture) {
3645
3773
  return {
3646
3774
  text: withDisclaimer(`No upcoming fixture found for ${code}.`),
@@ -3656,11 +3784,13 @@ ${matchLine(fixture, opts)}`),
3656
3784
  }
3657
3785
  async function toolGetMarketSignal(args) {
3658
3786
  const provider = resolveMarketProvider(args);
3787
+ const now = args.now ?? /* @__PURE__ */ new Date();
3659
3788
  if (args.matchId) {
3660
- const match = allFixtures().find((m) => m.id === args.matchId);
3661
- const sig = match ? await getMarketSignal(provider, match) : void 0;
3789
+ const { match } = await getMatchById(resolveAdapter(args), args.matchId);
3790
+ const relevant = match ? marketRelevant(match, now) : false;
3791
+ const sig = match && relevant ? await getMarketSignal(provider, match) : void 0;
3662
3792
  const shown2 = match && sig && marketDisplayable(sig) ? sig : void 0;
3663
- const text2 = !match ? `No match found with id ${args.matchId}.` : shown2 ? marketText(match, shown2) : `No reliable market signal for ${marketHeader(match)}.`;
3793
+ const text2 = !match ? `No match found with id ${args.matchId}.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now);
3664
3794
  return {
3665
3795
  text: withDisclaimer(text2),
3666
3796
  data: {
@@ -3672,10 +3802,11 @@ async function toolGetMarketSignal(args) {
3672
3802
  }
3673
3803
  if (args.team) {
3674
3804
  const code = args.team.toUpperCase();
3675
- const fixture = nextFixtureForTeam(code);
3676
- const sig = fixture ? await getMarketSignal(provider, fixture) : void 0;
3805
+ const { match: fixture } = await marketFixtureForTeam(resolveAdapter(args), code, now);
3806
+ const relevant = fixture ? marketRelevant(fixture, now) : false;
3807
+ const sig = fixture && relevant ? await getMarketSignal(provider, fixture) : void 0;
3677
3808
  const shown2 = fixture && sig && marketDisplayable(sig) ? sig : void 0;
3678
- const text2 = !fixture ? `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2) : `No reliable market signal for ${code}'s next fixture.`;
3809
+ const text2 = !fixture ? `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
3679
3810
  return {
3680
3811
  text: withDisclaimer(text2),
3681
3812
  data: {
@@ -3686,15 +3817,15 @@ async function toolGetMarketSignal(args) {
3686
3817
  }
3687
3818
  };
3688
3819
  }
3689
- const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
3820
+ const date = args.date ?? localDate(now.toISOString(), args.tz);
3690
3821
  const { matches } = await getMatchesForDate(resolveAdapter(args), date);
3691
- const todays = fixturesByDate(date, matches, args.tz);
3822
+ const todays = fixturesByDate(date, matches, args.tz).filter((m) => marketRelevant(m, now));
3692
3823
  const { signals } = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
3693
3824
  const shown = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
3694
3825
  (r) => !!r.signal && marketDisplayable(r.signal)
3695
3826
  );
3696
3827
  const text = shown.length ? `Market signals on ${date}:
3697
- ${shown.map(({ match, signal }) => marketText(match, signal)).join("\n\n")}` : `No reliable market signals on ${date}.`;
3828
+ ${shown.map(({ match, signal }) => marketText(match, signal, args)).join("\n\n")}` : `No reliable market signals on ${date}.`;
3698
3829
  return {
3699
3830
  text: withDisclaimer(text),
3700
3831
  data: {
@@ -3706,10 +3837,12 @@ ${shown.map(({ match, signal }) => marketText(match, signal)).join("\n\n")}` : `
3706
3837
  }
3707
3838
  async function reliableSignalMap(args, matches) {
3708
3839
  if (!marketsEnabled()) return /* @__PURE__ */ new Map();
3709
- const signals = await cachedMarketSignals(args, matches);
3710
- const now = /* @__PURE__ */ new Date();
3840
+ const now = args.now ?? /* @__PURE__ */ new Date();
3841
+ const relevant = matches.filter((m) => marketRelevant(m, now));
3842
+ if (relevant.length === 0) return /* @__PURE__ */ new Map();
3843
+ const signals = await cachedMarketSignals(args, relevant);
3711
3844
  const out = /* @__PURE__ */ new Map();
3712
- for (const m of matches) {
3845
+ for (const m of relevant) {
3713
3846
  const s = signals.get(m.id);
3714
3847
  if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(s)) out.set(m.id, s);
3715
3848
  }
@@ -3770,18 +3903,37 @@ async function toolGetShareSnippet(args) {
3770
3903
  { ...options, includeMarkets: false }
3771
3904
  );
3772
3905
  }
3773
- if (args.matchId) {
3774
- let match = allFixtures().find((m) => m.id === args.matchId);
3775
- let source2;
3776
- if (match) {
3777
- try {
3778
- const adapter = resolveAdapter(args);
3779
- const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
3780
- match = live.find((m) => m.id === args.matchId) ?? match;
3781
- source2 = adapter.name;
3782
- } catch {
3906
+ if (args.group) {
3907
+ const group = args.group.toUpperCase();
3908
+ const { tables, degraded, source: source2 } = await getStandings(resolveAdapter(args), group);
3909
+ const snippet = formatShareTable(
3910
+ {
3911
+ tables,
3912
+ // Degraded ⇒ static roster, no live provider: don't attribute one, and
3913
+ // surface the not-live notice (the card gets pasted publicly).
3914
+ source: degraded ? void 0 : source2,
3915
+ installLine: `npx @claudinho/cli table ${group}`,
3916
+ emptyNote: `No group ${group}.`,
3917
+ degraded
3918
+ },
3919
+ options
3920
+ );
3921
+ return {
3922
+ text: snippet,
3923
+ data: {
3924
+ kind: "table",
3925
+ target: "table",
3926
+ group,
3927
+ source: degraded ? null : source2 ?? null,
3928
+ degraded,
3929
+ informationalOnly: true,
3930
+ snippet,
3931
+ tables: tables.map((tb) => ({ group: tb.group, standings: tb.rows }))
3783
3932
  }
3784
- }
3933
+ };
3934
+ }
3935
+ if (args.matchId) {
3936
+ const { match, source: source2 } = await getMatchById(resolveAdapter(args), args.matchId);
3785
3937
  const matches = match ? [match] : [];
3786
3938
  return shareResult(
3787
3939
  "match",
@@ -3802,7 +3954,7 @@ async function toolGetShareSnippet(args) {
3802
3954
  }
3803
3955
  if (args.team) {
3804
3956
  const code = args.team.toUpperCase();
3805
- const fixture = nextFixtureForTeam(code);
3957
+ const fixture = nextFixtureForTeam(code, { from: args.now ?? /* @__PURE__ */ new Date() });
3806
3958
  const matches = fixture ? [fixture] : [];
3807
3959
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
3808
3960
  return shareResult(
@@ -3845,12 +3997,12 @@ async function toolGetShareSnippet(args) {
3845
3997
 
3846
3998
  // src/server.ts
3847
3999
  var SERVER_NAME = "claudinho";
3848
- var SERVER_VERSION = "0.4.2";
4000
+ var SERVER_VERSION = "0.5.0";
3849
4001
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
3850
4002
  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.`;
3851
4003
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
3852
4004
  Use get_live during matches, get_today for a day's schedule, get_next_fixture for a specific team (3-letter code, e.g. MEX), and get_standings for group tables.
3853
- Use get_market_signal for read-only prediction-market odds (a match, a team's next fixture, or a date). Market data is informational only \u2014 relay the percentages factually and never frame it as betting or trading advice.
4005
+ Use get_market_signal for read-only prediction-market signals (a match, a team's current-or-next fixture, or a date). Market data is informational only \u2014 relay the percentages factually and never frame it as betting or trading advice.
3854
4006
  Use get_share_snippet to produce a ready-to-paste match card (for a match, a team's next fixture, a date, or live matches) \u2014 hand the user the returned snippet text verbatim.${VOICE}
3855
4007
  ${DISCLAIMER}`;
3856
4008
  var dateArg = z.string().refine(isValidDate, "must be a real calendar date in YYYY-MM-DD form");
@@ -3879,7 +4031,7 @@ function buildServer() {
3879
4031
  "get_today",
3880
4032
  {
3881
4033
  title: "Today's matches",
3882
- description: "Fixtures for a given date (default: today), with live scores overlaid.",
4034
+ description: "All fixtures for a date (default: today), with live score and minute overlaid on any match in play. Use this for a whole day's card; for only in-play matches use get_live, for one team's match use get_next_fixture, for a single match's detail use get_match. Kickoffs render in tz; lang localizes text (en/es/pt/fr); flavor sets commentary tone.",
3883
4035
  inputSchema: {
3884
4036
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
3885
4037
  ...commonArgs
@@ -3893,7 +4045,7 @@ function buildServer() {
3893
4045
  "get_live",
3894
4046
  {
3895
4047
  title: "Live matches",
3896
- description: "Matches currently in play, with score and minute.",
4048
+ description: "Only matches in play right now \u2014 each with current score and minute (empty when nothing is live). Use during matches for in-play state; for a full day's schedule including upcoming and finished, use get_today. tz/lang/flavor affect formatting only.",
3897
4049
  inputSchema: { ...commonArgs },
3898
4050
  annotations: { readOnlyHint: true, openWorldHint: true }
3899
4051
  },
@@ -3903,7 +4055,7 @@ function buildServer() {
3903
4055
  "get_match",
3904
4056
  {
3905
4057
  title: "Match detail",
3906
- description: "A single match by its id, with live state if available.",
4058
+ description: "One match by its id, with live score/minute overlaid when it's in play. Get the id from get_today or get_live; to find a team's match without an id, use get_next_fixture. tz/lang/flavor affect formatting.",
3907
4059
  inputSchema: { id: z.string().describe("Match id"), ...commonArgs },
3908
4060
  annotations: { readOnlyHint: true, openWorldHint: true }
3909
4061
  },
@@ -3913,7 +4065,7 @@ function buildServer() {
3913
4065
  "get_standings",
3914
4066
  {
3915
4067
  title: "Group standings",
3916
- description: "Group table(s). Pass a group letter A\u2013L, or omit for all groups.",
4068
+ description: "Live cumulative group standings \u2014 pass a group letter A\u2013L, or omit for all 12. Returns ranked rows (team, played, W/D/L, goal difference, points). Use get_today for fixtures/scores and get_next_fixture for one team. Falls back to a roster at zero (flagged degraded) if live standings are unavailable.",
3917
4069
  inputSchema: {
3918
4070
  group: groupArg.optional().describe("Group letter A\u2013L (omit for all)"),
3919
4071
  ...commonArgs
@@ -3926,7 +4078,7 @@ function buildServer() {
3926
4078
  "get_next_fixture",
3927
4079
  {
3928
4080
  title: "Next fixture for a team",
3929
- description: "A team's next scheduled match. Use a 3-letter code, e.g. MEX, BRA, USA.",
4081
+ description: "A team's next scheduled match. Use a 3-letter code, e.g. MEX, BRA, USA. Instant and offline \u2014 answered from the bundled schedule, no network.",
3930
4082
  inputSchema: { team: teamArg.describe("3-letter team code, e.g. MEX"), ...commonArgs },
3931
4083
  // Read-only and served entirely from the bundled static schedule.
3932
4084
  annotations: { readOnlyHint: true, openWorldHint: false }
@@ -3937,10 +4089,12 @@ function buildServer() {
3937
4089
  "get_market_signal",
3938
4090
  {
3939
4091
  title: "Prediction-market signal",
3940
- description: "Read-only prediction-market odds for a match (by id), a team's next fixture, or a date (default: today). Returns market-implied percentages with attribution. Informational only \u2014 relay the numbers factually; do not add betting, trading, or 'value' advice, and do not invent links.",
4092
+ description: "Read-only prediction-market signals for a match (by id), a team's current-or-next fixture, or a date (default: today). Returns market-implied percentages with attribution. Shown only before and during a match \u2014 finished matches have no market read. Informational only \u2014 relay the numbers factually; do not add betting, trading, or 'value' advice, and do not invent links.",
3941
4093
  inputSchema: {
3942
4094
  matchId: z.string().optional().describe("Match id (most specific)"),
3943
- team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),
4095
+ team: teamArg.optional().describe(
4096
+ "3-letter team code, e.g. MEX \u2014 resolves to the team's in-play match when one is live, else their next fixture"
4097
+ ),
3944
4098
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today) for all that day's signals"),
3945
4099
  ...commonArgs
3946
4100
  },
@@ -3953,10 +4107,11 @@ function buildServer() {
3953
4107
  "get_share_snippet",
3954
4108
  {
3955
4109
  title: "Shareable match snippet",
3956
- description: "A polished, copy-pasteable match card (plain text) for a match (matchId), a team's next fixture (team), a date (default: today), or live matches (live: true). Returns the ready-to-paste snippet plus structured data \u2014 hand the snippet text to the user verbatim. No links; it carries a non-affiliation disclaimer, and any market line stays informational only.",
4110
+ description: `A polished, copy-pasteable card (plain text) for a match (matchId), a team's next fixture (team), a group's standings table (group, e.g. "A"), a date (default: today), or live matches (live: true). Returns the ready-to-paste snippet plus structured data \u2014 hand the snippet text to the user verbatim. No links; it carries a non-affiliation disclaimer, and any market line stays informational only.`,
3957
4111
  inputSchema: {
3958
4112
  matchId: z.string().optional().describe("Match id (most specific)"),
3959
4113
  team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),
4114
+ group: groupArg.optional().describe("Group letter A\u2013L for a standings card, e.g. A"),
3960
4115
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
3961
4116
  live: z.boolean().optional().describe("Snapshot of matches in play right now"),
3962
4117
  style: z.enum(["social", "compact"]).optional().describe("social (default, full card) or compact (one line per match)"),
@@ -3975,13 +4130,12 @@ function buildServer() {
3975
4130
  new ResourceTemplate("standings://{group}", { list: void 0 }),
3976
4131
  {
3977
4132
  title: "Group standings",
3978
- description: "Static group table for a group letter A\u2013L.",
4133
+ description: "Live group table for a group letter A\u2013L.",
3979
4134
  mimeType: "text/plain"
3980
4135
  },
3981
4136
  async (uri, variables) => {
3982
- const group = String(variables.group ?? "").toUpperCase();
3983
- const rows = computeStandings(fixturesByGroup(group));
3984
- const text = rows.length ? standingsTable(group, rows) : `No group ${group}.`;
4137
+ const group = String(variables.group ?? "");
4138
+ const text = await standingsResourceText(group, makeAdapter());
3985
4139
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text }] };
3986
4140
  }
3987
4141
  );
@@ -4032,7 +4186,7 @@ function buildServer() {
4032
4186
  role: "user",
4033
4187
  content: {
4034
4188
  type: "text",
4035
- text: `Using get_next_fixture, get_standings, and get_market_signal, tell me about ${team}'s next match in the 2026 tournament, their current group standing, and what prediction markets currently say about that match. Treat the market percentages as informational context only \u2014 relay them factually, never as betting or trading advice.`
4189
+ text: `Using get_next_fixture, get_standings, and get_market_signal, tell me about ${team}'s next match in the 2026 tournament, their current group standing, and what prediction markets currently say about that match. Always state each fixture's date so a market read is never mistaken for a different match. Treat the market percentages as informational context only \u2014 relay them factually, never as betting or trading advice.`
4036
4190
  }
4037
4191
  }
4038
4192
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "mcpName": "io.github.arturogarrido/claudinho",
5
5
  "description": "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",
@@ -54,7 +54,7 @@
54
54
  "tsup": "^8.0.0",
55
55
  "typescript": "^5.7.0",
56
56
  "vitest": "^4.1.0",
57
- "@claudinho/core": "0.4.2"
57
+ "@claudinho/core": "0.5.0"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsup",