@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.
- package/README.md +11 -5
- package/dist/index.js +50 -29
- 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`) —
|
|
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
|
-
|
|
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:
|
|
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
|
|
4138
|
-
if (
|
|
4149
|
+
const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
|
|
4150
|
+
if (slugs.length === 0) return { checked: true };
|
|
4139
4151
|
try {
|
|
4140
|
-
const
|
|
4141
|
-
|
|
4142
|
-
|
|
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
|
|
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
|
|
4222
|
-
if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return
|
|
4223
|
-
const
|
|
4224
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(
|
|
4225
|
-
return
|
|
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 })
|
|
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.
|
|
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.
|
|
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.
|
|
60
|
+
"@claudinho/core": "0.8.12"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsup",
|