@claudinho/mcp 0.8.18 → 0.9.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/dist/index.js +214 -50
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -385,7 +385,8 @@ function normalizeLang(lang) {
|
|
|
385
385
|
}
|
|
386
386
|
function t(lang, key, vars) {
|
|
387
387
|
const dict = CATALOGS[normalizeLang(lang)];
|
|
388
|
-
|
|
388
|
+
const en = EN;
|
|
389
|
+
let s = dict[key] ?? en[key] ?? key;
|
|
389
390
|
if (vars) {
|
|
390
391
|
for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
|
|
391
392
|
}
|
|
@@ -504,6 +505,33 @@ function shiftUtcDate(dateISO, days) {
|
|
|
504
505
|
const [y, m, d] = dateISO.slice(0, 10).split("-").map(Number);
|
|
505
506
|
return new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, (d ?? 1) + days)).toISOString().slice(0, 10);
|
|
506
507
|
}
|
|
508
|
+
var FEED_TEXT_MAX = 100;
|
|
509
|
+
function sanitizeFeedText(value, max = FEED_TEXT_MAX) {
|
|
510
|
+
let out = "";
|
|
511
|
+
let count = 0;
|
|
512
|
+
for (const ch of String(value)) {
|
|
513
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
514
|
+
const isWhitespaceControl = cp === 9 || cp === 10 || cp === 13;
|
|
515
|
+
if ((cp <= 31 || cp >= 127 && cp <= 159) && !isWhitespaceControl) continue;
|
|
516
|
+
if (count >= max) break;
|
|
517
|
+
out += isWhitespaceControl ? " " : ch;
|
|
518
|
+
count++;
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
var segmenter = new Intl.Segmenter();
|
|
523
|
+
var WIDE_CLUSTER = new RegExp("^(?:\\p{Regional_Indicator}|\\p{Extended_Pictographic})", "u");
|
|
524
|
+
function displayWidth(s) {
|
|
525
|
+
let w = 0;
|
|
526
|
+
for (const { segment } of segmenter.segment(s)) {
|
|
527
|
+
w += WIDE_CLUSTER.test(segment) ? 2 : 1;
|
|
528
|
+
}
|
|
529
|
+
return w;
|
|
530
|
+
}
|
|
531
|
+
function padVisible(s, width) {
|
|
532
|
+
const w = displayWidth(s);
|
|
533
|
+
return w >= width ? s : s + " ".repeat(width - w);
|
|
534
|
+
}
|
|
507
535
|
function outcomeFromScore(home, away) {
|
|
508
536
|
if (home > away) return "H";
|
|
509
537
|
if (home < away) return "A";
|
|
@@ -2880,10 +2908,27 @@ function rosterAtZero(matches) {
|
|
|
2880
2908
|
var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
|
|
2881
2909
|
var DEFAULT_COMPETITION = "fifa.world";
|
|
2882
2910
|
var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
|
|
2883
|
-
var USER_AGENT = "
|
|
2911
|
+
var USER_AGENT = `claudinho/${"0.9.0"} (+https://github.com/arturogarrido/claudinho)`;
|
|
2912
|
+
var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
2884
2913
|
function competitionBase(slug) {
|
|
2885
2914
|
return `${ESPN_SOCCER}/${slug}`;
|
|
2886
2915
|
}
|
|
2916
|
+
var DEFAULT_TIMEOUT_MS = 6e3;
|
|
2917
|
+
var STANDINGS_SHARE_MS = 3e4;
|
|
2918
|
+
var ProviderError = class extends Error {
|
|
2919
|
+
kind;
|
|
2920
|
+
status;
|
|
2921
|
+
constructor(message, kind, status) {
|
|
2922
|
+
super(message);
|
|
2923
|
+
this.name = "ProviderError";
|
|
2924
|
+
this.kind = kind;
|
|
2925
|
+
this.status = status;
|
|
2926
|
+
}
|
|
2927
|
+
/** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
|
|
2928
|
+
get throttled() {
|
|
2929
|
+
return this.kind === "http" && (this.status === 429 || this.status === 403);
|
|
2930
|
+
}
|
|
2931
|
+
};
|
|
2887
2932
|
function mapStatus(st) {
|
|
2888
2933
|
const name = (st?.type?.name ?? "").toUpperCase();
|
|
2889
2934
|
const state = st?.type?.state ?? "";
|
|
@@ -2924,9 +2969,15 @@ function toInt(s) {
|
|
|
2924
2969
|
return Number.isFinite(n) ? n : void 0;
|
|
2925
2970
|
}
|
|
2926
2971
|
function toTeam(t2) {
|
|
2927
|
-
const name =
|
|
2928
|
-
|
|
2929
|
-
|
|
2972
|
+
const name = sanitizeFeedText(
|
|
2973
|
+
t2?.displayName ?? t2?.name ?? t2?.location ?? t2?.shortDisplayName ?? "TBD"
|
|
2974
|
+
);
|
|
2975
|
+
const code = sanitizeFeedText(t2?.abbreviation ?? name.slice(0, 3)).toUpperCase();
|
|
2976
|
+
return {
|
|
2977
|
+
code,
|
|
2978
|
+
name,
|
|
2979
|
+
flag: nationToFlag(sanitizeFeedText(t2?.displayName ?? t2?.abbreviation ?? name))
|
|
2980
|
+
};
|
|
2930
2981
|
}
|
|
2931
2982
|
function mapEspnEvent(ev, ctx = {}) {
|
|
2932
2983
|
const comp = ev.competitions?.[0];
|
|
@@ -2957,9 +3008,9 @@ function mapEspnEvent(ev, ctx = {}) {
|
|
|
2957
3008
|
stage,
|
|
2958
3009
|
group,
|
|
2959
3010
|
kickoff: ev.date,
|
|
2960
|
-
venue: comp?.venue?.fullName ?? "",
|
|
2961
|
-
city: comp?.venue?.address?.city || void 0,
|
|
2962
|
-
country: comp?.venue?.address?.country || void 0,
|
|
3011
|
+
venue: sanitizeFeedText(comp?.venue?.fullName ?? ""),
|
|
3012
|
+
city: sanitizeFeedText(comp?.venue?.address?.city ?? "") || void 0,
|
|
3013
|
+
country: sanitizeFeedText(comp?.venue?.address?.country ?? "") || void 0,
|
|
2963
3014
|
home,
|
|
2964
3015
|
away,
|
|
2965
3016
|
score: hasScore ? { home: hs, away: as } : void 0,
|
|
@@ -3019,6 +3070,20 @@ var EspnAdapter = class {
|
|
|
3019
3070
|
capabilities = { push: false, latencyHintSec: 45 };
|
|
3020
3071
|
/** Cached team-code -> group-letter map (built lazily from standings). */
|
|
3021
3072
|
groupMap;
|
|
3073
|
+
/**
|
|
3074
|
+
* One in-flight/recent standings fetch shared by fetchStandings and
|
|
3075
|
+
* fetchGroupMap, so a command like `bracket` hits the endpoint once instead
|
|
3076
|
+
* of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
|
|
3077
|
+
* fetch clears the slot — a transient failure is never cached.
|
|
3078
|
+
*/
|
|
3079
|
+
standingsShared;
|
|
3080
|
+
/**
|
|
3081
|
+
* The most recent request failure (best-effort under concurrency; cleared
|
|
3082
|
+
* when a new request starts). A post-hoc hint for callers whose result path
|
|
3083
|
+
* fails closed to `degraded` booleans but who still need to distinguish a
|
|
3084
|
+
* throttle (persist a backoff) from an ordinary blip.
|
|
3085
|
+
*/
|
|
3086
|
+
lastError;
|
|
3022
3087
|
async fetchByDate(dateISO) {
|
|
3023
3088
|
return this.fetchScoreboard(toEspnDate(dateISO));
|
|
3024
3089
|
}
|
|
@@ -3040,37 +3105,58 @@ var EspnAdapter = class {
|
|
|
3040
3105
|
const base = this.opts.baseUrl ?? DEFAULT_BASE;
|
|
3041
3106
|
return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
|
|
3042
3107
|
}
|
|
3108
|
+
/** The shared standings fetch (see {@link standingsShared}). */
|
|
3109
|
+
sharedStandings() {
|
|
3110
|
+
const now = Date.now();
|
|
3111
|
+
if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
|
|
3112
|
+
return this.standingsShared.promise;
|
|
3113
|
+
}
|
|
3114
|
+
const promise = this.get(this.standingsUrl()).then(
|
|
3115
|
+
(d) => parseStandings(d)
|
|
3116
|
+
);
|
|
3117
|
+
this.standingsShared = { at: now, promise };
|
|
3118
|
+
promise.catch(() => {
|
|
3119
|
+
if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
|
|
3120
|
+
});
|
|
3121
|
+
return promise;
|
|
3122
|
+
}
|
|
3043
3123
|
/**
|
|
3044
3124
|
* Authoritative, cumulative group tables from the standings endpoint. Throws
|
|
3045
3125
|
* on fetch/parse failure (the caller decides the fallback). Group-stage only:
|
|
3046
3126
|
* non-group `children` are filtered out by {@link parseStandings}.
|
|
3047
3127
|
*/
|
|
3048
3128
|
async fetchStandings() {
|
|
3049
|
-
return
|
|
3129
|
+
return this.sharedStandings();
|
|
3050
3130
|
}
|
|
3051
3131
|
/**
|
|
3052
3132
|
* Build (and cache) a team-code -> group-letter map from the standings
|
|
3053
|
-
* endpoint. Best-effort: returns {} if standings are unavailable
|
|
3054
|
-
*
|
|
3133
|
+
* endpoint. Best-effort: returns {} if standings are unavailable — but a
|
|
3134
|
+
* transient failure is NOT cached (only a successful parse pins the map), so
|
|
3135
|
+
* one blip can't silently drop group letters for the adapter's lifetime.
|
|
3136
|
+
* Reuses the same parse/fetch as {@link fetchStandings}, so the two never
|
|
3137
|
+
* drift and one command never fetches standings twice.
|
|
3055
3138
|
*/
|
|
3056
3139
|
async fetchGroupMap(force = false) {
|
|
3057
3140
|
if (this.groupMap && !force) return this.groupMap;
|
|
3058
|
-
const map = {};
|
|
3059
3141
|
try {
|
|
3060
|
-
const tables =
|
|
3142
|
+
const tables = await this.sharedStandings();
|
|
3143
|
+
const map = {};
|
|
3061
3144
|
for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
|
|
3145
|
+
this.groupMap = map;
|
|
3146
|
+
return map;
|
|
3062
3147
|
} catch {
|
|
3148
|
+
return {};
|
|
3063
3149
|
}
|
|
3064
|
-
this.groupMap = map;
|
|
3065
|
-
return map;
|
|
3066
3150
|
}
|
|
3067
3151
|
async fetchScoreboard(dates) {
|
|
3068
3152
|
const base = this.opts.baseUrl ?? DEFAULT_BASE;
|
|
3069
3153
|
const url = new URL(`${base}/scoreboard`);
|
|
3070
3154
|
url.searchParams.set("limit", "300");
|
|
3071
3155
|
if (dates) url.searchParams.set("dates", dates);
|
|
3072
|
-
const groupByTeam =
|
|
3073
|
-
|
|
3156
|
+
const [groupByTeam, data] = await Promise.all([
|
|
3157
|
+
this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
|
|
3158
|
+
this.get(url.toString())
|
|
3159
|
+
]);
|
|
3074
3160
|
return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
|
|
3075
3161
|
}
|
|
3076
3162
|
async get(url) {
|
|
@@ -3078,17 +3164,51 @@ var EspnAdapter = class {
|
|
|
3078
3164
|
const controller = new AbortController();
|
|
3079
3165
|
const timer = setTimeout(
|
|
3080
3166
|
() => controller.abort(),
|
|
3081
|
-
this.opts.timeoutMs ??
|
|
3167
|
+
this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
3082
3168
|
);
|
|
3169
|
+
this.lastError = void 0;
|
|
3083
3170
|
try {
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3171
|
+
let res;
|
|
3172
|
+
try {
|
|
3173
|
+
res = await doFetch(url, {
|
|
3174
|
+
signal: controller.signal,
|
|
3175
|
+
// ESPN never legitimately redirects; following one would sidestep the
|
|
3176
|
+
// fixed-host guarantee, so treat any redirect as a failure (degraded).
|
|
3177
|
+
redirect: "error",
|
|
3178
|
+
headers: { Accept: "application/json", "User-Agent": USER_AGENT }
|
|
3179
|
+
});
|
|
3180
|
+
} catch (e) {
|
|
3181
|
+
throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
|
|
3182
|
+
}
|
|
3088
3183
|
if (!res.ok) {
|
|
3089
|
-
throw new
|
|
3184
|
+
throw new ProviderError(
|
|
3185
|
+
`ESPN request failed: ${res.status} ${res.statusText}`,
|
|
3186
|
+
"http",
|
|
3187
|
+
res.status
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
const length = Number(res.headers?.get?.("content-length"));
|
|
3191
|
+
if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
|
|
3192
|
+
throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
|
|
3090
3193
|
}
|
|
3091
|
-
|
|
3194
|
+
try {
|
|
3195
|
+
return await res.json();
|
|
3196
|
+
} catch (e) {
|
|
3197
|
+
throw new ProviderError(
|
|
3198
|
+
`ESPN response unparseable: ${e?.message ?? e}`,
|
|
3199
|
+
"parse"
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
} catch (e) {
|
|
3203
|
+
const pe = e instanceof ProviderError ? e : new ProviderError(String(e?.message ?? e), "http");
|
|
3204
|
+
this.lastError = pe;
|
|
3205
|
+
if (typeof process !== "undefined" && process.env?.CLAUDINHO_DEBUG) {
|
|
3206
|
+
process.stderr.write(
|
|
3207
|
+
`claudinho: espn ${pe.kind}${pe.status ? ` ${pe.status}` : ""}: ${url}
|
|
3208
|
+
`
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
throw pe;
|
|
3092
3212
|
} finally {
|
|
3093
3213
|
clearTimeout(timer);
|
|
3094
3214
|
}
|
|
@@ -3812,13 +3932,18 @@ function resolveCompetition(explicit) {
|
|
|
3812
3932
|
}
|
|
3813
3933
|
return DEFAULT_COMPETITION;
|
|
3814
3934
|
}
|
|
3935
|
+
var KNOWN_SOURCES = ["espn"];
|
|
3815
3936
|
function makeAdapter(source = "espn") {
|
|
3816
3937
|
switch (source) {
|
|
3817
|
-
|
|
3938
|
+
case "espn": {
|
|
3818
3939
|
const competition = resolveCompetition();
|
|
3819
3940
|
const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
|
|
3820
3941
|
return new EspnAdapter({ baseUrl });
|
|
3821
3942
|
}
|
|
3943
|
+
default:
|
|
3944
|
+
throw new Error(
|
|
3945
|
+
`Unknown data source "${source}" (available: ${KNOWN_SOURCES.join(", ")})`
|
|
3946
|
+
);
|
|
3822
3947
|
}
|
|
3823
3948
|
}
|
|
3824
3949
|
function mergeLive(base, live) {
|
|
@@ -3856,8 +3981,16 @@ async function getStandings(adapter, group) {
|
|
|
3856
3981
|
const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
|
|
3857
3982
|
return { tables, degraded: true };
|
|
3858
3983
|
}
|
|
3859
|
-
var
|
|
3860
|
-
|
|
3984
|
+
var knockoutWindowMemo;
|
|
3985
|
+
function knockoutWindow() {
|
|
3986
|
+
if (knockoutWindowMemo !== void 0) return knockoutWindowMemo;
|
|
3987
|
+
const days = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY").map((m) => m.kickoff.slice(0, 10).replace(/-/g, "")).filter((d) => d.length === 8);
|
|
3988
|
+
knockoutWindowMemo = days.length ? {
|
|
3989
|
+
start: days.reduce((a, b) => a < b ? a : b),
|
|
3990
|
+
end: days.reduce((a, b) => a > b ? a : b)
|
|
3991
|
+
} : null;
|
|
3992
|
+
return knockoutWindowMemo;
|
|
3993
|
+
}
|
|
3861
3994
|
async function getBracket(adapter, opts = {}) {
|
|
3862
3995
|
const topology = loadBracketTopology();
|
|
3863
3996
|
const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
|
|
@@ -3865,7 +3998,8 @@ async function getBracket(adapter, opts = {}) {
|
|
|
3865
3998
|
let liveDegraded = true;
|
|
3866
3999
|
let source;
|
|
3867
4000
|
try {
|
|
3868
|
-
const
|
|
4001
|
+
const win = knockoutWindow();
|
|
4002
|
+
const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
|
|
3869
4003
|
matches = mergeLive(base, live);
|
|
3870
4004
|
liveDegraded = false;
|
|
3871
4005
|
source = adapter.name;
|
|
@@ -3898,8 +4032,9 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
|
|
|
3898
4032
|
let fixtures = allFixtures();
|
|
3899
4033
|
let overlayFailed = false;
|
|
3900
4034
|
try {
|
|
3901
|
-
|
|
3902
|
-
|
|
4035
|
+
const win = knockoutWindow();
|
|
4036
|
+
if (adapter.fetchWindow && win) {
|
|
4037
|
+
fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
|
|
3903
4038
|
}
|
|
3904
4039
|
} catch {
|
|
3905
4040
|
overlayFailed = true;
|
|
@@ -3922,7 +4057,8 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
|
|
|
3922
4057
|
let degraded = true;
|
|
3923
4058
|
let liveById;
|
|
3924
4059
|
try {
|
|
3925
|
-
const
|
|
4060
|
+
const win = knockoutWindow();
|
|
4061
|
+
const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
|
|
3926
4062
|
matches = mergeLive(base, live);
|
|
3927
4063
|
degraded = false;
|
|
3928
4064
|
liveById = new Set(live.map((m) => m.id));
|
|
@@ -4170,7 +4306,7 @@ var mapping_2026_default = {
|
|
|
4170
4306
|
var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
|
|
4171
4307
|
var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
|
|
4172
4308
|
var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
|
|
4173
|
-
var
|
|
4309
|
+
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
4174
4310
|
var WC_SERIES_SLUG = "soccer-fifwc";
|
|
4175
4311
|
var WC_SPORT = "fifwc";
|
|
4176
4312
|
var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
|
|
@@ -4208,7 +4344,7 @@ var PolymarketProvider = class {
|
|
|
4208
4344
|
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
4209
4345
|
const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
|
|
4210
4346
|
if (slugs.length === 0) return { checked: true };
|
|
4211
|
-
const configured = options?.timeoutMs ?? this.opts.timeoutMs ??
|
|
4347
|
+
const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
4212
4348
|
try {
|
|
4213
4349
|
for (const slug of slugs) {
|
|
4214
4350
|
const remaining = deadline - Date.now();
|
|
@@ -4228,13 +4364,20 @@ var PolymarketProvider = class {
|
|
|
4228
4364
|
const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
|
|
4229
4365
|
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
4230
4366
|
const res = await doFetch(url, {
|
|
4231
|
-
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ??
|
|
4367
|
+
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
|
|
4368
|
+
// Gamma never legitimately redirects; following one would sidestep the
|
|
4369
|
+
// host allow-list (it only validates the base URL), so reject redirects.
|
|
4370
|
+
redirect: "error",
|
|
4232
4371
|
headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
|
|
4233
4372
|
});
|
|
4234
4373
|
if (res.status === 404) return void 0;
|
|
4235
4374
|
if (!res.ok) {
|
|
4236
4375
|
throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
|
|
4237
4376
|
}
|
|
4377
|
+
const length = Number(res.headers?.get?.("content-length"));
|
|
4378
|
+
if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
|
|
4379
|
+
throw new Error(`Polymarket response too large: ${length} bytes`);
|
|
4380
|
+
}
|
|
4238
4381
|
const data = await res.json();
|
|
4239
4382
|
const event = Array.isArray(data) ? data[0] : data;
|
|
4240
4383
|
return event && typeof event === "object" ? event : void 0;
|
|
@@ -4677,22 +4820,34 @@ function matchList(matches, empty, opts = {}) {
|
|
|
4677
4820
|
}
|
|
4678
4821
|
function standingsTable(group, rows) {
|
|
4679
4822
|
const header = `Group ${group}`;
|
|
4680
|
-
const
|
|
4681
|
-
const
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4823
|
+
const line = (team, p, w, d, l, gd2, pts) => `${padVisible(team, 24)} ${p.padStart(2)} ${w.padStart(2)} ${d.padStart(2)} ${l.padStart(2)} ${gd2.padStart(3)} ${pts.padStart(3)}`;
|
|
4824
|
+
const cols = line("Team", "P", "W", "D", "L", "GD", "Pts");
|
|
4825
|
+
const lines = rows.map(
|
|
4826
|
+
(r) => line(
|
|
4827
|
+
`${r.team.flag} ${r.team.name}`,
|
|
4828
|
+
String(r.played),
|
|
4829
|
+
String(r.won),
|
|
4830
|
+
String(r.drawn),
|
|
4831
|
+
String(r.lost),
|
|
4832
|
+
r.goalDiff > 0 ? `+${r.goalDiff}` : String(r.goalDiff),
|
|
4833
|
+
String(r.points)
|
|
4834
|
+
)
|
|
4835
|
+
);
|
|
4686
4836
|
return [header, cols, ...lines].join("\n");
|
|
4687
4837
|
}
|
|
4688
|
-
function pad(n) {
|
|
4689
|
-
return `${n}`.padStart(2);
|
|
4690
|
-
}
|
|
4691
4838
|
var DISCLAIMER = "Claudinho is an independent fan project \u2014 not affiliated with or endorsed by FIFA or Anthropic.";
|
|
4692
4839
|
|
|
4693
4840
|
// src/tools.ts
|
|
4841
|
+
var adapters = /* @__PURE__ */ new Map();
|
|
4694
4842
|
function resolveAdapter(args) {
|
|
4695
|
-
|
|
4843
|
+
if (args.adapter) return args.adapter;
|
|
4844
|
+
const key = `${args.source ?? "espn"}::${resolveCompetition()}`;
|
|
4845
|
+
let adapter = adapters.get(key);
|
|
4846
|
+
if (!adapter) {
|
|
4847
|
+
adapter = makeAdapter(args.source);
|
|
4848
|
+
adapters.set(key, adapter);
|
|
4849
|
+
}
|
|
4850
|
+
return adapter;
|
|
4696
4851
|
}
|
|
4697
4852
|
function resolveMarketProvider(args) {
|
|
4698
4853
|
return args.marketProvider ?? makeMarketProvider();
|
|
@@ -4928,14 +5083,16 @@ async function toolGetNextFixture(args) {
|
|
|
4928
5083
|
const msg = degraded ? `Couldn't reach the data provider \u2014 no upcoming fixture confirmed for ${code}.` : `No upcoming fixture found for ${code}.`;
|
|
4929
5084
|
return {
|
|
4930
5085
|
text: withDisclaimer(msg, void 0, args.lang),
|
|
4931
|
-
data: { team: code, fixture: null, degraded }
|
|
5086
|
+
data: { team: code, fixture: null, degraded, source: source ?? null }
|
|
4932
5087
|
};
|
|
4933
5088
|
}
|
|
4934
5089
|
const opts = fmtOpts(args);
|
|
4935
5090
|
return {
|
|
5091
|
+
// `source` in data mirrors the text's "Live data: …" attribution (parity
|
|
5092
|
+
// with CLI `next --json`); null for a static group fixture (no live source).
|
|
4936
5093
|
text: withDisclaimer(`Next up for ${code}:
|
|
4937
5094
|
${matchLine(fixture, opts)}`, source, args.lang),
|
|
4938
|
-
data: { team: code, fixture, degraded }
|
|
5095
|
+
data: { team: code, fixture, degraded, source: source ?? null }
|
|
4939
5096
|
};
|
|
4940
5097
|
}
|
|
4941
5098
|
function toolGetTeam(args) {
|
|
@@ -5219,7 +5376,7 @@ async function toolGetShareSnippet(args) {
|
|
|
5219
5376
|
|
|
5220
5377
|
// src/server.ts
|
|
5221
5378
|
var SERVER_NAME = "claudinho";
|
|
5222
|
-
var SERVER_VERSION = "0.
|
|
5379
|
+
var SERVER_VERSION = "0.9.0";
|
|
5223
5380
|
var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
|
|
5224
5381
|
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.`;
|
|
5225
5382
|
var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
|
|
@@ -5234,7 +5391,9 @@ var teamArg = z.string().regex(/^[A-Za-z]{3}$/, "a 3-letter team code, e.g. MEX"
|
|
|
5234
5391
|
var flavorArg = z.enum(["off", "subtle", "full"]);
|
|
5235
5392
|
var commonArgs = {
|
|
5236
5393
|
tz: z.string().optional().describe("IANA timezone for kickoff times, e.g. America/Mexico_City"),
|
|
5237
|
-
lang: z.string().optional().describe(
|
|
5394
|
+
lang: z.string().optional().describe(
|
|
5395
|
+
"Locale for dates, provider attribution, and commentary: en, es, pt, fr (the summary scaffold stays English; other locales fall back to en)"
|
|
5396
|
+
),
|
|
5238
5397
|
flavor: flavorArg.optional().describe("Commentary flair: off, subtle, full (default: full)")
|
|
5239
5398
|
};
|
|
5240
5399
|
var teamRef = z.object({ code: z.string(), name: z.string(), flag: z.string() }).partial().passthrough();
|
|
@@ -5281,7 +5440,12 @@ var bracketOut = {
|
|
|
5281
5440
|
standingsDegraded: z.boolean().optional(),
|
|
5282
5441
|
source: src.optional()
|
|
5283
5442
|
};
|
|
5284
|
-
var nextOut = {
|
|
5443
|
+
var nextOut = {
|
|
5444
|
+
team: z.string(),
|
|
5445
|
+
fixture: matchOut.nullable(),
|
|
5446
|
+
degraded: z.boolean(),
|
|
5447
|
+
source: src
|
|
5448
|
+
};
|
|
5285
5449
|
var marketOut = {
|
|
5286
5450
|
matchId: z.string().nullable().optional(),
|
|
5287
5451
|
team: z.string().optional(),
|
|
@@ -5333,7 +5497,7 @@ function buildServer() {
|
|
|
5333
5497
|
"get_today",
|
|
5334
5498
|
{
|
|
5335
5499
|
title: "Today's matches",
|
|
5336
|
-
description: "All fixtures for a date (default: today), with live score and minute overlaid on any match in play. Use this for a whole day's card; for only in-play matches use get_live, for one team's match use get_next_fixture, for a single match's detail use get_match. Kickoffs render in tz; lang localizes
|
|
5500
|
+
description: "All fixtures for a date (default: today), with live score and minute overlaid on any match in play. Use this for a whole day's card; for only in-play matches use get_live, for one team's match use get_next_fixture, for a single match's detail use get_match. Kickoffs render in tz; lang localizes dates, attribution, and commentary (en/es/pt/fr); flavor sets commentary tone.",
|
|
5337
5501
|
inputSchema: {
|
|
5338
5502
|
date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
|
|
5339
5503
|
...commonArgs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudinho/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"mcpName": "io.github.arturogarrido/claudinho",
|
|
5
5
|
"description": "MCP server for the 2026 men's football tournament — live scores, fixtures, standings, read-only prediction-market signals, and paste-ready match cards. Works with Claude Code, Cursor, Codex, Windsurf, Zed. Not affiliated with FIFA or Anthropic.",
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
|
+
"!dist/*.mcpb",
|
|
29
30
|
"README.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
@@ -54,10 +55,11 @@
|
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/node": "^22.20.0",
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
57
59
|
"tsup": "^8.0.0",
|
|
58
60
|
"typescript": "^5.7.0",
|
|
59
61
|
"vitest": "^4.1.9",
|
|
60
|
-
"@claudinho/core": "0.
|
|
62
|
+
"@claudinho/core": "0.9.0"
|
|
61
63
|
},
|
|
62
64
|
"scripts": {
|
|
63
65
|
"build": "tsup",
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
"dev": "tsup --watch",
|
|
66
68
|
"test": "vitest run",
|
|
67
69
|
"typecheck": "tsc --noEmit",
|
|
68
|
-
"start": "node dist/index.js"
|
|
70
|
+
"start": "node dist/index.js",
|
|
71
|
+
"smoke:stdio": "node scripts/stdio-smoke.mjs"
|
|
69
72
|
}
|
|
70
73
|
}
|