@claudinho/mcp 0.8.11 → 0.8.12

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 +11 -5
  2. package/dist/index.js +50 -29
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -3,10 +3,7 @@
3
3
  **An MCP server for the 2026 men's football tournament.** Ask your agent about live
4
4
  scores, fixtures, group standings, and the prediction-market read — in Claude Code,
5
5
  Cursor, Codex, Claude Desktop, Windsurf, Zed, VS Code, or any MCP client (stdio).
6
-
7
- > ⚠️ **Not affiliated with, endorsed by, or connected to FIFA or Anthropic.**
8
- > Claudinho is an independent, open-source fan project. Factual match data with
9
- > emoji flags only — no logos, crests, kits, footage, or player likenesses.
6
+ No API key, no signup — all 104 fixtures ship bundled; only live scores (and optional, read-only Polymarket signals) hit the network.
10
7
 
11
8
  ## Install
12
9
 
@@ -31,6 +28,11 @@ codex mcp add claudinho -- npx -y @claudinho/mcp
31
28
  Claude Desktop: Settings → Developer → Edit Config, then restart. Codex config file:
32
29
  `~/.codex/config.toml` (`[mcp_servers.claudinho]`, `command = "npx"`, `args = ["-y", "@claudinho/mcp"]`).
33
30
 
31
+ Then just ask your agent naturally — it picks the right tool and answers with live data:
32
+
33
+ > "What matches are on today?" · "Show me the live scores" · "What's Mexico's next game?"
34
+ > "Group A standings" · "Show the knockout bracket" · "Make me a shareable card for today"
35
+
34
36
  ## Tools
35
37
 
36
38
  | Tool | What it does |
@@ -40,7 +42,7 @@ Claude Desktop: Settings → Developer → Edit Config, then restart. Codex conf
40
42
  | `get_match` | a single match by id |
41
43
  | `get_standings` | live cumulative group table(s) — one group `A`–`L`, or all |
42
44
  | `get_bracket` | knockout bracket from the Round of 32 through the final — optional `stage` filter (`R32`, `R16`, `QF`, `SF`, `3P`, `F`) |
