@claudinho/mcp 0.4.3 → 0.5.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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +194 -58
  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
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 match card — hand the returned snippet to the user as-is |
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
package/dist/index.js CHANGED
@@ -2760,6 +2760,43 @@ function mapEspnEvent(ev, ctx = {}) {
2760
2760
  function toEspnDate(d) {
2761
2761
  return d.replace(/\D/g, "").slice(0, 8);
2762
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
+ }
2763
2800
  var EspnAdapter = class {
2764
2801
  constructor(opts = {}) {
2765
2802
  this.opts = opts;
@@ -2779,25 +2816,30 @@ var EspnAdapter = class {
2779
2816
  const today = await this.fetchScoreboard();
2780
2817
  return today.filter((m) => m.status === "LIVE" || m.status === "HT");
2781
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
+ }
2782
2832
  /**
2783
2833
  * Build (and cache) a team-code -> group-letter map from the standings
2784
- * 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.
2785
2836
  */
2786
2837
  async fetchGroupMap(force = false) {
2787
2838
  if (this.groupMap && !force) return this.groupMap;
2788
- const base = this.opts.baseUrl ?? DEFAULT_BASE;
2789
- const standingsUrl = `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
2790
2839
  const map = {};
2791
2840
  try {
2792
- const data = await this.get(standingsUrl);
2793
- for (const child of data.children ?? []) {
2794
- const letter = (child.name ?? child.abbreviation ?? "").match(/Group\s+([A-L])/i)?.[1]?.toUpperCase();
2795
- if (!letter) continue;
2796
- for (const e of child.standings?.entries ?? []) {
2797
- const code = e.team?.abbreviation?.toUpperCase();
2798
- if (code) map[code] = letter;
2799
- }
2800
- }
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;
2801
2843
  } catch {
2802
2844
  }
2803
2845
  this.groupMap = map;
@@ -2868,6 +2910,22 @@ async function getMatchesForDate(adapter, dateISO) {
2868
2910
  return { matches: base, degraded: true };
2869
2911
  }
2870
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
+ }
2871
2929
  function shiftUtcDate(dateISO, days) {
2872
2930
  const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
2873
2931
  return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
@@ -3407,11 +3465,53 @@ function formatShareSnippet(input, options = {}) {
3407
3465
  blocks.push(card.join("\n"));
3408
3466
  }
3409
3467
  }
3468
+ if (input.degraded && input.matches.length > 0) {
3469
+ blocks.push("(Live data unavailable \u2014 showing the bundled schedule, not live scores.)");
3470
+ }
3471
+ blocks.push(
3472
+ shareFooter({
3473
+ source: input.source,
3474
+ installLine: input.installLine,
3475
+ includeHashtag,
3476
+ includeInstall
3477
+ })
3478
+ );
3479
+ return blocks.join("\n\n");
3480
+ }
3481
+ function shareFooter(opts) {
3410
3482
  const footer = [];
3411
- if (input.source) footer.push(`Live data: ${liveSourceLabel(input.source)}`);
3412
- footer.push([includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 "));
3413
- if (includeInstall && input.installLine) footer.push(`Try it: ${input.installLine}`);
3414
- blocks.push(footer.join("\n"));
3483
+ if (opts.source) footer.push(`Live data: ${liveSourceLabel(opts.source)}`);
3484
+ footer.push(
3485
+ [opts.includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 ")
3486
+ );
3487
+ if (opts.includeInstall && opts.installLine) footer.push(`Try it: ${opts.installLine}`);
3488
+ return footer.join("\n");
3489
+ }
3490
+ function gd(n) {
3491
+ return n > 0 ? `+${n}` : `${n}`;
3492
+ }
3493
+ function tableRow(r, rank) {
3494
+ return `${rank}. ${r.team.flag} ${r.team.code} ${r.points} pts \xB7 ${r.won}-${r.drawn}-${r.lost} \xB7 ${gd(r.goalDiff)}`;
3495
+ }
3496
+ function formatShareTable(input, options = {}) {
3497
+ const includeHashtag = options.includeHashtag !== false;
3498
+ const includeInstall = options.includeInstallLine !== false;
3499
+ const blocks = [];
3500
+ if (input.tables.length === 0) {
3501
+ blocks.push(input.emptyNote ?? "No standings available.");
3502
+ } else {
3503
+ for (const { group, rows } of input.tables) {
3504
+ blocks.push(
3505
+ [`Group ${group} \xB7 standings`, "", ...rows.map((r, i) => tableRow(r, i + 1))].join("\n")
3506
+ );
3507
+ }
3508
+ if (input.degraded) {
3509
+ blocks.push("(Live standings unavailable \u2014 group roster, not live results.)");
3510
+ }
3511
+ }
3512
+ blocks.push(
3513
+ shareFooter({ source: input.source, installLine: input.installLine, includeHashtag, includeInstall })
3514
+ );
3415
3515
  return blocks.join("\n\n");
3416
3516
  }
3417
3517
 
@@ -3468,8 +3568,8 @@ function standingsTable(group, rows) {
3468
3568
  const cols = "Team P W D L GD Pts";
3469
3569
  const lines = rows.map((r) => {
3470
3570
  const name = `${r.team.flag} ${r.team.name}`.padEnd(24).slice(0, 24);
3471
- const gd = (r.goalDiff > 0 ? `+${r.goalDiff}` : `${r.goalDiff}`).padStart(3);
3472
- return `${name} ${pad(r.played)} ${pad(r.won)} ${pad(r.drawn)} ${pad(r.lost)} ${gd} ${pad(r.points)}`;
3571
+ const gd2 = (r.goalDiff > 0 ? `+${r.goalDiff}` : `${r.goalDiff}`).padStart(3);
3572
+ return `${name} ${pad(r.played)} ${pad(r.won)} ${pad(r.drawn)} ${pad(r.lost)} ${gd2} ${pad(r.points)}`;
3473
3573
  });
3474
3574
  return [header, cols, ...lines].join("\n");
3475
3575
  }
@@ -3592,8 +3692,9 @@ async function toolGetToday(args) {
3592
3692
  const { matches, degraded, source } = await getMatchesForDate(adapter, date);
3593
3693
  const todays = fixturesByDate(date, matches, args.tz);
3594
3694
  const opts = fmtOpts(args);
3595
- const text = `Matches on ${date}:
3695
+ let text = `Matches on ${date}:
3596
3696
  ${matchList(todays, "No matches scheduled.", opts)}`;
3697
+ if (degraded) text += "\n\n(Live scores unavailable \u2014 showing the bundled schedule.)";
3597
3698
  const marketSignals = await reliableMarketData(args, todays);
3598
3699
  return {
3599
3700
  text: withDisclaimer(text, source),
@@ -3611,7 +3712,7 @@ async function toolGetLive(args = {}) {
3611
3712
  const adapter = resolveAdapter(args);
3612
3713
  const { matches, degraded, source } = await getLiveMatches(adapter);
3613
3714
  const opts = fmtOpts(args);
3614
- const text = `Live now:
3715
+ const text = degraded ? "Live scores unavailable right now \u2014 could not reach the data provider." : `Live now:
3615
3716
  ${matchList(matches, "No matches in play right now.", opts)}`;
3616
3717
  return {
3617
3718
  text: withDisclaimer(text, source),
@@ -3631,8 +3732,9 @@ async function toolGetMatch(args) {
3631
3732
  if (s && isReliableMarketSignal(s, { now })) marketSignal = s;
3632
3733
  }
3633
3734
  const base = matchLine(match, opts);
3634
- const text = marketSignal ? `${base}
3735
+ let text = marketSignal ? `${base}
3635
3736
  ${marketBlock(marketSignal, match).join("\n")}` : base;
3737
+ if (degraded) text += "\n\n(Live state unavailable \u2014 showing the scheduled fixture.)";
3636
3738
  return {
3637
3739
  text: withDisclaimer(text, liveSource),
3638
3740
  data: {
@@ -3644,34 +3746,34 @@ ${marketBlock(marketSignal, match).join("\n")}` : base;
3644
3746
  };
3645
3747
  }
3646
3748
  async function toolGetStandings(args) {
3647
- const { matches, degraded, source } = await getMatchesForDate(
3648
- resolveAdapter(args),
3649
- localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz)
3650
- );
3651
- const known = groups(matches);
3652
- if (args.group) {
3653
- const g = args.group.toUpperCase();
3654
- if (!known.includes(g)) {
3655
- return {
3656
- text: withDisclaimer(`No group "${g}". Groups are ${known.join(", ")}.`, source),
3657
- data: { degraded, source: source ?? null, tables: null }
3658
- };
3659
- }
3749
+ const { tables, degraded, source } = await getStandings(resolveAdapter(args), args.group);
3750
+ const shaped = tables.map((tb) => ({ group: tb.group, standings: tb.rows }));
3751
+ if (shaped.length === 0) {
3752
+ const g = args.group?.toUpperCase();
3753
+ const msg = g ? `No group "${g}". Groups are A\u2013L.` : "No standings available.";
3754
+ return {
3755
+ text: withDisclaimer(degraded ? `${msg} (Live standings unavailable.)` : msg, source),
3756
+ data: { degraded, source: source ?? null, tables: args.group ? null : [] }
3757
+ };
3660
3758
  }
3661
- const wanted = args.group ? [args.group.toUpperCase()] : known;
3662
- const tables = wanted.map((g) => ({
3663
- group: g,
3664
- standings: computeStandings(fixturesByGroup(g, matches))
3665
- }));
3666
- const text = tables.map((t) => standingsTable(t.group, t.standings)).join("\n\n");
3759
+ let text = shaped.map((t) => standingsTable(t.group, t.standings)).join("\n\n");
3760
+ if (degraded) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
3667
3761
  return {
3668
- text: withDisclaimer(text || `No group found.`, source),
3669
- data: { degraded, source: source ?? null, tables: args.group ? tables[0] ?? null : tables }
3762
+ text: withDisclaimer(text, source),
3763
+ data: { degraded, source: source ?? null, tables: args.group ? shaped[0] ?? null : shaped }
3670
3764
  };
3671
3765
  }
3766
+ async function standingsResourceText(group, adapter) {
3767
+ const g = group.toUpperCase();
3768
+ const { tables, degraded, source } = await getStandings(adapter, g);
3769
+ const tb = tables[0];
3770
+ let text = tb ? standingsTable(tb.group, tb.rows) : `No group ${g}.`;
3771
+ if (degraded && tb) text += "\n\n(Live standings unavailable \u2014 showing the group roster.)";
3772
+ return withDisclaimer(text, source);
3773
+ }
3672
3774
  async function toolGetNextFixture(args) {
3673
3775
  const code = args.team.toUpperCase();
3674
- const fixture = nextFixtureForTeam(code);
3776
+ const fixture = nextFixtureForTeam(code, { from: args.now ?? /* @__PURE__ */ new Date() });
3675
3777
  if (!fixture) {
3676
3778
  return {
3677
3779
  text: withDisclaimer(`No upcoming fixture found for ${code}.`),
@@ -3772,6 +3874,7 @@ function shareResult(kind, target, team, input, options) {
3772
3874
  target,
3773
3875
  ...team ? { team } : {},
3774
3876
  source: input.source ?? null,
3877
+ degraded: input.degraded ?? false,
3775
3878
  informationalOnly: true,
3776
3879
  style: options.style ?? "social",
3777
3880
  snippet,
@@ -3789,7 +3892,7 @@ async function toolGetShareSnippet(args) {
3789
3892
  const options = shareOptions(args);
3790
3893
  const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve(/* @__PURE__ */ new Map()) : reliableSignalMap(args, ms);
3791
3894
  if (args.live) {
3792
- const { matches, source: source2 } = await getLiveMatches(resolveAdapter(args));
3895
+ const { matches, degraded: degraded2, source: source2 } = await getLiveMatches(resolveAdapter(args));
3793
3896
  return shareResult(
3794
3897
  "live",
3795
3898
  "live",
@@ -3798,7 +3901,9 @@ async function toolGetShareSnippet(args) {
3798
3901
  title: "Live match pulse",
3799
3902
  matches,
3800
3903
  source: source2,
3801
- emptyNote: "No matches in play right now.",
3904
+ degraded: degraded2,
3905
+ // Feed down ⇒ don't let an empty card read as "nothing is on".
3906
+ emptyNote: degraded2 ? "Live scores unavailable right now \u2014 couldn't reach the data provider." : "No matches in play right now.",
3802
3907
  installLine: "npx @claudinho/cli live",
3803
3908
  tz: args.tz,
3804
3909
  locale: args.lang
@@ -3806,8 +3911,37 @@ async function toolGetShareSnippet(args) {
3806
3911
  { ...options, includeMarkets: false }
3807
3912
  );
3808
3913
  }
3914
+ if (args.group) {
3915
+ const group = args.group.toUpperCase();
3916
+ const { tables, degraded: degraded2, source: source2 } = await getStandings(resolveAdapter(args), group);
3917
+ const snippet = formatShareTable(
3918
+ {
3919
+ tables,
3920
+ // Degraded ⇒ static roster, no live provider: don't attribute one, and
3921
+ // surface the not-live notice (the card gets pasted publicly).
3922
+ source: degraded2 ? void 0 : source2,
3923
+ installLine: `npx @claudinho/cli table ${group}`,
3924
+ emptyNote: `No group ${group}.`,
3925
+ degraded: degraded2
3926
+ },
3927
+ options
3928
+ );
3929
+ return {
3930
+ text: snippet,
3931
+ data: {
3932
+ kind: "table",
3933
+ target: "table",
3934
+ group,
3935
+ source: degraded2 ? null : source2 ?? null,
3936
+ degraded: degraded2,
3937
+ informationalOnly: true,
3938
+ snippet,
3939
+ tables: tables.map((tb) => ({ group: tb.group, standings: tb.rows }))
3940
+ }
3941
+ };
3942
+ }
3809
3943
  if (args.matchId) {
3810
- const { match, source: source2 } = await getMatchById(resolveAdapter(args), args.matchId);
3944
+ const { match, degraded: degraded2, source: source2 } = await getMatchById(resolveAdapter(args), args.matchId);
3811
3945
  const matches = match ? [match] : [];
3812
3946
  return shareResult(
3813
3947
  "match",
@@ -3818,6 +3952,7 @@ async function toolGetShareSnippet(args) {
3818
3952
  matches,
3819
3953
  marketSignals: await signalsFor(matches),
3820
3954
  source: source2,
3955
+ degraded: degraded2,
3821
3956
  emptyNote: `No match found with id ${args.matchId}.`,
3822
3957
  installLine: `npx @claudinho/cli match ${args.matchId}`,
3823
3958
  tz: args.tz,
@@ -3828,7 +3963,7 @@ async function toolGetShareSnippet(args) {
3828
3963
  }
3829
3964
  if (args.team) {
3830
3965
  const code = args.team.toUpperCase();
3831
- const fixture = nextFixtureForTeam(code);
3966
+ const fixture = nextFixtureForTeam(code, { from: args.now ?? /* @__PURE__ */ new Date() });
3832
3967
  const matches = fixture ? [fixture] : [];
3833
3968
  const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
3834
3969
  return shareResult(
@@ -3848,7 +3983,7 @@ async function toolGetShareSnippet(args) {
3848
3983
  );
3849
3984
  }
3850
3985
  const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
3851
- const { matches: all, source } = await getMatchesForDate(resolveAdapter(args), date);
3986
+ const { matches: all, degraded, source } = await getMatchesForDate(resolveAdapter(args), date);
3852
3987
  const todays = fixturesByDate(date, all, args.tz);
3853
3988
  const human = formatDate(`${date}T12:00:00.000Z`, { tz: args.tz, locale: args.lang });
3854
3989
  return shareResult(
@@ -3860,6 +3995,7 @@ async function toolGetShareSnippet(args) {
3860
3995
  matches: todays,
3861
3996
  marketSignals: await signalsFor(todays),
3862
3997
  source,
3998
+ degraded,
3863
3999
  emptyNote: `No matches scheduled for ${human}.`,
3864
4000
  installLine: "npx @claudinho/cli today",
3865
4001
  tz: args.tz,
@@ -3871,7 +4007,7 @@ async function toolGetShareSnippet(args) {
3871
4007
 
3872
4008
  // src/server.ts
3873
4009
  var SERVER_NAME = "claudinho";
3874
- var SERVER_VERSION = "0.4.3";
4010
+ var SERVER_VERSION = "0.5.1";
3875
4011
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
3876
4012
  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.`;
3877
4013
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -3905,7 +4041,7 @@ function buildServer() {
3905
4041
  "get_today",
3906
4042
  {
3907
4043
  title: "Today's matches",
3908
- description: "Fixtures for a given date (default: today), with live scores overlaid.",
4044
+ 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.",
3909
4045
  inputSchema: {
3910
4046
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
3911
4047
  ...commonArgs
@@ -3919,7 +4055,7 @@ function buildServer() {
3919
4055
  "get_live",
3920
4056
  {
3921
4057
  title: "Live matches",
3922
- description: "Matches currently in play, with score and minute.",
4058
+ 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.",
3923
4059
  inputSchema: { ...commonArgs },
3924
4060
  annotations: { readOnlyHint: true, openWorldHint: true }
3925
4061
  },
@@ -3929,7 +4065,7 @@ function buildServer() {
3929
4065
  "get_match",
3930
4066
  {
3931
4067
  title: "Match detail",
3932
- description: "A single match by its id, with live state if available.",
4068
+ 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.",
3933
4069
  inputSchema: { id: z.string().describe("Match id"), ...commonArgs },
3934
4070
  annotations: { readOnlyHint: true, openWorldHint: true }
3935
4071
  },
@@ -3939,7 +4075,7 @@ function buildServer() {
3939
4075
  "get_standings",
3940
4076
  {
3941
4077
  title: "Group standings",
3942
- description: "Group table(s). Pass a group letter A\u2013L, or omit for all groups.",
4078
+ 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.",
3943
4079
  inputSchema: {
3944
4080
  group: groupArg.optional().describe("Group letter A\u2013L (omit for all)"),
3945
4081
  ...commonArgs
@@ -3981,10 +4117,11 @@ function buildServer() {
3981
4117
  "get_share_snippet",
3982
4118
  {
3983
4119
  title: "Shareable match snippet",
3984
- 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.",
4120
+ 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.`,
3985
4121
  inputSchema: {
3986
4122
  matchId: z.string().optional().describe("Match id (most specific)"),
3987
4123
  team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),
4124
+ group: groupArg.optional().describe("Group letter A\u2013L for a standings card, e.g. A"),
3988
4125
  date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
3989
4126
  live: z.boolean().optional().describe("Snapshot of matches in play right now"),
3990
4127
  style: z.enum(["social", "compact"]).optional().describe("social (default, full card) or compact (one line per match)"),
@@ -4003,13 +4140,12 @@ function buildServer() {
4003
4140
  new ResourceTemplate("standings://{group}", { list: void 0 }),
4004
4141
  {
4005
4142
  title: "Group standings",
4006
- description: "Static group table for a group letter A\u2013L.",
4143
+ description: "Live group table for a group letter A\u2013L.",
4007
4144
  mimeType: "text/plain"
4008
4145
  },
4009
4146
  async (uri, variables) => {
4010
- const group = String(variables.group ?? "").toUpperCase();
4011
- const rows = computeStandings(fixturesByGroup(group));
4012
- const text = rows.length ? standingsTable(group, rows) : `No group ${group}.`;
4147
+ const group = String(variables.group ?? "");
4148
+ const text = await standingsResourceText(group, makeAdapter());
4013
4149
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text }] };
4014
4150
  }
4015
4151
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
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.3"
57
+ "@claudinho/core": "0.5.1"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsup",