@claudinho/mcp 0.2.0 → 0.4.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 +23 -5
- package/dist/index.js +883 -20
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @claudinho/mcp ⚽
|
|
2
2
|
|
|
3
3
|
**An MCP server for the 2026 men's football tournament.** Ask your agent about
|
|
4
|
-
live scores, fixtures,
|
|
5
|
-
any other MCP client.
|
|
4
|
+
live scores, fixtures, group standings, and prediction-market odds — in Claude
|
|
5
|
+
Code, Cursor, Codex, and any other MCP client.
|
|
6
6
|
|
|
7
7
|
> ⚠️ **Not affiliated with, endorsed by, or connected to FIFA or Anthropic.**
|
|
8
8
|
> Claudinho is an independent, open-source fan project. Factual match data with
|
|
@@ -48,12 +48,29 @@ any other MCP client with no changes.
|
|
|
48
48
|
| `get_match` | a single match by id |
|
|
49
49
|
| `get_standings` | group table(s) — one group `A`–`L`, or all |
|
|
50
50
|
| `get_next_fixture` | a team's next match (3-letter code, e.g. `MEX`) |
|
|
51
|
+
| `get_market_signal` | read-only prediction-market odds for a match, a team's next fixture, or a date — informational only |
|
|
52
|
+
| `get_share_snippet` | a copy-pasteable match-card snippet for a match, a team's next fixture, a date, or live — plain text, ready to paste |
|
|
51
53
|
|
|
52
54
|
All tools are **read-only** (annotated `readOnlyHint`) and accept optional
|
|
53
55
|
`tz` (IANA timezone), `lang` (`en`/`es`/`pt`/`fr`), and `flavor`
|
|
54
56
|
(`off`/`subtle`/`full`) arguments. Each response includes both human-readable
|
|
55
57
|
text and structured JSON.
|
|
56
58
|
|
|
59
|
+
`get_today` and `get_match` also include reliable prediction-market context when a
|
|
60
|
+
market is available. Match→market event slugs are derived automatically, so no
|
|
61
|
+
mapping is needed. Market data is **read-only and informational only — not betting
|
|
62
|
+
advice** (market-implied percentages with attribution, never links or trade
|
|
63
|
+
calls), sourced from Polymarket public data and shown only when a market maps
|
|
64
|
+
cleanly to the result. Disable it with `CLAUDINHO_MARKETS=off`; set
|
|
65
|
+
`CLAUDINHO_MARKETS_SOURCE=fake` (in the server `env`) for a network-free synthetic
|
|
66
|
+
preview.
|
|
67
|
+
|
|
68
|
+
`get_share_snippet` returns a polished, **copy-pasteable** match card (the same
|
|
69
|
+
artifact as the CLI's `claudinho share`) for a match, a team's next fixture, a
|
|
70
|
+
date, or live matches — plain text, no links, with the non-affiliation disclaimer
|
|
71
|
+
baked in. Hand the returned `snippet` to the user as-is. `style` picks `social`
|
|
72
|
+
(default) or `compact`; market lines (when reliable) stay **informational only**.
|
|
73
|
+
|
|
57
74
|
## Resources & prompts
|
|
58
75
|
|
|
59
76
|
- Resources: `standings://{group}` (e.g. `standings://A`), `fixtures://{date}` (UTC date, e.g. `fixtures://2026-06-11`)
|
|
@@ -87,9 +104,10 @@ to the `env` block of your MCP server entry.
|
|
|
87
104
|
|
|
88
105
|
## How it works
|
|
89
106
|
|
|
90
|
-
The fixture list ships bundled in the package; only live state hits the network
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
The fixture list ships bundled in the package; only live state hits the network —
|
|
108
|
+
live scores from **ESPN's** public scoreboard (a swappable provider, attributed
|
|
109
|
+
in the `source` field and text) and market odds from Polymarket. The server writes
|
|
110
|
+
nothing to stdout except the MCP protocol; diagnostics go to stderr.
|
|
93
111
|
|
|
94
112
|
## License
|
|
95
113
|
|
package/dist/index.js
CHANGED
|
@@ -315,6 +315,25 @@ function formatKickoff(iso, opts = {}) {
|
|
|
315
315
|
timeZone: tz
|
|
316
316
|
}).format(new Date(iso));
|
|
317
317
|
}
|
|
318
|
+
function formatDate(iso, opts = {}) {
|
|
319
|
+
const tz = resolveTz(opts.tz);
|
|
320
|
+
const locale = safeLocale(opts.locale);
|
|
321
|
+
return new Intl.DateTimeFormat(locale, {
|
|
322
|
+
month: "short",
|
|
323
|
+
day: "numeric",
|
|
324
|
+
timeZone: tz
|
|
325
|
+
}).format(new Date(iso));
|
|
326
|
+
}
|
|
327
|
+
function formatTime(iso, opts = {}) {
|
|
328
|
+
const tz = resolveTz(opts.tz);
|
|
329
|
+
const locale = safeLocale(opts.locale);
|
|
330
|
+
return new Intl.DateTimeFormat(locale, {
|
|
331
|
+
hour: "2-digit",
|
|
332
|
+
minute: "2-digit",
|
|
333
|
+
hour12: false,
|
|
334
|
+
timeZone: tz
|
|
335
|
+
}).format(new Date(iso));
|
|
336
|
+
}
|
|
318
337
|
function countdown(iso, from = /* @__PURE__ */ new Date()) {
|
|
319
338
|
const ms = new Date(iso).getTime() - from.getTime();
|
|
320
339
|
if (ms <= 0) return "now";
|
|
@@ -335,6 +354,9 @@ function localDate(iso, tz) {
|
|
|
335
354
|
timeZone: zone
|
|
336
355
|
}).format(new Date(iso));
|
|
337
356
|
}
|
|
357
|
+
function isLive(status) {
|
|
358
|
+
return status === "LIVE" || status === "HT";
|
|
359
|
+
}
|
|
338
360
|
function isFinished(status) {
|
|
339
361
|
return status === "FT";
|
|
340
362
|
}
|
|
@@ -2831,12 +2853,16 @@ function mergeLive(base, live) {
|
|
|
2831
2853
|
for (const m of live) byId.set(m.id, m);
|
|
2832
2854
|
return [...byId.values()];
|
|
2833
2855
|
}
|
|
2856
|
+
function liveSourceLabel(source) {
|
|
2857
|
+
const known = { espn: "ESPN" };
|
|
2858
|
+
return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
|
|
2859
|
+
}
|
|
2834
2860
|
async function getMatchesForDate(adapter, dateISO) {
|
|
2835
2861
|
const base = allFixtures();
|
|
2836
2862
|
const day = dateISO.slice(0, 10);
|
|
2837
2863
|
try {
|
|
2838
2864
|
const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
|
|
2839
|
-
return { matches: mergeLive(base, live), degraded: false };
|
|
2865
|
+
return { matches: mergeLive(base, live), degraded: false, source: adapter.name };
|
|
2840
2866
|
} catch {
|
|
2841
2867
|
return { matches: base, degraded: true };
|
|
2842
2868
|
}
|
|
@@ -2847,11 +2873,513 @@ function shiftUtcDate(dateISO, days) {
|
|
|
2847
2873
|
}
|
|
2848
2874
|
async function getLiveMatches(adapter) {
|
|
2849
2875
|
try {
|
|
2850
|
-
return { matches: await adapter.fetchLive(), degraded: false };
|
|
2876
|
+
return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
|
|
2851
2877
|
} catch {
|
|
2852
2878
|
return { matches: [], degraded: true };
|
|
2853
2879
|
}
|
|
2854
2880
|
}
|
|
2881
|
+
var DEFAULT_MAX_AGE_MS = 15 * 6e4;
|
|
2882
|
+
function normalizeOutcomes(outcomes) {
|
|
2883
|
+
const sum = outcomes.reduce(
|
|
2884
|
+
(s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
|
|
2885
|
+
0
|
|
2886
|
+
);
|
|
2887
|
+
if (sum <= 0) return outcomes.map((o) => ({ ...o, probability: 0 }));
|
|
2888
|
+
return outcomes.map((o) => ({
|
|
2889
|
+
...o,
|
|
2890
|
+
probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
|
|
2891
|
+
}));
|
|
2892
|
+
}
|
|
2893
|
+
function favoriteStrength(probability) {
|
|
2894
|
+
if (probability >= 0.65) return "clear";
|
|
2895
|
+
if (probability >= 0.52) return "slight";
|
|
2896
|
+
return "close";
|
|
2897
|
+
}
|
|
2898
|
+
function deriveFavorite(outcomes) {
|
|
2899
|
+
let top;
|
|
2900
|
+
for (const o of outcomes) {
|
|
2901
|
+
if (o.kind === "other") continue;
|
|
2902
|
+
if (!top || o.probability > top.probability) top = o;
|
|
2903
|
+
}
|
|
2904
|
+
if (!top || top.probability <= 0 || top.kind === "other") return void 0;
|
|
2905
|
+
return {
|
|
2906
|
+
kind: top.kind,
|
|
2907
|
+
teamCode: top.teamCode,
|
|
2908
|
+
probability: top.probability,
|
|
2909
|
+
strength: favoriteStrength(top.probability)
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
function mapsCleanly(match, outcomes) {
|
|
2913
|
+
if (outcomes.some((o) => o.kind === "other")) return false;
|
|
2914
|
+
const home = outcomes.find((o) => o.kind === "home");
|
|
2915
|
+
const away = outcomes.find((o) => o.kind === "away");
|
|
2916
|
+
const draw = outcomes.find((o) => o.kind === "draw");
|
|
2917
|
+
if (!home || !away) return false;
|
|
2918
|
+
if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
|
|
2919
|
+
return false;
|
|
2920
|
+
}
|
|
2921
|
+
if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
|
|
2922
|
+
return false;
|
|
2923
|
+
}
|
|
2924
|
+
if (match.stage === "GROUP" && !draw) return false;
|
|
2925
|
+
return true;
|
|
2926
|
+
}
|
|
2927
|
+
function hasSaneDistribution(outcomes) {
|
|
2928
|
+
const priced = outcomes.filter((o) => Number.isFinite(o.probability) && o.probability > 0);
|
|
2929
|
+
if (priced.length < 2) return false;
|
|
2930
|
+
const sum = priced.reduce((s, o) => s + o.probability, 0);
|
|
2931
|
+
return sum > 0.97 && sum < 1.03;
|
|
2932
|
+
}
|
|
2933
|
+
function isStaleSignal(signal, options = {}) {
|
|
2934
|
+
const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
2935
|
+
const asOf = Date.parse(signal.asOf);
|
|
2936
|
+
if (!Number.isFinite(asOf)) return true;
|
|
2937
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
2938
|
+
return now - asOf > maxAge;
|
|
2939
|
+
}
|
|
2940
|
+
function isReliableMarketSignal(signal, options = {}) {
|
|
2941
|
+
if (options.includeUnreliable) return true;
|
|
2942
|
+
if (signal.ambiguous) return false;
|
|
2943
|
+
if (!signal.favorite) return false;
|
|
2944
|
+
if (!hasSaneDistribution(signal.outcomes)) return false;
|
|
2945
|
+
if (signal.stale || isStaleSignal(signal, options)) return false;
|
|
2946
|
+
if (options.minLiquidity != null) {
|
|
2947
|
+
if (signal.liquidity == null || signal.liquidity < options.minLiquidity) return false;
|
|
2948
|
+
}
|
|
2949
|
+
return true;
|
|
2950
|
+
}
|
|
2951
|
+
function buildMarketSignal(input) {
|
|
2952
|
+
const outcomes = normalizeOutcomes(input.outcomes);
|
|
2953
|
+
const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
|
|
2954
|
+
const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
|
|
2955
|
+
const signal = {
|
|
2956
|
+
matchId: input.match.id,
|
|
2957
|
+
source: input.source,
|
|
2958
|
+
sourceMarketId: input.sourceMarketId,
|
|
2959
|
+
asOf: input.asOf,
|
|
2960
|
+
fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2961
|
+
outcomes,
|
|
2962
|
+
favorite,
|
|
2963
|
+
liquidity: input.liquidity,
|
|
2964
|
+
volume24h: input.volume24h,
|
|
2965
|
+
stale: false,
|
|
2966
|
+
ambiguous
|
|
2967
|
+
};
|
|
2968
|
+
signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
|
|
2969
|
+
return signal;
|
|
2970
|
+
}
|
|
2971
|
+
function pct(p) {
|
|
2972
|
+
return Math.round(p * 100);
|
|
2973
|
+
}
|
|
2974
|
+
function marketSourceLabel(source) {
|
|
2975
|
+
if (source === "polymarket") return "Polymarket";
|
|
2976
|
+
if (source === "fake") return "demo data";
|
|
2977
|
+
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
2978
|
+
}
|
|
2979
|
+
function outcomeLabel(o, match) {
|
|
2980
|
+
if (o.kind === "home") return match.home.name;
|
|
2981
|
+
if (o.kind === "away") return match.away.name;
|
|
2982
|
+
if (o.kind === "draw") return "Draw";
|
|
2983
|
+
return o.label;
|
|
2984
|
+
}
|
|
2985
|
+
function utcHhmm(iso) {
|
|
2986
|
+
const t = Date.parse(iso);
|
|
2987
|
+
if (!Number.isFinite(t)) return "";
|
|
2988
|
+
return `${new Date(t).toISOString().slice(11, 16)} UTC`;
|
|
2989
|
+
}
|
|
2990
|
+
function marketFavoriteText(signal, match) {
|
|
2991
|
+
const fav = signal.favorite;
|
|
2992
|
+
if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
|
|
2993
|
+
if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
|
|
2994
|
+
const name = fav.kind === "home" ? match.home.name : match.away.name;
|
|
2995
|
+
return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
|
|
2996
|
+
}
|
|
2997
|
+
function marketProbabilityText(signal, match) {
|
|
2998
|
+
const order = ["home", "draw", "away"];
|
|
2999
|
+
const parts = [];
|
|
3000
|
+
for (const kind of order) {
|
|
3001
|
+
const o = signal.outcomes.find((x) => x.kind === kind);
|
|
3002
|
+
if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
3003
|
+
}
|
|
3004
|
+
for (const o of signal.outcomes) {
|
|
3005
|
+
if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
3006
|
+
}
|
|
3007
|
+
return parts.join(" \xB7 ");
|
|
3008
|
+
}
|
|
3009
|
+
function marketAttributionText(signal) {
|
|
3010
|
+
const time = utcHhmm(signal.asOf);
|
|
3011
|
+
const src = `Source: ${marketSourceLabel(signal.source)}`;
|
|
3012
|
+
return time ? `${src} \xB7 updated ${time}` : src;
|
|
3013
|
+
}
|
|
3014
|
+
function marketLine(signal, match) {
|
|
3015
|
+
return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
|
|
3016
|
+
signal.source
|
|
3017
|
+
)} \xB7 informational only`;
|
|
3018
|
+
}
|
|
3019
|
+
function marketBlock(signal, match) {
|
|
3020
|
+
const lines = [];
|
|
3021
|
+
if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
|
|
3022
|
+
lines.push(marketFavoriteText(signal, match));
|
|
3023
|
+
lines.push(marketProbabilityText(signal, match));
|
|
3024
|
+
lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
|
|
3025
|
+
return lines;
|
|
3026
|
+
}
|
|
3027
|
+
var FakeMarketProvider = class {
|
|
3028
|
+
constructor(opts = {}) {
|
|
3029
|
+
this.opts = opts;
|
|
3030
|
+
}
|
|
3031
|
+
opts;
|
|
3032
|
+
name = "fake";
|
|
3033
|
+
async findSignal(match, options) {
|
|
3034
|
+
const preset = this.opts.signals?.[match.id];
|
|
3035
|
+
if (preset) return preset;
|
|
3036
|
+
if (this.opts.synthesize) return this.synthesize(match, options);
|
|
3037
|
+
return void 0;
|
|
3038
|
+
}
|
|
3039
|
+
async findSignals(matches, options) {
|
|
3040
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3041
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3042
|
+
for (const m of matches) {
|
|
3043
|
+
checked.add(m.id);
|
|
3044
|
+
const s = await this.findSignal(m, options);
|
|
3045
|
+
if (s) signals.set(m.id, s);
|
|
3046
|
+
}
|
|
3047
|
+
return { signals, checked };
|
|
3048
|
+
}
|
|
3049
|
+
synthesize(match, options) {
|
|
3050
|
+
const seed = hash(`${match.home.code}-${match.away.code}`);
|
|
3051
|
+
const home = 0.3 + seed % 33 / 100;
|
|
3052
|
+
const away = 0.18 + (seed >> 3) % 23 / 100;
|
|
3053
|
+
const draw = Math.max(0.05, 1 - home - away);
|
|
3054
|
+
const outcomes = [
|
|
3055
|
+
{ kind: "home", teamCode: match.home.code, label: match.home.name, probability: home },
|
|
3056
|
+
{ kind: "draw", label: "Draw", probability: draw },
|
|
3057
|
+
{ kind: "away", teamCode: match.away.code, label: match.away.name, probability: away }
|
|
3058
|
+
];
|
|
3059
|
+
const now = this.opts.now ?? options?.now ?? /* @__PURE__ */ new Date();
|
|
3060
|
+
const asOf = new Date(now.getTime() - 6e4).toISOString();
|
|
3061
|
+
return buildMarketSignal({
|
|
3062
|
+
match,
|
|
3063
|
+
source: "fake",
|
|
3064
|
+
sourceMarketId: `fake-${match.id}`,
|
|
3065
|
+
asOf,
|
|
3066
|
+
fetchedAt: now.toISOString(),
|
|
3067
|
+
outcomes,
|
|
3068
|
+
liquidity: 5e4,
|
|
3069
|
+
now,
|
|
3070
|
+
maxAgeMs: options?.maxAgeMs
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
function hash(s) {
|
|
3075
|
+
let h = 0;
|
|
3076
|
+
for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) >>> 0;
|
|
3077
|
+
return h % 1e5;
|
|
3078
|
+
}
|
|
3079
|
+
var mapping_2026_default = {
|
|
3080
|
+
version: 1,
|
|
3081
|
+
note: "Optional matchId -> Polymarket EVENT-slug OVERRIDES. By default the slug is DERIVED from each fixture (fifwc-{home}-{away}-{UTC-date}, e.g. fifwc-mex-rsa-2026-06-11), so most matches need NO entry here. Add an entry only for a fixture whose real Polymarket slug differs (e.g. a team abbreviation that isn't the FIFA code, or a tz-shifted date). The event payload carries the three moneyline binary markets; each outcome's probability is its 'Yes' price; validation fails closed. Entry: { eventSlug, eventId? }.",
|
|
3082
|
+
markets: {}
|
|
3083
|
+
};
|
|
3084
|
+
var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
|
|
3085
|
+
var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
|
|
3086
|
+
var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
|
|
3087
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
3088
|
+
var WC_SERIES_SLUG = "soccer-fifwc";
|
|
3089
|
+
var WC_SPORT = "fifwc";
|
|
3090
|
+
var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
|
|
3091
|
+
var NON_REGULAR_TIME = /extra time|penalt|to advance|to qualif|win the (group|tournament|cup|title)/i;
|
|
3092
|
+
var BUNDLED_MAPPING = mapping_2026_default.markets;
|
|
3093
|
+
var PolymarketProvider = class {
|
|
3094
|
+
constructor(opts = {}) {
|
|
3095
|
+
this.opts = opts;
|
|
3096
|
+
}
|
|
3097
|
+
opts;
|
|
3098
|
+
name = "polymarket";
|
|
3099
|
+
async findSignal(match, options) {
|
|
3100
|
+
return (await this.resolveOne(match, options)).signal;
|
|
3101
|
+
}
|
|
3102
|
+
async findSignals(matches, options) {
|
|
3103
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3104
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3105
|
+
const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
|
|
3106
|
+
for (const m of matches) {
|
|
3107
|
+
if (Date.now() >= deadline) break;
|
|
3108
|
+
const r = await this.resolveOne(m, options);
|
|
3109
|
+
if (r.checked) checked.add(m.id);
|
|
3110
|
+
if (r.signal) signals.set(m.id, r.signal);
|
|
3111
|
+
}
|
|
3112
|
+
return { signals, checked };
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
|
|
3116
|
+
* source and found no usable market, or the fixture is unmappable) from a
|
|
3117
|
+
* provider/network error — so transient failures are retried, not
|
|
3118
|
+
* negative-cached.
|
|
3119
|
+
*/
|
|
3120
|
+
async resolveOne(match, options) {
|
|
3121
|
+
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
3122
|
+
const eventSlug = entry?.eventSlug ?? deriveEventSlug(match);
|
|
3123
|
+
if (!eventSlug) return { checked: true };
|
|
3124
|
+
try {
|
|
3125
|
+
const event = await this.fetchEvent(eventSlug, options?.timeoutMs);
|
|
3126
|
+
const signal = event ? this.toSignal(match, eventSlug, event, options) : void 0;
|
|
3127
|
+
return { signal, checked: true };
|
|
3128
|
+
} catch {
|
|
3129
|
+
return { checked: false };
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
async fetchEvent(slug, timeoutMs) {
|
|
3133
|
+
const base = this.opts.baseUrl ?? DEFAULT_BASE2;
|
|
3134
|
+
assertAllowedHost(base);
|
|
3135
|
+
const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
|
|
3136
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
3137
|
+
const res = await doFetch(url, {
|
|
3138
|
+
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
3139
|
+
headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
|
|
3140
|
+
});
|
|
3141
|
+
if (res.status === 404) return void 0;
|
|
3142
|
+
if (!res.ok) {
|
|
3143
|
+
throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
|
|
3144
|
+
}
|
|
3145
|
+
const data = await res.json();
|
|
3146
|
+
const event = Array.isArray(data) ? data[0] : data;
|
|
3147
|
+
return event && typeof event === "object" ? event : void 0;
|
|
3148
|
+
}
|
|
3149
|
+
toSignal(match, eventSlug, event, options) {
|
|
3150
|
+
if (event.active === false || event.closed === true) return void 0;
|
|
3151
|
+
if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
|
|
3152
|
+
return void 0;
|
|
3153
|
+
}
|
|
3154
|
+
if (event.slug != null && event.slug !== eventSlug) return void 0;
|
|
3155
|
+
const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
|
|
3156
|
+
const kick = Date.parse(match.kickoff);
|
|
3157
|
+
if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
|
|
3158
|
+
return void 0;
|
|
3159
|
+
}
|
|
3160
|
+
const moneyline = (event.markets ?? []).filter(
|
|
3161
|
+
(m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
|
|
3162
|
+
);
|
|
3163
|
+
const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
|
|
3164
|
+
const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
|
|
3165
|
+
const drawMarket = pickDraw(moneyline);
|
|
3166
|
+
if (!homeMarket || !awayMarket) return void 0;
|
|
3167
|
+
const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
|
|
3168
|
+
if (new Set(legIds).size !== legIds.length) return void 0;
|
|
3169
|
+
const legs = [
|
|
3170
|
+
["home", homeMarket, match.home.code, match.home.name],
|
|
3171
|
+
["draw", drawMarket, void 0, "Draw"],
|
|
3172
|
+
["away", awayMarket, match.away.code, match.away.name]
|
|
3173
|
+
];
|
|
3174
|
+
const outcomes = [];
|
|
3175
|
+
let asOf = event.updatedAt;
|
|
3176
|
+
let liquidity;
|
|
3177
|
+
for (const [kind, market, teamCode, label] of legs) {
|
|
3178
|
+
if (!market) continue;
|
|
3179
|
+
if (market.closed === true || market.active === false) return void 0;
|
|
3180
|
+
if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
|
|
3181
|
+
const yes = yesPrice(market);
|
|
3182
|
+
if (yes == null) return void 0;
|
|
3183
|
+
outcomes.push({ kind, teamCode, label, probability: yes });
|
|
3184
|
+
if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
|
|
3185
|
+
const liq = numberish(market.liquidityNum ?? market.liquidity);
|
|
3186
|
+
if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
|
|
3187
|
+
}
|
|
3188
|
+
const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
|
|
3189
|
+
if (rawSum < 0.9 || rawSum > 1.15) return void 0;
|
|
3190
|
+
const signal = buildMarketSignal({
|
|
3191
|
+
match,
|
|
3192
|
+
source: "polymarket",
|
|
3193
|
+
sourceMarketId: event.id ?? eventSlug,
|
|
3194
|
+
asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3195
|
+
outcomes,
|
|
3196
|
+
liquidity,
|
|
3197
|
+
now: options?.now ?? this.opts.now,
|
|
3198
|
+
maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
|
|
3199
|
+
});
|
|
3200
|
+
return signal.ambiguous ? void 0 : signal;
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
function deriveEventSlug(match) {
|
|
3204
|
+
const home = match.home.code.toLowerCase();
|
|
3205
|
+
const away = match.away.code.toLowerCase();
|
|
3206
|
+
if (home === away || home === "tbd" || away === "tbd") return void 0;
|
|
3207
|
+
if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return void 0;
|
|
3208
|
+
const date = match.kickoff.slice(0, 10);
|
|
3209
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return void 0;
|
|
3210
|
+
return `fifwc-${home}-${away}-${date}`;
|
|
3211
|
+
}
|
|
3212
|
+
function slugToken(m) {
|
|
3213
|
+
return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
|
|
3214
|
+
}
|
|
3215
|
+
function isDrawMarket(m) {
|
|
3216
|
+
return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
|
|
3217
|
+
}
|
|
3218
|
+
function pickMarket(markets, teamCode, teamName) {
|
|
3219
|
+
const code = teamCode.toLowerCase();
|
|
3220
|
+
const name = teamName.trim().toLowerCase();
|
|
3221
|
+
const teamMarkets = markets.filter((m) => !isDrawMarket(m));
|
|
3222
|
+
const bySlug = teamMarkets.find((m) => slugToken(m) === code);
|
|
3223
|
+
if (bySlug) return bySlug;
|
|
3224
|
+
return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
|
|
3225
|
+
}
|
|
3226
|
+
function pickDraw(markets) {
|
|
3227
|
+
return markets.find(isDrawMarket);
|
|
3228
|
+
}
|
|
3229
|
+
function assertAllowedHost(base) {
|
|
3230
|
+
let host;
|
|
3231
|
+
try {
|
|
3232
|
+
host = new URL(base).host;
|
|
3233
|
+
} catch {
|
|
3234
|
+
throw new Error(`Invalid Polymarket base URL: ${base}`);
|
|
3235
|
+
}
|
|
3236
|
+
if (!ALLOWED_HOSTS.has(host)) {
|
|
3237
|
+
throw new Error(`Polymarket host not allow-listed: ${host}`);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
function yesPrice(market) {
|
|
3241
|
+
const labels = parseJsonArray(market.outcomes);
|
|
3242
|
+
const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
|
|
3243
|
+
if (labels.length === 0 || labels.length !== prices.length) return void 0;
|
|
3244
|
+
const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
|
|
3245
|
+
if (i < 0) return void 0;
|
|
3246
|
+
const p = prices[i];
|
|
3247
|
+
return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
|
|
3248
|
+
}
|
|
3249
|
+
function parseJsonArray(v) {
|
|
3250
|
+
if (Array.isArray(v)) return v.map((x) => String(x));
|
|
3251
|
+
if (typeof v === "string") {
|
|
3252
|
+
try {
|
|
3253
|
+
const parsed = JSON.parse(v);
|
|
3254
|
+
return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
|
|
3255
|
+
} catch {
|
|
3256
|
+
return [];
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
return [];
|
|
3260
|
+
}
|
|
3261
|
+
function numberish(v) {
|
|
3262
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
|
|
3263
|
+
if (typeof v === "string") {
|
|
3264
|
+
const n = Number(v);
|
|
3265
|
+
return Number.isFinite(n) ? n : void 0;
|
|
3266
|
+
}
|
|
3267
|
+
return void 0;
|
|
3268
|
+
}
|
|
3269
|
+
function resolveMarketSource(explicit) {
|
|
3270
|
+
if (explicit) return explicit;
|
|
3271
|
+
if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
|
|
3272
|
+
return process.env.CLAUDINHO_MARKETS_SOURCE;
|
|
3273
|
+
}
|
|
3274
|
+
return "polymarket";
|
|
3275
|
+
}
|
|
3276
|
+
function makeMarketProvider(source) {
|
|
3277
|
+
switch (resolveMarketSource(source)) {
|
|
3278
|
+
case "fake":
|
|
3279
|
+
return new FakeMarketProvider({ synthesize: true });
|
|
3280
|
+
case "none":
|
|
3281
|
+
case "off":
|
|
3282
|
+
return new FakeMarketProvider();
|
|
3283
|
+
// no synth → yields no signals, no network
|
|
3284
|
+
default:
|
|
3285
|
+
return new PolymarketProvider();
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
async function getMarketSignal(provider, match, options) {
|
|
3289
|
+
try {
|
|
3290
|
+
return await provider.findSignal(match, options);
|
|
3291
|
+
} catch {
|
|
3292
|
+
return void 0;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
async function getMarketSignals(provider, matches, options) {
|
|
3296
|
+
try {
|
|
3297
|
+
return await provider.findSignals(matches, options);
|
|
3298
|
+
} catch {
|
|
3299
|
+
return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
var SHARE_HASHTAG = "#VibingLaVidaLoca";
|
|
3303
|
+
var SHARE_DISCLAIMER = "Independent fan project \xB7 not affiliated with FIFA or Anthropic.";
|
|
3304
|
+
function mid(m) {
|
|
3305
|
+
return isLive(m.status) || m.status === "FT" ? scoreline(m) : "vs";
|
|
3306
|
+
}
|
|
3307
|
+
function statusTail(m) {
|
|
3308
|
+
switch (m.status) {
|
|
3309
|
+
case "LIVE":
|
|
3310
|
+
return m.minute ? `${m.minute}'` : "LIVE";
|
|
3311
|
+
case "HT":
|
|
3312
|
+
return "HT";
|
|
3313
|
+
case "FT":
|
|
3314
|
+
return "FT";
|
|
3315
|
+
case "POSTPONED":
|
|
3316
|
+
return "postponed";
|
|
3317
|
+
case "CANCELLED":
|
|
3318
|
+
return "cancelled";
|
|
3319
|
+
default:
|
|
3320
|
+
return "";
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
function compactLine(m, input, single) {
|
|
3324
|
+
const home = `${m.home.flag} ${m.home.code}`;
|
|
3325
|
+
const away = `${m.away.code} ${m.away.flag}`;
|
|
3326
|
+
const opts = { tz: input.tz, locale: input.locale };
|
|
3327
|
+
let tail;
|
|
3328
|
+
if (m.status === "SCHEDULED") {
|
|
3329
|
+
const time = formatTime(m.kickoff, opts);
|
|
3330
|
+
tail = single ? `${formatDate(m.kickoff, opts)} ${time}` : time;
|
|
3331
|
+
} else {
|
|
3332
|
+
tail = statusTail(m);
|
|
3333
|
+
}
|
|
3334
|
+
return `${home} ${mid(m)} ${away}${tail ? ` \xB7 ${tail}` : ""}`;
|
|
3335
|
+
}
|
|
3336
|
+
function socialCard(m, input) {
|
|
3337
|
+
const lines = [];
|
|
3338
|
+
const head = `${m.home.flag} ${m.home.name} ${mid(m)} ${m.away.name} ${m.away.flag}`;
|
|
3339
|
+
if (m.status === "SCHEDULED") {
|
|
3340
|
+
lines.push(head);
|
|
3341
|
+
const date = formatDate(m.kickoff, { tz: input.tz, locale: input.locale });
|
|
3342
|
+
const time = formatTime(m.kickoff, { tz: input.tz, locale: input.locale });
|
|
3343
|
+
const zone = input.tz ? ` ${input.tz}` : "";
|
|
3344
|
+
lines.push(`${date} \xB7 ${time}${zone}`);
|
|
3345
|
+
} else {
|
|
3346
|
+
const tail = statusTail(m);
|
|
3347
|
+
lines.push(tail ? `${head} \xB7 ${tail}` : head);
|
|
3348
|
+
}
|
|
3349
|
+
const loc = matchLocation(m);
|
|
3350
|
+
if (loc) lines.push(loc);
|
|
3351
|
+
return lines;
|
|
3352
|
+
}
|
|
3353
|
+
function formatShareSnippet(input, options = {}) {
|
|
3354
|
+
const style = options.style ?? "social";
|
|
3355
|
+
const includeMarkets = options.includeMarkets !== false;
|
|
3356
|
+
const includeHashtag = options.includeHashtag !== false;
|
|
3357
|
+
const includeInstall = options.includeInstallLine !== false;
|
|
3358
|
+
const signals = input.marketSignals ?? /* @__PURE__ */ new Map();
|
|
3359
|
+
const single = input.matches.length === 1;
|
|
3360
|
+
const blocks = [input.title];
|
|
3361
|
+
if (input.matches.length === 0) {
|
|
3362
|
+
if (input.emptyNote) blocks.push(input.emptyNote);
|
|
3363
|
+
} else if (style === "compact") {
|
|
3364
|
+
blocks.push(input.matches.map((m) => compactLine(m, input, single)).join("\n"));
|
|
3365
|
+
} else {
|
|
3366
|
+
for (const m of input.matches) {
|
|
3367
|
+
const card = socialCard(m, input);
|
|
3368
|
+
const sig = includeMarkets ? signals.get(m.id) : void 0;
|
|
3369
|
+
if (sig) {
|
|
3370
|
+
if (single) card.push("", ...marketBlock(sig, m));
|
|
3371
|
+
else card.push(marketLine(sig, m));
|
|
3372
|
+
}
|
|
3373
|
+
blocks.push(card.join("\n"));
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
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"));
|
|
3381
|
+
return blocks.join("\n\n");
|
|
3382
|
+
}
|
|
2855
3383
|
|
|
2856
3384
|
// src/format.ts
|
|
2857
3385
|
var STATUS_LABEL = {
|
|
@@ -2920,6 +3448,88 @@ var DISCLAIMER = "Claudinho is an independent fan project \u2014 not affiliated
|
|
|
2920
3448
|
function resolveAdapter(args) {
|
|
2921
3449
|
return args.adapter ?? makeAdapter(args.source);
|
|
2922
3450
|
}
|
|
3451
|
+
function resolveMarketProvider(args) {
|
|
3452
|
+
return args.marketProvider ?? makeMarketProvider();
|
|
3453
|
+
}
|
|
3454
|
+
function marketDisplayable(sig) {
|
|
3455
|
+
return !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
|
|
3456
|
+
}
|
|
3457
|
+
function marketHeader(m) {
|
|
3458
|
+
return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag}`;
|
|
3459
|
+
}
|
|
3460
|
+
function marketText(m, sig) {
|
|
3461
|
+
return `${marketHeader(m)}
|
|
3462
|
+
${marketBlock(sig, m).join("\n")}`;
|
|
3463
|
+
}
|
|
3464
|
+
function marketData(sig) {
|
|
3465
|
+
return {
|
|
3466
|
+
matchId: sig.matchId,
|
|
3467
|
+
source: sig.source,
|
|
3468
|
+
asOf: sig.asOf,
|
|
3469
|
+
fetchedAt: sig.fetchedAt,
|
|
3470
|
+
market: { id: sig.sourceMarketId ?? null, url: null },
|
|
3471
|
+
outcomes: sig.outcomes,
|
|
3472
|
+
favorite: sig.favorite ?? null,
|
|
3473
|
+
liquidity: sig.liquidity ?? null,
|
|
3474
|
+
stale: sig.stale,
|
|
3475
|
+
ambiguous: sig.ambiguous,
|
|
3476
|
+
informationalOnly: true
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
function marketsEnabled() {
|
|
3480
|
+
return (process.env.CLAUDINHO_MARKETS ?? "").toLowerCase() !== "off";
|
|
3481
|
+
}
|
|
3482
|
+
var marketMem = /* @__PURE__ */ new Map();
|
|
3483
|
+
var MEM_POSITIVE_TTL = 10 * 6e4;
|
|
3484
|
+
var MEM_NEGATIVE_TTL = 3 * 6e4;
|
|
3485
|
+
var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
|
|
3486
|
+
var MARKETS_TOOL_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
|
|
3487
|
+
function memKey(competition, id) {
|
|
3488
|
+
return `polymarket:${competition}:${id}`;
|
|
3489
|
+
}
|
|
3490
|
+
async function cachedMarketSignals(args, matches) {
|
|
3491
|
+
if (args.marketProvider) return (await getMarketSignals(args.marketProvider, matches)).signals;
|
|
3492
|
+
const source = resolveMarketSource();
|
|
3493
|
+
if (source !== "polymarket") {
|
|
3494
|
+
return (await getMarketSignals(makeMarketProvider(source), matches, DEFAULT_ON_MARKET_OPTS)).signals;
|
|
3495
|
+
}
|
|
3496
|
+
const competition = resolveCompetition();
|
|
3497
|
+
const now = Date.now();
|
|
3498
|
+
const result = /* @__PURE__ */ new Map();
|
|
3499
|
+
const miss = [];
|
|
3500
|
+
for (const m of matches) {
|
|
3501
|
+
const e = marketMem.get(memKey(competition, m.id));
|
|
3502
|
+
const ttl = e?.signal ? MEM_POSITIVE_TTL : MEM_NEGATIVE_TTL;
|
|
3503
|
+
if (e && now - e.at <= ttl) {
|
|
3504
|
+
if (e.signal) result.set(m.id, e.signal);
|
|
3505
|
+
} else {
|
|
3506
|
+
miss.push(m);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
if (miss.length > 0) {
|
|
3510
|
+
const { signals: fetched, checked } = await getMarketSignals(
|
|
3511
|
+
makeMarketProvider("polymarket"),
|
|
3512
|
+
miss,
|
|
3513
|
+
DEFAULT_ON_MARKET_OPTS
|
|
3514
|
+
);
|
|
3515
|
+
for (const id of checked) {
|
|
3516
|
+
marketMem.set(memKey(competition, id), { at: now, signal: fetched.get(id) ?? null });
|
|
3517
|
+
}
|
|
3518
|
+
for (const [id, s] of fetched) result.set(id, s);
|
|
3519
|
+
}
|
|
3520
|
+
return result;
|
|
3521
|
+
}
|
|
3522
|
+
async function reliableMarketData(args, matches) {
|
|
3523
|
+
if (!marketsEnabled()) return void 0;
|
|
3524
|
+
const signals = await cachedMarketSignals(args, matches);
|
|
3525
|
+
const now = /* @__PURE__ */ new Date();
|
|
3526
|
+
const out = {};
|
|
3527
|
+
for (const m of matches) {
|
|
3528
|
+
const s = signals.get(m.id);
|
|
3529
|
+
if (s && isReliableMarketSignal(s, { now })) out[m.id] = marketData(s);
|
|
3530
|
+
}
|
|
3531
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
3532
|
+
}
|
|
2923
3533
|
function fmtOpts(args) {
|
|
2924
3534
|
return {
|
|
2925
3535
|
tz: args.tz,
|
|
@@ -2927,43 +3537,55 @@ function fmtOpts(args) {
|
|
|
2927
3537
|
flavor: asFlavorLevel(args.flavor ?? process.env.CLAUDINHO_FLAVOR)
|
|
2928
3538
|
};
|
|
2929
3539
|
}
|
|
2930
|
-
function withDisclaimer(text) {
|
|
2931
|
-
|
|
3540
|
+
function withDisclaimer(text, source) {
|
|
3541
|
+
const live = source ? `
|
|
3542
|
+
Live data: ${liveSourceLabel(source)}` : "";
|
|
3543
|
+
return `${text}${live}
|
|
2932
3544
|
|
|
2933
3545
|
${DISCLAIMER}`;
|
|
2934
3546
|
}
|
|
2935
3547
|
async function toolGetToday(args) {
|
|
2936
3548
|
const adapter = resolveAdapter(args);
|
|
2937
3549
|
const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
|
|
2938
|
-
const { matches, degraded } = await getMatchesForDate(adapter, date);
|
|
3550
|
+
const { matches, degraded, source } = await getMatchesForDate(adapter, date);
|
|
2939
3551
|
const todays = fixturesByDate(date, matches, args.tz);
|
|
2940
3552
|
const opts = fmtOpts(args);
|
|
2941
3553
|
const text = `Matches on ${date}:
|
|
2942
3554
|
${matchList(todays, "No matches scheduled.", opts)}`;
|
|
3555
|
+
const marketSignals = await reliableMarketData(args, todays);
|
|
2943
3556
|
return {
|
|
2944
|
-
text: withDisclaimer(text),
|
|
2945
|
-
data: {
|
|
3557
|
+
text: withDisclaimer(text, source),
|
|
3558
|
+
data: {
|
|
3559
|
+
date,
|
|
3560
|
+
degraded,
|
|
3561
|
+
source: source ?? null,
|
|
3562
|
+
count: todays.length,
|
|
3563
|
+
matches: todays,
|
|
3564
|
+
...marketSignals ? { marketSignals } : {}
|
|
3565
|
+
}
|
|
2946
3566
|
};
|
|
2947
3567
|
}
|
|
2948
3568
|
async function toolGetLive(args = {}) {
|
|
2949
3569
|
const adapter = resolveAdapter(args);
|
|
2950
|
-
const { matches, degraded } = await getLiveMatches(adapter);
|
|
3570
|
+
const { matches, degraded, source } = await getLiveMatches(adapter);
|
|
2951
3571
|
const opts = fmtOpts(args);
|
|
2952
3572
|
const text = `Live now:
|
|
2953
3573
|
${matchList(matches, "No matches in play right now.", opts)}`;
|
|
2954
3574
|
return {
|
|
2955
|
-
text: withDisclaimer(text),
|
|
2956
|
-
data: { degraded, count: matches.length, matches }
|
|
3575
|
+
text: withDisclaimer(text, source),
|
|
3576
|
+
data: { degraded, source: source ?? null, count: matches.length, matches }
|
|
2957
3577
|
};
|
|
2958
3578
|
}
|
|
2959
3579
|
async function toolGetMatch(args) {
|
|
2960
3580
|
let match = allFixtures().find((m) => m.id === args.id);
|
|
2961
3581
|
let degraded = false;
|
|
3582
|
+
let liveSource;
|
|
2962
3583
|
if (match) {
|
|
2963
3584
|
try {
|
|
2964
3585
|
const adapter = resolveAdapter(args);
|
|
2965
3586
|
const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
|
|
2966
3587
|
match = live.find((m) => m.id === args.id) ?? match;
|
|
3588
|
+
liveSource = adapter.name;
|
|
2967
3589
|
} catch {
|
|
2968
3590
|
degraded = true;
|
|
2969
3591
|
}
|
|
@@ -2972,10 +3594,26 @@ async function toolGetMatch(args) {
|
|
|
2972
3594
|
return { text: withDisclaimer(`No match found with id ${args.id}.`), data: { match: null } };
|
|
2973
3595
|
}
|
|
2974
3596
|
const opts = fmtOpts(args);
|
|
2975
|
-
|
|
3597
|
+
let marketSignal;
|
|
3598
|
+
if (marketsEnabled()) {
|
|
3599
|
+
const s = (await cachedMarketSignals(args, [match])).get(match.id);
|
|
3600
|
+
if (s && isReliableMarketSignal(s, { now: /* @__PURE__ */ new Date() })) marketSignal = s;
|
|
3601
|
+
}
|
|
3602
|
+
const base = matchLine(match, opts);
|
|
3603
|
+
const text = marketSignal ? `${base}
|
|
3604
|
+
${marketBlock(marketSignal, match).join("\n")}` : base;
|
|
3605
|
+
return {
|
|
3606
|
+
text: withDisclaimer(text, liveSource),
|
|
3607
|
+
data: {
|
|
3608
|
+
degraded,
|
|
3609
|
+
source: liveSource ?? null,
|
|
3610
|
+
match,
|
|
3611
|
+
marketSignal: marketSignal ? marketData(marketSignal) : null
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
2976
3614
|
}
|
|
2977
3615
|
async function toolGetStandings(args) {
|
|
2978
|
-
const { matches, degraded } = await getMatchesForDate(
|
|
3616
|
+
const { matches, degraded, source } = await getMatchesForDate(
|
|
2979
3617
|
resolveAdapter(args),
|
|
2980
3618
|
localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz)
|
|
2981
3619
|
);
|
|
@@ -2984,10 +3622,8 @@ async function toolGetStandings(args) {
|
|
|
2984
3622
|
const g = args.group.toUpperCase();
|
|
2985
3623
|
if (!known.includes(g)) {
|
|
2986
3624
|
return {
|
|
2987
|
-
text: withDisclaimer(
|
|
2988
|
-
|
|
2989
|
-
),
|
|
2990
|
-
data: { degraded, tables: null }
|
|
3625
|
+
text: withDisclaimer(`No group "${g}". Groups are ${known.join(", ")}.`, source),
|
|
3626
|
+
data: { degraded, source: source ?? null, tables: null }
|
|
2991
3627
|
};
|
|
2992
3628
|
}
|
|
2993
3629
|
}
|
|
@@ -2998,8 +3634,8 @@ async function toolGetStandings(args) {
|
|
|
2998
3634
|
}));
|
|
2999
3635
|
const text = tables.map((t) => standingsTable(t.group, t.standings)).join("\n\n");
|
|
3000
3636
|
return {
|
|
3001
|
-
text: withDisclaimer(text || `No group found
|
|
3002
|
-
data: { degraded, tables: args.group ? tables[0] ?? null : tables }
|
|
3637
|
+
text: withDisclaimer(text || `No group found.`, source),
|
|
3638
|
+
data: { degraded, source: source ?? null, tables: args.group ? tables[0] ?? null : tables }
|
|
3003
3639
|
};
|
|
3004
3640
|
}
|
|
3005
3641
|
async function toolGetNextFixture(args) {
|
|
@@ -3018,14 +3654,204 @@ ${matchLine(fixture, opts)}`),
|
|
|
3018
3654
|
data: { team: code, fixture }
|
|
3019
3655
|
};
|
|
3020
3656
|
}
|
|
3657
|
+
async function toolGetMarketSignal(args) {
|
|
3658
|
+
const provider = resolveMarketProvider(args);
|
|
3659
|
+
if (args.matchId) {
|
|
3660
|
+
const match = allFixtures().find((m) => m.id === args.matchId);
|
|
3661
|
+
const sig = match ? await getMarketSignal(provider, match) : void 0;
|
|
3662
|
+
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)}.`;
|
|
3664
|
+
return {
|
|
3665
|
+
text: withDisclaimer(text2),
|
|
3666
|
+
data: {
|
|
3667
|
+
matchId: args.matchId,
|
|
3668
|
+
informationalOnly: true,
|
|
3669
|
+
signal: shown2 ? marketData(shown2) : null
|
|
3670
|
+
}
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
if (args.team) {
|
|
3674
|
+
const code = args.team.toUpperCase();
|
|
3675
|
+
const fixture = nextFixtureForTeam(code);
|
|
3676
|
+
const sig = fixture ? await getMarketSignal(provider, fixture) : void 0;
|
|
3677
|
+
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.`;
|
|
3679
|
+
return {
|
|
3680
|
+
text: withDisclaimer(text2),
|
|
3681
|
+
data: {
|
|
3682
|
+
team: code,
|
|
3683
|
+
matchId: fixture?.id ?? null,
|
|
3684
|
+
informationalOnly: true,
|
|
3685
|
+
signal: shown2 ? marketData(shown2) : null
|
|
3686
|
+
}
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
|
|
3690
|
+
const { matches } = await getMatchesForDate(resolveAdapter(args), date);
|
|
3691
|
+
const todays = fixturesByDate(date, matches, args.tz);
|
|
3692
|
+
const { signals } = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
|
|
3693
|
+
const shown = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
|
|
3694
|
+
(r) => !!r.signal && marketDisplayable(r.signal)
|
|
3695
|
+
);
|
|
3696
|
+
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}.`;
|
|
3698
|
+
return {
|
|
3699
|
+
text: withDisclaimer(text),
|
|
3700
|
+
data: {
|
|
3701
|
+
date,
|
|
3702
|
+
informationalOnly: true,
|
|
3703
|
+
signals: shown.map(({ signal }) => marketData(signal))
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
async function reliableSignalMap(args, matches) {
|
|
3708
|
+
if (!marketsEnabled()) return /* @__PURE__ */ new Map();
|
|
3709
|
+
const signals = await cachedMarketSignals(args, matches);
|
|
3710
|
+
const now = /* @__PURE__ */ new Date();
|
|
3711
|
+
const out = /* @__PURE__ */ new Map();
|
|
3712
|
+
for (const m of matches) {
|
|
3713
|
+
const s = signals.get(m.id);
|
|
3714
|
+
if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(s)) out.set(m.id, s);
|
|
3715
|
+
}
|
|
3716
|
+
return out;
|
|
3717
|
+
}
|
|
3718
|
+
function shareOptions(args) {
|
|
3719
|
+
return {
|
|
3720
|
+
style: args.style === "compact" ? "compact" : "social",
|
|
3721
|
+
includeMarkets: marketsEnabled() && args.includeMarkets !== false,
|
|
3722
|
+
includeHashtag: args.includeHashtag !== false,
|
|
3723
|
+
includeInstallLine: args.includeInstallLine !== false
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
function shareResult(kind, target, team, input, options) {
|
|
3727
|
+
const snippet = formatShareSnippet(input, options);
|
|
3728
|
+
return {
|
|
3729
|
+
// The snippet is self-contained: it carries its own non-affiliation
|
|
3730
|
+
// disclaimer (and, for any market line, the "informational only" caveat +
|
|
3731
|
+
// attribution), so it is deliberately NOT wrapped with withDisclaimer —
|
|
3732
|
+
// that would duplicate the disclaimer inside a paste-ready artifact.
|
|
3733
|
+
text: snippet,
|
|
3734
|
+
data: {
|
|
3735
|
+
kind,
|
|
3736
|
+
target,
|
|
3737
|
+
...team ? { team } : {},
|
|
3738
|
+
source: input.source ?? null,
|
|
3739
|
+
informationalOnly: true,
|
|
3740
|
+
style: options.style ?? "social",
|
|
3741
|
+
snippet,
|
|
3742
|
+
matches: input.matches,
|
|
3743
|
+
marketSignals: Object.fromEntries(
|
|
3744
|
+
[...input.marketSignals ?? /* @__PURE__ */ new Map()].map(([id, s]) => [
|
|
3745
|
+
id,
|
|
3746
|
+
marketData(s)
|
|
3747
|
+
])
|
|
3748
|
+
)
|
|
3749
|
+
}
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
async function toolGetShareSnippet(args) {
|
|
3753
|
+
const options = shareOptions(args);
|
|
3754
|
+
const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve(/* @__PURE__ */ new Map()) : reliableSignalMap(args, ms);
|
|
3755
|
+
if (args.live) {
|
|
3756
|
+
const { matches, source: source2 } = await getLiveMatches(resolveAdapter(args));
|
|
3757
|
+
return shareResult(
|
|
3758
|
+
"live",
|
|
3759
|
+
"live",
|
|
3760
|
+
void 0,
|
|
3761
|
+
{
|
|
3762
|
+
title: "Live match pulse",
|
|
3763
|
+
matches,
|
|
3764
|
+
source: source2,
|
|
3765
|
+
emptyNote: "No matches in play right now.",
|
|
3766
|
+
installLine: "npx @claudinho/cli live",
|
|
3767
|
+
tz: args.tz,
|
|
3768
|
+
locale: args.lang
|
|
3769
|
+
},
|
|
3770
|
+
{ ...options, includeMarkets: false }
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
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 {
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
const matches = match ? [match] : [];
|
|
3786
|
+
return shareResult(
|
|
3787
|
+
"match",
|
|
3788
|
+
args.matchId,
|
|
3789
|
+
void 0,
|
|
3790
|
+
{
|
|
3791
|
+
title: "Match pulse",
|
|
3792
|
+
matches,
|
|
3793
|
+
marketSignals: await signalsFor(matches),
|
|
3794
|
+
source: source2,
|
|
3795
|
+
emptyNote: `No match found with id ${args.matchId}.`,
|
|
3796
|
+
installLine: `npx @claudinho/cli match ${args.matchId}`,
|
|
3797
|
+
tz: args.tz,
|
|
3798
|
+
locale: args.lang
|
|
3799
|
+
},
|
|
3800
|
+
options
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
if (args.team) {
|
|
3804
|
+
const code = args.team.toUpperCase();
|
|
3805
|
+
const fixture = nextFixtureForTeam(code);
|
|
3806
|
+
const matches = fixture ? [fixture] : [];
|
|
3807
|
+
const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
|
|
3808
|
+
return shareResult(
|
|
3809
|
+
"next",
|
|
3810
|
+
"next",
|
|
3811
|
+
code,
|
|
3812
|
+
{
|
|
3813
|
+
title: `Next up for ${teamName}`,
|
|
3814
|
+
matches,
|
|
3815
|
+
marketSignals: await signalsFor(matches),
|
|
3816
|
+
emptyNote: `No upcoming fixture found for ${code}.`,
|
|
3817
|
+
installLine: `npx @claudinho/cli next ${code}`,
|
|
3818
|
+
tz: args.tz,
|
|
3819
|
+
locale: args.lang
|
|
3820
|
+
},
|
|
3821
|
+
options
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
|
|
3825
|
+
const { matches: all, source } = await getMatchesForDate(resolveAdapter(args), date);
|
|
3826
|
+
const todays = fixturesByDate(date, all, args.tz);
|
|
3827
|
+
const human = formatDate(`${date}T12:00:00.000Z`, { tz: args.tz, locale: args.lang });
|
|
3828
|
+
return shareResult(
|
|
3829
|
+
"today",
|
|
3830
|
+
date,
|
|
3831
|
+
void 0,
|
|
3832
|
+
{
|
|
3833
|
+
title: args.date ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`,
|
|
3834
|
+
matches: todays,
|
|
3835
|
+
marketSignals: await signalsFor(todays),
|
|
3836
|
+
source,
|
|
3837
|
+
emptyNote: `No matches scheduled for ${human}.`,
|
|
3838
|
+
installLine: "npx @claudinho/cli today",
|
|
3839
|
+
tz: args.tz,
|
|
3840
|
+
locale: args.lang
|
|
3841
|
+
},
|
|
3842
|
+
options
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3021
3845
|
|
|
3022
3846
|
// src/server.ts
|
|
3023
3847
|
var SERVER_NAME = "claudinho";
|
|
3024
|
-
var SERVER_VERSION = "0.
|
|
3848
|
+
var SERVER_VERSION = "0.4.0";
|
|
3025
3849
|
var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
|
|
3026
3850
|
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.`;
|
|
3027
3851
|
var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
|
|
3028
|
-
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
|
|
3852
|
+
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.
|
|
3854
|
+
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}
|
|
3029
3855
|
${DISCLAIMER}`;
|
|
3030
3856
|
var dateArg = z.string().refine(isValidDate, "must be a real calendar date in YYYY-MM-DD form");
|
|
3031
3857
|
var groupArg = z.string().regex(/^[A-La-l]$/, "a group letter A\u2013L");
|
|
@@ -3107,6 +3933,43 @@ function buildServer() {
|
|
|
3107
3933
|
},
|
|
3108
3934
|
async (args) => toContent(await toolGetNextFixture(args))
|
|
3109
3935
|
);
|
|
3936
|
+
server.registerTool(
|
|
3937
|
+
"get_market_signal",
|
|
3938
|
+
{
|
|
3939
|
+
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.",
|
|
3941
|
+
inputSchema: {
|
|
3942
|
+
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"),
|
|
3944
|
+
date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today) for all that day's signals"),
|
|
3945
|
+
...commonArgs
|
|
3946
|
+
},
|
|
3947
|
+
// Read-only; reaches an external prediction-market data provider.
|
|
3948
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
3949
|
+
},
|
|
3950
|
+
async (args) => toContent(await toolGetMarketSignal(args))
|
|
3951
|
+
);
|
|
3952
|
+
server.registerTool(
|
|
3953
|
+
"get_share_snippet",
|
|
3954
|
+
{
|
|
3955
|
+
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.",
|
|
3957
|
+
inputSchema: {
|
|
3958
|
+
matchId: z.string().optional().describe("Match id (most specific)"),
|
|
3959
|
+
team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),
|
|
3960
|
+
date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
|
|
3961
|
+
live: z.boolean().optional().describe("Snapshot of matches in play right now"),
|
|
3962
|
+
style: z.enum(["social", "compact"]).optional().describe("social (default, full card) or compact (one line per match)"),
|
|
3963
|
+
includeHashtag: z.boolean().optional().describe("Include the #VibingLaVidaLoca tag (default true)"),
|
|
3964
|
+
includeInstallLine: z.boolean().optional().describe('Include the "Try it: \u2026" run cue (default true)'),
|
|
3965
|
+
includeMarkets: z.boolean().optional().describe("Include the reliable market line when available (default true)"),
|
|
3966
|
+
...commonArgs
|
|
3967
|
+
},
|
|
3968
|
+
// Read-only; may reach the live/market data providers (live/today/match).
|
|
3969
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
3970
|
+
},
|
|
3971
|
+
async (args) => toContent(await toolGetShareSnippet(args))
|
|
3972
|
+
);
|
|
3110
3973
|
server.registerResource(
|
|
3111
3974
|
"standings",
|
|
3112
3975
|
new ResourceTemplate("standings://{group}", { list: void 0 }),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudinho/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Claudinho MCP server — ask your agent about the 2026 football tournament
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Claudinho MCP server — ask your agent about the 2026 men's football tournament: live scores, fixtures, standings, prediction-market odds. Not affiliated with FIFA or Anthropic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Arturo Garrido",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"tsup": "^8.0.0",
|
|
48
48
|
"typescript": "^5.7.0",
|
|
49
49
|
"vitest": "^4.1.0",
|
|
50
|
-
"@claudinho/core": "0.
|
|
50
|
+
"@claudinho/core": "0.4.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup",
|