43
- | `get_next_fixture` | a team's next match (3-letter code, e.g. `MEX`) — fully offline |
45
+ | `get_next_fixture` | a team's next match (3-letter code, e.g. `MEX`) — live-resolves a confirmed knockout tie from the feed; group fixtures offline, fails back to the bundled schedule if the feed is down |
44
46
  | `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 |
45
47
  | `get_share_snippet` | a copy-pasteable plain-text card — for a match, a team's next fixture, a group's standings table (`group`), the knockout bracket (`bracket: true`, optional `knockoutStage`), a date, or live — hand the returned snippet to the user as-is |
46
48
 
@@ -80,6 +82,10 @@ diagnostics go to stderr.
80
82
 
81
83
  MIT © 2026 Arturo Garrido · [source & issues](https://github.com/arturogarrido/claudinho)
82
84
 
85
+ > **Not affiliated with, endorsed by, or connected to FIFA or Anthropic.** An independent,
86
+ > open-source fan project showing factual match data with emoji flags only — no logos, crests,
87
+ > kits, broadcast footage, or player likenesses.
88
+
83
89
  ---
84
90
 
85
91
  _Built while watching the games._ **#VibingLaVidaLoca** ⚽
package/dist/index.js CHANGED
@@ -500,6 +500,10 @@ function localDate(iso, tz) {
500
500
  timeZone: zone
501
501
  }).format(new Date(iso));
502
502
  }
503
+ function shiftUtcDate(dateISO, days) {
504
+ const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
505
+ return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
506
+ }
503
507
  function outcomeFromScore(home, away) {
504
508
  if (home > away) return "H";
505
509
  if (home < away) return "A";
@@ -3829,14 +3833,19 @@ async function getBracket(adapter, opts = {}) {
3829
3833
  source
3830
3834
  };
3831
3835
  }
3832
- function shiftUtcDate(dateISO, days) {
3833
- const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
3834
- return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
3835
- }
3836
3836
  var EXTRA_TIME_SLACK_MS = 60 * 6e4;
3837
3837
  async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
3838
3838
  const nowMs = now.getTime();
3839
- const candidate = fixturesByTeam(code).find((m) => {
3839
+ let fixtures = allFixtures();
3840
+ let overlayFailed = false;
3841
+ try {
3842
+ if (adapter.fetchWindow) {
3843
+ fixtures = mergeLive(fixtures, await adapter.fetchWindow(KNOCKOUT_WINDOW_START, KNOCKOUT_WINDOW_END));
3844
+ }
3845
+ } catch {
3846
+ overlayFailed = true;
3847
+ }
3848
+ const candidate = fixturesByTeam(code, fixtures).find((m) => {
3840
3849
  const k = Date.parse(m.kickoff);
3841
3850
  return nowMs >= k && nowMs <= k + LIVE_WINDOW_MS + EXTRA_TIME_SLACK_MS;
3842
3851
  });
@@ -3845,8 +3854,8 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
3845
3854
  const m = r.match ?? candidate;
3846
3855
  if (!isFinished(m.status)) return { ...r, match: m };
3847
3856
  }
3848
- const next = nextFixtureForTeam(code, { from: now });
3849
- return { match: next, degraded: false };
3857
+ const next = nextFixtureForTeam(code, { from: now, fixtures });
3858
+ return { match: next, degraded: overlayFailed };
3850
3859
  }
3851
3860
  async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Date()) {
3852
3861
  const base = allFixtures();
@@ -3939,6 +3948,9 @@ function mapsCleanly(match, outcomes) {
3939
3948
  if (match.stage === "GROUP" && !draw) return false;
3940
3949
  return true;
3941
3950
  }
3951
+ function marketSignalRendersFor(match, signal) {
3952
+ return signal.matchId === match.id && mapsCleanly(match, signal.outcomes);
3953
+ }
3942
3954
  function hasSaneDistribution(outcomes) {
3943
3955
  const priced = outcomes.filter((o) => Number.isFinite(o.probability) && o.probability > 0);
3944
3956
  if (priced.length < 2) return false;
@@ -4134,12 +4146,15 @@ var PolymarketProvider = class {
4134
4146
  */
4135
4147
  async resolveOne(match, options) {
4136
4148
  const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4137
- const eventSlug = entry?.eventSlug ?? deriveEventSlug(match);
4138
- if (!eventSlug) return { checked: true };
4149
+ const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4150
+ if (slugs.length === 0) return { checked: true };
4139
4151
  try {
4140
- const event = await this.fetchEvent(eventSlug, options?.timeoutMs);
4141
- const signal = event ? this.toSignal(match, eventSlug, event, options) : void 0;
4142
- return { signal, checked: true };
4152
+ for (const slug of slugs) {
4153
+ const event = await this.fetchEvent(slug, options?.timeoutMs);
4154
+ const signal = event ? this.toSignal(match, slug, event, options) : void 0;
4155
+ if (signal) return { signal, checked: true };
4156
+ }
4157
+ return { checked: true };
4143
4158
  } catch {
4144
4159
  return { checked: false };
4145
4160
  }
@@ -4215,14 +4230,17 @@ var PolymarketProvider = class {
4215
4230
  return signal.ambiguous ? void 0 : signal;
4216
4231
  }
4217
4232
  };
4218
- function deriveEventSlug(match) {
4233
+ function deriveEventSlugs(match) {
4219
4234
  const home = match.home.code.toLowerCase();
4220
4235
  const away = match.away.code.toLowerCase();
4221
- if (home === away || home === "tbd" || away === "tbd") return void 0;
4222
- if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return void 0;
4223
- const date = match.kickoff.slice(0, 10);
4224
- if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return void 0;
4225
- return `fifwc-${home}-${away}-${date}`;
4236
+ if (home === away || home === "tbd" || away === "tbd") return [];
4237
+ if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return [];
4238
+ const utcDate = match.kickoff.slice(0, 10);
4239
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(utcDate)) return [];
4240
+ return [
4241
+ `fifwc-${home}-${away}-${utcDate}`,
4242
+ `fifwc-${home}-${away}-${shiftUtcDate(utcDate, -1)}`
4243
+ ];
4226
4244
  }
4227
4245
  function slugToken(m) {
4228
4246
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
@@ -4587,8 +4605,8 @@ function resolveAdapter(args) {
4587
4605
  function resolveMarketProvider(args) {
4588
4606
  return args.marketProvider ?? makeMarketProvider();
4589
4607
  }
4590
- function marketDisplayable(sig) {
4591
- return !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
4608
+ function marketDisplayable(match, sig) {
4609
+ return marketSignalRendersFor(match, sig) && !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
4592
4610
  }
4593
4611
  function marketHeader(m, args) {
4594
4612
  const when = formatDate(m.kickoff, { tz: args.tz, locale: args.lang });
@@ -4670,7 +4688,9 @@ async function reliableMarketData(args, matches) {
4670
4688
  const out = {};
4671
4689
  for (const m of relevant) {
4672
4690
  const s = signals.get(m.id);
4673
- if (s && isReliableMarketSignal(s, { now })) out[m.id] = marketData(s);
4691
+ if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(m, s)) {
4692
+ out[m.id] = marketData(s);
4693
+ }
4674
4694
  }
4675
4695
  return Object.keys(out).length > 0 ? out : void 0;
4676
4696
  }
@@ -4731,7 +4751,7 @@ async function toolGetMatch(args) {
4731
4751
  let marketSignal;
4732
4752
  if (marketsEnabled() && marketRelevant(match, now)) {
4733
4753
  const s = (await cachedMarketSignals(args, [match])).get(match.id);
4734
- if (s && isReliableMarketSignal(s, { now })) marketSignal = s;
4754
+ if (s && isReliableMarketSignal(s, { now }) && marketSignalRendersFor(match, s)) marketSignal = s;
4735
4755
  }
4736
4756
  const base = matchLine(match, opts);
4737
4757
  let text = marketSignal ? `${base}
