@claudinho/mcp 0.2.0 → 0.3.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 +16 -5
- package/dist/index.js +615 -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,22 @@ 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 |
|
|
51
52
|
|
|
52
53
|
All tools are **read-only** (annotated `readOnlyHint`) and accept optional
|
|
53
54
|
`tz` (IANA timezone), `lang` (`en`/`es`/`pt`/`fr`), and `flavor`
|
|
54
55
|
(`off`/`subtle`/`full`) arguments. Each response includes both human-readable
|
|
55
56
|
text and structured JSON.
|
|
56
57
|
|
|
58
|
+
`get_today` and `get_match` also include reliable prediction-market context when a
|
|
59
|
+
market is available. Match→market event slugs are derived automatically, so no
|
|
60
|
+
mapping is needed. Market data is **read-only and informational only — not betting
|
|
61
|
+
advice** (market-implied percentages with attribution, never links or trade
|
|
62
|
+
calls), sourced from Polymarket public data and shown only when a market maps
|
|
63
|
+
cleanly to the result. Disable it with `CLAUDINHO_MARKETS=off`; set
|
|
64
|
+
`CLAUDINHO_MARKETS_SOURCE=fake` (in the server `env`) for a network-free synthetic
|
|
65
|
+
preview.
|
|
66
|
+
|
|
57
67
|
## Resources & prompts
|
|
58
68
|
|
|
59
69
|
- Resources: `standings://{group}` (e.g. `standings://A`), `fixtures://{date}` (UTC date, e.g. `fixtures://2026-06-11`)
|
|
@@ -87,9 +97,10 @@ to the `env` block of your MCP server entry.
|
|
|
87
97
|
|
|
88
98
|
## How it works
|
|
89
99
|
|
|
90
|
-
The fixture list ships bundled in the package; only live state hits the network
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
The fixture list ships bundled in the package; only live state hits the network —
|
|
101
|
+
live scores from **ESPN's** public scoreboard (a swappable provider, attributed
|
|
102
|
+
in the `source` field and text) and market odds from Polymarket. The server writes
|
|
103
|
+
nothing to stdout except the MCP protocol; diagnostics go to stderr.
|
|
93
104
|
|
|
94
105
|
## License
|
|
95
106
|
|
package/dist/index.js
CHANGED
|
@@ -2831,12 +2831,16 @@ function mergeLive(base, live) {
|
|
|
2831
2831
|
for (const m of live) byId.set(m.id, m);
|
|
2832
2832
|
return [...byId.values()];
|
|
2833
2833
|
}
|
|
2834
|
+
function liveSourceLabel(source) {
|
|
2835
|
+
const known = { espn: "ESPN" };
|
|
2836
|
+
return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
|
|
2837
|
+
}
|
|
2834
2838
|
async function getMatchesForDate(adapter, dateISO) {
|
|
2835
2839
|
const base = allFixtures();
|
|
2836
2840
|
const day = dateISO.slice(0, 10);
|
|
2837
2841
|
try {
|
|
2838
2842
|
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 };
|
|
2843
|
+
return { matches: mergeLive(base, live), degraded: false, source: adapter.name };
|
|
2840
2844
|
} catch {
|
|
2841
2845
|
return { matches: base, degraded: true };
|
|
2842
2846
|
}
|
|
@@ -2847,11 +2851,427 @@ function shiftUtcDate(dateISO, days) {
|
|
|
2847
2851
|
}
|
|
2848
2852
|
async function getLiveMatches(adapter) {
|
|
2849
2853
|
try {
|
|
2850
|
-
return { matches: await adapter.fetchLive(), degraded: false };
|
|
2854
|
+
return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
|
|
2851
2855
|
} catch {
|
|
2852
2856
|
return { matches: [], degraded: true };
|
|
2853
2857
|
}
|
|
2854
2858
|
}
|
|
2859
|
+
var DEFAULT_MAX_AGE_MS = 15 * 6e4;
|
|
2860
|
+
function normalizeOutcomes(outcomes) {
|
|
2861
|
+
const sum = outcomes.reduce(
|
|
2862
|
+
(s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
|
|
2863
|
+
0
|
|
2864
|
+
);
|
|
2865
|
+
if (sum <= 0) return outcomes.map((o) => ({ ...o, probability: 0 }));
|
|
2866
|
+
return outcomes.map((o) => ({
|
|
2867
|
+
...o,
|
|
2868
|
+
probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
|
|
2869
|
+
}));
|
|
2870
|
+
}
|
|
2871
|
+
function favoriteStrength(probability) {
|
|
2872
|
+
if (probability >= 0.65) return "clear";
|
|
2873
|
+
if (probability >= 0.52) return "slight";
|
|
2874
|
+
return "close";
|
|
2875
|
+
}
|
|
2876
|
+
function deriveFavorite(outcomes) {
|
|
2877
|
+
let top;
|
|
2878
|
+
for (const o of outcomes) {
|
|
2879
|
+
if (o.kind === "other") continue;
|
|
2880
|
+
if (!top || o.probability > top.probability) top = o;
|
|
2881
|
+
}
|
|
2882
|
+
if (!top || top.probability <= 0 || top.kind === "other") return void 0;
|
|
2883
|
+
return {
|
|
2884
|
+
kind: top.kind,
|
|
2885
|
+
teamCode: top.teamCode,
|
|
2886
|
+
probability: top.probability,
|
|
2887
|
+
strength: favoriteStrength(top.probability)
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
function mapsCleanly(match, outcomes) {
|
|
2891
|
+
if (outcomes.some((o) => o.kind === "other")) return false;
|
|
2892
|
+
const home = outcomes.find((o) => o.kind === "home");
|
|
2893
|
+
const away = outcomes.find((o) => o.kind === "away");
|
|
2894
|
+
const draw = outcomes.find((o) => o.kind === "draw");
|
|
2895
|
+
if (!home || !away) return false;
|
|
2896
|
+
if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
|
|
2897
|
+
return false;
|
|
2898
|
+
}
|
|
2899
|
+
if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
|
|
2900
|
+
return false;
|
|
2901
|
+
}
|
|
2902
|
+
if (match.stage === "GROUP" && !draw) return false;
|
|
2903
|
+
return true;
|
|
2904
|
+
}
|
|
2905
|
+
function hasSaneDistribution(outcomes) {
|
|
2906
|
+
const priced = outcomes.filter((o) => Number.isFinite(o.probability) && o.probability > 0);
|
|
2907
|
+
if (priced.length < 2) return false;
|
|
2908
|
+
const sum = priced.reduce((s, o) => s + o.probability, 0);
|
|
2909
|
+
return sum > 0.97 && sum < 1.03;
|
|
2910
|
+
}
|
|
2911
|
+
function isStaleSignal(signal, options = {}) {
|
|
2912
|
+
const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
2913
|
+
const asOf = Date.parse(signal.asOf);
|
|
2914
|
+
if (!Number.isFinite(asOf)) return true;
|
|
2915
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
2916
|
+
return now - asOf > maxAge;
|
|
2917
|
+
}
|
|
2918
|
+
function isReliableMarketSignal(signal, options = {}) {
|
|
2919
|
+
if (options.includeUnreliable) return true;
|
|
2920
|
+
if (signal.ambiguous) return false;
|
|
2921
|
+
if (!signal.favorite) return false;
|
|
2922
|
+
if (!hasSaneDistribution(signal.outcomes)) return false;
|
|
2923
|
+
if (signal.stale || isStaleSignal(signal, options)) return false;
|
|
2924
|
+
if (options.minLiquidity != null) {
|
|
2925
|
+
if (signal.liquidity == null || signal.liquidity < options.minLiquidity) return false;
|
|
2926
|
+
}
|
|
2927
|
+
return true;
|
|
2928
|
+
}
|
|
2929
|
+
function buildMarketSignal(input) {
|
|
2930
|
+
const outcomes = normalizeOutcomes(input.outcomes);
|
|
2931
|
+
const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
|
|
2932
|
+
const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
|
|
2933
|
+
const signal = {
|
|
2934
|
+
matchId: input.match.id,
|
|
2935
|
+
source: input.source,
|
|
2936
|
+
sourceMarketId: input.sourceMarketId,
|
|
2937
|
+
asOf: input.asOf,
|
|
2938
|
+
fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2939
|
+
outcomes,
|
|
2940
|
+
favorite,
|
|
2941
|
+
liquidity: input.liquidity,
|
|
2942
|
+
volume24h: input.volume24h,
|
|
2943
|
+
stale: false,
|
|
2944
|
+
ambiguous
|
|
2945
|
+
};
|
|
2946
|
+
signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
|
|
2947
|
+
return signal;
|
|
2948
|
+
}
|
|
2949
|
+
function pct(p) {
|
|
2950
|
+
return Math.round(p * 100);
|
|
2951
|
+
}
|
|
2952
|
+
function marketSourceLabel(source) {
|
|
2953
|
+
if (source === "polymarket") return "Polymarket";
|
|
2954
|
+
if (source === "fake") return "demo data";
|
|
2955
|
+
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
2956
|
+
}
|
|
2957
|
+
function outcomeLabel(o, match) {
|
|
2958
|
+
if (o.kind === "home") return match.home.name;
|
|
2959
|
+
if (o.kind === "away") return match.away.name;
|
|
2960
|
+
if (o.kind === "draw") return "Draw";
|
|
2961
|
+
return o.label;
|
|
2962
|
+
}
|
|
2963
|
+
function utcHhmm(iso) {
|
|
2964
|
+
const t = Date.parse(iso);
|
|
2965
|
+
if (!Number.isFinite(t)) return "";
|
|
2966
|
+
return `${new Date(t).toISOString().slice(11, 16)} UTC`;
|
|
2967
|
+
}
|
|
2968
|
+
function marketFavoriteText(signal, match) {
|
|
2969
|
+
const fav = signal.favorite;
|
|
2970
|
+
if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
|
|
2971
|
+
if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
|
|
2972
|
+
const name = fav.kind === "home" ? match.home.name : match.away.name;
|
|
2973
|
+
return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
|
|
2974
|
+
}
|
|
2975
|
+
function marketProbabilityText(signal, match) {
|
|
2976
|
+
const order = ["home", "draw", "away"];
|
|
2977
|
+
const parts = [];
|
|
2978
|
+
for (const kind of order) {
|
|
2979
|
+
const o = signal.outcomes.find((x) => x.kind === kind);
|
|
2980
|
+
if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
2981
|
+
}
|
|
2982
|
+
for (const o of signal.outcomes) {
|
|
2983
|
+
if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
2984
|
+
}
|
|
2985
|
+
return parts.join(" \xB7 ");
|
|
2986
|
+
}
|
|
2987
|
+
function marketAttributionText(signal) {
|
|
2988
|
+
const time = utcHhmm(signal.asOf);
|
|
2989
|
+
const src = `Source: ${marketSourceLabel(signal.source)}`;
|
|
2990
|
+
return time ? `${src} \xB7 updated ${time}` : src;
|
|
2991
|
+
}
|
|
2992
|
+
function marketBlock(signal, match) {
|
|
2993
|
+
const lines = [];
|
|
2994
|
+
if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
|
|
2995
|
+
lines.push(marketFavoriteText(signal, match));
|
|
2996
|
+
lines.push(marketProbabilityText(signal, match));
|
|
2997
|
+
lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
|
|
2998
|
+
return lines;
|
|
2999
|
+
}
|
|
3000
|
+
var FakeMarketProvider = class {
|
|
3001
|
+
constructor(opts = {}) {
|
|
3002
|
+
this.opts = opts;
|
|
3003
|
+
}
|
|
3004
|
+
opts;
|
|
3005
|
+
name = "fake";
|
|
3006
|
+
async findSignal(match, options) {
|
|
3007
|
+
const preset = this.opts.signals?.[match.id];
|
|
3008
|
+
if (preset) return preset;
|
|
3009
|
+
if (this.opts.synthesize) return this.synthesize(match, options);
|
|
3010
|
+
return void 0;
|
|
3011
|
+
}
|
|
3012
|
+
async findSignals(matches, options) {
|
|
3013
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3014
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3015
|
+
for (const m of matches) {
|
|
3016
|
+
checked.add(m.id);
|
|
3017
|
+
const s = await this.findSignal(m, options);
|
|
3018
|
+
if (s) signals.set(m.id, s);
|
|
3019
|
+
}
|
|
3020
|
+
return { signals, checked };
|
|
3021
|
+
}
|
|
3022
|
+
synthesize(match, options) {
|
|
3023
|
+
const seed = hash(`${match.home.code}-${match.away.code}`);
|
|
3024
|
+
const home = 0.3 + seed % 33 / 100;
|
|
3025
|
+
const away = 0.18 + (seed >> 3) % 23 / 100;
|
|
3026
|
+
const draw = Math.max(0.05, 1 - home - away);
|
|
3027
|
+
const outcomes = [
|
|
3028
|
+
{ kind: "home", teamCode: match.home.code, label: match.home.name, probability: home },
|
|
3029
|
+
{ kind: "draw", label: "Draw", probability: draw },
|
|
3030
|
+
{ kind: "away", teamCode: match.away.code, label: match.away.name, probability: away }
|
|
3031
|
+
];
|
|
3032
|
+
const now = this.opts.now ?? options?.now ?? /* @__PURE__ */ new Date();
|
|
3033
|
+
const asOf = new Date(now.getTime() - 6e4).toISOString();
|
|
3034
|
+
return buildMarketSignal({
|
|
3035
|
+
match,
|
|
3036
|
+
source: "fake",
|
|
3037
|
+
sourceMarketId: `fake-${match.id}`,
|
|
3038
|
+
asOf,
|
|
3039
|
+
fetchedAt: now.toISOString(),
|
|
3040
|
+
outcomes,
|
|
3041
|
+
liquidity: 5e4,
|
|
3042
|
+
now,
|
|
3043
|
+
maxAgeMs: options?.maxAgeMs
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
function hash(s) {
|
|
3048
|
+
let h = 0;
|
|
3049
|
+
for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) >>> 0;
|
|
3050
|
+
return h % 1e5;
|
|
3051
|
+
}
|
|
3052
|
+
var mapping_2026_default = {
|
|
3053
|
+
version: 1,
|
|
3054
|
+
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? }.",
|
|
3055
|
+
markets: {}
|
|
3056
|
+
};
|
|
3057
|
+
var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
|
|
3058
|
+
var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
|
|
3059
|
+
var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
|
|
3060
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
3061
|
+
var WC_SERIES_SLUG = "soccer-fifwc";
|
|
3062
|
+
var WC_SPORT = "fifwc";
|
|
3063
|
+
var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
|
|
3064
|
+
var NON_REGULAR_TIME = /extra time|penalt|to advance|to qualif|win the (group|tournament|cup|title)/i;
|
|
3065
|
+
var BUNDLED_MAPPING = mapping_2026_default.markets;
|
|
3066
|
+
var PolymarketProvider = class {
|
|
3067
|
+
constructor(opts = {}) {
|
|
3068
|
+
this.opts = opts;
|
|
3069
|
+
}
|
|
3070
|
+
opts;
|
|
3071
|
+
name = "polymarket";
|
|
3072
|
+
async findSignal(match, options) {
|
|
3073
|
+
return (await this.resolveOne(match, options)).signal;
|
|
3074
|
+
}
|
|
3075
|
+
async findSignals(matches, options) {
|
|
3076
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3077
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3078
|
+
const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
|
|
3079
|
+
for (const m of matches) {
|
|
3080
|
+
if (Date.now() >= deadline) break;
|
|
3081
|
+
const r = await this.resolveOne(m, options);
|
|
3082
|
+
if (r.checked) checked.add(m.id);
|
|
3083
|
+
if (r.signal) signals.set(m.id, r.signal);
|
|
3084
|
+
}
|
|
3085
|
+
return { signals, checked };
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
|
|
3089
|
+
* source and found no usable market, or the fixture is unmappable) from a
|
|
3090
|
+
* provider/network error — so transient failures are retried, not
|
|
3091
|
+
* negative-cached.
|
|
3092
|
+
*/
|
|
3093
|
+
async resolveOne(match, options) {
|
|
3094
|
+
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
3095
|
+
const eventSlug = entry?.eventSlug ?? deriveEventSlug(match);
|
|
3096
|
+
if (!eventSlug) return { checked: true };
|
|
3097
|
+
try {
|
|
3098
|
+
const event = await this.fetchEvent(eventSlug, options?.timeoutMs);
|
|
3099
|
+
const signal = event ? this.toSignal(match, eventSlug, event, options) : void 0;
|
|
3100
|
+
return { signal, checked: true };
|
|
3101
|
+
} catch {
|
|
3102
|
+
return { checked: false };
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
async fetchEvent(slug, timeoutMs) {
|
|
3106
|
+
const base = this.opts.baseUrl ?? DEFAULT_BASE2;
|
|
3107
|
+
assertAllowedHost(base);
|
|
3108
|
+
const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
|
|
3109
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
3110
|
+
const res = await doFetch(url, {
|
|
3111
|
+
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
3112
|
+
headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
|
|
3113
|
+
});
|
|
3114
|
+
if (res.status === 404) return void 0;
|
|
3115
|
+
if (!res.ok) {
|
|
3116
|
+
throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
|
|
3117
|
+
}
|
|
3118
|
+
const data = await res.json();
|
|
3119
|
+
const event = Array.isArray(data) ? data[0] : data;
|
|
3120
|
+
return event && typeof event === "object" ? event : void 0;
|
|
3121
|
+
}
|
|
3122
|
+
toSignal(match, eventSlug, event, options) {
|
|
3123
|
+
if (event.active === false || event.closed === true) return void 0;
|
|
3124
|
+
if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
|
|
3125
|
+
return void 0;
|
|
3126
|
+
}
|
|
3127
|
+
if (event.slug != null && event.slug !== eventSlug) return void 0;
|
|
3128
|
+
const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
|
|
3129
|
+
const kick = Date.parse(match.kickoff);
|
|
3130
|
+
if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
|
|
3131
|
+
return void 0;
|
|
3132
|
+
}
|
|
3133
|
+
const moneyline = (event.markets ?? []).filter(
|
|
3134
|
+
(m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
|
|
3135
|
+
);
|
|
3136
|
+
const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
|
|
3137
|
+
const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
|
|
3138
|
+
const drawMarket = pickDraw(moneyline);
|
|
3139
|
+
if (!homeMarket || !awayMarket) return void 0;
|
|
3140
|
+
const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
|
|
3141
|
+
if (new Set(legIds).size !== legIds.length) return void 0;
|
|
3142
|
+
const legs = [
|
|
3143
|
+
["home", homeMarket, match.home.code, match.home.name],
|
|
3144
|
+
["draw", drawMarket, void 0, "Draw"],
|
|
3145
|
+
["away", awayMarket, match.away.code, match.away.name]
|
|
3146
|
+
];
|
|
3147
|
+
const outcomes = [];
|
|
3148
|
+
let asOf = event.updatedAt;
|
|
3149
|
+
let liquidity;
|
|
3150
|
+
for (const [kind, market, teamCode, label] of legs) {
|
|
3151
|
+
if (!market) continue;
|
|
3152
|
+
if (market.closed === true || market.active === false) return void 0;
|
|
3153
|
+
if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
|
|
3154
|
+
const yes = yesPrice(market);
|
|
3155
|
+
if (yes == null) return void 0;
|
|
3156
|
+
outcomes.push({ kind, teamCode, label, probability: yes });
|
|
3157
|
+
if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
|
|
3158
|
+
const liq = numberish(market.liquidityNum ?? market.liquidity);
|
|
3159
|
+
if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
|
|
3160
|
+
}
|
|
3161
|
+
const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
|
|
3162
|
+
if (rawSum < 0.9 || rawSum > 1.15) return void 0;
|
|
3163
|
+
const signal = buildMarketSignal({
|
|
3164
|
+
match,
|
|
3165
|
+
source: "polymarket",
|
|
3166
|
+
sourceMarketId: event.id ?? eventSlug,
|
|
3167
|
+
asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3168
|
+
outcomes,
|
|
3169
|
+
liquidity,
|
|
3170
|
+
now: options?.now ?? this.opts.now,
|
|
3171
|
+
maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
|
|
3172
|
+
});
|
|
3173
|
+
return signal.ambiguous ? void 0 : signal;
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
function deriveEventSlug(match) {
|
|
3177
|
+
const home = match.home.code.toLowerCase();
|
|
3178
|
+
const away = match.away.code.toLowerCase();
|
|
3179
|
+
if (home === away || home === "tbd" || away === "tbd") return void 0;
|
|
3180
|
+
if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return void 0;
|
|
3181
|
+
const date = match.kickoff.slice(0, 10);
|
|
3182
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return void 0;
|
|
3183
|
+
return `fifwc-${home}-${away}-${date}`;
|
|
3184
|
+
}
|
|
3185
|
+
function slugToken(m) {
|
|
3186
|
+
return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
|
|
3187
|
+
}
|
|
3188
|
+
function isDrawMarket(m) {
|
|
3189
|
+
return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
|
|
3190
|
+
}
|
|
3191
|
+
function pickMarket(markets, teamCode, teamName) {
|
|
3192
|
+
const code = teamCode.toLowerCase();
|
|
3193
|
+
const name = teamName.trim().toLowerCase();
|
|
3194
|
+
const teamMarkets = markets.filter((m) => !isDrawMarket(m));
|
|
3195
|
+
const bySlug = teamMarkets.find((m) => slugToken(m) === code);
|
|
3196
|
+
if (bySlug) return bySlug;
|
|
3197
|
+
return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
|
|
3198
|
+
}
|
|
3199
|
+
function pickDraw(markets) {
|
|
3200
|
+
return markets.find(isDrawMarket);
|
|
3201
|
+
}
|
|
3202
|
+
function assertAllowedHost(base) {
|
|
3203
|
+
let host;
|
|
3204
|
+
try {
|
|
3205
|
+
host = new URL(base).host;
|
|
3206
|
+
} catch {
|
|
3207
|
+
throw new Error(`Invalid Polymarket base URL: ${base}`);
|
|
3208
|
+
}
|
|
3209
|
+
if (!ALLOWED_HOSTS.has(host)) {
|
|
3210
|
+
throw new Error(`Polymarket host not allow-listed: ${host}`);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
function yesPrice(market) {
|
|
3214
|
+
const labels = parseJsonArray(market.outcomes);
|
|
3215
|
+
const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
|
|
3216
|
+
if (labels.length === 0 || labels.length !== prices.length) return void 0;
|
|
3217
|
+
const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
|
|
3218
|
+
if (i < 0) return void 0;
|
|
3219
|
+
const p = prices[i];
|
|
3220
|
+
return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
|
|
3221
|
+
}
|
|
3222
|
+
function parseJsonArray(v) {
|
|
3223
|
+
if (Array.isArray(v)) return v.map((x) => String(x));
|
|
3224
|
+
if (typeof v === "string") {
|
|
3225
|
+
try {
|
|
3226
|
+
const parsed = JSON.parse(v);
|
|
3227
|
+
return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
|
|
3228
|
+
} catch {
|
|
3229
|
+
return [];
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
return [];
|
|
3233
|
+
}
|
|
3234
|
+
function numberish(v) {
|
|
3235
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
|
|
3236
|
+
if (typeof v === "string") {
|
|
3237
|
+
const n = Number(v);
|
|
3238
|
+
return Number.isFinite(n) ? n : void 0;
|
|
3239
|
+
}
|
|
3240
|
+
return void 0;
|
|
3241
|
+
}
|
|
3242
|
+
function resolveMarketSource(explicit) {
|
|
3243
|
+
if (explicit) return explicit;
|
|
3244
|
+
if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
|
|
3245
|
+
return process.env.CLAUDINHO_MARKETS_SOURCE;
|
|
3246
|
+
}
|
|
3247
|
+
return "polymarket";
|
|
3248
|
+
}
|
|
3249
|
+
function makeMarketProvider(source) {
|
|
3250
|
+
switch (resolveMarketSource(source)) {
|
|
3251
|
+
case "fake":
|
|
3252
|
+
return new FakeMarketProvider({ synthesize: true });
|
|
3253
|
+
case "none":
|
|
3254
|
+
case "off":
|
|
3255
|
+
return new FakeMarketProvider();
|
|
3256
|
+
// no synth → yields no signals, no network
|
|
3257
|
+
default:
|
|
3258
|
+
return new PolymarketProvider();
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
async function getMarketSignal(provider, match, options) {
|
|
3262
|
+
try {
|
|
3263
|
+
return await provider.findSignal(match, options);
|
|
3264
|
+
} catch {
|
|
3265
|
+
return void 0;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
async function getMarketSignals(provider, matches, options) {
|
|
3269
|
+
try {
|
|
3270
|
+
return await provider.findSignals(matches, options);
|
|
3271
|
+
} catch {
|
|
3272
|
+
return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
2855
3275
|
|
|
2856
3276
|
// src/format.ts
|
|
2857
3277
|
var STATUS_LABEL = {
|
|
@@ -2920,6 +3340,88 @@ var DISCLAIMER = "Claudinho is an independent fan project \u2014 not affiliated
|
|
|
2920
3340
|
function resolveAdapter(args) {
|
|
2921
3341
|
return args.adapter ?? makeAdapter(args.source);
|
|
2922
3342
|
}
|
|
3343
|
+
function resolveMarketProvider(args) {
|
|
3344
|
+
return args.marketProvider ?? makeMarketProvider();
|
|
3345
|
+
}
|
|
3346
|
+
function marketDisplayable(sig) {
|
|
3347
|
+
return !sig.ambiguous && sig.favorite != null && hasSaneDistribution(sig.outcomes);
|
|
3348
|
+
}
|
|
3349
|
+
function marketHeader(m) {
|
|
3350
|
+
return `${m.home.flag} ${m.home.name} vs ${m.away.name} ${m.away.flag}`;
|
|
3351
|
+
}
|
|
3352
|
+
function marketText(m, sig) {
|
|
3353
|
+
return `${marketHeader(m)}
|
|
3354
|
+
${marketBlock(sig, m).join("\n")}`;
|
|
3355
|
+
}
|
|
3356
|
+
function marketData(sig) {
|
|
3357
|
+
return {
|
|
3358
|
+
matchId: sig.matchId,
|
|
3359
|
+
source: sig.source,
|
|
3360
|
+
asOf: sig.asOf,
|
|
3361
|
+
fetchedAt: sig.fetchedAt,
|
|
3362
|
+
market: { id: sig.sourceMarketId ?? null, url: null },
|
|
3363
|
+
outcomes: sig.outcomes,
|
|
3364
|
+
favorite: sig.favorite ?? null,
|
|
3365
|
+
liquidity: sig.liquidity ?? null,
|
|
3366
|
+
stale: sig.stale,
|
|
3367
|
+
ambiguous: sig.ambiguous,
|
|
3368
|
+
informationalOnly: true
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
function marketsEnabled() {
|
|
3372
|
+
return (process.env.CLAUDINHO_MARKETS ?? "").toLowerCase() !== "off";
|
|
3373
|
+
}
|
|
3374
|
+
var marketMem = /* @__PURE__ */ new Map();
|
|
3375
|
+
var MEM_POSITIVE_TTL = 10 * 6e4;
|
|
3376
|
+
var MEM_NEGATIVE_TTL = 3 * 6e4;
|
|
3377
|
+
var DEFAULT_ON_MARKET_OPTS = { deadlineMs: 2e3, timeoutMs: 2500 };
|
|
3378
|
+
var MARKETS_TOOL_OPTS = { deadlineMs: 12e3, timeoutMs: 6e3 };
|
|
3379
|
+
function memKey(competition, id) {
|
|
3380
|
+
return `polymarket:${competition}:${id}`;
|
|
3381
|
+
}
|
|
3382
|
+
async function cachedMarketSignals(args, matches) {
|
|
3383
|
+
if (args.marketProvider) return (await getMarketSignals(args.marketProvider, matches)).signals;
|
|
3384
|
+
const source = resolveMarketSource();
|
|
3385
|
+
if (source !== "polymarket") {
|
|
3386
|
+
return (await getMarketSignals(makeMarketProvider(source), matches, DEFAULT_ON_MARKET_OPTS)).signals;
|
|
3387
|
+
}
|
|
3388
|
+
const competition = resolveCompetition();
|
|
3389
|
+
const now = Date.now();
|
|
3390
|
+
const result = /* @__PURE__ */ new Map();
|
|
3391
|
+
const miss = [];
|
|
3392
|
+
for (const m of matches) {
|
|
3393
|
+
const e = marketMem.get(memKey(competition, m.id));
|
|
3394
|
+
const ttl = e?.signal ? MEM_POSITIVE_TTL : MEM_NEGATIVE_TTL;
|
|
3395
|
+
if (e && now - e.at <= ttl) {
|
|
3396
|
+
if (e.signal) result.set(m.id, e.signal);
|
|
3397
|
+
} else {
|
|
3398
|
+
miss.push(m);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
if (miss.length > 0) {
|
|
3402
|
+
const { signals: fetched, checked } = await getMarketSignals(
|
|
3403
|
+
makeMarketProvider("polymarket"),
|
|
3404
|
+
miss,
|
|
3405
|
+
DEFAULT_ON_MARKET_OPTS
|
|
3406
|
+
);
|
|
3407
|
+
for (const id of checked) {
|
|
3408
|
+
marketMem.set(memKey(competition, id), { at: now, signal: fetched.get(id) ?? null });
|
|
3409
|
+
}
|
|
3410
|
+
for (const [id, s] of fetched) result.set(id, s);
|
|
3411
|
+
}
|
|
3412
|
+
return result;
|
|
3413
|
+
}
|
|
3414
|
+
async function reliableMarketData(args, matches) {
|
|
3415
|
+
if (!marketsEnabled()) return void 0;
|
|
3416
|
+
const signals = await cachedMarketSignals(args, matches);
|
|
3417
|
+
const now = /* @__PURE__ */ new Date();
|
|
3418
|
+
const out = {};
|
|
3419
|
+
for (const m of matches) {
|
|
3420
|
+
const s = signals.get(m.id);
|
|
3421
|
+
if (s && isReliableMarketSignal(s, { now })) out[m.id] = marketData(s);
|
|
3422
|
+
}
|
|
3423
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
3424
|
+
}
|
|
2923
3425
|
function fmtOpts(args) {
|
|
2924
3426
|
return {
|
|
2925
3427
|
tz: args.tz,
|
|
@@ -2927,43 +3429,55 @@ function fmtOpts(args) {
|
|
|
2927
3429
|
flavor: asFlavorLevel(args.flavor ?? process.env.CLAUDINHO_FLAVOR)
|
|
2928
3430
|
};
|
|
2929
3431
|
}
|
|
2930
|
-
function withDisclaimer(text) {
|
|
2931
|
-
|
|
3432
|
+
function withDisclaimer(text, source) {
|
|
3433
|
+
const live = source ? `
|
|
3434
|
+
Live data: ${liveSourceLabel(source)}` : "";
|
|
3435
|
+
return `${text}${live}
|
|
2932
3436
|
|
|
2933
3437
|
${DISCLAIMER}`;
|
|
2934
3438
|
}
|
|
2935
3439
|
async function toolGetToday(args) {
|
|
2936
3440
|
const adapter = resolveAdapter(args);
|
|
2937
3441
|
const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
|
|
2938
|
-
const { matches, degraded } = await getMatchesForDate(adapter, date);
|
|
3442
|
+
const { matches, degraded, source } = await getMatchesForDate(adapter, date);
|
|
2939
3443
|
const todays = fixturesByDate(date, matches, args.tz);
|
|
2940
3444
|
const opts = fmtOpts(args);
|
|
2941
3445
|
const text = `Matches on ${date}:
|
|
2942
3446
|
${matchList(todays, "No matches scheduled.", opts)}`;
|
|
3447
|
+
const marketSignals = await reliableMarketData(args, todays);
|
|
2943
3448
|
return {
|
|
2944
|
-
text: withDisclaimer(text),
|
|
2945
|
-
data: {
|
|
3449
|
+
text: withDisclaimer(text, source),
|
|
3450
|
+
data: {
|
|
3451
|
+
date,
|
|
3452
|
+
degraded,
|
|
3453
|
+
source: source ?? null,
|
|
3454
|
+
count: todays.length,
|
|
3455
|
+
matches: todays,
|
|
3456
|
+
...marketSignals ? { marketSignals } : {}
|
|
3457
|
+
}
|
|
2946
3458
|
};
|
|
2947
3459
|
}
|
|
2948
3460
|
async function toolGetLive(args = {}) {
|
|
2949
3461
|
const adapter = resolveAdapter(args);
|
|
2950
|
-
const { matches, degraded } = await getLiveMatches(adapter);
|
|
3462
|
+
const { matches, degraded, source } = await getLiveMatches(adapter);
|
|
2951
3463
|
const opts = fmtOpts(args);
|
|
2952
3464
|
const text = `Live now:
|
|
2953
3465
|
${matchList(matches, "No matches in play right now.", opts)}`;
|
|
2954
3466
|
return {
|
|
2955
|
-
text: withDisclaimer(text),
|
|
2956
|
-
data: { degraded, count: matches.length, matches }
|
|
3467
|
+
text: withDisclaimer(text, source),
|
|
3468
|
+
data: { degraded, source: source ?? null, count: matches.length, matches }
|
|
2957
3469
|
};
|
|
2958
3470
|
}
|
|
2959
3471
|
async function toolGetMatch(args) {
|
|
2960
3472
|
let match = allFixtures().find((m) => m.id === args.id);
|
|
2961
3473
|
let degraded = false;
|
|
3474
|
+
let liveSource;
|
|
2962
3475
|
if (match) {
|
|
2963
3476
|
try {
|
|
2964
3477
|
const adapter = resolveAdapter(args);
|
|
2965
3478
|
const live = await adapter.fetchByDate(match.kickoff.slice(0, 10));
|
|
2966
3479
|
match = live.find((m) => m.id === args.id) ?? match;
|
|
3480
|
+
liveSource = adapter.name;
|
|
2967
3481
|
} catch {
|
|
2968
3482
|
degraded = true;
|
|
2969
3483
|
}
|
|
@@ -2972,10 +3486,26 @@ async function toolGetMatch(args) {
|
|
|
2972
3486
|
return { text: withDisclaimer(`No match found with id ${args.id}.`), data: { match: null } };
|
|
2973
3487
|
}
|
|
2974
3488
|
const opts = fmtOpts(args);
|
|
2975
|
-
|
|
3489
|
+
let marketSignal;
|
|
3490
|
+
if (marketsEnabled()) {
|
|
3491
|
+
const s = (await cachedMarketSignals(args, [match])).get(match.id);
|
|
3492
|
+
if (s && isReliableMarketSignal(s, { now: /* @__PURE__ */ new Date() })) marketSignal = s;
|
|
3493
|
+
}
|
|
3494
|
+
const base = matchLine(match, opts);
|
|
3495
|
+
const text = marketSignal ? `${base}
|
|
3496
|
+
${marketBlock(marketSignal, match).join("\n")}` : base;
|
|
3497
|
+
return {
|
|
3498
|
+
text: withDisclaimer(text, liveSource),
|
|
3499
|
+
data: {
|
|
3500
|
+
degraded,
|
|
3501
|
+
source: liveSource ?? null,
|
|
3502
|
+
match,
|
|
3503
|
+
marketSignal: marketSignal ? marketData(marketSignal) : null
|
|
3504
|
+
}
|
|
3505
|
+
};
|
|
2976
3506
|
}
|
|
2977
3507
|
async function toolGetStandings(args) {
|
|
2978
|
-
const { matches, degraded } = await getMatchesForDate(
|
|
3508
|
+
const { matches, degraded, source } = await getMatchesForDate(
|
|
2979
3509
|
resolveAdapter(args),
|
|
2980
3510
|
localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz)
|
|
2981
3511
|
);
|
|
@@ -2984,10 +3514,8 @@ async function toolGetStandings(args) {
|
|
|
2984
3514
|
const g = args.group.toUpperCase();
|
|
2985
3515
|
if (!known.includes(g)) {
|
|
2986
3516
|
return {
|
|
2987
|
-
text: withDisclaimer(
|
|
2988
|
-
|
|
2989
|
-
),
|
|
2990
|
-
data: { degraded, tables: null }
|
|
3517
|
+
text: withDisclaimer(`No group "${g}". Groups are ${known.join(", ")}.`, source),
|
|
3518
|
+
data: { degraded, source: source ?? null, tables: null }
|
|
2991
3519
|
};
|
|
2992
3520
|
}
|
|
2993
3521
|
}
|
|
@@ -2998,8 +3526,8 @@ async function toolGetStandings(args) {
|
|
|
2998
3526
|
}));
|
|
2999
3527
|
const text = tables.map((t) => standingsTable(t.group, t.standings)).join("\n\n");
|
|
3000
3528
|
return {
|
|
3001
|
-
text: withDisclaimer(text || `No group found
|
|
3002
|
-
data: { degraded, tables: args.group ? tables[0] ?? null : tables }
|
|
3529
|
+
text: withDisclaimer(text || `No group found.`, source),
|
|
3530
|
+
data: { degraded, source: source ?? null, tables: args.group ? tables[0] ?? null : tables }
|
|
3003
3531
|
};
|
|
3004
3532
|
}
|
|
3005
3533
|
async function toolGetNextFixture(args) {
|
|
@@ -3018,14 +3546,65 @@ ${matchLine(fixture, opts)}`),
|
|
|
3018
3546
|
data: { team: code, fixture }
|
|
3019
3547
|
};
|
|
3020
3548
|
}
|
|
3549
|
+
async function toolGetMarketSignal(args) {
|
|
3550
|
+
const provider = resolveMarketProvider(args);
|
|
3551
|
+
if (args.matchId) {
|
|
3552
|
+
const match = allFixtures().find((m) => m.id === args.matchId);
|
|
3553
|
+
const sig = match ? await getMarketSignal(provider, match) : void 0;
|
|
3554
|
+
const shown2 = match && sig && marketDisplayable(sig) ? sig : void 0;
|
|
3555
|
+
const text2 = !match ? `No match found with id ${args.matchId}.` : shown2 ? marketText(match, shown2) : `No reliable market signal for ${marketHeader(match)}.`;
|
|
3556
|
+
return {
|
|
3557
|
+
text: withDisclaimer(text2),
|
|
3558
|
+
data: {
|
|
3559
|
+
matchId: args.matchId,
|
|
3560
|
+
informationalOnly: true,
|
|
3561
|
+
signal: shown2 ? marketData(shown2) : null
|
|
3562
|
+
}
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
if (args.team) {
|
|
3566
|
+
const code = args.team.toUpperCase();
|
|
3567
|
+
const fixture = nextFixtureForTeam(code);
|
|
3568
|
+
const sig = fixture ? await getMarketSignal(provider, fixture) : void 0;
|
|
3569
|
+
const shown2 = fixture && sig && marketDisplayable(sig) ? sig : void 0;
|
|
3570
|
+
const text2 = !fixture ? `No upcoming fixture found for ${code}.` : shown2 ? marketText(fixture, shown2) : `No reliable market signal for ${code}'s next fixture.`;
|
|
3571
|
+
return {
|
|
3572
|
+
text: withDisclaimer(text2),
|
|
3573
|
+
data: {
|
|
3574
|
+
team: code,
|
|
3575
|
+
matchId: fixture?.id ?? null,
|
|
3576
|
+
informationalOnly: true,
|
|
3577
|
+
signal: shown2 ? marketData(shown2) : null
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
|
|
3582
|
+
const { matches } = await getMatchesForDate(resolveAdapter(args), date);
|
|
3583
|
+
const todays = fixturesByDate(date, matches, args.tz);
|
|
3584
|
+
const { signals } = await getMarketSignals(provider, todays, MARKETS_TOOL_OPTS);
|
|
3585
|
+
const shown = todays.map((m) => ({ match: m, signal: signals.get(m.id) })).filter(
|
|
3586
|
+
(r) => !!r.signal && marketDisplayable(r.signal)
|
|
3587
|
+
);
|
|
3588
|
+
const text = shown.length ? `Market signals on ${date}:
|
|
3589
|
+
${shown.map(({ match, signal }) => marketText(match, signal)).join("\n\n")}` : `No reliable market signals on ${date}.`;
|
|
3590
|
+
return {
|
|
3591
|
+
text: withDisclaimer(text),
|
|
3592
|
+
data: {
|
|
3593
|
+
date,
|
|
3594
|
+
informationalOnly: true,
|
|
3595
|
+
signals: shown.map(({ signal }) => marketData(signal))
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3021
3599
|
|
|
3022
3600
|
// src/server.ts
|
|
3023
3601
|
var SERVER_NAME = "claudinho";
|
|
3024
|
-
var SERVER_VERSION = "0.
|
|
3602
|
+
var SERVER_VERSION = "0.3.0";
|
|
3025
3603
|
var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
|
|
3026
3604
|
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
3605
|
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
|
|
3606
|
+
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.
|
|
3607
|
+
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.${VOICE}
|
|
3029
3608
|
${DISCLAIMER}`;
|
|
3030
3609
|
var dateArg = z.string().refine(isValidDate, "must be a real calendar date in YYYY-MM-DD form");
|
|
3031
3610
|
var groupArg = z.string().regex(/^[A-La-l]$/, "a group letter A\u2013L");
|
|
@@ -3107,6 +3686,22 @@ function buildServer() {
|
|
|
3107
3686
|
},
|
|
3108
3687
|
async (args) => toContent(await toolGetNextFixture(args))
|
|
3109
3688
|
);
|
|
3689
|
+
server.registerTool(
|
|
3690
|
+
"get_market_signal",
|
|
3691
|
+
{
|
|
3692
|
+
title: "Prediction-market signal",
|
|
3693
|
+
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.",
|
|
3694
|
+
inputSchema: {
|
|
3695
|
+
matchId: z.string().optional().describe("Match id (most specific)"),
|
|
3696
|
+
team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),
|
|
3697
|
+
date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today) for all that day's signals"),
|
|
3698
|
+
...commonArgs
|
|
3699
|
+
},
|
|
3700
|
+
// Read-only; reaches an external prediction-market data provider.
|
|
3701
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
3702
|
+
},
|
|
3703
|
+
async (args) => toContent(await toolGetMarketSignal(args))
|
|
3704
|
+
);
|
|
3110
3705
|
server.registerResource(
|
|
3111
3706
|
"standings",
|
|
3112
3707
|
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.3.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.3.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup",
|