@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.
- package/README.md +4 -3
- package/dist/index.js +255 -101
- 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
|
|
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
|
|
2792
|
-
for (const
|
|
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 (
|
|
3378
|
-
footer.push(
|
|
3379
|
-
|
|
3380
|
-
|
|
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
|
|
3438
|
-
return `${name} ${pad(r.played)} ${pad(r.won)} ${pad(r.drawn)} ${pad(r.lost)} ${
|
|
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
|
-
|
|
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
|
|
3525
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
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
|
-
|
|
3631
|
-
|
|
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
|
|
3638
|
-
data: { degraded, source: source ?? null, tables: args.group ?
|
|
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 =
|
|
3661
|
-
const
|
|
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) :
|
|
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 =
|
|
3676
|
-
const
|
|
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) :
|
|
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(
|
|
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
|
|
3710
|
-
const
|
|
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
|
|
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.
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
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.
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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(
|
|
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:
|
|
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: "
|
|
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 ?? "")
|
|
3983
|
-
const
|
|
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.
|
|
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.
|
|
57
|
+
"@claudinho/core": "0.5.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "tsup",
|