@@ -4833,7 +4853,7 @@ async function toolGetMarketSignal(args) {
4833
4853
  const { match } = await getMatchById(resolveAdapter(args), args.matchId);
4834
4854
  const relevant = match ? marketRelevant(match, now) : false;
4835
4855
  const sig = match && relevant ? await getMarketSignal(provider, match) : void 0;
4836
- const shown2 = match && sig && marketDisplayable(sig) ? sig : void 0;
4856
+ const shown2 = match && sig && marketDisplayable(match, sig) ? sig : void 0;
4837
4857
  const text2 = !match ? `No match found with id ${args.matchId}.` : shown2 ? marketText(match, shown2, args) : noSignalText(match, args, now);
4838
4858
  return {
4839
4859
  text: withDisclaimer(text2),
@@ -4846,16 +4866,17 @@ async function toolGetMarketSignal(args) {
4846
4866
  }
4847
4867
  if (args.team) {
4848
4868
  const code = args.team.toUpperCase();
4849
- const { match: fixture } = await marketFixtureForTeam(resolveAdapter(args), code, now);
4869
+ const { match: fixture, degraded } = await marketFixtureForTeam(resolveAdapter(args), code, now);
4850
4870
  const relevant = fixture ? marketRelevant(fixture, now) : false;
4851
4871
  const sig = fixture && relevant ? await getMarketSignal(provider, fixture) : void 0;
4852
- const shown2 = fixture && sig && marketDisplayable(sig) ? sig : void 0;
4853
- const text2 = !fixture ? `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
4872
+ const shown2 = fixture && sig && marketDisplayable(fixture, sig) ? sig : void 0;
4873
+ const text2 = !fixture ? degraded ? `Live feed unavailable \u2014 can't resolve ${code}'s next fixture right now.` : `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2, args) : noSignalText(fixture, args, now);
4854
4874
  return {
4855
4875
  text: withDisclaimer(text2),
4856
4876
  data: {
4857
4877
  team: code,
4858
4878
  matchId: fixture?.id ?? null,
4879
+ degraded,
4859
4880
  informationalOnly: true,
4860
4881
  signal: shown2 ? marketData(shown2) : null
4861
4882
  }
@@ -4866,7 +4887,7 @@ async function toolGetMarketSignal(args) {
4866
4887
  const todays = fixturesByDate(date, matches, args.tz).filter((m) => marketRelevant(m, now));
4867
4888
  const { signals } = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
4868
4889
  const shown = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
4869
- (r) => !!r.signal && marketDisplayable(r.signal)
4890
+ (r) => !!r.signal && marketDisplayable(r.match, r.signal)
4870
4891
  );
4871
4892
  const text = shown.length ? `Market signals on ${date}:
4872
4893
  ${shown.map(({ match, signal }) => marketText(match, signal, args)).join("\n\n")}` : `No reliable market signals on ${date}.`;
@@ -4888,7 +4909,7 @@ async function reliableSignalMap(args, matches) {
4888
4909
  const out = /* @__PURE__ */ new Map();
4889
4910
  for (const m of relevant) {
4890
4911
  const s = signals.get(m.id);
4891
- if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(s)) out.set(m.id, s);
4912
+ if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(m, s)) out.set(m.id, s);
4892
4913
  }
4893
4914
  return out;
4894
4915
  }
@@ -5093,7 +5114,7 @@ async function toolGetShareSnippet(args) {
5093
5114
 
5094
5115
  // src/server.ts
5095
5116
  var SERVER_NAME = "claudinho";
5096
- var SERVER_VERSION = "0.8.11";
5117
+ var SERVER_VERSION = "0.8.12";
5097
5118
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
5098
5119
  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.`;
5099
5120
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
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",
@@ -57,7 +57,7 @@
57
57
  "tsup": "^8.0.0",
58
58
  "typescript": "^5.7.0",
59
59
  "vitest": "^4.1.9",
60
- "@claudinho/core": "0.8.11"
60
+ "@claudinho/core": "0.8.12"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",